import AJV from 'ajv'; import ajvErrors from 'ajv-errors'; import { formatExtensions, frontmatterFormats, extensionFormatters, } from "Formats/formats"; import { IDENTIFIER_FIELDS } from "Constants/fieldInference"; /** * Config for fields in both file and folder collections. */ const fieldsConfig = { type: "array", minItems: 1, items: { // ------- Each field: ------- type: "object", properties: { name: { type: "string" }, label: { type: "string" }, widget: { type: "string" }, required: { type: "boolean" }, }, required: ["name"], }, }; /** * The schema had to be wrapped in a function to * fix a circular dependency problem for WebPack, * where the imports get resolved asyncronously. */ const getConfigSchema = () => ({ type: "object", properties: { backend: { type: "object", properties: { name: { type: "string", examples: ["test-repo"] } }, required: ["name"], }, display_url: { type: "string", examples: ["https://example.com"] }, media_folder: { type: "string", examples: ["assets/uploads"] }, public_folder: { type: "string", examples: ["/uploads"] }, publish_mode: { type: "string", enum: ["editorial_workflow"], examples: ["editorial_workflow"], }, slug: { type: "object", properties: { encoding: { type: "string", enum: ["unicode", "ascii"] }, clean_accents: { type: "boolean" }, }, }, collections: { type: "array", minItems: 1, items: { // ------- Each collection: ------- type: "object", properties: { name: { type: "string" }, label: { type: "string" }, label_singular: { type: "string" }, description: { type: "string" }, folder: { type: "string" }, files: { type: "array", items: { // ------- Each file: ------- type: "object", properties: { name: { type: "string" }, label: { type: "string" }, label_singular: { type: "string" }, description: { type: "string" }, file: { type: "string" }, fields: fieldsConfig, }, required: ["name", "label", "file", "fields"], }, }, slug: { type: "string" }, create: { type: "boolean" }, editor: { type: "object", properties: { preview: { type: "boolean" }, }, }, format: { type: "string", enum: Object.keys(formatExtensions) }, extension: { type: "string" }, frontmatter_delimiter: { type: "string" }, fields: fieldsConfig, }, required: ["name", "label"], oneOf: [{ required: ["files"] }, { required: ["folder", "fields"] }], if: { required: ["extension"] }, then: { // Cannot infer format from extension. if: { properties: { extension: { enum: Object.keys(extensionFormatters) }, }, }, else: { required: ["format"] }, }, dependencies: { frontmatter_delimiter: { properties: { format: { enum: frontmatterFormats }, }, required: ["format"], }, folder: { errorMessage: { _: 'must have a field that is a valid entry identifier', }, properties: { fields: { contains: { properties: { name: { enum: IDENTIFIER_FIELDS }, }, }, }, }, }, }, }, }, }, required: ["backend", "media_folder", "collections"], }); class ConfigError extends Error { constructor(errors, ...args) { const message = errors .map(({ message, dataPath }) => { const dotPath = dataPath .slice(1) .split("/") .map(seg => (seg.match(/^\d+$/) ? `[${seg}]` : `.${seg}`)) .join("") .slice(1); return `${dotPath ? `'${dotPath}'` : "config"} ${message}`; }) .join("\n"); super(message, ...args); this.errors = errors; this.message = message; } toString() { return this.message; } } /** * `validateConfig` is a pure function. It does not mutate * the config that is passed in. */ export function validateConfig(config) { const ajv = new AJV({ allErrors: true, jsonPointers: true }); ajvErrors(ajv); const valid = ajv.validate(getConfigSchema(), config); if (!valid) { console.error('Config Errors', ajv.errors); throw new ConfigError(ajv.errors); } }