Skip to content

Commit ccfca82

Browse files
authored
feat(input_schema): Resource properties (#484)
Update of input schema based on apify/apify-docs#1261: New properties in schema: `resourceProperty` and `resourceArrayProperty` To distinguish between resource properties with `resourceType` and string/array properties custom validation is done in `validateField` (because of clash on same `type`) Input ui to support this schmena is ready here: apify/apify-core#18093
1 parent aa8d542 commit ccfca82

File tree

6 files changed

+233
-6
lines changed

6 files changed

+233
-6
lines changed

packages/input_schema/src/input_schema.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Ajv, { ErrorObject, Schema } from 'ajv';
33
import { m } from './intl';
44
import schema from './schema.json';
55
import {
6+
CommonResourceFieldDefinition,
67
FieldDefinition,
78
InputSchema,
89
InputSchemaBaseChecked,
@@ -124,16 +125,23 @@ function validateField(validator: Ajv, fieldSchema: Record<string, unknown>, fie
124125
return;
125126
}
126127

127-
// If there are more matching definitions then the type is string, and we need to get the right one.
128+
// If there are more matching definitions then we need to get the right one.
128129
// If the definition contains "enum" property then it's enum type.
129130
if ((fieldSchema as StringFieldDefinition).enum) {
130131
const definition = matchingDefinitions.filter((item) => !!item.properties.enum).pop();
131132
if (!definition) throw new Error('Input schema validation failed to find "enum property" definition');
132133
validateAgainstSchemaOrThrow(validator, fieldSchema, definition, `schema.properties.${fieldKey}.enum`);
133134
return;
134135
}
136+
// If the definition contains "resourceType" property then it's resource type.
137+
if ((fieldSchema as CommonResourceFieldDefinition<unknown>).resourceType) {
138+
const definition = matchingDefinitions.filter((item) => !!item.properties.resourceType).pop();
139+
if (!definition) throw new Error('Input schema validation failed to find "resource property" definition');
140+
validateAgainstSchemaOrThrow(validator, fieldSchema, definition, `schema.properties.${fieldKey}`);
141+
return;
142+
}
135143
// Otherwise we use the other definition.
136-
const definition = matchingDefinitions.filter((item) => !item.properties.enum).pop();
144+
const definition = matchingDefinitions.filter((item) => !item.properties.enum && !item.properties.resourceType).pop();
137145
if (!definition) throw new Error('Input schema validation failed to find other than "enum property" definition');
138146

139147
validateAgainstSchemaOrThrow(validator, fieldSchema, definition, `schema.properties.${fieldKey}`);

packages/input_schema/src/schema.json

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
{ "$ref": "#/definitions/objectProperty" },
4040
{ "$ref": "#/definitions/integerProperty" },
4141
{ "$ref": "#/definitions/booleanProperty" },
42+
{ "$ref": "#/definitions/resourceProperty" },
43+
{ "$ref": "#/definitions/resourceArrayProperty" },
4244
{ "$ref": "#/definitions/anyProperty" }
4345
]
4446
}
@@ -86,7 +88,7 @@
8688
"title": { "type": "string" },
8789
"description": { "type": "string" },
8890
"nullable": { "type": "boolean" },
89-
"editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue"] },
91+
"editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden"] },
9092
"isSecret": { "type": "boolean" }
9193
},
9294
"required": ["type", "title", "description", "editor"],
@@ -147,7 +149,7 @@
147149
"nullable": { "type": "boolean" },
148150
"minLength": { "type": "integer" },
149151
"maxLength": { "type": "integer" },
150-
"editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue"] },
152+
"editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden"] },
151153
"isSecret": { "type": "boolean" },
152154
"sectionCaption": { "type": "string" },
153155
"sectionDescription": { "type": "string" }
@@ -306,6 +308,47 @@
306308
},
307309
"required": ["type", "title", "description"]
308310
},
311+
"resourceProperty": {
312+
"title": "Resource property",
313+
"type": "object",
314+
"additionalProperties": false,
315+
"properties": {
316+
"type": { "enum": ["string"] },
317+
"title": { "type": "string" },
318+
"description": { "type": "string" },
319+
"editor": { "enum": ["resourcePicker", "hidden"] },
320+
"resourceType": { "enum": ["dataset", "keyValueStore", "requestQueue"] },
321+
"default": { "type": "string" },
322+
"prefill": { "type": "string" },
323+
"example": { "type": "string" },
324+
"nullable": { "type": "boolean" },
325+
"sectionCaption": { "type": "string" },
326+
"sectionDescription": { "type": "string" }
327+
},
328+
"required": ["type", "title", "description", "resourceType"]
329+
},
330+
"resourceArrayProperty": {
331+
"title": "Resource array property",
332+
"type": "object",
333+
"additionalProperties": false,
334+
"properties": {
335+
"type": { "enum": ["array"] },
336+
"title": { "type": "string" },
337+
"description": { "type": "string" },
338+
"editor": { "enum": ["resourcePicker", "hidden"] },
339+
"default": { "type": "array" },
340+
"prefill": { "type": "array" },
341+
"example": { "type": "array" },
342+
"nullable": { "type": "boolean" },
343+
"minItems": { "type": "integer" },
344+
"maxItems": { "type": "integer" },
345+
"uniqueItems": { "type": "boolean" },
346+
"resourceType": { "enum": ["dataset", "keyValueStore", "requestQueue"] },
347+
"sectionCaption": { "type": "string" },
348+
"sectionDescription": { "type": "string" }
349+
},
350+
"required": ["type", "title", "description", "resourceType"]
351+
},
309352
"anyProperty": {
310353
"title": "Any property",
311354
"type": "object",

packages/input_schema/src/types.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type CommonFieldDefinition<T> = {
1111

1212
export type StringFieldDefinition = CommonFieldDefinition<string> & {
1313
type: 'string'
14-
editor: 'textfield' | 'textarea' | 'javascript' | 'python' | 'select' | 'datepicker' | 'hidden' | 'json' | 'dataset' | 'keyValueStore' | 'requestQueue';
14+
editor: 'textfield' | 'textarea' | 'javascript' | 'python' | 'select' | 'datepicker' | 'hidden' | 'json';
1515
pattern?: string;
1616
minLength?: number;
1717
maxLength?: number;
@@ -61,6 +61,19 @@ export type ArrayFieldDefinition = CommonFieldDefinition<Array<unknown>> & {
6161
items?: unknown;
6262
}
6363

64+
export type CommonResourceFieldDefinition<T> = CommonFieldDefinition<T> & {
65+
editor?: 'resourcePicker' | 'hidden';
66+
resourceType: 'dataset' | 'keyValueStore' | 'requestQueue'
67+
}
68+
69+
export type ResourceFieldDefinition = CommonResourceFieldDefinition<string> & {
70+
type: 'string'
71+
}
72+
73+
export type ResourceArrayFieldDefinition = CommonResourceFieldDefinition<string[]> & {
74+
type: 'array'
75+
}
76+
6477
type AllTypes = StringFieldDefinition['type']
6578
| BooleanFieldDefinition['type']
6679
| IntegerFieldDefinition['type']
@@ -78,6 +91,8 @@ export type FieldDefinition = StringFieldDefinition
7891
| ObjectFieldDefinition
7992
| ArrayFieldDefinition
8093
| MixedFieldDefinition
94+
| ResourceFieldDefinition
95+
| ResourceArrayFieldDefinition
8196

8297
/**
8398
* Type with checked base, but not properties

test/input_schema.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ describe('input_schema.json', () => {
156156

157157
expect(() => validateInputSchema(validator, schema)).toThrow(
158158
'Input schema is not valid (Field schema.properties.myField.editor must be equal to one of the allowed values: '
159-
+ '"javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue")',
159+
+ '"javascript", "python", "textfield", "textarea", "datepicker", "hidden")',
160160
);
161161
});
162162

@@ -278,5 +278,47 @@ describe('input_schema.json', () => {
278278
'Field schema.properties.something does not exist, but it is specified in schema.required. Either define the field or remove it from schema.required.',
279279
);
280280
});
281+
282+
describe('special cases for resourceProperty', () => {
283+
it('should not accept invalid resourceType', () => {
284+
const schema = {
285+
title: 'Test input schema',
286+
type: 'object',
287+
schemaVersion: 1,
288+
properties: {
289+
myField: {
290+
title: 'Field title',
291+
description: 'My test field',
292+
type: 'string',
293+
resourceType: 'xxx',
294+
},
295+
},
296+
};
297+
expect(() => validateInputSchema(validator, schema)).toThrow(
298+
'Input schema is not valid (Field schema.properties.myField.resourceType must be equal to one of the allowed values: '
299+
+ '"dataset", "keyValueStore", "requestQueue")',
300+
);
301+
});
302+
303+
it('should not accept invalid editor', () => {
304+
const schema = {
305+
title: 'Test input schema',
306+
type: 'object',
307+
schemaVersion: 1,
308+
properties: {
309+
myField: {
310+
title: 'Field title',
311+
description: 'My test field',
312+
type: 'string',
313+
resourceType: 'keyValueStore',
314+
editor: 'textfield',
315+
},
316+
},
317+
};
318+
expect(() => validateInputSchema(validator, schema)).toThrow(
319+
'Input schema is not valid (Field schema.properties.myField.editor must be equal to one of the allowed values: "resourcePicker", "hidden")',
320+
);
321+
});
322+
});
281323
});
282324
});

test/input_schema_definition.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,5 +465,56 @@ describe('input_schema.json', () => {
465465
+ 'Set "allowAbsolute", "allowRelative" or both properties.');
466466
});
467467
});
468+
469+
describe('special cases for resourceProperty', () => {
470+
it('should accept resourceType with editor', () => {
471+
expect(ajv.validate(inputSchema, {
472+
title: 'Test input schema',
473+
type: 'object',
474+
schemaVersion: 1,
475+
properties: {
476+
myField: {
477+
title: 'Field title',
478+
description: 'My test field',
479+
type: 'string',
480+
resourceType: 'dataset',
481+
editor: 'resourcePicker',
482+
},
483+
},
484+
})).toBe(true);
485+
});
486+
487+
it('should accept resourceType without editor', () => {
488+
expect(ajv.validate(inputSchema, {
489+
title: 'Test input schema',
490+
type: 'object',
491+
schemaVersion: 1,
492+
properties: {
493+
myField: {
494+
title: 'Field title',
495+
description: 'My test field',
496+
type: 'string',
497+
resourceType: 'dataset',
498+
},
499+
},
500+
})).toBe(true);
501+
});
502+
503+
it('should accept array resourceType', () => {
504+
expect(ajv.validate(inputSchema, {
505+
title: 'Test input schema',
506+
type: 'object',
507+
schemaVersion: 1,
508+
properties: {
509+
myField: {
510+
title: 'Field title',
511+
description: 'My test field',
512+
type: 'array',
513+
resourceType: 'dataset',
514+
},
515+
},
516+
})).toBe(true);
517+
});
518+
});
468519
});
469520
});

test/utilities.client.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,74 @@ describe('utilities.client', () => {
10191019
});
10201020
});
10211021

1022+
describe('special cases for resource property', () => {
1023+
it('should allow string value for single resource', () => {
1024+
const { inputSchema, validator } = buildInputSchema({
1025+
field: {
1026+
title: 'Field title',
1027+
description: 'My test field',
1028+
type: 'string',
1029+
resourceType: 'dataset',
1030+
nullable: true,
1031+
},
1032+
});
1033+
const inputs = [
1034+
// 2 invalid inputs
1035+
{ field: [] },
1036+
{ field: {} },
1037+
// Valid
1038+
{ field: 'DATASET_ID' },
1039+
{ field: null },
1040+
];
1041+
1042+
const results = inputs
1043+
.map((input) => validateInputUsingValidator(validator, inputSchema, input))
1044+
.filter((errors) => errors.length > 0);
1045+
1046+
// There should be 2 invalid inputs
1047+
expect(results.length).toEqual(2);
1048+
results.forEach((result) => {
1049+
// Only one error should be thrown
1050+
expect(result.length).toEqual(1);
1051+
expect(result[0].fieldKey).toEqual('field');
1052+
});
1053+
});
1054+
1055+
it('should allow array value for multiple resource', () => {
1056+
const { inputSchema, validator } = buildInputSchema({
1057+
field: {
1058+
title: 'Field title',
1059+
description: 'My test field',
1060+
type: 'array',
1061+
resourceType: 'dataset',
1062+
nullable: true,
1063+
},
1064+
});
1065+
const inputs = [
1066+
// 2 invalid inputs
1067+
{ field: 'DATASET_ID' },
1068+
{ field: {} },
1069+
// Valid
1070+
{ field: [] },
1071+
{ field: ['DATASET_ID'] },
1072+
{ field: ['DATASET_ID_1', 'DATASET_ID_2', 'DATASET_ID_3'] },
1073+
{ field: null },
1074+
];
1075+
1076+
const results = inputs
1077+
.map((input) => validateInputUsingValidator(validator, inputSchema, input))
1078+
.filter((errors) => errors.length > 0);
1079+
1080+
// There should be 2 invalid inputs
1081+
expect(results.length).toEqual(2);
1082+
results.forEach((result) => {
1083+
// Only one error should be thrown
1084+
expect(result.length).toEqual(1);
1085+
expect(result[0].fieldKey).toEqual('field');
1086+
});
1087+
});
1088+
});
1089+
10221090
/* TODO - enable this tests when the datepicker validation is back on
10231091
describe('special cases for datepicker string type', () => {
10241092
it('should allow absolute dates when allowAbsolute is omitted', () => {

0 commit comments

Comments
 (0)