From b218fb07df3c5475e0dc50d4db2078a756c446c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 20 Feb 2026 11:30:45 +0100 Subject: [PATCH] arrow functions and simplify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../applyPredicateEntityFilterToQuery.test.ts | 18 ++ .../applyPredicateEntityFilterToQuery.ts | 265 ++++++++---------- 2 files changed, 141 insertions(+), 142 deletions(-) diff --git a/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.test.ts b/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.test.ts index 2f216fd219..21e5d2c37a 100644 --- a/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.test.ts +++ b/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.test.ts @@ -116,6 +116,16 @@ describe.each(databases.eachSupportedId())( ); } + it('matches everything for empty field expression', async () => { + await expect(query({})).resolves.toEqual([ + 'api-d', + 'bare-e', + 'service-a', + 'service-b', + 'website-c', + ]); + }); + it('filters by direct field value', async () => { await expect(query({ kind: 'component' })).resolves.toEqual([ 'bare-e', @@ -140,6 +150,10 @@ describe.each(databases.eachSupportedId())( ).resolves.toEqual(['service-a', 'service-b']); }); + it('returns nothing for empty $all', async () => { + await expect(query({ $all: [] })).resolves.toEqual([]); + }); + it('filters with $any', async () => { await expect( query({ @@ -148,6 +162,10 @@ describe.each(databases.eachSupportedId())( ).resolves.toEqual(['service-a', 'service-b', 'website-c']); }); + it('returns nothing for empty $any', async () => { + await expect(query({ $any: [] })).resolves.toEqual([]); + }); + it('filters with $not', async () => { await expect( query({ diff --git a/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.ts b/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.ts index eb6e65604a..88b064866d 100644 --- a/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.ts +++ b/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.ts @@ -23,24 +23,6 @@ import { InputError } from '@backstage/errors'; import { Knex } from 'knex'; import { DbSearchRow } from '../../database/tables'; -function isAllPredicate( - filter: FilterPredicate, -): filter is { $all: FilterPredicate[] } { - return typeof filter === 'object' && filter !== null && '$all' in filter; -} - -function isAnyPredicate( - filter: FilterPredicate, -): filter is { $any: FilterPredicate[] } { - return typeof filter === 'object' && filter !== null && '$any' in filter; -} - -function isNotPredicate( - filter: FilterPredicate, -): filter is { $not: FilterPredicate } { - return typeof filter === 'object' && filter !== null && '$not' in filter; -} - function isPrimitive(value: unknown): value is FilterPredicatePrimitive { return ( typeof value === 'string' || @@ -49,30 +31,8 @@ function isPrimitive(value: unknown): value is FilterPredicatePrimitive { ); } -function isExistsValue( - value: FilterPredicateValue, -): value is { $exists: boolean } { - return typeof value === 'object' && value !== null && '$exists' in value; -} - -function isInValue( - value: FilterPredicateValue, -): value is { $in: FilterPredicatePrimitive[] } { - return typeof value === 'object' && value !== null && '$in' in value; -} - -function isHasPrefixValue( - value: FilterPredicateValue, -): value is { $hasPrefix: string } { - return typeof value === 'object' && value !== null && '$hasPrefix' in value; -} - -function isFieldExpression(filter: FilterPredicate): boolean { - if (typeof filter !== 'object' || filter === null) { - return false; - } - // Not a logical operator - return !('$all' in filter || '$any' in filter || '$not' in filter); +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); } /** @@ -114,7 +74,6 @@ function isFieldExpression(filter: FilterPredicate): boolean { * ORDER BY final_entities.entity_ref ASC; * ``` */ - export function applyPredicateEntityFilterToQuery(options: { filter: FilterPredicate; targetQuery: Knex.QueryBuilder; @@ -122,53 +81,9 @@ export function applyPredicateEntityFilterToQuery(options: { knex: Knex; }): Knex.QueryBuilder { const { filter, targetQuery, onEntityIdField, knex } = options; - // Handle $not - if (isNotPredicate(filter)) { - return targetQuery.andWhereNot(subQuery => - applyPredicateEntityFilterToQuery({ - filter: filter.$not, - targetQuery: subQuery, - onEntityIdField, - knex, - }), - ); - } - // Handle $all (AND) - if (isAllPredicate(filter)) { - return targetQuery.andWhere(function allFilter() { - for (const subFilter of filter.$all) { - this.andWhere(subQuery => - applyPredicateEntityFilterToQuery({ - filter: subFilter, - targetQuery: subQuery, - onEntityIdField, - knex, - }), - ); - } - }); - } - - // Handle $any (OR) - if (isAnyPredicate(filter)) { - return targetQuery.andWhere(function anyFilter() { - for (const subFilter of filter.$any) { - this.orWhere(subQuery => - applyPredicateEntityFilterToQuery({ - filter: subFilter, - targetQuery: subQuery, - onEntityIdField, - knex, - }), - ); - } - }); - } - - // Reject primitives at the top level. Matching by value without specifying - // a field key is ambiguous and should not be allowed. - if (isPrimitive(filter)) { + // We do not support top-level primitives; all matching happens through objects + if (!filter || typeof filter !== 'object' || Array.isArray(filter)) { throw new InputError( `Invalid filter predicate: top-level primitive values are not supported. ` + `Wrap the value in a field expression, e.g. { "kind": ${JSON.stringify( @@ -177,62 +92,128 @@ export function applyPredicateEntityFilterToQuery(options: { ); } - // Handle field expressions like { "kind": "component" } or { "spec.type": { "$in": ["service", "website"] } } - if (isFieldExpression(filter)) { - return targetQuery.andWhere(function fieldFilter() { - for (const [key, value] of Object.entries(filter)) { - const normalizedKey = key.toLocaleLowerCase('en-US'); + if ('$not' in filter) { + return targetQuery.andWhereNot(inner => + applyPredicateEntityFilterToQuery({ + filter: filter.$not, + targetQuery: inner, + onEntityIdField, + knex, + }), + ); + } - if (isExistsValue(value)) { - // Handle $exists - const existsQuery = knex('search') - .select('search.entity_id') - .where({ key: normalizedKey }); - - if (value.$exists) { - this.andWhere(onEntityIdField, 'in', existsQuery); - } else { - this.andWhere(onEntityIdField, 'not in', existsQuery); - } - } else if (isInValue(value)) { - // Handle $in - const values = value.$in.map(v => - String(v).toLocaleLowerCase('en-US'), - ); - const matchQuery = knex('search') - .select('search.entity_id') - .where({ key: normalizedKey }) - .whereIn('value', values); - this.andWhere(onEntityIdField, 'in', matchQuery); - } else if (isHasPrefixValue(value)) { - // Handle $hasPrefix - const prefix = value.$hasPrefix.toLocaleLowerCase('en-US'); - const escaped = prefix.replace(/[%_\\]/g, c => `\\${c}`); - const matchQuery = knex('search') - .select('search.entity_id') - .where({ key: normalizedKey }) - .andWhereRaw('?? like ? escape ?', ['value', `${escaped}%`, '\\']); - this.andWhere(onEntityIdField, 'in', matchQuery); - } else if (isPrimitive(value)) { - // Handle direct value match - const matchQuery = knex('search') - .select('search.entity_id') - .where({ - key: normalizedKey, - value: String(value).toLocaleLowerCase('en-US'), - }); - this.andWhere(onEntityIdField, 'in', matchQuery); - } else { - // Reject unsupported/invalid predicate values - throw new InputError( - `Invalid filter predicate value for field "${key}": expected a primitive value, $exists, $in, or $hasPrefix operator, but got ${JSON.stringify( - value, - )}`, - ); - } + if ('$all' in filter) { + if (filter.$all.length === 0) { + return targetQuery.andWhereRaw('1 = 0'); + } + return targetQuery.andWhere(outer => { + for (const subFilter of filter.$all) { + outer.andWhere(inner => + applyPredicateEntityFilterToQuery({ + filter: subFilter, + targetQuery: inner, + onEntityIdField, + knex, + }), + ); } }); } - return targetQuery; + if ('$any' in filter) { + if (filter.$any.length === 0) { + return targetQuery.andWhereRaw('1 = 0'); + } + return targetQuery.andWhere(outer => { + for (const subFilter of filter.$any) { + outer.orWhere(inner => + applyPredicateEntityFilterToQuery({ + filter: subFilter, + targetQuery: inner, + onEntityIdField, + knex, + }), + ); + } + }); + } + + // Treat the filter as a field expression like { "kind": "component" } or { "spec.type": { "$in": ["service", "website"] } } + if (Object.keys(filter).length === 0) { + return targetQuery; + } + return targetQuery.andWhere(inner => { + for (const [keyAnyCase, value] of Object.entries(filter)) { + applyFieldCondition({ + key: keyAnyCase.toLocaleLowerCase('en-US'), + value, + targetQuery: inner, + onEntityIdField, + knex, + }); + } + }); +} + +/** + * Applies a single { key: value } filter to the target query. + */ +function applyFieldCondition(options: { + key: string; + value: FilterPredicateValue; + targetQuery: Knex.QueryBuilder; + onEntityIdField: string; + knex: Knex; +}): Knex.QueryBuilder { + const { key, value, targetQuery, onEntityIdField, knex } = options; + + if (isPrimitive(value)) { + const matchQuery = knex('search') + .select('search.entity_id') + .where({ + key, + value: String(value).toLocaleLowerCase('en-US'), + }); + return targetQuery.andWhere(onEntityIdField, 'in', matchQuery); + } + + if (isObject(value)) { + if ('$exists' in value) { + const existsQuery = knex('search') + .select('search.entity_id') + .where({ key }); + if (value.$exists) { + return targetQuery.andWhere(onEntityIdField, 'in', existsQuery); + } + return targetQuery.andWhere(onEntityIdField, 'not in', existsQuery); + } + + if ('$in' in value) { + const values = (value.$in as FilterPredicatePrimitive[]).map(v => + String(v).toLocaleLowerCase('en-US'), + ); + const matchQuery = knex('search') + .select('search.entity_id') + .where({ key }) + .whereIn('value', values); + return targetQuery.andWhere(onEntityIdField, 'in', matchQuery); + } + + if ('$hasPrefix' in value) { + const prefix = (value.$hasPrefix as string).toLocaleLowerCase('en-US'); + const escaped = prefix.replace(/[%_\\]/g, c => `\\${c}`); + const matchQuery = knex('search') + .select('search.entity_id') + .where({ key }) + .andWhereRaw('?? like ? escape ?', ['value', `${escaped}%`, '\\']); + return targetQuery.andWhere(onEntityIdField, 'in', matchQuery); + } + } + + throw new InputError( + `Invalid filter predicate value for field "${key}": expected a primitive value, $exists, $in, or $hasPrefix operator, but got ${JSON.stringify( + value, + )}`, + ); }