Skip to content
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 @@ -25,7 +25,7 @@ const { definitions } = schema;
export function parseAjvError(
error: ErrorObject,
rootName: string,
properties: Record<string, { nullable?: boolean }> = {},
properties: Record<string, { nullable?: boolean, editor?: string }> = {},
input: Record<string, unknown> = {},
): { fieldKey: string; message: string } | null {
// There are 3 possible errors comming from validation:
Expand Down Expand Up @@ -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 });
Expand Down
10 changes: 10 additions & 0 deletions packages/input_schema/src/intl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ 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 "+/- 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 "+/- 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.',
};

/**
Expand Down
68 changes: 52 additions & 16 deletions packages/input_schema/src/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,22 +100,58 @@
}
},
"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" }
"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": 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": {
Expand Down
4 changes: 4 additions & 0 deletions packages/input_schema/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export type StringFieldDefinition = CommonFieldDefinition<string> & {
enum?: readonly string[]; // required if editor is 'select'
enumTitles?: readonly string[]
isSecret?: 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<boolean> & {
Expand Down
52 changes: 48 additions & 4 deletions packages/input_schema/src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,12 @@ export function validateInputUsingValidator(
}

Object.keys(properties).forEach((property) => {
const value = input[property] as Record<string, any>;
const { type, editor, patternKey, patternValue } = properties[property];
const value = input[property] as Record<string, any> | Array<any> | string | number | boolean | null;
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') {
const proxyValidationErrors = validateProxyField(property as any, value, required.includes(property), options.proxy);
const proxyValidationErrors = validateProxyField(property as any, value as Record<string, any>, required.includes(property), options.proxy);
proxyValidationErrors.forEach((error) => {
fieldErrors.push(error);
});
Expand Down Expand Up @@ -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<string, any>)[key];
if (typeof propertyValue !== 'string' || !check.test(propertyValue)) invalidKeys.push(key);
});
if (invalidKeys.length) {
Expand All @@ -247,6 +247,50 @@ export function validateInputUsingValidator(
}
}
}

// Check datepicker editor format
if (type === 'string' && editor === 'datepicker' && value && typeof value === 'string') {
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;

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) {
fieldErrors.push(m('inputSchema.validation.datepickerInvalidDate', {
rootName: 'input',
fieldKey: property,
}));
}
}

if (fieldErrors.length > 0) {
const message = fieldErrors.join(', ');
errors.push({ fieldKey: property, message });
Expand Down
158 changes: 158 additions & 0 deletions test/input_schema_definition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

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

I would rather test if the error message is correct as the logic is not trivial.

Copy link
Member Author

@mfori mfori Sep 27, 2024

Choose a reason for hiding this comment

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

Added check of error messages

});

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);
Copy link
Member

Choose a reason for hiding this comment

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

Same here.

});
});
});
});
Loading