Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding renderType flag to prerendered search endpoint #2107

Merged
merged 9 commits into from
Jan 31, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -920,9 +920,6 @@ module('Integration | operator-mode', function (hooks) {
assert.dom(`[data-test-stack-card-index="0"]`).exists();
assert.dom(`[data-test-cards-grid-item]`).exists();

assert
.dom(`[data-test-cards-grid-item="${testRealmURL}BlogPost/1"]`)
.includesText('Blog Post');
assert
.dom(`[data-test-cards-grid-item="${testRealmURL}BlogPost/1"] `)
.includesText('Outer Space Journey');
Expand Down
16 changes: 8 additions & 8 deletions packages/realm-server/tests/realm-endpoints-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2452,35 +2452,35 @@ module(basename(__filename), function () {
assert.true(
json.data[0].attributes.html
.replace(/\s+/g, ' ')
.includes('Person Aaron'),
'embedded html looks correct (CardDef template)',
.includes('Embedded Card Person: Aaron'),
'embedded html looks correct (Person template)',
);

// 2nd card: Person Craig
assert.strictEqual(json.data[1].type, 'prerendered-card');
assert.true(
json.data[1].attributes.html
.replace(/\s+/g, ' ')
.includes('Person Craig'),
'embedded html for Craig looks correct (CardDef template)',
.includes('Embedded Card Person: Craig'),
'embedded html for Craig looks correct (Person template)',
);

// 3rd card: FancyPerson Jane
assert.strictEqual(json.data[2].type, 'prerendered-card');
assert.true(
json.data[2].attributes.html
.replace(/\s+/g, ' ')
.includes('FancyPerson Jane'),
'embedded html for Jane looks correct (CardDef template)',
.includes('Embedded Card FancyPerson: Jane'),
'embedded html for Jane looks correct (FancyPerson template)',
);

// 4th card: FancyPerson Jimmy
assert.strictEqual(json.data[3].type, 'prerendered-card');
assert.true(
json.data[3].attributes.html
.replace(/\s+/g, ' ')
.includes('FancyPerson Jimmy'),
'embedded html for Jimmy looks correct (CardDef template)',
.includes('Embedded Card FancyPerson: Jimmy'),
'embedded html for Jimmy looks correct (FancyPerson template)',
);

assertScopedCssUrlsContain(
Expand Down
1 change: 1 addition & 0 deletions packages/runtime-common/card-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ export function transformResultsToPrerenderedCardsDoc(results: {
id: card.url,
attributes: {
html: card.html,
usedRenderType: card.usedRenderType,
...(card.isError ? { isError: true as const } : {}),
},
}));
Expand Down
148 changes: 118 additions & 30 deletions packages/runtime-common/index-query-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import {
fieldArity,
tableValuedFunctionsPlaceholder,
query,
dbExpression,
isDbExpression,
DBSpecificExpression,
Param,
} from './expression';
import {
type Query,
Expand Down Expand Up @@ -120,6 +123,7 @@ export type QueryOptions = WIPOptions & PrerenderedCardOptions;

interface PrerenderedCardOptions {
htmlFormat?: 'embedded' | 'fitted' | 'atom';
renderType?: ResolvedCodeRef;
includeErrors?: true;
cardUrls?: string[];
}
Expand All @@ -131,6 +135,7 @@ interface WIPOptions {
export interface PrerenderedCard {
url: string;
html: string | null;
usedRenderType: ResolvedCodeRef;
isError?: true;
}

Expand Down Expand Up @@ -461,47 +466,35 @@ export class IndexQueryEngine {
);
}

let ref: ResolvedCodeRef;
let filterOnValue = filter && 'type' in filter ? filter.type : filter?.on;
if (filterOnValue) {
ref = filterOnValue as ResolvedCodeRef;
} else {
ref = baseCardRef;
}

let htmlColumnExpression;
switch (opts.htmlFormat) {
case 'embedded':
htmlColumnExpression = [
'embedded_html ->> ',
param(internalKeyFor(ref, undefined)),
];
break;
case 'fitted':
htmlColumnExpression = [
'fitted_html ->> ',
param(internalKeyFor(ref, undefined)),
];
break;
case 'atom':
default:
htmlColumnExpression = ['atom_html'];
break;
}
let htmlColumnExpression = this.buildHtmlColumnExpression({
htmlFormat: opts.htmlFormat,
renderType: opts.renderType,
});
let usedRenderTypeColumnExpression =
this.buildUsedRenderTypeColumnExpression({
htmlFormat: opts.htmlFormat,
renderType: opts.renderType,
});

let { results, meta } = (await this._search(
realmURL,
{ filter, sort, page },
loader,
opts,
[
'SELECT url, ANY_VALUE(i.type) as type, ANY_VALUE(file_alias) as file_alias, ANY_VALUE(',
'SELECT url, ANY_VALUE(i.type) as type, ANY_VALUE(file_alias) as file_alias, ',
...htmlColumnExpression,
') as html, ANY_VALUE(deps) as deps',
' as html,',
...usedRenderTypeColumnExpression,
' as used_render_type,',
'ANY_VALUE(deps) as deps',
],
)) as {
meta: QueryResultsMeta;
results: (Partial<BoxelIndexTable> & { html: string | null })[];
results: (Partial<BoxelIndexTable> & {
html: string | null;
used_render_type: string;
})[];
};

// We need a way to get scoped css urls even from cards linked from foreign realms.These are saved in the deps column of instances and modules.
Expand All @@ -518,16 +511,111 @@ export class IndexQueryEngine {
}
});

let moduleNameSeparatorIndex = card.used_render_type.lastIndexOf('/');
return {
url: card.url!,
html: card.html,
usedRenderType: {
module: card.used_render_type.substring(0, moduleNameSeparatorIndex),
name: card.used_render_type.substring(moduleNameSeparatorIndex + 1),
},
...(card.type === 'error' ? { isError: true as const } : {}),
};
});

return { prerenderedCards, scopedCssUrls: [...scopedCssUrls], meta };
}

private buildHtmlColumnExpression({
htmlFormat,
renderType,
}: {
htmlFormat: 'embedded' | 'fitted' | 'atom' | undefined;
renderType?: ResolvedCodeRef;
}): (string | Param | DBSpecificExpression)[] {
let fieldName = htmlFormat ? `${htmlFormat}_html` : `atom_html`;
if (!htmlFormat || htmlFormat === 'atom') {
return [`ANY_VALUE(${fieldName})`];
}

let htmlColumnExpression = [];
htmlColumnExpression.push('COALESCE(');
if (renderType) {
htmlColumnExpression.push(`ANY_VALUE(${fieldName}) ->> `);
htmlColumnExpression.push(param(internalKeyFor(renderType, undefined)));
htmlColumnExpression.push(',');
}

htmlColumnExpression.push(
...[
`(
CASE WHEN ANY_VALUE(${fieldName}) IS NOT NULL AND `,
dbExpression({
pg: `jsonb_typeof(ANY_VALUE(${fieldName})) = 'object'`,
sqlite: `json_type(ANY_VALUE(${fieldName})) = 'object'`,
}),
` THEN ( SELECT value FROM `,
dbExpression({
pg: `jsonb_each_text(ANY_VALUE(${fieldName}))`,
sqlite: `json_each(ANY_VALUE(${fieldName}))`,
}),
` WHERE key = (SELECT replace(ANY_VALUE( `,
dbExpression({
pg: `types[0]::text`,
sqlite: `json_extract(types, '$[0]')`,
}),
`), '"', ''))) ELSE NULL END), NULL)`,
],
);

return htmlColumnExpression;
}

private buildUsedRenderTypeColumnExpression({
htmlFormat,
renderType,
}: {
htmlFormat: 'embedded' | 'fitted' | 'atom' | undefined;
renderType?: ResolvedCodeRef;
}): (string | Param | DBSpecificExpression)[] {
let usedRenderTypeColumnExpression = [];
if (htmlFormat && htmlFormat !== 'atom' && renderType) {
usedRenderTypeColumnExpression.push(`CASE`);
usedRenderTypeColumnExpression.push(
`WHEN ANY_VALUE(${htmlFormat}_html) ->> `,
);
usedRenderTypeColumnExpression.push(
param(internalKeyFor(renderType, undefined)),
);
usedRenderTypeColumnExpression.push(
`IS NOT NULL THEN '${internalKeyFor(renderType, undefined)}'`,
);
usedRenderTypeColumnExpression.push(
...[
`ELSE replace(ANY_VALUE(`,
dbExpression({
pg: `types[0]::text`,
sqlite: `json_extract(types, '$[0]')`,
}),
`), '"', '') END`,
],
);
} else {
usedRenderTypeColumnExpression.push(
...[
`replace(ANY_VALUE(`,
dbExpression({
pg: `types[0]::text`,
sqlite: `json_extract(types, '$[0]')`,
}),
`), '"', '')`,
],
);
}

return usedRenderTypeColumnExpression;
}

async fetchCardTypeSummary(realmURL: URL): Promise<CardTypeSummary[]> {
let results = (await this.#query([
`SELECT value
Expand Down
4 changes: 4 additions & 0 deletions packages/runtime-common/realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
type QueuePublisher,
type FileMeta,
type DirectoryMeta,
type ResolvedCodeRef,
} from './index';
import merge from 'lodash/merge';
import mergeWith from 'lodash/mergeWith';
Expand Down Expand Up @@ -1645,6 +1646,7 @@ export class Realm {
let parsedQueryString = parseQuery(href);
let htmlFormat = parsedQueryString.prerenderedHtmlFormat as string;
let cardUrls = parsedQueryString.cardUrls as string[];
let renderType = parsedQueryString.renderType as ResolvedCodeRef;

if (!isValidPrerenderedHtmlFormat(htmlFormat)) {
return badRequest(
Expand All @@ -1659,6 +1661,7 @@ export class Realm {
// prerenderedHtmlFormat and cardUrls are special parameters only for this endpoint so don't include it in our Query for standard card search
delete parsedQueryString.prerenderedHtmlFormat;
delete parsedQueryString.cardUrls;
delete parsedQueryString.renderType;

let cardsQuery = parsedQueryString;
assertQuery(parsedQueryString);
Expand All @@ -1669,6 +1672,7 @@ export class Realm {
useWorkInProgressIndex,
htmlFormat,
cardUrls,
renderType,
includeErrors: true,
},
);
Expand Down
Loading