diff --git a/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js b/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js index 20b7ebe7..ee2a4fb6 100644 --- a/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js +++ b/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js @@ -173,13 +173,13 @@ describe('config', () => { it('should throw if collection publish is not a boolean', () => { expect(() => { - validateConfig(merge(validConfig, { collections: [{ publish: 'false' }] })); + validateConfig(merge({}, validConfig, { collections: [{ publish: 'false' }] })); }).toThrowError("'collections[0].publish' should be boolean"); }); it('should not throw if collection publish is a boolean', () => { expect(() => { - validateConfig(merge(validConfig, { collections: [{ publish: false }] })); + validateConfig(merge({}, validConfig, { collections: [{ publish: false }] })); }).not.toThrowError(); }); @@ -201,10 +201,48 @@ describe('config', () => { }).not.toThrow(); }); + it('should throw if collection names are not unique', () => { + expect(() => { + validateConfig( + merge({}, validConfig, { + collections: [validConfig.collections[0], validConfig.collections[0]], + }), + ); + }).toThrowError("'collections' collections names must be unique"); + }); + + it('should throw if collection file names are not unique', () => { + expect(() => { + validateConfig( + merge({}, validConfig, { + collections: [ + {}, + { + files: [ + { + name: 'a', + label: 'a', + file: 'a.md', + fields: [{ name: 'title', label: 'title', widget: 'string' }], + }, + { + name: 'a', + label: 'b', + file: 'b.md', + fields: [{ name: 'title', label: 'title', widget: 'string' }], + }, + ], + }, + ], + }), + ); + }).toThrowError("'collections[1].files' files names must be unique"); + }); + it('should throw if collection fields names are not unique', () => { expect(() => { validateConfig( - merge(validConfig, { + merge({}, validConfig, { collections: [ { fields: [ @@ -221,7 +259,7 @@ describe('config', () => { it('should not throw if collection fields are unique across nesting levels', () => { expect(() => { validateConfig( - merge(validConfig, { + merge({}, validConfig, { collections: [ { fields: [ @@ -257,7 +295,7 @@ describe('config', () => { it('should throw if nested relation displayFields and searchFields are not arrays', () => { expect(() => { validateConfig( - merge(validConfig, { + merge({}, validConfig, { collections: [ { fields: [ @@ -288,7 +326,7 @@ describe('config', () => { it('should not throw if nested relation displayFields and searchFields are arrays', () => { expect(() => { validateConfig( - merge(validConfig, { + merge({}, validConfig, { collections: [ { fields: [ @@ -358,5 +396,45 @@ describe('config', () => { ); }).not.toThrow(); }); + + it('should throw if collection field pattern is not an array', () => { + expect(() => { + validateConfig(merge({}, validConfig, { collections: [{ fields: [{ pattern: '' }] }] })); + }).toThrowError("'collections[0].fields[0].pattern' should be array"); + }); + + it('should throw if collection field pattern is not an array of [string|regex, string]', () => { + expect(() => { + validateConfig( + merge({}, validConfig, { collections: [{ fields: [{ pattern: [1, ''] }] }] }), + ); + }).toThrowError( + "'collections[0].fields[0].pattern[0]' should be string\n'collections[0].fields[0].pattern[0]' should be a regular expression", + ); + + expect(() => { + validateConfig( + merge({}, validConfig, { collections: [{ fields: [{ pattern: ['', 1] }] }] }), + ); + }).toThrowError("'collections[0].fields[0].pattern[1]' should be string"); + }); + + it('should allow collection field pattern to be an array of [string|regex, string]', () => { + expect(() => { + validateConfig( + merge({}, validConfig, { + collections: [{ fields: [{ pattern: ['pattern', 'error'] }] }], + }), + ); + }).not.toThrow(); + + expect(() => { + validateConfig( + merge({}, validConfig, { + collections: [{ fields: [{ pattern: [/pattern/, 'error'] }] }], + }), + ); + }).not.toThrow(); + }); }); }); diff --git a/packages/netlify-cms-core/src/constants/configSchema.js b/packages/netlify-cms-core/src/constants/configSchema.js index ee714d51..b1269830 100644 --- a/packages/netlify-cms-core/src/constants/configSchema.js +++ b/packages/netlify-cms-core/src/constants/configSchema.js @@ -1,5 +1,5 @@ import AJV from 'ajv'; -import { select, uniqueItemProperties } from 'ajv-keywords/keywords'; +import { select, uniqueItemProperties, instanceof as instanceOf } from 'ajv-keywords/keywords'; import ajvErrors from 'ajv-errors'; import { formatExtensions, frontmatterFormats, extensionFormatters } from 'Formats/formats'; import { getWidgets } from 'Lib/registry'; @@ -21,7 +21,11 @@ const fieldsConfig = () => ({ widget: { type: 'string' }, required: { type: 'boolean' }, hint: { type: 'string' }, - pattern: { type: 'array', minItems: 2, items: { type: 'string' } }, + pattern: { + type: 'array', + minItems: 2, + items: [{ oneOf: [{ type: 'string' }, { instanceof: 'RegExp' }] }, { type: 'string' }], + }, field: { $ref: 'field' }, fields: { $ref: 'fields' }, types: { $ref: 'fields' }, @@ -274,25 +278,40 @@ export function validateConfig(config) { const ajv = new AJV({ allErrors: true, jsonPointers: true, $data: true }); uniqueItemProperties(ajv); select(ajv); + instanceOf(ajv); ajvErrors(ajv); const valid = ajv.validate(getConfigSchema(), config); if (!valid) { const errors = ajv.errors.map(e => { - // TODO: remove after https://github.com/ajv-validator/ajv-keywords/pull/123 is merged - if (e.keyword === 'uniqueItemProperties') { - const path = e.dataPath || ''; - let newError = e; - if (path.endsWith('/fields')) { - newError = { ...e, message: 'fields names must be unique' }; - } else if (path.endsWith('/files')) { - newError = { ...e, message: 'files names must be unique' }; - } else if (path.endsWith('/collections')) { - newError = { ...e, message: 'collections names must be unique' }; + switch (e.keyword) { + // TODO: remove after https://github.com/ajv-validator/ajv-keywords/pull/123 is merged + case 'uniqueItemProperties': { + const path = e.dataPath || ''; + let newError = e; + if (path.endsWith('/fields')) { + newError = { ...e, message: 'fields names must be unique' }; + } else if (path.endsWith('/files')) { + newError = { ...e, message: 'files names must be unique' }; + } else if (path.endsWith('/collections')) { + newError = { ...e, message: 'collections names must be unique' }; + } + return newError; } - return newError; + case 'instanceof': { + const path = e.dataPath || ''; + let newError = e; + if (/fields\/\d+\/pattern\/\d+/.test(path)) { + newError = { + ...e, + message: 'should be a regular expression', + }; + } + return newError; + } + default: + return e; } - return e; }); console.error('Config Errors', errors); throw new ConfigError(errors);