Skip to content

Commit 843edf7

Browse files
mforiB4nan
andauthored
feat(input_schema): datepicker allowAbsolute/allowRelative properties and validation (#477)
This PR introduce new input_schema properties `allowAbsolute` and `allowRelative` used when the `editor` property is set to `datepicker`: - the `schema.json` is updated in the way, that properties `allowAbsolute` and `allowRelative` are valid only when `editor` is `datepicker`, `allowAbsolute` is considered as default and any of `allowAbsolute` or `allowRelative` is true - value format validation - unit tests for schema validation and value validation - New datepicker with support for absolute and relative dates PR: apify/apify-core#17465 - Docs PR: apify/apify-docs#1227 --------- Co-authored-by: Martin Adámek <[email protected]>
1 parent f89c65e commit 843edf7

File tree

7 files changed

+509
-23
lines changed

7 files changed

+509
-23
lines changed

packages/input_schema/src/input_schema.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const { definitions } = schema;
2525
export function parseAjvError(
2626
error: ErrorObject,
2727
rootName: string,
28-
properties: Record<string, { nullable?: boolean }> = {},
28+
properties: Record<string, { nullable?: boolean, editor?: string }> = {},
2929
input: Record<string, unknown> = {},
3030
): { fieldKey: string; message: string } | null {
3131
// There are 3 possible errors comming from validation:
@@ -47,14 +47,27 @@ export function parseAjvError(
4747
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message });
4848
} else if (error.keyword === 'required') {
4949
fieldKey = error.params.missingProperty;
50-
message = m('inputSchema.validation.required', { rootName, fieldKey });
50+
if (fieldKey === 'allowRelative') {
51+
// this is the case, when allowAbsolute is set to false, but allowRelative is not set
52+
message = m('inputSchema.validation.datepickerNoType', { rootName });
53+
} else {
54+
message = m('inputSchema.validation.required', { rootName, fieldKey });
55+
}
5156
} else if (error.keyword === 'additionalProperties') {
5257
fieldKey = error.params.additionalProperty;
5358
message = m('inputSchema.validation.additionalProperty', { rootName, fieldKey });
5459
} else if (error.keyword === 'enum') {
5560
fieldKey = error.instancePath.split('/').pop()!;
5661
const errorMessage = `${error.message}: "${error.params.allowedValues.join('", "')}"`;
5762
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: errorMessage });
63+
} else if (error.keyword === 'const') {
64+
fieldKey = error.instancePath.split('/').pop()!;
65+
// This is a special case for datepicker fields, where both allowAbsolute and allowRelative properties are set to false
66+
if (fieldKey === 'allowRelative' || fieldKey === 'allowAbsolute') {
67+
message = m('inputSchema.validation.datepickerNoType', { rootName });
68+
} else {
69+
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message });
70+
}
5871
} else {
5972
fieldKey = error.instancePath.split('/').pop()!;
6073
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message });

packages/input_schema/src/intl.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ const intlStrings = {
3333
'Field schema.properties.{fieldKey} does not exist, but it is specified in schema.required. Either define the field or remove it from schema.required.',
3434
'inputSchema.validation.proxyGroupMustBeArrayOfStrings':
3535
'Field {rootName}.{fieldKey}.apifyProxyGroups must be an array of strings.',
36+
'inputSchema.validation.datepickerInvalidFormatAbsolute':
37+
'Field {rootName}.{fieldKey} must be a string in format "YYYY-MM-DD".',
38+
'inputSchema.validation.datepickerInvalidFormatRelative':
39+
'Field {rootName}.{fieldKey} must be a string in format "+/- number unit". Supported units are "day", "week", "month" and "year".',
40+
'inputSchema.validation.datepickerInvalidFormatBoth':
41+
'Field {rootName}.{fieldKey} must be a string in format "YYYY-MM-DD" or "+/- number unit". Supported units are "day", "week", "month" and "year".',
42+
'inputSchema.validation.datepickerInvalidDate':
43+
'Field {rootName}.{fieldKey} must be a valid date.',
44+
'inputSchema.validation.datepickerNoType':
45+
'Field {rootName} must accept absolute, relative or both dates. Set "allowAbsolute", "allowRelative" or both properties.',
3646
};
3747

3848
/**

packages/input_schema/src/schema.json

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,22 +100,58 @@
100100
}
101101
},
102102
"then": {
103-
"additionalProperties": false,
104-
"properties": {
105-
"type": { "enum": ["string"] },
106-
"title": { "type": "string" },
107-
"description": { "type": "string" },
108-
"default": { "type": "string" },
109-
"prefill": { "type": "string" },
110-
"example": { "type": "string" },
111-
"pattern": { "type": "string" },
112-
"nullable": { "type": "boolean" },
113-
"minLength": { "type": "integer" },
114-
"maxLength": { "type": "integer" },
115-
"editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue"] },
116-
"isSecret": { "type": "boolean" },
117-
"sectionCaption": { "type": "string" },
118-
"sectionDescription": { "type": "string" }
103+
"if": {
104+
"properties": {
105+
"editor": { "const": "datepicker" }
106+
}
107+
},
108+
"then": {
109+
"additionalProperties": false,
110+
"properties": {
111+
"type": { "enum": ["string"] },
112+
"title": { "type": "string" },
113+
"description": { "type": "string" },
114+
"default": { "type": "string" },
115+
"prefill": { "type": "string" },
116+
"example": { "type": "string" },
117+
"pattern": { "type": "string" },
118+
"nullable": { "type": "boolean" },
119+
"minLength": { "type": "integer" },
120+
"maxLength": { "type": "integer" },
121+
"editor": { "enum": ["datepicker"] },
122+
"sectionCaption": { "type": "string" },
123+
"sectionDescription": { "type": "string" },
124+
"allowAbsolute": { "type": "boolean" },
125+
"allowRelative": { "type": "boolean" }
126+
},
127+
"if": {
128+
"required": ["allowAbsolute"],
129+
"properties": { "allowAbsolute": { "const": false } }
130+
},
131+
"then": { "required": ["type", "title", "description", "editor", "allowRelative"] },
132+
"anyOf": [
133+
{ "properties": { "allowAbsolute": { "const": true } } },
134+
{ "properties": { "allowRelative": { "const": true } } }
135+
]
136+
},
137+
"else": {
138+
"additionalProperties": false,
139+
"properties": {
140+
"type": { "enum": ["string"] },
141+
"title": { "type": "string" },
142+
"description": { "type": "string" },
143+
"default": { "type": "string" },
144+
"prefill": { "type": "string" },
145+
"example": { "type": "string" },
146+
"pattern": { "type": "string" },
147+
"nullable": { "type": "boolean" },
148+
"minLength": { "type": "integer" },
149+
"maxLength": { "type": "integer" },
150+
"editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue"] },
151+
"isSecret": { "type": "boolean" },
152+
"sectionCaption": { "type": "string" },
153+
"sectionDescription": { "type": "string" }
154+
}
119155
}
120156
},
121157
"else": {

packages/input_schema/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export type StringFieldDefinition = CommonFieldDefinition<string> & {
1818
enum?: readonly string[]; // required if editor is 'select'
1919
enumTitles?: readonly string[]
2020
isSecret?: boolean;
21+
// Used for 'datepicker' editor, allowAbsolute is considered with default value true
22+
// If only relative time is wanted, allowAbsolute must be explicitly set to false
23+
allowAbsolute?: boolean;
24+
allowRelative?: boolean;
2125
}
2226

2327
export type BooleanFieldDefinition = CommonFieldDefinition<boolean> & {

packages/input_schema/src/utilities.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,12 @@ export function validateInputUsingValidator(
137137
}
138138

139139
Object.keys(properties).forEach((property) => {
140-
const value = input[property] as Record<string, any>;
141-
const { type, editor, patternKey, patternValue } = properties[property];
140+
const value = input[property];
141+
const { type, editor, patternKey, patternValue, allowAbsolute, allowRelative } = properties[property];
142142
const fieldErrors = [];
143143
// Check that proxy is required, if yes, valides that it's correctly setup
144144
if (type === 'object' && editor === 'proxy') {
145-
const proxyValidationErrors = validateProxyField(property as any, value, required.includes(property), options.proxy);
145+
const proxyValidationErrors = validateProxyField(property as any, value as Record<string, any>, required.includes(property), options.proxy);
146146
proxyValidationErrors.forEach((error) => {
147147
fieldErrors.push(error);
148148
});
@@ -234,7 +234,7 @@ export function validateInputUsingValidator(
234234
const check = new RegExp(patternValue);
235235
const invalidKeys: any[] = [];
236236
Object.keys(value).forEach((key) => {
237-
const propertyValue = value[key];
237+
const propertyValue = (value as Record<string, any>)[key];
238238
if (typeof propertyValue !== 'string' || !check.test(propertyValue)) invalidKeys.push(key);
239239
});
240240
if (invalidKeys.length) {
@@ -247,6 +247,50 @@ export function validateInputUsingValidator(
247247
}
248248
}
249249
}
250+
251+
// Check datepicker editor format
252+
if (type === 'string' && editor === 'datepicker' && value && typeof value === 'string') {
253+
const acceptAbsolute = allowAbsolute !== false;
254+
const acceptRelative = allowRelative === true;
255+
const isValidAbsolute = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(value);
256+
const isValidRelative = /^[+-] [0-9]+ (day|week|month|year)s?$/.test(value);
257+
let isValidDate: boolean | undefined;
258+
259+
if (isValidAbsolute) {
260+
const [year, month, day] = value.split('-').map(Number);
261+
const date = new Date(`${year}-${month}-${day}`);
262+
263+
// Check if the date object is valid and matches the input string
264+
isValidDate = date.getFullYear() === year
265+
&& date.getMonth() + 1 === month
266+
&& date.getDate() === day;
267+
}
268+
269+
if (acceptAbsolute && !acceptRelative && !isValidAbsolute) {
270+
fieldErrors.push(m('inputSchema.validation.datepickerInvalidFormatAbsolute', {
271+
rootName: 'input',
272+
fieldKey: property,
273+
}));
274+
} else if (acceptRelative && !acceptAbsolute && !isValidRelative) {
275+
fieldErrors.push(m('inputSchema.validation.datepickerInvalidFormatRelative', {
276+
rootName: 'input',
277+
fieldKey: property,
278+
}));
279+
} else if ((acceptAbsolute && !acceptRelative && !isValidAbsolute)
280+
|| (acceptRelative && !acceptAbsolute && !isValidRelative)
281+
|| (acceptRelative && acceptAbsolute && !isValidAbsolute && !isValidRelative)) {
282+
fieldErrors.push(m('inputSchema.validation.datepickerInvalidFormatBoth', {
283+
rootName: 'input',
284+
fieldKey: property,
285+
}));
286+
} else if (isValidDate === false && acceptAbsolute) {
287+
fieldErrors.push(m('inputSchema.validation.datepickerInvalidDate', {
288+
rootName: 'input',
289+
fieldKey: property,
290+
}));
291+
}
292+
}
293+
250294
if (fieldErrors.length > 0) {
251295
const message = fieldErrors.join(', ');
252296
errors.push({ fieldKey: property, message });

test/input_schema_definition.test.ts

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { inputSchema } from '@apify/input_schema';
1+
import { inputSchema, parseAjvError } from '@apify/input_schema';
22
import Ajv from 'ajv';
33

44
/**
@@ -285,5 +285,171 @@ describe('input_schema.json', () => {
285285
})).toBe(false);
286286
});
287287
});
288+
289+
describe('special cases for datepicker editor type', () => {
290+
it('should accept allowAbsolute and allowRelative fields omitted', () => {
291+
expect(ajv.validate(inputSchema, {
292+
title: 'Test input schema',
293+
type: 'object',
294+
schemaVersion: 1,
295+
properties: {
296+
myField: {
297+
title: 'Field title',
298+
description: 'My test field',
299+
type: 'string',
300+
editor: 'datepicker',
301+
},
302+
},
303+
})).toBe(true);
304+
});
305+
306+
it('should accept allowAbsolute and allowRelative both set to true', () => {
307+
expect(ajv.validate(inputSchema, {
308+
title: 'Test input schema',
309+
type: 'object',
310+
schemaVersion: 1,
311+
properties: {
312+
myField: {
313+
title: 'Field title',
314+
description: 'My test field',
315+
type: 'string',
316+
editor: 'datepicker',
317+
allowAbsolute: true,
318+
allowRelative: true,
319+
},
320+
},
321+
})).toBe(true);
322+
});
323+
324+
it('should accept allowAbsolute=true and allowRelative=false', () => {
325+
expect(ajv.validate(inputSchema, {
326+
title: 'Test input schema',
327+
type: 'object',
328+
schemaVersion: 1,
329+
properties: {
330+
myField: {
331+
title: 'Field title',
332+
description: 'My test field',
333+
type: 'string',
334+
editor: 'datepicker',
335+
allowAbsolute: true,
336+
allowRelative: false,
337+
},
338+
},
339+
})).toBe(true);
340+
});
341+
342+
it('should accept allowAbsolute=false and allowRelative=true', () => {
343+
expect(ajv.validate(inputSchema, {
344+
title: 'Test input schema',
345+
type: 'object',
346+
schemaVersion: 1,
347+
properties: {
348+
myField: {
349+
title: 'Field title',
350+
description: 'My test field',
351+
type: 'string',
352+
editor: 'datepicker',
353+
allowAbsolute: false,
354+
allowRelative: true,
355+
},
356+
},
357+
})).toBe(true);
358+
});
359+
360+
it('should accept allowAbsolute=true', () => {
361+
expect(ajv.validate(inputSchema, {
362+
title: 'Test input schema',
363+
type: 'object',
364+
schemaVersion: 1,
365+
properties: {
366+
myField: {
367+
title: 'Field title',
368+
description: 'My test field',
369+
type: 'string',
370+
editor: 'datepicker',
371+
allowAbsolute: true,
372+
},
373+
},
374+
})).toBe(true);
375+
});
376+
377+
it('should accept allowRelative=true', () => {
378+
expect(ajv.validate(inputSchema, {
379+
title: 'Test input schema',
380+
type: 'object',
381+
schemaVersion: 1,
382+
properties: {
383+
myField: {
384+
title: 'Field title',
385+
description: 'My test field',
386+
type: 'string',
387+
editor: 'datepicker',
388+
allowRelative: true,
389+
},
390+
},
391+
})).toBe(true);
392+
});
393+
394+
it('should accept allowRelative=false', () => {
395+
expect(ajv.validate(inputSchema, {
396+
title: 'Test input schema',
397+
type: 'object',
398+
schemaVersion: 1,
399+
properties: {
400+
myField: {
401+
title: 'Field title',
402+
description: 'My test field',
403+
type: 'string',
404+
editor: 'datepicker',
405+
allowRelative: false,
406+
},
407+
},
408+
})).toBe(true);
409+
});
410+
411+
it('should not accept allowAbsolute=false', () => {
412+
expect(ajv.validate(inputSchema, {
413+
title: 'Test input schema',
414+
type: 'object',
415+
schemaVersion: 1,
416+
properties: {
417+
myField: {
418+
title: 'Field title',
419+
description: 'My test field',
420+
type: 'string',
421+
editor: 'datepicker',
422+
allowAbsolute: false,
423+
},
424+
},
425+
})).toBe(false);
426+
expect(ajv.errorsText()).toContain('data/properties/myField must have required property \'allowRelative\'');
427+
expect(parseAjvError(ajv.errors![0], 'schema.properties.myField')?.message)
428+
.toEqual('Field schema.properties.myField must accept absolute, relative or both dates. '
429+
+ 'Set "allowAbsolute", "allowRelative" or both properties.');
430+
});
431+
432+
it('should not accept allowAbsolute=false allowRelative=false', () => {
433+
expect(ajv.validate(inputSchema, {
434+
title: 'Test input schema',
435+
type: 'object',
436+
schemaVersion: 1,
437+
properties: {
438+
myField: {
439+
title: 'Field title',
440+
description: 'My test field',
441+
type: 'string',
442+
editor: 'datepicker',
443+
allowAbsolute: false,
444+
allowRelative: false,
445+
},
446+
},
447+
})).toBe(false);
448+
expect(ajv.errorsText()).toContain('data/properties/myField/allowAbsolute must be equal to constant');
449+
expect(parseAjvError(ajv.errors![0], 'schema.properties.myField')?.message)
450+
.toEqual('Field schema.properties.myField must accept absolute, relative or both dates. '
451+
+ 'Set "allowAbsolute", "allowRelative" or both properties.');
452+
});
453+
});
288454
});
289455
});

0 commit comments

Comments
 (0)