From 1e5464cdf0eac841176479897b52d63828112d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Sat, 14 Feb 2026 20:59:16 +0100 Subject: [PATCH] more copilot comments fixed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../providers/DefaultLocationStore.test.ts | 21 ++++++ .../src/providers/DefaultLocationStore.ts | 68 +++++++++++-------- .../src/service/createRouter.ts | 2 +- .../src/service/request/parseLocationQuery.ts | 37 ++++++++-- 4 files changed, 96 insertions(+), 32 deletions(-) diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts b/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts index ac57a8d075..f12293fa1f 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts +++ b/plugins/catalog-backend/src/providers/DefaultLocationStore.test.ts @@ -951,6 +951,27 @@ describe('DefaultLocationStore', () => { items: [l1, l2, l3, l4], totalItems: 4, }); + + await expect( + store.queryLocations({ limit: 10, query: { $all: [] } }), + ).resolves.toEqual({ + items: [], + totalItems: 0, + }); + + await expect( + store.queryLocations({ limit: 10, query: { $any: [] } }), + ).resolves.toEqual({ + items: [], + totalItems: 0, + }); + + await expect( + store.queryLocations({ limit: 10, query: { type: { $in: [] } } }), + ).resolves.toEqual({ + items: [], + totalItems: 0, + }); }, ); }); diff --git a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts b/plugins/catalog-backend/src/providers/DefaultLocationStore.ts index 2c3f2a0da5..74509eb1d5 100644 --- a/plugins/catalog-backend/src/providers/DefaultLocationStore.ts +++ b/plugins/catalog-backend/src/providers/DefaultLocationStore.ts @@ -109,7 +109,10 @@ export class DefaultLocationStore implements LocationStore, EntityProvider { afterId?: string; query?: FilterPredicate; }): Promise<{ items: Location[]; totalItems: number }> { - let itemsQuery = this.db('locations'); + let itemsQuery = this.db('locations').whereNot( + 'type', + 'bootstrap', + ); if (options.query) { itemsQuery = applyLocationFilterToQuery( @@ -558,6 +561,9 @@ function applyLocationFilterToQuery( } if ('$all' in query) { + if (query.$all.length === 0) { + return result.whereRaw('1 = 0'); + } return result.where(outer => { for (const subQuery of query.$all) { outer.andWhere(inner => { @@ -568,6 +574,9 @@ function applyLocationFilterToQuery( } if ('$any' in query) { + if (query.$any.length === 0) { + return result.whereRaw('1 = 0'); + } return result.where(outer => { for (const subQuery of query.$any) { outer.orWhere(inner => { @@ -636,46 +645,51 @@ function applyLocationFilterToQuery( ? result.whereNotNull(key) : result.whereNull(key); } else if ('$in' in value) { - if (key === 'id') { + if (value.$in.length === 0) { + result = result.whereRaw('1 = 0'); + } else if (key === 'id') { result = result.whereIn(key, value.$in); } else if (clientType === 'pg') { - result = result.whereRaw( - `UPPER(??::text) IN (${value.$in - .map(() => 'UPPER(?::text)') - .join(', ')})`, - [key, ...value.$in], - ); + const rhs = value.$in.map(() => 'UPPER(?::text)').join(', '); + result = result.whereRaw(`UPPER(??::text) IN (${rhs})`, [ + key, + ...value.$in, + ]); } else if (clientType.includes('mysql')) { - result = result.whereRaw( - `UPPER(CAST(?? AS CHAR)) IN (${value.$in - .map(() => 'UPPER(CAST(? AS CHAR))') - .join(', ')})`, - [key, ...value.$in], - ); + const rhs = value.$in.map(() => 'UPPER(CAST(? AS CHAR))').join(', '); + result = result.whereRaw(`UPPER(CAST(?? AS CHAR)) IN (${rhs})`, [ + key, + ...value.$in, + ]); } else { - result = result.whereRaw( - `UPPER(??) IN (${value.$in.map(() => 'UPPER(?)').join(', ')})`, - [key, ...value.$in], - ); + const rhs = value.$in.map(() => 'UPPER(?)').join(', '); + result = result.whereRaw(`UPPER(??) IN (${rhs})`, [ + key, + ...value.$in, + ]); } } else if ('$hasPrefix' in value) { const escaped = value.$hasPrefix.replace(/([\\%_])/g, '\\$1'); if (clientType === 'pg') { - result = result.whereRaw('?? ilike ?', [key, `${escaped}%`]); + result = result.whereRaw("?? ilike ? escape '\\'", [ + key, + `${escaped}%`, + ]); + } else if (clientType.includes('mysql')) { + result = result.whereRaw("UPPER(??) like UPPER(?) escape '\\\\'", [ + key, + `${escaped}%`, + ]); } else { - result = result.whereRaw('UPPER(??) like UPPER(?)', [ + result = result.whereRaw("UPPER(??) like UPPER(?) escape '\\'", [ key, `${escaped}%`, ]); } } else if ('$contains' in value) { - // There are no array shaped values for location queries, so we throw - // an error since it cannot possibly match. An alternative could be to - // make the query always fail (eg with 1 = 0) but this felt more - // immediately helpful to the end user. - throw new InputError( - `Invalid filter predicate, '$contains' is not supported for location queries`, - ); + // There are no array shaped values for location queries, so we just + // always fail here + result = result.whereRaw('1 = 0'); } else { throw new InputError( `Invalid filter predicate, got unknown matcher object '${JSON.stringify( diff --git a/plugins/catalog-backend/src/service/createRouter.ts b/plugins/catalog-backend/src/service/createRouter.ts index 9dba615d36..11e459ae82 100644 --- a/plugins/catalog-backend/src/service/createRouter.ts +++ b/plugins/catalog-backend/src/service/createRouter.ts @@ -605,7 +605,7 @@ export async function createRouter( }); try { - const request = parseLocationQuery(req.body); + const request = parseLocationQuery(req.body ?? {}); const result = await locationService.queryLocations({ ...request, limit: request.limit + 1, diff --git a/plugins/catalog-backend/src/service/request/parseLocationQuery.ts b/plugins/catalog-backend/src/service/request/parseLocationQuery.ts index 6021c148f7..61a926613e 100644 --- a/plugins/catalog-backend/src/service/request/parseLocationQuery.ts +++ b/plugins/catalog-backend/src/service/request/parseLocationQuery.ts @@ -32,6 +32,20 @@ const locationCursorParser = z.object({ query: filterPredicateSchema.optional(), }); +function isSupportedFilterPredicateRoot( + value: FilterPredicate | undefined, +): boolean { + if (value === undefined) { + return true; + } + + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + return false; + } + + return true; +} + export function parseLocationQuery( request: Readonly, ): { @@ -39,7 +53,11 @@ export function parseLocationQuery( afterId?: string; query?: FilterPredicate; } { - if (request.cursor) { + if (request.cursor !== undefined) { + if (!request.cursor) { + throw new InputError('Cursor cannot be empty'); + } + let parsed: JsonValue; try { const data = Buffer.from(request.cursor, 'base64').toString('utf8'); @@ -52,10 +70,16 @@ export function parseLocationQuery( if (!result.success) { throw new InputError(`Malformed cursor: ${fromZodError(result.error)}`); } + + const { query, limit, afterId } = result.data; + if (!isSupportedFilterPredicateRoot(query)) { + throw new InputError('Query must be an object'); + } + return { - limit: result.data.limit, - afterId: result.data.afterId, - query: result.data.query, + limit, + afterId, + query, }; } @@ -70,6 +94,11 @@ export function parseLocationQuery( if (!result.success) { throw new InputError(`Invalid query: ${fromZodError(result.error)}`); } + + if (!isSupportedFilterPredicateRoot(result.data)) { + throw new InputError('Query must be an object'); + } + query = result.data; }