From f3b3b1dafb8eddc77f0506b2bf949a9c9568bab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 20 Feb 2026 10:55:24 +0100 Subject: [PATCH] implement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../applyPredicateEntityFilterToQuery.test.ts | 12 +++++++++++ .../applyPredicateEntityFilterToQuery.ts | 21 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.test.ts b/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.test.ts index 63b0bb9b24..2f216fd219 100644 --- a/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.test.ts +++ b/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.test.ts @@ -181,6 +181,18 @@ describe.each(databases.eachSupportedId())( ).resolves.toEqual(['bare-e']); }); + it('filters with $hasPrefix', async () => { + await expect( + query({ 'metadata.name': { $hasPrefix: 'service' } }), + ).resolves.toEqual(['service-a', 'service-b']); + }); + + it('filters with $hasPrefix case-insensitively', async () => { + await expect( + query({ 'metadata.name': { $hasPrefix: 'Service' } }), + ).resolves.toEqual(['service-a', 'service-b']); + }); + it('handles nested logical operators', 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 db39a2195a..d077bd1460 100644 --- a/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.ts +++ b/plugins/catalog-backend/src/service/request/applyPredicateEntityFilterToQuery.ts @@ -61,6 +61,12 @@ function isInValue( 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; @@ -202,6 +208,19 @@ function applyPredicateInStrategy( .where({ key: normalizedKey }) .whereIn('value', values); this.andWhere(onEntityIdField, 'in', matchQuery); + } else if (isHasPrefixValue(value)) { + // Handle $hasPrefix + const prefix = value.$hasPrefix.toLowerCase(); + 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') @@ -214,7 +233,7 @@ function applyPredicateInStrategy( } else { // Reject unsupported/invalid predicate values throw new InputError( - `Invalid filter predicate value for field "${key}": expected a primitive value, $exists, or $in operator, but got ${JSON.stringify( + `Invalid filter predicate value for field "${key}": expected a primitive value, $exists, $in, or $hasPrefix operator, but got ${JSON.stringify( value, )}`, );