Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/boxel-cli/src/commands/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,23 @@ export async function search(
ensureTrailingSlash,
);

// The CLI only ever surfaces `data[]` to its callers, so default to JSON:API
// `include: []` and skip the server's `loadLinks` work entirely. Callers can
// still override by setting `include` themselves on `query`.
let body: Record<string, unknown> = {
realms,
include: [],
...query,
};

try {
let response = await pm.authedRealmServerFetch(searchUrl, {
method: 'QUERY',
headers: {
Accept: 'application/vnd.card+json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ realms, ...query }),
body: JSON.stringify(body),
});

if (!response.ok) {
Expand Down
5 changes: 5 additions & 0 deletions packages/host/app/components/prerendered-card-search.gts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ export default class PrerenderedCardSearch extends Component<PrerenderedCardComp
{
isLive: false,
storeService: getOwner(this)!.lookup('service:render-store') as any,
// We only consume `instance.id` and `instance.constructor` from the
// results (see live-prerendered-search.ts); skipping link side-loads
// saves N×M sequential link queries per search on the cards-grid
// prerender path.
include: [],
},
);

Expand Down
12 changes: 11 additions & 1 deletion packages/host/app/resources/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface Args<T extends CardDef | FileDef = CardDef> {
isAutoSaved?: boolean;
storeService?: StoreService;
doWhileRefreshing?: (() => void) | undefined;
include?: string[];
seed?:
| {
cards: T[];
Expand Down Expand Up @@ -86,6 +87,8 @@ export class SearchResource<
#previousQuery: Query | undefined;
#previousQueryString: string | undefined;
#previousRealms: string[] | undefined;
#previousInclude: string[] | undefined;
#include: string[] | undefined;
#dependencyTracking: RuntimeDependencyTrackingContext | undefined;
#log = runtimeLogger('search-resource');
#trackedLoadCount = 0;
Expand Down Expand Up @@ -182,6 +185,7 @@ export class SearchResource<
seed,
owner,
storeService,
include,
} = named;

setOwner(this, owner); // works around problem where lifetime parent is used as owner when they should be allowed to differ
Expand All @@ -201,6 +205,7 @@ export class SearchResource<
this.#isLive = isLive;
this.#doWhileRefreshing = doWhileRefreshing;
this.#dependencyTracking = named.dependencyTracking;
this.#include = include;
this.realmsToSearch =
realms === undefined || realms.length === 0
? this.realmServer.availableRealmURLs
Expand Down Expand Up @@ -269,7 +274,8 @@ export class SearchResource<
let queryString = buildQueryParamValue(normalizeQueryForSignature(query));
if (
isEqual(queryString, this.#previousQueryString) &&
isEqual(realms, this.#previousRealms)
isEqual(realms, this.#previousRealms) &&
isEqual(include, this.#previousInclude)
) {
// we want to only run the search when there is a deep equality
// difference, not a strict equality difference
Expand All @@ -284,6 +290,7 @@ export class SearchResource<
this.#previousRealms = realms;
this.#previousQuery = query;
this.#previousQueryString = queryString;
this.#previousInclude = include;
this.trackStoreLoad(this.search.perform(query), 'search');
}
get isLoading() {
Expand Down Expand Up @@ -408,6 +415,7 @@ export class SearchResource<
{
includeMeta: true,
dependencyTrackingContext,
include: this.#include,
},
);
this.#log.info(
Expand Down Expand Up @@ -442,6 +450,7 @@ export function getSearch<T extends CardDef | FileDef = CardDef>(
isLive?: boolean;
storeService?: StoreService;
doWhileRefreshing?: (() => void) | undefined;
include?: string[];
seed?:
| {
cards: T[];
Expand Down Expand Up @@ -469,6 +478,7 @@ export function getSearch<T extends CardDef | FileDef = CardDef>(
doWhileRefreshing: opts?.doWhileRefreshing,
seed: opts?.seed,
dependencyTracking: opts?.dependencyTracking,
include: opts?.include,
owner,
},
}));
Expand Down
24 changes: 21 additions & 3 deletions packages/host/app/services/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,7 @@ export default class StoreService extends Service implements StoreInterface {
opts: {
includeMeta: true;
dependencyTrackingContext?: RuntimeDependencyTrackingContext;
include?: string[];
},
): Promise<{
resources: (CardResource<Saved> | FileMetaResource)[];
Expand All @@ -759,6 +760,7 @@ export default class StoreService extends Service implements StoreInterface {
opts: {
includeMeta: true;
dependencyTrackingContext?: RuntimeDependencyTrackingContext;
include?: string[];
},
): Promise<{ instances: T[]; meta: QueryResultsMeta }>;
async search<T extends CardDef | FileDef = CardDef>(
Expand All @@ -767,6 +769,7 @@ export default class StoreService extends Service implements StoreInterface {
opts?: {
includeMeta?: boolean;
dependencyTrackingContext?: RuntimeDependencyTrackingContext;
include?: string[];
},
): Promise<
| T[]
Expand Down Expand Up @@ -795,13 +798,18 @@ export default class StoreService extends Service implements StoreInterface {
: [];
}
if (query.asData) {
let result = await this.fetchSearchData(query, searchRealms);
let result = await this.fetchSearchData(
query,
searchRealms,
opts?.include,
);
return opts?.includeMeta ? result : result.resources;
}
let result = await this.fetchAndHydrateSearchResults<T>(
query,
searchRealms,
opts?.dependencyTrackingContext,
opts?.include,
);
return opts?.includeMeta ? result : result.instances;
}
Expand All @@ -812,6 +820,7 @@ export default class StoreService extends Service implements StoreInterface {
query: Query,
realms: string[],
dependencyTrackingContext?: RuntimeDependencyTrackingContext,
include?: string[],
): Promise<{ instances: T[]; meta: QueryResultsMeta }> {
let realmServerURLs = this.realmServer.getRealmServersForRealms(realms);
// TODO remove this assertion after multi-realm server/federated identity is supported
Expand All @@ -827,7 +836,11 @@ export default class StoreService extends Service implements StoreInterface {
Accept: SupportedMimeType.CardJson,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ...query, realms }),
body: JSON.stringify({
...query,
realms,
...(include !== undefined ? { include } : {}),
Comment thread
habdelra marked this conversation as resolved.
}),
},
);
if (!response.ok) {
Expand Down Expand Up @@ -875,6 +888,7 @@ export default class StoreService extends Service implements StoreInterface {
private async fetchSearchData(
query: Query,
realms: string[],
include?: string[],
): Promise<{
resources: (CardResource<Saved> | FileMetaResource)[];
meta: QueryResultsMeta;
Expand All @@ -893,7 +907,11 @@ export default class StoreService extends Service implements StoreInterface {
Accept: SupportedMimeType.CardJson,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ...query, realms }),
body: JSON.stringify({
...query,
realms,
...(include !== undefined ? { include } : {}),
}),
},
);
if (!response.ok) {
Expand Down
9 changes: 5 additions & 4 deletions packages/host/tests/helpers/realm-server-mock/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ensureTrailingSlash,
parsePrerenderedSearchRequestFromPayload,
parseRealmsFromPayload,
parseSearchQueryFromPayload,
parseSearchRequestFromPayload,
parseSearchRequestPayload,
SearchRequestError,
searchPrerenderedRealms,
Expand Down Expand Up @@ -108,9 +108,9 @@ function registerSearchRoutes() {
throw e;
}

let cardsQuery;
let searchRequest;
try {
cardsQuery = parseSearchQueryFromPayload(payload);
searchRequest = parseSearchRequestFromPayload(payload);
} catch (e) {
if (e instanceof SearchRequestError) {
return buildSearchErrorResponse(e.message);
Expand All @@ -120,7 +120,8 @@ function registerSearchRoutes() {

let combined = await searchRealms(
realmList.map((realmURL) => getSearchableRealmForURL(realmURL)),
cardsQuery,
searchRequest.query,
{ include: searchRequest.include },
);

return new Response(JSON.stringify(combined), {
Expand Down
74 changes: 74 additions & 0 deletions packages/host/tests/integration/store-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -1613,6 +1613,80 @@ module('Integration | Store', function (hooks) {
);
});

test('search forwards include:[] into the _federated-search request body', async function (assert) {
let networkService = getService('network');
let capturedBodies: any[] = [];
let handler = async (req: Request) => {
let url = new URL(req.url);
if (url.pathname.endsWith('/_federated-search')) {
try {
capturedBodies.push(JSON.parse(await req.clone().text()));
} catch {
capturedBodies.push(null);
}
}
return null;
};
networkService.virtualNetwork.mount(handler, { prepend: true });
try {
await storeService.search(
{
filter: {
on: { module: testRRI('person'), name: 'Person' },
eq: { name: 'Hassan' },
},
},
[testRealmURL],
{ includeMeta: true, include: [] },
);
assert.ok(capturedBodies.length > 0, 'a _federated-search was issued');
let body = capturedBodies[capturedBodies.length - 1];
assert.deepEqual(
body.include,
[],
'request body carries include:[] verbatim',
);
} finally {
networkService.virtualNetwork.unmount(handler);
}
});

test('search omits the include key from the request body when not specified', async function (assert) {
let networkService = getService('network');
let capturedBodies: any[] = [];
let handler = async (req: Request) => {
let url = new URL(req.url);
if (url.pathname.endsWith('/_federated-search')) {
try {
capturedBodies.push(JSON.parse(await req.clone().text()));
} catch {
capturedBodies.push(null);
}
}
return null;
};
networkService.virtualNetwork.mount(handler, { prepend: true });
try {
await storeService.search(
{
filter: {
on: { module: testRRI('person'), name: 'Person' },
eq: { name: 'Hassan' },
},
},
[testRealmURL],
);
assert.ok(capturedBodies.length > 0, 'a _federated-search was issued');
let body = capturedBodies[capturedBodies.length - 1];
assert.notOk(
'include' in body,
'include key is absent so server-side default behavior is preserved',
);
} finally {
networkService.virtualNetwork.unmount(handler);
}
});

test<TestContextWithSave>('an instance live updates from indexing events for an instance update', async function (assert) {
assert.expect(2);
let didSave = false;
Expand Down
15 changes: 8 additions & 7 deletions packages/realm-server/handlers/handle-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type Koa from 'koa';
import {
buildSearchErrorResponse,
SupportedMimeType,
parseSearchQueryFromPayload,
parseSearchQueryFromRequest,
parseSearchRequestFromPayload,
parseSearchRequestFromRequest,
SearchRequestError,
searchRealms,
} from '@cardstack/runtime-common';
Expand All @@ -21,14 +21,14 @@ export default function handleSearch(): (ctxt: Koa.Context) => Promise<void> {
return async function (ctxt: Koa.Context) {
let { realmList, realmByURL } = getMultiRealmAuthorization(ctxt);

let cardsQuery;
let searchRequest;
let request = await fetchRequestFromContext(ctxt);
try {
let payload = getSearchRequestPayload(ctxt);
cardsQuery =
searchRequest =
payload !== undefined
? parseSearchQueryFromPayload(payload)
: await parseSearchQueryFromRequest(request);
? parseSearchRequestFromPayload(payload)
: await parseSearchRequestFromRequest(request);
} catch (e) {
if (e instanceof SearchRequestError) {
if (e.code === 'invalid-query') {
Expand All @@ -43,7 +43,8 @@ export default function handleSearch(): (ctxt: Koa.Context) => Promise<void> {

let combined = await searchRealms(
realmList.map((realmURL) => realmByURL.get(realmURL)),
cardsQuery,
searchRequest.query,
{ include: searchRequest.include },
);

await setContextResponse(
Expand Down
Loading
Loading