Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 38 additions & 0 deletions packages/realm-server/tests/search-entries-engine-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,44 @@ module(basename(import.meta.filename), function () {
);
});

test('fields[search-entry]=item: the included item id matches the entry even when pristine_doc was stored under a registered-prefix alias', async function (assert) {
// Realms with a registered alias prefix (catalog, base, skills,
// openrouter) store `pristine_doc.id` in that alias form for storage
// portability (`unresolveResourceInstanceURLs` at index time), while
// every other identifier this engine emits — the search-entry itself,
// its `item` relationship, `links.self` — uses the row's resolved URL.
// Simulate that storage shape directly (no need to stand up a fully
// aliased realm) and confirm the served item's identity still matches
// the relationship that points at it, not the stored alias.
await dbAdapter.execute(
`UPDATE boxel_index
SET pristine_doc = jsonb_set(pristine_doc, '{id}', '"@fake-alias/john"'::jsonb)
WHERE url = '${johnId}.json' AND type = 'instance'`,
);
try {
let doc = await testRealm.realmIndexQueryEngine.searchEntries(
personQuery({ fields: { 'search-entry': ['item'] } }),
);
let entry = entryFor(doc, johnId)!;
assert.deepEqual(entry.relationships.item, {
data: { type: 'card', id: johnId },
});
let item = itemIn(doc, johnId);
assert.ok(
item,
'the included item is keyed by the resolved id the relationship points at, not the stored alias',
);
assert.strictEqual(item!.id, johnId);
assert.strictEqual(item!.links?.self, johnId);
} finally {
await dbAdapter.execute(
`UPDATE boxel_index
SET pristine_doc = jsonb_set(pristine_doc, '{id}', '"${johnId}"'::jsonb)
WHERE url = '${johnId}.json' AND type = 'instance'`,
);
}
});

test('fields[search-entry]=item.<field>: sparse items carry meta.sparseFields', async function (assert) {
let doc = await testRealm.realmIndexQueryEngine.searchEntries(
personQuery({ fields: { 'search-entry': ['item.firstName'] } }),
Expand Down
26 changes: 22 additions & 4 deletions packages/runtime-common/realm-index-query-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,9 +408,17 @@ export class RealmIndexQueryEngine {
emitItem = false;
}
if (emitItem && pristine) {
// `pristine_doc` is stored with its id unresolved to the realm's
// registered alias prefix (e.g. `@cardstack/catalog/...`) for
// storage portability. The search-entry's `item` relationship
// always points at `cardUrl` (the row's resolved absolute URL,
// shared with the entry's own id) — the included resource's id
// must match that exactly or a JSON:API consumer's relationship
// lookup silently finds nothing.
let item: CardResource<Saved> = {
...pristine,
links: { self: pristine.id },
id: cardUrl as RealmResourceIdentifier,
links: { self: cardUrl },
Comment on lines +420 to +421

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize loadLinks root identities consistently

When this path is used through Realm.searchEntries, loadLinks is enabled by default (packages/runtime-common/realm.ts:5393-5395). For registered-prefix realms, this new resolved id means the roots are placed in loadLinks's visited/omit sets as resolved URLs, while fetched linked resources and relationshipIdStr are still compared in unresolved alias form (visited.has(linkResource.id), omitSet.has(entry.relationshipIdStr)). If a full item links to itself or to another card that is already a search result, loadLinks no longer recognizes that root and can add the same logical card again under the alias id and rewrite the relationship to that alias, leaving duplicate/mismatched identities in the search-entry document. Normalize both the root ids and the loadLinks comparison keys to the same form when overriding the item id here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Claude Code 🤖] Good catch, confirmed and fixed in 95f05d3. The item's id override left loadLinks's visited/omitSet comparing resolved-URL roots against alias-form ids elsewhere in the same pass. Fixed by indexing both spellings of each root via VirtualNetwork.equivalentURLForms so either form matches.

};
if (fieldset.item.kind === 'sparse') {
item = buildSparseItemResource(item, fieldset.item.fields);
Expand Down Expand Up @@ -1385,8 +1393,16 @@ export class RealmIndexQueryEngine {
let invocationId = `${Date.now().toString(36)}-${Math.random()
.toString(36)
.slice(2, 8)}`;
let realmPath = new RealmPaths(realmURL, this.#realm.virtualNetwork);
let omitSet = new Set(omit);
let vnForIdentity = this.#realm.virtualNetwork;
let realmPath = new RealmPaths(realmURL, vnForIdentity);
// `omit`/root ids may arrive in either resolved-URL or registered-alias
// form (the search-entry item resource's id is always resolved, while a
// fetched linked resource's own pristine_doc-derived id — and the
// relationship pointing at it, `relationshipIdStr` below — stays in
// alias form for storage portability). Index both spellings under one
// key so a root recognized via one form still matches when the same
// card is reached again via the other.
let omitSet = new Set(omit.flatMap((id) => vnForIdentity.equivalentURLForms(id)));
let visited = new Set<string>();

type LayerItem = {
Expand All @@ -1406,7 +1422,9 @@ export class RealmIndexQueryEngine {
if (visited.has(resource.id)) {
continue;
}
visited.add(resource.id);
for (let form of vnForIdentity.equivalentURLForms(resource.id)) {
visited.add(form);
}
}
layer.push({
resource,
Expand Down
Loading