diff --git a/src/actions/__tests__/config.spec.js b/src/actions/__tests__/config.spec.js index f32f2d44..c17db99f 100644 --- a/src/actions/__tests__/config.spec.js +++ b/src/actions/__tests__/config.spec.js @@ -58,7 +58,17 @@ describe('config', () => { describe('validateConfig', () => { it('should return the config if no errors', () => { - const config = fromJS({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', collections: [{}] }); + const collections = [{ + name: 'posts', + folder: '_posts', + fields: [{ name: 'title', label: 'title' }], + }]; + const config = fromJS({ + foo: 'bar', + backend: { name: 'bar' }, + media_folder: 'baz', + collections, + }); expect( validateConfig(config) ).toEqual(config); diff --git a/src/actions/config.js b/src/actions/config.js index c290b4fe..f563fa22 100644 --- a/src/actions/config.js +++ b/src/actions/config.js @@ -2,6 +2,9 @@ import yaml from "js-yaml"; import { Map, List, fromJS } from "immutable"; import { trimStart, flow, isBoolean, get } from "lodash"; import { authenticateUser } from "Actions/auth"; +import { formatByExtension, supportedFormats, frontmatterFormats } from "Formats/formats"; +import { selectIdentifier } from "Reducers/collections"; +import { IDENTIFIER_FIELDS } from "Constants/fieldInference"; import * as publishModes from "Constants/publishModes"; export const CONFIG_REQUEST = "CONFIG_REQUEST"; @@ -40,6 +43,38 @@ export function applyDefaults(config) { }); } +function validateCollection(collection) { + const { + name, + folder, + files, + format, + extension, + frontmatter_delimiter: delimiter, + fields, + } = collection.toJS(); + + if (!folder && !files) { + throw new Error(`Unknown collection type for collection "${name}". Collections can be either Folder based or File based.`); + } + if (format && !supportedFormats.includes(format)) { + throw new Error(`Unknown collection format for collection "${name}". Supported formats are ${supportedFormats.join(',')}`); + } + if (!format && extension && !formatByExtension(extension)) { + // Cannot infer format from extension. + throw new Error(`Please set a format for collection "${name}". Supported formats are ${supportedFormats.join(',')}`); + } + if (delimiter && !frontmatterFormats.includes(format)) { + // Cannot set custom delimiter without explicit and proper frontmatter format declaration + throw new Error(`Please set a proper frontmatter format for collection "${name}" to use a custom delimiter. Supported frontmatter formats are yaml-frontmatter, toml-frontmatter, and json-frontmatter.`); + } + if (folder && !selectIdentifier(collection)) { + // Verify that folder-type collections have an identifier field for slug creation. + throw new Error(`Collection "${name}" must have a field that is a valid entry identifier. Supported fields are ${IDENTIFIER_FIELDS.join(', ')}.`); + } +} + + export function validateConfig(config) { if (!config.get('backend')) { throw new Error("Error in configuration file: A `backend` wasn't found. Check your config.yml file."); @@ -70,6 +105,12 @@ export function validateConfig(config) { if (!List.isList(collections) || collections.isEmpty() || !collections.first()) { throw new Error("Error in configuration file: Your `collections` must be an array with at least one element. Check your config.yml file."); } + + /** + * Validate Collections + */ + config.get('collections').forEach(validateCollection); + return config; } diff --git a/src/backends/backend.js b/src/backends/backend.js index 33a3fc28..cf1962bc 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -8,7 +8,8 @@ import { selectEntryPath, selectAllowNewEntries, selectAllowDeletion, - selectFolderEntryExtension + selectFolderEntryExtension, + selectIdentifier, } from "Reducers/collections"; import { createEntry } from "ValueObjects/Entry"; import { sanitizeSlug } from "Lib/urlHelper"; @@ -42,23 +43,14 @@ class LocalStorageAuthStore { } } -const slugFormatter = (template = "{{slug}}", entryData, slugConfig) => { +const slugFormatter = (collection, entryData, slugConfig) => { + const template = collection.get('slug') || "{{slug}}"; const date = new Date(); - const getIdentifier = (entryData) => { - const validIdentifierFields = ["title", "path"]; - const identifiers = validIdentifierFields.map((field) => - entryData.find((_, key) => key.toLowerCase().trim() === field) - ); - - const identifier = identifiers.find(ident => ident !== undefined); - - if (identifier === undefined) { - throw new Error("Collection must have a field name that is a valid entry identifier"); - } - - return identifier; - }; + const identifier = entryData.get(selectIdentifier(collection)); + if (!identifier) { + throw new Error("Collection must have a field name that is a valid entry identifier"); + } const slug = template.replace(/\{\{([^\}]+)\}\}/g, (_, field) => { switch (field) { @@ -75,7 +67,7 @@ const slugFormatter = (template = "{{slug}}", entryData, slugConfig) => { case "second": return (`0${ date.getSeconds() }`).slice(-2); case "slug": - return getIdentifier(entryData).trim(); + return identifier.trim(); default: return entryData.get(field, "").trim(); } @@ -275,7 +267,7 @@ class Backend { if (!selectAllowNewEntries(collection)) { throw (new Error("Not allowed to create new entries in this collection")); } - const slug = slugFormatter(collection.get("slug"), entryDraft.getIn(["entry", "data"]), config.get("slug")); + const slug = slugFormatter(collection, entryDraft.getIn(["entry", "data"]), config.get("slug")); const path = selectEntryPath(collection, slug); entryObj = { path, diff --git a/src/constants/fieldInference.js b/src/constants/fieldInference.js index cb13b0ab..0811834f 100644 --- a/src/constants/fieldInference.js +++ b/src/constants/fieldInference.js @@ -1,6 +1,8 @@ import React from 'react'; /* eslint-disable */ +export const IDENTIFIER_FIELDS = ['title', 'path']; + export const INFERABLE_FIELDS = { title: { type: 'string', diff --git a/src/reducers/collections.js b/src/reducers/collections.js index 3a1abaf1..d5c7288b 100644 --- a/src/reducers/collections.js +++ b/src/reducers/collections.js @@ -3,14 +3,13 @@ import { has, get, escapeRegExp } from 'lodash'; import consoleError from 'Lib/consoleError'; import { CONFIG_SUCCESS } from 'Actions/config'; import { FILES, FOLDER } from 'Constants/collectionTypes'; -import { INFERABLE_FIELDS } from 'Constants/fieldInference'; -import { formatByExtension, formatToExtension, supportedFormats, frontmatterFormats } from 'Formats/formats'; +import { INFERABLE_FIELDS, IDENTIFIER_FIELDS } from 'Constants/fieldInference'; +import { formatToExtension } from 'Formats/formats'; const collections = (state = null, action) => { switch (action.type) { case CONFIG_SUCCESS: const configCollections = action.payload ? action.payload.get('collections') : List(); - configCollections.forEach(validateCollection) return configCollections .toOrderedMap() .map(collection => { @@ -27,32 +26,6 @@ const collections = (state = null, action) => { } }; -function validateCollection(configCollection) { - const { - name, - folder, - files, - format, - extension, - frontmatter_delimiter: delimiter - } = configCollection.toJS(); - - if (!folder && !files) { - throw new Error(`Unknown collection type for collection "${name}". Collections can be either Folder based or File based.`); - } - if (format && !supportedFormats.includes(format)) { - throw new Error(`Unknown collection format for collection "${name}". Supported formats are ${supportedFormats.join(',')}`); - } - if (!format && extension && !formatByExtension(extension)) { - // Cannot infer format from extension. - throw new Error(`Please set a format for collection "${name}". Supported formats are ${supportedFormats.join(',')}`); - } - if (delimiter && !frontmatterFormats.includes(format)) { - // Cannot set custom delimiter without explicit and proper frontmatter format declaration - throw new Error(`Please set a proper frontmatter format for collection "${name}" to use a custom delimiter. Supported frontmatter formats are yaml-frontmatter, toml-frontmatter, and json-frontmatter.`); - } -} - const selectors = { [FOLDER]: { entryExtension(collection) { @@ -120,6 +93,10 @@ export const selectListMethod = collection => selectors[collection.get('type')]. export const selectAllowNewEntries = collection => selectors[collection.get('type')].allowNewEntries(collection); export const selectAllowDeletion = collection => selectors[collection.get('type')].allowDeletion(collection); export const selectTemplateName = (collection, slug) => selectors[collection.get('type')].templateName(collection, slug); +export const selectIdentifier = collection => { + const fieldNames = collection.get('fields').map(field => field.get('name')); + return IDENTIFIER_FIELDS.find(id => fieldNames.find(name => name.toLowerCase().trim() === id)); +}; export const selectInferedField = (collection, fieldName) => { const inferableField = INFERABLE_FIELDS[fieldName]; const fields = collection.get('fields');