diff --git a/packages/netlify-cms-core/package.json b/packages/netlify-cms-core/package.json index bd4ad35d..a36cf225 100644 --- a/packages/netlify-cms-core/package.json +++ b/packages/netlify-cms-core/package.json @@ -27,6 +27,7 @@ "@iarna/toml": "2.2.5", "ajv": "^6.10.0", "ajv-errors": "^1.0.1", + "ajv-keywords": "^3.4.1", "copy-text-to-clipboard": "^2.0.0", "diacritics": "^1.3.0", "fuzzy": "^0.1.1", 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 6dc31805..bc249750 100644 --- a/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js +++ b/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js @@ -182,5 +182,44 @@ describe('config', () => { validateConfig(merge({}, validConfig, { collections: [{ sortableFields: [] }] })); }).not.toThrow(); }); + + it('should throw if collection fields names are not unique', () => { + expect(() => { + validateConfig( + merge(validConfig, { + collections: [ + { + fields: [ + { name: 'title', label: 'title', widget: 'string' }, + { name: 'title', label: 'other title', widget: 'string' }, + ], + }, + ], + }), + ); + }).toThrowError("'collections[0].fields' fields names must be unique"); + }); + + it('should not throw if collection fields are unique across nesting levels', () => { + expect(() => { + validateConfig( + merge(validConfig, { + collections: [ + { + fields: [ + { name: 'title', label: 'title', widget: 'string' }, + { + name: 'object', + label: 'Object', + widget: 'object', + fields: [{ name: 'title', label: 'title', widget: 'string' }], + }, + ], + }, + ], + }), + ); + }).not.toThrow(); + }); }); }); diff --git a/packages/netlify-cms-core/src/constants/configSchema.js b/packages/netlify-cms-core/src/constants/configSchema.js index caf226d0..17392cf4 100644 --- a/packages/netlify-cms-core/src/constants/configSchema.js +++ b/packages/netlify-cms-core/src/constants/configSchema.js @@ -1,4 +1,5 @@ import AJV from 'ajv'; +import uniqueItemProperties from 'ajv-keywords/keywords/uniqueItemProperties'; import ajvErrors from 'ajv-errors'; import { formatExtensions, frontmatterFormats, extensionFormatters } from 'Formats/formats'; @@ -19,6 +20,7 @@ const fieldsConfig = { }, required: ['name'], }, + uniqueItemProperties: ['name'], }; const viewFilters = { @@ -223,11 +225,20 @@ class ConfigError extends Error { */ export function validateConfig(config) { const ajv = new AJV({ allErrors: true, jsonPointers: true }); + uniqueItemProperties(ajv); ajvErrors(ajv); const valid = ajv.validate(getConfigSchema(), config); if (!valid) { - console.error('Config Errors', ajv.errors); - throw new ConfigError(ajv.errors); + 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 newError = { ...e, message: 'fields names must be unique' }; + return newError; + } + return e; + }); + console.error('Config Errors', errors); + throw new ConfigError(errors); } }