Resolve relative references in serialized card documents against each resource's own id#5578
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f4de21602
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Preview deploymentsHost Test Results 1 files 1 suites 2h 52m 50s ⏱️ Results for commit 0ac14cc. Realm Server Test Results 1 files ±0 1 suites ±0 11m 43s ⏱️ -11s Results for commit 0ac14cc. ± Comparison against earlier commit c46008f. |
fdc7854 to
c46008f
Compare
There was a problem hiding this comment.
Pull request overview
This PR standardizes how relative references inside card resources are interpreted and produced: all resource-local references (including meta.adoptsFrom.module and relationship/instance URLs) are treated as relative to the resource’s own id, regardless of whether the resource is the primary data or arrives via included[].
Changes:
- Update
relativizeResourceto relativize all URLs/module refs against each resource’s own id base (rather than the enclosing document’s base). - Update host-side deserialization (
cardClassFromResource,_updateFromSerialized) to resolveadoptsFromand relationship links against the resource/instance id when available. - Refresh expectations across realm-server + host integration tests and add a new nested-link deserialization test for the reported failure mode.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/runtime-common/realm-index-query-engine.ts | Change relativization base from document primary URL to per-resource URL; expanded contract comments. |
| packages/base/card-serialization.ts | Ensure adoptsFrom.module resolution prefers resource.id over caller context when present. |
| packages/base/card-api.gts | Resolve adoptsFrom and relationship links against the instance’s own id when available; fall back to threaded context otherwise. |
| packages/realm-server/tests/card-endpoints-test.ts | Update served-document expectations for relationship links.self and adoptsFrom.module under the new base rule. |
| packages/host/tests/integration/realm-test.gts | Update integration expectations and clarify included-card adoptsFrom.module semantics. |
| packages/host/tests/integration/realm-indexing-test.gts | Update indexing-related expectations for links.self relative forms. |
| packages/host/tests/integration/components/serialization-test.gts | Add a regression test covering nested linked-card hydration with own-id-relative adoptsFrom. |
| packages/host/tests/integration/components/card-copy-test.gts | Update copied-card included adoptsFrom.module expectation to own-id-relative form. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c46008f0dc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Every reference in a card resource — its adoptsFrom module and its relationship/instance links — is serialized relative to that resource's own id, independent of the document that delivers it. The one rule holds for a document's primary resource, for each included[] resource, and for every member of a collection response, so a resource carries the same relative refs no matter how it is delivered; a consumer inverts it the same way everywhere, resolving a resource's references against its own id (data.id / included[].id). Previously included resources were relativized against the primary document URL, and the host threaded that base down into nested link deserialization, so a linked card in a subdirectory resolved its module one directory off and an existing, cleanly-indexed card surfaced as Card Not Found. - relativizeResource relativizes every reference against the resource's own id. - cardClassFromResource and _updateFromSerialized resolve a resource's adoptsFrom and relationship links against its own id when it is saved, falling back to the caller's base only for an id-less contained field. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
c46008f to
0ac14cc
Compare
richardhjtan
left a comment
There was a problem hiding this comment.
[Claude Code 🤖] Reviewed through the lens of the JSON:API relative-reference contract between the realm server's serializer and the host's deserializer — the part of this codebase where a wrong base silently produces a relative string that still parses, so the failure surfaces later as a runtime "not found" rather than a type error.
Bottom line: no blocking issues. The change is internally consistent with the rest of the system and the new regression test exercises the exact failure mode described.
What lands right
relativizeResource(packages/runtime-common/realm-index-query-engine.ts) now relativizes every reference in a resource — instance/relationship URLs and module deps alike — against that resource's own id rather than the document's primary URL. Its only call site iscardDocument()in the same file, whichrealm.tsuses uniformly for the GET, POST-create, and PATCH-save response bodies, so read and write responses can't drift apart.- Collection responses (
/_search, built insearchEntries()) never went throughrelativizeResource— they servepristine_docclose to as-is — and the indexer resolves a card's ownadoptsFrom.modulerelative to its own id at index time. So the claim that collections "already carry the own-id-relative form natively" checks out: this PR brings the single-cardincluded[]path in line with a convention the rest of the system already followed, rather than introducing a new one. - On the client,
cardClassFromResource(packages/base/card-serialization.ts) and the relationship/override resolution in_updateFromSerialized(packages/base/card-api.gts) flip the same priority: resource id before the threadedrelativeTo. That fallback matters only for id-less contained fields, which correctly keep inheriting the parent's base since they have no id of their own. - The new test in
packages/host/tests/integration/components/serialization-test.gtshand-builds a two-levellinksToMany→linksTochain with own-id-relativeadoptsFrominincluded[]. Under the old priority (relativeTo ?? resource.id) the linked resources would resolve their module against the root card's id instead of their own — reproducing the reported one-directory-off "Card Not Found" — and pass only under the new order. It pins the actual regression, not just document shape.
On the two points already raised and closed in review threads
- The mounted-realm double-rebase conflict was real against the intermediate commit and was resolved by updating the affected test's expectation rather than the resolver. Tracing the fixture's directory layout against
relativizeResource's math confirms the new expected value is the correct relative form, so the reconciliation holds. - The descendant-link round-trip gap (a resource whose id is a path-prefix of a link target loses a segment on resolve, because the URL-form base branch resolves with standard
new URL(ref, base)semantics, which drop a non-slash-terminated base's last segment) reproduces when traced throughrelativeReference/resolveRRIReference, and is correctly assessed as unreachable today since card ids are flat (<CardType>/<name>) — no card id is ever a prefix of another. It predates this PR (the primary resource already relativized its own links against its own id before this change) but this PR does widen which resources run through that math, from just the primary to everyincluded[]resource. Worth a look ifrelativeReferenceis ever asked to round-trip a hierarchical id space; not something this PR needs to address.
No further recommendations — the diff is narrow, the added docstrings state the rule once and point at where it's enforced, and the four surfaces checked (single-card serve, /_search, client deserialization, index-time storage) agree with each other.
Serialized card documents carry relative references — a card's
adoptsFrommodule and its relationship links — and those references were being resolved against different bases depending on how the document was assembled. The single-card path rebasedincluded[]refs onto the primary document URL; the search / prerendered path (which the host actually loads cards through) left them relative to each resource's own id. A consumer can't tell which base a given ref used, so no single resolution rule was correct for both.The concrete symptom: when the host hydrates a linked card out of a parent card's document (the linked resource rides in
included[]), a linked instance that lives in a subdirectory — instances are stored under a<CardType>/container, so their id is a directory deeper than the module that defines them — had its relativeadoptsFrom.moduleresolved against the parent card's base. It landed one directory off, fetched a module URL that doesn't exist, threwcould not find card, and presented an existing, cleanly-indexed card as Card Not Found.The rule
Every reference in a resource — its
adoptsFrommodule and its relationship/instance links — is relative to that resource's own id, independent of the document that delivers it. One rule for the document's primary resource, for eachincluded[]resource, and for every member of a collection response. A consumer inverts it the same way everywhere: resolve a resource's references against that resource's own id (data.id/included[].id), never against the enclosing document.JSON:API gives no explicit guidance here — its relative-URL handling is scoped to
links, andadoptsFromlives inmeta, which the spec treats as opaque; the closest binding authority it defers to, RFC 3986 §5.1, establishes one base per document with no per-embedded-resource base. So the convention is ours to define, and own-id is the one that makes sense and is the only base that works uniformly across single-card documents,included[], and collections (a collection has no single primary to rebase against).Changes
relativizeResourcerelativizes every reference in a resource — module deps and instance/relationship URLs alike — against the resource's own id. For a document's primary resource the base is unchanged; onlyincluded[]/ collection members shift.cardClassFromResourceand_updateFromSerializedresolve a resource'sadoptsFromand relationship links against its own id when saved, falling back to the caller's base only for an id-less contained field.One deliberate artifact of applying the rule uniformly: a resource's own
links.selfis also relativized against its own id, so an included resource's self-link becomes self-referential (e.g../ownerrather than the previous document-relative../dir/owner). Consumers resolve it againstincluded[].id, so it round-trips correctly.Because the wire form of
included[]references changes (own-id-relative rather than document-relative), served-output and save-payload expectations across the suite are updated to match the rule.Test
A new deserialization test hydrates a two-level link graph — a root card whose
linksToManytargets live in a subdirectory, each with a furtherlinksTointo a deeper subdirectory, every linked resource carrying an own-id-relativeadoptsFrom. It fails on the old resolution base (http://test-realm/test/section not found) and passes with the fix.