Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(input_schema): Resource properties #484

Merged
merged 5 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/input_schema/src/input_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Ajv, { ErrorObject, Schema } from 'ajv';
import { m } from './intl';
import schema from './schema.json';
import {
CommonResourceFieldDefinition,
FieldDefinition,
InputSchema,
InputSchemaBaseChecked,
Expand Down Expand Up @@ -124,14 +125,21 @@ function validateField(validator: Ajv, fieldSchema: Record<string, unknown>, 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();
if (!definition) throw new Error('Input schema validation failed to find "enum property" definition');
validateAgainstSchemaOrThrow(validator, fieldSchema, definition, `schema.properties.${fieldKey}.enum`);
return;
}
// If the definition contains "resourceType" property then it's resource type.
if ((fieldSchema as CommonResourceFieldDefinition<unknown>).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}.resourceType`);
return;
}
// Otherwise we use the other definition.
const definition = matchingDefinitions.filter((item) => !item.properties.enum).pop();
if (!definition) throw new Error('Input schema validation failed to find other than "enum property" definition');
Expand Down
80 changes: 69 additions & 11 deletions packages/input_schema/src/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,80 @@
"type": "object",
"patternProperties": {
"^": {
"oneOf": [
{ "$ref": "#/definitions/stringProperty" },
{ "$ref": "#/definitions/stringEnumProperty" },
{ "$ref": "#/definitions/arrayProperty" },
{ "$ref": "#/definitions/objectProperty" },
{ "$ref": "#/definitions/integerProperty" },
{ "$ref": "#/definitions/booleanProperty" },
{ "$ref": "#/definitions/anyProperty" }
]
"if": {
"properties": {
"resourceType": {
"not": {
MFori marked this conversation as resolved.
Show resolved Hide resolved
"type": "string"
}
}
}
},
"then": {
"oneOf": [
{ "$ref": "#/definitions/stringProperty" },
{ "$ref": "#/definitions/stringEnumProperty" },
{ "$ref": "#/definitions/arrayProperty" },
{ "$ref": "#/definitions/objectProperty" },
{ "$ref": "#/definitions/integerProperty" },
{ "$ref": "#/definitions/booleanProperty" },
{ "$ref": "#/definitions/anyProperty" }
]
},
"else": {
"oneOf": [
{ "$ref": "#/definitions/resourceProperty" },
{ "$ref": "#/definitions/resourceArrayProperty" }
]
}
}
}
}
},
"additionalProperties": false,
"required": ["title", "type", "properties", "schemaVersion"],
"definitions": {
"resourceProperty": {
"title": "Resource property",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: It looks the title, description, nullable, sectionCaption, sectionDescription appear in all definition. Could we move these to special definitions?

{
...
    "definitions": {
        "baseProperty": {
            "type": "object",
            "properties": {
                "title": {"type": "string"},
                "description": {
                    "type": "string"
                },
                "nullable": { "type": "boolean"}
            },
            "required": ["title", "description"]
        },
        "resourceProperty": {
            "allOf": [
                {"$ref": "#/definitions/baseProperty"},
                {
                    "properties": {
                        ...the rest of props on top of baseProperty
                    }
                }
            ],
            "required": ["type", "title", "description", "resourceType"]
        },
        ...
    }
...
}

"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"]
},
"stringEnumProperty": {
"title": "Enum property",
"type": "object",
Expand Down Expand Up @@ -86,7 +144,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"],
Expand Down Expand Up @@ -147,7 +205,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" }
Expand Down
17 changes: 16 additions & 1 deletion packages/input_schema/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type CommonFieldDefinition<T> = {

export type StringFieldDefinition = CommonFieldDefinition<string> & {
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;
Expand Down Expand Up @@ -61,6 +61,19 @@ export type ArrayFieldDefinition = CommonFieldDefinition<Array<unknown>> & {
items?: unknown;
}

export type CommonResourceFieldDefinition<T> = CommonFieldDefinition<T> & {
editor?: 'resourcePicker' | 'hidden';
resourceType: 'dataset' | 'keyValueStore' | 'requestQueue'
}

export type ResourceFieldDefinition = CommonResourceFieldDefinition<string> & {
type: 'string'
}

export type ResourceArrayFieldDefinition = CommonResourceFieldDefinition<Array<string>> & {
MFori marked this conversation as resolved.
Show resolved Hide resolved
type: 'array'
}

type AllTypes = StringFieldDefinition['type']
| BooleanFieldDefinition['type']
| IntegerFieldDefinition['type']
Expand All @@ -78,6 +91,8 @@ export type FieldDefinition = StringFieldDefinition
| ObjectFieldDefinition
| ArrayFieldDefinition
| MixedFieldDefinition
| ResourceFieldDefinition
| ResourceArrayFieldDefinition

/**
* Type with checked base, but not properties
Expand Down
2 changes: 1 addition & 1 deletion test/input_schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")',
);
});

Expand Down
91 changes: 91 additions & 0 deletions test/input_schema_definition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,5 +451,96 @@ 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);
});

it('should not accept invalid resourceType', () => {
expect(ajv.validate(inputSchema, {
title: 'Test input schema',
type: 'object',
schemaVersion: 1,
properties: {
myField: {
title: 'Field title',
description: 'My test field',
type: 'string',
resourceType: 'xxx',
},
},
})).toBe(false);
expect(ajv.errorsText()).toContain('data/properties/myField/resourceType must be equal to one of the allowed values');
expect(parseAjvError(ajv.errors![0], 'schema.properties.myField')?.message)
.toEqual('Field schema.properties.myField.resourceType must be equal to one of the allowed values: '
+ '"dataset", "keyValueStore", "requestQueue"');
});

it('should not accept invalid 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: 'keyValueStore',
editor: 'textfield',
},
},
})).toBe(false);
expect(ajv.errorsText()).toContain('data/properties/myField/editor must be equal to one of the allowed values');
expect(parseAjvError(ajv.errors![0], 'schema.properties.myField')?.message)
.toEqual('Field schema.properties.myField.editor must be equal to one of the allowed values: "resourcePicker", "hidden"');
});
});
});
});
68 changes: 68 additions & 0 deletions test/utilities.client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down