diff --git a/apps/roam/src/utils/importSharedRelations.ts b/apps/roam/src/utils/importSharedRelations.ts index adb7aa5cd..d2fe2d435 100644 --- a/apps/roam/src/utils/importSharedRelations.ts +++ b/apps/roam/src/utils/importSharedRelations.ts @@ -180,7 +180,7 @@ const localSpaceUrl = canonicalRoamUrl(window.roamAlphaAPI.graph.name); const findTargetUid = async ( localOrRid: string, spaceUri: string, - ridType?: string, + ridType: string, ): Promise => { if (isRid(localOrRid)) { const { spaceUri, sourceLocalId } = ridToSpaceUriAndLocalId(localOrRid); @@ -193,11 +193,7 @@ const findTargetUid = async ( return sourceLocalId; } } else { - localOrRid = spaceUriAndLocalIdToRid( - spaceUri, - localOrRid, - ridType ?? "note", - ); + localOrRid = spaceUriAndLocalIdToRid(spaceUri, localOrRid, ridType); } return await findImportedNodeUidBySourceRid(localOrRid); }; @@ -216,10 +212,10 @@ const importRelations = async ( const relationBlockUid = schemaRidToLocalId[schemaRid]; if (relationBlockUid === undefined) throw new Error(`Missing relation type: ${relationType}`); - const sourceUid = await findTargetUid(source, spaceUri); + const sourceUid = await findTargetUid(source, spaceUri, "note"); if (sourceUid === null) throw new Error(`Missing relation source: ${source}`); - const destinationUid = await findTargetUid(destination, spaceUri); + const destinationUid = await findTargetUid(destination, spaceUri, "note"); if (destinationUid === null) throw new Error(`Missing relation destination: ${destination}`); if (existing.has(sourceNodeRid)) { diff --git a/packages/database/src/lib/__tests__/dbToCrossAppConverters.test.ts b/packages/database/src/lib/__tests__/dbToCrossAppConverters.test.ts index 1251cb5e2..4f41665f9 100644 --- a/packages/database/src/lib/__tests__/dbToCrossAppConverters.test.ts +++ b/packages/database/src/lib/__tests__/dbToCrossAppConverters.test.ts @@ -1,11 +1,13 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { dbNodeSchemaToCrossApp, dbRelationTypeSchemaToCrossApp, dbRelationTripleSchemaToCrossApp, dbRelationToCrossApp, + dbRelationsToCrossApp, } from "../dbToCrossAppConverters"; import { Tables, Json } from "../../dbTypes"; +import type { DGSupabaseClient } from "../client"; type Concept = Tables<"Concept">; @@ -249,8 +251,8 @@ describe("dbRelationTripleSchemaToCrossApp", () => { describe("dbRelationToCrossApp", () => { const conceptMap: Record = { 10: "orn:obsidian.schema:vault-a/relation-type-1", - 20: "orn:obsidian.node:vault-a/source-node-1", - 30: "orn:obsidian.node:vault-a/destination-node-1", + 20: "orn:obsidian.note:vault-a/source-node-1", + 30: "orn:obsidian.note:vault-a/destination-node-1", }; it("converts a relation, resolving type/source/destination to local ids", () => { @@ -281,7 +283,7 @@ describe("dbRelationToCrossApp", () => { }); const foreignConceptMap = { ...conceptMap, - 20: "orn:obsidian.node:vault-b/source-node-1", + 20: "orn:obsidian.note:vault-b/source-node-1", }; expect( dbRelationToCrossApp({ @@ -290,7 +292,7 @@ describe("dbRelationToCrossApp", () => { accountMap, conceptMap: foreignConceptMap, }).source, - ).toBe("orn:obsidian.node:vault-b/source-node-1"); + ).toBe("orn:obsidian.note:vault-b/source-node-1"); }); it("throws when the relation type is missing", () => { @@ -304,3 +306,82 @@ describe("dbRelationToCrossApp", () => { ).toThrow("Missing relationType"); }); }); + +describe("dbRelationsToCrossApp endpoint rids", () => { + const crossSpaceMap: Record = { + 1: "obsidian:vault-a", + 2: "obsidian:vault-b", + 3: "https://roamresearch.com/#/app/research-graph", + }; + + const conceptRows = [ + { + id: 10, + space_id: 1, + source_local_id: "relation-type-1", + is_schema: true, + is_relation: true, + }, + { + id: 20, + space_id: 2, + source_local_id: "source-node-1", + is_schema: false, + is_relation: false, + }, + { + id: 30, + space_id: 3, + source_local_id: "roam-uid-1", + is_schema: false, + is_relation: false, + }, + ]; + + const makeClient = (): DGSupabaseClient => { + const result = { data: conceptRows, error: null }; + const builder = { + select: vi.fn(), + in: vi.fn(), + not: vi.fn(), + then: ( + resolve: (value: typeof result) => unknown, + reject?: (reason: unknown) => unknown, + ) => Promise.resolve(result).then(resolve, reject), + }; + builder.select.mockReturnValue(builder); + builder.in.mockReturnValue(builder); + builder.not.mockReturnValue(builder); + return { from: vi.fn(() => builder) } as unknown as DGSupabaseClient; + }; + + const relation = baseConcept({ + is_schema: false, + schema_id: 10, + reference_content: { source: 20, destination: 30 }, + }); + + // The Obsidian rid must match the `importedFrom.sourceNodeRid` Roam stores for + // an imported note, or importSharedRelations cannot resolve the endpoint. + it("gives an Obsidian endpoint the note subtype", async () => { + const [converted] = await dbRelationsToCrossApp({ + client: makeClient(), + relations: [relation], + accountMap, + spaceMap: crossSpaceMap, + }); + expect(converted?.source).toBe("orn:obsidian.note:vault-b/source-node-1"); + }); + + it("leaves a Roam endpoint as a url rid with no subtype", async () => { + const [converted] = await dbRelationsToCrossApp({ + client: makeClient(), + relations: [relation], + accountMap, + spaceMap: crossSpaceMap, + }); + expect(converted?.destination).toBe( + "https://roamresearch.com/#/app/research-graph/roam-uid-1", + ); + }); +}); diff --git a/packages/database/src/lib/__tests__/sharedNodes.test.ts b/packages/database/src/lib/__tests__/sharedNodes.test.ts index b854153ee..f251d30d1 100644 --- a/packages/database/src/lib/__tests__/sharedNodes.test.ts +++ b/packages/database/src/lib/__tests__/sharedNodes.test.ts @@ -208,7 +208,7 @@ describe("buildSharedNodes", () => { })[0]?.slots, ).toEqual({ evidence: "node-5", - claim: "orn:obsidian:vault-b/node-6", + claim: "orn:obsidian.note:vault-b/node-6", }); }); diff --git a/packages/database/src/lib/dbToCrossAppConverters.ts b/packages/database/src/lib/dbToCrossAppConverters.ts index 938ac8e95..28171c97c 100644 --- a/packages/database/src/lib/dbToCrossAppConverters.ts +++ b/packages/database/src/lib/dbToCrossAppConverters.ts @@ -18,18 +18,23 @@ const getConceptMap = async ( if (conceptIds.length === 0) return {}; const request = await client .from("my_concepts") - .select("id, space_id, source_local_id") + .select("id, space_id, source_local_id, is_schema, is_relation") .in("id", conceptIds) .not("source_local_id", "is", null); if (request.error) throw request.error; return Object.fromEntries( (request.data || []) - .map(({ id, source_local_id, space_id }) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + .map(({ id, source_local_id, space_id, is_schema, is_relation }) => { const spaceUri: string | undefined = spaceMap[space_id ?? 0]; return [ id!, spaceUri !== undefined - ? spaceUriAndLocalIdToRid(spaceUri, source_local_id) + ? spaceUriAndLocalIdToRid( + spaceUri, + source_local_id, + is_schema ? "schema" : is_relation ? "relation" : "note", + ) : undefined, ]; }) diff --git a/packages/database/src/lib/rid.ts b/packages/database/src/lib/rid.ts index f326cad25..4c419900f 100644 --- a/packages/database/src/lib/rid.ts +++ b/packages/database/src/lib/rid.ts @@ -3,7 +3,6 @@ // Either a Web URL, with the last segment as the sourceLocalId; // OR the format `orn:.:/` // With the assumption that the sourceUri has the form : -// The subtype may be omitted. export const isRid = (value: string): boolean => value.startsWith("https://") || value.startsWith("orn:"); @@ -11,7 +10,7 @@ export const isRid = (value: string): boolean => export const spaceUriAndLocalIdToRid = ( spaceUri: string, localId: string, - subtype?: string, + subtype: string, ): string => { // Both RID forms use `/` as the sourceLocalId delimiter, so callers must pass // slash-free localIds (or pre-encode them) for ridToSpaceUriAndLocalId to @@ -19,9 +18,7 @@ export const spaceUriAndLocalIdToRid = ( if (spaceUri.startsWith("http")) return `${spaceUri}/${localId}`; const parts = spaceUri.split(":"); if (parts.length === 2) - return subtype - ? `orn:${parts[0]}.${subtype}:${parts[1]}/${localId}` - : `orn:${parts[0]}:${parts[1]}/${localId}`; + return `orn:${parts[0]}.${subtype}:${parts[1]}/${localId}`; throw new Error("Unrecognized spaceUri"); }; diff --git a/packages/database/src/lib/sharedNodes.ts b/packages/database/src/lib/sharedNodes.ts index a71310876..15771fbed 100644 --- a/packages/database/src/lib/sharedNodes.ts +++ b/packages/database/src/lib/sharedNodes.ts @@ -190,11 +190,7 @@ export const buildSharedNodes = ({ let rid: string; try { - rid = spaceUriAndLocalIdToRid( - space.url, - node.source_local_id, - space.platform === "Obsidian" ? "note" : undefined, - ); + rid = spaceUriAndLocalIdToRid(space.url, node.source_local_id, "note"); } catch { return []; } @@ -208,7 +204,8 @@ export const buildSharedNodes = ({ if (!space || !c.source_local_id || !c.id) return [c.id, undefined]; return [ c.id, - spaceUriAndLocalIdToRid(space.url, c.source_local_id), + // To be reviewed if we have relations of relations + spaceUriAndLocalIdToRid(space.url, c.source_local_id, "note"), ]; }) as [number, string | undefined][], );