diff --git a/packages/input_schema/src/input_schema.ts b/packages/input_schema/src/input_schema.ts index 99439cf4..07f42647 100644 --- a/packages/input_schema/src/input_schema.ts +++ b/packages/input_schema/src/input_schema.ts @@ -3,6 +3,7 @@ import Ajv, { ErrorObject, Schema } from 'ajv'; import { m } from './intl'; import schema from './schema.json'; import { + CommonResourceFieldDefinition, FieldDefinition, InputSchema, InputSchemaBaseChecked, @@ -124,7 +125,7 @@ function validateField(validator: Ajv, fieldSchema: Record, fie return; } - // If there are more matching definitions then the type is string, and we need to get the right one. + // If there are more matching definitions then we need to get the right one. // If the definition contains "enum" property then it's enum type. if ((fieldSchema as StringFieldDefinition).enum) { const definition = matchingDefinitions.filter((item) => !!item.properties.enum).pop(); @@ -132,8 +133,15 @@ function validateField(validator: Ajv, fieldSchema: Record, fie validateAgainstSchemaOrThrow(validator, fieldSchema, definition, `schema.properties.${fieldKey}.enum`); return; } + // If the definition contains "resourceType" property then it's resource type. + if ((fieldSchema as CommonResourceFieldDefinition).resourceType) { + const definition = matchingDefinitions.filter((item) => !!item.properties.resourceType).pop(); + if (!definition) throw new Error('Input schema validation failed to find "resource property" definition'); + validateAgainstSchemaOrThrow(validator, fieldSchema, definition, `schema.properties.${fieldKey}`); + return; + } // Otherwise we use the other definition. - const definition = matchingDefinitions.filter((item) => !item.properties.enum).pop(); + const definition = matchingDefinitions.filter((item) => !item.properties.enum && !item.properties.resourceType).pop(); if (!definition) throw new Error('Input schema validation failed to find other than "enum property" definition'); validateAgainstSchemaOrThrow(validator, fieldSchema, definition, `schema.properties.${fieldKey}`); diff --git a/packages/input_schema/src/schema.json b/packages/input_schema/src/schema.json index c9f3e802..5dd6741e 100644 --- a/packages/input_schema/src/schema.json +++ b/packages/input_schema/src/schema.json @@ -39,6 +39,8 @@ { "$ref": "#/definitions/objectProperty" }, { "$ref": "#/definitions/integerProperty" }, { "$ref": "#/definitions/booleanProperty" }, + { "$ref": "#/definitions/resourceProperty" }, + { "$ref": "#/definitions/resourceArrayProperty" }, { "$ref": "#/definitions/anyProperty" } ] } @@ -86,7 +88,7 @@ "title": { "type": "string" }, "description": { "type": "string" }, "nullable": { "type": "boolean" }, - "editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue"] }, + "editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden"] }, "isSecret": { "type": "boolean" } }, "required": ["type", "title", "description", "editor"], @@ -147,7 +149,7 @@ "nullable": { "type": "boolean" }, "minLength": { "type": "integer" }, "maxLength": { "type": "integer" }, - "editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue"] }, + "editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden"] }, "isSecret": { "type": "boolean" }, "sectionCaption": { "type": "string" }, "sectionDescription": { "type": "string" } @@ -306,6 +308,47 @@ }, "required": ["type", "title", "description"] }, + "resourceProperty": { + "title": "Resource property", + "type": "object", + "additionalProperties": false, + "properties": { + "type": { "enum": ["string"] }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "editor": { "enum": ["resourcePicker", "hidden"] }, + "resourceType": { "enum": ["dataset", "keyValueStore", "requestQueue"] }, + "default": { "type": "string" }, + "prefill": { "type": "string" }, + "example": { "type": "string" }, + "nullable": { "type": "boolean" }, + "sectionCaption": { "type": "string" }, + "sectionDescription": { "type": "string" } + }, + "required": ["type", "title", "description", "resourceType"] + }, + "resourceArrayProperty": { + "title": "Resource array property", + "type": "object", + "additionalProperties": false, + "properties": { + "type": { "enum": ["array"] }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "editor": { "enum": ["resourcePicker", "hidden"] }, + "default": { "type": "array" }, + "prefill": { "type": "array" }, + "example": { "type": "array" }, + "nullable": { "type": "boolean" }, + "minItems": { "type": "integer" }, + "maxItems": { "type": "integer" }, + "uniqueItems": { "type": "boolean" }, + "resourceType": { "enum": ["dataset", "keyValueStore", "requestQueue"] }, + "sectionCaption": { "type": "string" }, + "sectionDescription": { "type": "string" } + }, + "required": ["type", "title", "description", "resourceType"] + }, "anyProperty": { "title": "Any property", "type": "object", diff --git a/packages/input_schema/src/types.ts b/packages/input_schema/src/types.ts index 1b573cc4..515246a0 100644 --- a/packages/input_schema/src/types.ts +++ b/packages/input_schema/src/types.ts @@ -11,7 +11,7 @@ type CommonFieldDefinition = { export type StringFieldDefinition = CommonFieldDefinition & { type: 'string' - editor: 'textfield' | 'textarea' | 'javascript' | 'python' | 'select' | 'datepicker' | 'hidden' | 'json' | 'dataset' | 'keyValueStore' | 'requestQueue'; + editor: 'textfield' | 'textarea' | 'javascript' | 'python' | 'select' | 'datepicker' | 'hidden' | 'json'; pattern?: string; minLength?: number; maxLength?: number; @@ -61,6 +61,19 @@ export type ArrayFieldDefinition = CommonFieldDefinition> & { items?: unknown; } +export type CommonResourceFieldDefinition = CommonFieldDefinition & { + editor?: 'resourcePicker' | 'hidden'; + resourceType: 'dataset' | 'keyValueStore' | 'requestQueue' +} + +export type ResourceFieldDefinition = CommonResourceFieldDefinition & { + type: 'string' +} + +export type ResourceArrayFieldDefinition = CommonResourceFieldDefinition & { + type: 'array' +} + type AllTypes = StringFieldDefinition['type'] | BooleanFieldDefinition['type'] | IntegerFieldDefinition['type'] @@ -78,6 +91,8 @@ export type FieldDefinition = StringFieldDefinition | ObjectFieldDefinition | ArrayFieldDefinition | MixedFieldDefinition + | ResourceFieldDefinition + | ResourceArrayFieldDefinition /** * Type with checked base, but not properties diff --git a/test/input_schema.test.ts b/test/input_schema.test.ts index 321768fc..50e4290d 100644 --- a/test/input_schema.test.ts +++ b/test/input_schema.test.ts @@ -156,7 +156,7 @@ describe('input_schema.json', () => { expect(() => validateInputSchema(validator, schema)).toThrow( 'Input schema is not valid (Field schema.properties.myField.editor must be equal to one of the allowed values: ' - + '"javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue")', + + '"javascript", "python", "textfield", "textarea", "datepicker", "hidden")', ); }); @@ -278,5 +278,47 @@ describe('input_schema.json', () => { 'Field schema.properties.something does not exist, but it is specified in schema.required. Either define the field or remove it from schema.required.', ); }); + + describe('special cases for resourceProperty', () => { + it('should not accept invalid resourceType', () => { + const schema = { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + resourceType: 'xxx', + }, + }, + }; + expect(() => validateInputSchema(validator, schema)).toThrow( + 'Input schema is not valid (Field schema.properties.myField.resourceType must be equal to one of the allowed values: ' + + '"dataset", "keyValueStore", "requestQueue")', + ); + }); + + it('should not accept invalid editor', () => { + const schema = { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + resourceType: 'keyValueStore', + editor: 'textfield', + }, + }, + }; + expect(() => validateInputSchema(validator, schema)).toThrow( + 'Input schema is not valid (Field schema.properties.myField.editor must be equal to one of the allowed values: "resourcePicker", "hidden")', + ); + }); + }); }); }); diff --git a/test/input_schema_definition.test.ts b/test/input_schema_definition.test.ts index c021d7d2..d23cc309 100644 --- a/test/input_schema_definition.test.ts +++ b/test/input_schema_definition.test.ts @@ -465,5 +465,56 @@ describe('input_schema.json', () => { + 'Set "allowAbsolute", "allowRelative" or both properties.'); }); }); + + describe('special cases for resourceProperty', () => { + it('should accept resourceType with editor', () => { + expect(ajv.validate(inputSchema, { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + resourceType: 'dataset', + editor: 'resourcePicker', + }, + }, + })).toBe(true); + }); + + it('should accept resourceType without editor', () => { + expect(ajv.validate(inputSchema, { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + resourceType: 'dataset', + }, + }, + })).toBe(true); + }); + + it('should accept array resourceType', () => { + expect(ajv.validate(inputSchema, { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'array', + resourceType: 'dataset', + }, + }, + })).toBe(true); + }); + }); }); }); diff --git a/test/utilities.client.test.ts b/test/utilities.client.test.ts index 4083567e..8511f04e 100644 --- a/test/utilities.client.test.ts +++ b/test/utilities.client.test.ts @@ -1019,6 +1019,74 @@ describe('utilities.client', () => { }); }); + describe('special cases for resource property', () => { + it('should allow string value for single resource', () => { + const { inputSchema, validator } = buildInputSchema({ + field: { + title: 'Field title', + description: 'My test field', + type: 'string', + resourceType: 'dataset', + nullable: true, + }, + }); + const inputs = [ + // 2 invalid inputs + { field: [] }, + { field: {} }, + // Valid + { field: 'DATASET_ID' }, + { field: null }, + ]; + + const results = inputs + .map((input) => validateInputUsingValidator(validator, inputSchema, input)) + .filter((errors) => errors.length > 0); + + // There should be 2 invalid inputs + expect(results.length).toEqual(2); + results.forEach((result) => { + // Only one error should be thrown + expect(result.length).toEqual(1); + expect(result[0].fieldKey).toEqual('field'); + }); + }); + + it('should allow array value for multiple resource', () => { + const { inputSchema, validator } = buildInputSchema({ + field: { + title: 'Field title', + description: 'My test field', + type: 'array', + resourceType: 'dataset', + nullable: true, + }, + }); + const inputs = [ + // 2 invalid inputs + { field: 'DATASET_ID' }, + { field: {} }, + // Valid + { field: [] }, + { field: ['DATASET_ID'] }, + { field: ['DATASET_ID_1', 'DATASET_ID_2', 'DATASET_ID_3'] }, + { field: null }, + ]; + + const results = inputs + .map((input) => validateInputUsingValidator(validator, inputSchema, input)) + .filter((errors) => errors.length > 0); + + // There should be 2 invalid inputs + expect(results.length).toEqual(2); + results.forEach((result) => { + // Only one error should be thrown + expect(result.length).toEqual(1); + expect(result[0].fieldKey).toEqual('field'); + }); + }); + }); + /* TODO - enable this tests when the datepicker validation is back on describe('special cases for datepicker string type', () => { it('should allow absolute dates when allowAbsolute is omitted', () => {