From b073e36398f28c7bb4dea0e310a49ff59e6f2739 Mon Sep 17 00:00:00 2001 From: martinforejt Date: Mon, 16 Sep 2024 12:46:28 +0200 Subject: [PATCH 01/10] feat(input_schema): datepicker isAbsolute/isRelative properties --- packages/input_schema/src/schema.json | 4 +++- packages/input_schema/src/types.ts | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/input_schema/src/schema.json b/packages/input_schema/src/schema.json index 3d6bed0e0..65bb6d41f 100644 --- a/packages/input_schema/src/schema.json +++ b/packages/input_schema/src/schema.json @@ -115,7 +115,9 @@ "editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue"] }, "isSecret": { "type": "boolean" }, "sectionCaption": { "type": "string" }, - "sectionDescription": { "type": "string" } + "sectionDescription": { "type": "string" }, + "isAbsolute": { "type": "boolean" }, + "isRelative": { "type": "boolean" } } }, "else": { diff --git a/packages/input_schema/src/types.ts b/packages/input_schema/src/types.ts index 4fa25a885..cf672fb23 100644 --- a/packages/input_schema/src/types.ts +++ b/packages/input_schema/src/types.ts @@ -18,6 +18,9 @@ export type StringFieldDefinition = CommonFieldDefinition & { enum?: readonly string[]; // required if editor is 'select' enumTitles?: readonly string[] isSecret?: boolean; + // used for 'datepicker' editor, isAbsolute is considered with default value true + isAbsolute?: boolean; + isRelative?: boolean; } export type BooleanFieldDefinition = CommonFieldDefinition & { From 3e1fd9f76969765f81f2064c5c9fb17e6eaa008a Mon Sep 17 00:00:00 2001 From: martinforejt Date: Mon, 16 Sep 2024 15:51:47 +0200 Subject: [PATCH 02/10] Added custom validation --- packages/input_schema/src/intl.ts | 8 ++++ packages/input_schema/src/utilities.ts | 52 ++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/input_schema/src/intl.ts b/packages/input_schema/src/intl.ts index 5ce3c7f11..f7798b280 100644 --- a/packages/input_schema/src/intl.ts +++ b/packages/input_schema/src/intl.ts @@ -33,6 +33,14 @@ const intlStrings = { 'Field schema.properties.{fieldKey} does not exist, but it is specified in schema.required. Either define the field or remove it from schema.required.', 'inputSchema.validation.proxyGroupMustBeArrayOfStrings': 'Field {rootName}.{fieldKey}.apifyProxyGroups must be an array of strings.', + 'inputSchema.validation.datepickerInvalidFormatAbsolute': + 'Field {rootName}.{fieldKey} must be a string in format "YYYY-MM-DD".', + 'inputSchema.validation.datepickerInvalidFormatRelative': + 'Field {rootName}.{fieldKey} must be a string in format "+ 1 day", "+ 3 months", "- 4 years" etc.', + 'inputSchema.validation.datepickerInvalidFormatBoth': + 'Field {rootName}.{fieldKey} must be a string in format "YYYY-MM-DD" or "+ 1 day", "+ 3 months", "- 4 years" etc.', + 'inputSchema.validation.datepickerInvalidDate': + 'Field {rootName}.{fieldKey} must be a valid date.', }; /** diff --git a/packages/input_schema/src/utilities.ts b/packages/input_schema/src/utilities.ts index 2c2278515..b4feb4616 100644 --- a/packages/input_schema/src/utilities.ts +++ b/packages/input_schema/src/utilities.ts @@ -137,12 +137,12 @@ export function validateInputUsingValidator( } Object.keys(properties).forEach((property) => { - const value = input[property] as Record; - const { type, editor, patternKey, patternValue } = properties[property]; + const value = input[property] as Record | Array | string | number | boolean | null; + const { type, editor, patternKey, patternValue, isAbsolute, isRelative } = properties[property]; const fieldErrors = []; // Check that proxy is required, if yes, valides that it's correctly setup if (type === 'object' && editor === 'proxy') { - const proxyValidationErrors = validateProxyField(property as any, value, required.includes(property), options.proxy); + const proxyValidationErrors = validateProxyField(property as any, value as Record, required.includes(property), options.proxy); proxyValidationErrors.forEach((error) => { fieldErrors.push(error); }); @@ -234,7 +234,7 @@ export function validateInputUsingValidator( const check = new RegExp(patternValue); const invalidKeys: any[] = []; Object.keys(value).forEach((key) => { - const propertyValue = value[key]; + const propertyValue = (value as Record)[key]; if (typeof propertyValue !== 'string' || !check.test(propertyValue)) invalidKeys.push(key); }); if (invalidKeys.length) { @@ -247,6 +247,50 @@ export function validateInputUsingValidator( } } } + + // Check datepicker editor format + if (type === 'string' && editor === 'datepicker' && value && typeof value === 'string') { + const acceptAbsolute = isAbsolute !== false; + const acceptRelative = isRelative === true; + const isValidAbsolute = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(value); + const isValidRelative = /^[+-] [0-9]+ (day|month|year)s?$/.test(value); + let isValidDate: boolean | undefined; + + if (isValidAbsolute) { + const [year, month, day] = value.split('-').map(Number); + const date = new Date(`${year}-${month}-${day}`); + + // Check if the date object is valid and matches the input string + isValidDate = date.getFullYear() === year + && date.getMonth() + 1 === month + && date.getDate() === day; + } + + if (acceptAbsolute && !acceptRelative && !isValidAbsolute) { + fieldErrors.push(m('inputSchema.validation.datepickerInvalidFormatAbsolute', { + rootName: 'input', + fieldKey: property, + })); + } else if (acceptRelative && !acceptAbsolute && !isValidRelative) { + fieldErrors.push(m('inputSchema.validation.datepickerInvalidFormatRelative', { + rootName: 'input', + fieldKey: property, + })); + } else if ((acceptAbsolute && !acceptRelative && !isValidAbsolute) + || (acceptRelative && !acceptAbsolute && !isValidRelative) + || (acceptRelative && acceptAbsolute && !isValidAbsolute && !isValidRelative)) { + fieldErrors.push(m('inputSchema.validation.datepickerInvalidFormatBoth', { + rootName: 'input', + fieldKey: property, + })); + } else if (isValidDate === false && acceptAbsolute && !acceptRelative) { + fieldErrors.push(m('inputSchema.validation.datepickerInvalidDate', { + rootName: 'input', + fieldKey: property, + })); + } + } + if (fieldErrors.length > 0) { const message = fieldErrors.join(', '); errors.push({ fieldKey: property, message }); From 2d26f34c56d124ad6040f6ff1d9f6a3033779217 Mon Sep 17 00:00:00 2001 From: martinforejt Date: Wed, 25 Sep 2024 15:09:39 +0200 Subject: [PATCH 03/10] update validation --- packages/input_schema/src/intl.ts | 4 ++-- packages/input_schema/src/utilities.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/input_schema/src/intl.ts b/packages/input_schema/src/intl.ts index f7798b280..24f55917a 100644 --- a/packages/input_schema/src/intl.ts +++ b/packages/input_schema/src/intl.ts @@ -36,9 +36,9 @@ const intlStrings = { 'inputSchema.validation.datepickerInvalidFormatAbsolute': 'Field {rootName}.{fieldKey} must be a string in format "YYYY-MM-DD".', 'inputSchema.validation.datepickerInvalidFormatRelative': - 'Field {rootName}.{fieldKey} must be a string in format "+ 1 day", "+ 3 months", "- 4 years" etc.', + 'Field {rootName}.{fieldKey} must be a string in format "+/- number unit ". Supported units are "day", "week", "month", "year".', 'inputSchema.validation.datepickerInvalidFormatBoth': - 'Field {rootName}.{fieldKey} must be a string in format "YYYY-MM-DD" or "+ 1 day", "+ 3 months", "- 4 years" etc.', + 'Field {rootName}.{fieldKey} must be a string in format "YYYY-MM-DD" or "+/- number unit ". Supported units are "day", "week", "month", "year".', 'inputSchema.validation.datepickerInvalidDate': 'Field {rootName}.{fieldKey} must be a valid date.', }; diff --git a/packages/input_schema/src/utilities.ts b/packages/input_schema/src/utilities.ts index b4feb4616..ffd5fc6a3 100644 --- a/packages/input_schema/src/utilities.ts +++ b/packages/input_schema/src/utilities.ts @@ -253,7 +253,7 @@ export function validateInputUsingValidator( const acceptAbsolute = isAbsolute !== false; const acceptRelative = isRelative === true; const isValidAbsolute = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(value); - const isValidRelative = /^[+-] [0-9]+ (day|month|year)s?$/.test(value); + const isValidRelative = /^[+-] [0-9]+ (day|week|month|year)s?$/.test(value); let isValidDate: boolean | undefined; if (isValidAbsolute) { @@ -283,7 +283,7 @@ export function validateInputUsingValidator( rootName: 'input', fieldKey: property, })); - } else if (isValidDate === false && acceptAbsolute && !acceptRelative) { + } else if (isValidDate === false && acceptAbsolute) { fieldErrors.push(m('inputSchema.validation.datepickerInvalidDate', { rootName: 'input', fieldKey: property, From aee4c0c8ec5c92f3a4b86b93aa057c4bfffcc65a Mon Sep 17 00:00:00 2001 From: martinforejt Date: Thu, 26 Sep 2024 12:30:51 +0200 Subject: [PATCH 04/10] rename isAbsolute/isRelative props to allowAbsolute/allowRelative Update schema.json to not accept both sets to `false` Create unit tests --- packages/input_schema/src/input_schema.ts | 10 +- packages/input_schema/src/intl.ts | 2 + packages/input_schema/src/schema.json | 121 +++++++----- packages/input_schema/src/types.ts | 7 +- packages/input_schema/src/utilities.ts | 6 +- test/input_schema_definition.test.ts | 158 ++++++++++++++++ test/utilities.client.test.ts | 213 ++++++++++++++++++++++ 7 files changed, 467 insertions(+), 50 deletions(-) diff --git a/packages/input_schema/src/input_schema.ts b/packages/input_schema/src/input_schema.ts index a56cd5b3d..2c79a5421 100644 --- a/packages/input_schema/src/input_schema.ts +++ b/packages/input_schema/src/input_schema.ts @@ -25,7 +25,7 @@ const { definitions } = schema; export function parseAjvError( error: ErrorObject, rootName: string, - properties: Record = {}, + properties: Record = {}, input: Record = {}, ): { fieldKey: string; message: string } | null { // There are 3 possible errors comming from validation: @@ -55,6 +55,14 @@ export function parseAjvError( fieldKey = error.instancePath.split('/').pop()!; const errorMessage = `${error.message}: "${error.params.allowedValues.join('", "')}"`; message = m('inputSchema.validation.generic', { rootName, fieldKey, message: errorMessage }); + } else if (error.keyword === 'const') { + fieldKey = error.instancePath.split('/').pop()!; + // This is a special case for datepicker fields, where both allowAbsolute and allowRelative properties are set to false + if (properties[fieldKey] && properties[fieldKey].editor === 'datepicker') { + message = m('inputSchema.validation.datepickerNoType', { rootName, fieldKey }); + } else { + message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message }); + } } else { fieldKey = error.instancePath.split('/').pop()!; message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message }); diff --git a/packages/input_schema/src/intl.ts b/packages/input_schema/src/intl.ts index 24f55917a..2c91e22bf 100644 --- a/packages/input_schema/src/intl.ts +++ b/packages/input_schema/src/intl.ts @@ -41,6 +41,8 @@ const intlStrings = { 'Field {rootName}.{fieldKey} must be a string in format "YYYY-MM-DD" or "+/- number unit ". Supported units are "day", "week", "month", "year".', 'inputSchema.validation.datepickerInvalidDate': 'Field {rootName}.{fieldKey} must be a valid date.', + 'inputSchema.validation.datepickerNoType': + 'Field {rootName}.{fieldKey} must accept absolute, relative or both dates. Set "allowAbsolute" and "allowRelative" properties.', }; /** diff --git a/packages/input_schema/src/schema.json b/packages/input_schema/src/schema.json index 65bb6d41f..ec49fcfcb 100644 --- a/packages/input_schema/src/schema.json +++ b/packages/input_schema/src/schema.json @@ -90,50 +90,85 @@ "isSecret": { "type": "boolean" } }, "required": ["type", "title", "description", "editor"], - "if": { - "properties": { - "isSecret": { - "not": { - "const": true + "if": { + "properties": { + "isSecret": { + "not": { + "const": true + } + } + } + }, + "then": { + "if": { + "properties": { + "editor": { "const": "datepicker" } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "type": { "enum": ["string"] }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "default": { "type": "string" }, + "prefill": { "type": "string" }, + "example": { "type": "string" }, + "pattern": { "type": "string" }, + "nullable": { "type": "boolean" }, + "minLength": { "type": "integer" }, + "maxLength": { "type": "integer" }, + "editor": { "enum": ["datepicker"] }, + "sectionCaption": { "type": "string" }, + "sectionDescription": { "type": "string" }, + "allowAbsolute": {"type": "boolean"}, + "allowRelative": {"type": "boolean"} + }, + "if": { + "required": ["allowAbsolute"], + "properties": { "allowAbsolute": { "const": false } } + }, + "then": { "required": ["type", "title", "description", "editor", "allowRelative"] }, + "anyOf": [ + { "properties": { "allowAbsolute": { "const": null }, "allowRelative": {"const": null} } }, + { "properties": { "allowAbsolute": { "const": true } } }, + { "properties": { "allowRelative": { "const": true } } } + ] + }, + "else": { + "additionalProperties": false, + "properties": { + "type": { "enum": ["string"] }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "default": { "type": "string" }, + "prefill": { "type": "string" }, + "example": { "type": "string" }, + "pattern": { "type": "string" }, + "nullable": { "type": "boolean" }, + "minLength": { "type": "integer" }, + "maxLength": { "type": "integer" }, + "editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue"] }, + "isSecret": { "type": "boolean" }, + "sectionCaption": { "type": "string" }, + "sectionDescription": { "type": "string" } + } + } + }, + "else": { + "additionalProperties": false, + "properties": { + "type": { "enum": ["string"] }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "example": { "type": "string" }, + "nullable": { "type": "boolean" }, + "editor": { "enum": ["textfield", "textarea", "hidden"] }, + "isSecret": { "type": "boolean" }, + "sectionCaption": { "type": "string" }, + "sectionDescription": { "type": "string" } + } } - } - } - }, - "then": { - "additionalProperties": false, - "properties": { - "type": { "enum": ["string"] }, - "title": { "type": "string" }, - "description": { "type": "string" }, - "default": { "type": "string" }, - "prefill": { "type": "string" }, - "example": { "type": "string" }, - "pattern": { "type": "string" }, - "nullable": { "type": "boolean" }, - "minLength": { "type": "integer" }, - "maxLength": { "type": "integer" }, - "editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue"] }, - "isSecret": { "type": "boolean" }, - "sectionCaption": { "type": "string" }, - "sectionDescription": { "type": "string" }, - "isAbsolute": { "type": "boolean" }, - "isRelative": { "type": "boolean" } - } - }, - "else": { - "additionalProperties": false, - "properties": { - "type": { "enum": ["string"] }, - "title": { "type": "string" }, - "description": { "type": "string" }, - "example": { "type": "string" }, - "nullable": { "type": "boolean" }, - "editor": { "enum": ["textfield", "textarea", "hidden"] }, - "isSecret": { "type": "boolean" }, - "sectionCaption": { "type": "string" }, - "sectionDescription": { "type": "string" } - } - } }, "arrayProperty": { "title": "Array property", diff --git a/packages/input_schema/src/types.ts b/packages/input_schema/src/types.ts index cf672fb23..1b573cc4b 100644 --- a/packages/input_schema/src/types.ts +++ b/packages/input_schema/src/types.ts @@ -18,9 +18,10 @@ export type StringFieldDefinition = CommonFieldDefinition & { enum?: readonly string[]; // required if editor is 'select' enumTitles?: readonly string[] isSecret?: boolean; - // used for 'datepicker' editor, isAbsolute is considered with default value true - isAbsolute?: boolean; - isRelative?: boolean; + // Used for 'datepicker' editor, allowAbsolute is considered with default value true + // If only relative time is wanted, allowAbsolute must be explicitly set to false + allowAbsolute?: boolean; + allowRelative?: boolean; } export type BooleanFieldDefinition = CommonFieldDefinition & { diff --git a/packages/input_schema/src/utilities.ts b/packages/input_schema/src/utilities.ts index ffd5fc6a3..d0b76efcf 100644 --- a/packages/input_schema/src/utilities.ts +++ b/packages/input_schema/src/utilities.ts @@ -138,7 +138,7 @@ export function validateInputUsingValidator( Object.keys(properties).forEach((property) => { const value = input[property] as Record | Array | string | number | boolean | null; - const { type, editor, patternKey, patternValue, isAbsolute, isRelative } = properties[property]; + const { type, editor, patternKey, patternValue, allowAbsolute, allowRelative } = properties[property]; const fieldErrors = []; // Check that proxy is required, if yes, valides that it's correctly setup if (type === 'object' && editor === 'proxy') { @@ -250,8 +250,8 @@ export function validateInputUsingValidator( // Check datepicker editor format if (type === 'string' && editor === 'datepicker' && value && typeof value === 'string') { - const acceptAbsolute = isAbsolute !== false; - const acceptRelative = isRelative === true; + const acceptAbsolute = allowAbsolute !== false; + const acceptRelative = allowRelative === true; const isValidAbsolute = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(value); const isValidRelative = /^[+-] [0-9]+ (day|week|month|year)s?$/.test(value); let isValidDate: boolean | undefined; diff --git a/test/input_schema_definition.test.ts b/test/input_schema_definition.test.ts index 12e19dc1e..95065d3e5 100644 --- a/test/input_schema_definition.test.ts +++ b/test/input_schema_definition.test.ts @@ -285,5 +285,163 @@ describe('input_schema.json', () => { })).toBe(false); }); }); + + describe('special cases for datepicker editor type', () => { + it('should accept allowAbsolute and allowRelative fields omitted', () => { + expect(ajv.validate(inputSchema, { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + }, + }, + })).toBe(true); + }); + + it('should accept allowAbsolute and allowRelative both set to true', () => { + expect(ajv.validate(inputSchema, { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + allowAbsolute: true, + allowRelative: true, + }, + }, + })).toBe(true); + }); + + it('should accept allowAbsolute=true and allowRelative=false', () => { + expect(ajv.validate(inputSchema, { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + allowAbsolute: true, + allowRelative: false, + }, + }, + })).toBe(true); + }); + + it('should accept allowAbsolute=false and allowRelative=true', () => { + expect(ajv.validate(inputSchema, { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + allowAbsolute: false, + allowRelative: true, + }, + }, + })).toBe(true); + }); + + it('should accept allowAbsolute=true', () => { + expect(ajv.validate(inputSchema, { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + allowAbsolute: true, + }, + }, + })).toBe(true); + }); + + it('should accept allowRelative=true', () => { + expect(ajv.validate(inputSchema, { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + allowRelative: true, + }, + }, + })).toBe(true); + }); + + it('should accept allowRelative=false', () => { + expect(ajv.validate(inputSchema, { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + allowRelative: false, + }, + }, + })).toBe(true); + }); + + it('should not accept allowAbsolute=false', () => { + expect(ajv.validate(inputSchema, { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + allowAbsolute: false, + }, + }, + })).toBe(false); + }); + + it('should not accept allowAbsolute=false allowRelative=false', () => { + expect(ajv.validate(inputSchema, { + title: 'Test input schema', + type: 'object', + schemaVersion: 1, + properties: { + myField: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + allowAbsolute: false, + allowRelative: false, + }, + }, + })).toBe(false); + }); + }); }); }); diff --git a/test/utilities.client.test.ts b/test/utilities.client.test.ts index 351db6e1d..ad059e70a 100644 --- a/test/utilities.client.test.ts +++ b/test/utilities.client.test.ts @@ -1018,6 +1018,219 @@ describe('utilities.client', () => { expect(result[0].fieldKey).toEqual('field'); }); }); + + describe('special cases for datepicker string type', () => { + it('should allow absolute dates when allowAbsolute is omitted', () => { + const { inputSchema, validator } = buildInputSchema({ + field: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + nullable: true, + }, + }); + const inputs = [ + // 5 invalid inputs + { field: {} }, + { field: [] }, + { field: 'invalid string' }, + { field: '12.10.2024' }, + { field: '+ 1 day' }, + // Valid + { field: '2022-12-31' }, + { field: '2024-01-31' }, + { field: '1999-02-28' }, + { field: null }, + ]; + + const results = inputs + .map((input) => validateInputUsingValidator(validator, inputSchema, input)) + .filter((errors) => errors.length > 0); + + // There should be 5 invalid inputs + expect(results.length).toEqual(5); + results.forEach((result) => { + // Only one error should be thrown + expect(result.length).toEqual(1); + expect(result[0].fieldKey).toEqual('field'); + }); + }); + + it('should allow absolute dates when allowAbsolute is set to true', () => { + const { inputSchema, validator } = buildInputSchema({ + field: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + allowAbsolute: true, + }, + }); + const inputs = [ + // 6 invalid inputs + { field: {} }, + { field: [] }, + { field: 'invalid string' }, + { field: '12.10.2024' }, + { field: '+ 1 day' }, + { field: null }, + // Valid + { field: '2022-12-31' }, + { field: '2024-01-31' }, + { field: '1999-02-28' }, + ]; + + const results = inputs + .map((input) => validateInputUsingValidator(validator, inputSchema, input)) + .filter((errors) => errors.length > 0); + + // There should be 6 invalid inputs + expect(results.length).toEqual(6); + results.forEach((result) => { + // Only one error should be thrown + expect(result.length).toEqual(1); + expect(result[0].fieldKey).toEqual('field'); + }); + }); + + it('should allow only relative dates when allowRelative is true and allowAbsolute is false', () => { + const { inputSchema, validator } = buildInputSchema({ + field: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + allowAbsolute: false, + allowRelative: true, + }, + }); + const inputs = [ + // 11 invalid inputs + { field: '2022-12-31' }, + { field: '2024-01-31' }, + { field: '1999-02-28' }, + { field: {} }, + { field: [] }, + { field: 'invalid string' }, + { field: '12.10.2024' }, + { field: null }, + { field: '+ 1 minutes' }, + { field: '1 day' }, + { field: '- 11 sec' }, + // Valid + { field: '+ 1 day' }, + { field: '+ 3 days' }, + { field: '+ 5 weeks' }, + { field: '- 10 months' }, + { field: '+ 3 years' }, + { field: '- 0 days' }, + ]; + + const results = inputs + .map((input) => validateInputUsingValidator(validator, inputSchema, input)) + .filter((errors) => errors.length > 0); + + // There should be 11 invalid inputs + expect(results.length).toEqual(11); + results.forEach((result) => { + // Only one error should be thrown + expect(result.length).toEqual(1); + expect(result[0].fieldKey).toEqual('field'); + }); + }); + + it('should allow both dates when allowRelative is true', () => { + const { inputSchema, validator } = buildInputSchema({ + field: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + allowRelative: true, + }, + }); + const inputs = [ + // 8 invalid inputs + { field: {} }, + { field: [] }, + { field: 'invalid string' }, + { field: '12.10.2024' }, + { field: null }, + { field: '+ 1 minutes' }, + { field: '1 day' }, + { field: '- 11 sec' }, + // Valid + { field: '2022-12-31' }, + { field: '2024-01-31' }, + { field: '1999-02-28' }, + { field: '+ 1 day' }, + { field: '+ 3 days' }, + { field: '+ 5 weeks' }, + { field: '- 10 months' }, + { field: '+ 3 years' }, + { field: '- 0 days' }, + ]; + + const results = inputs + .map((input) => validateInputUsingValidator(validator, inputSchema, input)) + .filter((errors) => errors.length > 0); + + // There should be 8 invalid inputs + expect(results.length).toEqual(8); + results.forEach((result) => { + // Only one error should be thrown + expect(result.length).toEqual(1); + expect(result[0].fieldKey).toEqual('field'); + }); + }); + + it('should allow both dates when allowRelative is true and allowAbsolute is true', () => { + const { inputSchema, validator } = buildInputSchema({ + field: { + title: 'Field title', + description: 'My test field', + type: 'string', + editor: 'datepicker', + allowRelative: true, + allowAbsolute: true, + }, + }); + const inputs = [ + // 8 invalid inputs + { field: {} }, + { field: [] }, + { field: 'invalid string' }, + { field: '12.10.2024' }, + { field: null }, + { field: '+ 1 minutes' }, + { field: '1 day' }, + { field: '- 11 sec' }, + // Valid + { field: '2022-12-31' }, + { field: '2024-01-31' }, + { field: '1999-02-28' }, + { field: '+ 1 day' }, + { field: '+ 3 days' }, + { field: '+ 5 weeks' }, + { field: '- 10 months' }, + { field: '+ 3 years' }, + { field: '- 0 days' }, + ]; + + const results = inputs + .map((input) => validateInputUsingValidator(validator, inputSchema, input)) + .filter((errors) => errors.length > 0); + + // There should be 8 invalid inputs + expect(results.length).toEqual(8); + results.forEach((result) => { + // Only one error should be thrown + expect(result.length).toEqual(1); + expect(result[0].fieldKey).toEqual('field'); + }); + }); + }); }); describe('#jsonStringifyExtended()', () => { From 316f0a8a65f0dae40dc2ce5f5397e55878ed8cfc Mon Sep 17 00:00:00 2001 From: martinforejt Date: Thu, 26 Sep 2024 14:04:23 +0200 Subject: [PATCH 05/10] cleanup schema --- packages/input_schema/src/schema.json | 152 +++++++++++++------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/packages/input_schema/src/schema.json b/packages/input_schema/src/schema.json index ec49fcfcb..2be9bc437 100644 --- a/packages/input_schema/src/schema.json +++ b/packages/input_schema/src/schema.json @@ -90,85 +90,85 @@ "isSecret": { "type": "boolean" } }, "required": ["type", "title", "description", "editor"], - "if": { - "properties": { - "isSecret": { - "not": { - "const": true - } - } + "if": { + "properties": { + "isSecret": { + "not": { + "const": true } + } + } + }, + "then": { + "if": { + "properties": { + "editor": { "const": "datepicker" } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "type": { "enum": ["string"] }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "default": { "type": "string" }, + "prefill": { "type": "string" }, + "example": { "type": "string" }, + "pattern": { "type": "string" }, + "nullable": { "type": "boolean" }, + "minLength": { "type": "integer" }, + "maxLength": { "type": "integer" }, + "editor": { "enum": ["datepicker"] }, + "sectionCaption": { "type": "string" }, + "sectionDescription": { "type": "string" }, + "allowAbsolute": {"type": "boolean"}, + "allowRelative": {"type": "boolean"} }, - "then": { - "if": { - "properties": { - "editor": { "const": "datepicker" } - } - }, - "then": { - "additionalProperties": false, - "properties": { - "type": { "enum": ["string"] }, - "title": { "type": "string" }, - "description": { "type": "string" }, - "default": { "type": "string" }, - "prefill": { "type": "string" }, - "example": { "type": "string" }, - "pattern": { "type": "string" }, - "nullable": { "type": "boolean" }, - "minLength": { "type": "integer" }, - "maxLength": { "type": "integer" }, - "editor": { "enum": ["datepicker"] }, - "sectionCaption": { "type": "string" }, - "sectionDescription": { "type": "string" }, - "allowAbsolute": {"type": "boolean"}, - "allowRelative": {"type": "boolean"} - }, - "if": { - "required": ["allowAbsolute"], - "properties": { "allowAbsolute": { "const": false } } - }, - "then": { "required": ["type", "title", "description", "editor", "allowRelative"] }, - "anyOf": [ - { "properties": { "allowAbsolute": { "const": null }, "allowRelative": {"const": null} } }, - { "properties": { "allowAbsolute": { "const": true } } }, - { "properties": { "allowRelative": { "const": true } } } - ] - }, - "else": { - "additionalProperties": false, - "properties": { - "type": { "enum": ["string"] }, - "title": { "type": "string" }, - "description": { "type": "string" }, - "default": { "type": "string" }, - "prefill": { "type": "string" }, - "example": { "type": "string" }, - "pattern": { "type": "string" }, - "nullable": { "type": "boolean" }, - "minLength": { "type": "integer" }, - "maxLength": { "type": "integer" }, - "editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue"] }, - "isSecret": { "type": "boolean" }, - "sectionCaption": { "type": "string" }, - "sectionDescription": { "type": "string" } - } - } + "if": { + "required": ["allowAbsolute"], + "properties": { "allowAbsolute": { "const": false } } }, - "else": { - "additionalProperties": false, - "properties": { - "type": { "enum": ["string"] }, - "title": { "type": "string" }, - "description": { "type": "string" }, - "example": { "type": "string" }, - "nullable": { "type": "boolean" }, - "editor": { "enum": ["textfield", "textarea", "hidden"] }, - "isSecret": { "type": "boolean" }, - "sectionCaption": { "type": "string" }, - "sectionDescription": { "type": "string" } - } - } + "then": { "required": ["type", "title", "description", "editor", "allowRelative"] }, + "anyOf": [ + { "properties": { "allowAbsolute": { "const": null }, "allowRelative": {"const": null} } }, + { "properties": { "allowAbsolute": { "const": true } } }, + { "properties": { "allowRelative": { "const": true } } } + ] + }, + "else": { + "additionalProperties": false, + "properties": { + "type": { "enum": ["string"] }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "default": { "type": "string" }, + "prefill": { "type": "string" }, + "example": { "type": "string" }, + "pattern": { "type": "string" }, + "nullable": { "type": "boolean" }, + "minLength": { "type": "integer" }, + "maxLength": { "type": "integer" }, + "editor": { "enum": ["javascript", "python", "textfield", "textarea", "datepicker", "hidden", "dataset", "keyValueStore", "requestQueue"] }, + "isSecret": { "type": "boolean" }, + "sectionCaption": { "type": "string" }, + "sectionDescription": { "type": "string" } + } + } + }, + "else": { + "additionalProperties": false, + "properties": { + "type": { "enum": ["string"] }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "example": { "type": "string" }, + "nullable": { "type": "boolean" }, + "editor": { "enum": ["textfield", "textarea", "hidden"] }, + "isSecret": { "type": "boolean" }, + "sectionCaption": { "type": "string" }, + "sectionDescription": { "type": "string" } + } + } }, "arrayProperty": { "title": "Array property", From 5716f07bd93da4f83638006fb3fc0171f90ae9f6 Mon Sep 17 00:00:00 2001 From: martinforejt Date: Thu, 26 Sep 2024 14:17:02 +0200 Subject: [PATCH 06/10] cleanup schema --- packages/input_schema/src/schema.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/input_schema/src/schema.json b/packages/input_schema/src/schema.json index 2be9bc437..f5bd1a1c9 100644 --- a/packages/input_schema/src/schema.json +++ b/packages/input_schema/src/schema.json @@ -130,7 +130,6 @@ }, "then": { "required": ["type", "title", "description", "editor", "allowRelative"] }, "anyOf": [ - { "properties": { "allowAbsolute": { "const": null }, "allowRelative": {"const": null} } }, { "properties": { "allowAbsolute": { "const": true } } }, { "properties": { "allowRelative": { "const": true } } } ] From 3427325e2c6ce5c310ab16060b8b5348987254a6 Mon Sep 17 00:00:00 2001 From: Martin Forejt Date: Fri, 27 Sep 2024 09:08:37 +0200 Subject: [PATCH 07/10] Update packages/input_schema/src/schema.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit indentation Co-authored-by: Martin Adámek --- packages/input_schema/src/schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/input_schema/src/schema.json b/packages/input_schema/src/schema.json index f5bd1a1c9..ce0f332dc 100644 --- a/packages/input_schema/src/schema.json +++ b/packages/input_schema/src/schema.json @@ -121,8 +121,8 @@ "editor": { "enum": ["datepicker"] }, "sectionCaption": { "type": "string" }, "sectionDescription": { "type": "string" }, - "allowAbsolute": {"type": "boolean"}, - "allowRelative": {"type": "boolean"} + "allowAbsolute": { "type": "boolean" }, + "allowRelative": { "type": "boolean" } }, "if": { "required": ["allowAbsolute"], From 4f983cb5700dc3de07a0487d88a6822c099bab3b Mon Sep 17 00:00:00 2001 From: martinforejt Date: Fri, 27 Sep 2024 09:13:17 +0200 Subject: [PATCH 08/10] remove unnecessary retyping --- packages/input_schema/src/utilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/input_schema/src/utilities.ts b/packages/input_schema/src/utilities.ts index d0b76efcf..b2919dc0e 100644 --- a/packages/input_schema/src/utilities.ts +++ b/packages/input_schema/src/utilities.ts @@ -137,7 +137,7 @@ export function validateInputUsingValidator( } Object.keys(properties).forEach((property) => { - const value = input[property] as Record | Array | string | number | boolean | null; + const value = input[property]; const { type, editor, patternKey, patternValue, allowAbsolute, allowRelative } = properties[property]; const fieldErrors = []; // Check that proxy is required, if yes, valides that it's correctly setup From a088af343a9066a666a7086809427e651a3e69c3 Mon Sep 17 00:00:00 2001 From: martinforejt Date: Fri, 27 Sep 2024 09:17:09 +0200 Subject: [PATCH 09/10] address intl PR change requests --- packages/input_schema/src/intl.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/input_schema/src/intl.ts b/packages/input_schema/src/intl.ts index 2c91e22bf..ad4c5aedc 100644 --- a/packages/input_schema/src/intl.ts +++ b/packages/input_schema/src/intl.ts @@ -36,13 +36,13 @@ const intlStrings = { 'inputSchema.validation.datepickerInvalidFormatAbsolute': 'Field {rootName}.{fieldKey} must be a string in format "YYYY-MM-DD".', 'inputSchema.validation.datepickerInvalidFormatRelative': - 'Field {rootName}.{fieldKey} must be a string in format "+/- number unit ". Supported units are "day", "week", "month", "year".', + 'Field {rootName}.{fieldKey} must be a string in format "+/- number unit". Supported units are "day", "week", "month" and "year".', 'inputSchema.validation.datepickerInvalidFormatBoth': - 'Field {rootName}.{fieldKey} must be a string in format "YYYY-MM-DD" or "+/- number unit ". Supported units are "day", "week", "month", "year".', + 'Field {rootName}.{fieldKey} must be a string in format "YYYY-MM-DD" or "+/- number unit". Supported units are "day", "week", "month" and "year".', 'inputSchema.validation.datepickerInvalidDate': 'Field {rootName}.{fieldKey} must be a valid date.', 'inputSchema.validation.datepickerNoType': - 'Field {rootName}.{fieldKey} must accept absolute, relative or both dates. Set "allowAbsolute" and "allowRelative" properties.', + 'Field {rootName}.{fieldKey} must accept absolute, relative or both dates. Set "allowAbsolute", "allowRelative" or both properties.', }; /** From 9f5928d70e3de6e26e53cfd590c74fffadda0723 Mon Sep 17 00:00:00 2001 From: martinforejt Date: Fri, 27 Sep 2024 10:50:13 +0200 Subject: [PATCH 10/10] update tests to check error message --- packages/input_schema/src/input_schema.ts | 11 ++++++++--- packages/input_schema/src/intl.ts | 2 +- test/input_schema_definition.test.ts | 10 +++++++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/input_schema/src/input_schema.ts b/packages/input_schema/src/input_schema.ts index 2c79a5421..99439cf41 100644 --- a/packages/input_schema/src/input_schema.ts +++ b/packages/input_schema/src/input_schema.ts @@ -47,7 +47,12 @@ export function parseAjvError( message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message }); } else if (error.keyword === 'required') { fieldKey = error.params.missingProperty; - message = m('inputSchema.validation.required', { rootName, fieldKey }); + if (fieldKey === 'allowRelative') { + // this is the case, when allowAbsolute is set to false, but allowRelative is not set + message = m('inputSchema.validation.datepickerNoType', { rootName }); + } else { + message = m('inputSchema.validation.required', { rootName, fieldKey }); + } } else if (error.keyword === 'additionalProperties') { fieldKey = error.params.additionalProperty; message = m('inputSchema.validation.additionalProperty', { rootName, fieldKey }); @@ -58,8 +63,8 @@ export function parseAjvError( } else if (error.keyword === 'const') { fieldKey = error.instancePath.split('/').pop()!; // This is a special case for datepicker fields, where both allowAbsolute and allowRelative properties are set to false - if (properties[fieldKey] && properties[fieldKey].editor === 'datepicker') { - message = m('inputSchema.validation.datepickerNoType', { rootName, fieldKey }); + if (fieldKey === 'allowRelative' || fieldKey === 'allowAbsolute') { + message = m('inputSchema.validation.datepickerNoType', { rootName }); } else { message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message }); } diff --git a/packages/input_schema/src/intl.ts b/packages/input_schema/src/intl.ts index ad4c5aedc..38a3c2c2e 100644 --- a/packages/input_schema/src/intl.ts +++ b/packages/input_schema/src/intl.ts @@ -42,7 +42,7 @@ const intlStrings = { 'inputSchema.validation.datepickerInvalidDate': 'Field {rootName}.{fieldKey} must be a valid date.', 'inputSchema.validation.datepickerNoType': - 'Field {rootName}.{fieldKey} must accept absolute, relative or both dates. Set "allowAbsolute", "allowRelative" or both properties.', + 'Field {rootName} must accept absolute, relative or both dates. Set "allowAbsolute", "allowRelative" or both properties.', }; /** diff --git a/test/input_schema_definition.test.ts b/test/input_schema_definition.test.ts index 95065d3e5..0b049d7c2 100644 --- a/test/input_schema_definition.test.ts +++ b/test/input_schema_definition.test.ts @@ -1,4 +1,4 @@ -import { inputSchema } from '@apify/input_schema'; +import { inputSchema, parseAjvError } from '@apify/input_schema'; import Ajv from 'ajv'; /** @@ -423,6 +423,10 @@ describe('input_schema.json', () => { }, }, })).toBe(false); + expect(ajv.errorsText()).toContain('data/properties/myField must have required property \'allowRelative\''); + expect(parseAjvError(ajv.errors![0], 'schema.properties.myField')?.message) + .toEqual('Field schema.properties.myField must accept absolute, relative or both dates. ' + + 'Set "allowAbsolute", "allowRelative" or both properties.'); }); it('should not accept allowAbsolute=false allowRelative=false', () => { @@ -441,6 +445,10 @@ describe('input_schema.json', () => { }, }, })).toBe(false); + expect(ajv.errorsText()).toContain('data/properties/myField/allowAbsolute must be equal to constant'); + expect(parseAjvError(ajv.errors![0], 'schema.properties.myField')?.message) + .toEqual('Field schema.properties.myField must accept absolute, relative or both dates. ' + + 'Set "allowAbsolute", "allowRelative" or both properties.'); }); }); });