176 lines
4.7 KiB
JavaScript
176 lines
4.7 KiB
JavaScript
|
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);
|
||
|
}
|
||
|
}
|