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 <ben@blam.sh>
This commit is contained in:
blam
2022-08-03 14:56:39 +02:00
parent 7b186350d8
commit d120031676
4 changed files with 221 additions and 45 deletions
+1
View File
@@ -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",
@@ -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<string> = (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<unknown>,
AddressField: AddressField as CustomFieldValidator<unknown>,
},
{
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!'],
}),
});
});
});
@@ -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<string, FieldValidation> = {};
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);
};
};
+40
View File
@@ -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"