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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user