import { ComponentInputMetaSchema, JSONSchemaService } from './JsonSchemaService'; import { SchemaValidationError } from './errors/SchemaValidationError'; import { FormattedText } from './formatted-text/v1/formattedText'; import { JSONSchema } from '@squiz/json-schema-library'; import { AnyPrimitiveType, AnyResolvableType, PrimitiveType, ResolvableType, TypeResolver, } from './jsonTypeResolution/TypeResolver'; import { FormattedTextType, SquizImageType, SquizLinkType } from './primitiveTypes'; import { TypeResolverBuilder } from './jsonTypeResolution/TypeResolverBuilder'; import { MatrixAssetUri } from './validators/utils/matrixAssetValidator'; import { MatrixAssetLinkType, MatrixAssetType } from './resolvableTypes'; function expectToThrowErrorMatchingTypeAndMessage( // eslint-disable-next-line @typescript-eslint/ban-types received: Function, // eslint-disable-next-line @typescript-eslint/ban-types errorType: Function, message: string, validationExpected?: any, ) { let error: null | SchemaValidationError = null; try { received(); } catch (e: any) { error = e; } expect(error).toBeDefined(); expect(error?.message).toEqual(message); expect(error).toBeInstanceOf(errorType); expect(error?.validationData).toEqual(validationExpected); } const defaultSchema: JSONSchema = { type: 'object', properties: { myProperty: { type: 'string', }, }, required: ['myProperty'], }; function primitiveTypeFixture(title: T, schema: JSONSchema = defaultSchema) { return PrimitiveType({ ...schema, title, }); } function resolvableTypeFixture(title: T, schema: JSONSchema = defaultSchema) { return ResolvableType({ ...schema, title, }); } describe('JsonSchemaService', () => { let jsonSchemaService: JSONSchemaService; beforeAll(() => { const typeResolver = new TypeResolver([FormattedTextType]); jsonSchemaService = new JSONSchemaService(typeResolver, ComponentInputMetaSchema); }); describe('validateContentSchema', () => { it('should return true for a valid content schema', () => { const contentSchema = { type: 'object', properties: { 'my-input': { type: 'number' }, }, required: ['my-input'], }; const result = jsonSchemaService.validateInput(contentSchema); expect(result).toBe(true); }); it('should return false for an invalid content schema, where the required property is missed from a child object', () => { const contentSchema = { type: 'object', properties: { 'my-input': { type: 'object', properties: { 'my-deep-input': { type: 'object' }, }, }, }, required: ['my-input'], }; expectToThrowErrorMatchingTypeAndMessage( () => { jsonSchemaService.validateInput(contentSchema); }, SchemaValidationError, 'failed validation: The required property `required` is missing at `#/properties/my-input`', { '#/properties/my-input': [ { data: { key: 'required', pointer: '#/properties/my-input' }, message: 'The required property `required` is missing at `#/properties/my-input`', }, ], }, ); }); it('should throw a SchemaValidationError for an invalid content schema, top level type must be object', () => { const contentSchema = { type: 'array', items: { type: 'object', properties: { 'my-input': { type: 'number' }, }, required: ['my-input'], }, }; expectToThrowErrorMatchingTypeAndMessage( () => { jsonSchemaService.validateInput(contentSchema); }, SchemaValidationError, `failed validation: Expected value at \`#/type\` to be \`object\`, but value given is \`array\``, { '#/type': [ { data: { expected: 'object', pointer: '#/type', value: 'array' }, message: 'Expected value at `#/type` to be `object`, but value given is `array`', }, ], }, ); }); it('should throw a SchemaValidationError for an invalid content schema missing the required property', () => { const contentSchema = { type: 'object', properties: { 'my-input': { type: 'number' }, }, }; expectToThrowErrorMatchingTypeAndMessage( () => { jsonSchemaService.validateInput(contentSchema); }, SchemaValidationError, 'failed validation: The required property `required` is missing at `#`', { '#': [ { data: { key: 'required', pointer: '#' }, message: 'The required property `required` is missing at `#`' }, ], }, ); }); }); describe('SquizImage input', () => { it('should validate a squizImage input', () => { const schema = { type: 'object', properties: { image: { type: 'SquizImage', }, }, }; const value: SquizImageType['__shape__'] = { name: 'My Image', alt: 'My Image that did not load', caption: 'This above is a loaded image', imageVariations: { original: { url: 'https://picsum.photos/200/300', width: 100, height: 100, byteSize: 1000, mimeType: 'image/jpeg', aspectRatio: '1:1', sha1Hash: '1234567890abcdef1234567890abcdef12345678', }, }, }; expect(jsonSchemaService.validateInput(value, schema)).toEqual(true); }); it('should error if SquizImage type is missing required properties', async () => { const schema = { type: 'object', properties: { myProp: { type: 'SquizImage', }, }, }; const value: { myProp: SquizImageType['__shape__'] } = { // @ts-expect-error - missing required properties myProp: { alt: 'alt', caption: 'caption', name: 'name', }, }; expectToThrowErrorMatchingTypeAndMessage( () => { jsonSchemaService.validateInput(value, schema); }, SchemaValidationError, 'failed validation: Expected `Object {"alt":"alt","caption":"caption","name":"name"}` (object) in `#/myProp` to be of type `SquizImage`', { '#/myProp': [ { data: { expected: 'SquizImage', pointer: '#/myProp', received: 'object', value: { alt: 'alt', caption: 'caption', name: 'name' }, }, message: 'Expected `Object {"alt":"alt","caption":"caption","name":"name"}` (object) in `#/myProp` to be of type `SquizImage`', }, ], }, ); }); }); describe('FormattedText input', () => { it('should handle type as an array', () => { const functionInputSchema = { type: 'object', properties: { 'my-input': { type: ['number', 'string'] }, }, required: ['my-input'], }; expect( jsonSchemaService.validateInput( { 'my-input': 'formattedText', }, functionInputSchema, ), ).toEqual(true); expect( jsonSchemaService.validateInput( { 'my-input': 123, }, functionInputSchema, ), ).toEqual(true); }); it.failing('should handle type is an array of both string and FormattedText', () => { const formattedText: FormattedText = [ { tag: 'p', type: 'tag', children: [ { type: 'text', value: 'This is some ' }, { type: 'text', value: 'Link to asset 12345' }, { type: 'text', value: ' with an image ' }, { type: 'text', value: '.' }, ], }, ]; const functionInputSchema = { type: 'object', properties: { 'my-input': { type: ['FormattedText', 'string'] }, }, required: ['my-input'], }; expect( jsonSchemaService.validateInput( { 'my-input': formattedText, }, functionInputSchema, ), ).toEqual(true); expect( jsonSchemaService.validateInput( { 'my-input': 'hello', }, functionInputSchema, ), ).toEqual(true); }); it('should return true if input is formatted text', () => { const functionInputSchema = { type: 'object', properties: { 'my-input': { type: 'FormattedText' }, }, required: ['my-input'], }; const formattedText: FormattedText = [ { tag: 'p', type: 'tag', children: [ { type: 'text', value: 'This is some ' }, { type: 'text', value: 'Link to asset 12345' }, { type: 'text', value: ' with an image ' }, { type: 'text', value: '.' }, ], }, ]; const inputValue = { 'my-input': formattedText, }; expect(() => jsonSchemaService.validateInput(inputValue, functionInputSchema)).not.toThrow(); }); it('should throw an error if the FormattedText input is not formatted text', () => { const functionInputSchema = { type: 'object', properties: { 'my-input': { type: 'FormattedText' }, }, required: ['my-input'], }; const inputValue = { 'my-input': 123, }; expectToThrowErrorMatchingTypeAndMessage( () => { jsonSchemaService.validateInput(inputValue, functionInputSchema); }, SchemaValidationError, 'failed validation: Value `123` in `#/my-input` does not match any given oneof schema', { '#/my-input': [ { data: { errors: [ { code: 'type-error', data: { expected: 'array', pointer: '#/my-input', received: 'number', value: 123 }, message: 'Expected `123` (number) in `#/my-input` to be of type `array`', name: 'TypeError', type: 'error', }, ], oneOf: [{ $ref: 'FormattedText.json' }], pointer: '#/my-input', value: '123', }, message: 'Value `123` in `#/my-input` does not match any given oneof schema', }, ], }, ); }); it('should throw an error if the FormattedText input is invalid formatted text', () => { const functionInputSchema = { type: 'object', properties: { 'my-input': { type: 'FormattedText' }, }, required: ['my-input'], }; const inputValue = { 'my-input': { something: 'aa', }, }; expectToThrowErrorMatchingTypeAndMessage( () => { jsonSchemaService.validateInput(inputValue, functionInputSchema); }, SchemaValidationError, 'failed validation: Value `{"something":"aa"}` in `#/my-input` does not match any given oneof schema', { '#/my-input': [ { data: { errors: [ { code: 'type-error', data: { expected: 'array', pointer: '#/my-input', received: 'object', value: { something: 'aa' } }, message: 'Expected `Object {"something":"aa"}` (object) in `#/my-input` to be of type `array`', name: 'TypeError', type: 'error', }, ], oneOf: [{ $ref: 'FormattedText.json' }], pointer: '#/my-input', value: '{"something":"aa"}', }, message: 'Value `{"something":"aa"}` in `#/my-input` does not match any given oneof schema', }, ], }, ); }); it('should throw an error if the FormattedText input is invalid formatted text (deeply nested)', () => { const functionInputSchema = { type: 'object', properties: { 'my-input': { type: 'FormattedText' }, }, required: ['my-input'], }; const formattedText: FormattedText = [ { tag: 'p', type: 'tag', children: [ { type: 'text', value: 'This is some ' }, { type: 'text', value: 'Link to asset 12345' }, { type: 'text', value: ' with an image ' }, { type: 'text', value: 123 as any }, // see here { type: 'text', value: '.' }, ], }, ]; const inputValue = { 'my-input': formattedText, }; expectToThrowErrorMatchingTypeAndMessage( () => { jsonSchemaService.validateInput(inputValue, functionInputSchema); }, SchemaValidationError, 'failed validation: Value `[{"tag":"p","type":"tag","children":[{"type":"text","value":"This is some "},{"type":"text","value":"Link to asset 12345"},{"type":"text","value":" with an image "},{"type":"text","value":123},{"type":"text","value":"."}]}]` in `#/my-input` does not match any given oneof schema', { '#/my-input': [ { data: { errors: [ { code: 'any-of-error', data: { anyOf: [ { $ref: '#/definitions/HigherOrderFormattedNodes' }, { $ref: '#/definitions/BaseFormattedNodes' }, ], pointer: '#/my-input/0', value: { children: [ { type: 'text', value: 'This is some ' }, { type: 'text', value: 'Link to asset 12345' }, { type: 'text', value: ' with an image ' }, { type: 'text', value: 123 }, { type: 'text', value: '.' }, ], tag: 'p', type: 'tag', }, }, message: 'Object at `#/my-input/0` does not match any schema', name: 'AnyOfError', type: 'error', }, ], oneOf: [{ $ref: 'FormattedText.json' }], pointer: '#/my-input', value: '[{"tag":"p","type":"tag","children":[{"type":"text","value":"This is some "},{"type":"text","value":"Link to asset 12345"},{"type":"text","value":" with an image "},{"type":"text","value":123},{"type":"text","value":"."}]}]', }, message: 'Value `[{"tag":"p","type":"tag","children":[{"type":"text","value":"This is some "},{"type":"text","value":"Link to asset 12345"},{"type":"text","value":" with an image "},{"type":"text","value":123},{"type":"text","value":"."}]}]` in `#/my-input` does not match any given oneof schema', }, ], }, ); }); it('should validate an array of formatted texts', () => { const formattedText: FormattedText = [ { tag: 'p', type: 'tag', children: [{ type: 'text', value: 'hello' }], }, ]; const schema = { type: 'object', properties: { arr: { type: 'array', items: { type: 'FormattedText' }, }, }, }; const value = { arr: [formattedText], }; expect(jsonSchemaService.validateInput(value, schema)).toEqual(true); }); it('should throw an error if the FormattedText input is invalid formatted text (deeply, deeply nested)', () => { const formattedText: FormattedText = [ { tag: 'p', type: 'tag', children: [{ type: 'text', value: 'hello' }], }, ]; const inputValue: any = { 'my-input': formattedText, deep: { arr: [ { prop: formattedText, formattedTextArray: [formattedText, formattedText, { bad: 'data' }], }, ], }, }; const functionInputSchema = { type: 'object', properties: { 'my-input': { type: 'FormattedText' }, deep: { type: 'object', properties: { arr: { type: 'array', items: { type: 'object', required: ['prop', 'formattedTextArray'], properties: { prop: 'FormattedText', formattedTextArray: { type: 'array', items: { type: 'FormattedText', }, }, }, }, }, }, required: ['arr'], }, }, required: ['my-input', 'deep'], }; expectToThrowErrorMatchingTypeAndMessage( () => { jsonSchemaService.validateInput(inputValue, functionInputSchema); }, SchemaValidationError, 'failed validation: Value `{"bad":"data"}` in `#/deep/arr/0/formattedTextArray/2` does not match any given oneof schema', { '#/deep/arr/0/formattedTextArray/2': [ { data: { errors: [ { code: 'type-error', data: { expected: 'array', pointer: '#/deep/arr/0/formattedTextArray/2', received: 'object', value: { bad: 'data' }, }, message: 'Expected `Object {"bad":"data"}` (object) in `#/deep/arr/0/formattedTextArray/2` to be of type `array`', name: 'TypeError', type: 'error', }, ], oneOf: [{ $ref: 'FormattedText.json' }], pointer: '#/deep/arr/0/formattedTextArray/2', value: '{"bad":"data"}', }, message: 'Value `{"bad":"data"}` in `#/deep/arr/0/formattedTextArray/2` does not match any given oneof schema', }, ], }, ); }); it('should validate a FormattedText value when the schema is a $ref', async () => { const formattedText: FormattedText = [ { tag: 'p', type: 'tag', children: [{ type: 'text', value: 'hello' }], }, ]; const schema = { type: 'object', properties: { 'my-input': { $ref: '#/definitions/FormattedText' }, }, definitions: { FormattedText: { type: 'FormattedText' }, }, required: ['my-input'], }; const value = { 'my-input': formattedText, }; expect(jsonSchemaService.validateInput(value, schema)).toEqual(true); }); it('should error when a FormattedText value is invalid when the schema is a $ref', async () => { const schema = { type: 'object', properties: { 'my-input': { $ref: '#/definitions/FormattedText' }, }, definitions: { FormattedText: { type: 'FormattedText' }, }, required: ['my-input'], }; const value = { 'my-input': { bad: 'data' }, }; expectToThrowErrorMatchingTypeAndMessage( () => { jsonSchemaService.validateInput(value, schema); }, SchemaValidationError, 'failed validation: Value `{"bad":"data"}` in `#/my-input` does not match any given oneof schema', { '#/my-input': [ { data: { errors: [ { code: 'type-error', data: { expected: 'array', pointer: '#/my-input', received: 'object', value: { bad: 'data' } }, message: 'Expected `Object {"bad":"data"}` (object) in `#/my-input` to be of type `array`', name: 'TypeError', type: 'error', }, ], oneOf: [{ $ref: 'FormattedText.json' }], pointer: '#/my-input', value: '{"bad":"data"}', }, message: 'Value `{"bad":"data"}` in `#/my-input` does not match any given oneof schema', }, ], }, ); }); it('should validate a FormattedText value when the schema is a $ref to a $ref', async () => { const formattedText: FormattedText = [ { tag: 'p', type: 'tag', children: [{ type: 'text', value: 'hello' }], }, ]; const schema = { type: 'object', properties: { 'my-input': { $ref: '#/definitions/FormattedText' }, }, definitions: { FormattedText: { $ref: '#/definitions/FormattedText2' }, FormattedText2: { type: 'FormattedText' }, }, required: ['my-input'], }; const value = { 'my-input': formattedText, }; expect(jsonSchemaService.validateInput(value, schema)).toEqual(true); }); it('should validate a FormattedText value when the schema is in an if/then/else', async () => { const formattedText: FormattedText = [ { tag: 'p', type: 'tag', children: [{ type: 'text', value: 'hello' }], }, ]; const schema = { type: 'object', properties: { 'my-input': { if: { type: 'string' }, then: { type: 'string' }, else: { type: 'FormattedText' }, }, }, required: ['my-input'], }; const value = { 'my-input': formattedText, }; expect(jsonSchemaService.validateInput(value, schema)).toEqual(true); }); it('should allow an empty array', async () => { const schema = { type: 'object', properties: { 'my-input': { type: 'FormattedText' }, }, required: [], }; expect(jsonSchemaService.validateInput({ 'my-input': [] }, schema)).toEqual(true); }); it.each([ ['with attributes', { title: 'Link title' }], ['without attributes', undefined], ])('should validate link-to-matrix-asset - %s', (_: string, attributes?: Record) => { const formattedText: FormattedText = [ { type: 'link-to-matrix-asset', matrixIdentifier: 'matrix-identifier', matrixDomain: 'https://matrix-api-url', target: '_blank', matrixAssetId: '12345', children: [{ type: 'text', value: 'hello' }], attributes, }, ]; const schema = { type: 'object', properties: { myInput: { type: 'FormattedText', }, }, }; const value = { myInput: formattedText, }; expect(jsonSchemaService.validateInput(value, schema)).toBe(true); }); it('should throw if link-to-matrix-asset contains a reserved attribute', () => { const formattedText: FormattedText = [ { type: 'link-to-matrix-asset', matrixIdentifier: 'matrix-identifier', matrixDomain: 'https://matrix-api-url', target: '_blank', matrixAssetId: '12345', children: [{ type: 'text', value: 'hello' }], attributes: { // href is reserved (resolved from the selected Matrix asset) href: 'https://www.my-matrix.squiz.net', } as any, }, ]; const schema = { type: 'object', properties: { myInput: { type: 'FormattedText', }, }, }; const value = { myInput: formattedText, }; expect(() => jsonSchemaService.validateInput(value, schema)).toThrow(/does not match any given oneof schema/); }); it.each([ ['with attributes', { title: 'Link title' }], ['without attributes', undefined], ])('should validate link-to-dam-asset - %s', (_: string, attributes?: Record) => { const formattedText: FormattedText = [ { type: 'link-to-dam-asset', damSystemIdentifier: 'dam-identifier', damObjectId: '12345', damSystemType: 'bynder', target: '_blank', children: [{ type: 'text', value: 'hello' }], attributes, }, ]; const schema = { type: 'object', properties: { myInput: { type: 'FormattedText', }, }, }; const value = { myInput: formattedText, }; expect(jsonSchemaService.validateInput(value, schema)).toBe(true); }); it('should throw if link-to-dam-asset contains a reserved attribute', () => { const formattedText: FormattedText = [ { type: 'link-to-dam-asset', damSystemIdentifier: 'dam-identifier', damObjectId: '12345', damSystemType: 'bynder', target: '_blank', children: [{ type: 'text', value: 'hello' }], attributes: { // href is reserved (resolved from the selected Matrix asset) href: 'https://www.my-matrix.squiz.net', } as any, }, ]; const schema = { type: 'object', properties: { myInput: { type: 'FormattedText', }, }, }; const value = { myInput: formattedText, }; expect(() => jsonSchemaService.validateInput(value, schema)).toThrow(/does not match any given oneof schema/); }); it.each([ ['with attributes', { title: 'Link title' }], ['without attributes', undefined], ])('should validate matrix-image - %s', (_: string, attributes?: Record) => { const formattedText: FormattedText = [ { type: 'matrix-image', matrixIdentifier: 'matrix-identifier', matrixDomain: 'https://matrix-api-url', matrixAssetId: '12345', attributes, }, ]; const schema = { type: 'object', properties: { myInput: { type: 'FormattedText', }, }, }; const value = { myInput: formattedText, }; expect(jsonSchemaService.validateInput(value, schema)).toBe(true); }); it('should throw if matrix-image contains a reserved attribute', () => { const formattedText: FormattedText = [ { type: 'matrix-image', matrixIdentifier: 'matrix-identifier', matrixDomain: 'https://matrix-api-url', matrixAssetId: '12345', attributes: { // src is reserved (resolved from the selected Matrix asset) src: 'https://www.my-matrix.squiz.net/image.png', } as any, }, ]; const schema = { type: 'object', properties: { myInput: { type: 'FormattedText', }, }, }; const value = { myInput: formattedText, }; expect(() => jsonSchemaService.validateInput(value, schema)).toThrow(/does not match any given oneof schema/); }); it.each([ ['with attributes', { title: 'Link title' }], ['without attributes', undefined], ])('should validate dam-image - %s', (_: string, attributes?: Record) => { const formattedText: FormattedText = [ { type: 'dam-image', damSystemIdentifier: 'dam-identifier', damObjectId: '12345', damSystemType: 'bynder', attributes, }, ]; const schema = { type: 'object', properties: { myInput: { type: 'FormattedText', }, }, }; const value = { myInput: formattedText, }; expect(jsonSchemaService.validateInput(value, schema)).toBe(true); }); it('should throw if dam-image contains a reserved attribute', () => { const formattedText: FormattedText = [ { type: 'dam-image', damSystemIdentifier: 'dam-identifier', damObjectId: '12345', damSystemType: 'bynder', attributes: { // src is reserved (resolved from the selected DAM asset) src: 'https://www.my-matrix.squiz.net/image.png', } as any, }, ]; const schema = { type: 'object', properties: { myInput: { type: 'FormattedText', }, }, }; const value = { myInput: formattedText, }; expect(() => jsonSchemaService.validateInput(value, schema)).toThrow(/does not match any given oneof schema/); }); }); describe('standard inputs', () => { const functionInputSchema = { type: 'object', properties: { 'my-input': { type: 'number' }, }, required: ['my-input'], }; it('should return true for valid input values', () => { const inputValue = { 'my-input': 123, }; const result = jsonSchemaService.validateInput(inputValue, functionInputSchema); expect(result).toBe(true); }); it('should throw a SchemaValidationError for invalid input type', () => { const inputValue = { 'my-input': '123', }; expectToThrowErrorMatchingTypeAndMessage( () => { jsonSchemaService.validateInput(inputValue, functionInputSchema); }, SchemaValidationError, `failed validation: Expected \`123\` (string) in \`#/my-input\` to be of type \`number\``, { '#/my-input': [ { data: { expected: 'number', pointer: '#/my-input', received: 'string', value: '123' }, message: 'Expected `123` (string) in `#/my-input` to be of type `number`', }, ], }, ); }); it('should throw a SchemaValidationError for invalid input missing properties', () => { const inputValue = {}; expectToThrowErrorMatchingTypeAndMessage( () => { jsonSchemaService.validateInput(inputValue, functionInputSchema); }, SchemaValidationError, `failed validation: The required property \`my-input\` is missing at \`#\``, { '#': [ { data: { key: 'my-input', pointer: '#' }, message: 'The required property `my-input` is missing at `#`' }, ], }, ); }); it('should throw a SchemaValidationError with a truncated enum value list if there are more than 5 enum options', () => { expectToThrowErrorMatchingTypeAndMessage( () => { jsonSchemaService.validateInput( { enum: 'z' }, { type: 'object', properties: { enum: { type: 'string', enum: ['a', 'b', 'c', 'd', 'e', 'f', 'g'] } } }, ); }, SchemaValidationError, 'failed validation: Expected given value `z` in #/enum` to be one of `[a, b, c, d, e, ... 2 more]`', { '#/enum': [ { data: { pointer: '#/enum', value: 'z', values: ['a', 'b', 'c', 'd', 'e', 'f', 'g'] }, message: 'Expected given value `z` in #/enum` to be one of `[a, b, c, d, e, ... 2 more]`', }, ], }, ); }); // TODO DEVX-658 it.skip('should throw a SchemaValidationError for invalid input additional properties', () => { const inputValue = { 'my-input': 123, 'my-input-2': 123, }; expect(() => { jsonSchemaService.validateInput(inputValue, functionInputSchema); }).toThrowErrorMatchingInlineSnapshot(); }); }); describe('validateInput', () => { it.each([String('123'), Number(123), [123]])( 'should validate any primitive type with its resolvable type %s', (propertyValue) => { const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' }); const MyResolvableNumber = resolvableTypeFixture('MyResolvableNumber', { type: 'number' }); const MyResolvableArray = resolvableTypeFixture('MyResolvableArray', { type: 'array' }); const jsonSchemaService = new JSONSchemaService( new TypeResolver([primitiveSchema], [MyResolvableNumber, MyResolvableArray], { MyPrimitive: { // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableNumber: (value: number) => value.toString(), // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableArray: (value: any[]) => value.join(''), }, }), ComponentInputMetaSchema, ); expect( jsonSchemaService.validateInput( { myProperty: propertyValue }, { type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } }, ), ).toEqual(true); }, ); it('should error when a primitive type is provided a value that cannot be resolved by its resolvable types', () => { const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' }); const MyResolvableNumber = resolvableTypeFixture('MyResolvableNumber', { type: 'number' }); const MyResolvableArray = resolvableTypeFixture('MyResolvableArray', { type: 'array' }); const jsonSchemaService = new JSONSchemaService( new TypeResolver([primitiveSchema], [MyResolvableNumber, MyResolvableArray], { MyPrimitive: { // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableNumber: (value: number) => value.toString(), // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableArray: (value: any[]) => value.join(''), }, }), ComponentInputMetaSchema, ); expect(() => { jsonSchemaService.validateInput( { myProperty: true }, { type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } }, ); }).toThrowError(); }); it.each([String('123'), Number(123), [123]])( 'should validate a primitive type when defined as a ref with resolvable value %s', (propertyValue) => { const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' }); const MyResolvableNumber = resolvableTypeFixture('MyResolvableNumber', { type: 'number' }); const MyResolvableArray = resolvableTypeFixture('MyResolvableArray', { type: 'array' }); const jsonSchemaService = new JSONSchemaService( new TypeResolver([primitiveSchema], [MyResolvableNumber, MyResolvableArray], { MyPrimitive: { // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableNumber: (value: number) => value.toString(), // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableArray: (value: any[]) => value.join(''), }, }), ComponentInputMetaSchema, ); expect( jsonSchemaService.validateInput( { myProperty: propertyValue }, { type: 'object', properties: { myProperty: { $ref: '#/definitions/Ref' } }, definitions: { Ref: { type: 'MyPrimitive' } }, }, ), ).toEqual(true); }, ); it('should not validate on a primitive type against a resolvable type when a resolver is not defined', () => { const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' }); const MyResolvableNumber = resolvableTypeFixture('MyResolvableNumber', { type: 'number' }); const MyResolvableArray = resolvableTypeFixture('MyResolvableArray', { type: 'array' }); const jsonSchemaService = new JSONSchemaService( new TypeResolver([primitiveSchema], [MyResolvableNumber, MyResolvableArray], { MyPrimitive: { // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableNumber: (value: number) => value.toString(), }, }), ComponentInputMetaSchema, ); expect(() => { jsonSchemaService.validateInput( { myProperty: [123] }, { type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } }, ); }).toThrowError(); }); it('should validate a primitive type against similar but different resolvable types', () => { const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' }); const jsonSchemaService = new JSONSchemaService( new TypeResolver( [primitiveSchema], [ resolvableTypeFixture('MyResolvableSrcNumber', { type: 'object', properties: { src: { type: 'number' }, }, }), resolvableTypeFixture('MyResolvableSrcString', { type: 'object', properties: { src: { type: 'string' }, }, }), ], { MyPrimitive: { // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableSrcNumber: (value: { src: number }) => value.src.toString(), // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableSrcString: (value: { src: string }) => value.src, }, }, ), ComponentInputMetaSchema, ); expect( jsonSchemaService.validateInput( { myProperty: { src: 123, }, }, { type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } }, ), ).toEqual(true); expect( jsonSchemaService.validateInput( { myProperty: { src: '123', }, }, { type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } }, ), ).toEqual(true); }); }); describe('resolveInput', () => { it.each([String('123'), Number(123), [123]])( 'should resolve a primitive type from its resolvable type %s', async (resolvableValue) => { const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' }); const jsonSchemaService = new JSONSchemaService( new TypeResolver( [primitiveSchema], [ resolvableTypeFixture('MyResolvableNumber', { type: 'number' }), resolvableTypeFixture('MyResolvableArray', { type: 'array' }), ], { MyPrimitive: { // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableNumber: (value: number) => value.toString(), // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableArray: (value: any[]) => value.join(''), }, }, ), ComponentInputMetaSchema, ); await expect( jsonSchemaService.resolveInput( { myProperty: resolvableValue }, { type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } }, ), ).resolves.toEqual({ myProperty: '123' }); }, ); it.each([ [{ src: 'MyString' }, 'MyString'], [{ src: 1132 }, '1132'], ])('should resolve a resolvable type %s against the correct resolver to %s', async (resolvableValue, output) => { const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' }); const jsonSchemaService = new JSONSchemaService( new TypeResolver( [primitiveSchema], [ resolvableTypeFixture('MyResolvableSrcString', { type: 'object', properties: { src: { type: 'string' } }, }), resolvableTypeFixture('MyResolvableSrcNumber', { type: 'object', properties: { src: { type: 'number' } }, }), ], { MyPrimitive: { // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableSrcNumber: (value: { src: number }) => value.src.toString(), // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableSrcString: (value: { src: string }) => value.src, }, }, ), ComponentInputMetaSchema, ); await expect( jsonSchemaService.resolveInput( { myProperty: resolvableValue }, { type: 'object', properties: { myProperty: { type: 'MyPrimitive' } } }, ), ).resolves.toEqual({ myProperty: output }); }); it('should resolve a primitive type from its resolvable type %s', async () => { const primitiveSchema = primitiveTypeFixture('MyPrimitive', { type: 'string' }); const jsonSchemaService = new JSONSchemaService( new TypeResolver([primitiveSchema], [resolvableTypeFixture('MyResolvableWithError', { type: 'number' })], { MyPrimitive: { // @ts-expect-error - fixture is unknown but we know the actual shape MyResolvableWithError: (_value: number) => { throw new Error('Failed resolving!!'); }, }, }), ComponentInputMetaSchema, ); await expect( jsonSchemaService.resolveInput( { myProperty: 123, myProperty2: 234 }, { type: 'object', properties: { myProperty: { type: 'MyPrimitive' }, myProperty2: { type: 'MyPrimitive' } } }, ), ).rejects.toThrowErrorMatchingInlineSnapshot(` "Error(s) occurred when resolving JSON: Error: Error resolving JSON at #/myProperty: Failed resolving!! Error: Error resolving JSON at #/myProperty2: Failed resolving!!" `); }); it('should resolve a FormattedText empty list', async () => { const types = TypeResolverBuilder.new().addPrimitive(FormattedTextType).build(); const jsonSchemaService = new JSONSchemaService(types, ComponentInputMetaSchema); await expect( jsonSchemaService.resolveInput( { myProp: [] }, { type: 'object', properties: { myProp: { type: 'FormattedText' } }, required: [] }, ), ).resolves.toEqual({ myProp: [] }); }); }); }); describe('JSONSchemaService - validation', () => { const typeResolver = TypeResolverBuilder.new() .addPrimitive(SquizImageType) .addPrimitive(SquizLinkType) .addPrimitive(FormattedTextType) .build(); const jsonSchemaService = new JSONSchemaService(typeResolver, ComponentInputMetaSchema); it('should validate a schema with all the squiz primitive types', () => { const schema = { type: 'object', properties: { image: { type: 'SquizImage' }, link: { type: 'SquizLink' }, text: { type: 'FormattedText' }, }, required: ['image', 'link', 'text'], }; const formattedText: FormattedText = [ { tag: 'p', type: 'tag', children: [{ type: 'text', value: 'hello' }], }, ]; const input: { image: SquizImageType['__shape__']; link: SquizLinkType['__shape__']; text: FormattedText; } = { image: { name: 'test-image.jpeg', imageVariations: { original: { aspectRatio: '1:1', height: 100, width: 100, url: 'https://www.squiz.net', byteSize: 100, mimeType: 'image/jpeg', sha1Hash: '123', }, }, }, link: { text: 'test-link', url: 'https://www.squiz.net', target: '_blank', }, text: formattedText, }; const result = jsonSchemaService.validateInput(input, schema); expect(result).toEqual(true); }); it('should validate a schema with all the squiz primitive types and matrix-asset-uri format', () => { const schema = { type: 'object', properties: { image: { type: 'SquizImage' }, link: { type: 'SquizLink' }, text: { type: 'FormattedText' }, asset: { type: 'string', format: 'matrix-asset-uri', }, }, required: ['image', 'link', 'text', 'asset'], }; const formattedText: FormattedText = [ { tag: 'p', type: 'tag', children: [{ type: 'text', value: 'hello' }], }, ]; const input: { image: SquizImageType['__shape__']; link: SquizLinkType['__shape__']; text: FormattedText; asset: MatrixAssetUri; } = { image: { name: 'test-image.jpeg', imageVariations: { original: { aspectRatio: '1:1', height: 100, width: 100, url: 'https://www.squiz.net', byteSize: 100, mimeType: 'image/jpeg', sha1Hash: '123', }, }, }, link: { text: 'test-link', url: 'https://www.squiz.net', target: '_blank', }, text: formattedText, asset: 'matrix-asset://identifier/123', }; const result = jsonSchemaService.validateInput(input, schema); expect(result).toEqual(true); }); it('should catch validation errors when there is a schema with all the squiz primitive types', () => { const schema = { type: 'object', properties: { image: { type: 'SquizImage' }, link: { type: 'SquizLink' }, text: { type: 'FormattedText' }, }, required: ['image', 'link', 'text'], }; const formattedText: FormattedText = [ //@ts-expect-error - wrong type { children: [{ type: 'text', value: 'hello' }], }, ]; const input: { image: SquizImageType['__shape__']; link: SquizLinkType['__shape__']; text: FormattedText; } = { image: { name: 'test-image.jpeg', imageVariations: { //@ts-expect-error - wrong type original: { width: 100, url: 'https://www.squiz.net', byteSize: 100, mimeType: 'image/jpeg', sha1Hash: '123', }, }, }, //@ts-expect-error - wrong type link: { text: 'test-link', target: '_blank', }, text: formattedText, }; expectToThrowErrorMatchingTypeAndMessage( () => jsonSchemaService.validateInput(input, schema), SchemaValidationError, 'failed validation: Value `{"name":"test-image.jpeg","imageVariations":{"original":{"width":100,"url":"https://www.squiz.net","byteSize":100,"mimeType":"image/jpeg","sha1Hash":"123"}}}` in `#/image` does not match any given oneof schema,\nValue `{"text":"test-link","target":"_blank"}` in `#/link` does not match any given oneof schema,\nValue `[{"children":[{"type":"text","value":"hello"}]}]` in `#/text` does not match any given oneof schema', { '#/image': [ { data: { errors: [ { code: 'required-property-error', data: { key: 'height', pointer: '#/image/imageVariations/original' }, message: 'The required property `height` is missing at `#/image/imageVariations/original`', name: 'RequiredPropertyError', type: 'error', }, { code: 'required-property-error', data: { key: 'aspectRatio', pointer: '#/image/imageVariations/original' }, message: 'The required property `aspectRatio` is missing at `#/image/imageVariations/original`', name: 'RequiredPropertyError', type: 'error', }, ], oneOf: [{ $ref: 'SquizImage.json' }], pointer: '#/image', value: '{"name":"test-image.jpeg","imageVariations":{"original":{"width":100,"url":"https://www.squiz.net","byteSize":100,"mimeType":"image/jpeg","sha1Hash":"123"}}}', }, message: 'Value `{"name":"test-image.jpeg","imageVariations":{"original":{"width":100,"url":"https://www.squiz.net","byteSize":100,"mimeType":"image/jpeg","sha1Hash":"123"}}}` in `#/image` does not match any given oneof schema', }, ], '#/link': [ { data: { errors: [ { code: 'required-property-error', data: { key: 'url', pointer: '#/link' }, message: 'The required property `url` is missing at `#/link`', name: 'RequiredPropertyError', type: 'error', }, ], oneOf: [{ $ref: 'SquizLink.json' }], pointer: '#/link', value: '{"text":"test-link","target":"_blank"}', }, message: 'Value `{"text":"test-link","target":"_blank"}` in `#/link` does not match any given oneof schema', }, ], '#/text': [ { data: { errors: [ { code: 'any-of-error', data: { anyOf: [ { $ref: '#/definitions/HigherOrderFormattedNodes' }, { $ref: '#/definitions/BaseFormattedNodes' }, ], pointer: '#/text/0', value: { children: [{ type: 'text', value: 'hello' }] }, }, message: 'Object at `#/text/0` does not match any schema', name: 'AnyOfError', type: 'error', }, ], oneOf: [{ $ref: 'FormattedText.json' }], pointer: '#/text', value: '[{"children":[{"type":"text","value":"hello"}]}]', }, message: 'Value `[{"children":[{"type":"text","value":"hello"}]}]` in `#/text` does not match any given oneof schema', }, ], }, ); }); it('should catch validation errors when invalid matrix-asset-uri is provided with invalid other squiz primitive types ', () => { const schema = { type: 'object', properties: { image: { type: 'SquizImage' }, link: { type: 'SquizLink' }, text: { type: 'FormattedText' }, asset: { type: 'string', format: 'matrix-asset-uri', }, }, required: ['image', 'link', 'text', 'asset'], }; const formattedText: FormattedText = [ //@ts-expect-error - wrong type { children: [{ type: 'text', value: 'hello' }], }, ]; const input: { image: SquizImageType['__shape__']; link: SquizLinkType['__shape__']; text: FormattedText; asset: MatrixAssetUri; } = { image: { name: 'test-image.jpeg', imageVariations: { //@ts-expect-error - wrong type original: { width: 100, url: 'https://www.squiz.net', byteSize: 100, mimeType: 'image/jpeg', sha1Hash: '123', }, }, }, //@ts-expect-error - wrong type link: { text: 'test-link', target: '_blank', }, text: formattedText, // @ts-expect-error - wrong type asset: 'matrix://123', }; expectToThrowErrorMatchingTypeAndMessage( () => jsonSchemaService.validateInput(input, schema), SchemaValidationError, 'failed validation: Value `{"name":"test-image.jpeg","imageVariations":{"original":{"width":100,"url":"https://www.squiz.net","byteSize":100,"mimeType":"image/jpeg","sha1Hash":"123"}}}` in `#/image` does not match any given oneof schema,\nValue `{"text":"test-link","target":"_blank"}` in `#/link` does not match any given oneof schema,\nValue `[{"children":[{"type":"text","value":"hello"}]}]` in `#/text` does not match any given oneof schema,\nValue matrix-asset-uri (matrix://123) in `#/asset` is not a valid matrix asset uri', { '#/asset': [ { data: { errors: { assetId: { data: { expected: /^\d+(?::.+)?$/, received: '' }, message: 'Matrix Asset Id has invalid format, must match /^d+(?::.+)?$/', }, scheme: { data: { expected: 'matrix-asset', received: 'matrix' }, message: 'Uri scheme is invalid, must match "matrix-asset"', }, }, pointer: '#/asset', value: 'matrix://123', }, message: 'Value matrix-asset-uri (matrix://123) in `#/asset` is not a valid matrix asset uri', }, ], '#/image': [ { data: { errors: [ { code: 'required-property-error', data: { key: 'height', pointer: '#/image/imageVariations/original' }, message: 'The required property `height` is missing at `#/image/imageVariations/original`', name: 'RequiredPropertyError', type: 'error', }, { code: 'required-property-error', data: { key: 'aspectRatio', pointer: '#/image/imageVariations/original' }, message: 'The required property `aspectRatio` is missing at `#/image/imageVariations/original`', name: 'RequiredPropertyError', type: 'error', }, ], oneOf: [{ $ref: 'SquizImage.json' }], pointer: '#/image', value: '{"name":"test-image.jpeg","imageVariations":{"original":{"width":100,"url":"https://www.squiz.net","byteSize":100,"mimeType":"image/jpeg","sha1Hash":"123"}}}', }, message: 'Value `{"name":"test-image.jpeg","imageVariations":{"original":{"width":100,"url":"https://www.squiz.net","byteSize":100,"mimeType":"image/jpeg","sha1Hash":"123"}}}` in `#/image` does not match any given oneof schema', }, ], '#/link': [ { data: { errors: [ { code: 'required-property-error', data: { key: 'url', pointer: '#/link' }, message: 'The required property `url` is missing at `#/link`', name: 'RequiredPropertyError', type: 'error', }, ], oneOf: [{ $ref: 'SquizLink.json' }], pointer: '#/link', value: '{"text":"test-link","target":"_blank"}', }, message: 'Value `{"text":"test-link","target":"_blank"}` in `#/link` does not match any given oneof schema', }, ], '#/text': [ { data: { errors: [ { code: 'any-of-error', data: { anyOf: [ { $ref: '#/definitions/HigherOrderFormattedNodes' }, { $ref: '#/definitions/BaseFormattedNodes' }, ], pointer: '#/text/0', value: { children: [{ type: 'text', value: 'hello' }] }, }, message: 'Object at `#/text/0` does not match any schema', name: 'AnyOfError', type: 'error', }, ], oneOf: [{ $ref: 'FormattedText.json' }], pointer: '#/text', value: '[{"children":[{"type":"text","value":"hello"}]}]', }, message: 'Value `[{"children":[{"type":"text","value":"hello"}]}]` in `#/text` does not match any given oneof schema', }, ], }, ); }); it('should validate when MatrixAssetType is being used for a squiz primitive type with resolver', () => { const typeResolver = TypeResolverBuilder.new() .addPrimitive(SquizImageType) .addPrimitive(SquizLinkType) .addPrimitive(FormattedTextType) .addResolver(SquizImageType, MatrixAssetType, (): SquizImageType['__shape__'] => { return { name: '', imageVariations: { original: { width: 0, height: 0, url: '', mimeType: '', byteSize: 0, sha1Hash: '', aspectRatio: '', }, }, }; }) .build(); const jsonSchemaService = new JSONSchemaService(typeResolver, ComponentInputMetaSchema); const schema = { type: 'object', properties: { image: { type: 'SquizImage' }, link: { type: 'SquizLink' }, text: { type: 'FormattedText' }, asset: { type: 'string', format: 'matrix-asset-uri', }, }, required: ['image', 'link', 'text', 'asset'], }; const formattedText: FormattedText = [ { tag: 'p', type: 'tag', children: [{ type: 'text', value: 'hello' }], }, ]; const input: { image: MatrixAssetType['__shape__']; link: SquizLinkType['__shape__']; text: FormattedText; asset: MatrixAssetUri; } = { image: { matrixAssetId: '123', matrixIdentifier: 'identifier', matrixDomain: 'domain', }, link: { text: 'test-link', url: 'https://www.squiz.net', target: '_blank', }, text: formattedText, asset: 'matrix-asset://identifier/123', }; expect(jsonSchemaService.validateInput(input, schema)).toEqual(true); }); it('it should catch MatrixAssetType validation errors when being use for squiz primitive type with resolver', () => { const typeResolver = TypeResolverBuilder.new() .addPrimitive(SquizImageType) .addPrimitive(SquizLinkType) .addPrimitive(FormattedTextType) .addResolver(SquizImageType, MatrixAssetType, (): SquizImageType['__shape__'] => { return { name: '', imageVariations: { original: { width: 0, height: 0, url: '', mimeType: '', byteSize: 0, sha1Hash: '', aspectRatio: '', }, }, }; }) .build(); const jsonSchemaService = new JSONSchemaService(typeResolver, ComponentInputMetaSchema); const schema = { type: 'object', properties: { image: { type: 'SquizImage' }, }, required: ['image'], }; const input: { image: MatrixAssetType['__shape__']; } = { //@ts-expect-error - intentionally invalid input image: { matrixIdentifier: 'identifier', }, }; expectToThrowErrorMatchingTypeAndMessage( () => jsonSchemaService.validateInput(input, schema), SchemaValidationError, 'failed validation: Value `{"matrixIdentifier":"identifier"}` in `#/image` does not match any given oneof schema', { '#/image': [ { data: { errors: [ { code: 'required-property-error', data: { key: 'name', pointer: '#/image' }, message: 'The required property `name` is missing at `#/image`', name: 'RequiredPropertyError', type: 'error', }, { code: 'required-property-error', data: { key: 'imageVariations', pointer: '#/image' }, message: 'The required property `imageVariations` is missing at `#/image`', name: 'RequiredPropertyError', type: 'error', }, { code: 'required-property-error', data: { key: 'matrixAssetId', pointer: '#/image' }, message: 'The required property `matrixAssetId` is missing at `#/image`', name: 'RequiredPropertyError', type: 'error', }, ], oneOf: [{ $ref: 'SquizImage.json' }, { $ref: 'MatrixAsset.json' }], pointer: '#/image', value: '{"matrixIdentifier":"identifier"}', }, message: 'Value `{"matrixIdentifier":"identifier"}` in `#/image` does not match any given oneof schema', }, ], }, ); }); it('should only resolve an array of items containing resolvable types, other arrays are unaffected', async () => { const typeResolver = TypeResolverBuilder.new() .addPrimitive(SquizImageType) .addPrimitive(SquizLinkType) .addPrimitive(FormattedTextType) .addResolver(SquizImageType, MatrixAssetType, (): SquizImageType['__shape__'] => { return { name: '', imageVariations: { original: { width: 0, height: 0, url: '', mimeType: '', byteSize: 0, sha1Hash: '', aspectRatio: '', }, }, }; }) .addResolver(SquizLinkType, MatrixAssetLinkType, (): SquizLinkType['__shape__'] => { return { text: 'link text', url: 'www.test.com', target: '_self', }; }) .build(); const jsonSchemaService = new JSONSchemaService(typeResolver, ComponentInputMetaSchema); const schema = { type: 'object', properties: { images: { description: 'Gallery images', type: 'array', items: { type: 'SquizImage', }, }, squizLink: { title: 'Squiz link', type: 'array', items: { type: 'SquizLink', }, }, //custom format multiLine: { title: 'Multi-line (textarea)', type: 'array', items: { type: 'string', format: 'multi-line', }, }, //string array listOfStrings: { type: 'array', title: 'A list of strings', items: { type: 'string', default: 'Holy smokes', }, }, }, required: ['images', 'links'], }; const input: { images: Array; squizLink: Array; multiline: Array; listOfStrings: Array; } = { images: [ { matrixDomain: 'https://feaas-page-render-us.dev.matrix.squiz.cloud/59', matrixAssetId: '160', matrixIdentifier: 'feaas-matrix-us', }, ], squizLink: [ { matrixDomain: 'https://feaas-page-render-us.dev.matrix.squiz.cloud/59', matrixAssetId: '160', matrixIdentifier: 'feaas-matrix-us', target: '_self', }, ], multiline: ['wow', 'much', 'multiline'], listOfStrings: ['very', 'string'], }; const result = await jsonSchemaService.resolveInput(input, schema); expect(result).toEqual({ images: [ { imageVariations: { original: { aspectRatio: '', byteSize: 0, height: 0, mimeType: '', sha1Hash: '', url: '', width: 0 }, }, name: '', }, ], squizLink: [ { text: 'link text', url: 'www.test.com', target: '_self', }, ], multiline: ['wow', 'much', 'multiline'], listOfStrings: ['very', 'string'], }); }); it('should resolve multiple primitive type array items in multi-level nested array structures', async () => { const typeResolver = TypeResolverBuilder.new() .addPrimitive(SquizLinkType) .addResolver( SquizLinkType, MatrixAssetLinkType, (input: MatrixAssetLinkType['__shape__']): SquizLinkType['__shape__'] => { if ('matrixIdentifier' in input) { return { text: 'link text', url: `www.test.com/asset/${input.matrixAssetId}`, target: input.target, }; } return input; }, ) .build(); const jsonSchemaService = new JSONSchemaService(typeResolver, ComponentInputMetaSchema); const schema = { type: 'object', properties: { object1: { type: 'object', required: [], properties: { array1: { type: 'array', items: { type: 'object', properties: { linksArray: { type: 'array', items: { type: 'SquizLink', }, }, }, }, }, }, }, }, }; const input: { object1: { array1: Array<{ linksArray: Array; }>; }; } = { object1: { array1: [ { linksArray: [ { url: '#LineOne', text: 'Link One', target: '_blank', }, { url: '#LineTwo', text: 'Link Two', target: '_blank', }, { matrixAssetId: '100', matrixDomain: 'my-matrix.squiz.net', matrixIdentifier: 'my-matrix-identifier', target: '_blank', }, ], }, ], }, }; const result = await jsonSchemaService.resolveInput(input, schema); expect(result).toEqual({ object1: { array1: [ { linksArray: [ { url: '#LineOne', text: 'Link One', target: '_blank', }, { url: '#LineTwo', text: 'Link Two', target: '_blank', }, { url: 'www.test.com/asset/100', text: 'link text', target: '_blank', }, ], }, ], }, }); }); it('should not use the resolver for primitive items in a resolvable array', async () => { const mockMatrixAssetResolver = jest.fn((input: MatrixAssetType['__shape__']) => { return { name: input.matrixAssetId, imageVariations: { original: { width: 0, height: 0, url: '', mimeType: '', byteSize: 0, sha1Hash: '', aspectRatio: '', }, }, }; }); const mockMatrixLinkResolver = jest.fn((input: MatrixAssetLinkType['__shape__']): SquizLinkType['__shape__'] => { return { text: input.matrixAssetId, url: 'www.test.com', target: '_self', }; }); const typeResolver = TypeResolverBuilder.new() .addPrimitive(SquizImageType) .addPrimitive(SquizLinkType) .addPrimitive(FormattedTextType) .addResolver(SquizImageType, MatrixAssetType, mockMatrixAssetResolver) .addResolver(SquizLinkType, MatrixAssetLinkType, mockMatrixLinkResolver) .build(); const jsonSchemaService = new JSONSchemaService(typeResolver, ComponentInputMetaSchema); const schema = { type: 'object', properties: { images: { description: 'Gallery images', type: 'array', items: { type: 'SquizImage', }, }, squizLink: { title: 'Squiz link', type: 'array', items: { type: 'SquizLink', }, }, //custom format multiLine: { title: 'Multi-line (textarea)', type: 'array', items: { type: 'string', format: 'multi-line', }, }, //string array listOfStrings: { type: 'array', title: 'A list of strings', items: { type: 'string', default: 'Holy smokes', }, }, }, required: ['images', 'links'], }; const input: { images: Array; squizLink: Array; multiline: Array; listOfStrings: Array; } = { images: [ { matrixDomain: 'https://feaas-page-render-us.dev.matrix.squiz.cloud/59', matrixAssetId: '160', matrixIdentifier: 'feaas-matrix-us', }, { imageVariations: { original: { aspectRatio: '', byteSize: 0, height: 0, mimeType: '', sha1Hash: '', url: '', width: 0 }, }, name: '', }, ], squizLink: [ { matrixDomain: 'https://feaas-page-render-us.dev.matrix.squiz.cloud/59', matrixAssetId: '160', matrixIdentifier: 'feaas-matrix-us', target: '_self', }, { text: 'link text', url: 'www.test.com', target: '_self', }, ], multiline: ['wow', 'much', 'multiline'], listOfStrings: ['very', 'string'], }; const result = await jsonSchemaService.resolveInput(input, schema); expect(result).toEqual({ images: [ { imageVariations: { original: { aspectRatio: '', byteSize: 0, height: 0, mimeType: '', sha1Hash: '', url: '', width: 0 }, }, name: (input.images[0] as MatrixAssetType['__shape__']).matrixAssetId, }, { imageVariations: { original: { aspectRatio: '', byteSize: 0, height: 0, mimeType: '', sha1Hash: '', url: '', width: 0 }, }, name: '', }, ], squizLink: [ { text: (input.squizLink[0] as MatrixAssetLinkType['__shape__']).matrixAssetId, url: 'www.test.com', target: '_self', }, { text: 'link text', url: 'www.test.com', target: '_self', }, ], multiline: ['wow', 'much', 'multiline'], listOfStrings: ['very', 'string'], }); expect(mockMatrixAssetResolver).toHaveBeenCalledTimes(1); expect(mockMatrixLinkResolver).toHaveBeenCalledTimes(1); }); });