From 025e122c3e5147c91eb9e5b963170c797c0d5383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 27 Jan 2021 21:32:39 +0100 Subject: [PATCH] catalog-model: replace yup with ajv, for validation of catalog entities --- .changeset/little-pets-cross.md | 5 ++ .changeset/metal-pans-leave.md | 2 +- .changeset/six-ravens-heal.md | 2 +- packages/catalog-model/package.json | 4 +- .../policies/SchemaValidEntityPolicy.ts | 58 ++++++++----------- .../src/kinds/ApiEntityV1alpha1.ts | 24 +++----- .../src/kinds/ComponentEntityV1alpha1.ts | 26 +++------ .../src/kinds/DomainEntityV1alpha1.ts | 20 +++---- .../src/kinds/GroupEntityV1alpha1.ts | 37 +++--------- .../src/kinds/LocationEntityV1alpha1.ts | 22 +++---- .../src/kinds/ResourceEntityV1alpha1.ts | 22 +++---- .../src/kinds/SystemEntityV1alpha1.ts | 21 +++---- .../src/kinds/TemplateEntityV1alpha1.ts | 23 +++----- .../src/kinds/UserEntityV1alpha1.ts | 35 +++-------- packages/catalog-model/src/kinds/util.ts | 44 ++++++++++++-- .../{ => src}/schema/Entity.schema.json | 0 .../{ => src}/schema/EntityMeta.schema.json | 28 +++++++++ .../schema/kinds/API.v1alpha1.schema.json | 0 .../kinds/Component.v1alpha1.schema.json} | 0 .../schema/kinds/Domain.v1alpha1.schema.json | 0 .../schema/kinds/Group.v1alpha1.schema.json | 0 .../kinds/Location.v1alpha1.schema.json | 0 .../kinds/Resource.v1alpha1.schema.json | 0 .../schema/kinds/System.v1alpha1.schema.json | 0 .../kinds/Template.v1alpha1.schema.json | 0 .../schema/kinds/User.v1alpha1.schema.json | 0 .../schema/shared/common.schema.json | 0 yarn.lock | 22 +++++++ 28 files changed, 187 insertions(+), 208 deletions(-) create mode 100644 .changeset/little-pets-cross.md rename packages/catalog-model/{ => src}/schema/Entity.schema.json (100%) rename packages/catalog-model/{ => src}/schema/EntityMeta.schema.json (77%) rename packages/catalog-model/{ => src}/schema/kinds/API.v1alpha1.schema.json (100%) rename packages/catalog-model/{schema/kinds/Component.v1alpha1.schema copy.json => src/schema/kinds/Component.v1alpha1.schema.json} (100%) rename packages/catalog-model/{ => src}/schema/kinds/Domain.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/kinds/Group.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/kinds/Location.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/kinds/Resource.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/kinds/System.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/kinds/Template.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/kinds/User.v1alpha1.schema.json (100%) rename packages/catalog-model/{ => src}/schema/shared/common.schema.json (100%) diff --git a/.changeset/little-pets-cross.md b/.changeset/little-pets-cross.md new file mode 100644 index 0000000000..0f4e2605cd --- /dev/null +++ b/.changeset/little-pets-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Replace `yup` with `ajv`, for validation of catalog entities. diff --git a/.changeset/metal-pans-leave.md b/.changeset/metal-pans-leave.md index 249ffe7aec..16d5478e43 100644 --- a/.changeset/metal-pans-leave.md +++ b/.changeset/metal-pans-leave.md @@ -2,4 +2,4 @@ '@backstage/config-loader': patch --- -Bump config-loader to ajv 7, to enable v7 feature use elsewhere +Bump `config-loader` to `ajv` 7, to enable v7 feature use elsewhere diff --git a/.changeset/six-ravens-heal.md b/.changeset/six-ravens-heal.md index 91ed92c89c..960c482be9 100644 --- a/.changeset/six-ravens-heal.md +++ b/.changeset/six-ravens-heal.md @@ -2,4 +2,4 @@ '@backstage/catalog-model': patch --- -Introduce jsonschema variants of the yup vlidation schemas +Introduce json schema variants of the `yup` validation schemas diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 56d167d390..e81bc3a9b6 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -32,6 +32,7 @@ "@backstage/config": "^0.1.2", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.8", + "ajv": "^7.0.3", "json-schema": "^0.2.5", "lodash": "^4.17.15", "uuid": "^8.0.0", @@ -45,7 +46,6 @@ "yaml": "^1.9.2" }, "files": [ - "dist", - "schema" + "dist" ] } diff --git a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts index 2796826b20..7e0a8df268 100644 --- a/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/SchemaValidEntityPolicy.ts @@ -14,32 +14,13 @@ * limitations under the License. */ -import * as yup from 'yup'; -import { Entity, EntityLink } from '../Entity'; +import Ajv, { ValidateFunction } from 'ajv'; +import entitySchema from '../../schema/Entity.schema.json'; +import entityMetaSchema from '../../schema/EntityMeta.schema.json'; +import commonSchema from '../../schema/shared/common.schema.json'; +import { Entity } from '../Entity'; import { EntityPolicy } from './types'; -const DEFAULT_ENTITY_SCHEMA = yup - .object({ - apiVersion: yup.string().required(), - kind: yup.string().required(), - metadata: yup - .object({ - uid: yup.string().notRequired().min(1), - etag: yup.string().notRequired().min(1), - generation: yup.number().notRequired().integer().min(1), - name: yup.string().required(), - namespace: yup.string().notRequired(), - description: yup.string().notRequired(), - labels: yup.object>().notRequired(), - annotations: yup.object>().notRequired(), - tags: yup.array().notRequired(), - links: yup.array().notRequired(), - }) - .required(), - spec: yup.object({}).notRequired(), - }) - .required(); - /** * Ensures that the entity spec is valid according to a schema. * @@ -48,17 +29,28 @@ const DEFAULT_ENTITY_SCHEMA = yup * typescript type. */ export class SchemaValidEntityPolicy implements EntityPolicy { - private readonly schema: yup.Schema; - - constructor(schema: yup.Schema = DEFAULT_ENTITY_SCHEMA) { - this.schema = schema; - } + private validate: ValidateFunction | undefined; async enforce(entity: Entity): Promise { - try { - return await this.schema.validate(entity, { strict: true }); - } catch (e) { - throw new Error(`Malformed envelope, ${e}`); + if (!this.validate) { + const ajv = new Ajv({ allowUnionTypes: true }); + this.validate = ajv + .addSchema([commonSchema, entityMetaSchema], undefined, undefined, true) + .compile(entitySchema); } + + const result = this.validate(entity); + if (result === true) { + return entity; + } + + const [error] = this.validate.errors || []; + if (!error) { + throw new Error(`Malformed envelope, Unknown error`); + } + + throw new Error( + `Malformed envelope, ${error.dataPath || ''} ${error.message}`, + ); } } diff --git a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts index 2c634ff091..5432cafdeb 100644 --- a/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ApiEntityV1alpha1.ts @@ -14,27 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/API.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'API' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - lifecycle: yup.string().required().min(1), - owner: yup.string().required().min(1), - definition: yup.string().required().min(1), - system: yup.string().notRequired().min(1), - }) - .required(), -}); - export interface ApiEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -47,8 +36,9 @@ export interface ApiEntityV1alpha1 extends Entity { }; } -export const apiEntityV1alpha1Validator = schemaValidator( +export const apiEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts index c55c48055a..3006a4288c 100644 --- a/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ComponentEntityV1alpha1.ts @@ -14,29 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/Component.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Component' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - lifecycle: yup.string().required().min(1), - owner: yup.string().required().min(1), - subcomponentOf: yup.string().notRequired().min(1), - providesApis: yup.array(yup.string().required()).notRequired(), - consumesApis: yup.array(yup.string().required()).notRequired(), - system: yup.string().notRequired().min(1), - }) - .required(), -}); - export interface ComponentEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -51,8 +38,9 @@ export interface ComponentEntityV1alpha1 extends Entity { }; } -export const componentEntityV1alpha1Validator = schemaValidator( +export const componentEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts index 60b11aa124..7aab35e367 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts @@ -14,23 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/Domain.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Domain' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - owner: yup.string().required().min(1), - }) - .required(), -}); - export interface DomainEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -39,8 +32,9 @@ export interface DomainEntityV1alpha1 extends Entity { }; } -export const domainEntityV1alpha1Validator = schemaValidator( +export const domainEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts index 6f2664af77..d039fdaba8 100644 --- a/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/GroupEntityV1alpha1.ts @@ -14,40 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/Group.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Group' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - profile: yup - .object({ - displayName: yup.string().min(1).notRequired(), - email: yup.string().min(1).notRequired(), - picture: yup.string().min(1).notRequired(), - }) - .notRequired(), - parent: yup.string().notRequired().min(1), - // Use these manual tests because yup .required() requires at least - // one element and there is no simple workaround -_- - // the cast is there to convince typescript that the array itself is - // required without using .required() - children: yup.array(yup.string().required()).test({ - name: 'isDefined', - message: 'children must be defined', - test: v => Boolean(v), - }) as yup.ArraySchema, - }) - .required(), -}); - export interface GroupEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -63,8 +39,9 @@ export interface GroupEntityV1alpha1 extends Entity { }; } -export const groupEntityV1alpha1Validator = schemaValidator( +export const groupEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts index 9cd767de94..fb452b6ac7 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts @@ -14,25 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/Location.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Location' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().notRequired().min(1), - target: yup.string().notRequired().min(1), - targets: yup.array(yup.string().required()).notRequired(), - }) - .required(), -}); - export interface LocationEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -43,8 +34,9 @@ export interface LocationEntityV1alpha1 extends Entity { }; } -export const locationEntityV1alpha1Validator = schemaValidator( +export const locationEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts index 12df7f6664..520a39f02c 100644 --- a/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/ResourceEntityV1alpha1.ts @@ -14,25 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/Resource.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Resource' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - owner: yup.string().required().min(1), - system: yup.string().notRequired().min(1), - }) - .required(), -}); - export interface ResourceEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -43,8 +34,9 @@ export interface ResourceEntityV1alpha1 extends Entity { }; } -export const resourceEntityV1alpha1Validator = schemaValidator( +export const resourceEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts index 764514efdd..1ee19466f0 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts @@ -14,24 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/System.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'System' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - owner: yup.string().required().min(1), - domain: yup.string().notRequired().min(1), - }) - .required(), -}); - export interface SystemEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -41,8 +33,9 @@ export interface SystemEntityV1alpha1 extends Entity { }; } -export const systemEntityV1alpha1Validator = schemaValidator( +export const systemEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts index b16fcdc7e4..72479c7efa 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1alpha1.ts @@ -14,27 +14,17 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; +import schema from '../schema/kinds/Template.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; import type { JSONSchema } from '../types'; -import { schemaValidator } from './util'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'Template' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - type: yup.string().required().min(1), - path: yup.string(), - schema: yup.object().required(), - templater: yup.string().required(), - }) - .required(), -}); - export interface TemplateEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -46,8 +36,9 @@ export interface TemplateEntityV1alpha1 extends Entity { }; } -export const templateEntityV1alpha1Validator = schemaValidator( +export const templateEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts index 16a5a86e05..a8700a496e 100644 --- a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts @@ -14,38 +14,16 @@ * limitations under the License. */ -import * as yup from 'yup'; import type { Entity } from '../entity/Entity'; -import { schemaValidator } from './util'; +import schema from '../schema/kinds/User.v1alpha1.schema.json'; +import entitySchema from '../schema/Entity.schema.json'; +import entityMetaSchema from '../schema/EntityMeta.schema.json'; +import commonSchema from '../schema/shared/common.schema.json'; +import { ajvCompiledJsonSchemaValidator } from './util'; const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; const KIND = 'User' as const; -const schema = yup.object>({ - apiVersion: yup.string().required().oneOf(API_VERSION), - kind: yup.string().required().equals([KIND]), - spec: yup - .object({ - profile: yup - .object({ - displayName: yup.string().min(1).notRequired(), - email: yup.string().min(1).notRequired(), - picture: yup.string().min(1).notRequired(), - }) - .notRequired(), - // Use this manual test because yup .required() requires at least one - // element and there is no simple workaround -_- - // the cast is there to convince typescript that the array itself is - // required without using .required() - memberOf: yup.array(yup.string().required()).test({ - name: 'isDefined', - message: 'memberOf must be defined', - test: v => Boolean(v), - }) as yup.ArraySchema, - }) - .required(), -}); - export interface UserEntityV1alpha1 extends Entity { apiVersion: typeof API_VERSION[number]; kind: typeof KIND; @@ -59,8 +37,9 @@ export interface UserEntityV1alpha1 extends Entity { }; } -export const userEntityV1alpha1Validator = schemaValidator( +export const userEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator( KIND, API_VERSION, schema, + [commonSchema, entityMetaSchema, entitySchema], ); diff --git a/packages/catalog-model/src/kinds/util.ts b/packages/catalog-model/src/kinds/util.ts index f1b02cfba1..4002f2806c 100644 --- a/packages/catalog-model/src/kinds/util.ts +++ b/packages/catalog-model/src/kinds/util.ts @@ -14,9 +14,13 @@ * limitations under the License. */ +import Ajv, { AnySchema } from 'ajv'; import * as yup from 'yup'; import { KindValidator } from './types'; +/** + * @deprecated We no longer use yup for the catalog model. This utility method will be removed. + */ export function schemaValidator( kind: string, apiVersion: readonly string[], @@ -24,10 +28,7 @@ export function schemaValidator( ): KindValidator { return { async check(envelope) { - if ( - kind !== envelope.kind || - !apiVersion.includes(envelope.apiVersion as any) - ) { + if (kind !== envelope.kind || !apiVersion.includes(envelope.apiVersion)) { return false; } await schema.validate(envelope, { strict: true }); @@ -35,3 +36,38 @@ export function schemaValidator( }, }; } + +export function ajvCompiledJsonSchemaValidator( + kind: string, + apiVersion: readonly string[], + schema: AnySchema, + extraSchemas?: AnySchema[], +): KindValidator { + const ajv = new Ajv({ allowUnionTypes: true }); + if (extraSchemas) { + ajv.addSchema(extraSchemas, undefined, undefined, true); + } + const validate = ajv.compile(schema); + + return { + async check(envelope) { + if (kind !== envelope.kind || !apiVersion.includes(envelope.apiVersion)) { + return false; + } + + const result = validate(envelope); + if (result === true) { + return true; + } + + const [error] = validate.errors || []; + if (!error) { + throw new TypeError(`Malformed ${kind}, Unknown error`); + } + + throw new TypeError( + `Malformed ${kind}, ${error.dataPath || ''} ${error.message}`, + ); + }, + }; +} diff --git a/packages/catalog-model/schema/Entity.schema.json b/packages/catalog-model/src/schema/Entity.schema.json similarity index 100% rename from packages/catalog-model/schema/Entity.schema.json rename to packages/catalog-model/src/schema/Entity.schema.json diff --git a/packages/catalog-model/schema/EntityMeta.schema.json b/packages/catalog-model/src/schema/EntityMeta.schema.json similarity index 77% rename from packages/catalog-model/schema/EntityMeta.schema.json rename to packages/catalog-model/src/schema/EntityMeta.schema.json index d44ca84bf4..ff0f9c84a8 100644 --- a/packages/catalog-model/schema/EntityMeta.schema.json +++ b/packages/catalog-model/src/schema/EntityMeta.schema.json @@ -84,6 +84,34 @@ "type": "string", "minLength": 1 } + }, + "links": { + "type": "array", + "description": "A list of external hyperlinks related to the entity. Links can provide additional contextual information that may be located outside of Backstage itself. For example, an admin dashboard or external CMS page.", + "items": { + "type": "object", + "required": ["url"], + "properties": { + "url": { + "type": "string", + "description": "A url in a standard uri format.", + "examples": ["https://admin.example-org.com"], + "minLength": 1 + }, + "title": { + "type": "string", + "description": "A user friendly display name for the link.", + "examples": ["Admin Dashboard"], + "minLength": 1 + }, + "icon": { + "type": "string", + "description": "A key representing a visual icon to be displayed in the UI.", + "examples": ["dashboard"], + "minLength": 1 + } + } + } } } } diff --git a/packages/catalog-model/schema/kinds/API.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/API.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/API.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/API.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/Component.v1alpha1.schema copy.json b/packages/catalog-model/src/schema/kinds/Component.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/Component.v1alpha1.schema copy.json rename to packages/catalog-model/src/schema/kinds/Component.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/Domain.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/Domain.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/Group.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Group.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/Group.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/Group.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/Location.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Location.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/Location.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/Location.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/Resource.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Resource.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/Resource.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/Resource.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/System.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/System.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/Template.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/Template.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/Template.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/kinds/User.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/User.v1alpha1.schema.json similarity index 100% rename from packages/catalog-model/schema/kinds/User.v1alpha1.schema.json rename to packages/catalog-model/src/schema/kinds/User.v1alpha1.schema.json diff --git a/packages/catalog-model/schema/shared/common.schema.json b/packages/catalog-model/src/schema/shared/common.schema.json similarity index 100% rename from packages/catalog-model/schema/shared/common.schema.json rename to packages/catalog-model/src/schema/shared/common.schema.json diff --git a/yarn.lock b/yarn.lock index 43bcf73364..f553207d23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2478,6 +2478,7 @@ "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" + ajv "^7.0.3" json-schema "^0.2.5" lodash "^4.17.15" uuid "^8.0.0" @@ -2489,6 +2490,7 @@ "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" + ajv "^7.0.3" json-schema "^0.2.5" lodash "^4.17.15" uuid "^8.0.0" @@ -7970,6 +7972,16 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.1, ajv@^6.10.2, ajv@^6.12.4, ajv@^6.12.5, ajv json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^7.0.3: + version "7.0.3" + resolved "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz#13ae747eff125cafb230ac504b2406cf371eece2" + integrity sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + alphanum-sort@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" @@ -16954,6 +16966,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-schema@0.2.3: version "0.2.3" resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -22693,6 +22710,11 @@ require-directory@^2.1.1: resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"