From cfdaad4d4ec2db3f54d0a159d3c88dc1fe7abf56 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 30 Jul 2026 13:50:16 +0530 Subject: [PATCH 1/4] [ENG-1859] Add Roam import action for selected shared nodes --- .../components/DiscoverSharedNodesDialog.tsx | 327 ++++++++++++++---- .../__tests__/discoverSharedNodes.test.ts | 15 +- .../utils/__tests__/importSharedNodes.test.ts | 113 ++++++ .../__tests__/materializeSharedNode.test.ts | 56 +++ apps/roam/src/utils/discoverSharedNodes.ts | 17 +- apps/roam/src/utils/importSharedNodes.ts | 53 +++ apps/roam/src/utils/materializeSharedNode.ts | 58 +++- 7 files changed, 538 insertions(+), 101 deletions(-) create mode 100644 apps/roam/src/utils/__tests__/importSharedNodes.test.ts create mode 100644 apps/roam/src/utils/importSharedNodes.ts diff --git a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx index a27a5f1eb..df0bcf88d 100644 --- a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx +++ b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx @@ -1,6 +1,7 @@ import { Button, Callout, + Checkbox, Classes, Dialog, HTMLTable, @@ -17,80 +18,160 @@ import { discoverSharedNodes, type DiscoveredSharedNode, } from "~/utils/discoverSharedNodes"; +import { + importSharedNodes, + isFailedSharedNodeImport, + type SharedNodeImportItem, +} from "~/utils/importSharedNodes"; import internalError from "~/utils/internalError"; import { getLoggedInClient, getSupabaseContext } from "~/utils/supabaseContext"; +const CONNECTION_ERROR_MESSAGE = "Could not connect to shared persistence."; +const IMPORT_ERROR_TYPE = "Shared node import failed"; +const IMPORT_ERROR_OPERATION = "import-shared-nodes"; + const formatModifiedAt = (modifiedAt: string): string => new Date(modifiedAt).toLocaleString(); -const SharedNodeRow = ({ node }: { node: DiscoveredSharedNode }) => ( - - - {node.sourceApp} - - -
- {node.sourceSpaceName} -
-
- {node.sourceSpaceId} -
- - -
- {node.title} -
- - - {node.sourceNodeId ? ( +const isImportableSharedNode = (node: DiscoveredSharedNode): boolean => + node.sharedNode.platform === "Obsidian"; + +const SharedNodeRow = ({ + node, + selected, + selectionDisabled, + onToggleSelected, +}: { + node: DiscoveredSharedNode; + selected: boolean; + selectionDisabled: boolean; + onToggleSelected: () => void; +}) => { + const { sharedNode } = node; + return ( + + + + + + {sharedNode.platform} + + +
+ {sharedNode.spaceName} +
- {node.sourceNodeId} + {sharedNode.spaceUri} +
+ + +
+ {sharedNode.title}
- ) : ( - Not provided + + + {sharedNode.sourceLocalId ? ( +
+ {sharedNode.sourceLocalId} +
+ ) : ( + Not provided + )} + + + {formatModifiedAt(sharedNode.lastModified)} + + + {node.alreadyImported ? ( + + Imported + + ) : ( + Available + )} + + + ); +}; + +const ImportResultsSummary = ({ + results, +}: { + results: SharedNodeImportItem[]; +}) => { + const importedCount = results.filter( + (item) => item.status === "imported", + ).length; + const skippedCount = results.filter( + (item) => item.status === "skipped", + ).length; + const failedImports = results.filter(isFailedSharedNodeImport); + return ( + 0 ? Intent.WARNING : Intent.SUCCESS} + title={`${importedCount} imported, ${skippedCount} skipped, ${failedImports.length} failed`} + > + {skippedCount > 0 && ( +
Skipped nodes were already up to date in this graph.
)} - - - {formatModifiedAt(node.modifiedAt)} - - - {node.alreadyImported ? ( - - Imported - - ) : ( - Available + {failedImports.length > 0 && ( +
    + {failedImports.map((item) => ( +
  • + {item.sharedNode.title}:{" "} + {item.message} +
  • + ))} +
)} - - -); +
+ ); +}; const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { const [nodes, setNodes] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [searchTerm, setSearchTerm] = useState(""); + const [selectedRids, setSelectedRids] = useState>(new Set()); + const [importProgress, setImportProgress] = useState<{ + current: number; + total: number; + } | null>(null); + const [importResults, setImportResults] = useState< + SharedNodeImportItem[] | null + >(null); + const importing = importProgress !== null; const loadNodes = useCallback(async (): Promise => { setLoading(true); setError(""); + setSelectedRids(new Set()); + setImportResults(null); try { const context = await getSupabaseContext(); - if (!context) throw new Error("Could not connect to shared persistence."); + if (!context) throw new Error(CONNECTION_ERROR_MESSAGE); const client = await getLoggedInClient(); - if (!client) throw new Error("Could not connect to shared persistence."); + if (!client) throw new Error(CONNECTION_ERROR_MESSAGE); setNodes( await discoverSharedNodes({ client, @@ -123,21 +204,112 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { if (!normalizedSearch) return nodes; return nodes.filter((node) => [ - node.sourceApp, - node.sourceSpaceName, - node.sourceSpaceId, - node.title, - node.sourceNodeId, - ].some((value) => value?.toLocaleLowerCase().includes(normalizedSearch)), + node.sharedNode.platform, + node.sharedNode.spaceName, + node.sharedNode.spaceUri, + node.sharedNode.title, + node.sharedNode.sourceLocalId, + ].some((value) => value.toLocaleLowerCase().includes(normalizedSearch)), ); }, [nodes, searchTerm]); + const importableVisibleRids = visibleNodes + .filter(isImportableSharedNode) + .map((node) => node.sharedNode.rid); + const allVisibleSelected = + importableVisibleRids.length > 0 && + importableVisibleRids.every((rid) => selectedRids.has(rid)); + const someVisibleSelected = importableVisibleRids.some((rid) => + selectedRids.has(rid), + ); + + const toggleNodeSelected = (rid: string): void => { + setSelectedRids((previous) => { + const next = new Set(previous); + if (next.has(rid)) next.delete(rid); + else next.add(rid); + return next; + }); + }; + + const toggleAllVisibleSelected = (): void => { + setSelectedRids((previous) => { + const next = new Set(previous); + if (allVisibleSelected) + importableVisibleRids.forEach((rid) => next.delete(rid)); + else importableVisibleRids.forEach((rid) => next.add(rid)); + return next; + }); + }; + + const importSelectedNodes = async (): Promise => { + const selectedNodes = nodes + .filter((node) => selectedRids.has(node.sharedNode.rid)) + .map((node) => node.sharedNode); + + setImportResults(null); + setImportProgress({ current: 0, total: selectedNodes.length }); + try { + const client = await getLoggedInClient(); + if (!client) throw new Error(CONNECTION_ERROR_MESSAGE); + const results = await importSharedNodes({ + client, + sharedNodes: selectedNodes, + onProgress: (current, total) => setImportProgress({ current, total }), + }); + setImportResults(results); + const importedRids = new Set( + results + .filter((item) => item.status !== "failed") + .map((item) => item.sharedNode.rid), + ); + setNodes((previous) => + previous.map((node) => + importedRids.has(node.sharedNode.rid) + ? { ...node, alreadyImported: true } + : node, + ), + ); + const failedImports = results.filter(isFailedSharedNodeImport); + setSelectedRids( + new Set(failedImports.map((item) => item.sharedNode.rid)), + ); + if (failedImports.length > 0) { + internalError({ + error: new Error( + `${failedImports.length} of ${results.length} shared node imports failed`, + ), + type: IMPORT_ERROR_TYPE, + context: { + operation: IMPORT_ERROR_OPERATION, + failureMessages: failedImports.map((item) => item.message), + }, + sendEmail: false, + }); + } + } catch (importError) { + internalError({ + error: importError, + type: IMPORT_ERROR_TYPE, + context: { operation: IMPORT_ERROR_OPERATION }, + sendEmail: false, + userMessage: + importError instanceof Error + ? importError.message + : "Could not import the selected shared nodes.", + }); + } finally { + setImportProgress(null); + } + }; + return ( void }) => { +
+ + +
diff --git a/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts b/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts index 4f2667b40..9739a514b 100644 --- a/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts +++ b/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts @@ -17,24 +17,13 @@ const sharedNode: SharedNode = { }; describe("toDiscoveredSharedNodes", () => { - it("maps a shared node to the exact discovered shared node shape", () => { + it("wraps each shared node with its imported status", () => { expect( toDiscoveredSharedNodes({ sharedNodes: [sharedNode], importedSourceRids: new Set([sharedNode.rid]), }), - ).toEqual([ - { - alreadyImported: true, - modifiedAt: "2026-06-14T15:00:00.000Z", - sourceApp: "Obsidian", - sourceNodeId: "node-1", - sourceNodeRid: "orn:obsidian.note:vault-a/node-1", - sourceSpaceId: "obsidian:vault-a", - sourceSpaceName: "Research vault", - title: "EVD - REM sleep and recall", - }, - ]); + ).toEqual([{ alreadyImported: true, sharedNode }]); }); it("matches imports by RID rather than source-local ID alone", () => { diff --git a/apps/roam/src/utils/__tests__/importSharedNodes.test.ts b/apps/roam/src/utils/__tests__/importSharedNodes.test.ts new file mode 100644 index 000000000..d89d70400 --- /dev/null +++ b/apps/roam/src/utils/__tests__/importSharedNodes.test.ts @@ -0,0 +1,113 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { DGSupabaseClient } from "@repo/database/lib/client"; +import type { SharedNode } from "@repo/database/lib/sharedNodes"; +import { + importSharedNodes, + isFailedSharedNodeImport, +} from "~/utils/importSharedNodes"; +import { materializeSharedNode } from "~/utils/materializeSharedNode"; + +vi.mock("~/utils/materializeSharedNode", async () => { + const actual = await vi.importActual< + typeof import("~/utils/materializeSharedNode") + >("~/utils/materializeSharedNode"); + return { ...actual, materializeSharedNode: vi.fn() }; +}); + +const mockedMaterializeSharedNode = vi.mocked(materializeSharedNode); + +const client = {} as DGSupabaseClient; + +const makeSharedNode = (sourceLocalId: string): SharedNode => ({ + rid: `orn:obsidian.note:vault-a/${sourceLocalId}`, + sourceLocalId, + spaceId: 20, + spaceName: "Research vault", + spaceUri: "obsidian:vault-a", + platform: "Obsidian", + title: `EVD - ${sourceLocalId}`, + created: "2026-06-14T12:30:00.000Z", + lastModified: "2026-06-14T15:00:00.000Z", + authorId: 7, + directMetadata: null, +}); + +const successResult = ( + sharedNode: SharedNode, + action: "created" | "updated" | "skipped", +) => ({ + success: true as const, + action, + pageUid: `page-${sharedNode.sourceLocalId}`, + sourceModifiedAt: sharedNode.lastModified, + sourceNodeRid: sharedNode.rid, +}); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("importSharedNodes", () => { + it("reports one outcome per node and progress after each", async () => { + const sharedNodes = ["node-1", "node-2", "node-3", "node-4"].map( + makeSharedNode, + ); + mockedMaterializeSharedNode + .mockResolvedValueOnce(successResult(sharedNodes[0], "created")) + .mockResolvedValueOnce(successResult(sharedNodes[1], "updated")) + .mockResolvedValueOnce(successResult(sharedNodes[2], "skipped")) + .mockResolvedValueOnce({ + success: false, + sourceModifiedAt: sharedNodes[3].lastModified, + sourceNodeRid: sharedNodes[3].rid, + error: { message: "title collision", stage: "title-collision" }, + }); + const onProgress = vi.fn(); + + const items = await importSharedNodes({ client, sharedNodes, onProgress }); + + expect(items).toEqual([ + { sharedNode: sharedNodes[0], status: "imported" }, + { sharedNode: sharedNodes[1], status: "imported" }, + { sharedNode: sharedNodes[2], status: "skipped" }, + { + sharedNode: sharedNodes[3], + status: "failed", + message: "title collision", + }, + ]); + expect(items.filter(isFailedSharedNodeImport)).toEqual([items[3]]); + expect(onProgress.mock.calls).toEqual([ + [1, 4], + [2, 4], + [3, 4], + [4, 4], + ]); + expect(mockedMaterializeSharedNode).toHaveBeenNthCalledWith(1, { + client, + sharedNode: sharedNodes[0], + }); + }); + + it("keeps importing the remaining nodes when a materialization throws", async () => { + const sharedNodes = ["node-1", "node-2"].map(makeSharedNode); + mockedMaterializeSharedNode + .mockRejectedValueOnce(new Error("roam api unavailable")) + .mockResolvedValueOnce(successResult(sharedNodes[1], "created")); + + const items = await importSharedNodes({ + client, + sharedNodes, + onProgress: vi.fn(), + }); + + expect(items).toEqual([ + { + sharedNode: sharedNodes[0], + status: "failed", + message: "roam api unavailable", + }, + { sharedNode: sharedNodes[1], status: "imported" }, + ]); + }); +}); diff --git a/apps/roam/src/utils/__tests__/materializeSharedNode.test.ts b/apps/roam/src/utils/__tests__/materializeSharedNode.test.ts index eb562462a..be20076ee 100644 --- a/apps/roam/src/utils/__tests__/materializeSharedNode.test.ts +++ b/apps/roam/src/utils/__tests__/materializeSharedNode.test.ts @@ -7,6 +7,7 @@ import type { DGSupabaseClient } from "@repo/database/lib/client"; import type { SharedNode } from "@repo/database/lib/sharedNodes"; import { findImportedNodeUidBySourceRid, + readImportedSourceIdentity, writeImportedSourceIdentity, } from "~/utils/importedSourceIdentity"; import { materializeSharedNode } from "~/utils/materializeSharedNode"; @@ -23,6 +24,7 @@ vi.mock("roamjs-components/queries/getShallowTreeByParentUid", () => ({ vi.mock("roamjs-components/writes/deleteBlock", () => ({ default: vi.fn() })); vi.mock("~/utils/importedSourceIdentity", () => ({ findImportedNodeUidBySourceRid: vi.fn(), + readImportedSourceIdentity: vi.fn(), writeImportedSourceIdentity: vi.fn(), })); @@ -33,6 +35,7 @@ const mockedDeleteBlock = vi.mocked(deleteBlock); const mockedFindImportedNodeUidBySourceRid = vi.mocked( findImportedNodeUidBySourceRid, ); +const mockedReadImportedSourceIdentity = vi.mocked(readImportedSourceIdentity); const mockedWriteImportedSourceIdentity = vi.mocked( writeImportedSourceIdentity, ); @@ -120,6 +123,7 @@ beforeEach(() => { mockedGetShallowTreeByParentUid.mockReturnValue([]); mockedGetPageUidByPageTitle.mockReturnValue(""); mockedFindImportedNodeUidBySourceRid.mockResolvedValue(null); + mockedReadImportedSourceIdentity.mockReturnValue(undefined); }); describe("materializeSharedNode", () => { @@ -232,6 +236,58 @@ describe("materializeSharedNode", () => { ); }); + it("skips an imported page whose source has not changed", async () => { + const { client, from } = clientWithFullContent({ text: FULL_MARKDOWN }); + mockedFindImportedNodeUidBySourceRid.mockResolvedValue(EXISTING_PAGE_UID); + mockedReadImportedSourceIdentity.mockReturnValue({ + sourceModifiedAt: sharedNode.lastModified, + sourceNodeRid: sharedNode.rid, + }); + + await expect( + materializeSharedNode({ client, sharedNode }), + ).resolves.toEqual({ + success: true, + action: "skipped", + pageUid: EXISTING_PAGE_UID, + sourceModifiedAt: sharedNode.lastModified, + sourceNodeRid: sharedNode.rid, + }); + expect(from).not.toHaveBeenCalled(); + expect(blockFromMarkdown).not.toHaveBeenCalled(); + expect(mockedDeleteBlock).not.toHaveBeenCalled(); + expect(mockedWriteImportedSourceIdentity).not.toHaveBeenCalled(); + }); + + it("updates an imported page whose source changed since the import", async () => { + const { client } = clientWithFullContent({ text: FULL_MARKDOWN }); + mockedFindImportedNodeUidBySourceRid.mockResolvedValue(EXISTING_PAGE_UID); + mockedGetPageTitleByPageUid.mockReturnValue(sharedNode.title); + mockedReadImportedSourceIdentity.mockReturnValue({ + sourceModifiedAt: "2026-06-14T14:00:00.000Z", + sourceNodeRid: sharedNode.rid, + }); + + const result = await materializeSharedNode({ client, sharedNode }); + + expect(result).toMatchObject({ success: true, action: "updated" }); + expect(blockFromMarkdown).toHaveBeenCalled(); + }); + + it("updates an imported page whose stored modified time is invalid", async () => { + const { client } = clientWithFullContent({ text: FULL_MARKDOWN }); + mockedFindImportedNodeUidBySourceRid.mockResolvedValue(EXISTING_PAGE_UID); + mockedGetPageTitleByPageUid.mockReturnValue(sharedNode.title); + mockedReadImportedSourceIdentity.mockReturnValue({ + sourceModifiedAt: "not-a-date", + sourceNodeRid: sharedNode.rid, + }); + + const result = await materializeSharedNode({ client, sharedNode }); + + expect(result).toMatchObject({ success: true, action: "updated" }); + }); + it("renames the imported page when the source title changed", async () => { const { client } = clientWithFullContent({ text: FULL_MARKDOWN }); mockedFindImportedNodeUidBySourceRid.mockResolvedValue(EXISTING_PAGE_UID); diff --git a/apps/roam/src/utils/discoverSharedNodes.ts b/apps/roam/src/utils/discoverSharedNodes.ts index 13d651d81..32ee6d423 100644 --- a/apps/roam/src/utils/discoverSharedNodes.ts +++ b/apps/roam/src/utils/discoverSharedNodes.ts @@ -3,18 +3,11 @@ import { listGroupSharedNodes, type SharedNode, } from "@repo/database/lib/sharedNodes"; -import type { Enums } from "@repo/database/dbTypes"; import { getImportedSourceRids } from "./importedSourceIdentity"; export type DiscoveredSharedNode = { alreadyImported: boolean; - modifiedAt: string; - sourceApp: Enums<"Platform">; - sourceNodeId?: string; - sourceNodeRid: string; - sourceSpaceId: string; - sourceSpaceName: string; - title: string; + sharedNode: SharedNode; }; export const toDiscoveredSharedNodes = ({ @@ -26,13 +19,7 @@ export const toDiscoveredSharedNodes = ({ }): DiscoveredSharedNode[] => sharedNodes.map((sharedNode) => ({ alreadyImported: importedSourceRids.has(sharedNode.rid), - modifiedAt: sharedNode.lastModified, - sourceApp: sharedNode.platform, - sourceNodeId: sharedNode.sourceLocalId || undefined, - sourceNodeRid: sharedNode.rid, - sourceSpaceId: sharedNode.spaceUri, - sourceSpaceName: sharedNode.spaceName, - title: sharedNode.title, + sharedNode, })); export const discoverSharedNodes = async ({ diff --git a/apps/roam/src/utils/importSharedNodes.ts b/apps/roam/src/utils/importSharedNodes.ts new file mode 100644 index 000000000..9557182d0 --- /dev/null +++ b/apps/roam/src/utils/importSharedNodes.ts @@ -0,0 +1,53 @@ +import type { DGSupabaseClient } from "@repo/database/lib/client"; +import type { SharedNode } from "@repo/database/lib/sharedNodes"; +import { + getErrorMessage, + materializeSharedNode, +} from "./materializeSharedNode"; + +export type FailedSharedNodeImport = { + sharedNode: SharedNode; + status: "failed"; + message: string; +}; + +export type SharedNodeImportItem = + | { sharedNode: SharedNode; status: "imported" | "skipped" } + | FailedSharedNodeImport; + +export const isFailedSharedNodeImport = ( + item: SharedNodeImportItem, +): item is FailedSharedNodeImport => item.status === "failed"; + +export const importSharedNodes = async ({ + client, + sharedNodes, + onProgress, +}: { + client: DGSupabaseClient; + sharedNodes: SharedNode[]; + onProgress: (current: number, total: number) => void; +}): Promise => { + const items: SharedNodeImportItem[] = []; + for (const sharedNode of sharedNodes) { + try { + const result = await materializeSharedNode({ client, sharedNode }); + items.push( + result.success + ? { + sharedNode, + status: result.action === "skipped" ? "skipped" : "imported", + } + : { sharedNode, status: "failed", message: result.error.message }, + ); + } catch (error) { + items.push({ + sharedNode, + status: "failed", + message: getErrorMessage(error), + }); + } + onProgress(items.length, sharedNodes.length); + } + return items; +}; diff --git a/apps/roam/src/utils/materializeSharedNode.ts b/apps/roam/src/utils/materializeSharedNode.ts index 5646db8d1..9c9300e39 100644 --- a/apps/roam/src/utils/materializeSharedNode.ts +++ b/apps/roam/src/utils/materializeSharedNode.ts @@ -12,7 +12,9 @@ import getShallowTreeByParentUid from "roamjs-components/queries/getShallowTreeB import deleteBlock from "roamjs-components/writes/deleteBlock"; import { findImportedNodeUidBySourceRid, + readImportedSourceIdentity, writeImportedSourceIdentity, + type ImportedSourceIdentity, } from "./importedSourceIdentity"; type MaterializationStage = @@ -41,7 +43,7 @@ type MaterializationFailure = SourceIdentity & { type MaterializationSuccess = SourceIdentity & { success: true; - action: "created" | "updated"; + action: "created" | "updated" | "skipped"; pageUid: string; }; @@ -67,9 +69,22 @@ type RoamMarkdownApi = { const getRoamMarkdownApi = (): RoamMarkdownApi => window.roamAlphaAPI.data as unknown as RoamMarkdownApi; -const getErrorMessage = (error: unknown): string => +export const getErrorMessage = (error: unknown): string => error instanceof Error ? error.message : String(error); +const isImportUpToDate = ({ + sourceModifiedAt, + storedModifiedAt, +}: { + sourceModifiedAt: string; + storedModifiedAt: string; +}): boolean => { + const storedTime = Date.parse(storedModifiedAt); + return ( + !Number.isNaN(storedTime) && storedTime >= Date.parse(sourceModifiedAt) + ); +}; + const failure = ({ error, identity, @@ -295,19 +310,13 @@ export const materializeSharedNode = async ({ sourceNodeRid: sharedNode.rid, }; - const content = await fetchFullMarkdown({ client, sharedNode }).catch( - (error: unknown) => ({ error: getErrorMessage(error) }), - ); - if ("error" in content) - return failure({ - identity, - message: `Could not fetch the content of "${sharedNode.title}" from "${sharedNode.spaceName}": ${content.error}`, - stage: "fetch-content", - }); - let importedPageUid: string | null; + let storedIdentity: ImportedSourceIdentity | undefined; try { importedPageUid = await findImportedNodeUidBySourceRid(sharedNode.rid); + storedIdentity = importedPageUid + ? readImportedSourceIdentity(importedPageUid) + : undefined; } catch (error) { return failure({ error, @@ -317,6 +326,31 @@ export const materializeSharedNode = async ({ }); } + if ( + importedPageUid && + storedIdentity && + isImportUpToDate({ + sourceModifiedAt: identity.sourceModifiedAt, + storedModifiedAt: storedIdentity.sourceModifiedAt, + }) + ) + return { + ...identity, + success: true, + action: "skipped", + pageUid: importedPageUid, + }; + + const content = await fetchFullMarkdown({ client, sharedNode }).catch( + (error: unknown) => ({ error: getErrorMessage(error) }), + ); + if ("error" in content) + return failure({ + identity, + message: `Could not fetch the content of "${sharedNode.title}" from "${sharedNode.spaceName}": ${content.error}`, + stage: "fetch-content", + }); + return importedPageUid ? updateImportedPage({ identity, From 8f4a15c36f710c20289769d07edf646b0acce57c Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 30 Jul 2026 23:05:17 +0530 Subject: [PATCH 2/4] [ENG-1859] Keep the landed discovered-node shape, add sharedNode additively --- .../components/DiscoverSharedNodesDialog.tsx | 155 +++++++++--------- .../__tests__/discoverSharedNodes.test.ts | 16 +- apps/roam/src/utils/discoverSharedNodes.ts | 15 ++ 3 files changed, 104 insertions(+), 82 deletions(-) diff --git a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx index df0bcf88d..f4d6eedad 100644 --- a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx +++ b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx @@ -26,7 +26,6 @@ import { import internalError from "~/utils/internalError"; import { getLoggedInClient, getSupabaseContext } from "~/utils/supabaseContext"; -const CONNECTION_ERROR_MESSAGE = "Could not connect to shared persistence."; const IMPORT_ERROR_TYPE = "Shared node import failed"; const IMPORT_ERROR_OPERATION = "import-shared-nodes"; @@ -34,7 +33,7 @@ const formatModifiedAt = (modifiedAt: string): string => new Date(modifiedAt).toLocaleString(); const isImportableSharedNode = (node: DiscoveredSharedNode): boolean => - node.sharedNode.platform === "Obsidian"; + node.sourceApp === "Obsidian"; const SharedNodeRow = ({ node, @@ -46,72 +45,68 @@ const SharedNodeRow = ({ selected: boolean; selectionDisabled: boolean; onToggleSelected: () => void; -}) => { - const { sharedNode } = node; - return ( - - - - - - {sharedNode.platform} - - -
- {sharedNode.spaceName} -
+}) => ( + + + + + + {node.sourceApp} + + +
+ {node.sourceSpaceName} +
+
+ {node.sourceSpaceId} +
+ + +
+ {node.title} +
+ + + {node.sourceNodeId ? (
- {sharedNode.spaceUri} + {node.sourceNodeId}
- - -
- {sharedNode.title} -
- - - {sharedNode.sourceLocalId ? ( -
- {sharedNode.sourceLocalId} -
- ) : ( - Not provided - )} - - - {formatModifiedAt(sharedNode.lastModified)} - - - {node.alreadyImported ? ( - - Imported - - ) : ( - Available - )} - - - ); -}; + ) : ( + Not provided + )} + + + {formatModifiedAt(node.modifiedAt)} + + + {node.alreadyImported ? ( + + Imported + + ) : ( + Available + )} + + +); const ImportResultsSummary = ({ results, @@ -169,9 +164,9 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { setImportResults(null); try { const context = await getSupabaseContext(); - if (!context) throw new Error(CONNECTION_ERROR_MESSAGE); + if (!context) throw new Error("Could not connect to shared persistence."); const client = await getLoggedInClient(); - if (!client) throw new Error(CONNECTION_ERROR_MESSAGE); + if (!client) throw new Error("Could not connect to shared persistence."); setNodes( await discoverSharedNodes({ client, @@ -204,18 +199,18 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { if (!normalizedSearch) return nodes; return nodes.filter((node) => [ - node.sharedNode.platform, - node.sharedNode.spaceName, - node.sharedNode.spaceUri, - node.sharedNode.title, - node.sharedNode.sourceLocalId, - ].some((value) => value.toLocaleLowerCase().includes(normalizedSearch)), + node.sourceApp, + node.sourceSpaceName, + node.sourceSpaceId, + node.title, + node.sourceNodeId, + ].some((value) => value?.toLocaleLowerCase().includes(normalizedSearch)), ); }, [nodes, searchTerm]); const importableVisibleRids = visibleNodes .filter(isImportableSharedNode) - .map((node) => node.sharedNode.rid); + .map((node) => node.sourceNodeRid); const allVisibleSelected = importableVisibleRids.length > 0 && importableVisibleRids.every((rid) => selectedRids.has(rid)); @@ -244,14 +239,14 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { const importSelectedNodes = async (): Promise => { const selectedNodes = nodes - .filter((node) => selectedRids.has(node.sharedNode.rid)) + .filter((node) => selectedRids.has(node.sourceNodeRid)) .map((node) => node.sharedNode); setImportResults(null); setImportProgress({ current: 0, total: selectedNodes.length }); try { const client = await getLoggedInClient(); - if (!client) throw new Error(CONNECTION_ERROR_MESSAGE); + if (!client) throw new Error("Could not connect to shared persistence."); const results = await importSharedNodes({ client, sharedNodes: selectedNodes, @@ -265,7 +260,7 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { ); setNodes((previous) => previous.map((node) => - importedRids.has(node.sharedNode.rid) + importedRids.has(node.sourceNodeRid) ? { ...node, alreadyImported: true } : node, ), @@ -389,12 +384,12 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { {visibleNodes.map((node) => ( - toggleNodeSelected(node.sharedNode.rid) + toggleNodeSelected(node.sourceNodeRid) } - selected={selectedRids.has(node.sharedNode.rid)} + selected={selectedRids.has(node.sourceNodeRid)} selectionDisabled={importing} /> ))} diff --git a/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts b/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts index 9739a514b..5892e83c9 100644 --- a/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts +++ b/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts @@ -17,13 +17,25 @@ const sharedNode: SharedNode = { }; describe("toDiscoveredSharedNodes", () => { - it("wraps each shared node with its imported status", () => { + it("maps a shared node to the exact discovered shared node shape", () => { expect( toDiscoveredSharedNodes({ sharedNodes: [sharedNode], importedSourceRids: new Set([sharedNode.rid]), }), - ).toEqual([{ alreadyImported: true, sharedNode }]); + ).toEqual([ + { + alreadyImported: true, + modifiedAt: "2026-06-14T15:00:00.000Z", + sharedNode, + sourceApp: "Obsidian", + sourceNodeId: "node-1", + sourceNodeRid: "orn:obsidian.note:vault-a/node-1", + sourceSpaceId: "obsidian:vault-a", + sourceSpaceName: "Research vault", + title: "EVD - REM sleep and recall", + }, + ]); }); it("matches imports by RID rather than source-local ID alone", () => { diff --git a/apps/roam/src/utils/discoverSharedNodes.ts b/apps/roam/src/utils/discoverSharedNodes.ts index 32ee6d423..ccc6b47cf 100644 --- a/apps/roam/src/utils/discoverSharedNodes.ts +++ b/apps/roam/src/utils/discoverSharedNodes.ts @@ -3,11 +3,19 @@ import { listGroupSharedNodes, type SharedNode, } from "@repo/database/lib/sharedNodes"; +import type { Enums } from "@repo/database/dbTypes"; import { getImportedSourceRids } from "./importedSourceIdentity"; export type DiscoveredSharedNode = { alreadyImported: boolean; + modifiedAt: string; sharedNode: SharedNode; + sourceApp: Enums<"Platform">; + sourceNodeId?: string; + sourceNodeRid: string; + sourceSpaceId: string; + sourceSpaceName: string; + title: string; }; export const toDiscoveredSharedNodes = ({ @@ -19,7 +27,14 @@ export const toDiscoveredSharedNodes = ({ }): DiscoveredSharedNode[] => sharedNodes.map((sharedNode) => ({ alreadyImported: importedSourceRids.has(sharedNode.rid), + modifiedAt: sharedNode.lastModified, sharedNode, + sourceApp: sharedNode.platform, + sourceNodeId: sharedNode.sourceLocalId || undefined, + sourceNodeRid: sharedNode.rid, + sourceSpaceId: sharedNode.spaceUri, + sourceSpaceName: sharedNode.spaceName, + title: sharedNode.title, })); export const discoverSharedNodes = async ({ From c25151e42d68b422b40fc12d0b6f561efee36cd7 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 30 Jul 2026 23:30:31 +0530 Subject: [PATCH 3/4] [ENG-1859] Send the numeric space id as a flat key instead of embedding the shared node --- .../components/DiscoverSharedNodesDialog.tsx | 3 ++- .../__tests__/discoverSharedNodes.test.ts | 21 +++++++++++++++++-- apps/roam/src/utils/discoverSharedNodes.ts | 18 ++++++++++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx index f4d6eedad..f0322cec0 100644 --- a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx +++ b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx @@ -16,6 +16,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import createOverlayRender from "roamjs-components/util/createOverlayRender"; import { discoverSharedNodes, + toSharedNode, type DiscoveredSharedNode, } from "~/utils/discoverSharedNodes"; import { @@ -240,7 +241,7 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { const importSelectedNodes = async (): Promise => { const selectedNodes = nodes .filter((node) => selectedRids.has(node.sourceNodeRid)) - .map((node) => node.sharedNode); + .map(toSharedNode); setImportResults(null); setImportProgress({ current: 0, total: selectedNodes.length }); diff --git a/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts b/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts index 5892e83c9..95f5c8483 100644 --- a/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts +++ b/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { toDiscoveredSharedNodes } from "~/utils/discoverSharedNodes"; +import { + toDiscoveredSharedNodes, + toSharedNode, +} from "~/utils/discoverSharedNodes"; import type { SharedNode } from "@repo/database/lib/sharedNodes"; const sharedNode: SharedNode = { @@ -27,10 +30,10 @@ describe("toDiscoveredSharedNodes", () => { { alreadyImported: true, modifiedAt: "2026-06-14T15:00:00.000Z", - sharedNode, sourceApp: "Obsidian", sourceNodeId: "node-1", sourceNodeRid: "orn:obsidian.note:vault-a/node-1", + sourceSpaceDbId: 20, sourceSpaceId: "obsidian:vault-a", sourceSpaceName: "Research vault", title: "EVD - REM sleep and recall", @@ -38,6 +41,20 @@ describe("toDiscoveredSharedNodes", () => { ]); }); + it("toSharedNode rebuilds every field the materializer reads", () => { + const [discovered] = toDiscoveredSharedNodes({ + sharedNodes: [sharedNode], + importedSourceRids: new Set(), + }); + + expect(toSharedNode(discovered)).toEqual({ + ...sharedNode, + authorId: undefined, + created: null, + directMetadata: null, + }); + }); + it("matches imports by RID rather than source-local ID alone", () => { expect( toDiscoveredSharedNodes({ diff --git a/apps/roam/src/utils/discoverSharedNodes.ts b/apps/roam/src/utils/discoverSharedNodes.ts index ccc6b47cf..216834acb 100644 --- a/apps/roam/src/utils/discoverSharedNodes.ts +++ b/apps/roam/src/utils/discoverSharedNodes.ts @@ -9,10 +9,10 @@ import { getImportedSourceRids } from "./importedSourceIdentity"; export type DiscoveredSharedNode = { alreadyImported: boolean; modifiedAt: string; - sharedNode: SharedNode; sourceApp: Enums<"Platform">; sourceNodeId?: string; sourceNodeRid: string; + sourceSpaceDbId: number; sourceSpaceId: string; sourceSpaceName: string; title: string; @@ -28,15 +28,29 @@ export const toDiscoveredSharedNodes = ({ sharedNodes.map((sharedNode) => ({ alreadyImported: importedSourceRids.has(sharedNode.rid), modifiedAt: sharedNode.lastModified, - sharedNode, sourceApp: sharedNode.platform, sourceNodeId: sharedNode.sourceLocalId || undefined, sourceNodeRid: sharedNode.rid, + sourceSpaceDbId: sharedNode.spaceId, sourceSpaceId: sharedNode.spaceUri, sourceSpaceName: sharedNode.spaceName, title: sharedNode.title, })); +export const toSharedNode = (node: DiscoveredSharedNode): SharedNode => ({ + rid: node.sourceNodeRid, + sourceLocalId: node.sourceNodeId ?? "", + spaceId: node.sourceSpaceDbId, + spaceName: node.sourceSpaceName, + spaceUri: node.sourceSpaceId, + platform: node.sourceApp, + title: node.title, + lastModified: node.modifiedAt, + // materializer never reads these + created: null, + directMetadata: null, +}); + export const discoverSharedNodes = async ({ client, currentSpaceId, From 5dd063b4a403d0efcdb78036ae56298f8b405d47 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 31 Jul 2026 01:35:44 +0530 Subject: [PATCH 4/4] [ENG-1859] Use SharedNode directly and derive already-imported from the rid set --- .../components/DiscoverSharedNodesDialog.tsx | 95 +++++++++---------- .../__tests__/discoverSharedNodes.test.ts | 66 ------------- apps/roam/src/utils/discoverSharedNodes.ts | 53 +---------- 3 files changed, 49 insertions(+), 165 deletions(-) delete mode 100644 apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts diff --git a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx index f0322cec0..4a6b94fbb 100644 --- a/apps/roam/src/components/DiscoverSharedNodesDialog.tsx +++ b/apps/roam/src/components/DiscoverSharedNodesDialog.tsx @@ -14,11 +14,8 @@ import { } from "@blueprintjs/core"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import createOverlayRender from "roamjs-components/util/createOverlayRender"; -import { - discoverSharedNodes, - toSharedNode, - type DiscoveredSharedNode, -} from "~/utils/discoverSharedNodes"; +import type { SharedNode } from "@repo/database/lib/sharedNodes"; +import { discoverSharedNodes } from "~/utils/discoverSharedNodes"; import { importSharedNodes, isFailedSharedNodeImport, @@ -33,16 +30,18 @@ const IMPORT_ERROR_OPERATION = "import-shared-nodes"; const formatModifiedAt = (modifiedAt: string): string => new Date(modifiedAt).toLocaleString(); -const isImportableSharedNode = (node: DiscoveredSharedNode): boolean => - node.sourceApp === "Obsidian"; +const isImportableSharedNode = (node: SharedNode): boolean => + node.platform === "Obsidian"; const SharedNodeRow = ({ node, + alreadyImported, selected, selectionDisabled, onToggleSelected, }: { - node: DiscoveredSharedNode; + node: SharedNode; + alreadyImported: boolean; selected: boolean; selectionDisabled: boolean; onToggleSelected: () => void; @@ -58,11 +57,11 @@ const SharedNodeRow = ({ /> - {node.sourceApp} + {node.platform}
- {node.sourceSpaceName} + {node.spaceName}
- {node.sourceSpaceId} + {node.spaceUri}
@@ -81,24 +80,24 @@ const SharedNodeRow = ({ - {node.sourceNodeId ? ( + {node.sourceLocalId ? (
- {node.sourceNodeId} + {node.sourceLocalId}
) : ( Not provided )} - - {formatModifiedAt(node.modifiedAt)} + + {formatModifiedAt(node.lastModified)} - {node.alreadyImported ? ( + {alreadyImported ? ( Imported @@ -144,7 +143,8 @@ const ImportResultsSummary = ({ }; const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { - const [nodes, setNodes] = useState([]); + const [nodes, setNodes] = useState([]); + const [importedRids, setImportedRids] = useState>(new Set()); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [searchTerm, setSearchTerm] = useState(""); @@ -168,12 +168,12 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { if (!context) throw new Error("Could not connect to shared persistence."); const client = await getLoggedInClient(); if (!client) throw new Error("Could not connect to shared persistence."); - setNodes( - await discoverSharedNodes({ - client, - currentSpaceId: context.spaceId, - }), - ); + const { sharedNodes, importedSourceRids } = await discoverSharedNodes({ + client, + currentSpaceId: context.spaceId, + }); + setNodes(sharedNodes); + setImportedRids(importedSourceRids); } catch (loadError) { internalError({ error: loadError, @@ -200,18 +200,18 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { if (!normalizedSearch) return nodes; return nodes.filter((node) => [ - node.sourceApp, - node.sourceSpaceName, - node.sourceSpaceId, + node.platform, + node.spaceName, + node.spaceUri, node.title, - node.sourceNodeId, - ].some((value) => value?.toLocaleLowerCase().includes(normalizedSearch)), + node.sourceLocalId, + ].some((value) => value.toLocaleLowerCase().includes(normalizedSearch)), ); }, [nodes, searchTerm]); const importableVisibleRids = visibleNodes .filter(isImportableSharedNode) - .map((node) => node.sourceNodeRid); + .map((node) => node.rid); const allVisibleSelected = importableVisibleRids.length > 0 && importableVisibleRids.every((rid) => selectedRids.has(rid)); @@ -239,9 +239,7 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { }; const importSelectedNodes = async (): Promise => { - const selectedNodes = nodes - .filter((node) => selectedRids.has(node.sourceNodeRid)) - .map(toSharedNode); + const selectedNodes = nodes.filter((node) => selectedRids.has(node.rid)); setImportResults(null); setImportProgress({ current: 0, total: selectedNodes.length }); @@ -254,18 +252,14 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { onProgress: (current, total) => setImportProgress({ current, total }), }); setImportResults(results); - const importedRids = new Set( - results - .filter((item) => item.status !== "failed") - .map((item) => item.sharedNode.rid), - ); - setNodes((previous) => - previous.map((node) => - importedRids.has(node.sourceNodeRid) - ? { ...node, alreadyImported: true } - : node, - ), - ); + const newlyImportedRids = results + .filter((item) => item.status !== "failed") + .map((item) => item.sharedNode.rid); + setImportedRids((previous) => { + const next = new Set(previous); + newlyImportedRids.forEach((rid) => next.add(rid)); + return next; + }); const failedImports = results.filter(isFailedSharedNodeImport); setSelectedRids( new Set(failedImports.map((item) => item.sharedNode.rid)), @@ -385,12 +379,11 @@ const DiscoverSharedNodesDialog = ({ onClose }: { onClose: () => void }) => { {visibleNodes.map((node) => ( - toggleNodeSelected(node.sourceNodeRid) - } - selected={selectedRids.has(node.sourceNodeRid)} + alreadyImported={importedRids.has(node.rid)} + onToggleSelected={() => toggleNodeSelected(node.rid)} + selected={selectedRids.has(node.rid)} selectionDisabled={importing} /> ))} diff --git a/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts b/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts deleted file mode 100644 index 95f5c8483..000000000 --- a/apps/roam/src/utils/__tests__/discoverSharedNodes.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - toDiscoveredSharedNodes, - toSharedNode, -} from "~/utils/discoverSharedNodes"; -import type { SharedNode } from "@repo/database/lib/sharedNodes"; - -const sharedNode: SharedNode = { - rid: "orn:obsidian.note:vault-a/node-1", - sourceLocalId: "node-1", - spaceId: 20, - spaceName: "Research vault", - spaceUri: "obsidian:vault-a", - platform: "Obsidian", - title: "EVD - REM sleep and recall", - created: "2026-06-14T12:30:00.000Z", - lastModified: "2026-06-14T15:00:00.000Z", - authorId: 7, - directMetadata: null, -}; - -describe("toDiscoveredSharedNodes", () => { - it("maps a shared node to the exact discovered shared node shape", () => { - expect( - toDiscoveredSharedNodes({ - sharedNodes: [sharedNode], - importedSourceRids: new Set([sharedNode.rid]), - }), - ).toEqual([ - { - alreadyImported: true, - modifiedAt: "2026-06-14T15:00:00.000Z", - sourceApp: "Obsidian", - sourceNodeId: "node-1", - sourceNodeRid: "orn:obsidian.note:vault-a/node-1", - sourceSpaceDbId: 20, - sourceSpaceId: "obsidian:vault-a", - sourceSpaceName: "Research vault", - title: "EVD - REM sleep and recall", - }, - ]); - }); - - it("toSharedNode rebuilds every field the materializer reads", () => { - const [discovered] = toDiscoveredSharedNodes({ - sharedNodes: [sharedNode], - importedSourceRids: new Set(), - }); - - expect(toSharedNode(discovered)).toEqual({ - ...sharedNode, - authorId: undefined, - created: null, - directMetadata: null, - }); - }); - - it("matches imports by RID rather than source-local ID alone", () => { - expect( - toDiscoveredSharedNodes({ - sharedNodes: [sharedNode], - importedSourceRids: new Set(["orn:obsidian.note:another-vault/node-1"]), - })[0]?.alreadyImported, - ).toBe(false); - }); -}); diff --git a/apps/roam/src/utils/discoverSharedNodes.ts b/apps/roam/src/utils/discoverSharedNodes.ts index 216834acb..cdf226df3 100644 --- a/apps/roam/src/utils/discoverSharedNodes.ts +++ b/apps/roam/src/utils/discoverSharedNodes.ts @@ -3,64 +3,21 @@ import { listGroupSharedNodes, type SharedNode, } from "@repo/database/lib/sharedNodes"; -import type { Enums } from "@repo/database/dbTypes"; import { getImportedSourceRids } from "./importedSourceIdentity"; -export type DiscoveredSharedNode = { - alreadyImported: boolean; - modifiedAt: string; - sourceApp: Enums<"Platform">; - sourceNodeId?: string; - sourceNodeRid: string; - sourceSpaceDbId: number; - sourceSpaceId: string; - sourceSpaceName: string; - title: string; -}; - -export const toDiscoveredSharedNodes = ({ - sharedNodes, - importedSourceRids, -}: { - sharedNodes: SharedNode[]; - importedSourceRids: ReadonlySet; -}): DiscoveredSharedNode[] => - sharedNodes.map((sharedNode) => ({ - alreadyImported: importedSourceRids.has(sharedNode.rid), - modifiedAt: sharedNode.lastModified, - sourceApp: sharedNode.platform, - sourceNodeId: sharedNode.sourceLocalId || undefined, - sourceNodeRid: sharedNode.rid, - sourceSpaceDbId: sharedNode.spaceId, - sourceSpaceId: sharedNode.spaceUri, - sourceSpaceName: sharedNode.spaceName, - title: sharedNode.title, - })); - -export const toSharedNode = (node: DiscoveredSharedNode): SharedNode => ({ - rid: node.sourceNodeRid, - sourceLocalId: node.sourceNodeId ?? "", - spaceId: node.sourceSpaceDbId, - spaceName: node.sourceSpaceName, - spaceUri: node.sourceSpaceId, - platform: node.sourceApp, - title: node.title, - lastModified: node.modifiedAt, - // materializer never reads these - created: null, - directMetadata: null, -}); - export const discoverSharedNodes = async ({ client, currentSpaceId, }: { client: DGSupabaseClient; currentSpaceId: number; -}): Promise => { +}): Promise<{ + sharedNodes: SharedNode[]; + importedSourceRids: Set; +}> => { const [sharedNodes, importedSourceRids] = await Promise.all([ listGroupSharedNodes({ client, currentSpaceId }), getImportedSourceRids(), ]); - return toDiscoveredSharedNodes({ sharedNodes, importedSourceRids }); + return { sharedNodes, importedSourceRids }; };