399 lines
11 KiB
JavaScript
Raw Normal View History

import AJV from 'ajv';
2020-09-20 10:30:46 -07:00
import {
select,
uniqueItemProperties,
instanceof as instanceOf,
prohibited,
} from 'ajv-keywords/dist/keywords';
import ajvErrors from 'ajv-errors';
import { formatExtensions, frontmatterFormats, extensionFormatters } from '../formats/formats';
import { getWidgets } from '../lib/registry';
import uuid from 'uuid/v4';
2020-09-20 10:30:46 -07:00
import { I18N_STRUCTURE, I18N_FIELD } from '../lib/i18n';
const localeType = { type: 'string', minLength: 2, maxLength: 10, pattern: '^[a-zA-Z-_]+$' };
const i18n = {
type: 'object',
properties: {
structure: { type: 'string', enum: Object.values(I18N_STRUCTURE) },
locales: {
type: 'array',
minItems: 2,
items: localeType,
uniqueItems: true,
},
default_locale: localeType,
},
};
const i18nRoot = {
...i18n,
required: ['structure', 'locales'],
};
const i18nCollection = {
oneOf: [{ type: 'boolean' }, i18n],
};
const i18nField = {
oneOf: [{ type: 'boolean' }, { type: 'string', enum: Object.values(I18N_FIELD) }],
};
/**
* Config for fields in both file and folder collections.
*/
function fieldsConfig() {
const id = uuid();
return {
$id: `fields_${id}`,
type: 'array',
minItems: 1,
items: {
// ------- Each field: -------
$id: `field_${id}`,
type: 'object',
properties: {
name: { type: 'string' },
label: { type: 'string' },
widget: { type: 'string' },
required: { type: 'boolean' },
i18n: i18nField,
hint: { type: 'string' },
pattern: {
type: 'array',
minItems: 2,
items: [{ oneOf: [{ type: 'string' }, { instanceof: 'RegExp' }] }, { type: 'string' }],
},
field: { $ref: `field_${id}` },
fields: { $ref: `fields_${id}` },
types: { $ref: `fields_${id}` },
},
select: { $data: '0/widget' },
selectCases: {
...getWidgetSchemas(),
},
required: ['name'],
},
uniqueItemProperties: ['name'],
};
}
const viewFilters = {
type: 'array',
minItems: 1,
items: {
type: 'object',
properties: {
label: { type: 'string' },
field: { type: 'string' },
pattern: {
oneOf: [
{ type: 'boolean' },
{
type: 'string',
},
],
},
},
additionalProperties: false,
required: ['label', 'field', 'pattern'],
},
};
const viewGroups = {
type: 'array',
minItems: 1,
items: {
type: 'object',
properties: {
label: { type: 'string' },
field: { type: 'string' },
pattern: { type: 'string' },
},
additionalProperties: false,
required: ['label', 'field'],
},
};
/**
* The schema had to be wrapped in a function to
* fix a circular dependency problem for WebPack,
* where the imports get resolved asynchronously.
*/
function getConfigSchema() {
return {
type: 'object',
properties: {
backend: {
type: 'object',
properties: {
name: { type: 'string', examples: ['test-repo'] },
auth_scope: {
type: 'string',
examples: ['repo', 'public_repo'],
enum: ['repo', 'public_repo'],
},
cms_label_prefix: { type: 'string', minLength: 1 },
open_authoring: { type: 'boolean', examples: [true] },
},
required: ['name'],
},
local_backend: {
oneOf: [
{ type: 'boolean' },
{
type: 'object',
properties: {
url: { type: 'string', examples: ['http://localhost:8081/api/v1'] },
allowed_hosts: {
type: 'array',
items: { type: 'string' },
},
},
additionalProperties: false,
},
],
},
locale: { type: 'string', examples: ['en', 'fr', 'de'] },
i18n: i18nRoot,
site_url: { type: 'string', examples: ['https://example.com'] },
display_url: { type: 'string', examples: ['https://example.com'] },
logo_url: { type: 'string', examples: ['https://example.com/images/logo.svg'] },
show_preview_links: { type: 'boolean' },
media_folder: { type: 'string', examples: ['assets/uploads'] },
public_folder: { type: 'string', examples: ['/uploads'] },
media_folder_relative: { type: 'boolean' },
media_library: {
type: 'object',
properties: {
name: { type: 'string', examples: ['uploadcare'] },
config: { type: 'object' },
},
required: ['name'],
},
publish_mode: {
type: 'string',
enum: ['simple', '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' },
preview_path: { type: 'string' },
preview_path_date_field: { type: 'string' },
fields: fieldsConfig(),
},
required: ['name', 'label', 'file', 'fields'],
},
uniqueItemProperties: ['name'],
},
identifier_field: { type: 'string' },
summary: { type: 'string' },
slug: { type: 'string' },
path: { type: 'string' },
preview_path: { type: 'string' },
preview_path_date_field: { type: 'string' },
create: { type: 'boolean' },
publish: { type: 'boolean' },
hide: { type: 'boolean' },
editor: {
type: 'object',
properties: {
preview: { type: 'boolean' },
},
},
format: { type: 'string', enum: Object.keys(formatExtensions) },
extension: { type: 'string' },
frontmatter_delimiter: {
type: ['string', 'array'],
minItems: 2,
maxItems: 2,
items: {
type: 'string',
},
},
fields: fieldsConfig(),
sortable_fields: {
type: 'array',
items: {
type: 'string',
},
},
sortableFields: {
type: 'array',
items: {
type: 'string',
},
Feat: entry sorting (#3494) * refactor: typescript search actions, add tests avoid duplicate search * refactor: switch from promise chain to async/await in loadEntries * feat: add sorting, initial commit * fix: set isFetching to true on entries request * fix: ui improvments and bug fixes * test: fix tests * feat(backend-gitlab): cache local tree) * fix: fix prop type warning * refactor: code cleanup * feat(backend-bitbucket): add local tree caching support * feat: swtich to orderBy and support multiple sort keys * fix: backoff function * fix: improve backoff * feat: infer sortable fields * feat: fetch file commit metadata - initial commit * feat: extract file author and date, finalize GitLab & Bitbucket * refactor: code cleanup * feat: handle github rate limit errors * refactor: code cleanup * fix(github): add missing author and date when traversing cursor * fix: add missing author and date when traversing cursor * refactor: code cleanup * refactor: code cleanup * refactor: code cleanup * test: fix tests * fix: rebuild local tree when head doesn't exist in remote branch * fix: allow sortable fields to be an empty array * fix: allow translation of built in sort fields * build: fix proxy server build * fix: hide commit author and date fields by default on non git backends * fix(algolia): add listAllEntries method for alogolia integration * fix: handle sort fields overflow * test(bitbucket): re-record some bitbucket e2e tests * test(bitbucket): fix media library test * refactor(gitgateway-gitlab): share request code and handle 404 errors * fix: always show commit date by default * docs: add sortableFields * refactor: code cleanup * improvement: drop multi-sort, rework sort UI * chore: force main package bumps Co-authored-by: Shawn Erquhart <shawn@erquh.art>
2020-04-01 06:13:27 +03:00
},
view_filters: viewFilters,
view_groups: viewGroups,
nested: {
type: 'object',
properties: {
depth: { type: 'number', minimum: 1, maximum: 1000 },
summary: { type: 'string' },
},
required: ['depth'],
2020-06-18 10:11:37 +03:00
},
meta: {
type: 'object',
properties: {
path: {
type: 'object',
properties: {
label: { type: 'string' },
widget: { type: 'string' },
index_file: { type: 'string' },
},
required: ['label', 'widget', 'index_file'],
2020-06-18 10:11:37 +03:00
},
},
additionalProperties: false,
minProperties: 1,
2020-06-18 10:11:37 +03:00
},
i18n: i18nCollection,
2020-06-18 10:11:37 +03:00
},
required: ['name', 'label'],
oneOf: [{ required: ['files'] }, { required: ['folder', 'fields'] }],
not: {
required: ['sortable_fields', 'sortableFields'],
},
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'],
},
},
},
uniqueItemProperties: ['name'],
},
editor: {
type: 'object',
properties: {
preview: { type: 'boolean' },
},
},
},
required: ['backend', 'collections'],
anyOf: [{ required: ['media_folder'] }, { required: ['media_library'] }],
};
}
function getWidgetSchemas() {
const schemas = getWidgets().map(widget => ({ [widget.name]: widget.schema }));
return Object.assign(...schemas);
}
class ConfigError extends Error {
constructor(errors, ...args) {
const message = errors
.map(({ message, instancePath }) => {
const dotPath = instancePath
.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, $data: true, strict: false });
uniqueItemProperties(ajv);
select(ajv);
instanceOf(ajv);
2020-09-20 10:30:46 -07:00
prohibited(ajv);
ajvErrors(ajv);
const valid = ajv.validate(getConfigSchema(), config);
if (!valid) {
const errors = ajv.errors.map(e => {
switch (e.keyword) {
// TODO: remove after https://github.com/ajv-validator/ajv-keywords/pull/123 is merged
case 'uniqueItemProperties': {
const path = e.instancePath || '';
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;
}
case 'instanceof': {
const path = e.instancePath || '';
let newError = e;
if (/fields\/\d+\/pattern\/\d+/.test(path)) {
newError = {
...e,
message: 'must be a regular expression',
};
}
return newError;
}
default:
return e;
}
});
console.error('Config Errors', errors);
throw new ConfigError(errors);
}
}