diff --git a/packages/realm-server/tests/search-entries-engine-test.ts b/packages/realm-server/tests/search-entries-engine-test.ts index af2caa1ccb..1f329eb05e 100644 --- a/packages/realm-server/tests/search-entries-engine-test.ts +++ b/packages/realm-server/tests/search-entries-engine-test.ts @@ -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.: sparse items carry meta.sparseFields', async function (assert) { let doc = await testRealm.realmIndexQueryEngine.searchEntries( personQuery({ fields: { 'search-entry': ['item.firstName'] } }), diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index b754adea05..5b7aca7d05 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -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 = { ...pristine, - links: { self: pristine.id }, + id: cardUrl as RealmResourceIdentifier, + links: { self: cardUrl }, }; if (fieldset.item.kind === 'sparse') { item = buildSparseItemResource(item, fieldset.item.fields); @@ -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(); type LayerItem = { @@ -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,