From c6a275fda385d3ba11efd96560da931a1311eff3 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Mon, 26 Jan 2026 18:47:20 -0500 Subject: [PATCH 1/6] =?UTF-8?q?Add=20search=20support=20for=20file?= =?UTF-8?q?=E2=80=91meta=20entries=20in=20index/query=20pipeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/helpers/realm-server-mock/routes.ts | 6 +- .../tests/integration/realm-querying-test.gts | 69 ++++++++++- packages/realm-server/tests/cards/sample.md | 3 + .../tests/realm-endpoints/search-test.ts | 90 +++++++++++++- packages/runtime-common/document-types.ts | 34 ++++++ packages/runtime-common/index-query-engine.ts | 115 +++++++++++++++++- packages/runtime-common/index.ts | 4 + .../realm-index-query-engine.ts | 98 ++++++++++++++- packages/runtime-common/realm.ts | 8 +- packages/runtime-common/search-utils.ts | 14 +-- 10 files changed, 418 insertions(+), 23 deletions(-) create mode 100644 packages/realm-server/tests/cards/sample.md diff --git a/packages/host/tests/helpers/realm-server-mock/routes.ts b/packages/host/tests/helpers/realm-server-mock/routes.ts index c49f9b2df14..c258a39e843 100644 --- a/packages/host/tests/helpers/realm-server-mock/routes.ts +++ b/packages/host/tests/helpers/realm-server-mock/routes.ts @@ -15,7 +15,7 @@ import { } from '@cardstack/runtime-common'; import type { - CardCollectionDocument, + LinkableCollectionDocument, PrerenderedCardCollectionDocument, } from '@cardstack/runtime-common/document-types'; @@ -33,7 +33,7 @@ const TEST_MATRIX_USER = '@testuser:localhost'; type SearchableRealm = { url?: string; - search: (query: Query) => Promise; + search: (query: Query) => Promise; searchPrerendered: ( query: Query, opts: Pick< @@ -315,7 +315,7 @@ function getSearchableRealmForURL( `Remote realm search failed for ${resolvedRealmURL}: ${response.status} ${responseText}`, ); } - return (await response.json()) as CardCollectionDocument; + return (await response.json()) as LinkableCollectionDocument; }, async searchPrerendered(query: Query, opts) { let url = new URL('_search-prerendered', resolvedRealmURL); diff --git a/packages/host/tests/integration/realm-querying-test.gts b/packages/host/tests/integration/realm-querying-test.gts index fcf953d0da8..6bcdd18123e 100644 --- a/packages/host/tests/integration/realm-querying-test.gts +++ b/packages/host/tests/integration/realm-querying-test.gts @@ -577,7 +577,11 @@ module(`Integration | realm querying`, function (hooks) { hooks.beforeEach(async function () { let { realm } = await setupIntegrationTestRealm({ mockMatrixUtils, - contents: sampleCards, + contents: { + ...sampleCards, + 'files/sample.txt': 'Hello world', + 'files/sample.md': 'Hello markdown', + }, }); queryEngine = realm.realmIndexQueryEngine; }); @@ -719,6 +723,69 @@ module(`Integration | realm querying`, function (hooks) { ); }); + test('can search for file-meta entries by FileDef type', async function (assert) { + let fileDefRef = { module: `${baseRealm.url}file-api`, name: 'FileDef' }; + let result = (await queryEngine.search({ + filter: { + type: fileDefRef, + }, + })) as unknown as { + data: { id?: string; type: string }[]; + }; + let fileEntry = result.data.find( + (entry) => entry.id === `${testRealmURL}files/sample.txt`, + ); + assert.ok(fileEntry, 'file-meta entry is returned'); + assert.strictEqual( + fileEntry?.type, + 'file-meta', + 'search results include file-meta resource', + ); + }); + + test('can search for file-meta entries by url', async function (assert) { + let fileDefRef = { module: `${baseRealm.url}file-api`, name: 'FileDef' }; + let targetUrl = `${testRealmURL}files/sample.txt`; + let result = (await queryEngine.search({ + filter: { + on: fileDefRef, + eq: { url: targetUrl }, + }, + })) as unknown as { + data: { id?: string; type: string }[]; + }; + assert.deepEqual( + result.data.map((entry) => entry.id), + [targetUrl], + 'filters file-meta entries by url', + ); + assert.strictEqual( + result.data[0]?.type, + 'file-meta', + 'url filter returns file-meta resource', + ); + }); + + test('can search for file-meta entries by FileDef subclass type', async function (assert) { + let markdownRef = { + module: `${baseRealm.url}markdown-file-def`, + name: 'MarkdownDef', + }; + let result = (await queryEngine.search({ + filter: { + type: markdownRef, + }, + })) as unknown as { + data: { id?: string; type: string }[]; + }; + assert.ok( + result.data.some( + (entry) => entry.id === `${testRealmURL}files/sample.md`, + ), + 'returns file-meta entries for subclass FileDef type', + ); + }); + test('can combine multiple filters', async function (assert) { let { data: matching } = await queryEngine.search({ filter: { diff --git a/packages/realm-server/tests/cards/sample.md b/packages/realm-server/tests/cards/sample.md new file mode 100644 index 00000000000..fd551cf4a68 --- /dev/null +++ b/packages/realm-server/tests/cards/sample.md @@ -0,0 +1,3 @@ +# Sample + +Hello markdown diff --git a/packages/realm-server/tests/realm-endpoints/search-test.ts b/packages/realm-server/tests/realm-endpoints/search-test.ts index b98105f98fa..7bdd31f7faf 100644 --- a/packages/realm-server/tests/realm-endpoints/search-test.ts +++ b/packages/realm-server/tests/realm-endpoints/search-test.ts @@ -1,7 +1,7 @@ import { module, test } from 'qunit'; import type { Test, SuperTest } from 'supertest'; import { basename } from 'path'; -import type { Realm } from '@cardstack/runtime-common'; +import { baseRealm, type Realm } from '@cardstack/runtime-common'; import type { Query } from '@cardstack/runtime-common/query'; import { setupPermissionedRealm, createJWT } from '../helpers'; import '@cardstack/runtime-common/helpers/code-equality-assertion'; @@ -39,6 +39,18 @@ module(`realm-endpoints/${basename(__filename)}`, function () { }; } + function buildFileDefQuery(): Query { + return { + filter: { + type: { + module: `${baseRealm.url}file-api`, + name: 'FileDef', + }, + }, + }; + } + + module('QUERY request (public realm)', function (_hooks) { let query = () => buildPersonQuery('Mango'); @@ -178,6 +190,82 @@ module(`realm-endpoints/${basename(__filename)}`, function () { 'different pages should return different results', ); }); + + test('serves file-meta results when querying for FileDef', async function (assert) { + let response = await request + .post(searchPath) + .set('Accept', 'application/vnd.card+json') + .set('X-HTTP-Method-Override', 'QUERY') + .send(buildFileDefQuery()); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body as { data: { id?: string; type: string }[] }; + + assert.ok(json.data.length > 0, 'file-meta results are returned'); + assert.ok( + json.data.every((entry) => entry.type === 'file-meta'), + 'all results are file-meta resources', + ); + assert.ok( + json.data.some((entry) => entry.id === `${realmHref}dir/foo.txt`), + 'expected file-meta entry is present', + ); + }); + + test('filters file-meta results by url', async function (assert) { + let response = await request + .post(searchPath) + .set('Accept', 'application/vnd.card+json') + .set('X-HTTP-Method-Override', 'QUERY') + .send({ + filter: { + on: { + module: `${baseRealm.url}file-api`, + name: 'FileDef', + }, + eq: { + url: `${realmHref}dir/foo.txt`, + }, + }, + }); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body as { data: { id?: string; type: string }[] }; + + assert.deepEqual( + json.data.map((entry) => entry.id), + [`${realmHref}dir/foo.txt`], + 'url filter returns matching file-meta entry', + ); + assert.strictEqual( + json.data[0]?.type, + 'file-meta', + 'url filter returns file-meta resource', + ); + }); + + test('filters file-meta results by FileDef subclass type', async function (assert) { + let response = await request + .post(searchPath) + .set('Accept', 'application/vnd.card+json') + .set('X-HTTP-Method-Override', 'QUERY') + .send({ + filter: { + type: { + module: `${baseRealm.url}markdown-file-def`, + name: 'MarkdownDef', + }, + }, + }); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body as { data: { id?: string; type: string }[] }; + + assert.ok( + json.data.some((entry) => entry.id === `${realmHref}sample.md`), + 'returns file-meta entries for subclass FileDef type', + ); + }); }); }); diff --git a/packages/runtime-common/document-types.ts b/packages/runtime-common/document-types.ts index 3b3f1ab41bc..b946baac9ed 100644 --- a/packages/runtime-common/document-types.ts +++ b/packages/runtime-common/document-types.ts @@ -40,6 +40,12 @@ export interface FileMetaCollectionDocument { meta: QueryResultsMeta; } +export interface LinkableCollectionDocument { + data: (CardResource | FileMetaResource)[]; + included?: (FileMetaResource | CardResource)[]; + meta: QueryResultsMeta; +} + export type CardDocument = SingleCardDocument | CardCollectionDocument; export type FileMetaDocument = | SingleFileMetaDocument @@ -101,6 +107,34 @@ export function isCardCollectionDocument( return data.every((resource) => isCardResource(resource)); } +export function isFileMetaCollectionDocument( + doc: any, +): doc is FileMetaCollectionDocument { + if (typeof doc !== 'object' || doc == null) { + return false; + } + if (!('data' in doc)) { + return false; + } + let { data } = doc; + if (!Array.isArray(data)) { + return false; + } + if ('included' in doc) { + let { included } = doc; + if (!isIncluded(included)) { + return false; + } + } + return data.every((resource) => isFileMetaResource(resource)); +} + +export function isLinkableCollectionDocument( + doc: any, +): doc is LinkableCollectionDocument { + return isCardCollectionDocument(doc) || isFileMetaCollectionDocument(doc); +} + export function isPrerenderedCardCollectionDocument( doc: any, ): doc is PrerenderedCardCollectionDocument { diff --git a/packages/runtime-common/index-query-engine.ts b/packages/runtime-common/index-query-engine.ts index e2e6e50c5a8..a946a349744 100644 --- a/packages/runtime-common/index-query-engine.ts +++ b/packages/runtime-common/index-query-engine.ts @@ -360,6 +360,10 @@ export class IndexQueryEngine { types, display_names: displayNames, } = maybeResult; + realmVersion = + typeof realmVersion === 'string' + ? parseInt(realmVersion) + : (realmVersion ?? 0); return { type: 'file', canonicalURL, @@ -377,6 +381,52 @@ export class IndexQueryEngine { }; } + async hasFileType( + realmURL: URL, + ref: CodeRef, + opts?: GetEntryOptions, + ): Promise { + if (!isResolvedCodeRef(ref)) { + return false; + } + let typeKey = internalKeyFor(ref, undefined); + let rows = (await this.#query([ + 'SELECT 1', + `FROM ${tableFromOpts(opts)} AS i ${tableValuedFunctionsPlaceholder}`, + 'WHERE', + ...every([ + ['i.realm_url =', param(realmURL.href)], + ['i.type =', param('file')], + [tableValuedEach('types'), '=', param(typeKey)], + ]), + 'LIMIT 1', + ] as Expression)) as unknown as { 1: number }[]; + return rows.length > 0; + } + + async hasInstanceType( + realmURL: URL, + ref: CodeRef, + opts?: GetEntryOptions, + ): Promise { + if (!isResolvedCodeRef(ref)) { + return false; + } + let typeKey = internalKeyFor(ref, undefined); + let rows = (await this.#query([ + 'SELECT 1', + `FROM ${tableFromOpts(opts)} AS i ${tableValuedFunctionsPlaceholder}`, + 'WHERE', + ...every([ + ['i.realm_url =', param(realmURL.href)], + ['i.type =', param('instance')], + [tableValuedEach('types'), '=', param(typeKey)], + ]), + 'LIMIT 1', + ] as Expression)) as unknown as { 1: number }[]; + return rows.length > 0; + } + private async getDefinition(codeRef: CodeRef): Promise { if (!isResolvedCodeRef(codeRef)) { throw new Error( @@ -395,6 +445,7 @@ export class IndexQueryEngine { { filter, sort, page }: Query, opts: QueryOptions, selectClauseExpression: CardExpression, + entryType: 'instance' | 'file' = 'instance', ): Promise<{ meta: QueryResultsMeta; results: Partial[]; @@ -406,17 +457,21 @@ export class IndexQueryEngine { ]; if (opts.includeErrors) { - conditions.push(['i.type =', param('instance')]); + conditions.push(['i.type =', param(entryType)]); } else { conditions.push( every([ - ['i.type =', param('instance')], + ['i.type =', param(entryType)], any([['i.has_error = FALSE'], ['i.has_error IS NULL']]), ]), ); } - if (opts.cardUrls && opts.cardUrls.length > 0) { + if ( + entryType === 'instance' && + opts.cardUrls && + opts.cardUrls.length > 0 + ) { conditions.push([ 'i.url IN', ...addExplicitParens( @@ -486,6 +541,7 @@ export class IndexQueryEngine { [ 'SELECT url, ANY_VALUE(pristine_doc) AS pristine_doc, ANY_VALUE(error_doc) AS error_doc', ], + 'instance', ); let cards = results @@ -495,6 +551,58 @@ export class IndexQueryEngine { return { cards, meta }; } + async searchFiles( + realmURL: URL, + { filter, sort, page }: Query, + opts: QueryOptions = {}, + ): Promise<{ files: IndexedFile[]; meta: QueryResultsMeta }> { + let { results, meta } = await this._search( + realmURL, + { filter, sort, page }, + opts, + [ + 'SELECT url, ANY_VALUE(pristine_doc) AS pristine_doc, ANY_VALUE(search_doc) AS search_doc, ANY_VALUE(types) AS types, ANY_VALUE(display_names) AS display_names, ANY_VALUE(deps) AS deps, ANY_VALUE(last_modified) AS last_modified, ANY_VALUE(resource_created_at) AS resource_created_at, ANY_VALUE(realm_version) AS realm_version, ANY_VALUE(realm_url) AS realm_url, ANY_VALUE(indexed_at) AS indexed_at', + ], + 'file', + ); + + let files = results.map((result) => this.fileEntryFromResult(result)); + return { files, meta }; + } + + private fileEntryFromResult(result: Partial): IndexedFile { + let canonicalURL = result.url; + if (!canonicalURL) { + throw new Error('expected file search result to include url'); + } + let lastModified = + typeof result.last_modified === 'string' + ? parseInt(result.last_modified) + : (result.last_modified ?? null); + let resourceCreatedAt = + typeof result.resource_created_at === 'string' + ? parseInt(result.resource_created_at) + : (result.resource_created_at ?? null); + let indexedAt = + typeof result.indexed_at === 'string' + ? parseInt(result.indexed_at) + : (result.indexed_at ?? null); + return { + type: 'file', + canonicalURL, + searchDoc: (result.search_doc as Record | null) ?? null, + resource: (result.pristine_doc as FileMetaResource | null) ?? null, + types: (result.types as string[] | null) ?? null, + displayNames: (result.display_names as string[] | null) ?? null, + deps: (result.deps as string[] | null) ?? null, + lastModified, + resourceCreatedAt, + realmVersion: result.realm_version ?? 0, + realmURL: result.realm_url ?? '', + indexedAt, + }; + } + private generalFieldSortColumn(field: string) { let mappedField = generalSortFields[field]; if (mappedField) { @@ -565,6 +673,7 @@ export class IndexQueryEngine { ' as used_render_type,', 'ANY_VALUE(deps) as deps', ], + 'instance', )) as { meta: QueryResultsMeta; results: (Partial & { diff --git a/packages/runtime-common/index.ts b/packages/runtime-common/index.ts index 2be0c95e246..edcb97bbedb 100644 --- a/packages/runtime-common/index.ts +++ b/packages/runtime-common/index.ts @@ -257,6 +257,8 @@ export type { SingleCardDocument, SingleFileMetaDocument, CardCollectionDocument, + FileMetaCollectionDocument, + LinkableCollectionDocument, } from './document-types'; export type { CardResource, @@ -275,6 +277,8 @@ export { isCardCollectionDocument, isSingleCardDocument, isSingleFileMetaDocument, + isFileMetaCollectionDocument, + isLinkableCollectionDocument, isCardDocumentString, } from './document-types'; export { diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index 6111bd50a11..3ace568d6f5 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -27,14 +27,23 @@ import { import type { Realm } from './realm'; import { FILE_META_RESERVED_KEYS } from './realm'; import { RealmPaths } from './paths'; -import type { Query } from './query'; +import type { Filter, Query } from './query'; import { CardError, type SerializedError } from './error'; -import { isCodeRef, isResolvedCodeRef, visitModuleDeps } from './code-ref'; +import { + isCodeRef, + isResolvedCodeRef, + ResolvedCodeRef, + visitModuleDeps, + type CodeRef, +} from './code-ref'; import { isCardCollectionDocument, isSingleCardDocument, type SingleCardDocument, type CardCollectionDocument, + type FileMetaCollectionDocument, + type LinkableCollectionDocument, + isLinkableCollectionDocument, } from './document-types'; import { relationshipEntries } from './relationship-utils'; import type { CardResource, FileMetaResource, Saved } from './resource-types'; @@ -121,7 +130,45 @@ export class RealmIndexQueryEngine { return new URL(this.#realm.url); } - async search(query: Query, opts?: Options): Promise { + async search( + query: Query, + opts?: Options, + ): Promise { + if (await this.queryTargetsFileMeta(query.filter, opts)) { + let { files, meta } = await this.#indexQueryEngine.searchFiles( + new URL(this.#realm.url), + query, + opts, + ); + let data = files.map((fileEntry) => + fileResourceFromIndex(new URL(fileEntry.canonicalURL), fileEntry), + ); + let doc: FileMetaCollectionDocument = { + data, + meta, + }; + + let omit = doc.data.map((r) => r.id).filter(Boolean) as string[]; + if (opts?.loadLinks) { + let included: (CardResource | FileMetaResource)[] = []; + for (let resource of doc.data) { + included = await this.loadLinks( + { + realmURL: this.realmURL, + resource, + omit, + included, + }, + opts, + ); + } + if (included.length > 0) { + doc.included = included; + } + } + return doc; + } + let doc: CardCollectionDocument; let { cards: data, meta } = await this.#indexQueryEngine.search( new URL(this.#realm.url), @@ -160,6 +207,30 @@ export class RealmIndexQueryEngine { return doc; } + private async queryTargetsFileMeta( + filter: Filter | undefined, + opts?: Options, + ): Promise { + if (!filter) { + return false; + } + let refs: CodeRef[] = []; + collectFilterRefs(filter, refs); + let fileMatch = false; + let instanceMatch = false; + for (let ref of refs) { + if (await this.#indexQueryEngine.hasFileType(this.realmURL, ref, opts)) { + fileMatch = true; + } + if ( + await this.#indexQueryEngine.hasInstanceType(this.realmURL, ref, opts) + ) { + instanceMatch = true; + } + } + return fileMatch && !instanceMatch; + } + async fetchCardTypeSummary() { let results = await this.#indexQueryEngine.fetchCardTypeSummary( new URL(this.#realm.url), @@ -537,7 +608,7 @@ export class RealmIndexQueryEngine { }; } let json = await response.json(); - if (!isCardCollectionDocument(json)) { + if (!isLinkableCollectionDocument(json)) { return { cards: [], error: { @@ -726,6 +797,25 @@ export class RealmIndexQueryEngine { } } +function collectFilterRefs(filter: Filter, refs: CodeRef[]) { + let filterWithType = filter as { type?: CodeRef; on?: CodeRef }; + if (filterWithType.type) { + refs.push(filterWithType.type); + } + if (filterWithType.on) { + refs.push(filterWithType.on); + } + if ('every' in filter) { + filter.every.forEach((inner) => collectFilterRefs(inner, refs)); + } + if ('any' in filter) { + filter.any.forEach((inner) => collectFilterRefs(inner, refs)); + } + if ('not' in filter) { + collectFilterRefs(filter.not, refs); + } +} + function relativizeDocument(doc: SingleCardDocument, realmURL: URL): void { let primarySelf = doc.data.links?.self ?? doc.data.id; if (!primarySelf) { diff --git a/packages/runtime-common/realm.ts b/packages/runtime-common/realm.ts index 2ffe301e64a..323bfd05fa4 100644 --- a/packages/runtime-common/realm.ts +++ b/packages/runtime-common/realm.ts @@ -4,7 +4,7 @@ import { transformResultsToPrerenderedCardsDoc, type SingleCardDocument, type SingleFileMetaDocument, - type CardCollectionDocument, + type LinkableCollectionDocument, type PrerenderedCardCollectionDocument, } from './document-types'; import { isMeta, type CardResource, type Relationship } from './resource-types'; @@ -2965,9 +2965,9 @@ export class Realm { return this.#realmIndexUpdater.isIgnored(url); } - public async search(cardsQuery: Query): Promise { - assertQuery(cardsQuery); - return await this.#realmIndexQueryEngine.search(cardsQuery, { + public async search(query: Query): Promise { + assertQuery(query); + return await this.#realmIndexQueryEngine.search(query, { loadLinks: true, }); } diff --git a/packages/runtime-common/search-utils.ts b/packages/runtime-common/search-utils.ts index 30e31546b84..a2ebc03297e 100644 --- a/packages/runtime-common/search-utils.ts +++ b/packages/runtime-common/search-utils.ts @@ -6,7 +6,7 @@ import { type PrerenderedHtmlFormat, } from './prerendered-html-format'; import type { - CardCollectionDocument, + LinkableCollectionDocument, PrerenderedCardCollectionDocument, } from './document-types'; import { SupportedMimeType } from './router'; @@ -253,13 +253,13 @@ export function parsePrerenderedSearchRequestFromPayload(payload: unknown): { } export function combineSearchResults( - docs: CardCollectionDocument[], -): CardCollectionDocument { - let combined: CardCollectionDocument = { + docs: LinkableCollectionDocument[], +): LinkableCollectionDocument { + let combined: LinkableCollectionDocument = { data: [], meta: { page: { total: 0 } }, }; - let included: NonNullable = []; + let included: NonNullable = []; let includedById = new Set(); for (let doc of docs) { @@ -313,14 +313,14 @@ export function combinePrerenderedSearchResults( } type SearchableRealm = { - search: (query: Query) => Promise; + search: (query: Query) => Promise; url?: string; }; export async function searchRealms( realms: Array, query: Query, -): Promise { +): Promise { let realmEntries = realms .filter((realm): realm is SearchableRealm => Boolean(realm)) .map((realm) => ({ From b4630817db69c350c959cd3b58c8250f7ab2a28c Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 27 Jan 2026 19:37:19 -0500 Subject: [PATCH 2/6] Fix test --- packages/realm-server/tests/realm-endpoints-test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/realm-server/tests/realm-endpoints-test.ts b/packages/realm-server/tests/realm-endpoints-test.ts index 4ddfafa7e09..ec261bf94f8 100644 --- a/packages/realm-server/tests/realm-endpoints-test.ts +++ b/packages/realm-server/tests/realm-endpoints-test.ts @@ -1531,6 +1531,14 @@ module(basename(__filename), function () { kind: 'file', }, }, + 'sample.md': { + links: { + related: `${testRealmHref}sample.md`, + }, + meta: { + kind: 'file', + }, + }, 'timers-card.gts': { links: { related: `${testRealmHref}timers-card.gts`, From 9c1ea1d7e29533284597d9d29f9537573685dda9 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 27 Jan 2026 19:37:34 -0500 Subject: [PATCH 3/6] Linting and refactoring --- .../code-submode/card-playground-test.gts | 8 +- .../code-submode/field-playground-test.gts | 4 +- .../tests/integration/realm-indexing-test.gts | 2 +- .../tests/integration/realm-querying-test.gts | 86 ++--- .../host/tests/integration/realm-test.gts | 6 +- .../tests/unit/index-query-engine-test.ts | 347 ++++++++++-------- packages/realm-server/tests/indexing-test.ts | 16 +- packages/runtime-common/index-query-engine.ts | 2 +- .../realm-index-query-engine.ts | 87 ++--- packages/runtime-common/realm.ts | 29 +- 10 files changed, 304 insertions(+), 283 deletions(-) diff --git a/packages/host/tests/acceptance/code-submode/card-playground-test.gts b/packages/host/tests/acceptance/code-submode/card-playground-test.gts index 1ae90dee5aa..2401e94c10d 100644 --- a/packages/host/tests/acceptance/code-submode/card-playground-test.gts +++ b/packages/host/tests/acceptance/code-submode/card-playground-test.gts @@ -1381,7 +1381,7 @@ module('Acceptance | code-submode | card playground', function (_hooks) { 'recent file count is correct', ); - let { data: results } = await realm.realmIndexQueryEngine.search({ + let { data: results } = await realm.realmIndexQueryEngine.searchCards({ filter: { type: { module: `${testRealmURL}person`, name: 'Person' } }, }); assert.strictEqual(results.length, 0); @@ -1407,7 +1407,7 @@ module('Acceptance | code-submode | card playground', function (_hooks) { .dom('[data-option-index]') .exists({ count: 1 }, 'new card shows up in instance chooser dropdown'); - ({ data: results } = await realm.realmIndexQueryEngine.search({ + ({ data: results } = await realm.realmIndexQueryEngine.searchCards({ filter: { type: { module: `${testRealmURL}person`, name: 'Person' } }, })); assert.strictEqual(results.length, 1); @@ -1417,7 +1417,7 @@ module('Acceptance | code-submode | card playground', function (_hooks) { test('does not autogenerate card instance if one exists in the realm but is not in recent cards', async function (assert) { removeRecentFiles(); const cardId = `${testRealmURL}Person/pet-mango`; - let { data: results } = await realm.realmIndexQueryEngine.search({ + let { data: results } = await realm.realmIndexQueryEngine.searchCards({ filter: { type: { module: `${testRealmURL}person`, name: 'Pet' } }, }); assert.strictEqual(results.length, 1); @@ -1436,7 +1436,7 @@ module('Acceptance | code-submode | card playground', function (_hooks) { assert.dom('[data-option-index]').exists({ count: 1 }); assert.dom('[data-option-index="0"]').containsText('Mango'); - ({ data: results } = await realm.realmIndexQueryEngine.search({ + ({ data: results } = await realm.realmIndexQueryEngine.searchCards({ filter: { type: { module: `${testRealmURL}person`, name: 'Pet' } }, })); assert.strictEqual(results.length, 1); diff --git a/packages/host/tests/acceptance/code-submode/field-playground-test.gts b/packages/host/tests/acceptance/code-submode/field-playground-test.gts index 8166e0bc48a..c8aa5273e4f 100644 --- a/packages/host/tests/acceptance/code-submode/field-playground-test.gts +++ b/packages/host/tests/acceptance/code-submode/field-playground-test.gts @@ -1188,7 +1188,7 @@ module('Acceptance | code-submode | field playground', function (_hooks) { test('can autogenerate new Spec and field instance (no preexisting Spec)', async function (assert) { let queryEngine = realm.realmIndexQueryEngine; - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: specRef, eq: { @@ -1210,7 +1210,7 @@ module('Acceptance | code-submode | field playground', function (_hooks) { assertFieldExists(assert, 'edit'); assert.dom('[data-test-field="quote"] input').hasNoValue(); - ({ data: matching } = await queryEngine.search({ + ({ data: matching } = await queryEngine.searchCards({ filter: { on: specRef, eq: { diff --git a/packages/host/tests/integration/realm-indexing-test.gts b/packages/host/tests/integration/realm-indexing-test.gts index b0856a10842..a61a1ec0f9b 100644 --- a/packages/host/tests/integration/realm-indexing-test.gts +++ b/packages/host/tests/integration/realm-indexing-test.gts @@ -134,7 +134,7 @@ module(`Integration | realm indexing`, function (hooks) { }, }); let queryEngine = realm.realmIndexQueryEngine; - let { data: cards } = await queryEngine.search({}); + let { data: cards } = await queryEngine.searchCards({}); assert.deepEqual(cards, [ { id: `${testRealmURL}empty`, diff --git a/packages/host/tests/integration/realm-querying-test.gts b/packages/host/tests/integration/realm-querying-test.gts index 6bcdd18123e..5d64675f795 100644 --- a/packages/host/tests/integration/realm-querying-test.gts +++ b/packages/host/tests/integration/realm-querying-test.gts @@ -587,7 +587,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can search for cards by using the 'eq' filter`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}post`, name: 'Post' }, eq: { cardTitle: 'Card 1', cardDescription: 'Sample post' }, @@ -600,7 +600,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can use 'eq' to find empty values`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}booking`, name: 'Booking' }, eq: { 'posts.author.lastName': null }, @@ -613,7 +613,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can use 'eq' to find missing values`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}type-examples`, @@ -629,7 +629,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can use 'eq' to find empty containsMany field and missing containsMany field`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}type-examples`, @@ -645,7 +645,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can use 'eq' to find empty linksToMany field and missing linksToMany field`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}friends`, @@ -661,7 +661,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can use 'eq' to find empty linksTo field`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}friend`, @@ -677,7 +677,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can search for cards by using a computed field`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}post`, name: 'Post' }, eq: { 'author.fullName': 'Carl Stack' }, @@ -690,7 +690,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can search for cards by using a linksTo field', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}friend`, name: 'Friend' }, eq: { 'friend.firstName': 'Mango' }, @@ -703,7 +703,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can search for cards that have code-ref queryableValue`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${baseRealm.url}spec`, @@ -725,7 +725,7 @@ module(`Integration | realm querying`, function (hooks) { test('can search for file-meta entries by FileDef type', async function (assert) { let fileDefRef = { module: `${baseRealm.url}file-api`, name: 'FileDef' }; - let result = (await queryEngine.search({ + let result = (await queryEngine.searchCards({ filter: { type: fileDefRef, }, @@ -746,7 +746,7 @@ module(`Integration | realm querying`, function (hooks) { test('can search for file-meta entries by url', async function (assert) { let fileDefRef = { module: `${baseRealm.url}file-api`, name: 'FileDef' }; let targetUrl = `${testRealmURL}files/sample.txt`; - let result = (await queryEngine.search({ + let result = (await queryEngine.searchCards({ filter: { on: fileDefRef, eq: { url: targetUrl }, @@ -771,7 +771,7 @@ module(`Integration | realm querying`, function (hooks) { module: `${baseRealm.url}markdown-file-def`, name: 'MarkdownDef', }; - let result = (await queryEngine.search({ + let result = (await queryEngine.searchCards({ filter: { type: markdownRef, }, @@ -787,7 +787,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can combine multiple filters', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}post`, @@ -804,7 +804,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can handle a filter with double negatives', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}post`, name: 'Post' }, not: { not: { not: { eq: { 'author.firstName': 'Carl' } } } }, @@ -817,7 +817,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can filter by card type', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { type: { module: `${testModuleRealm}article`, name: 'Article' }, }, @@ -829,7 +829,7 @@ module(`Integration | realm querying`, function (hooks) { ); matching = ( - await queryEngine.search({ + await queryEngine.searchCards({ filter: { type: { module: `${testModuleRealm}post`, name: 'Post' }, }, @@ -843,7 +843,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can filter on a card's own fields using range`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}post`, name: 'Post' }, range: { @@ -860,7 +860,7 @@ module(`Integration | realm querying`, function (hooks) { test(`can filter on a nested field inside a containsMany using 'range'`, async function (assert) { { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}booking`, name: 'Booking' }, range: { @@ -875,7 +875,7 @@ module(`Integration | realm querying`, function (hooks) { ); } { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}booking`, name: 'Booking' }, range: { @@ -891,7 +891,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can use an eq filter with a date field', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}event`, name: 'Event' }, eq: { @@ -906,7 +906,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can filter on a nested field using 'eq'`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}post`, name: 'Post' }, eq: { 'author.firstName': 'Carl' }, @@ -920,7 +920,7 @@ module(`Integration | realm querying`, function (hooks) { test(`can filter on a nested field inside a containsMany using 'eq'`, async function (assert) { { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}booking`, name: 'Booking' }, eq: { 'hosts.firstName': 'Arthur' }, @@ -933,7 +933,7 @@ module(`Integration | realm querying`, function (hooks) { ); } { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}booking`, name: 'Booking' }, eq: { 'hosts.firstName': null }, @@ -942,7 +942,7 @@ module(`Integration | realm querying`, function (hooks) { assert.strictEqual(matching.length, 0, 'eq on null hosts.firstName'); } { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}booking`, name: 'Booking' }, eq: { @@ -958,7 +958,7 @@ module(`Integration | realm querying`, function (hooks) { ); } { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}booking`, name: 'Booking' }, eq: { @@ -977,7 +977,7 @@ module(`Integration | realm querying`, function (hooks) { test(`can filter on an array of primitive fields inside a containsMany using 'eq'`, async function (assert) { { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}booking`, @@ -993,7 +993,7 @@ module(`Integration | realm querying`, function (hooks) { ); } { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}booking`, @@ -1009,7 +1009,7 @@ module(`Integration | realm querying`, function (hooks) { ); } { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}booking`, @@ -1030,7 +1030,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can negate a filter', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}article`, name: 'Article' }, not: { eq: { 'author.firstName': 'Carl' } }, @@ -1043,7 +1043,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can combine multiple types', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { any: [ { @@ -1068,7 +1068,7 @@ module(`Integration | realm querying`, function (hooks) { // sorting test('can sort in alphabetical order', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ sort: [ { by: 'author.lastName', @@ -1086,7 +1086,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can sort in reverse alphabetical order', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ sort: [ { by: 'author.firstName', @@ -1109,7 +1109,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can sort by card display name (card type shown in the interface)', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ sort: [ { on: baseCardRef, @@ -1155,7 +1155,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can sort by multiple string field conditions in given directions', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ sort: [ { by: 'author.lastName', @@ -1184,7 +1184,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can sort by number value', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ sort: [ { by: 'editions', @@ -1211,7 +1211,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can sort by date', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ sort: [ { by: 'pubDate', @@ -1239,7 +1239,7 @@ module(`Integration | realm querying`, function (hooks) { }); test('can sort by mixed field types', async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ sort: [ { by: 'editions', @@ -1267,7 +1267,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can sort on multiple paths in combination with 'any' filter`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ sort: [ { by: 'author.lastName', @@ -1310,7 +1310,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can sort on multiple paths in combination with 'every' filter`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ sort: [ { by: 'author.firstName', @@ -1341,7 +1341,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can search for cards by using the 'contains' filter`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { contains: { cardTitle: 'ca' }, }, @@ -1360,7 +1360,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can search on specific card by using 'contains' filter`, async function (assert) { - let { data: personMatchingByTitle } = await queryEngine.search({ + let { data: personMatchingByTitle } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}person`, name: 'Person' }, contains: { cardTitle: 'ca' }, @@ -1372,7 +1372,7 @@ module(`Integration | realm querying`, function (hooks) { [`${paths.url}person-card1`, `${paths.url}person-card2`], ); - let { data: dogMatchingByFirstName } = await queryEngine.search({ + let { data: dogMatchingByFirstName } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}dog`, name: 'Dog' }, contains: { firstName: 'go' }, @@ -1386,7 +1386,7 @@ module(`Integration | realm querying`, function (hooks) { }); test(`can use 'contains' filter to find 'null' values`, async function (assert) { - let { data: matching } = await queryEngine.search({ + let { data: matching } = await queryEngine.searchCards({ filter: { on: { module: `${testModuleRealm}dog`, name: 'Dog' }, contains: { cardTitle: null }, diff --git a/packages/host/tests/integration/realm-test.gts b/packages/host/tests/integration/realm-test.gts index b0d6b5f095c..e711987221a 100644 --- a/packages/host/tests/integration/realm-test.gts +++ b/packages/host/tests/integration/realm-test.gts @@ -1014,7 +1014,7 @@ module('Integration | realm', function (hooks) { 'field value is correct', ); - let { data: cards } = await queryEngine.search({ + let { data: cards } = await queryEngine.searchCards({ filter: { on: { module: `http://localhost:4202/test/person`, @@ -2743,7 +2743,7 @@ module('Integration | realm', function (hooks) { let queryEngine = realm.realmIndexQueryEngine; - let { data: cards } = await queryEngine.search({}); + let { data: cards } = await queryEngine.searchCards({}); assert.strictEqual(cards.length, 2, 'two cards found'); let result = await queryEngine.cardDocument( @@ -2794,7 +2794,7 @@ module('Integration | realm', function (hooks) { 'card 1 is still there', ); - cards = (await queryEngine.search({})).data; + cards = (await queryEngine.searchCards({})).data; assert.strictEqual(cards.length, 1, 'only one card remains'); }); diff --git a/packages/host/tests/unit/index-query-engine-test.ts b/packages/host/tests/unit/index-query-engine-test.ts index bd6be2bf057..2929fa2b903 100644 --- a/packages/host/tests/unit/index-query-engine-test.ts +++ b/packages/host/tests/unit/index-query-engine-test.ts @@ -245,7 +245,7 @@ module('Unit | query', function (hooks) { let { mango, vangogh, paper } = testCards; await setupIndex(dbAdapter, [mango, vangogh, paper]); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), {}, ); @@ -269,7 +269,10 @@ module('Unit | query', function (hooks) { { card: paper, data: { is_deleted: true } }, ]); - let { meta } = await indexQueryEngine.search(new URL(testRealmURL), {}); + let { meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + {}, + ); assert.strictEqual(meta.page.total, 2, 'the total results meta is correct'); }); @@ -309,7 +312,7 @@ module('Unit | query', function (hooks) { error_doc: undefined, }, ]); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), {}, ); @@ -327,7 +330,7 @@ module('Unit | query', function (hooks) { await setupIndex(dbAdapter, [mango, vangogh, paper]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { type } }, ); @@ -351,7 +354,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -407,7 +410,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -449,7 +452,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -493,7 +496,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -531,7 +534,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -579,7 +582,7 @@ module('Unit | query', function (hooks) { let type = await personCardType(testCards); { - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -597,7 +600,7 @@ module('Unit | query', function (hooks) { assert.deepEqual(getIds(results), [mango.id], 'results are correct'); } { - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -615,7 +618,7 @@ module('Unit | query', function (hooks) { assert.deepEqual(getIds(results), [vangogh.id], 'results are correct'); } { - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -658,7 +661,7 @@ module('Unit | query', function (hooks) { ]); let type = await SimpleSpecType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -707,7 +710,7 @@ module('Unit | query', function (hooks) { ]); let type = await eventType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -757,7 +760,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -805,7 +808,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -853,7 +856,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -901,7 +904,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -939,7 +942,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -975,7 +978,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -1031,7 +1034,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -1082,7 +1085,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -1103,15 +1106,18 @@ module('Unit | query', function (hooks) { test(`returns empty results when query refers to missing card`, async function (assert) { await setupIndex(dbAdapter, []); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: { - module: `${testRealmURL}nonexistent`, - name: 'Nonexistent', + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { + on: { + module: `${testRealmURL}nonexistent`, + name: 'Nonexistent', + }, + eq: { nonExistentField: 'hello' }, }, - eq: { nonExistentField: 'hello' }, }, - }); + ); assert.strictEqual(cards.length, 0, 'no cards are returned'); assert.strictEqual(meta.page.total, 0, 'total count is zero'); @@ -1122,7 +1128,7 @@ module('Unit | query', function (hooks) { let type = await personCardType(testCards); try { - await indexQueryEngine.search(new URL(testRealmURL), { + await indexQueryEngine.searchCards(new URL(testRealmURL), { filter: { on: type, eq: { @@ -1166,7 +1172,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -1210,7 +1216,7 @@ module('Unit | query', function (hooks) { let type = await personCardType(testCards); { - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -1228,7 +1234,7 @@ module('Unit | query', function (hooks) { assert.deepEqual(getIds(results), [mango.id], 'results are correct'); } { - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -1275,7 +1281,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -1319,7 +1325,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -1360,7 +1366,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -1392,7 +1398,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -1444,7 +1450,7 @@ module('Unit | query', function (hooks) { ); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -1501,7 +1507,7 @@ module('Unit | query', function (hooks) { ); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { filter: { @@ -1545,7 +1551,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { sort: [ @@ -1595,7 +1601,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results, meta } = await indexQueryEngine.search( + let { cards: results, meta } = await indexQueryEngine.searchCards( new URL(testRealmURL), { sort: [ @@ -1646,7 +1652,7 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards: results } = await indexQueryEngine.search( + let { cards: results } = await indexQueryEngine.searchCards( new URL(testRealmURL), { sort: [ @@ -1710,12 +1716,15 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: type, - range: { age: { gt: 25 } }, + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { + on: type, + range: { age: { gt: 25 } }, + }, }, - }); + ); assert.strictEqual(meta.page.total, 2, 'the total results meta is correct'); assert.deepEqual( @@ -1770,13 +1779,16 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: type, - type, - range: { age: { gt: 25 } }, + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { + on: type, + type, + range: { age: { gt: 25 } }, + }, }, - }); + ); assert.strictEqual(meta.page.total, 2, 'the total results meta is correct'); assert.deepEqual( @@ -1816,13 +1828,16 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: type, - type, - eq: { name: 'Mango' }, + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { + on: type, + type, + eq: { name: 'Mango' }, + }, }, - }); + ); assert.strictEqual(meta.page.total, 1, 'the total results meta is correct'); assert.deepEqual(getIds(cards), [mango.id], 'results are correct'); @@ -1867,13 +1882,16 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: type, - type, - contains: { 'address.city': 'Barks' }, + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { + on: type, + type, + contains: { 'address.city': 'Barks' }, + }, }, - }); + ); assert.strictEqual(meta.page.total, 2, 'the total results meta is correct'); assert.deepEqual( @@ -1913,22 +1931,25 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: type, - type, - not: { - eq: { name: 'Mango' }, - }, - }, - sort: [ - { + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { on: type, - by: 'name', - direction: 'asc', + type, + not: { + eq: { name: 'Mango' }, + }, }, - ], - }); + sort: [ + { + on: type, + by: 'name', + direction: 'asc', + }, + ], + }, + ); assert.strictEqual(meta.page.total, 2, 'the total results meta is correct'); assert.deepEqual( @@ -1993,13 +2014,16 @@ module('Unit | query', function (hooks) { let personType = await personCardType(testCards); let catType = internalKeyToCodeRef([...(await getTypes(paper))].shift()!); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: personType, - type: catType, - range: { age: { gt: 25 } }, + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { + on: personType, + type: catType, + range: { age: { gt: 25 } }, + }, }, - }); + ); assert.strictEqual(meta.page.total, 2, 'the total results meta is correct'); assert.deepEqual( @@ -2054,19 +2078,22 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: type, - range: { age: { gte: 25 } }, - }, - sort: [ - { + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { on: type, - by: 'age', - direction: 'desc', + range: { age: { gte: 25 } }, }, - ], - }); + sort: [ + { + on: type, + by: 'age', + direction: 'desc', + }, + ], + }, + ); assert.strictEqual(meta.page.total, 3, 'the total results meta is correct'); assert.deepEqual( @@ -2121,16 +2148,19 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: type, - range: { - 'address.number': { - gt: 100, + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { + on: type, + range: { + 'address.number': { + gt: 100, + }, }, }, }, - }); + ); assert.strictEqual(meta.page.total, 2, 'the total results meta is correct'); assert.deepEqual( @@ -2173,16 +2203,19 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: type, - range: { - lotteryNumbers: { - gt: 50, + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { + on: type, + range: { + lotteryNumbers: { + gt: 50, + }, }, }, }, - }); + ); assert.strictEqual(meta.page.total, 2, 'the total results meta is correct'); assert.deepEqual( @@ -2237,16 +2270,19 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: type, - range: { - 'friends.age': { - gt: 25, + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { + on: type, + range: { + 'friends.age': { + gt: 25, + }, }, }, }, - }); + ); assert.strictEqual(meta.page.total, 2, 'the total results meta is correct'); assert.deepEqual( @@ -2301,19 +2337,22 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: type, - range: { age: { lt: 35 } }, - }, - sort: [ - { + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { on: type, - by: 'age', - direction: 'desc', + range: { age: { lt: 35 } }, }, - ], - }); + sort: [ + { + on: type, + by: 'age', + direction: 'desc', + }, + ], + }, + ); assert.strictEqual(meta.page.total, 2, 'the total results meta is correct'); assert.deepEqual( @@ -2368,19 +2407,22 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: type, - range: { age: { lte: 35 } }, - }, - sort: [ - { + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { on: type, - by: 'age', - direction: 'desc', + range: { age: { lte: 35 } }, }, - ], - }); + sort: [ + { + on: type, + by: 'age', + direction: 'desc', + }, + ], + }, + ); assert.strictEqual(meta.page.total, 3, 'the total results meta is correct'); assert.deepEqual( @@ -2435,19 +2477,22 @@ module('Unit | query', function (hooks) { ]); let type = await personCardType(testCards); - let { cards, meta } = await indexQueryEngine.search(new URL(testRealmURL), { - filter: { - on: type, - range: { age: { gt: 25, lt: 35 } }, - }, - sort: [ - { + let { cards, meta } = await indexQueryEngine.searchCards( + new URL(testRealmURL), + { + filter: { on: type, - by: 'age', - direction: 'desc', + range: { age: { gt: 25, lt: 35 } }, }, - ], - }); + sort: [ + { + on: type, + by: 'age', + direction: 'desc', + }, + ], + }, + ); assert.strictEqual(meta.page.total, 1, 'the total results meta is correct'); assert.deepEqual(getIds(cards), [vangogh.id], 'results are correct'); @@ -2499,7 +2544,7 @@ module('Unit | query', function (hooks) { let type = await personCardType(testCards); assert.rejects( - indexQueryEngine.search(new URL(testRealmURL), { + indexQueryEngine.searchCards(new URL(testRealmURL), { filter: { on: type, range: { age: { gt: null } }, diff --git a/packages/realm-server/tests/indexing-test.ts b/packages/realm-server/tests/indexing-test.ts index ca983533a68..47e890eb2a0 100644 --- a/packages/realm-server/tests/indexing-test.ts +++ b/packages/realm-server/tests/indexing-test.ts @@ -1100,7 +1100,7 @@ module(basename(__filename), function () { } as LooseSingleCardDocument), ); - let { data: result } = await realm.realmIndexQueryEngine.search({ + let { data: result } = await realm.realmIndexQueryEngine.searchCards({ filter: { on: { module: `${testRealm}person`, name: 'Person' }, eq: { firstName: 'Mang-Mang' }, @@ -1135,7 +1135,7 @@ module(basename(__filename), function () { export class Intentionally Thrown Error {} `, ); - let { data: result } = await realm.realmIndexQueryEngine.search({ + let { data: result } = await realm.realmIndexQueryEngine.searchCards({ filter: { type: { module: `${testRealm}person`, name: 'Person' }, }, @@ -1157,7 +1157,7 @@ module(basename(__filename), function () { `, ); result = ( - await realm.realmIndexQueryEngine.search({ + await realm.realmIndexQueryEngine.searchCards({ filter: { type: { module: `${testRealm}person`, name: 'Person' }, }, @@ -1453,7 +1453,7 @@ module(basename(__filename), function () { test('can incrementally index deleted instance', async function (assert) { await realm.delete('mango.json'); - let { data: result } = await realm.realmIndexQueryEngine.search({ + let { data: result } = await realm.realmIndexQueryEngine.searchCards({ filter: { on: { module: `${testRealm}person`, name: 'Person' }, eq: { firstName: 'Mango' }, @@ -1508,7 +1508,7 @@ module(basename(__filename), function () { `, ); - let { data: result } = await realm.realmIndexQueryEngine.search({ + let { data: result } = await realm.realmIndexQueryEngine.searchCards({ filter: { on: { module: `${testRealm}post`, name: 'Post' }, eq: { nickName: 'Van Gogh-poo' }, @@ -1541,7 +1541,7 @@ module(basename(__filename), function () { `, ); - let { data: result } = await realm.realmIndexQueryEngine.search({ + let { data: result } = await realm.realmIndexQueryEngine.searchCards({ filter: { on: { module: `${testRealm}post`, name: 'Post' }, eq: { 'author.nickName': 'Van Gogh-poo' }, @@ -1553,7 +1553,7 @@ module(basename(__filename), function () { test('can incrementally index instance that depends on deleted card source', async function (assert) { await realm.delete('post.gts'); { - let { data: result } = await realm.realmIndexQueryEngine.search({ + let { data: result } = await realm.realmIndexQueryEngine.searchCards({ filter: { type: { module: `${testRealm}post`, name: 'Post' }, }, @@ -1608,7 +1608,7 @@ module(basename(__filename), function () { `, ); { - let { data: result } = await realm.realmIndexQueryEngine.search({ + let { data: result } = await realm.realmIndexQueryEngine.searchCards({ filter: { on: { module: `${testRealm}post`, name: 'Post' }, eq: { nickName: 'Van Gogh-poo' }, diff --git a/packages/runtime-common/index-query-engine.ts b/packages/runtime-common/index-query-engine.ts index a946a349744..ef25dff6b4e 100644 --- a/packages/runtime-common/index-query-engine.ts +++ b/packages/runtime-common/index-query-engine.ts @@ -527,7 +527,7 @@ export class IndexQueryEngine { } } - async search( + async searchCards( realmURL: URL, { filter, sort, page }: Query, opts: QueryOptions = {}, diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index 3ace568d6f5..e4863beba95 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -32,16 +32,12 @@ import { CardError, type SerializedError } from './error'; import { isCodeRef, isResolvedCodeRef, - ResolvedCodeRef, visitModuleDeps, type CodeRef, } from './code-ref'; import { - isCardCollectionDocument, isSingleCardDocument, type SingleCardDocument, - type CardCollectionDocument, - type FileMetaCollectionDocument, type LinkableCollectionDocument, isLinkableCollectionDocument, } from './document-types'; @@ -130,64 +126,44 @@ export class RealmIndexQueryEngine { return new URL(this.#realm.url); } - async search( + async searchCards( query: Query, opts?: Options, ): Promise { + let doc: LinkableCollectionDocument; + if (await this.queryTargetsFileMeta(query.filter, opts)) { let { files, meta } = await this.#indexQueryEngine.searchFiles( new URL(this.#realm.url), query, opts, ); - let data = files.map((fileEntry) => - fileResourceFromIndex(new URL(fileEntry.canonicalURL), fileEntry), + doc = { + data: files.map((fileEntry) => + fileResourceFromIndex(new URL(fileEntry.canonicalURL), fileEntry), + ), + meta, + }; + } else { + let { cards, meta } = await this.#indexQueryEngine.searchCards( + new URL(this.#realm.url), + query, + opts, ); - let doc: FileMetaCollectionDocument = { - data, + doc = { + data: cards.map((resource) => ({ + ...resource, + ...{ links: { self: resource.id } }, + })), meta, }; - - let omit = doc.data.map((r) => r.id).filter(Boolean) as string[]; - if (opts?.loadLinks) { - let included: (CardResource | FileMetaResource)[] = []; - for (let resource of doc.data) { - included = await this.loadLinks( - { - realmURL: this.realmURL, - resource, - omit, - included, - }, - opts, - ); - } - if (included.length > 0) { - doc.included = included; - } - } - return doc; } - let doc: CardCollectionDocument; - let { cards: data, meta } = await this.#indexQueryEngine.search( - new URL(this.#realm.url), - query, - opts, - ); - doc = { - data: data.map((resource) => ({ - ...resource, - ...{ links: { self: resource.id } }, - })), - meta, - }; - - let omit = doc.data.map((r) => r.id).filter(Boolean) as string[]; // TODO eventually the links will be cached in the index, and this will only // fill in the included resources for links that were not cached (e.g. // volatile fields) if (opts?.loadLinks) { + let omit = doc.data.map((r) => r.id).filter(Boolean) as string[]; let included: (CardResource | FileMetaResource)[] = []; for (let resource of doc.data) { included = await this.loadLinks( @@ -386,7 +362,7 @@ export class RealmIndexQueryEngine { realmURL: URL; opts?: Options; }): Promise<{ - results: CardResource[]; + results: (CardResource | FileMetaResource)[]; errors: QueryFieldErrorDetail[]; searchURL: string; }> { @@ -409,14 +385,14 @@ export class RealmIndexQueryEngine { let { query, realm } = normalized; let searchURL = buildQuerySearchURL(realm, query); - let aggregated: CardResource[] = []; + let aggregated: (CardResource | FileMetaResource)[] = []; let seen = new Set(); let errors: QueryFieldErrorDetail[] = []; - let realmResults: CardResource[] = []; + let realmResults: (CardResource | FileMetaResource)[] = []; if (realm === this.realmURL.href) { try { - let collection = await this.#indexQueryEngine.search( + let collection = await this.#indexQueryEngine.searchCards( this.realmURL, query, opts, @@ -445,12 +421,12 @@ export class RealmIndexQueryEngine { realmResults = remoteResult.cards; } - for (let card of realmResults) { - if (!card?.id || seen.has(card.id)) { + for (let result of realmResults) { + if (!result?.id || seen.has(result.id)) { continue; } - seen.add(card.id); - aggregated.push(card); + seen.add(result.id); + aggregated.push(result); } if ( @@ -486,7 +462,7 @@ export class RealmIndexQueryEngine { fieldDefinition: FieldDefinition; fieldName: string; resource: LooseCardResource | FileMetaResource; - results: CardResource[]; + results: (CardResource | FileMetaResource)[]; errors: QueryFieldErrorDetail[]; searchURL: string; }): void { @@ -574,7 +550,10 @@ export class RealmIndexQueryEngine { private async fetchRemoteQueryResults( realmHref: string, query: Query, - ): Promise<{ cards: CardResource[]; error?: QueryFieldErrorDetail }> { + ): Promise<{ + cards: (CardResource | FileMetaResource)[]; + error?: QueryFieldErrorDetail; + }> { try { let searchURL = buildQuerySearchURL(realmHref, query); let { realm, realms, ...queryWithoutRealm } = query as Query & { diff --git a/packages/runtime-common/realm.ts b/packages/runtime-common/realm.ts index 323bfd05fa4..8829893ff22 100644 --- a/packages/runtime-common/realm.ts +++ b/packages/runtime-common/realm.ts @@ -2967,7 +2967,7 @@ export class Realm { public async search(query: Query): Promise { assertQuery(query); - return await this.#realmIndexQueryEngine.search(query, { + return await this.#realmIndexQueryEngine.searchCards(query, { loadLinks: true, }); } @@ -3095,23 +3095,20 @@ export class Realm { } public async searchPrerendered( - cardsQuery: Query, + query: Query, opts: { htmlFormat: PrerenderedHtmlFormat; cardUrls?: string[]; renderType?: ResolvedCodeRef; }, ): Promise { - assertQuery(cardsQuery); - let results = await this.#realmIndexQueryEngine.searchPrerendered( - cardsQuery, - { - htmlFormat: opts.htmlFormat, - cardUrls: opts.cardUrls, - renderType: opts.renderType, - includeErrors: true, - }, - ); + assertQuery(query); + let results = await this.#realmIndexQueryEngine.searchPrerendered(query, { + htmlFormat: opts.htmlFormat, + cardUrls: opts.cardUrls, + renderType: opts.renderType, + includeErrors: true, + }); return transformResultsToPrerenderedCardsDoc(results); } @@ -3124,7 +3121,7 @@ export class Realm { let htmlFormat: string | undefined; let cardUrls: string[] | string | undefined; let renderType: CodeRef | undefined; - let cardsQuery: unknown; + let query: unknown; if (request.method !== 'QUERY') { return createResponse({ @@ -3172,7 +3169,7 @@ export class Realm { delete payload.prerenderedHtmlFormat; delete payload.cardUrls; delete payload.renderType; - cardsQuery = payload; + query = payload; if (!isValidPrerenderedHtmlFormat(htmlFormat)) { return createResponse({ @@ -3222,7 +3219,7 @@ export class Realm { : undefined; try { - assertQuery(cardsQuery); + assertQuery(query); } catch (e) { if (e instanceof InvalidQueryError) { return createResponse({ @@ -3245,7 +3242,7 @@ export class Realm { throw e; } - let doc = await this.searchPrerendered(cardsQuery as Query, { + let doc = await this.searchPrerendered(query as Query, { htmlFormat, cardUrls: normalizedCardUrls, renderType: normalizedRenderType, From fec5d396c21573e77dd25041ebce44cd2cfc6a02 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Wed, 28 Jan 2026 00:35:45 -0500 Subject: [PATCH 4/6] Fix failing test --- packages/realm-server/lib/retrieve-scoped-css.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/realm-server/lib/retrieve-scoped-css.ts b/packages/realm-server/lib/retrieve-scoped-css.ts index b503fca9706..a8860679114 100644 --- a/packages/realm-server/lib/retrieve-scoped-css.ts +++ b/packages/realm-server/lib/retrieve-scoped-css.ts @@ -27,10 +27,10 @@ export async function retrieveScopedCSS({ } let scopedCSSQuery: Expression = [ - `SELECT deps, realm_version FROM boxel_index_working WHERE deps IS NOT NULL AND`, + `SELECT deps, realm_version FROM boxel_index_working WHERE type = 'instance' AND deps IS NOT NULL AND`, ...indexCandidateExpressions(candidates), `UNION ALL - SELECT deps, realm_version FROM boxel_index WHERE deps IS NOT NULL AND`, + SELECT deps, realm_version FROM boxel_index WHERE type = 'instance' AND deps IS NOT NULL AND`, ...indexCandidateExpressions(candidates), `ORDER BY realm_version DESC LIMIT 1`, From 4c7ee316c4b6b5975322854684615474459bff10 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Wed, 28 Jan 2026 16:30:56 -0500 Subject: [PATCH 5/6] Add client-side SearchResource support for file-meta queries The client-side search pipeline (SearchResource) was built exclusively for card resources, causing "not a card collection document" errors when queries returned file-meta results. This broadens the pipeline to accept and correctly process file-meta resources alongside cards. - Broaden generic type constraints from `` to `` across SearchResource, StoreSearchResource, GetSearchResourceFunc, and getSearchResource - Replace isCardCollectionDocument with isLinkableCollectionDocument to accept file-meta collection responses - Route file-meta resources through store.get(id, { type: 'file-meta' }) instead of store.add({ data }) which only handles cards - Use type-aware store.peek with { type: 'file-meta' } for retrieval - Fix wireUpNewReference to check file-meta map before falling back to getCardInstance, eliminating spurious 415 errors for non-card URLs - Downgrade noisy query-field-support cache-hit log from info to debug - Add client-side SearchResource tests for file-meta search and live file-meta search Co-Authored-By: Claude Opus 4.5 --- packages/base/card-api.gts | 4 +- packages/base/query-field-support.ts | 2 +- .../FileSearchPlayground/playground.json | 29 +++ .../file-search-playground.gts | 231 ++++++++++++++++++ packages/host/app/lib/gc-card-store.ts | 4 +- packages/host/app/resources/search.ts | 69 ++++-- packages/host/app/services/store.ts | 12 +- packages/host/tests/cards/file-query-card.gts | 20 ++ .../tests/integration/realm-querying-test.gts | 72 +++++- .../integration/resources/search-test.ts | 86 +++++++ .../tests/realm-endpoints/search-test.ts | 1 - packages/runtime-common/query-field-utils.ts | 2 +- .../realm-index-query-engine.ts | 34 ++- 13 files changed, 527 insertions(+), 39 deletions(-) create mode 100644 packages/experiments-realm/FileSearchPlayground/playground.json create mode 100644 packages/experiments-realm/file-search-playground.gts create mode 100644 packages/host/tests/cards/file-query-card.gts diff --git a/packages/base/card-api.gts b/packages/base/card-api.gts index 356769afda1..cd6e05753c9 100644 --- a/packages/base/card-api.gts +++ b/packages/base/card-api.gts @@ -367,7 +367,7 @@ export async function flushLogs() { await logger.flush(); } -export interface StoreSearchResource { +export interface StoreSearchResource { readonly instances: T[]; readonly instancesByRealm: { realm: string; cards: T[] }[]; readonly isLoading: boolean; @@ -384,7 +384,7 @@ export type GetSearchResourceFuncOpts = { realms?: string[]; }; }; -export type GetSearchResourceFunc = ( +export type GetSearchResourceFunc = ( parent: object, getQuery: () => Query | undefined, getRealms?: () => string[] | undefined, diff --git a/packages/base/query-field-support.ts b/packages/base/query-field-support.ts index d02ec391e82..b96998c927b 100644 --- a/packages/base/query-field-support.ts +++ b/packages/base/query-field-support.ts @@ -59,7 +59,7 @@ export function ensureQueryFieldSearchResource( let fieldState = queryFieldState?.get(field.name); let searchResource = fieldState?.searchResource; if (searchResource) { - log.info( + log.debug( `ensureQueryFieldSearchResource: reusing existing resource from fieldState for field=${field.name}`, ); return searchResource; diff --git a/packages/experiments-realm/FileSearchPlayground/playground.json b/packages/experiments-realm/FileSearchPlayground/playground.json new file mode 100644 index 00000000000..bfbc40952b0 --- /dev/null +++ b/packages/experiments-realm/FileSearchPlayground/playground.json @@ -0,0 +1,29 @@ +{ + "data": { + "meta": { + "adoptsFrom": { + "name": "FileSearchPlayground", + "module": "../file-search-playground" + } + }, + "type": "card", + "attributes": { + "cardInfo": { + "name": "File Search Playground", + "notes": null, + "summary": "Interactively search files in the realm using query-backed linksToMany(FileDef).", + "cardThumbnailURL": null + }, + "pageSize": 4, + "nameFilter": "rate", + "sortDirection": "asc" + }, + "relationships": { + "cardInfo.theme": { + "links": { + "self": null + } + } + } + } +} \ No newline at end of file diff --git a/packages/experiments-realm/file-search-playground.gts b/packages/experiments-realm/file-search-playground.gts new file mode 100644 index 00000000000..1f69ec29911 --- /dev/null +++ b/packages/experiments-realm/file-search-playground.gts @@ -0,0 +1,231 @@ +import { + contains, + field, + linksToMany, + CardDef, + Component, +} from 'https://cardstack.com/base/card-api'; +import NumberField from 'https://cardstack.com/base/number'; +import StringField from 'https://cardstack.com/base/string'; +import enumField from 'https://cardstack.com/base/enum'; +import { FileDef } from 'https://cardstack.com/base/file-api'; + +const fileSearchQuery = { + filter: { + every: [ + { + type: { + module: 'https://cardstack.com/base/file-api', + name: 'FileDef', + }, + }, + { + on: { + module: 'https://cardstack.com/base/file-api', + name: 'FileDef', + }, + contains: { + name: '$this.nameFilter', + }, + }, + ], + }, + sort: [ + { + by: 'name', + direction: '$this.sortDirection', + }, + ], + page: { + size: '$this.pageSize', + }, + realm: '$thisRealm', +}; + +export class FileSearchPlayground extends CardDef { + static displayName = 'File Search Playground'; + + @field nameFilter = contains(StringField); + @field pageSize = contains(NumberField); + @field sortDirection = contains( + enumField(StringField, { options: ['asc', 'desc'] }), + ); + + @field matchingFiles = linksToMany(FileDef, { + query: fileSearchQuery, + }); + + static isolated = class Isolated extends Component { + get filterDisplay() { + let value = this.args.model.nameFilter; + if (!value?.length) { + return '(empty string)'; + } + return value; + } + + get directionLabel() { + let direction = (this.args.model.sortDirection ?? '').toLowerCase(); + return direction === 'desc' ? 'descending' : 'ascending'; + } + + + }; +} diff --git a/packages/host/app/lib/gc-card-store.ts b/packages/host/app/lib/gc-card-store.ts index ed950e7caea..63da5c3b7b9 100644 --- a/packages/host/app/lib/gc-card-store.ts +++ b/packages/host/app/lib/gc-card-store.ts @@ -35,7 +35,7 @@ type InstanceGraph = Map>; type StoredInstance = CardDef | FileDef; type StoreHooks = { - getSearchResource( + getSearchResource( parent: object, getQuery: () => Query | undefined, getRealms?: () => string[] | undefined, @@ -780,7 +780,7 @@ export default class CardStoreWithGarbageCollection implements CardStore { return dependencyGraph; } - getSearchResource( + getSearchResource( parent: object, getQuery: () => Query | undefined, getRealms?: () => string[] | undefined, diff --git a/packages/host/app/resources/search.ts b/packages/host/app/resources/search.ts index 12c03858e69..1d640c250b0 100644 --- a/packages/host/app/resources/search.ts +++ b/packages/host/app/resources/search.ts @@ -12,11 +12,16 @@ import difference from 'lodash/difference'; import isEqual from 'lodash/isEqual'; import { TrackedArray } from 'tracked-built-ins'; -import type { QueryResultsMeta, ErrorEntry } from '@cardstack/runtime-common'; +import type { + QueryResultsMeta, + ErrorEntry, + SingleCardDocument, +} from '@cardstack/runtime-common'; import { subscribeToRealm, - isCardCollectionDocument, + isLinkableCollectionDocument, isCardInstance, + isFileDefInstance, logger as runtimeLogger, normalizeQueryForSignature, buildQueryParamValue, @@ -25,6 +30,7 @@ import { import type { Query } from '@cardstack/runtime-common/query'; import type { CardDef } from 'https://cardstack.com/base/card-api'; +import type { FileDef } from 'https://cardstack.com/base/file-api'; import type { RealmEventContent } from 'https://cardstack.com/base/matrix-event'; import type RealmServerService from '../services/realm-server'; @@ -32,7 +38,7 @@ import type StoreService from '../services/store'; const waiter = buildWaiter('search-resource:search-waiter'); -export interface Args { +export interface Args { named: { query: Query | undefined; realms: string[] | undefined; @@ -52,9 +58,9 @@ export interface Args { owner: Owner; }; } -export class SearchResource extends Resource< - Args -> { +export class SearchResource< + T extends CardDef | FileDef = CardDef, +> extends Resource> { @service declare private realmServer: RealmServerService; @service declare private store: StoreService; @tracked private realmsToSearch: string[] = []; @@ -228,15 +234,24 @@ export class SearchResource extends Resource< } let newReferences = this._instances.map((i) => i.id); for (let card of this._instances) { - let maybeInstance = card?.id ? this.store.peek(card.id) : undefined; + let isFileMeta = isFileDefInstance(card); + let maybeInstance = card?.id + ? isFileMeta + ? this.store.peek(card.id, { type: 'file-meta' }) + : this.store.peek(card.id) + : undefined; if ( !maybeInstance && (card as unknown as { type?: string })?.type !== 'not-loaded' // TODO: under what circumstances could this happen? ) { - await this.store.add( - card, - { doNotPersist: true }, // search results always have id's - ); + if (isFileMeta) { + await this.store.get(card.id, { type: 'file-meta' }); + } else { + await this.store.add( + card as CardDef, + { doNotPersist: true }, // search results always have id's + ); + } } } let referencesToDrop = difference(oldReferences, newReferences); @@ -293,25 +308,37 @@ export class SearchResource extends Resource< throw err; } let json = await response.json(); - if (!isCardCollectionDocument(json)) { + if (!isLinkableCollectionDocument(json)) { throw new Error( - `The realm search response was not a card collection document: + `The realm search response was not a valid collection document: ${JSON.stringify(json, null, 2)}`, ); } let collectionDoc = json; for (let data of collectionDoc.data) { - let maybeInstance = this.store.peek(data.id!); + let isFileMeta = data.type === 'file-meta'; + let maybeInstance = isFileMeta + ? this.store.peek(data.id!, { type: 'file-meta' }) + : this.store.peek(data.id!); if (!maybeInstance) { - await this.store.add( - { data }, - { doNotPersist: true, relativeTo: new URL(data.id!) }, // search results always have id's - ); + if (isFileMeta) { + await this.store.get(data.id!, { type: 'file-meta' }); + } else { + await this.store.add( + { data } as SingleCardDocument, + { doNotPersist: true, relativeTo: new URL(data.id!) }, // search results always have id's + ); + } } } let results = collectionDoc.data - .map((r) => this.store.peek(r.id!)) // all results will have id's - .filter((i) => isCardInstance(i)) as T[]; + .map((r) => { + let isFileMeta = r.type === 'file-meta'; + return isFileMeta + ? this.store.peek(r.id!, { type: 'file-meta' }) + : this.store.peek(r.id!); + }) + .filter((i) => isCardInstance(i) || isFileDefInstance(i)) as T[]; this.#log.info( `search task complete; total instances=${results.length}; refs=${results .map((r) => r.id) @@ -335,7 +362,7 @@ export class SearchResource extends Resource< // ``` // If you need to use `getSearch()`/`getCards()` in something that is not a Component, then // let's talk. -export function getSearch( +export function getSearch( parent: object, owner: Owner, getQuery: () => Query | undefined, diff --git a/packages/host/app/services/store.ts b/packages/host/app/services/store.ts index 412fa70789a..45172fefbab 100644 --- a/packages/host/app/services/store.ts +++ b/packages/host/app/services/store.ts @@ -644,7 +644,7 @@ export default class StoreService extends Service implements StoreInterface { ).filter(Boolean) as CardDef[]; } - getSearchResource( + getSearchResource( parent: object, getQuery: () => Query | undefined, getRealms?: () => string[] | undefined, @@ -742,6 +742,16 @@ export default class StoreService extends Service implements StoreInterface { this.newReferencePromises.push(deferred.promise); try { await this.ready; + // Check file-meta map as well as card map — file-meta instances + // are loaded into their own map by store.get(id, { type: 'file-meta' }) + let fileMetaInstance = + this.peekError(url, { type: 'file-meta' }) ?? + this.peek(url, { type: 'file-meta' }); + if (fileMetaInstance) { + // File-meta instances don't need auto-saving or card wiring + deferred.fulfill(); + return; + } let instanceOrError = this.peekError(url) ?? this.peek(url); if (!instanceOrError) { instanceOrError = await this.getCardInstance({ diff --git a/packages/host/tests/cards/file-query-card.gts b/packages/host/tests/cards/file-query-card.gts new file mode 100644 index 00000000000..341033feb39 --- /dev/null +++ b/packages/host/tests/cards/file-query-card.gts @@ -0,0 +1,20 @@ +import { + contains, + linksToMany, + field, + CardDef, +} from 'https://cardstack.com/base/card-api'; +import { FileDef } from 'https://cardstack.com/base/file-api'; +import StringField from 'https://cardstack.com/base/string'; + +const fileSearchQuery = { + filter: { + type: { module: 'https://cardstack.com/base/file-api', name: 'FileDef' }, + }, + realm: '$thisRealm', +}; + +export class FileQueryCard extends CardDef { + @field nameFilter = contains(StringField); + @field matchingFiles = linksToMany(FileDef, { query: fileSearchQuery }); +} diff --git a/packages/host/tests/integration/realm-querying-test.gts b/packages/host/tests/integration/realm-querying-test.gts index 5d64675f795..110b98fdb06 100644 --- a/packages/host/tests/integration/realm-querying-test.gts +++ b/packages/host/tests/integration/realm-querying-test.gts @@ -581,6 +581,20 @@ module(`Integration | realm querying`, function (hooks) { ...sampleCards, 'files/sample.txt': 'Hello world', 'files/sample.md': 'Hello markdown', + 'file-query-card-instance.json': { + data: { + type: 'card', + attributes: { + nameFilter: '', + }, + meta: { + adoptsFrom: { + module: `${testModuleRealm}file-query-card`, + name: 'FileQueryCard', + }, + }, + }, + }, }, }); queryEngine = realm.realmIndexQueryEngine; @@ -1136,6 +1150,7 @@ module(`Integration | realm querying`, function (hooks) { `${paths.url}vangogh`, // dog `${paths.url}event-1`, // event `${paths.url}event-2`, // event + `${paths.url}file-query-card-instance`, // file query card `${paths.url}friend1`, // friend `${paths.url}friend2`, // friend `${paths.url}empty`, // friends @@ -1346,13 +1361,14 @@ module(`Integration | realm querying`, function (hooks) { contains: { cardTitle: 'ca' }, }, }); - assert.strictEqual(matching.length, 5); + assert.strictEqual(matching.length, 6); assert.deepEqual( matching.map((m) => m.id), [ `${paths.url}card-1`, `${paths.url}cards/1`, `${paths.url}cards/2`, + `${paths.url}file-query-card-instance`, `${paths.url}person-card1`, `${paths.url}person-card2`, ], @@ -1394,4 +1410,58 @@ module(`Integration | realm querying`, function (hooks) { }); assert.strictEqual(matching.length, 3); }); + + test('query field with linksToMany(FileDef) populates file-meta relationships', async function (assert) { + let result = await queryEngine.cardDocument( + new URL(`${testRealmURL}file-query-card-instance`), + { loadLinks: true }, + ); + assert.strictEqual(result?.type, 'doc', 'result is a doc'); + + let { data } = (result as { type: 'doc'; doc: { data: any } }).doc; + let relationships = data.relationships ?? {}; + + // The query field should have a top-level matchingFiles relationship with data array + let matchingFiles = relationships['matchingFiles'] as { + links?: Record; + data?: { type: string; id: string }[]; + }; + assert.ok(matchingFiles, 'matchingFiles relationship exists'); + assert.ok( + Array.isArray(matchingFiles?.data), + 'matchingFiles has a data array', + ); + assert.ok( + (matchingFiles?.data?.length ?? 0) >= 2, + 'matchingFiles contains at least 2 file results', + ); + + // Each entry in the data array should have type 'file-meta' + for (let entry of matchingFiles?.data ?? []) { + assert.strictEqual( + entry.type, + 'file-meta', + `relationship data entry ${entry.id} has type file-meta`, + ); + } + + // Individual indexed relationships should also have type 'file-meta' + let indexedKeys = Object.keys(relationships).filter((k) => + k.startsWith('matchingFiles.'), + ); + assert.ok( + indexedKeys.length >= 2, + 'individual file relationships are present', + ); + for (let key of indexedKeys) { + let rel = relationships[key] as { + data?: { type: string; id: string }; + }; + assert.strictEqual( + rel?.data?.type, + 'file-meta', + `${key} relationship data has type file-meta`, + ); + } + }); }); diff --git a/packages/host/tests/integration/resources/search-test.ts b/packages/host/tests/integration/resources/search-test.ts index a70aba8159f..80622fa256e 100644 --- a/packages/host/tests/integration/resources/search-test.ts +++ b/packages/host/tests/integration/resources/search-test.ts @@ -8,6 +8,7 @@ import { module, test } from 'qunit'; import type { Loader, Query } from '@cardstack/runtime-common'; import { baseRealm, + isFileDefInstance, type Realm, type LooseSingleCardDocument, } from '@cardstack/runtime-common'; @@ -295,6 +296,8 @@ module(`Integration | search resource`, function (hooks) { 'book.gts': { Book }, 'post.gts': { Post }, ...sampleCards, + 'files/hello.txt': 'Hello world', + 'files/notes.txt': 'Some notes', }, })); }); @@ -587,4 +590,87 @@ module(`Integration | search resource`, function (hooks) { 'meta.page.total remains correct on empty page', ); }); + + test(`can search for file-meta instances using SearchResource`, async function (assert) { + let query: Query = { + filter: { + type: { + module: `${baseRealm.url}file-api`, + name: 'FileDef', + }, + }, + }; + let search = getSearchResourceForTest(loaderService, () => ({ + named: { + query, + realms: [testRealmURL], + isLive: false, + isAutoSaved: false, + storeService, + owner: this.owner, + }, + })); + await search.loaded; + + assert.ok(search.instances.length >= 2, 'returns file-meta instances'); + let ids = search.instances.map((i) => i.id); + assert.ok( + ids.includes(`${testRealmURL}files/hello.txt`), + 'hello.txt is in results', + ); + assert.ok( + ids.includes(`${testRealmURL}files/notes.txt`), + 'notes.txt is in results', + ); + for (let instance of search.instances) { + assert.ok( + isFileDefInstance(instance), + `${instance.id} is a FileDef instance`, + ); + } + }); + + test(`can perform a live search for file-meta instances`, async function (assert) { + let query: Query = { + filter: { + type: { + module: `${baseRealm.url}file-api`, + name: 'FileDef', + }, + }, + }; + let search = getSearchResourceForTest(loaderService, () => ({ + named: { + query, + realms: [testRealmURL], + isLive: true, + isAutoSaved: false, + storeService, + owner: this.owner, + }, + })); + await search.loaded; + + let initialCount = search.instances.length; + assert.ok(initialCount >= 2, 'initial results include file-meta instances'); + + // Write a new file to trigger a live update + await realm.write('files/new-file.txt', 'New content'); + + await waitUntil(() => search.instances.length > initialCount); + + let ids = search.instances.map((i) => i.id); + assert.ok( + ids.includes(`${testRealmURL}files/new-file.txt`), + 'new file appears in live search results', + ); + assert.ok( + isFileDefInstance( + search.instances.find( + (i) => i.id === `${testRealmURL}files/new-file.txt`, + ), + ), + 'new file is a FileDef instance', + ); + }); }); diff --git a/packages/realm-server/tests/realm-endpoints/search-test.ts b/packages/realm-server/tests/realm-endpoints/search-test.ts index 7bdd31f7faf..3f58562f66b 100644 --- a/packages/realm-server/tests/realm-endpoints/search-test.ts +++ b/packages/realm-server/tests/realm-endpoints/search-test.ts @@ -50,7 +50,6 @@ module(`realm-endpoints/${basename(__filename)}`, function () { }; } - module('QUERY request (public realm)', function (_hooks) { let query = () => buildPersonQuery('Mango'); diff --git a/packages/runtime-common/query-field-utils.ts b/packages/runtime-common/query-field-utils.ts index caf23d74be8..f71d343eeda 100644 --- a/packages/runtime-common/query-field-utils.ts +++ b/packages/runtime-common/query-field-utils.ts @@ -234,7 +234,7 @@ export function normalizeQueryDefinition({ let filter = queryAny.filter as Record | undefined; if (!filter || Object.keys(filter).length === 0) { queryAny.filter = { type: targetRef }; - } else if (!filter.on) { + } else if (!filter.on && !filter.type) { filter.on = targetRef; } diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index e4863beba95..63e5028c0c0 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -392,12 +392,28 @@ export class RealmIndexQueryEngine { let realmResults: (CardResource | FileMetaResource)[] = []; if (realm === this.realmURL.href) { try { - let collection = await this.#indexQueryEngine.searchCards( - this.realmURL, - query, - opts, - ); - realmResults = Array.isArray(collection.cards) ? collection.cards : []; + if (await this.queryTargetsFileMeta(query.filter, opts)) { + let { files } = await this.#indexQueryEngine.searchFiles( + this.realmURL, + query, + opts, + ); + realmResults = files.map((fileEntry) => + fileResourceFromIndex( + new URL(fileEntry.canonicalURL), + fileEntry, + ), + ); + } else { + let collection = await this.#indexQueryEngine.searchCards( + this.realmURL, + query, + opts, + ); + realmResults = Array.isArray(collection.cards) + ? collection.cards + : []; + } } catch (err: unknown) { let message = err instanceof Error ? err.message : String(err ?? 'unknown error'); @@ -501,7 +517,7 @@ export class RealmIndexQueryEngine { if (first && first.id) { relationship.links.self = first.id; if (searchURL) { - relationship.data = { type: 'card', id: first.id }; + relationship.data = { type: first.type ?? 'card', id: first.id }; } } else { relationship.links.self = null; @@ -527,7 +543,7 @@ export class RealmIndexQueryEngine { (card): card is CardResource & { id: string } => typeof card.id === 'string', ) - .map((card) => ({ type: 'card', id: card.id })) + .map((card) => ({ type: card.type ?? 'card', id: card.id })) : undefined; resource.relationships[fieldName] = { @@ -542,7 +558,7 @@ export class RealmIndexQueryEngine { } resource.relationships![`${fieldName}.${index}`] = { links: { self: card.id }, - data: { type: 'card', id: card.id }, + data: { type: card.type ?? 'card', id: card.id }, }; }); } From 1cc10bf1c46b7a7a3bd8b6f6e0e8293eb1aef5b2 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Wed, 28 Jan 2026 17:43:06 -0500 Subject: [PATCH 6/6] Lint fix --- packages/runtime-common/realm-index-query-engine.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index 63e5028c0c0..289883157b7 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -399,10 +399,7 @@ export class RealmIndexQueryEngine { opts, ); realmResults = files.map((fileEntry) => - fileResourceFromIndex( - new URL(fileEntry.canonicalURL), - fileEntry, - ), + fileResourceFromIndex(new URL(fileEntry.canonicalURL), fileEntry), ); } else { let collection = await this.#indexQueryEngine.searchCards(