From d1200316767c8c69b18077ccbbf6e61f48f93233 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 3 Aug 2022 14:56:39 +0200 Subject: [PATCH] chore: added a library to work with the json schema form and make it easier to take into account all the different versions of jsonschema Signed-off-by: blam --- plugins/scaffolder/package.json | 1 + .../Stepper/createAsyncValidators.test.ts | 154 ++++++++++++++++++ .../Stepper/createAsyncValidators.ts | 71 +++----- yarn.lock | 40 +++++ 4 files changed, 221 insertions(+), 45 deletions(-) create mode 100644 plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.test.ts diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 626a20d73e..52be3be8a4 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -65,6 +65,7 @@ "humanize-duration": "^3.25.1", "immer": "^9.0.1", "json-schema": "^0.4.0", + "json-schema-library": "^6.0.0", "lodash": "^4.17.21", "luxon": "^3.0.0", "qs": "^6.9.4", diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.test.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.test.ts new file mode 100644 index 0000000000..f101ecf0f0 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.test.ts @@ -0,0 +1,154 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JsonObject } from '@backstage/types'; +import { CustomFieldValidator } from '../../../extensions'; +import { createAsyncValidator } from './createAsyncValidators'; + +describe('createAsyncValidators', () => { + it('should call the correct functions for validation', async () => { + const schema: JsonObject = { + type: 'object', + properties: { + name: { + type: 'string', + 'ui:field': 'NameField', + }, + address: { + type: 'object', + 'ui:field': 'AddressField', + properties: { + street: { + type: 'string', + }, + postcode: { + type: 'string', + }, + }, + }, + }, + }; + + const validators = { NameField: jest.fn(), AddressField: jest.fn() }; + + const validate = createAsyncValidator(schema, validators, { + apiHolder: { get: jest.fn() }, + }); + + await validate({ + name: 'asd', + address: { street: 'street', postcode: 'postcode' }, + }); + + expect(validators.NameField).toHaveBeenCalled(); + expect(validators.AddressField).toHaveBeenCalled(); + }); + + it('should return the correct errors to the frontend', async () => { + const schema: JsonObject = { + type: 'object', + properties: { + name: { + type: 'string', + 'ui:field': 'NameField', + }, + address: { + type: 'object', + 'ui:field': 'AddressField', + properties: { + street: { + type: 'string', + }, + postcode: { + type: 'string', + }, + }, + }, + }, + }; + + const NameField: CustomFieldValidator = (value, { addError }) => { + if (!value) { + addError('something is broken here!'); + } + }; + + const AddressField: CustomFieldValidator<{ + street?: string; + postcode?: string; + }> = (value, { addError }) => { + if (!value.postcode) { + addError('postcode is missing!'); + } + + if (!value.street) { + addError('street is missing here!'); + } + }; + + const validate = createAsyncValidator( + schema, + { + NameField: NameField as CustomFieldValidator, + AddressField: AddressField as CustomFieldValidator, + }, + { + apiHolder: { get: jest.fn() }, + }, + ); + + await expect( + validate({ + name: 'asd', + address: { street: 'street', postcode: 'postcode' }, + }), + ).resolves.toEqual({ + name: expect.objectContaining({ + __errors: [], + }), + address: expect.objectContaining({ + __errors: [], + }), + }); + + await expect( + validate({ + name: 'asd', + address: { street: '', postcode: 'postcode' }, + }), + ).resolves.toEqual({ + name: expect.objectContaining({ + __errors: [], + }), + address: expect.objectContaining({ + __errors: ['street is missing here!'], + }), + }); + + await expect( + validate({ + name: '', + address: { street: '', postcode: '' }, + }), + ).resolves.toEqual({ + name: expect.objectContaining({ + __errors: ['something is broken here!'], + }), + address: expect.objectContaining({ + __errors: ['postcode is missing!', 'street is missing here!'], + }), + }); + }); +}); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.ts index 613deb57b3..26a51bf376 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.ts +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/createAsyncValidators.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import { FormValidation } from '@rjsf/core'; -import { JsonObject, JsonValue } from '@backstage/types'; +import { FieldValidation, FormValidation } from '@rjsf/core'; +import { JsonObject } from '@backstage/types'; import { ApiHolder } from '@backstage/core-plugin-api'; import { CustomFieldValidator } from '../../../extensions'; +import { Draft07 as JSONSchema } from 'json-schema-library'; function isObject(obj: unknown): obj is JsonObject { return typeof obj === 'object' && obj !== null && !Array.isArray(obj); @@ -30,54 +31,34 @@ export const createAsyncValidator = ( apiHolder: ApiHolder; }, ) => { - async function validate( - schema: JsonObject, - formData: JsonObject, - errors: FormValidation, - ) { - const schemaProps = schema.properties; - const customObject = schema.type === 'object' && schemaProps === undefined; + async function validate(formData: JsonObject, pathPrefix: string = '#') { + const parsedSchema = new JSONSchema(rootSchema); + const formValidation: Record = {}; + for (const [key, value] of Object.entries(formData)) { + const definitionInSchema = parsedSchema.getSchema( + `${pathPrefix}/${key}`, + formData, + ); - if (!isObject(schemaProps) && !customObject) { - return; - } - - if (schemaProps) { - for (const [key, propData] of Object.entries(formData)) { - const propValidation = errors[key]; - - if (isObject(propData)) { - const propSchemaProps = schemaProps[key]; - if (isObject(propSchemaProps)) { - await validate( - propSchemaProps, - propData as JsonObject, - propValidation as FormValidation, - ); - } - } else { - const propSchema = schemaProps[key]; - const fieldName = - isObject(propSchema) && (propSchema['ui:field'] as string); - if (fieldName && typeof validators[fieldName] === 'function') { - await validators[fieldName]!( - propData as JsonValue, - propValidation, - context, - ); - } + if (definitionInSchema && 'ui:field' in definitionInSchema) { + const validator = validators[definitionInSchema['ui:field']]; + if (validator) { + const fieldValidation = { + __errors: [] as string[], + addError: (message: string) => { + fieldValidation.__errors.push(message); + }, + }; + await validator(value, fieldValidation, context); + formValidation[key] = fieldValidation; } } - } else if (customObject) { - const fieldName = schema['ui:field'] as string; - if (fieldName && typeof validators[fieldName] === 'function') { - await validators[fieldName]!(formData, errors, context); - } } + + return formValidation; } - return async (formData: JsonObject, errors: FormValidation) => { - await validate(rootSchema, formData, errors); - return errors; + return async (formData: JsonObject) => { + return await validate(formData); }; }; diff --git a/yarn.lock b/yarn.lock index e697cffa41..b95e1bfe8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12254,6 +12254,11 @@ easy-table@1.1.0: optionalDependencies: wcwidth ">=1.0.1" +ebnf@^1.9.0: + version "1.9.0" + resolved "https://registry.npmjs.org/ebnf/-/ebnf-1.9.0.tgz#9c2dd6052f3ed43a69c1f0b07b15bd03cefda764" + integrity sha512-LKK899+j758AgPq00ms+y90mo+2P86fMKUWD28sH0zLKUj7aL6iIH2wy4jejAMM9I2BawJ+2kp6C3mMXj+Ii5g== + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -14651,6 +14656,25 @@ grpc-docs@^1.0.6, grpc-docs@^1.1.2: rollup-plugin-smart-asset "^2.1.2" styled-components "^5.3.3" +gson-conform@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/gson-conform/-/gson-conform-1.0.3.tgz#6f982f98ea84199280bd48b6bfbcd0ae7447f1e2" + integrity sha512-gaZQN/5ZbohkLBOs4JKHkg/IdOB9kuuEr5SVLAjHUs+Q+Nl746DRe18GgQy4oxxVXStO//Zga2wg4eWL9Mfshw== + +gson-pointer@4.1.1, gson-pointer@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/gson-pointer/-/gson-pointer-4.1.1.tgz#088d6aa38d52753530288d13b5a75b6a43170e9b" + integrity sha512-rOS/zCLUI8BT0+/U+p5nzI0RfhIyRmzkpwzyCj8gcLoRl3rHuysk6ci1IQ955AQQZDSNN3mVec26Sx9wRZ0EjA== + +gson-query@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/gson-query/-/gson-query-5.1.0.tgz#8f34a062849f8c08be0c35eddaa2ad18a4326a49" + integrity sha512-xKb/90XfmLLKGgX8Y7LRF4yKnMR/ckV5WOQ/Ip0pRXuDh7jUhdY1UYjPvQnF7/UMbNsAo0168j2j0yt9+4QdaA== + dependencies: + ebnf "^1.9.0" + gson-conform "^1.0.3" + gson-pointer "4.1.1" + gtoken@^6.0.0: version "6.0.1" resolved "https://registry.npmjs.org/gtoken/-/gtoken-6.0.1.tgz#1276371d51e93c4eface76e3f30f9e8f1cad3f1a" @@ -16989,6 +17013,17 @@ json-schema-compare@^0.2.2: dependencies: lodash "^4.17.4" +json-schema-library@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/json-schema-library/-/json-schema-library-6.0.0.tgz#2a823d850031842c6917f41a8c576af3b603ab48" + integrity sha512-1nTpSmyfmgJpaNPIBO1SnHpXiMUh76hZnqSuCrJr7Lcu2N2SB55UPgf5F790r4dy4i4o/Moaw7OdqDhEmxP/Rw== + dependencies: + deepmerge "^4.2.2" + fast-deep-equal "^3.1.3" + gson-pointer "^4.1.1" + gson-query "^5.1.0" + valid-url "^1.0.9" + json-schema-merge-allof@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/json-schema-merge-allof/-/json-schema-merge-allof-0.6.0.tgz#64d48820fec26b228db837475ce3338936bf59a5" @@ -25926,6 +25961,11 @@ v8-to-istanbul@^8.1.0: convert-source-map "^1.6.0" source-map "^0.7.3" +valid-url@^1.0.9: + version "1.0.9" + resolved "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" + integrity sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA== + validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"