From c65b4f31c91694388084c05598a2d40753807e3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Nov 2024 17:00:59 +0100 Subject: [PATCH] fix link deletion logic in delete_thing, fix thing_to_record logic and get_latest_thing logic, add tests --- .../zomes/coordinator/generic_zome/src/api.rs | 148 ++++-- lib/src/index.ts | 9 +- package-lock.json | 1 + tests/package.json | 1 + tests/src/generic_dna/generic_zome/common.ts | 31 +- .../generic_zome/thing-to-agents.test.ts | 99 ---- .../generic_dna/generic_zome/thing.test.ts | 437 +++++++++++++----- ui/src/elements/all-posts.ts | 7 +- 8 files changed, 443 insertions(+), 290 deletions(-) delete mode 100644 tests/src/generic_dna/generic_zome/thing-to-agents.test.ts diff --git a/dnas/generic_dna/zomes/coordinator/generic_zome/src/api.rs b/dnas/generic_dna/zomes/coordinator/generic_zome/src/api.rs index 08f0ba1..f3608b6 100644 --- a/dnas/generic_dna/zomes/coordinator/generic_zome/src/api.rs +++ b/dnas/generic_dna/zomes/coordinator/generic_zome/src/api.rs @@ -1,6 +1,5 @@ use crate::{derive_link_tag, NodeLink, NodeLinkMeta, Signal, SignalKind, Thing}; use generic_zome_integrity::*; -use hdi::prelude::holochain_integrity_types::action; use hdk::prelude::*; #[derive(Serialize, Deserialize, Clone, Debug)] @@ -103,19 +102,25 @@ pub fn create_thing(input: CreateThingInput) -> ExternResult { /// Gets the latest known version of a Thing #[hdk_extern] pub fn get_latest_thing(thing_id: ActionHash) -> ExternResult> { - let links = get_links( - GetLinksInputBuilder::try_new(thing_id.clone(), LinkTypes::ThingUpdates)?.build(), - )?; - let thing_record = get_latest_thing_from_links(links)?; - match thing_record { - Some(r) => Ok(Some(thing_record_to_thing(r)?)), - None => { - let maybe_original_record = get(thing_id, GetOptions::default())?; - match maybe_original_record { - Some(r) => Ok(Some(thing_record_to_thing(r)?)), - None => Ok(None), + let original_thing = get_original_thing(thing_id.clone())?; + match original_thing { + Some(thing) => { + let links = get_links( + GetLinksInputBuilder::try_new(thing_id.clone(), LinkTypes::ThingUpdates)?.build(), + )?; + let thing_record = get_latest_thing_from_links(links)?; + match thing_record { + Some(r) => Ok(Some(thing_record_to_thing(r, thing)?)), + None => { + let maybe_original_record = get(thing_id, GetOptions::default())?; + match maybe_original_record { + Some(r) => Ok(Some(thing_record_to_thing(r, thing)?)), + None => Ok(None), + } + } } } + None => Ok(None), } } @@ -149,7 +154,7 @@ pub fn get_original_thing(original_thing_id: ActionHash) -> ExternResult { - let thing = thing_record_to_thing(record)?; + let thing = original_thing_record_to_thing(record)?; Ok(Some(thing)) } None => Ok(None), @@ -158,9 +163,9 @@ pub fn get_original_thing(original_thing_id: ActionHash) -> ExternResult ExternResult> { - let Some(original_record) = get(thing_id.clone(), GetOptions::default())? else { + let Some(original_thing) = get_original_thing(thing_id.clone())? else { return Err(wasm_error!(WasmErrorInner::Guest( - "No original record found for this thing_id (action hash).".into() + "No original Thing found for this thing_id (action hash).".into() ))); }; let links = get_links( @@ -181,13 +186,14 @@ pub fn get_all_revisions_for_thing(thing_id: ActionHash) -> ExternResult>>()?; let records = HDK.with(|hdk| hdk.borrow().get(get_input))?; - let mut records: Vec = records.into_iter().flatten().collect(); - records.insert(0, original_record); - Ok(records + let records: Vec = records.into_iter().flatten().collect(); + let mut things = records .into_iter() - .map(|r| thing_record_to_thing(r).ok()) + .map(|r| thing_record_to_thing(r, original_thing.clone()).ok()) .filter_map(|t| t) - .collect()) + .collect::>(); + things.insert(0, original_thing); + Ok(things) } #[derive(Serialize, Deserialize, Debug)] @@ -198,13 +204,17 @@ pub struct UpdateThingInput { #[hdk_extern] pub fn update_thing(input: UpdateThingInput) -> ExternResult { - let updated_thing_hash = create_entry(&EntryTypes::Thing(ThingEntry { - content: input.updated_content.clone(), - }))?; + let original_thing_record = + get(input.thing_id.clone(), GetOptions::default())?.ok_or(wasm_error!( + WasmErrorInner::Guest("Failed to get record of original Thing.".into()) + ))?; - let thing_record = get(input.thing_id.clone(), GetOptions::default())?.ok_or(wasm_error!( - WasmErrorInner::Guest("Failed to get record of original Thing.".into()) - ))?; + let updated_thing_hash = update_entry( + input.thing_id.clone(), + &EntryTypes::Thing(ThingEntry { + content: input.updated_content.clone(), + }), + )?; let updated_thing_record = get(updated_thing_hash.clone(), GetOptions::default())?.ok_or( wasm_error!(WasmErrorInner::Guest( @@ -222,8 +232,8 @@ pub fn update_thing(input: UpdateThingInput) -> ExternResult { let thing = Thing { id: input.thing_id, content: input.updated_content, - creator: thing_record.action().author().clone(), - created_at: thing_record.action().timestamp(), + creator: original_thing_record.action().author().clone(), + created_at: original_thing_record.action().timestamp(), updated_at: Some(updated_thing_record.action().timestamp()), }; @@ -263,7 +273,7 @@ pub fn delete_thing(input: DeleteThingInput) -> ExternResult<()> { // not retreivable without the original Thing entry) delete_entry(input.thing_id.clone())?; - // 2. Delete all backlinks from bidirectional links + // 2. Delete all backlinks from bidirectional links. We do NOT delete links pointing away from it. if input.delete_backlinks { let links_to_agents = get_links( GetLinksInputBuilder::try_new(input.thing_id.clone(), LinkTypes::ToAgent)?.build(), @@ -559,23 +569,57 @@ pub fn delete_links_from_node(input: CreateOrDeleteLinksInput) -> ExternResult<( } fn delete_links_from_node_inner(input: CreateOrDeleteLinksInput) -> ExternResult> { - let base = linkable_hash_from_node_id(input.src.clone())?; - let mut links_deleted: Vec = Vec::new(); - let anchor_link_inputs = input + // Discern between "From" links and "To" or "Bidirectional" links + let from_links = input + .links + .clone() + .into_iter() + .filter_map(|l| match l.direction { + LinkDirection::From => Some(l), + _ => None, + }) + .collect::>(); + + for link_input in from_links { + let base = linkable_hash_from_node_id(link_input.node_id)?; + let link_type = match input.src { + NodeId::Agent(_) => LinkTypes::ToAgent, + NodeId::Anchor(_) => LinkTypes::ToAnchor, + NodeId::Thing(_) => LinkTypes::ToThing, + }; + let links_to_base = + get_links(GetLinksInputBuilder::try_new(base.clone(), link_type)?.build())?; + for link in links_to_base { + if link.target == base { + delete_link(link.create_link_hash)?; + } + } + } + + let to_or_bidirectional_links = input .links + .clone() + .into_iter() + .filter_map(|l| match l.direction { + LinkDirection::From => None, + _ => Some(l), + }) + .collect::>(); + + // Delete "To" and "Bidirectional" links + let anchor_link_inputs = to_or_bidirectional_links .clone() .into_iter() .map(|l| match l.node_id { - NodeId::Agent(_) => Some(l), + NodeId::Anchor(_) => Some(l), _ => None, }) .filter_map(|l| l) .collect::>(); - let agent_link_inputs = input - .links + let agent_link_inputs = to_or_bidirectional_links .clone() .into_iter() .map(|l| match l.node_id { @@ -585,17 +629,18 @@ fn delete_links_from_node_inner(input: CreateOrDeleteLinksInput) -> ExternResult .filter_map(|l| l) .collect::>(); - let thing_link_inputs = input - .links + let thing_link_inputs = to_or_bidirectional_links .clone() .into_iter() .map(|l| match l.node_id { - NodeId::Agent(_) => Some(l), + NodeId::Thing(_) => Some(l), _ => None, }) .filter_map(|l| l) .collect::>(); + let base = linkable_hash_from_node_id(input.src.clone())?; + if anchor_link_inputs.len() > 0 { for link_input in anchor_link_inputs { let links_to_anchors = get_links( @@ -956,7 +1001,32 @@ fn linkable_hash_from_node_id(node_id: NodeId) -> ExternResult } } -fn thing_record_to_thing(record: Record) -> ExternResult { +fn thing_record_to_thing(record: Record, original_thing: Thing) -> ExternResult { + let thing_entry = record + .entry() + .to_app_option::() + .map_err(|e| { + wasm_error!(WasmErrorInner::Guest( + format!("Failed to deserialize Record at the given action hash (thing_id) to a ThingEntry: {e}") + )) + })? + .ok_or(wasm_error!(WasmErrorInner::Guest( + "No Thing associated to this thing id (AcionHash).".into() + )))?; + let updated_at = match record.action_address() == &original_thing.id { + true => None, + false => Some(record.action().timestamp()), + }; + Ok(Thing { + id: record.action_address().clone(), + content: thing_entry.content, + creator: original_thing.creator, + created_at: original_thing.created_at, + updated_at, + }) +} + +fn original_thing_record_to_thing(record: Record) -> ExternResult { let thing_entry = record .entry() .to_app_option::() diff --git a/lib/src/index.ts b/lib/src/index.ts index 6f64d2a..d20b97a 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -30,9 +30,9 @@ import { Thing, ThingId, UpdateThingInput, -} from "./types"; +} from "./types.js"; -export * from "./types"; +export * from "./types.js"; import { derived, @@ -129,11 +129,12 @@ export class NodeStore { // If it is already complete, we assume that the Thing arrived through emit_signal // otherwise we set it to "error" if (currentThing.status !== "complete") { + console.log("currentThing.status: ", currentThing.status); this.nodeStore.set({ status: "error", error: `Failed to get Thing record for thing with id ${encodeHashToBase64( this.nodeId.id - )}`, + )}${currentThing.status === "error" ? currentThing.error : ''}`, }); } return; @@ -827,7 +828,7 @@ export class SimpleHolochain { } } -function linkInputToRustFormat(linkInput: LinkInput): LinkInputRust { +export function linkInputToRustFormat(linkInput: LinkInput): LinkInputRust { let linkDirection: LinkDirectionRust; switch (linkInput.direction) { case LinkDirection.From: diff --git a/package-lock.json b/package-lock.json index d162f95..323770e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8004,6 +8004,7 @@ "version": "0.1.0", "dependencies": { "@holochain/client": "^0.19.0-dev.0", + "@holochain/simple-holochain": "*", "@holochain/tryorama": "^0.18.0-dev.0", "@msgpack/msgpack": "^2.8.0", "typescript": "^4.9.4", diff --git a/tests/package.json b/tests/package.json index 9f170df..06d737b 100644 --- a/tests/package.json +++ b/tests/package.json @@ -9,6 +9,7 @@ "@msgpack/msgpack": "^2.8.0", "@holochain/client": "^0.19.0-dev.0", "@holochain/tryorama": "^0.18.0-dev.0", + "@holochain/simple-holochain": "*", "typescript": "^4.9.4", "vitest": "^0.28.4" }, diff --git a/tests/src/generic_dna/generic_zome/common.ts b/tests/src/generic_dna/generic_zome/common.ts index 8b3edc1..3689ab9 100644 --- a/tests/src/generic_dna/generic_zome/common.ts +++ b/tests/src/generic_dna/generic_zome/common.ts @@ -1,29 +1,6 @@ -import { - ActionHash, - AppBundleSource, - fakeActionHash, - fakeAgentPubKey, - fakeDnaHash, - fakeEntryHash, - hashFrom32AndType, - NewEntryAction, - Record, -} from "@holochain/client"; -import { CallableCell } from "@holochain/tryorama"; +import { CallableCell, Player } from "@holochain/tryorama"; -export async function sampleThing(cell: CallableCell, partialThing = {}) { - return { - ...{ - content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", - }, - ...partialThing, - }; -} - -export async function createThing(cell: CallableCell, thing = undefined): Promise { - return cell.callZome({ - zome_name: "generic_zome", - fn_name: "create_thing", - payload: thing || await sampleThing(cell), - }); +export function getCellByRoleName(player: Player, roleName: string): CallableCell { + const cells = player.cells; + return cells.find((cell) => cell.name === roleName); } diff --git a/tests/src/generic_dna/generic_zome/thing-to-agents.test.ts b/tests/src/generic_dna/generic_zome/thing-to-agents.test.ts deleted file mode 100644 index 64c02bd..0000000 --- a/tests/src/generic_dna/generic_zome/thing-to-agents.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { assert, test } from "vitest"; - -import { - ActionHash, - AppBundleSource, - CreateLink, - DeleteLink, - fakeActionHash, - fakeAgentPubKey, - fakeEntryHash, - hashFrom32AndType, - Link, - NewEntryAction, - Record, - SignedActionHashed, -} from "@holochain/client"; -import { CallableCell, dhtSync, runScenario } from "@holochain/tryorama"; -import { decode } from "@msgpack/msgpack"; - -import { createThing } from "./common.js"; - -test("link a Thing to a Agent", async () => { - await runScenario(async scenario => { - // Construct proper paths for your app. - // This assumes app bundle created by the `hc app pack` command. - const testAppPath = process.cwd() + "/../workdir/generic-dna.happ"; - - // Set up the app to be installed - const appSource = { appBundleSource: { path: testAppPath } }; - - // Add 2 players with the test app to the Scenario. The returned players - // can be destructured. - const [alice, bob] = await scenario.addPlayersWithApps([appSource, appSource]); - - // Shortcut peer discovery through gossip and register all agents in every - // conductor of the scenario. - await scenario.shareAllAgents(); - - const baseRecord = await createThing(alice.cells[0]); - const baseAddress = baseRecord.signed_action.hashed.hash; - const targetAddress = alice.agentPubKey; - - // Bob gets the links, should be empty - let linksOutput: Link[] = await bob.cells[0].callZome({ - zome_name: "generic_zome", - fn_name: "get_agents_for_thing", - payload: baseAddress, - }); - assert.equal(linksOutput.length, 0); - - // Alice creates a link from Thing to Agent - await alice.cells[0].callZome({ - zome_name: "generic_zome", - fn_name: "add_agent_for_thing", - payload: { - base_thing_hash: baseAddress, - target_agent: targetAddress, - }, - }); - - await dhtSync([alice, bob], alice.cells[0].cell_id[0]); - - // Bob gets the links again - linksOutput = await bob.cells[0].callZome({ - zome_name: "generic_zome", - fn_name: "get_agents_for_thing", - payload: baseAddress, - }); - assert.equal(linksOutput.length, 1); - - await alice.cells[0].callZome({ - zome_name: "generic_zome", - fn_name: "delete_agent_for_thing", - payload: { - base_thing_hash: baseAddress, - target_agent: targetAddress, - }, - }); - - await dhtSync([alice, bob], alice.cells[0].cell_id[0]); - - // Bob gets the links again - linksOutput = await bob.cells[0].callZome({ - zome_name: "generic_zome", - fn_name: "get_agents_for_thing", - payload: baseAddress, - }); - assert.equal(linksOutput.length, 0); - - // Bob gets the deleted links - let deletedLinksOutput: Array<[SignedActionHashed, SignedActionHashed[]]> = await bob - .cells[0].callZome({ - zome_name: "generic_zome", - fn_name: "get_deleted_agents_for_thing", - payload: baseAddress, - }); - assert.equal(deletedLinksOutput.length, 1); - }); -}); diff --git a/tests/src/generic_dna/generic_zome/thing.test.ts b/tests/src/generic_dna/generic_zome/thing.test.ts index be6a96b..5ff0d19 100644 --- a/tests/src/generic_dna/generic_zome/thing.test.ts +++ b/tests/src/generic_dna/generic_zome/thing.test.ts @@ -2,24 +2,28 @@ import { assert, test } from "vitest"; import { ActionHash, - AppBundleSource, - CreateLink, - DeleteLink, - fakeActionHash, - fakeAgentPubKey, - fakeEntryHash, - Link, - NewEntryAction, - Record, - SignedActionHashed, + AgentPubKey, + encodeHashToBase64, } from "@holochain/client"; -import { CallableCell, dhtSync, runScenario } from "@holochain/tryorama"; -import { decode } from "@msgpack/msgpack"; +import { dhtSync, runScenario } from "@holochain/tryorama"; +import { decode, encode } from "@msgpack/msgpack"; -import { createThing, sampleThing } from "./common.js"; - -test("create Thing", async () => { - await runScenario(async scenario => { +import { getCellByRoleName } from "./common.js"; +import { + CreateThingInput, + DeleteThingInput, + LinkDirection, + LinkInput, + linkInputToRustFormat, + LinkTagContent, + NodeContent, + NodeId, + Thing, + UpdateThingInput, +} from "@holochain/simple-holochain"; + +test("Create Thing and update it twice", async () => { + await runScenario(async (scenario) => { // Construct proper paths for your app. // This assumes app bundle created by the `hc app pack` command. const testAppPath = process.cwd() + "/../workdir/generic-dna.happ"; @@ -29,56 +33,108 @@ test("create Thing", async () => { // Add 2 players with the test app to the Scenario. The returned players // can be destructured. - const [alice, bob] = await scenario.addPlayersWithApps([appSource, appSource]); + const [alice, bob] = await scenario.addPlayersWithApps([ + appSource, + appSource, + ]); // Shortcut peer discovery through gossip and register all agents in every // conductor of the scenario. await scenario.shareAllAgents(); - // Alice creates a Thing - const record: Record = await createThing(alice.cells[0]); - assert.ok(record); - }); -}); + const aliceCell = getCellByRoleName(alice, "generic_dna"); + const bobCell = getCellByRoleName(bob, "generic_dna"); -test("create and read Thing", async () => { - await runScenario(async scenario => { - // Construct proper paths for your app. - // This assumes app bundle created by the `hc app pack` command. - const testAppPath = process.cwd() + "/../workdir/generic-dna.happ"; + // Alice creates a Thing + const thingInput: CreateThingInput = { + content: "hello", + }; + const thing: Thing = await aliceCell.callZome({ + zome_name: "generic_zome", + fn_name: "create_thing", + payload: thingInput, + }); - // Set up the app to be installed - const appSource = { appBundleSource: { path: testAppPath } }; + assert.equal( + encodeHashToBase64(thing.creator), + encodeHashToBase64(aliceCell.cell_id[1]) + ); + assert.equal(thing.content, thingInput.content); - // Add 2 players with the test app to the Scenario. The returned players - // can be destructured. - const [alice, bob] = await scenario.addPlayersWithApps([appSource, appSource]); + // Bob gets the thing + await dhtSync([alice, bob], aliceCell.cell_id[0]); + const maybeThing: Thing | undefined = await bobCell.callZome({ + zome_name: "generic_zome", + fn_name: "get_latest_thing", + payload: thing.id, + }); - // Shortcut peer discovery through gossip and register all agents in every - // conductor of the scenario. - await scenario.shareAllAgents(); + assert.equal( + encodeHashToBase64(maybeThing.creator), + encodeHashToBase64(aliceCell.cell_id[1]) + ); + assert.equal(maybeThing.content, thingInput.content); - const sample = await sampleThing(alice.cells[0]); + // Bob updates the thing + const updateThingInput: UpdateThingInput = { + thing_id: thing.id, + updated_content: "good bye", + }; + const _updatedThing: Thing = await bobCell.callZome({ + zome_name: "generic_zome", + fn_name: "update_thing", + payload: updateThingInput, + }); - // Alice creates a Thing - const record: Record = await createThing(alice.cells[0], sample); - assert.ok(record); + // Alice reads the updated thing + await dhtSync([alice, bob], aliceCell.cell_id[0]); + const maybeUpdatedThing: Thing | undefined = await aliceCell.callZome({ + zome_name: "generic_zome", + fn_name: "get_latest_thing", + payload: thing.id, + }); - // Wait for the created entry to be propagated to the other node. - await dhtSync([alice, bob], alice.cells[0].cell_id[0]); + assert.equal( + encodeHashToBase64(maybeUpdatedThing.creator), + encodeHashToBase64(aliceCell.cell_id[1]) + ); + assert.equal(maybeUpdatedThing.content, updateThingInput.updated_content); + assert.equal(maybeUpdatedThing.created_at, maybeThing.created_at); + assert.equal(maybeUpdatedThing.created_at, thing.created_at); + assert(!!maybeUpdatedThing.updated_at); + + // Bob updates the thing again + const updateThingInput2: UpdateThingInput = { + thing_id: thing.id, + updated_content: "good bye, but now for real!", + }; + const _updatedThing2: Thing = await bobCell.callZome({ + zome_name: "generic_zome", + fn_name: "update_thing", + payload: updateThingInput2, + }); - // Bob gets the created Thing - const createReadOutput: Record = await bob.cells[0].callZome({ + // Alice reads the again updated thing + await dhtSync([alice, bob], aliceCell.cell_id[0]); + const maybeUpdatedThing2: Thing | undefined = await aliceCell.callZome({ zome_name: "generic_zome", - fn_name: "get_original_thing", - payload: record.signed_action.hashed.hash, + fn_name: "get_latest_thing", + payload: thing.id, }); - assert.deepEqual(sample, decode((createReadOutput.entry as any).Present.entry) as any); + + assert.equal( + encodeHashToBase64(maybeUpdatedThing2.creator), + encodeHashToBase64(aliceCell.cell_id[1]) + ); + assert.equal(maybeUpdatedThing2.content, updateThingInput2.updated_content); + assert.equal(maybeUpdatedThing2.created_at, maybeThing.created_at); + assert.equal(maybeUpdatedThing2.created_at, thing.created_at); + assert(!!maybeUpdatedThing2.updated_at); }); }); -test("create and update Thing", async () => { - await runScenario(async scenario => { +test("Create Thing and a bidirectional link from the creator, then delete it and try to retrieve it", async () => { + await runScenario(async (scenario) => { // Construct proper paths for your app. // This assumes app bundle created by the `hc app pack` command. const testAppPath = process.cwd() + "/../workdir/generic-dna.happ"; @@ -88,83 +144,159 @@ test("create and update Thing", async () => { // Add 2 players with the test app to the Scenario. The returned players // can be destructured. - const [alice, bob] = await scenario.addPlayersWithApps([appSource, appSource]); + const [alice, bob] = await scenario.addPlayersWithApps([ + appSource, + appSource, + ]); // Shortcut peer discovery through gossip and register all agents in every // conductor of the scenario. await scenario.shareAllAgents(); - // Alice creates a Thing - const record: Record = await createThing(alice.cells[0]); - assert.ok(record); - - const originalActionHash = record.signed_action.hashed.hash; + const aliceCell = getCellByRoleName(alice, "generic_dna"); + const bobCell = getCellByRoleName(bob, "generic_dna"); - // Alice updates the Thing - let contentUpdate: any = await sampleThing(alice.cells[0]); - let updateInput = { - original_thing_hash: originalActionHash, - previous_thing_hash: originalActionHash, - updated_thing: contentUpdate, + // Alice creates a Thing and a bidirectional link to her agent anchor + const aliceAgentAnchor: NodeId = { + type: "Agent", + id: aliceCell.cell_id[1], }; + let linkInput: LinkInput = { + direction: LinkDirection.Bidirectional, + node_id: aliceAgentAnchor, + tag: encode("tag content"), + }; + const thingInput: CreateThingInput = { + content: "hello", + links: [linkInputToRustFormat(linkInput)], + }; + const thing: Thing = await aliceCell.callZome({ + zome_name: "generic_zome", + fn_name: "create_thing", + payload: thingInput, + }); + + // - Bob checks all links that should have been created as a consequence + await dhtSync([alice, bob], aliceCell.cell_id[0]); - let updatedRecord: Record = await alice.cells[0].callZome({ + // Get the links pointing away from the thing node + const thingNode: NodeId = { type: "Thing", id: thing.id }; + const linkedAgents: [AgentPubKey, LinkTagContent][] = await bobCell.callZome({ zome_name: "generic_zome", - fn_name: "update_thing", - payload: updateInput, + fn_name: "get_linked_agents", + payload: thingNode, + }); + assert(linkedAgents.length === 1); + assert.equal(encodeHashToBase64(aliceCell.cell_id[1]), encodeHashToBase64(linkedAgents[0][0])); + // A backlink action hash should exist pointing to the backlink from the agent anchor to the thing + assert(!!linkedAgents[0][1].backlink_action_hash); + assert.deepEqual(linkedAgents[0][1].target_node_id, aliceAgentAnchor); + assert.deepEqual(decode(linkedAgents[0][1].tag), decode(linkInput.tag)); + assert.isNull(linkedAgents[0][1].thing_created_at); + + const linkedNodesFromThing: NodeContent[] = await bobCell.callZome({ + zome_name: "generic_zome", + fn_name: "get_all_linked_nodes", + payload: thingNode, }); - assert.ok(updatedRecord); + assert(linkedNodesFromThing.length === 1); + assert.deepEqual(linkedNodesFromThing[0], { type: "Agent", content: aliceCell.cell_id[1]}); - // Wait for the updated entry to be propagated to the other node. - await dhtSync([alice, bob], alice.cells[0].cell_id[0]); - // Bob gets the updated Thing - const readUpdatedOutput0: Record = await bob.cells[0].callZome({ + // Get the links pointing towards the thing node from the agent anchor + const linkedNodes: NodeContent[] = await bobCell.callZome({ zome_name: "generic_zome", - fn_name: "get_latest_thing", - payload: updatedRecord.signed_action.hashed.hash, + fn_name: "get_all_linked_nodes", + payload: aliceAgentAnchor, }); - assert.deepEqual(contentUpdate, decode((readUpdatedOutput0.entry as any).Present.entry) as any); - // Alice updates the Thing again - contentUpdate = await sampleThing(alice.cells[0]); - updateInput = { - original_thing_hash: originalActionHash, - previous_thing_hash: updatedRecord.signed_action.hashed.hash, - updated_thing: contentUpdate, + assert(linkedNodes.length === 1); + const expectedNodeContent: NodeContent = { + type: "Thing", + content: thing, }; + assert.deepEqual(linkedNodes[0], expectedNodeContent); - updatedRecord = await alice.cells[0].callZome({ + const linkedThings: Thing[] = await bobCell.callZome({ zome_name: "generic_zome", - fn_name: "update_thing", - payload: updateInput, + fn_name: "get_linked_things", + payload: aliceAgentAnchor, + }); + assert(linkedThings.length === 1); + assert.deepEqual(linkedThings[0], thing); + + const linkedThingIds: [ActionHash, LinkTagContent][] = await bobCell.callZome({ + zome_name: "generic_zome", + fn_name: "get_linked_thing_ids", + payload: aliceAgentAnchor, + }); + assert(linkedThingIds.length === 1); + assert.equal(encodeHashToBase64(linkedThingIds[0][0]) ,encodeHashToBase64(thing.id)); + assert(!linkedThingIds[0][1].backlink_action_hash); + assert.equal(linkedThingIds[0][1].thing_created_at , thing.created_at); + assert.deepEqual(linkedThingIds[0][1].target_node_id , thingNode); + assert.equal(decode(linkedThingIds[0][1].tag), decode(linkInput.tag)); + + + // - Alice deletes the thing and the link to her agent anchor should disappear + // since delete_links_from_creator is set to true + const deleteThingInput: DeleteThingInput = { + thing_id: thing.id, + delete_backlinks: true, + delete_links_from_creator: true, + }; + await aliceCell.callZome({ + zome_name: "generic_zome", + fn_name: "delete_thing", + payload: deleteThingInput, }); - assert.ok(updatedRecord); - // Wait for the updated entry to be propagated to the other node. - await dhtSync([alice, bob], alice.cells[0].cell_id[0]); + // Bob tries to get the linked nodes again + await dhtSync([alice, bob], aliceCell.cell_id[0]); - // Bob gets the updated Thing - const readUpdatedOutput1: Record = await bob.cells[0].callZome({ + // Get the links pointing away from the thing node. They should still be the same, + // only links poitning towards it are deleted + const linkedAgents2: [AgentPubKey, LinkTagContent][] = await bobCell.callZome({ zome_name: "generic_zome", - fn_name: "get_latest_thing", - payload: updatedRecord.signed_action.hashed.hash, + fn_name: "get_linked_agents", + payload: thingNode, + }); + assert(linkedAgents2.length === 1); + + const linkedNodesFromThing2: NodeContent[] = await bobCell.callZome({ + zome_name: "generic_zome", + fn_name: "get_all_linked_nodes", + payload: thingNode, + }); + assert(linkedNodesFromThing2.length === 1); + + // Get the links pointing towards the thing node from the agent anchor. + // They should have been deleted now. + const linkedNodes2: NodeContent[] = await bobCell.callZome({ + zome_name: "generic_zome", + fn_name: "get_all_linked_nodes", + payload: aliceAgentAnchor, + }); + assert(linkedNodes2.length === 0); + + const linkedThings2: Thing[] = await bobCell.callZome({ + zome_name: "generic_zome", + fn_name: "get_linked_things", + payload: aliceAgentAnchor, }); - assert.deepEqual(contentUpdate, decode((readUpdatedOutput1.entry as any).Present.entry) as any); + assert(linkedThings2.length === 0); - // Bob gets all the revisions for Thing - const revisions: Record[] = await bob.cells[0].callZome({ + const linkedThingIds2: [ActionHash, LinkTagContent][] = await bobCell.callZome({ zome_name: "generic_zome", - fn_name: "get_all_revisions_for_thing", - payload: originalActionHash, + fn_name: "get_linked_thing_ids", + payload: aliceAgentAnchor, }); - assert.equal(revisions.length, 3); - assert.deepEqual(contentUpdate, decode((revisions[2].entry as any).Present.entry) as any); + assert(linkedThingIds2.length === 0); }); }); -test("create and delete Thing", async () => { - await runScenario(async scenario => { +test("Create Thing and an anchor, then delete the thing and the anchor link", async () => { + await runScenario(async (scenario) => { // Construct proper paths for your app. // This assumes app bundle created by the `hc app pack` command. const testAppPath = process.cwd() + "/../workdir/generic-dna.happ"; @@ -174,45 +306,114 @@ test("create and delete Thing", async () => { // Add 2 players with the test app to the Scenario. The returned players // can be destructured. - const [alice, bob] = await scenario.addPlayersWithApps([appSource, appSource]); + const [alice, bob] = await scenario.addPlayersWithApps([ + appSource, + appSource, + ]); // Shortcut peer discovery through gossip and register all agents in every // conductor of the scenario. await scenario.shareAllAgents(); - const sample = await sampleThing(alice.cells[0]); + const aliceCell = getCellByRoleName(alice, "generic_dna"); + const bobCell = getCellByRoleName(bob, "generic_dna"); - // Alice creates a Thing - const record: Record = await createThing(alice.cells[0], sample); - assert.ok(record); + // Alice creates a Thing and a bidirectional link to her agent anchor + const allThingsAnchor: NodeId = { + type: "Anchor", + id: "ALL_THINGS", + }; + let linkInput: LinkInput = { + direction: LinkDirection.From, + node_id: allThingsAnchor, + }; + const thingInput: CreateThingInput = { + content: "hello", + links: [linkInputToRustFormat(linkInput)], + }; + const thing: Thing = await aliceCell.callZome({ + zome_name: "generic_zome", + fn_name: "create_thing", + payload: thingInput, + }); - await dhtSync([alice, bob], alice.cells[0].cell_id[0]); + const thingNode: NodeId = { type: "Thing", id: thing.id }; - // Alice deletes the Thing - const deleteActionHash = await alice.cells[0].callZome({ + // - Bob tries to get the thing from the anchor + await dhtSync([alice, bob], aliceCell.cell_id[0]); + + // Get the links pointing towards the thing node from the ALL_THINGS anchor + const linkedNodes: NodeContent[] = await bobCell.callZome({ + zome_name: "generic_zome", + fn_name: "get_all_linked_nodes", + payload: allThingsAnchor, + }); + + assert(linkedNodes.length === 1); + const expectedNodeContent: NodeContent = { + type: "Thing", + content: thing, + }; + assert.deepEqual(linkedNodes[0], expectedNodeContent); + + const linkedThings: Thing[] = await bobCell.callZome({ + zome_name: "generic_zome", + fn_name: "get_linked_things", + payload: allThingsAnchor, + }); + assert(linkedThings.length === 1); + assert.deepEqual(linkedThings[0], thing); + + const linkedThingIds: [ActionHash, LinkTagContent][] = await bobCell.callZome({ + zome_name: "generic_zome", + fn_name: "get_linked_thing_ids", + payload: allThingsAnchor, + }); + assert(linkedThingIds.length === 1); + assert.equal(encodeHashToBase64(linkedThingIds[0][0]) ,encodeHashToBase64(thing.id)); + assert(!linkedThingIds[0][1].backlink_action_hash); + assert.equal(linkedThingIds[0][1].thing_created_at , thing.created_at); + assert.deepEqual(linkedThingIds[0][1].target_node_id , thingNode); + assert.isNull(linkedThingIds[0][1].tag); + + + // - Alice deletes the thing and the link to the ALL_THINGS anchor should disappear + const deleteThingInput: DeleteThingInput = { + thing_id: thing.id, + delete_backlinks: false, + delete_links_from_creator: false, + delete_links: [linkInputToRustFormat(linkInput)], + }; + await aliceCell.callZome({ zome_name: "generic_zome", fn_name: "delete_thing", - payload: record.signed_action.hashed.hash, + payload: deleteThingInput, }); - assert.ok(deleteActionHash); - // Wait for the entry deletion to be propagated to the other node. - await dhtSync([alice, bob], alice.cells[0].cell_id[0]); + // Bob tries to get the linked nodes again and they should all be zero now + await dhtSync([alice, bob], aliceCell.cell_id[0]); + + // Get the links pointing towards the thing node from the ALL_THINGS anchor. + // They should have been deleted now. + const linkedNodes2: NodeContent[] = await bobCell.callZome({ + zome_name: "generic_zome", + fn_name: "get_all_linked_nodes", + payload: allThingsAnchor, + }); + assert(linkedNodes2.length === 0); - // Bob gets the oldest delete for the Thing - const oldestDeleteForThing: SignedActionHashed = await bob.cells[0].callZome({ + const linkedThings2: Thing[] = await bobCell.callZome({ zome_name: "generic_zome", - fn_name: "get_oldest_delete_for_thing", - payload: record.signed_action.hashed.hash, + fn_name: "get_linked_things", + payload: allThingsAnchor, }); - assert.ok(oldestDeleteForThing); + assert(linkedThings2.length === 0); - // Bob gets the deletions for the Thing - const deletesForThing: SignedActionHashed[] = await bob.cells[0].callZome({ + const linkedThingIds2: [ActionHash, LinkTagContent][] = await bobCell.callZome({ zome_name: "generic_zome", - fn_name: "get_all_deletes_for_thing", - payload: record.signed_action.hashed.hash, + fn_name: "get_linked_thing_ids", + payload: allThingsAnchor, }); - assert.equal(deletesForThing.length, 1); + assert(linkedThingIds2.length === 0); }); }); diff --git a/ui/src/elements/all-posts.ts b/ui/src/elements/all-posts.ts index 3d1c0c1..119a55e 100644 --- a/ui/src/elements/all-posts.ts +++ b/ui/src/elements/all-posts.ts @@ -9,6 +9,7 @@ import { simpleHolochainContext } from '../contexts'; import { AsyncStatus, NodeId, + NodeIdAndMetaTag, NodeStoreContent, SimpleHolochain, } from '@holochain/simple-holochain'; @@ -41,11 +42,11 @@ export class AllPosts extends LitElement { if (this.nodeStoreUnsubscriber) this.nodeStoreUnsubscriber(); } - renderNodes(nodeIds: NodeId[]) { - const thingNodes = nodeIds.filter(nodeId => nodeId.type === 'Thing'); + renderNodes(nodeIdAndMetas: NodeIdAndMetaTag[]) { + const thingNodes = nodeIdAndMetas.filter(idAndMeta => idAndMeta.node_id.type === 'Thing'); console.log('Rendering thingNodes: ', thingNodes); return thingNodes.map( - node => html` ` + idAndMeta => html` ` ); }