Skip to content

ENG-2019: Extract shared cross-space node discovery into packages/database#1216

Open
sid597 wants to merge 4 commits into
eng-1855-add-roam-shared-node-import-discovery-v2from
eng-2019-extract-shared-cross-space-node-discovery-into
Open

ENG-2019: Extract shared cross-space node discovery into packages/database#1216
sid597 wants to merge 4 commits into
eng-1855-add-roam-shared-node-import-discovery-v2from
eng-2019-extract-shared-cross-space-node-discovery-into

Conversation

@sid597

@sid597 sid597 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Note

Stacked on #1214 (ENG-1855). Base is eng-1855-add-roam-shared-node-import-discovery-v2, so this diff shows only the extraction. Merge after #1214.

Closes ENG-2019.

What

One shared module — @repo/database/lib/sharedNodes — now lists group-visible shared nodes cross-space for both apps:

Old New
Roam discoverSharedNodes.ts: getGroupSharedResources, getSharedNodeRows, buildDiscoveredSharedNodes, chunk, getLatestTimestamp moved to packages/database/src/lib/sharedNodes.ts (listGroupSharedNodes + pure buildSharedNodeCandidates)
Obsidian getPublishedNodesForGroups: inline my_contents query thin wrapper over listGroupSharedNodes, maps to the existing PublishedNode shape

Roam keeps its DiscoveredSharedNode type, discoverSharedNodes entry point, and app-side imported-RID detection (roamAlphaAPI); it maps candidates through a small exported toDiscoveredSharedNodes. Already-imported detection stays app-side in both apps by design.

The one behavior change (Obsidian)

Obsidian's import listing previously returned any cross-space my_contents row (RLS-trust only — its groupIds parameter was dead code — no pagination, no contract check). It now uses the shared semantics extracted from Roam:

  • Contract gate: only nodes with an instance concept (schema_id set), a direct content, and a typed full content are listed.
  • Explicit group gating: candidates come from ResourceAccessgetAvailableGroupIds (RLS stays as defense-in-depth). Whether to move to RLS-trust instead is deliberately out of scope.
  • Pagination: getAllPages + 100-id chunked reads (the old query silently capped at the PostgREST row limit).
  • Minor: modifiedAt now takes the latest timestamp across the concept and both content variants (was: content rows only).

Deviations from a pure code move

  • The my_contents select gains author_id, created, metadata — fields the Obsidian consumer needs.
  • metadata !== null guard in the Obsidian mapper: the old code crashed on a direct row with null metadata (typeof null === "object" passes, then .filePath throws); Roam-origin rows make that case likely now.
  • The Obsidian wrapper re-wraps query failures in Error (the shared module throws raw PostgrestError, which the modal's Notice would render as [object Object]).
  • listGroupSharedNodes takes optional groupIds so the Obsidian modal (which already fetches them for its empty-groups notice) doesn't trigger a second getAvailableGroupIds query. Roam omits it.

Infra

  • packages/database gets a minimal vitest setup mirroring packages/content-model; script is test:unit because cucumber owns test. Roam's pure-builder tests moved over (9 tests); Roam keeps 2 mapper tests.
  • turbo.json + CI gain a test:unit step so the moved tests stay enforced (they previously ran via Roam's CI-filtered suite).
  • pnpm-lock.yaml is a 3-line hand edit reusing the catalog's existing vitest resolution — a regenerated lockfile re-resolved unrelated packages and broke Obsidian's typecheck, so re-resolution was avoided entirely.

Out of scope, unchanged on purpose: the 4 other chunk copies, importPreview.ts (preview-time schema checks are a different concern), and any RLS-vs-explicit gating redesign.

Verification

  • tsc --noEmit clean in packages/database, apps/roam, apps/obsidian; eslint/prettier clean on all touched files (importNodes.ts keeps its 21 pre-existing warnings, count unchanged).
  • 9 shared-module tests + 45 Roam tests green; turbo run test:unit verified locally.
  • Roam built with SUPABASE_USE_DB=local; dist staged at dg-test-eng-2019 for manual verification.

Open in Devin Review

@linear-code

linear-code Bot commented Jul 10, 2026

Copy link
Copy Markdown

ENG-2019

@supabase

supabase Bot commented Jul 10, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
discourse-graph Skipped Skipped Jul 10, 2026 1:50pm

Request Review

@graphite-app

graphite-app Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

PR size/scope check

This PR is over our review-size guideline.

  • Recommended: ~200 lines changed
  • Acceptable limit: up to 400 lines when well-scoped/self-contained
  • Preferred file count: fewer than 5 files

Please split this into smaller PRs unless there is a clear reason the changes need to land together.

If keeping it as one PR, please add a brief justification covering:

  • What single problem this PR solves
  • Why the files/changes are coupled

@sid597 sid597 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer orientation — the four places to read, in order.

};
};

export const listGroupSharedNodes = async ({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entry point. Everything below this file's exports is Roam's ENG-1855 code moved verbatim; the only additions are the three extra my_contents columns (author_id, created, metadata) and the optional groupIds (caller-supplied to avoid a duplicate getAvailableGroupIds query — Obsidian passes it, Roam doesn't).

);
};

export const buildSharedNodeCandidates = ({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pure builder = the shared semantics both apps now use: contract gate (instance concept with schema_id + direct + typed full), exact ResourceAccess identity match, current-space exclusion, newest-first sort. This is the semantics reconciliation — Obsidian previously had none of these gates.

concepts,
contents,
currentSpaceId,
export const toDiscoveredSharedNodes = ({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roam swap: behavior-identical output. What stays app-side is the imported-RID lookup (roamAlphaAPI block props) and this mapper to the existing DiscoveredSharedNode shape (sourceSpaceId was always the space URI — the candidate now carries it directly).

.neq("space_id", currentSpaceId);

if (error) {
const candidates = await listGroupSharedNodes({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obsidian swap — the PR's one behavior change lands here (see description). Same PublishedNode shape out; timestamp conversions (+ "Z") preserved from the old query. Two hardening deltas vs the old code: null-metadata guard (old code threw on metadata: null) and re-wrapping PostgrestError so the modal Notice shows a real message.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

@sid597 sid597 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved-to map for the deleted code — each comment marks where it landed in packages/database/src/lib/sharedNodes.ts and whether it changed.

spaces: SharedSpace[];
};

const getResourceKey = ({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

→ sharedNodes.ts:61. getResourceKey, getLatestTimestamp (L65), chunk (L235), and the row types are all verbatim copies — except SharedContent gains author_id, created, metadata (Obsidian consumer needs them).

);
};

export const buildDiscoveredSharedNodes = ({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

→ sharedNodes.ts:80 as buildSharedNodeCandidates. Gate/sort/identity logic unchanged. Modified: drops importedSourceRids/alreadyImported (now mapped app-side, L21 of this file) and outputs the neutral candidate shape (rid, platform, lastModified, + spaceUri/created/authorId/directMetadata pass-throughs).

);
};

const getGroupSharedResources = async (

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

→ sharedNodes.ts:210. Verbatim except a new optional groupIds param — callers that already fetched group ids (Obsidian modal) skip the internal getAvailableGroupIds refetch.

values.slice(index * size, (index + 1) * size),
);

const getSharedNodeRows = async ({

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

→ sharedNodes.ts:246. Verbatim except the my_contents select adds author_id, created, metadata and it threads groupIds through.


// Query my_contents (RLS applied); exclude current space. Get both variants so we can use
// the latest last_modified per node and prefer "direct" for text (title).
const { data, error } = await client

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not moved — replaced. This query's semantics (any cross-space row, RLS-only, unpaginated) are superseded by the shared module's gated listing; the variant-grouping/direct-preference logic it did by hand is what buildSharedNodeCandidates does behind the gate.

@sid597

sid597 commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

ENG-1855 — addressed in this PR
├── RID correctness: preserve source local IDs that are already https:// or orn: RIDs
├── UTC correctness: normalize PostgREST timestamps before comparison and return canonical ISO values
├── searchTerm / setSearchTerm rename
└── PostHog/internalError reporting for discovery-load failures

ENG-2019 — follow-up in #1216
├── shared discovery query and candidate architecture
├── no full-text transfer during discovery; defer fetching full content until import
├── optional full content, following ENG-2016
├── separate direct-content rows from full-content summaries
├── unique ResourceAccess pagination ordering
├── named shared Platform type backed by Enums<"Platform">
├── evaluate and remove candidate properties that can safely be derived from RID
├── preserve the ENG-1855 RID and UTC behavior during extraction
└── comprehensive shared-module tests for Roam and Obsidian consumers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant