From 6047a3e1b330fb1d8824c0f4d2087861858d9bce Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 24 Feb 2025 00:38:11 +0100 Subject: [PATCH] catalog-react: merge $all and $elemMatch into $contains + allow primitives at root level Signed-off-by: Patrik Oldsberg --- .../createEntityPredicateSchema.test.ts | 25 ++++++++------ .../predicates/createEntityPredicateSchema.ts | 33 +++++++++++-------- .../evaluateEntityPredicate.test.ts | 22 +++++++++---- .../predicates/evaluateEntityPredicate.ts | 14 ++++---- .../src/alpha/predicates/types.ts | 4 +-- 5 files changed, 58 insertions(+), 40 deletions(-) diff --git a/plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.test.ts b/plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.test.ts index 20824a5c2d..3df0b1a95f 100644 --- a/plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.test.ts +++ b/plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.test.ts @@ -21,23 +21,32 @@ describe('createEntityPredicateSchema', () => { const schema = createEntityPredicateSchema(z); it.each([ + 'string', + '', + [], + 1, { kind: 'component', 'spec.type': 'service' }, - { 'metadata.tags': { $all: ['java'] } }, - { 'metadata.tags': { $all: ['java', 'spring'] } }, + { 'metadata.tags': { $in: ['java'] } }, + { + $and: [ + { 'metadata.tags': { $contains: 'java' } }, + { 'metadata.tags': { $contains: 'spring' } }, + ], + }, { 'metadata.tags': ['java', 'spring'] }, - { 'metadata.tags': { $all: ['go'] } }, + { 'metadata.tags': { $in: ['go'] } }, { 'metadata.tags.0': 'java' }, - { $not: { 'metadata.tags': { $all: ['java'] } } }, + { $not: { 'metadata.tags': { $in: ['java'] } } }, { $or: [{ kind: 'component', 'spec.type': 'service' }, { kind: 'group' }], }, { relations: { - $elemMatch: { type: 'ownedBy', targetRef: 'group:default/g' }, + $contains: { type: 'ownedBy', targetRef: 'group:default/g' }, }, }, { - metadata: { $elemMatch: { name: 'a' } }, + metadata: { $contains: { name: 'a' } }, }, { kind: 'component', 'spec.type': { $in: ['service', 'website'] } }, { @@ -78,10 +87,6 @@ describe('createEntityPredicateSchema', () => { { kind: { $in: [{ x: 'foo' }] } }, { kind: { $in: [{ x: 'foo' }] } }, { 'spec.type': null }, - 'string', - '', - [], - 1, { $and: [{ x: { $unknown: true } }] }, { $or: [{ x: { $unknown: true } }] }, { $not: { x: { $unknown: true } } }, diff --git a/plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.ts b/plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.ts index 9b7efeb274..c9c5c4815c 100644 --- a/plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.ts +++ b/plugins/catalog-react/src/alpha/predicates/createEntityPredicateSchema.ts @@ -21,25 +21,32 @@ import type { z as zImpl, ZodType } from 'zod'; export function createEntityPredicateSchema(z: typeof zImpl) { const primitiveSchema = z.union([z.string(), z.number(), z.boolean()]); - const filterValueSchema = z.union([ + const comparableValueSchema = z.union([ primitiveSchema, z.array(primitiveSchema), + ]); + + // eslint-disable-next-line prefer-const + let valuePredicateSchema: ZodType; + + const predicateSchema = z.lazy(() => + z.union([ + comparableValueSchema, + z.object({ $and: z.array(predicateSchema) }), + z.object({ $or: z.array(predicateSchema) }), + z.object({ $not: predicateSchema }), + z.record(z.string().regex(/^(?!\$).*$/), valuePredicateSchema), + ]), + ) as ZodType; + + valuePredicateSchema = z.union([ + comparableValueSchema, z.object({ $exists: z.boolean() }), z.object({ $eq: z.union([primitiveSchema, z.array(primitiveSchema)]) }), z.object({ $ne: z.union([primitiveSchema, z.array(primitiveSchema)]) }), z.object({ $in: z.array(primitiveSchema) }), - z.object({ $all: z.array(primitiveSchema) }), - z.object({ $elemMatch: z.lazy(() => z.record(filterValueSchema)) }), + z.object({ $contains: predicateSchema }), ]) as ZodType; - const filterSchema = z.lazy(() => - z.union([ - z.object({ $and: z.array(filterSchema) }), - z.object({ $or: z.array(filterSchema) }), - z.object({ $not: filterSchema }), - z.record(z.string().regex(/^(?!\$).*$/), filterValueSchema), - ]), - ) as ZodType; - - return filterSchema; + return predicateSchema; } diff --git a/plugins/catalog-react/src/alpha/predicates/evaluateEntityPredicate.test.ts b/plugins/catalog-react/src/alpha/predicates/evaluateEntityPredicate.test.ts index 4d19a6234d..e6a46d4a2d 100644 --- a/plugins/catalog-react/src/alpha/predicates/evaluateEntityPredicate.test.ts +++ b/plugins/catalog-react/src/alpha/predicates/evaluateEntityPredicate.test.ts @@ -128,17 +128,25 @@ describe('evaluateEntityPredicate', () => { it.each([ ['s', { kind: 'component', 'spec.type': 'service' }], - ['s', { 'metadata.tags': { $all: ['java'] } }], - ['s', { 'metadata.tags': { $all: ['java', 'spring'] } }], + ['s', { 'metadata.tags': { $contains: 'java' } }], + [ + 's', + { + $and: [ + { 'metadata.tags': { $contains: 'java' } }, + { 'metadata.tags': { $contains: 'spring' } }, + ], + }, + ], ['s', { 'metadata.tags': ['java', 'spring'] }], ['', { 1: 'foo' }], ['s,w,g,a', {}], ['', { kind: { $unknown: 'foo' } }], ['', { '': 'component' }], ['s,w,g,a', Object.create({ kind: 'component' })], - ['', { 'metadata.tags': { $all: ['go'] } }], + ['', { 'metadata.tags': { $contains: 'go' } }], ['', { 'metadata.tags.0': 'java' }], - ['w,g,a', { $not: { 'metadata.tags': { $all: ['java'] } } }], + ['w,g,a', { $not: { 'metadata.tags': { $contains: 'java' } } }], [ 's,g', { @@ -160,14 +168,14 @@ describe('evaluateEntityPredicate', () => { 's,w,a', { relations: { - $elemMatch: { type: 'ownedBy', targetRef: 'group:default/g' }, + $contains: { type: 'ownedBy', targetRef: 'group:default/g' }, }, }, ], [ '', { - metadata: { $elemMatch: { name: 'a' } }, + metadata: { $contains: { name: 'a' } }, }, ], ['', { $unknown: 'ignored' } as unknown as EntityPredicate], @@ -213,7 +221,7 @@ describe('evaluateEntityPredicate', () => { 'metadata.annotations.github.com/repo': { $exists: true }, }, ], - ])('filter entry %s', (expected, filter) => { + ])('filter entry %#', (expected, filter) => { const filtered = entities.filter(entity => evaluateEntityPredicate(filter, entity), ); diff --git a/plugins/catalog-react/src/alpha/predicates/evaluateEntityPredicate.ts b/plugins/catalog-react/src/alpha/predicates/evaluateEntityPredicate.ts index 722442a8e1..b687773604 100644 --- a/plugins/catalog-react/src/alpha/predicates/evaluateEntityPredicate.ts +++ b/plugins/catalog-react/src/alpha/predicates/evaluateEntityPredicate.ts @@ -40,6 +40,10 @@ export function evaluateEntityPredicate( filter: EntityPredicate, value: JsonValue, ): boolean { + if (typeof filter !== 'object' || filter === null || Array.isArray(filter)) { + return valuesAreEqual(value, filter); + } + if ('$and' in filter) { return filter.$and.every(f => evaluateEntityPredicate(f, value)); } @@ -80,17 +84,11 @@ function evaluatePredicateValue( return valuesAreEqual(value, filter); } - if ('$elemMatch' in filter) { + if ('$contains' in filter) { if (!Array.isArray(value)) { return false; } - return value.some(v => evaluateEntityPredicate(filter.$elemMatch, v)); - } - if ('$all' in filter) { - if (!Array.isArray(value)) { - return false; - } - return filter.$all.every(v => value.includes(v)); + return value.some(v => evaluateEntityPredicate(filter.$contains, v)); } if ('$in' in filter) { return filter.$in.includes(value as EntityPredicatePrimitive); diff --git a/plugins/catalog-react/src/alpha/predicates/types.ts b/plugins/catalog-react/src/alpha/predicates/types.ts index 7c6e805e17..e6932c4006 100644 --- a/plugins/catalog-react/src/alpha/predicates/types.ts +++ b/plugins/catalog-react/src/alpha/predicates/types.ts @@ -17,6 +17,7 @@ /** @alpha */ export type EntityPredicate = | EntityPredicateExpression + | EntityPredicatePrimitive | { $and: EntityPredicate[] } | { $or: EntityPredicate[] } | { $not: EntityPredicate }; @@ -35,8 +36,7 @@ export type EntityPredicateValue = | { $eq: EntityPredicatePrimitive } | { $ne: EntityPredicatePrimitive } | { $in: EntityPredicatePrimitive[] } - | { $all: EntityPredicatePrimitive[] } - | { $elemMatch: EntityPredicateExpression }; + | { $contains: EntityPredicateExpression }; /** @alpha */ export type EntityPredicatePrimitive = string | number | boolean;