From 04a426920b69cf04f30092d73d1c0e47957a2c93 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Fri, 20 Feb 2026 13:10:22 +0100 Subject: [PATCH] Add FileDef support for prerendered search (CS-10125) Route file-meta queries through the prerendered search pipeline by detecting FileDef type filters via queryTargetsFileMeta() and passing the appropriate entryType to IndexQueryEngine.searchPrerendered(). Co-Authored-By: Claude Opus 4.6 --- .../prerendered-card-search-test.gts | 45 ++++ .../tests/search-prerendered-test.ts | 194 ++++++++++++++++++ packages/runtime-common/index-query-engine.ts | 3 +- .../realm-index-query-engine.ts | 2 + 4 files changed, 243 insertions(+), 1 deletion(-) diff --git a/packages/host/tests/integration/components/prerendered-card-search-test.gts b/packages/host/tests/integration/components/prerendered-card-search-test.gts index ccbefcb987..bd2ea02147 100644 --- a/packages/host/tests/integration/components/prerendered-card-search-test.gts +++ b/packages/host/tests/integration/components/prerendered-card-search-test.gts @@ -328,6 +328,7 @@ module(`Integration | prerendered-card-search`, function (hooks) { 'person.gts': { PersonField }, 'post.gts': { Post }, 'publisher.gts': { Publisher }, + 'sample-doc.md': '# Test Document\n\nSome content here.', ...sampleCards, }, })); @@ -836,4 +837,48 @@ module(`Integration | prerendered-card-search`, function (hooks) { .dom('[data-test-meta-page-total="5"]') .exists('meta.page.total remains correct on last page'); }); + + test(`can search for files using a FileDef type filter`, async function (assert) { + let query: Query = { + filter: { + type: { + module: `${baseRealm.url}markdown-file-def`, + name: 'MarkdownDef', + }, + }, + }; + let realms = [testRealmURL]; + + await render( + , + ); + await waitFor('#ember-testing > [data-test-boxel-card-container]'); + assert + .dom('#ember-testing > [data-test-boxel-card-container]') + .exists({ count: 1 }, 'one markdown file result is rendered'); + assert + .dom('#ember-testing > [data-test-boxel-card-container]') + .containsText('Test Document', 'markdown file title appears in rendered html'); + assert + .dom('[data-test-meta-page-total="1"]') + .exists('meta.page.total is correct for file-meta search'); + }); }); diff --git a/packages/realm-server/tests/search-prerendered-test.ts b/packages/realm-server/tests/search-prerendered-test.ts index b0881e8099..fa8675f913 100644 --- a/packages/realm-server/tests/search-prerendered-test.ts +++ b/packages/realm-server/tests/search-prerendered-test.ts @@ -973,6 +973,200 @@ module(basename(__filename), function () { }); }); + module('file-meta queries', function (hooks) { + setupPermissionedRealm(hooks, { + realmURL, + permissions: { + '*': ['read'], + }, + fileSystem: { + 'sample.md': `# Test Document\n\nThis is a sample markdown file for testing.`, + 'notes.md': `# Notes\n\nSome notes content here.`, + 'readme.txt': 'Plain text file contents.', + 'person.gts': ` + import { contains, field, CardDef, Component } from "https://cardstack.com/base/card-api"; + import StringField from "https://cardstack.com/base/string"; + + export class Person extends CardDef { + @field firstName = contains(StringField); + static embedded = class Embedded extends Component { + + } + } + `, + 'john.json': { + data: { + attributes: { + firstName: 'John', + }, + meta: { + adoptsFrom: { + module: './person', + name: 'Person', + }, + }, + }, + }, + }, + onRealmSetup, + }); + + test('returns prerendered file results for FileDef type filter', async function (assert) { + let query: Query & { prerenderedHtmlFormat: string } = { + filter: { + type: { + module: `https://cardstack.com/base/file-api`, + name: 'FileDef', + }, + }, + prerenderedHtmlFormat: 'embedded', + }; + let response = await request + .post(searchPath) + .set('Accept', 'application/vnd.card+json') + .set('X-HTTP-Method-Override', 'QUERY') + .send(query); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + + assert.true( + json.data.length >= 2, + 'at least the markdown files are returned', + ); + + json.data.forEach( + (item: { type: string; attributes: { html: string } }) => { + assert.strictEqual( + item.type, + 'prerendered-card', + 'result type is prerendered-card', + ); + assert.ok( + item.attributes.html, + 'result has non-empty html attribute', + ); + }, + ); + + assert.true( + json.meta.page.total >= 2, + 'total count includes file results', + ); + }); + + test('returns prerendered file results for MarkdownDef subclass filter', async function (assert) { + let query: Query & { prerenderedHtmlFormat: string } = { + filter: { + type: { + module: `https://cardstack.com/base/markdown-file-def`, + name: 'MarkdownDef', + }, + }, + prerenderedHtmlFormat: 'embedded', + }; + let response = await request + .post(searchPath) + .set('Accept', 'application/vnd.card+json') + .set('X-HTTP-Method-Override', 'QUERY') + .send(query); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + + assert.strictEqual( + json.data.length, + 2, + 'only markdown files are returned', + ); + + // MarkdownDef uses MarkdownFilePreview template which renders + // an article with class 'markdown-file-preview' + let htmls = json.data.map((item: { attributes: { html: string } }) => + item.attributes.html.replace(/\s+/g, ' '), + ); + assert.true( + htmls.some((html: string) => html.includes('Test Document')), + 'sample.md title appears in prerendered html', + ); + assert.true( + htmls.some((html: string) => html.includes('Notes')), + 'notes.md title appears in prerendered html', + ); + + assert.strictEqual( + json.meta.page.total, + 2, + 'total count matches markdown files', + ); + }); + + test('returns fitted format for file results', async function (assert) { + let query: Query & { prerenderedHtmlFormat: string } = { + filter: { + type: { + module: `https://cardstack.com/base/markdown-file-def`, + name: 'MarkdownDef', + }, + }, + prerenderedHtmlFormat: 'fitted', + }; + let response = await request + .post(searchPath) + .set('Accept', 'application/vnd.card+json') + .set('X-HTTP-Method-Override', 'QUERY') + .send(query); + + assert.strictEqual(response.status, 200, 'HTTP 200 status'); + let json = response.body; + + assert.strictEqual( + json.data.length, + 2, + 'markdown files are returned', + ); + + json.data.forEach( + (item: { type: string; attributes: { html: string } }) => { + assert.strictEqual( + item.type, + 'prerendered-card', + 'result type is prerendered-card', + ); + assert.ok(item.attributes.html, 'fitted html is non-empty'); + }, + ); + }); + + test('file-meta query does not return card instances', async function (assert) { + let query: Query & { prerenderedHtmlFormat: string } = { + filter: { + type: { + module: `https://cardstack.com/base/markdown-file-def`, + name: 'MarkdownDef', + }, + }, + prerenderedHtmlFormat: 'embedded', + }; + let response = await request + .post(searchPath) + .set('Accept', 'application/vnd.card+json') + .set('X-HTTP-Method-Override', 'QUERY') + .send(query); + + let json = response.body; + + // Verify no card instances (john.json) are in the results + let ids = json.data.map((item: { id: string }) => item.id); + assert.false( + ids.some((id: string) => id.includes('john.json')), + 'card instance john.json is not in file-meta results', + ); + }); + }); + module('permissioned realm', function (hooks) { setupPermissionedRealm(hooks, { realmURL, diff --git a/packages/runtime-common/index-query-engine.ts b/packages/runtime-common/index-query-engine.ts index 62798dba18..8883ed8a28 100644 --- a/packages/runtime-common/index-query-engine.ts +++ b/packages/runtime-common/index-query-engine.ts @@ -584,6 +584,7 @@ export class IndexQueryEngine { realmURL: URL, { filter, sort, page }: Query, opts: QueryOptions = { includeErrors: true }, + entryType: 'instance' | 'file' = 'instance', ): Promise<{ prerenderedCards: PrerenderedCard[]; scopedCssUrls: string[]; @@ -617,7 +618,7 @@ export class IndexQueryEngine { ' as used_render_type,', 'ANY_VALUE(deps) as deps', ], - 'instance', + entryType, )) as { meta: QueryResultsMeta; results: (Partial & { diff --git a/packages/runtime-common/realm-index-query-engine.ts b/packages/runtime-common/realm-index-query-engine.ts index b4aa522579..f4256086b5 100644 --- a/packages/runtime-common/realm-index-query-engine.ts +++ b/packages/runtime-common/realm-index-query-engine.ts @@ -277,10 +277,12 @@ export class RealmIndexQueryEngine { } async searchPrerendered(query: Query, opts?: Options) { + let isFileMetaQuery = await this.queryTargetsFileMeta(query.filter, opts); let results = await this.#indexQueryEngine.searchPrerendered( new URL(this.#realm.url), query, opts, + isFileMetaQuery ? 'file' : 'instance', ); return results;