diff --git a/apps/obsidian/package.json b/apps/obsidian/package.json index 1b715d839..93d28ec80 100644 --- a/apps/obsidian/package.json +++ b/apps/obsidian/package.json @@ -43,7 +43,6 @@ "@repo/utils": "workspace:*", "@supabase/supabase-js": "catalog:", "date-fns": "^4.1.0", - "gray-matter": "^4.0.3", "mime-types": "^3.0.1", "nanoid": "^4.0.2", "react": "catalog:obsidian", diff --git a/apps/obsidian/src/components/ImportNodesModal.tsx b/apps/obsidian/src/components/ImportNodesModal.tsx index 2859eaf12..d91b026e0 100644 --- a/apps/obsidian/src/components/ImportNodesModal.tsx +++ b/apps/obsidian/src/components/ImportNodesModal.tsx @@ -8,11 +8,14 @@ import { getAvailableGroupIds } from "@repo/database/lib/groups"; import { fetchUserNames, getPublishedNodesForGroups, - getLocalNodeInstanceIds, getSpaceNameFromIds, getSpaceUris, importSelectedNodes, } from "~/utils/importNodes"; +import { + getImportedNodeKey, + getImportedNodesInfo, +} from "~/utils/relationsStore"; import { getLoggedInClient, getSupabaseContext } from "~/utils/supabaseContext"; import { computeImportPreview, @@ -70,11 +73,20 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { currentSpaceId: context.spaceId, }); - const localNodeInstanceIds = getLocalNodeInstanceIds(plugin); + const { nodeKeys: importedNodeKeys } = await getImportedNodesInfo({ + plugin, + client, + }); // Filter out nodes that already exist locally const importableNodes = publishedNodes.filter( - (node) => !localNodeInstanceIds.has(node.source_local_id), + (node) => + !importedNodeKeys.has( + getImportedNodeKey({ + spaceId: node.space_id, + sourceLocalId: node.source_local_id, + }), + ), ); const uniqueSpaceIds = [ @@ -219,6 +231,7 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { keyToRid: previewData.keyToRid, keyToRelationEndpointId: previewData.keyToRelationEndpointId, relationInstancesBySpace: previewData.relationInstancesBySpace, + sourceNodeTypeIdByKey: previewData.sourceNodeTypeIdByKey, } : undefined, }); @@ -228,6 +241,7 @@ const ImportNodesContent = ({ plugin, onClose }: ImportNodesModalProps) => { `Import completed with some issues:\n${result.success} files imported successfully\n${result.failed} files failed`, 5000, ); + console.error("Import errors:", result.errors); } else { new Notice(`Successfully imported ${result.success} node(s)`, 3000); } diff --git a/apps/obsidian/src/services/QueryEngine.ts b/apps/obsidian/src/services/QueryEngine.ts index 0edee84e6..20f11f4c0 100644 --- a/apps/obsidian/src/services/QueryEngine.ts +++ b/apps/obsidian/src/services/QueryEngine.ts @@ -306,13 +306,15 @@ export class QueryEngine { } /** - * Return all markdown pages under import/ that have importedFromRid and nodeInstanceId. + * Return all markdown pages that have importedFromRid and nodeInstanceId, + * wherever they now live: the frontmatter pair identifies an imported node, + * and users move notes out of import/ after importing them. * Uses DataCore when available; falls back to vault iteration otherwise. */ getImportedNodePages = (): TFile[] => { if (this.dc) { try { - const dcQuery = `@page and path("import") and exists(importedFromRid) and exists(nodeInstanceId)`; + const dcQuery = `@page and exists(importedFromRid) and exists(nodeInstanceId)`; const pages = this.dc.query(dcQuery); const files: TFile[] = []; for (const page of pages) { @@ -498,7 +500,6 @@ export class QueryEngine { const files: TFile[] = []; const allFiles = this.app.vault.getMarkdownFiles(); for (const f of allFiles) { - if (!f.path.startsWith("import/")) continue; const fm = this.app.metadataCache.getFileCache(f)?.frontmatter; if ( (fm as Record | undefined)?.importedFromRid && diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index d1122eddf..5d21deb1f 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -1,5 +1,4 @@ import type { Json } from "@repo/database/dbTypes"; -import matter from "gray-matter"; import { App, Notice, TFile } from "obsidian"; import type { DGSupabaseClient } from "@repo/database/lib/client"; import { listGroupSharedNodes } from "@repo/database/lib/sharedNodes"; @@ -8,6 +7,7 @@ import { getLoggedInClient, getSupabaseContext } from "./supabaseContext"; import type { DiscourseNode, ImportableNode } from "~/types"; import { QueryEngine } from "~/services/QueryEngine"; import { + getImportedNodeKey, getImportedNodesInfo, getLocalNodeKeyToEndpointId, } from "~/utils/relationsStore"; @@ -70,24 +70,6 @@ export const getPublishedNodesForGroups = async ({ }); }; -export const getLocalNodeInstanceIds = ( - plugin: DiscourseGraphPlugin, -): Set => { - const queryEngine = new QueryEngine(plugin.app); - const files = queryEngine.getFilesWithNodeInstanceId(); - const nodeInstanceIds = new Set(); - - for (const file of files) { - const cache = plugin.app.metadataCache.getFileCache(file); - const frontmatter = cache?.frontmatter; - if (frontmatter?.nodeInstanceId) { - nodeInstanceIds.add(frontmatter.nodeInstanceId as string); - } - } - - return nodeInstanceIds; -}; - /** * Returns the space name for a given space ID. * Falls back to "space-{id}" if the lookup fails. @@ -322,6 +304,106 @@ const fetchNodeContentForImport = async ({ }; }; +type SourceNodeConcept = Pick< + Tables<"my_concepts">, + "source_local_id" | "schema_id" +>; +type SourceNodeSchema = Pick, "id" | "source_local_id">; + +/** + * Maps each node instance to the source_local_id of its node type schema, keyed by + * getImportedNodeKey. Roam-origin markdown carries no frontmatter, so the node type + * has to be resolved from the source space instead of parsed out of the payload. + */ +export const buildSourceNodeTypeIdMap = ({ + spaceId, + concepts, + schemas, +}: { + spaceId: number; + concepts: SourceNodeConcept[]; + schemas: SourceNodeSchema[]; +}): Map => { + const sourceNodeTypeIdBySchemaId = new Map(); + for (const schema of schemas) { + if (schema.id !== null && schema.source_local_id !== null) { + sourceNodeTypeIdBySchemaId.set(schema.id, schema.source_local_id); + } + } + + const sourceNodeTypeIdByKey = new Map(); + for (const concept of concepts) { + if (concept.source_local_id === null || concept.schema_id === null) + continue; + const sourceNodeTypeId = sourceNodeTypeIdBySchemaId.get(concept.schema_id); + if (!sourceNodeTypeId) continue; + sourceNodeTypeIdByKey.set( + getImportedNodeKey({ + spaceId, + sourceLocalId: concept.source_local_id, + }), + sourceNodeTypeId, + ); + } + return sourceNodeTypeIdByKey; +}; + +/** + * Fallback for callers that have no import preview (refreshImportedFile); + * computeImportPreview resolves the same map while building its preview. + */ +const fetchSourceNodeTypeIds = async ({ + client, + spaceId, + nodeInstanceIds, +}: { + client: DGSupabaseClient; + spaceId: number; + nodeInstanceIds: string[]; +}): Promise<{ + sourceNodeTypeIdByKey: Map; + error?: string; +}> => { + const { data: concepts, error: conceptError } = await client + .from("my_concepts") + .select("source_local_id, schema_id") + .eq("space_id", spaceId) + .eq("is_schema", false) + .eq("arity", 0) + .in("source_local_id", nodeInstanceIds); + if (conceptError) { + return { sourceNodeTypeIdByKey: new Map(), error: conceptError.message }; + } + + const schemaIds = [ + ...new Set( + concepts.flatMap((concept) => + concept.schema_id === null ? [] : [concept.schema_id], + ), + ), + ]; + if (schemaIds.length === 0) return { sourceNodeTypeIdByKey: new Map() }; + + const { data: schemas, error: schemaError } = await client + .from("my_concepts") + .select("id, source_local_id") + .eq("space_id", spaceId) + .eq("is_schema", true) + .eq("arity", 0) + .in("id", schemaIds); + if (schemaError) { + return { sourceNodeTypeIdByKey: new Map(), error: schemaError.message }; + } + + return { + sourceNodeTypeIdByKey: buildSourceNodeTypeIdMap({ + spaceId, + concepts, + schemas, + }), + }; +}; + /** * Fetches created/last_modified from the source space Content (my_contents) for an imported node. * Used by the discourse context view to show "last modified in original vault". @@ -937,22 +1019,33 @@ const sanitizePathForImport = (path: string): string => { .join("/"); }; -type ParsedFrontmatter = { - nodeTypeId?: string; - nodeInstanceId?: string; - publishedToGroups?: string[]; - authorId?: number; - [key: string]: unknown; -}; - -const parseFrontmatter = ( - content: string, -): { frontmatter: ParsedFrontmatter; body: string } => { - const { data, content: body } = matter(content); - return { - frontmatter: (data ?? {}) as ParsedFrontmatter, - body: body ?? "", - }; +/** + * Two source nodes can share a title, so the second one gets a " (n)" suffix + * instead of overwriting the first. + */ +const getAvailableImportPath = async ({ + desiredPath, + pathExists, +}: { + desiredPath: string; + pathExists: (path: string) => Promise; +}): Promise => { + if (!(await pathExists(desiredPath))) return desiredPath; + + const extensionIndex = desiredPath.lastIndexOf("."); + const hasExtension = extensionIndex > desiredPath.lastIndexOf("/"); + const basePath = hasExtension + ? desiredPath.slice(0, extensionIndex) + : desiredPath; + const extension = hasExtension ? desiredPath.slice(extensionIndex) : ""; + + let counter = 1; + let availablePath = `${basePath} (${counter})${extension}`; + while (await pathExists(availablePath)) { + counter++; + availablePath = `${basePath} (${counter})${extension}`; + } + return availablePath; }; /** @@ -1096,7 +1189,9 @@ const processFileContent = async ({ sourceSpaceId, sourceSpaceUri, rawContent, - originalFilePath, + sourceNodeId, + sourceNodeTypeId, + importedFromRid, filePath, importedCreatedAt, importedModifiedAt, @@ -1107,48 +1202,24 @@ const processFileContent = async ({ sourceSpaceId: number; sourceSpaceUri: string; rawContent: string; - originalFilePath?: string; + sourceNodeId: string; + sourceNodeTypeId: string; + importedFromRid: string; filePath: string; - importedCreatedAt?: number; - importedModifiedAt?: number; - authorId?: number; -}): Promise< - { file: TFile; error?: never } | { file?: never; error: string } -> => { + importedCreatedAt: number; + importedModifiedAt: number; + authorId: number; +}): Promise => { // 1. Create or update the file with the fetched content first. // On create, set file metadata (ctime/mtime) to original vault dates via vault adapter. let file: TFile | null = plugin.app.vault.getFileByPath(filePath); - const stat = - importedCreatedAt !== undefined && importedModifiedAt !== undefined - ? { - ctime: importedCreatedAt, - mtime: importedModifiedAt, - } - : undefined; + const stat = { ctime: importedCreatedAt, mtime: importedModifiedAt }; if (!file) { file = await plugin.app.vault.create(filePath, rawContent, stat); } else { await plugin.app.vault.process(file, () => rawContent, stat); } - // 2. Parse frontmatter from rawContent (metadataCache is updated async and is - // often empty immediately after create/modify), then map nodeTypeId and update frontmatter. - const { frontmatter } = parseFrontmatter(rawContent); - const sourceNodeTypeId = frontmatter.nodeTypeId; - if (typeof sourceNodeTypeId !== "string") { - await plugin.app.vault.delete(file); - return { - error: "importedNode missing sourceNodeTypeId", - }; - } - const sourceNodeId = frontmatter.nodeInstanceId; - if (typeof sourceNodeId !== "string") { - await plugin.app.vault.delete(file); - return { - error: "importedNode missing nodeInstanceId", - }; - } - const mappedNodeTypeId = await mapNodeTypeIdToLocal({ plugin, client, @@ -1161,21 +1232,16 @@ const processFileContent = async ({ file, (fm) => { const record = fm as Record; - if (mappedNodeTypeId !== undefined) { - record.nodeTypeId = mappedNodeTypeId; - } - record.importedFromRid = spaceUriAndLocalIdToRid( - sourceSpaceUri, - sourceNodeId, - "note", - ); + record.nodeInstanceId = sourceNodeId; + record.nodeTypeId = mappedNodeTypeId; + record.importedFromRid = importedFromRid; record.lastModified = importedModifiedAt; - if (authorId) record.authorId = authorId; + record.authorId = authorId; }, stat, ); - return { file }; + return file; }; export const importSelectedNodes = async ({ @@ -1192,8 +1258,13 @@ export const importSelectedNodes = async ({ keyToRid: Map; keyToRelationEndpointId: Map; relationInstancesBySpace: Map; + sourceNodeTypeIdByKey: Map; }; -}): Promise<{ success: number; failed: number }> => { +}): Promise<{ + success: number; + failed: number; + errors: Array<{ nodeTitle: string; nodeInstanceId: string; error: string }>; +}> => { const client = await getLoggedInClient(plugin); if (!client) { throw new Error("Cannot get Supabase client"); @@ -1205,11 +1276,28 @@ export const importSelectedNodes = async ({ } const queryEngine = new QueryEngine(plugin.app); + const pathExists = (path: string) => plugin.app.vault.adapter.exists(path); let successCount = 0; - let failedCount = 0; let processedCount = 0; const totalNodes = selectedNodes.length; + // Collected per node so the caller can report why an import failed, the same + // way refreshAllImportedFiles does. Titles are not unique across source + // spaces, so each entry carries the instance id too. + const errors: Array<{ + nodeTitle: string; + nodeInstanceId: string; + error: string; + }> = []; + const recordFailure = (node: ImportableNode, error: string) => { + errors.push({ + nodeTitle: node.title, + nodeInstanceId: node.nodeInstanceId, + error, + }); + processedCount++; + onProgress?.(processedCount, totalNodes); + }; // Group nodes by space to create folders efficiently const nodesBySpace = new Map(); @@ -1229,15 +1317,31 @@ export const importSelectedNodes = async ({ for (const [spaceId, nodes] of nodesBySpace.entries()) { const spaceUri = spaceUris.get(spaceId); if (!spaceUri) { - for (const _node of nodes) { - failedCount++; - processedCount++; - onProgress?.(processedCount, totalNodes); + for (const node of nodes) { + recordFailure(node, `No space URL found for space ${spaceId}`); } continue; } const spaceName = spaceNames.get(spaceId) ?? `space-${spaceId}`; + let sourceNodeTypeIdByKey = precomputedData?.sourceNodeTypeIdByKey; + if (!sourceNodeTypeIdByKey) { + const fetched = await fetchSourceNodeTypeIds({ + client, + spaceId, + nodeInstanceIds: nodes.map((node) => node.nodeInstanceId), + }); + if (fetched.error) { + for (const node of nodes) { + recordFailure( + node, + `Could not read node types from space ${spaceId}: ${fetched.error}`, + ); + } + continue; + } + sourceNodeTypeIdByKey = fetched.sourceNodeTypeIdByKey; + } const importFolderPath = await resolveFolderForSpaceUri({ adapter: plugin.app.vault.adapter, spaceUri, @@ -1247,6 +1351,16 @@ export const importSelectedNodes = async ({ // Process each node in this space for (const node of nodes) { try { + const sourceNodeTypeId = sourceNodeTypeIdByKey.get( + getImportedNodeKey({ + spaceId, + sourceLocalId: node.nodeInstanceId, + }), + ); + if (!sourceNodeTypeId) { + recordFailure(node, `No node type schema in space ${spaceId}`); + continue; + } const importedFromRid = spaceUriAndLocalIdToRid( spaceUri, node.nodeInstanceId, @@ -1265,9 +1379,10 @@ export const importSelectedNodes = async ({ }); if (!nodeContent) { - failedCount++; - processedCount++; - onProgress?.(processedCount, totalNodes); + recordFailure( + node, + `No importable title/body content in space ${spaceId}`, + ); continue; } @@ -1298,13 +1413,16 @@ export const importSelectedNodes = async ({ contentFilePath && contentFilePath.includes("/") ? sanitizePathForImport(contentFilePath) : `${sanitizedFileName}.md`; - finalFilePath = `${importFolderPath}/${pathUnderImport}`; + finalFilePath = await getAvailableImportPath({ + desiredPath: `${importFolderPath}/${pathUnderImport}`, + pathExists, + }); // Ensure all parent folders exist (e.g. import/VaultName/Discourse Nodes/SubFolder) const dirParts = finalFilePath.split("/"); for (let i = 1; i < dirParts.length - 1; i++) { const folderPath = dirParts.slice(0, i + 1).join("/"); - if (!(await plugin.app.vault.adapter.exists(folderPath))) { + if (!(await pathExists(folderPath))) { await plugin.app.vault.createFolder(folderPath); } } @@ -1312,33 +1430,21 @@ export const importSelectedNodes = async ({ // Process the file content (maps nodeTypeId, handles frontmatter, stores import timestamps) // This updates existing file or creates new one - const result = await processFileContent({ + const processedFile = await processFileContent({ plugin, client, sourceSpaceId: spaceId, sourceSpaceUri: spaceUri, rawContent: content, - originalFilePath: contentFilePath, + sourceNodeId: node.nodeInstanceId, + sourceNodeTypeId, + importedFromRid, filePath: finalFilePath, importedCreatedAt: createdAt, importedModifiedAt: modifiedAt, authorId, }); - if (result.error) { - console.error( - `Error processing file content for node ${node.nodeInstanceId}:`, - result.error, - ); - failedCount++; - processedCount++; - onProgress?.(processedCount, totalNodes); - continue; - } - - // typescript should not need this assertion? - const processedFile = result.file!; - // Import assets for this node (use originalNodePath so assets go under import/{space}/ relative to note) const assetImportResult = await importAssetsForNode({ plugin, @@ -1370,13 +1476,10 @@ export const importSelectedNodes = async ({ const currentDir = processedFile.path.includes("/") ? processedFile.path.replace(/\/[^/]*$/, "") : importFolderPath; - const newPath = `${currentDir}/${sanitizedFileName}.md`; - let targetPath = newPath; - let counter = 1; - while (await plugin.app.vault.adapter.exists(targetPath)) { - targetPath = `${currentDir}/${sanitizedFileName} (${counter}).md`; - counter++; - } + const targetPath = await getAvailableImportPath({ + desiredPath: `${currentDir}/${sanitizedFileName}.md`, + pathExists, + }); await plugin.app.fileManager.renameFile(processedFile, targetPath); } @@ -1384,10 +1487,10 @@ export const importSelectedNodes = async ({ processedCount++; onProgress?.(processedCount, totalNodes); } catch (error) { - console.error(`Error importing node ${node.nodeInstanceId}:`, error); - failedCount++; - processedCount++; - onProgress?.(processedCount, totalNodes); + recordFailure( + node, + error instanceof Error ? error.message : String(error), + ); } } @@ -1425,7 +1528,7 @@ export const importSelectedNodes = async ({ } } - return { success: successCount, failed: failedCount }; + return { success: successCount, failed: errors.length, errors }; }; /** @@ -1499,7 +1602,7 @@ export const refreshImportedFile = async ({ }); return { success: result.success > 0, - error: result.failed > 0 ? "Failed to refresh imported file" : undefined, + error: result.errors[0]?.error, }; }; diff --git a/apps/obsidian/src/utils/importPreview.ts b/apps/obsidian/src/utils/importPreview.ts index 327cda171..649c416b8 100644 --- a/apps/obsidian/src/utils/importPreview.ts +++ b/apps/obsidian/src/utils/importPreview.ts @@ -2,10 +2,11 @@ import type DiscourseGraphPlugin from "~/index"; import type { ImportableNode } from "~/types"; import { getLoggedInClient, getSupabaseContext } from "./supabaseContext"; import { + getImportedNodeKey, getImportedNodesInfo, getLocalNodeKeyToEndpointId, } from "./relationsStore"; -import { getSpaceUris } from "./importNodes"; +import { buildSourceNodeTypeIdMap, getSpaceUris } from "./importNodes"; import { QueryEngine } from "~/services/QueryEngine"; import { fetchRelationInstancesFromSpace, @@ -39,6 +40,8 @@ export type ImportPreviewData = { keyToRelationEndpointId: Map; /** Relation instances per spaceId, for reuse during import */ relationInstancesBySpace: Map; + /** Key -> source node type's source_local_id, so import doesn't re-query the source space */ + sourceNodeTypeIdByKey: Map; }; export const computeImportPreview = async ({ @@ -73,6 +76,8 @@ export const computeImportPreview = async ({ const newNodeTypeSchemas: Array<{ id: string; name: string }> = []; const seenNodeTypeIds = new Set(); + // Key -> source node type id, handed to importSelectedNodes so it doesn't re-query + const sourceNodeTypeIdByKey = new Map(); // Maps source_local_id -> name for all node type schemas we encounter (for triplet resolution) const nodeTypeIdToName = new Map(); @@ -89,20 +94,16 @@ export const computeImportPreview = async ({ .select("source_local_id, schema_id") .eq("space_id", spaceId) .eq("is_schema", false) + .eq("arity", 0) .in("source_local_id", nodeInstanceIds); if (!conceptRows) continue; const schemaIds = [ ...new Set( - ( - conceptRows as Array<{ - source_local_id: string; - schema_id: number | null; - }> - ) - .map((r) => r.schema_id) - .filter((id): id is number => id != null), + conceptRows.flatMap((row) => + row.schema_id === null ? [] : [row.schema_id], + ), ), ]; @@ -111,18 +112,25 @@ export const computeImportPreview = async ({ // Resolve schema_ids to node type info const { data: schemaRows } = await client .from("my_concepts") - .select("source_local_id, name") + .select("id, source_local_id, name") .eq("space_id", spaceId) .eq("is_schema", true) + .eq("arity", 0) .in("id", schemaIds); if (!schemaRows) continue; - for (const schema of schemaRows as Array<{ - source_local_id: string; - name: string; - }>) { + for (const [key, sourceNodeTypeId] of buildSourceNodeTypeIdMap({ + spaceId, + concepts: conceptRows, + schemas: schemaRows, + })) { + sourceNodeTypeIdByKey.set(key, sourceNodeTypeId); + } + + for (const schema of schemaRows) { const sourceNodeTypeId = schema.source_local_id; + if (sourceNodeTypeId === null || schema.name === null) continue; // Track name for triplet resolution if (!nodeTypeIdToName.has(sourceNodeTypeId)) { @@ -161,7 +169,10 @@ export const computeImportPreview = async ({ const spaceUri = spaceUris.get(spaceId); if (!spaceUri) continue; for (const node of nodes) { - const key = `${spaceId}:${node.nodeInstanceId}`; + const key = getImportedNodeKey({ + spaceId, + sourceLocalId: node.nodeInstanceId, + }); nodeKeys.add(key); if (!keyToRid.has(key)) { keyToRid.set( @@ -206,8 +217,14 @@ export const computeImportPreview = async ({ ); if (!sourceData || !destData) continue; - const sourceKey = `${sourceData.space_id}:${sourceData.source_local_id}`; - const destKey = `${destData.space_id}:${destData.source_local_id}`; + const sourceKey = getImportedNodeKey({ + spaceId: sourceData.space_id, + sourceLocalId: sourceData.source_local_id, + }); + const destKey = getImportedNodeKey({ + spaceId: destData.space_id, + sourceLocalId: destData.source_local_id, + }); if ( keyToRelationEndpointId.has(sourceKey) && @@ -471,5 +488,6 @@ export const computeImportPreview = async ({ keyToRid, keyToRelationEndpointId, relationInstancesBySpace, + sourceNodeTypeIdByKey, }; }; diff --git a/apps/obsidian/src/utils/relationsStore.ts b/apps/obsidian/src/utils/relationsStore.ts index d27ead83b..6aa49cdb0 100644 --- a/apps/obsidian/src/utils/relationsStore.ts +++ b/apps/obsidian/src/utils/relationsStore.ts @@ -433,9 +433,21 @@ export const buildEndpointToFileMap = ( return map; }; +/** + * The identity of a shared node is its source space plus its id in that space: + * two spaces can each have a node with the same local id. + */ +export const getImportedNodeKey = ({ + spaceId, + sourceLocalId, +}: { + spaceId: number; + sourceLocalId: string; +}): string => `${spaceId}:${sourceLocalId}`; + /** * Build key -> relation endpoint id (RID) for local nodes in this vault. - * Key format: `${localSpaceId}:${nodeInstanceId}`. Value: constructed RID for storage. + * Keys come from getImportedNodeKey. Value: constructed RID for storage. * Uses DataCore when available; falls back to vault iteration otherwise. */ export const getLocalNodeKeyToEndpointId = ( @@ -452,7 +464,10 @@ export const getLocalNodeKeyToEndpointId = ( const fm = cache?.frontmatter as Record | undefined; const nodeInstanceId = fm?.nodeInstanceId as string | undefined; if (nodeInstanceId && fm?.nodeTypeId) { - const key = `${localSpaceId}:${nodeInstanceId}`; + const key = getImportedNodeKey({ + spaceId: localSpaceId, + sourceLocalId: nodeInstanceId, + }); map.set( key, spaceUriAndLocalIdToRid(localSpaceUri, nodeInstanceId, "note"), @@ -496,7 +511,7 @@ export const getImportedNodesInfo = async ({ const spaceUri = ridToSpaceUriAndLocalId(importedFromRid).spaceUri; const spaceId = spaceIdsByUri.get(spaceUri) ?? -1; if (spaceId < 0) continue; - const key = `${spaceId}:${nodeInstanceId}`; + const key = getImportedNodeKey({ spaceId, sourceLocalId: nodeInstanceId }); nodeKeys.add(key); keyToRid.set(key, importedFromRid); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e80fe1c3a..60a863f8a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,9 +136,6 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 - gray-matter: - specifier: ^4.0.3 - version: 4.0.3 mime-types: specifier: ^3.0.1 version: 3.0.2