Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
da4d214
[ENG-1855] Add Roam shared-node import discovery
sid597 Jul 10, 2026
b805ddf
[ENG-1855] Keep discovery dialog open
sid597 Jul 10, 2026
6d2b17a
[ENG-1855] Fix discovery dialog dismissing on keyboard launch
sid597 Jul 10, 2026
aba903d
[ENG-1855] Use getAllPages for ResourceAccess pagination
sid597 Jul 10, 2026
3b91d1b
[ENG-2019] Add shared cross-space node discovery module to packages/d…
sid597 Jul 10, 2026
6dccc48
[ENG-2019] Swap Roam shared-node discovery onto the shared module
sid597 Jul 10, 2026
69fa231
[ENG-2019] Swap Obsidian import listing onto the shared module
sid597 Jul 10, 2026
0f0adf9
[ENG-2019] Narrow Json metadata without any-casts in Obsidian mapper
sid597 Jul 10, 2026
9f7f93e
Avoid racing Supabase context initialization
sid597 Jul 12, 2026
2fa228c
Merge remote-tracking branch 'origin/main' into eng-1855-add-roam-sha…
sid597 Jul 15, 2026
d70f4d1
[ENG-1855] Address shared-node discovery review feedback
sid597 Jul 15, 2026
f35b7ca
[ENG-1855] Match UTC parsing with Obsidian
sid597 Jul 15, 2026
a587350
[ENG-1855] Defer shared discovery refactor to ENG-2019
sid597 Jul 15, 2026
d4071b4
[ENG-1855] Exclude relations from shared-node discovery
sid597 Jul 16, 2026
9d2f8c6
Merge remote-tracking branch 'origin/eng-1855-add-roam-shared-node-im…
sid597 Jul 16, 2026
d4d437d
[ENG-2019] Use the Platform enum for shared discovery types
sid597 Jul 16, 2026
9ab3603
[ENG-2019] Stop fetching full text during shared-node discovery
sid597 Jul 16, 2026
7542d59
[ENG-2019] Filter current space and order uniquely in the ResourceAcc…
sid597 Jul 16, 2026
c4e68cd
[ENG-2019] Alias the Platform enum locally to match existing usage
sid597 Jul 16, 2026
74b6de3
Merge remote-tracking branch 'origin/main' into eng-2019-extract-shar…
sid597 Jul 16, 2026
612e651
[ENG-2019] Address review: drop groupIds param, rename candidate nami…
sid597 Jul 20, 2026
3130758
[ENG-2019] Address review: derive RID directly and drop per-space que…
sid597 Jul 23, 2026
66fe6cb
[ENG-2019] Query shared nodes directly from RLS-scoped views
sid597 Jul 23, 2026
c512f49
[ENG-2019] Rename buildSharedNodes input from concepts to nodes
sid597 Jul 23, 2026
9eb8f9f
[ENG-1856] Store source identity metadata for Roam imported nodes
sid597 Jul 10, 2026
81c7947
[ENG-1856] Address pre-PR review: hoist prop-key literals, type RID v…
sid597 Jul 24, 2026
bcf3362
Merge remote-tracking branch 'origin/main' into eng-1856-store-source…
sid597 Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions apps/roam/src/utils/__tests__/importedSourceIdentity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DISCOURSE_GRAPH_PROP_NAME } from "~/utils/createReifiedBlock";
import {
findImportedNodeUidBySourceRid,
getImportedSourceRids,
IMPORTED_FROM_PROP_KEY,
readImportedSourceIdentity,
writeImportedSourceIdentity,
} from "~/utils/importedSourceIdentity";
import type { json } from "~/utils/getBlockProps";

const SOURCE_NODE_RID = "orn:obsidian.note:vault-a/node-1";
const SOURCE_MODIFIED_AT = "2026-06-14T15:00:00.000Z";
const PAGE_UID = "page-uid";

const propsByUid = new Map<string, Record<string, json>>();
const query = vi.fn();

const setRoamAlphaApi = (): void => {
(globalThis as { window: unknown }).window = {
roamAlphaAPI: {
data: {
async: { q: query },
block: {
update: vi.fn(
({
block,
}: {
block: { props: Record<string, json>; uid: string };
}) => {
propsByUid.set(block.uid, block.props);
},
),
},
},
pull: (_pattern: string, [, uid]: [string, string]) => ({
":block/props": propsByUid.get(uid) ?? {},
}),
},
};
};

beforeEach(() => {
propsByUid.clear();
query.mockReset();
setRoamAlphaApi();
});

describe("imported source identity metadata", () => {
it("reads the source RID without depending on display metadata", () => {
propsByUid.set(PAGE_UID, {
[DISCOURSE_GRAPH_PROP_NAME]: {
[IMPORTED_FROM_PROP_KEY]: {
sourceModifiedAt: SOURCE_MODIFIED_AT,
sourceNodeRid: SOURCE_NODE_RID,
sourceTitle: "Legacy title that may change",
},
},
});

expect(readImportedSourceIdentity(PAGE_UID)).toEqual({
sourceModifiedAt: SOURCE_MODIFIED_AT,
sourceNodeRid: SOURCE_NODE_RID,
});
});

it("returns undefined for missing or malformed source identity", () => {
expect(readImportedSourceIdentity(PAGE_UID)).toBeUndefined();

propsByUid.set(PAGE_UID, {
[DISCOURSE_GRAPH_PROP_NAME]: {
[IMPORTED_FROM_PROP_KEY]: { sourceNodeRid: 123 },
},
});
expect(readImportedSourceIdentity(PAGE_UID)).toBeUndefined();
});

it("writes the source RID and modified time while preserving sibling metadata", () => {
propsByUid.set(PAGE_UID, {
[DISCOURSE_GRAPH_PROP_NAME]: {
"relation-migration": { relationUid: 1718000000000 },
},
"other-extension": { enabled: true },
});

writeImportedSourceIdentity({
pageUid: PAGE_UID,
sourceModifiedAt: SOURCE_MODIFIED_AT,
sourceNodeRid: SOURCE_NODE_RID,
});

expect(readImportedSourceIdentity(PAGE_UID)).toEqual({
sourceModifiedAt: SOURCE_MODIFIED_AT,
sourceNodeRid: SOURCE_NODE_RID,
});
expect(propsByUid.get(PAGE_UID)).toEqual({
[DISCOURSE_GRAPH_PROP_NAME]: {
"relation-migration": { relationUid: 1718000000000 },
[IMPORTED_FROM_PROP_KEY]: {
sourceModifiedAt: SOURCE_MODIFIED_AT,
sourceNodeRid: SOURCE_NODE_RID,
},
},
"other-extension": { enabled: true },
});
});
});

describe("imported source identity lookup", () => {
it("returns the stored RID set used for duplicate prevention", async () => {
query.mockResolvedValue([SOURCE_NODE_RID, 123, null]);

await expect(getImportedSourceRids()).resolves.toEqual(
new Set([SOURCE_NODE_RID]),
);
expect(query).toHaveBeenCalledOnce();
expect(query.mock.calls[0]?.[0]).toContain(":sourceNodeRid");
});

it("finds the imported Roam page by source RID", async () => {
query.mockResolvedValue([[PAGE_UID]]);

await expect(findImportedNodeUidBySourceRid(SOURCE_NODE_RID)).resolves.toBe(
PAGE_UID,
);
expect(query).toHaveBeenCalledWith(
expect.stringContaining(":sourceNodeRid"),
SOURCE_NODE_RID,
);
});

it("returns null when the source RID has not been imported", async () => {
query.mockResolvedValue([]);

await expect(
findImportedNodeUidBySourceRid(SOURCE_NODE_RID),
).resolves.toBeNull();
});
});
17 changes: 1 addition & 16 deletions apps/roam/src/utils/discoverSharedNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import {
type SharedNode,
} from "@repo/database/lib/sharedNodes";
import type { Enums } from "@repo/database/dbTypes";
import { DISCOURSE_GRAPH_PROP_NAME } from "./createReifiedBlock";

const IMPORTED_FROM_PROP_KEY = "importedFrom";
import { getImportedSourceRids } from "./importedSourceIdentity";

export type DiscoveredSharedNode = {
alreadyImported: boolean;
Expand Down Expand Up @@ -37,19 +35,6 @@ export const toDiscoveredSharedNodes = ({
title: sharedNode.title,
}));

const getImportedSourceRids = async (): Promise<Set<string>> => {
const query = `[:find [?rid ...]
:where
[?page :block/props ?props]
[(get ?props :${DISCOURSE_GRAPH_PROP_NAME}) ?dgData]
[(get ?dgData :${IMPORTED_FROM_PROP_KEY}) ?imported]
[(get ?imported :sourceNodeRid) ?rid]]`;
const result = (await window.roamAlphaAPI.data.async.q(query)) as unknown[];
return new Set(
result.filter((rid): rid is string => typeof rid === "string"),
);
};

export const discoverSharedNodes = async ({
client,
currentSpaceId,
Expand Down
97 changes: 97 additions & 0 deletions apps/roam/src/utils/importedSourceIdentity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { Rid } from "@repo/database/crossAppContracts";
import { DISCOURSE_GRAPH_PROP_NAME } from "./createReifiedBlock";
import getBlockProps, { type json } from "./getBlockProps";
import setBlockProps from "./setBlockProps";

export type ImportedSourceIdentity = {
sourceModifiedAt: string;
sourceNodeRid: Rid;
};

export const IMPORTED_FROM_PROP_KEY = "importedFrom";
const SOURCE_NODE_RID_KEY = "sourceNodeRid";
const SOURCE_MODIFIED_AT_KEY = "sourceModifiedAt";

const isJsonObject = (value: json): value is Record<string, json> =>
typeof value === "object" && value !== null && !Array.isArray(value);

const parseImportedSourceIdentity = (
props: Record<string, json>,
): ImportedSourceIdentity | undefined => {
const discourseGraphProps = props[DISCOURSE_GRAPH_PROP_NAME];
if (!isJsonObject(discourseGraphProps)) return undefined;

const importedFrom = discourseGraphProps[IMPORTED_FROM_PROP_KEY];
if (!isJsonObject(importedFrom)) return undefined;

const sourceModifiedAt = importedFrom[SOURCE_MODIFIED_AT_KEY];
const sourceNodeRid = importedFrom[SOURCE_NODE_RID_KEY];
if (typeof sourceModifiedAt !== "string" || typeof sourceNodeRid !== "string")
return undefined;

return { sourceModifiedAt, sourceNodeRid };
};

export const readImportedSourceIdentity = (
pageUid: string,
): ImportedSourceIdentity | undefined =>
parseImportedSourceIdentity(getBlockProps(pageUid));

export const writeImportedSourceIdentity = ({
pageUid,
sourceModifiedAt,
sourceNodeRid,
}: {
pageUid: string;
sourceModifiedAt: string;
sourceNodeRid: string;
}): void => {
const existing = getBlockProps(pageUid)[DISCOURSE_GRAPH_PROP_NAME];
const discourseGraphProps = isJsonObject(existing) ? existing : {};

setBlockProps(pageUid, {
[DISCOURSE_GRAPH_PROP_NAME]: {
...discourseGraphProps,
[IMPORTED_FROM_PROP_KEY]: {
[SOURCE_MODIFIED_AT_KEY]: sourceModifiedAt,
[SOURCE_NODE_RID_KEY]: sourceNodeRid,
},
},
});
};

export const getImportedSourceRids = async (): Promise<Set<string>> => {
const query = `[:find [?rid ...]
:where
[?page :block/props ?props]
[(get ?props :${DISCOURSE_GRAPH_PROP_NAME}) ?dgData]
[(get ?dgData :${IMPORTED_FROM_PROP_KEY}) ?importedFrom]
[(get ?importedFrom :${SOURCE_NODE_RID_KEY}) ?rid]]`;
const result = (await window.roamAlphaAPI.data.async.q(query)) as unknown[];

return new Set(
result.filter((rid): rid is string => typeof rid === "string"),
);
};

export const findImportedNodeUidBySourceRid = async (
sourceNodeRid: string,
): Promise<string | null> => {
const query = `[:find ?uid
:in $ ?sourceNodeRid
:where
[?page :block/uid ?uid]
[?page :block/props ?props]
[(get ?props :${DISCOURSE_GRAPH_PROP_NAME}) ?dgData]
[(get ?dgData :${IMPORTED_FROM_PROP_KEY}) ?importedFrom]
[(get ?importedFrom :${SOURCE_NODE_RID_KEY}) ?sourceNodeRid]]`;
const result = (await window.roamAlphaAPI.data.async.q(
query,
sourceNodeRid,
)) as unknown[];

const [first] = result;
if (!Array.isArray(first)) return null;
const [uid] = first as unknown[];
return typeof uid === "string" ? uid : null;
};