Merge pull request #32065 from karpfslp/extend-entity-predicate-value-type
fix: $contains may have string value in an entity filter.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
$contains may have string value in an entity filter. Typescript type and Zod parser were not the same.
|
||||
@@ -549,7 +549,7 @@ export type EntityPredicateValue =
|
||||
$in: EntityPredicatePrimitive[];
|
||||
}
|
||||
| {
|
||||
$contains: EntityPredicateExpression;
|
||||
$contains: EntityPredicate;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
|
||||
@@ -60,20 +60,75 @@ describe('EntityCardBlueprint', () => {
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
],
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
],
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$exists": {
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$exists",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$in": {
|
||||
"items": {
|
||||
"$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0/additionalProperties/anyOf/0",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$in",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$contains": {
|
||||
"$ref": "#/properties/filter/anyOf/1",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$contains",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
"propertyNames": {
|
||||
"pattern": "^(?!\\$).*$",
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0",
|
||||
"additionalProperties": {
|
||||
"not": {},
|
||||
},
|
||||
"type": "array",
|
||||
"propertyNames": {
|
||||
"pattern": "^\\$",
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0/additionalProperties/anyOf/0",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -116,58 +171,6 @@ describe('EntityCardBlueprint', () => {
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/properties/filter/anyOf/1/anyOf/0",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$exists": {
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$exists",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$in": {
|
||||
"items": {
|
||||
"$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$in",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$contains": {
|
||||
"$ref": "#/properties/filter/anyOf/1",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$contains",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
"propertyNames": {
|
||||
"pattern": "^(?!\\$).*$",
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -62,20 +62,75 @@ describe('EntityContentBlueprint', () => {
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
],
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
],
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$exists": {
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$exists",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$in": {
|
||||
"items": {
|
||||
"$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0/additionalProperties/anyOf/0",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$in",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$contains": {
|
||||
"$ref": "#/properties/filter/anyOf/1",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$contains",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
"propertyNames": {
|
||||
"pattern": "^(?!\\$).*$",
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0",
|
||||
"additionalProperties": {
|
||||
"not": {},
|
||||
},
|
||||
"type": "array",
|
||||
"propertyNames": {
|
||||
"pattern": "^\\$",
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0/additionalProperties/anyOf/0",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -118,58 +173,6 @@ describe('EntityContentBlueprint', () => {
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/properties/filter/anyOf/1/anyOf/0",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$exists": {
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$exists",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$in": {
|
||||
"items": {
|
||||
"$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$in",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$contains": {
|
||||
"$ref": "#/properties/filter/anyOf/1",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$contains",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
"propertyNames": {
|
||||
"pattern": "^(?!\\$).*$",
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
+63
-60
@@ -73,20 +73,75 @@ describe('EntityContextMenuItemBlueprint', () => {
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
],
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": [
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
],
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$exists": {
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$exists",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$in": {
|
||||
"items": {
|
||||
"$ref": "#/properties/filter/anyOf/0/anyOf/0/additionalProperties/anyOf/0",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$in",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$contains": {
|
||||
"$ref": "#/properties/filter",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$contains",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
"propertyNames": {
|
||||
"pattern": "^(?!\\$).*$",
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/properties/filter/anyOf/0/anyOf/0",
|
||||
"additionalProperties": {
|
||||
"not": {},
|
||||
},
|
||||
"type": "array",
|
||||
"propertyNames": {
|
||||
"pattern": "^\\$",
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"$ref": "#/properties/filter/anyOf/0/anyOf/0/additionalProperties/anyOf/0",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -129,58 +184,6 @@ describe('EntityContextMenuItemBlueprint', () => {
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/properties/filter/anyOf/0",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$exists": {
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$exists",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$in": {
|
||||
"items": {
|
||||
"$ref": "#/properties/filter/anyOf/0/anyOf/0",
|
||||
},
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$in",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"$contains": {
|
||||
"$ref": "#/properties/filter",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"$contains",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
"propertyNames": {
|
||||
"pattern": "^(?!\\$).*$",
|
||||
},
|
||||
"type": "object",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -16,84 +16,100 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
import { createEntityPredicateSchema } from './createEntityPredicateSchema';
|
||||
import { EntityPredicate } from './types';
|
||||
|
||||
describe('createEntityPredicateSchema', () => {
|
||||
const schema = createEntityPredicateSchema(z);
|
||||
|
||||
it.each([
|
||||
'string',
|
||||
'',
|
||||
[],
|
||||
1,
|
||||
{ kind: 'component', 'spec.type': 'service' },
|
||||
{ 'metadata.tags': { $in: ['java'] } },
|
||||
{
|
||||
$all: [
|
||||
{ 'metadata.tags': { $contains: 'java' } },
|
||||
{ 'metadata.tags': { $contains: 'spring' } },
|
||||
],
|
||||
},
|
||||
{ 'metadata.tags': ['java', 'spring'] },
|
||||
{ 'metadata.tags': { $in: ['go'] } },
|
||||
{ 'metadata.tags.0': 'java' },
|
||||
{ $not: { 'metadata.tags': { $in: ['java'] } } },
|
||||
{
|
||||
$any: [{ kind: 'component', 'spec.type': 'service' }, { kind: 'group' }],
|
||||
},
|
||||
{
|
||||
relations: {
|
||||
$contains: { type: 'ownedBy', targetRef: 'group:default/g' },
|
||||
describe('valid predicates', () => {
|
||||
const predicates: EntityPredicate[] = [
|
||||
'string',
|
||||
'',
|
||||
1,
|
||||
{ kind: 'component', 'spec.type': 'service' },
|
||||
{ 'metadata.tags': { $in: ['java'] } },
|
||||
{ 'metadata.tags': { $contains: 'java' } },
|
||||
{
|
||||
$all: [
|
||||
{ 'metadata.tags': { $contains: 'java' } },
|
||||
{ 'metadata.tags': { $contains: 'spring' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
metadata: { $contains: { name: 'a' } },
|
||||
},
|
||||
{ kind: 'component', 'spec.type': { $in: ['service', 'website'] } },
|
||||
{
|
||||
$any: [
|
||||
{
|
||||
$all: [
|
||||
{
|
||||
kind: 'component',
|
||||
'spec.type': { $in: ['service', 'website'] },
|
||||
},
|
||||
],
|
||||
{ 'metadata.tags': { $in: ['go'] } },
|
||||
{ 'metadata.tags.0': 'java' },
|
||||
{ $not: { 'metadata.tags': { $in: ['java'] } } },
|
||||
{
|
||||
$any: [
|
||||
{ kind: 'component', 'spec.type': 'service' },
|
||||
{ kind: 'group' },
|
||||
],
|
||||
},
|
||||
{
|
||||
relations: {
|
||||
$contains: { type: 'ownedBy', targetRef: 'group:default/g' },
|
||||
},
|
||||
{ $all: [{ kind: 'api', 'spec.type': 'grpc' }] },
|
||||
],
|
||||
},
|
||||
{ kind: 'component', 'spec.type': { $in: ['service'] } },
|
||||
{ 'spec.owner': { $exists: true } },
|
||||
{ 'spec.owner': { $exists: false } },
|
||||
{ 'spec.type': 'service' },
|
||||
{ $not: { 'spec.type': 'service' } },
|
||||
{
|
||||
kind: 'component',
|
||||
'metadata.annotations.github.com/repo': { $exists: true },
|
||||
},
|
||||
{ $all: [{ x: { $exists: true } }] },
|
||||
{ $any: [{ x: { $exists: true } }] },
|
||||
{ $not: { x: { $exists: true } } },
|
||||
{ $not: { $all: [{ x: { $exists: true } }] } },
|
||||
])('should accept valid predicate %j', predicate => {
|
||||
expect(schema.parse(predicate)).toEqual(predicate);
|
||||
},
|
||||
{
|
||||
metadata: { $contains: { name: 'a' } },
|
||||
},
|
||||
{ kind: 'component', 'spec.type': { $in: ['service', 'website'] } },
|
||||
{
|
||||
$any: [
|
||||
{
|
||||
$all: [
|
||||
{
|
||||
kind: 'component',
|
||||
'spec.type': { $in: ['service', 'website'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ $all: [{ kind: 'api', 'spec.type': 'grpc' }] },
|
||||
],
|
||||
},
|
||||
{ kind: 'component', 'spec.type': { $in: ['service'] } },
|
||||
{ 'spec.owner': { $exists: true } },
|
||||
{ 'spec.owner': { $exists: false } },
|
||||
{ 'spec.type': 'service' },
|
||||
{ $not: { 'spec.type': 'service' } },
|
||||
{
|
||||
kind: 'component',
|
||||
'metadata.annotations.github.com/repo': { $exists: true },
|
||||
},
|
||||
{ $all: [{ x: { $exists: true } }] },
|
||||
{ $any: [{ x: { $exists: true } }] },
|
||||
{ $not: { x: { $exists: true } } },
|
||||
{ $not: { $all: [{ x: { $exists: true } }] } },
|
||||
];
|
||||
|
||||
it.each(predicates)('should accept valid predicate %j', predicate => {
|
||||
expect(schema.parse(predicate)).toEqual(predicate);
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ kind: { 1: 'foo' } },
|
||||
{ kind: { foo: 'bar' } },
|
||||
{ kind: { $unknown: 'foo' } },
|
||||
{ kind: { $in: 'foo' } },
|
||||
{ kind: { $in: [{ x: 'foo' }] } },
|
||||
{ kind: { $in: [{ x: 'foo' }] } },
|
||||
{ 'spec.type': null },
|
||||
{ $all: [{ x: { $unknown: true } }] },
|
||||
{ $any: [{ x: { $unknown: true } }] },
|
||||
{ $not: { x: { $unknown: true } } },
|
||||
{ $not: { $all: [{ x: { $unknown: true } }] } },
|
||||
{ $unknown: 'foo' },
|
||||
])('should reject invalid predicate %j', predicate => {
|
||||
const result = schema.safeParse(predicate);
|
||||
expect(result.success).toBe(false);
|
||||
describe('invalid predicates', () => {
|
||||
const predicates: Array<
|
||||
Exclude<EntityPredicate | unknown, EntityPredicate>
|
||||
> = [
|
||||
[],
|
||||
['foo', 'bar'],
|
||||
{ kind: { 1: 'foo' } },
|
||||
{ kind: { foo: 'bar' } },
|
||||
{ kind: { $unknown: 'foo' } },
|
||||
{ kind: { $in: 'foo' } },
|
||||
{ kind: { $in: [{ x: 'foo' }] } },
|
||||
{ kind: { $in: [{ x: 'foo' }] } },
|
||||
{ 'spec.type': null },
|
||||
{ $all: [{ x: { $unknown: true } }] },
|
||||
{ $any: [{ x: { $unknown: true } }] },
|
||||
{ $not: { x: { $unknown: true } } },
|
||||
{ $not: { $all: [{ x: { $unknown: true } }] } },
|
||||
{ $unknown: 'foo' },
|
||||
{ 'metadata.tags': ['foo', 'bar'] },
|
||||
];
|
||||
|
||||
it.each(predicates)('should reject invalid predicate %j', predicate => {
|
||||
const result = schema.safeParse(predicate);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,33 +14,44 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EntityPredicate, EntityPredicateValue } from './types';
|
||||
import {
|
||||
EntityPredicate,
|
||||
EntityPredicateExpression,
|
||||
EntityPredicatePrimitive,
|
||||
EntityPredicateValue,
|
||||
} from './types';
|
||||
import type { z as zImpl, ZodType } from 'zod';
|
||||
|
||||
/** @internal */
|
||||
export function createEntityPredicateSchema(z: typeof zImpl) {
|
||||
const primitiveSchema = z.union([z.string(), z.number(), z.boolean()]);
|
||||
|
||||
const comparableValueSchema = z.union([
|
||||
primitiveSchema,
|
||||
z.array(primitiveSchema),
|
||||
]);
|
||||
const primitiveSchema = z.union([
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.boolean(),
|
||||
]) as ZodType<EntityPredicatePrimitive>;
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let valuePredicateSchema: ZodType<EntityPredicateValue>;
|
||||
|
||||
const expressionSchema = z.lazy(() =>
|
||||
z.union([
|
||||
z.record(z.string().regex(/^(?!\$).*$/), valuePredicateSchema),
|
||||
z.record(z.string().regex(/^\$/), z.never()),
|
||||
]),
|
||||
) as ZodType<EntityPredicateExpression>;
|
||||
|
||||
const predicateSchema = z.lazy(() =>
|
||||
z.union([
|
||||
comparableValueSchema,
|
||||
expressionSchema,
|
||||
primitiveSchema,
|
||||
z.object({ $all: z.array(predicateSchema) }),
|
||||
z.object({ $any: z.array(predicateSchema) }),
|
||||
z.object({ $not: predicateSchema }),
|
||||
z.record(z.string().regex(/^(?!\$).*$/), valuePredicateSchema),
|
||||
]),
|
||||
) as ZodType<EntityPredicate>;
|
||||
|
||||
valuePredicateSchema = z.union([
|
||||
comparableValueSchema,
|
||||
primitiveSchema,
|
||||
z.object({ $exists: z.boolean() }),
|
||||
z.object({ $in: z.array(primitiveSchema) }),
|
||||
z.object({ $contains: predicateSchema }),
|
||||
|
||||
@@ -34,7 +34,7 @@ export type EntityPredicateValue =
|
||||
| EntityPredicatePrimitive
|
||||
| { $exists: boolean }
|
||||
| { $in: EntityPredicatePrimitive[] }
|
||||
| { $contains: EntityPredicateExpression };
|
||||
| { $contains: EntityPredicate };
|
||||
|
||||
/** @alpha */
|
||||
export type EntityPredicatePrimitive = string | number | boolean;
|
||||
|
||||
Reference in New Issue
Block a user