Skip to content
Open
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
26 changes: 18 additions & 8 deletions packages/input_schema/src/input_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import { inputSchema as schema } from '@apify/json_schemas';

import { m } from './intl';
import type {
ArrayFieldDefinition,
CommonResourceFieldDefinition,
FieldDefinition,
InputSchema,
InputSchemaBaseChecked,
ObjectFieldDefinition,
StringFieldDefinition,
} from './types';
import { ensureAjvSupportsDraft2019, validateRegexpPattern } from './utilities';
Expand Down Expand Up @@ -143,6 +141,9 @@ export function parseAjvError(
} else if (error.keyword === 'const') {
fieldKey = cleanPropertyName(error.instancePath);
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message });
} else if (error.keyword === 'pattern' && error.propertyName && error.params?.pattern) {
fieldKey = cleanPropertyName(`${error.instancePath}/${error.propertyName}`);
message = m('inputSchema.validation.propertyName', { rootName, fieldKey, pattern: error.params.pattern });
} else {
fieldKey = cleanPropertyName(error.instancePath);
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message });
Expand Down Expand Up @@ -257,12 +258,21 @@ function validateField(validator: Ajv, fieldSchema: Record<string, unknown>, fie
validateFieldAgainstSchemaDefinition(validator, fieldSchema, fieldKey, isSubField);

// Validate regex patterns if defined.
const { pattern } = fieldSchema as Partial<StringFieldDefinition>;
const { patternKey, patternValue } = fieldSchema as Partial<ObjectFieldDefinition & ArrayFieldDefinition>;

if (pattern) validateRegexpPattern(pattern, `${fieldKey}.pattern`);
if (patternKey) validateRegexpPattern(patternKey, `${fieldKey}.patternKey`);
if (patternValue) validateRegexpPattern(patternValue, `${fieldKey}.patternValue`);
if ('pattern' in fieldSchema && fieldSchema.pattern) {
validateRegexpPattern(fieldSchema.pattern, `${fieldKey}.pattern`);
}
if ('patternKey' in fieldSchema && fieldSchema.patternKey) {
validateRegexpPattern(fieldSchema.patternKey, `${fieldKey}.patternKey`);
}
if ('patternValue' in fieldSchema && fieldSchema.patternValue) {
validateRegexpPattern(fieldSchema.patternValue, `${fieldKey}.patternValue`);
}
if ('propertyNames' in fieldSchema && fieldSchema.propertyNames?.pattern) {
validateRegexpPattern(fieldSchema.propertyNames.pattern, `${fieldKey}.propertyNames.pattern`);
}
if ('patternProperties' in fieldSchema && fieldSchema.patternProperties?.['.*'].pattern) {
validateRegexpPattern(fieldSchema.patternProperties['.*'].pattern, `${fieldKey}.patternProperties.*.pattern`);
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/input_schema/src/intl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const intlStrings = {
'The regular expression "{pattern}" in field schema.properties.{fieldKey} must be valid.',
'inputSchema.validation.regexpNotSafe':
'The regular expression "{pattern}" in field schema.properties.{fieldKey} may cause excessive backtracking or be unsafe to execute.',
'inputSchema.validation.propertyName':
'Property name of {rootName}.{fieldKey} must match pattern "{pattern}".',
};

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/input_schema/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export type ObjectFieldDefinition = CommonFieldDefinition<object> & {
properties?: Record<string, unknown>;
required?: string[];
additionalProperties?: boolean;
propertyNames?: { pattern: string };
patternProperties?: { '.*': { type: 'string', pattern: string } };
}

export type ArrayFieldDefinition = CommonFieldDefinition<unknown[]> & {
Expand Down
32 changes: 32 additions & 0 deletions packages/json_schemas/schemas/input.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,8 @@
"additionalProperties": {
"type": "boolean"
},
"propertyNames": { "$ref": "#/definitions/propertyNamesDefinition" },
"patternProperties": { "$ref": "#/definitions/patternPropertiesDefinition" },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"unevaluatedProperties": false,
Expand Down Expand Up @@ -429,6 +431,8 @@
"additionalProperties": {
"type": "boolean"
},
"propertyNames": { "$ref": "#/definitions/propertyNamesDefinition" },
"patternProperties": { "$ref": "#/definitions/patternPropertiesDefinition" },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
}
}
Expand Down Expand Up @@ -997,6 +1001,8 @@
"additionalProperties": {
"type": "boolean"
},
"propertyNames": { "$ref": "#/definitions/propertyNamesDefinition" },
"patternProperties": { "$ref": "#/definitions/patternPropertiesDefinition" },
"errorMessage": { "$ref": "#/definitions/errorMessage" }
},
"required": ["type", "title", "description"],
Expand Down Expand Up @@ -1644,6 +1650,32 @@
"patternValue": { "type": "string" }
},
"additionalProperties": false
},
"propertyNamesDefinition": {
"title": "Utils: Property names definition",
"type": "object",
"properties": {
"pattern": { "type": "string" }
},
"additionalProperties": false,
"required": ["pattern"]
},
"patternPropertiesDefinition": {
"title": "Utils: Pattern properties definition",
"type": "object",
"properties": {
".*": {
"type": "object",
"properties": {
"type": { "enum": ["string"] },
"pattern": { "type": "string" }
},
"additionalProperties": false,
"required": ["type", "pattern"]
}
},
"additionalProperties": false,
"required": [".*"]
}
}
}
243 changes: 243 additions & 0 deletions test/input_schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,15 @@ describe('input_schema.json', () => {
editor: 'json',
patternKey: '^[a-z]+$',
patternValue: '^[0-9]+$',
propertyNames: {
pattern: '^[a-zA-Z_][a-zA-Z0-9_]*$',
},
patternProperties: {
'.*': {
type: 'string',
pattern: '^[0-9]+$',
},
},
},
arrayField: {
title: 'Array field',
Expand Down Expand Up @@ -1335,6 +1344,240 @@ describe('input_schema.json', () => {
'Input schema is not valid (The regular expression "^[0-9+$" in field schema.properties.objectField.patternValue must be valid.)',
);
});

it('should throw error on invalid propertyNames regexp', () => {
const schema = {
title: 'Test input schema',
type: 'object',
schemaVersion: 1,
properties: {
objectField: {
title: 'Object field',
type: 'object',
description: 'Some description ...',
editor: 'json',
propertyNames: {
pattern: '^[0-9+$', // invalid regexp
},
},
},
};

expect(() => validateInputSchema(validator, schema)).toThrow(
'Input schema is not valid (The regular expression "^[0-9+$" in field schema.properties.objectField.propertyNames.pattern must be valid.)',
);
});

it('should throw error on invalid patternProperties regexp', () => {
const schema = {
title: 'Test input schema',
type: 'object',
schemaVersion: 1,
properties: {
objectField: {
title: 'Object field',
type: 'object',
description: 'Some description ...',
editor: 'json',
patternProperties: {
'.*': {
type: 'string',
pattern: '^[0-9+$', // invalid regexp
},
},
},
},
};

expect(() => validateInputSchema(validator, schema)).toThrow(
// eslint-disable-next-line max-len
'Input schema is not valid (The regular expression "^[0-9+$" in field schema.properties.objectField.patternProperties.*.pattern must be valid.)',
);
});
});

describe('propertyNames working correctly', () => {
it('should accept valid property names', () => {
const schema = {
title: 'Test input schema',
type: 'object',
schemaVersion: 1,
properties: {
myField: {
title: 'Field title',
type: 'object',
description: 'My test field',
editor: 'json',
propertyNames: {
pattern: '^[a-zA-Z_][a-zA-Z0-9_]*$',
},
},
},
};
expect(() => validateInputSchema(validator, schema)).not.toThrow();
});

it('should throw if pattern is missing', () => {
const schema = {
title: 'Test input schema',
type: 'object',
schemaVersion: 1,
properties: {
myField: {
title: 'Field title',
type: 'object',
description: 'My test field',
editor: 'json',
propertyNames: {
// missing pattern
},
},
},
};
expect(() => validateInputSchema(validator, schema)).toThrow(
'Input schema is not valid (Field schema.properties.myField.propertyNames.pattern is required)',
);
});

it('should not allow propertyNames for other than object type', () => {
const types = {
string: 'textfield',
integer: 'number',
number: 'number',
boolean: 'checkbox',
array: 'json',
};
Object.entries(types).forEach(([type, editor]) => {
const schema = {
title: 'Test input schema',
type: 'object',
schemaVersion: 1,
properties: {
myField: {
title: 'Field title',
type,
description: 'My test field',
editor,
propertyNames: {
pattern: '^[a-zA-Z_][a-zA-Z0-9_]*$',
},
},
},
};
expect(() => validateInputSchema(validator, schema)).toThrow(
`Input schema is not valid (Property schema.properties.myField.propertyNames is not allowed.)`,
);
});
});
});

describe('patternProperties working correctly', () => {
it('should accept valid patternProperties', () => {
const schema = {
title: 'Test input schema',
type: 'object',
schemaVersion: 1,
properties: {
myField: {
title: 'Field title',
type: 'object',
description: 'My test field',
editor: 'json',
patternProperties: {
'.*': {
type: 'string',
pattern: '^[0-9]+$',
},
},
},
},
};
expect(() => validateInputSchema(validator, schema)).not.toThrow();
});

it('should throw if patternProperties value is missing type', () => {
const schema = {
title: 'Test input schema',
type: 'object',
schemaVersion: 1,
properties: {
myField: {
title: 'Field title',
type: 'object',
description: 'My test field',
editor: 'json',
patternProperties: {
'.*': {
// missing type
},
},
},
},
};
expect(() => validateInputSchema(validator, schema)).toThrow(
'Input schema is not valid (Field schema.properties.myField.patternProperties..*.type is required)',
);
});

it('should not allow additional properties in patternProperties value', () => {
const schema = {
title: 'Test input schema',
type: 'object',
schemaVersion: 1,
properties: {
myField: {
title: 'Field title',
type: 'object',
description: 'My test field',
editor: 'json',
patternProperties: {
'.*': {
type: 'string',
pattern: '^[0-9]+$',
extraProperty: 'not allowed',
},
},
},
},
};
expect(() => validateInputSchema(validator, schema)).toThrow(
'Input schema is not valid (Property schema.properties.myField.patternProperties..*.extraProperty is not allowed.)',
);
});

it('should not allow patternProperties for other than object type', () => {
const types = {
string: 'textfield',
integer: 'number',
number: 'number',
boolean: 'checkbox',
array: 'json',
};
Object.entries(types).forEach(([type, editor]) => {
const schema = {
title: 'Test input schema',
type: 'object',
schemaVersion: 1,
properties: {
myField: {
title: 'Field title',
type,
description: 'My test field',
editor,
patternProperties: {
'^[a-zA-Z_][a-zA-Z0-9_]*$': {
type: 'string',
regex: '^[0-9]+$',
},
},
},
},
};
expect(() => validateInputSchema(validator, schema)).toThrow(
`Input schema is not valid (Property schema.properties.myField.patternProperties is not allowed.)`,
);
});
});
});

describe('custom error messages', () => {
Expand Down
Loading