diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index 1502a3417..aebf0a4e9 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -10,7 +10,7 @@ import { getImportedNodesInfo, getLocalNodeKeyToEndpointId, } from "~/utils/relationsStore"; -import { spaceUriAndLocalIdToRid } from "./rid"; +import { spaceUriAndLocalIdToRid } from "@repo/database/lib/rid"; import type { PostgrestResponse } from "@supabase/supabase-js"; import type { Tables } from "@repo/database/dbTypes"; import { getSpaceNameIdFromRid } from "./spaceFromRid"; diff --git a/apps/obsidian/src/utils/importPreview.ts b/apps/obsidian/src/utils/importPreview.ts index 3d2e85335..327cda171 100644 --- a/apps/obsidian/src/utils/importPreview.ts +++ b/apps/obsidian/src/utils/importPreview.ts @@ -11,7 +11,7 @@ import { fetchRelationInstancesFromSpace, type RemoteRelationInstance, } from "./importRelations"; -import { spaceUriAndLocalIdToRid } from "./rid"; +import { spaceUriAndLocalIdToRid } from "@repo/database/lib/rid"; export type RelationTriplet = { sourceNodeTypeName: string; diff --git a/apps/obsidian/src/utils/importRelations.ts b/apps/obsidian/src/utils/importRelations.ts index a1acd207c..a3efd01e1 100644 --- a/apps/obsidian/src/utils/importRelations.ts +++ b/apps/obsidian/src/utils/importRelations.ts @@ -3,7 +3,7 @@ import type { DGSupabaseClient } from "@repo/database/lib/client"; import { uuidv7 } from "uuidv7"; import type DiscourseGraphPlugin from "~/index"; import type { DiscourseRelationType, DiscourseRelation } from "~/types"; -import { spaceUriAndLocalIdToRid } from "./rid"; +import { spaceUriAndLocalIdToRid } from "@repo/database/lib/rid"; import { loadRelations, addRelationNoCheck, diff --git a/apps/obsidian/src/utils/relationsStore.ts b/apps/obsidian/src/utils/relationsStore.ts index b7c8f92cc..d27ead83b 100644 --- a/apps/obsidian/src/utils/relationsStore.ts +++ b/apps/obsidian/src/utils/relationsStore.ts @@ -7,7 +7,10 @@ import { getVaultId, getLocalSpaceUri } from "./supabaseContext"; import type { RelationInstance } from "~/types"; import { QueryEngine, getImportedNodesRaw } from "~/services/QueryEngine"; import { publishNewRelation } from "./publishNode"; -import { ridToSpaceUriAndLocalId, spaceUriAndLocalIdToRid } from "./rid"; +import { + ridToSpaceUriAndLocalId, + spaceUriAndLocalIdToRid, +} from "@repo/database/lib/rid"; import { getSpaceIdsBySpaceUris } from "./spaceFromRid"; const RELATIONS_FILE_NAME = "relations.json"; diff --git a/apps/obsidian/src/utils/spaceFromRid.ts b/apps/obsidian/src/utils/spaceFromRid.ts index 3210fbf88..68f5ca8a8 100644 --- a/apps/obsidian/src/utils/spaceFromRid.ts +++ b/apps/obsidian/src/utils/spaceFromRid.ts @@ -1,5 +1,5 @@ import type { DGSupabaseClient } from "@repo/database/lib/client"; -import { ridToSpaceUriAndLocalId } from "./rid"; +import { ridToSpaceUriAndLocalId } from "@repo/database/lib/rid"; export const getSpaceNameIdFromRid = async ( client: DGSupabaseClient, diff --git a/apps/obsidian/src/utils/typeUtils.ts b/apps/obsidian/src/utils/typeUtils.ts index 09ebee4b7..9540ea81f 100644 --- a/apps/obsidian/src/utils/typeUtils.ts +++ b/apps/obsidian/src/utils/typeUtils.ts @@ -1,6 +1,6 @@ import type DiscourseGraphPlugin from "~/index"; import { DiscourseNode, DiscourseRelationType, ImportStatus } from "~/types"; -import { ridToSpaceUriAndLocalId } from "./rid"; +import { ridToSpaceUriAndLocalId } from "@repo/database/lib/rid"; export const getNodeTypeById = ( plugin: DiscourseGraphPlugin, diff --git a/packages/database/package.json b/packages/database/package.json index b2c0de431..ab7f36f84 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -11,7 +11,8 @@ "default": "./src/dbDotEnv.mjs" }, "./dbTypes": "./src/dbTypes.ts", - "./inputTypes": "./src/inputTypes.ts" + "./inputTypes": "./src/inputTypes.ts", + "./crossAppNodeContract": "./src/crossAppNodeContract.ts" }, "typesVersions": { "*": { diff --git a/packages/database/src/crossAppNodeContract.example.ts b/packages/database/src/crossAppNodeContract.example.ts new file mode 100644 index 000000000..efb0f5b1f --- /dev/null +++ b/packages/database/src/crossAppNodeContract.example.ts @@ -0,0 +1,67 @@ +import type { CrossAppNode } from "./crossAppNodeContract"; +import { spaceUriAndLocalIdToRid } from "./lib/rid"; + +const ROAM_SOURCE_SPACE_ID = "https://roamresearch.com/#/app/MAPLab"; +const ROAM_SOURCE_NODE_ID = "tgWb6JozF"; + +const roamFullMarkdown = `# Sleep improves memory consolidation + +Multiple studies show that sleep after learning strengthens memory traces. + +- Supported by [[EVD]] - Rasch & Born 2013 +`; + +export const roamOriginNodeExample: CrossAppNode = { + sourceApp: "roam", + sourceSpaceId: ROAM_SOURCE_SPACE_ID, + sourceSpaceName: "MAPLab", + sourceNodeId: ROAM_SOURCE_NODE_ID, + sourceNodeRid: spaceUriAndLocalIdToRid( + ROAM_SOURCE_SPACE_ID, + ROAM_SOURCE_NODE_ID, + ), + nodeType: { + sourceNodeTypeId: "rCLM0schema", + label: "Claim", + }, + content: { + direct: { value: "Sleep improves memory consolidation" }, + full: { format: "text/markdown", value: roamFullMarkdown }, + }, + sourceModifiedAt: "2026-06-12T14:00:00.000Z", +}; + +const OBSIDIAN_SOURCE_SPACE_ID = "obsidian:9a8b7c6d5e4f3210"; +const OBSIDIAN_SOURCE_NODE_ID = "0192f1a0-7b3c-7e2a-9f10-1a2b3c4d5e6f"; +const OBSIDIAN_SOURCE_NODE_TYPE_ID = "evd-7c1f9a2b"; + +const obsidianFullMarkdown = `--- +nodeTypeId: ${OBSIDIAN_SOURCE_NODE_TYPE_ID} +nodeInstanceId: ${OBSIDIAN_SOURCE_NODE_ID} +--- + +# REM sleep correlates with recall + +Participants with more REM sleep showed better next-day recall. +`; + +export const obsidianOriginNodeExample: CrossAppNode = { + sourceApp: "obsidian", + sourceSpaceId: OBSIDIAN_SOURCE_SPACE_ID, + sourceSpaceName: "Research Vault", + sourceNodeId: OBSIDIAN_SOURCE_NODE_ID, + sourceNodeRid: spaceUriAndLocalIdToRid( + OBSIDIAN_SOURCE_SPACE_ID, + OBSIDIAN_SOURCE_NODE_ID, + "note", + ), + nodeType: { + sourceNodeTypeId: OBSIDIAN_SOURCE_NODE_TYPE_ID, + label: "Evidence", + }, + content: { + direct: { value: "EVD - REM sleep and recall" }, + full: { format: "text/markdown", value: obsidianFullMarkdown }, + }, + sourceModifiedAt: "2026-06-14T10:30:00.000Z", +}; diff --git a/packages/database/src/crossAppNodeContract.ts b/packages/database/src/crossAppNodeContract.ts new file mode 100644 index 000000000..1804a98e7 --- /dev/null +++ b/packages/database/src/crossAppNodeContract.ts @@ -0,0 +1,39 @@ +export type CrossAppNode = { + sourceApp: "roam" | "obsidian"; + /** + * Stable source-space id. Maps to `Space.url`, not numeric `Space.id`. + */ + sourceSpaceId: string; + sourceSpaceName: string; + /** + * Node id inside the source app/space. + * Roam: page/block UID. + * Obsidian: nodeInstanceId. + */ + sourceNodeId: string; + sourceNodeRid: string; + nodeType: { + /** + * Node type/schema id inside the source app/space. + * Maps to the schema Concept's `source_local_id`. + */ + sourceNodeTypeId: string; + label: string; + }; + content: { + direct: { value: string }; + full: { + /** + * Contract media type for `full.value`; current Content rows store the + * markdown in `text`, not in a typed media column. + */ + format: "text/markdown"; + value: string; + }; + }; + /** + * Source modified timestamp, or latest required `Content.last_modified` when + * deriving from persisted `direct` and `full` rows. + */ + sourceModifiedAt: string; +}; diff --git a/apps/obsidian/src/utils/rid.ts b/packages/database/src/lib/rid.ts similarity index 87% rename from apps/obsidian/src/utils/rid.ts rename to packages/database/src/lib/rid.ts index c96db9e3d..f2a200acf 100644 --- a/apps/obsidian/src/utils/rid.ts +++ b/packages/database/src/lib/rid.ts @@ -10,6 +10,9 @@ export const spaceUriAndLocalIdToRid = ( localId: 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 + // round-trip the original pair. if (spaceUri.startsWith("http")) return `${spaceUri}/${localId}`; const parts = spaceUri.split(":"); if (parts.length === 2)