Merge pull request #16189 from acierto/enhance-schema-validator
Scaffolder template. Added a missing validator check for items in an array
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
---
|
||||
|
||||
Added a missing validator check for items in an array
|
||||
@@ -272,4 +272,59 @@ describe('createAsyncValidators', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should call a validator for array property from a custom field extension', async () => {
|
||||
const schema: JsonObject = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tags: {
|
||||
title: 'Tags',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
'ui:field': 'TagField',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const validators = { TagField: jest.fn() };
|
||||
|
||||
const validate = createAsyncValidators(schema, validators, {
|
||||
apiHolder: { get: jest.fn() },
|
||||
});
|
||||
|
||||
await validate({
|
||||
tags: ['tag-1', 'tag-2'],
|
||||
});
|
||||
|
||||
expect(validators.TagField).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should does not call a validator if no ui field specified', async () => {
|
||||
const schema: JsonObject = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tags: {
|
||||
title: 'Tags',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const validators = { TagField: jest.fn() };
|
||||
|
||||
const validate = createAsyncValidators(schema, validators, {
|
||||
apiHolder: { get: jest.fn() },
|
||||
});
|
||||
|
||||
await validate({
|
||||
tags: ['asd', 'asd$'],
|
||||
});
|
||||
|
||||
expect(validators.TagField).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { FieldValidation } from '@rjsf/utils';
|
||||
import type { JsonObject } from '@backstage/types';
|
||||
import type { JsonObject, JsonValue } from '@backstage/types';
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { Draft07 as JSONSchema } from 'json-schema-library';
|
||||
import { createFieldValidation } from '../../lib';
|
||||
@@ -44,20 +44,38 @@ export const createAsyncValidators = (
|
||||
const parsedSchema = new JSONSchema(rootSchema);
|
||||
const formValidation: FormValidation = {};
|
||||
|
||||
const validateForm = async (
|
||||
validatorName: string,
|
||||
key: string,
|
||||
value: JsonValue | undefined,
|
||||
) => {
|
||||
const validator = validators[validatorName];
|
||||
if (validator) {
|
||||
const fieldValidation = createFieldValidation();
|
||||
try {
|
||||
await validator(value, fieldValidation, { ...context, formData });
|
||||
} catch (ex) {
|
||||
fieldValidation.addError(ex.message);
|
||||
}
|
||||
formValidation[key] = fieldValidation;
|
||||
}
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(current)) {
|
||||
const path = `${pathPrefix}/${key}`;
|
||||
const definitionInSchema = parsedSchema.getSchema(path, formData);
|
||||
|
||||
if (definitionInSchema && 'ui:field' in definitionInSchema) {
|
||||
const validator = validators[definitionInSchema['ui:field']];
|
||||
if (validator) {
|
||||
const fieldValidation = createFieldValidation();
|
||||
try {
|
||||
await validator(value, fieldValidation, { ...context, formData });
|
||||
} catch (ex) {
|
||||
fieldValidation.addError(ex.message);
|
||||
}
|
||||
formValidation[key] = fieldValidation;
|
||||
if ('ui:field' in definitionInSchema) {
|
||||
await validateForm(definitionInSchema['ui:field'], key, value);
|
||||
}
|
||||
} else if (
|
||||
definitionInSchema &&
|
||||
definitionInSchema.items &&
|
||||
'ui:field' in definitionInSchema.items
|
||||
) {
|
||||
if ('ui:field' in definitionInSchema.items) {
|
||||
await validateForm(definitionInSchema.items['ui:field'], key, value);
|
||||
}
|
||||
} else if (isObject(value)) {
|
||||
formValidation[key] = await validate(formData, path, value);
|
||||
|
||||
@@ -17,14 +17,32 @@
|
||||
import { createValidator } from './createValidator';
|
||||
import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react';
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { FormValidation } from '@rjsf/core';
|
||||
import { FieldValidation, FormValidation } from '@rjsf/core';
|
||||
|
||||
describe('createValidator', () => {
|
||||
const validators: Record<string, undefined | CustomFieldValidator<unknown>> =
|
||||
{
|
||||
CustomPicker: (value, validation, _context) => {
|
||||
CustomPicker: (
|
||||
value: unknown,
|
||||
fieldValidation: FieldValidation,
|
||||
_context: { apiHolder: ApiHolder },
|
||||
) => {
|
||||
if (!value || !(value as { value?: unknown }).value) {
|
||||
validation.addError('Error !');
|
||||
fieldValidation.addError('Error !');
|
||||
}
|
||||
},
|
||||
TagPicker: (
|
||||
values: unknown,
|
||||
fieldValidation: FieldValidation,
|
||||
_context: { apiHolder: ApiHolder },
|
||||
) => {
|
||||
const input = values as string[];
|
||||
for (const item of input) {
|
||||
if (!/^[a-z0-9-]+$/.test(item)) {
|
||||
fieldValidation.addError(
|
||||
'A tag name can only contain lowercase letters, numeric characters or dashes',
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -39,7 +57,7 @@ describe('createValidator', () => {
|
||||
apiHolder: apiHolderMock,
|
||||
};
|
||||
|
||||
it('Should call validator for object property from a custom field extension', () => {
|
||||
it('should call validator for object property from a custom field extension', () => {
|
||||
/* GIVEN */
|
||||
const rootSchema = {
|
||||
title: 'Title',
|
||||
@@ -70,4 +88,39 @@ describe('createValidator', () => {
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.p1.addError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call validator for array property from a custom field extension', () => {
|
||||
/* GIVEN */
|
||||
const rootSchema = {
|
||||
title: 'My form',
|
||||
properties: {
|
||||
tags: {
|
||||
title: 'Tags',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
'ui:field': 'TagPicker',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const validator = createValidator(rootSchema, validators, context);
|
||||
|
||||
const formData = {
|
||||
tags: ['invalid-tag$$'],
|
||||
};
|
||||
const errors = {
|
||||
addError: jest.fn(),
|
||||
tags: {
|
||||
addError: jest.fn(),
|
||||
} as unknown as FormValidation,
|
||||
} as unknown as FormValidation;
|
||||
|
||||
/* WHEN */
|
||||
const result = validator(formData, errors);
|
||||
|
||||
/* THEN */
|
||||
expect(result).not.toBeNull();
|
||||
expect(result.tags.addError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,10 @@ function isObject(obj: unknown): obj is JsonObject {
|
||||
return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
|
||||
}
|
||||
|
||||
function isArray(obj: unknown): obj is JsonObject {
|
||||
return typeof obj === 'object' && obj !== null && Array.isArray(obj);
|
||||
}
|
||||
|
||||
export const createValidator = (
|
||||
rootSchema: JsonObject,
|
||||
validators: Record<string, undefined | CustomFieldValidator<unknown>>,
|
||||
@@ -46,8 +50,8 @@ export const createValidator = (
|
||||
for (const [key, propData] of Object.entries(formData)) {
|
||||
const propValidation = errors[key];
|
||||
|
||||
const propSchemaProps = schemaProps[key];
|
||||
if (isObject(propData)) {
|
||||
const propSchemaProps = schemaProps[key];
|
||||
if (isObject(propSchemaProps)) {
|
||||
validate(
|
||||
propSchemaProps,
|
||||
@@ -55,10 +59,24 @@ export const createValidator = (
|
||||
propValidation as FormValidation,
|
||||
);
|
||||
}
|
||||
} else if (isArray(propData)) {
|
||||
if (isObject(propSchemaProps)) {
|
||||
const { items } = propSchemaProps;
|
||||
if (isObject(items)) {
|
||||
const fieldName = items['ui:field'] as string;
|
||||
if (fieldName && typeof validators[fieldName] === 'function') {
|
||||
validators[fieldName]!(
|
||||
propData as JsonObject[],
|
||||
propValidation,
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const propSchema = schemaProps[key];
|
||||
const fieldName =
|
||||
isObject(propSchema) && (propSchema['ui:field'] as string);
|
||||
isObject(propSchemaProps) &&
|
||||
(propSchemaProps['ui:field'] as string);
|
||||
if (fieldName && typeof validators[fieldName] === 'function') {
|
||||
validators[fieldName]!(
|
||||
propData as JsonValue,
|
||||
|
||||
Reference in New Issue
Block a user