catalog-react: merge $all and $elemMatch into $contains + allow primitives at root level

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-02-24 00:38:11 +01:00
parent 99306adc3c
commit 6047a3e1b3
5 changed files with 58 additions and 40 deletions
@@ -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 } } },
@@ -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<EntityPredicateValue>;
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<EntityPredicate>;
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<EntityPredicateValue>;
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<EntityPredicate>;
return filterSchema;
return predicateSchema;
}
@@ -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),
);
@@ -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);
@@ -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;