Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ pids
yarn.lock
packages/*/package-lock.json
.npmrc

.vscode
15,312 changes: 15,124 additions & 188 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@types/underscore": "^1.11.2",
"@typescript-eslint/eslint-plugin": "^4.22.1",
"@typescript-eslint/parser": "^4.22.1",
"ajv": "^8.6.2",
"clone-deep": "^4.0.1",
"commitlint": "^13.0.0",
"deploy-web-to-s3": "^1.3.0",
Expand Down
4 changes: 3 additions & 1 deletion packages/input_schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
},
"dependencies": {
"@apify/consts": "^1.2.0",
"ajv": "^6.12.6",
"countries-list": "^2.6.1",
"escaya": "^0.0.61"
},
"peerDependencies": {
"ajv": "^8.0.0"
}
}
20 changes: 10 additions & 10 deletions packages/input_schema/src/input_schema.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { AdditionalPropertiesParams, Ajv, ErrorObject, RequiredParams } from 'ajv';
import Ajv, { ErrorObject } from 'ajv';
import schema from './schema.json';
import { m } from './intl';

export { schema as inputSchema };
const { definitions } = schema;

/**
* This function parses AJV error and transformes it into a redable string.
* This function parses AJV error and transforms it into a readable string.
*
* @param error An error as returned from AJV.
* @param rootName Usually 'input' or 'schema' based on if we are passing the input or schema.
Expand All @@ -21,30 +21,30 @@ export function parseAjvError(
input: Record<string, unknown> = {},
): { fieldKey: string; message: string } | null {
// There are 3 possible errors comming from validation:
// - either { keword: 'anything', dataPath: '.someField', message: 'error message that we can use' }
// - or { keyword: 'additionalProperties', params: { additionalProperty: 'field' }, message: 'should NOT have additional properties' }
// - or { keyword: 'required', dataPath: '', params.missingProperty: 'someField' }
// - either { keword: 'anything', instancePath: '/someField', message: 'error message that we can use' }
// - or { keyword: 'additionalProperties', params: { additionalProperty: 'field' }, message: 'must NOT have additional properties' }
// - or { keyword: 'required', instancePath: '', params.missingProperty: 'someField' }

let fieldKey: string;
let message: string;

// If error is with keyword type, it means that type of input is incorrect
// this can mean that provided value is null
if (error.keyword === 'type') {
fieldKey = error.dataPath.split('.').pop()!;
fieldKey = error.instancePath.split('/').pop()!;
// Check if value is null and field is nullable, if yes, then skip this error
if (properties[fieldKey] && properties[fieldKey].nullable && input[fieldKey] === null) {
return null;
}
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message });
} else if (error.keyword === 'required') {
fieldKey = (error.params as RequiredParams).missingProperty;
fieldKey = error.params.missingProperty;
message = m('inputSchema.validation.required', { rootName, fieldKey });
} else if (error.keyword === 'additionalProperties') {
fieldKey = (error.params as AdditionalPropertiesParams).additionalProperty;
fieldKey = error.params.additionalProperty;
message = m('inputSchema.validation.additionalProperty', { rootName, fieldKey });
} else {
fieldKey = error.dataPath.split('.').pop()!;
fieldKey = error.instancePath.split('/').pop()!;
message = m('inputSchema.validation.generic', { rootName, fieldKey, message: error.message });
}

Expand Down Expand Up @@ -102,7 +102,7 @@ const validateField = <T extends Record<string, any>> (validator: Ajv, fieldSche
if (fieldSchema.enum) {
const definition = matchingDefinitions.filter((item) => !!item.properties.enum).pop();
if (!definition) throw new Error('Input schema validation failed to find "enum property" definition');
validateAgainstSchemaOrThrow(validator, fieldSchema, definition, `schema.properties.${fieldKey}`);
validateAgainstSchemaOrThrow(validator, fieldSchema, definition, `schema.properties.${fieldKey}.enum`);
return;
}
// Otherwise we use the other definition.
Expand Down
8 changes: 4 additions & 4 deletions packages/input_schema/src/intl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ const intlStrings = {
'inputSchema.validation.requestListSourcesInvalid':
'Items in {rootName}.{fieldKey} at positions [{invalidIndexes}] do not contain valid URLs',
'inputSchema.validation.arrayKeysInvalid':
'Keys in {rootName}.{fieldKey} at positions [{invalidIndexes}] should match regular expression "{pattern}"',
'Keys in {rootName}.{fieldKey} at positions [{invalidIndexes}] must match regular expression "{pattern}"',
'inputSchema.validation.arrayValuesInvalid':
'Values in {rootName}.{fieldKey} at positions [{invalidIndexes}] should match regular expression "{pattern}"',
'Values in {rootName}.{fieldKey} at positions [{invalidIndexes}] must match regular expression "{pattern}"',
'inputSchema.validation.objectKeysInvalid':
'Keys [{invalidKeys}] in {rootName}.{fieldKey} should match regular expression "{pattern}',
'Keys [{invalidKeys}] in {rootName}.{fieldKey} must match regular expression "{pattern}',
'inputSchema.validation.objectValuesInvalid':
'Keys [{invalidKeys}] in {rootName}.{fieldKey} should have string value which matches regular expression "{pattern}"',
'Keys [{invalidKeys}] in {rootName}.{fieldKey} must have string value which matches regular expression "{pattern}"',
'inputSchema.validation.additionalProperty':
'Property {rootName}.{fieldKey} is not allowed.',
'inputSchema.validation.proxyGroupsNotAvailable':
Expand Down
10 changes: 5 additions & 5 deletions test/input_schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Ajv from 'ajv';
import { validateInputSchema } from '@apify/input_schema';

describe('input_schema.json', () => {
const validator = new Ajv();
const validator = new Ajv({ strict: false });

describe('type any', () => {
it('should not throw on valid schema', () => {
Expand Down Expand Up @@ -58,7 +58,7 @@ describe('input_schema.json', () => {
};

expect(() => validateInputSchema(validator, schema2)).toThrow(
'Input schema is not valid (Field schema.schemaVersion should be <= 1)',
'Input schema is not valid (Field schema.schemaVersion must be <= 1)',
);
});

Expand All @@ -78,7 +78,7 @@ describe('input_schema.json', () => {
};

expect(() => validateInputSchema(validator, schema)).toThrow(
'Input schema is not valid (Field schema.properties.myField.editor should be equal to one of the allowed values)',
'Input schema is not valid (Field schema.properties.myField.editor must be equal to one of the allowed values)',
);
});

Expand Down Expand Up @@ -140,7 +140,7 @@ describe('input_schema.json', () => {
};

expect(() => validateInputSchema(validator, schema)).toThrow(
'Input schema is not valid (Field schema.properties.myField.editor should be equal to one of the allowed values)',
'Input schema is not valid (Field schema.properties.myField.editor must be equal to one of the allowed values)',
);
});

Expand All @@ -160,7 +160,7 @@ describe('input_schema.json', () => {
};

expect(() => validateInputSchema(validator, schema)).toThrow(
'Input schema is not valid (Field schema.properties.myField.enum[0] should be string)',
'Input schema is not valid (Field schema.properties.myField.enum.0 must be string)',
);
});
});
Expand Down
86 changes: 83 additions & 3 deletions test/input_schema_definition.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
import Ajv from 'ajv';
import { inputSchema } from '@apify/input_schema';

/**
* Temporarily replace console.warn with implementation that throws error instead
* of logging the warning.
*
* @returns {() => void} Function that will turn this behavior off.
*/
const setThrowErrorOnConsoleWarn = (): () => void => {
const consoleWarn = console.warn;
console.warn = () => {
throw new Error('Console.warn has been called!');
};
const turnOff = () => {
console.warn = consoleWarn;
};
return turnOff;
};

describe('input_schema.json', () => {
const ajv = new Ajv();
const ajv = new Ajv({ strict: false });

describe('type any', () => {
it('should allow to compile only a valid type any', () => {
Expand Down Expand Up @@ -122,10 +139,73 @@ describe('input_schema.json', () => {
};

test({ foo: 'bar' });
expect(() => test([1, 2, 'foo'])).toThrow('data.myField should be object,string,boolean');
expect(() => test([1, 2, 'foo'])).toThrow('data/myField must be object,string,boolean');
test('something');
expect(() => test(324567)).toThrow('data.myField should be object,string,boolean');
expect(() => test(324567)).toThrow('data/myField must be object,string,boolean');
test(true);
});

it('should not generate console warnings for schema containing `id` (1)', () => {
const turnOffConsoleWarnErrors = setThrowErrorOnConsoleWarn();

expect(() => console.warn('OK')).toThrow('Console.warn has been called!');

const schema = {
$id: 'http://mydomain/schemas/node.json',
type: 'object',
properties: {
id: {
description: 'The unique identifier for a node',
type: 'string',
},
},
required: ['id'],
example: {
id: 'test',
},
};

const test = (data: unknown) => {
if (!ajv.validate(schema, data)) throw new Error(ajv.errorsText());
};

test({ id: 'test' });

turnOffConsoleWarnErrors();
});

it('should not generate console warnings for schema containing `id` (2)', () => {
const turnOffConsoleWarnErrors = setThrowErrorOnConsoleWarn();

expect(() => console.warn('OK')).toThrow('Console.warn has been called!');

const schema = {
title: 'Status dashboard',
type: 'object',
schemaVersion: 1,
properties: {
test: {
title: 'Test',
description: '',
type: 'object',
editor: 'json',
prefill: {
id: 'KubaTestPrefill',
apiKey: 'aaa',
workspace: 'aaa',
title: 'aaaa',
},
},
},
};

const test = (data: unknown) => {
if (!ajv.validate(schema, data)) throw new Error(ajv.errorsText());
};

test({ test: { id: 1 } });

turnOffConsoleWarnErrors();
});
});
});
2 changes: 1 addition & 1 deletion test/utilities.client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ describe('utilities.client', () => {
properties: {},
required: ['field'],
};
const ajv = new Ajv();
const ajv = new Ajv({ strict: false });
const buildInputSchema = (properties: any) => {
const inputSchema = { ...baseInputSchema, properties };
const validator = ajv.compile(inputSchema);
Expand Down