From 57122c4acb29eb82f5a3d12ea842384276bfa6ab Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Tue, 14 Jan 2020 18:04:07 +0100 Subject: [PATCH] Add kind property to content-type-builder Signed-off-by: Alexandre Bodin --- .../controllers/ContentTypes.js | 18 ++- .../controllers/validation/component.js | 22 +-- .../controllers/validation/constants.js | 10 ++ .../controllers/validation/content-type.js | 110 ++++++++------ .../controllers/validation/model-schema.js | 136 ++++++++++-------- .../validation/remove-empty-defaults.js | 30 ++++ .../services/ContentTypes.js | 3 +- .../schema-builder/content-type-builder.js | 4 + .../services/schema-builder/schema-handler.js | 1 + 9 files changed, 211 insertions(+), 123 deletions(-) create mode 100644 packages/strapi-plugin-content-type-builder/controllers/validation/remove-empty-defaults.js diff --git a/packages/strapi-plugin-content-type-builder/controllers/ContentTypes.js b/packages/strapi-plugin-content-type-builder/controllers/ContentTypes.js index adae50d9ba..225290f7a0 100644 --- a/packages/strapi-plugin-content-type-builder/controllers/ContentTypes.js +++ b/packages/strapi-plugin-content-type-builder/controllers/ContentTypes.js @@ -5,10 +5,19 @@ const _ = require('lodash'); const { validateContentTypeInput, validateUpdateContentTypeInput, + validateKind, } = require('./validation/content-type'); module.exports = { - getContentTypes(ctx) { + async getContentTypes(ctx) { + const { kind } = ctx.query; + + try { + await validateKind(kind); + } catch (error) { + return ctx.send({ error }, 400); + } + const contentTypeService = strapi.plugins['content-type-builder'].services.contenttypes; @@ -17,6 +26,13 @@ module.exports = { if (uid.startsWith('strapi::')) return false; if (uid === 'plugins::upload.file') return false; // TODO: add a flag in the content type instead + if ( + kind && + _.get(strapi.contentTypes[uid], 'kind', 'collectionType') !== kind + ) { + return false; + } + return true; }) .map(uid => diff --git a/packages/strapi-plugin-content-type-builder/controllers/validation/component.js b/packages/strapi-plugin-content-type-builder/controllers/validation/component.js index ad978c2cf9..fd62d0361b 100644 --- a/packages/strapi-plugin-content-type-builder/controllers/validation/component.js +++ b/packages/strapi-plugin-content-type-builder/controllers/validation/component.js @@ -6,6 +6,7 @@ const yup = require('yup'); const { isValidCategoryName, isValidIcon } = require('./common'); const formatYupErrors = require('./yup-formatter'); const createSchema = require('./model-schema'); +const removeEmptyDefaults = require('./remove-empty-defaults'); const { modelTypes, DEFAULT_TYPES } = require('./constants'); const VALID_RELATIONS = ['oneWay', 'manyWay']; @@ -63,26 +64,7 @@ const validateComponentInput = data => { }; const validateUpdateComponentInput = data => { - // convert zero length string on default attributes to undefined - if (_.has(data, ['component', 'attributes'])) { - Object.keys(data.component.attributes).forEach(attribute => { - if (data.component.attributes[attribute].default === '') { - data.component.attributes[attribute].default = undefined; - } - }); - } - - if (_.has(data, 'components') && Array.isArray(data.components)) { - data.components.forEach(data => { - if (_.has(data, 'attributes') && _.has(data, 'uid')) { - Object.keys(data.attributes).forEach(attribute => { - if (data.attributes[attribute].default === '') { - data.attributes[attribute].default = undefined; - } - }); - } - }); - } + removeEmptyDefaults(data); return yup .object({ diff --git a/packages/strapi-plugin-content-type-builder/controllers/validation/constants.js b/packages/strapi-plugin-content-type-builder/controllers/validation/constants.js index f96f7ad400..225058192d 100644 --- a/packages/strapi-plugin-content-type-builder/controllers/validation/constants.js +++ b/packages/strapi-plugin-content-type-builder/controllers/validation/constants.js @@ -3,6 +3,9 @@ const CONTENT_TYPE = 'CONTENT_TYPE'; const COMPONENT = 'COMPONENT'; +const SINGLE_TYPE = 'singleType'; +const COLLECTION_TYPE = 'collectionType'; + const DEFAULT_TYPES = [ // advanced types 'media', @@ -28,8 +31,15 @@ const DEFAULT_TYPES = [ const FORBIDDEN_ATTRIBUTE_NAMES = ['__component', '__contentType']; +const CONTENT_TYPE_KINDS = [SINGLE_TYPE, COLLECTION_TYPE]; + module.exports = { DEFAULT_TYPES, + CONTENT_TYPE_KINDS, + typeKinds: { + SINGLE_TYPE, + COLLECTION_TYPE, + }, modelTypes: { CONTENT_TYPE, COMPONENT, diff --git a/packages/strapi-plugin-content-type-builder/controllers/validation/content-type.js b/packages/strapi-plugin-content-type-builder/controllers/validation/content-type.js index f6046b726a..513dd823fb 100644 --- a/packages/strapi-plugin-content-type-builder/controllers/validation/content-type.js +++ b/packages/strapi-plugin-content-type-builder/controllers/validation/content-type.js @@ -2,35 +2,66 @@ const _ = require('lodash'); const yup = require('yup'); + const formatYupErrors = require('./yup-formatter'); - const createSchema = require('./model-schema'); +const removeEmptyDefaults = require('./remove-empty-defaults'); const { nestedComponentSchema } = require('./component'); -const { modelTypes, DEFAULT_TYPES } = require('./constants'); +const { + modelTypes, + DEFAULT_TYPES, + CONTENT_TYPE_KINDS, + typeKinds, +} = require('./constants'); -const VALID_RELATIONS = [ - 'oneWay', - 'manyWay', - 'oneToOne', - 'oneToMany', - 'manyToOne', - 'manyToMany', -]; +/** + * Allowed relation per type kind + */ +const VALID_RELATIONS = { + [typeKinds.SINGLE_TYPE]: ['oneWay', 'manyWay'], + [typeKinds.COLLECTION_TYPE]: [ + 'oneWay', + 'manyWay', + 'oneToOne', + 'oneToMany', + 'manyToOne', + 'manyToMany', + ], +}; + +/** + * Allowed types + */ const VALID_TYPES = [...DEFAULT_TYPES, 'component', 'dynamiczone']; -const contentTypeSchema = createSchema(VALID_TYPES, VALID_RELATIONS, { - modelType: modelTypes.CONTENT_TYPE, -}); +/** + * Returns a yup schema to validate a content type payload + * @param {Object} data payload + */ +const createContentTypeSchema = data => { + const kind = _.get(data, 'kind', typeKinds.COLLECTION_TYPE); -const createContentTypeSchema = yup - .object({ - contentType: contentTypeSchema.required().noUnknown(), - components: nestedComponentSchema, - }) - .noUnknown(); + const contentTypeSchema = createSchema( + VALID_TYPES, + VALID_RELATIONS[kind] || [], + { + modelType: modelTypes.CONTENT_TYPE, + } + ); + return yup + .object({ + contentType: contentTypeSchema.required().noUnknown(), + components: nestedComponentSchema, + }) + .noUnknown(); +}; + +/** + * Validator for content type creation + */ const validateContentTypeInput = data => { - return createContentTypeSchema + return createContentTypeSchema(data) .validate(data, { strict: true, abortEarly: false, @@ -38,29 +69,13 @@ const validateContentTypeInput = data => { .catch(error => Promise.reject(formatYupErrors(error))); }; +/** + * Validator for content type edition + */ const validateUpdateContentTypeInput = data => { - // convert zero length string on default attributes to undefined - if (_.has(data, 'attributes')) { - Object.keys(data.attributes).forEach(attribute => { - if (data.attributes[attribute].default === '') { - data.attributes[attribute].default = undefined; - } - }); - } + removeEmptyDefaults(data); - if (_.has(data, 'components') && Array.isArray(data.components)) { - data.components.forEach(data => { - if (_.has(data, 'attributes') && _.has(data, 'uid')) { - Object.keys(data.attributes).forEach(attribute => { - if (data.attributes[attribute].default === '') { - data.attributes[attribute].default = undefined; - } - }); - } - }); - } - - return createContentTypeSchema + return createContentTypeSchema(data) .validate(data, { strict: true, abortEarly: false, @@ -68,7 +83,20 @@ const validateUpdateContentTypeInput = data => { .catch(error => Promise.reject(formatYupErrors(error))); }; +/** + * Validates type kind + */ +const validateKind = kind => { + return yup + .string() + .oneOf(CONTENT_TYPE_KINDS) + .nullable() + .validate(kind) + .catch(error => Promise.reject(formatYupErrors(error))); +}; + module.exports = { validateContentTypeInput, validateUpdateContentTypeInput, + validateKind, }; diff --git a/packages/strapi-plugin-content-type-builder/controllers/validation/model-schema.js b/packages/strapi-plugin-content-type-builder/controllers/validation/model-schema.js index ce58fe233d..6e6ae9726f 100644 --- a/packages/strapi-plugin-content-type-builder/controllers/validation/model-schema.js +++ b/packages/strapi-plugin-content-type-builder/controllers/validation/model-schema.js @@ -3,72 +3,88 @@ const _ = require('lodash'); const yup = require('yup'); -const { FORBIDDEN_ATTRIBUTE_NAMES } = require('./constants'); +const { + modelTypes, + FORBIDDEN_ATTRIBUTE_NAMES, + CONTENT_TYPE_KINDS, +} = require('./constants'); const { isValidCollectionName, isValidKey } = require('./common'); const { getTypeShape } = require('./types'); const getRelationValidator = require('./relations'); -const createSchema = (types, relations, { modelType } = {}) => - yup - .object({ - name: yup - .string() - .min(1) - .required('name.required'), - description: yup.string(), - connection: yup.string(), - collectionName: yup - .string() - .nullable() - .test(isValidCollectionName), - attributes: yup.lazy(attributes => { - return yup - .object() - .shape( - _.mapValues(attributes, (attribute, key) => { - if (FORBIDDEN_ATTRIBUTE_NAMES.includes(key)) { - return yup.object().test({ - name: 'forbiddenKeys', - message: `Attribute keys cannot be one of ${FORBIDDEN_ATTRIBUTE_NAMES.join( - ', ' - )}`, - test: () => false, - }); - } - - if (_.has(attribute, 'type')) { - const shape = { - type: yup - .string() - .oneOf(types) - .required(), - configurable: yup.boolean().nullable(), - private: yup.boolean().nullable(), - ...getTypeShape(attribute, { modelType }), - }; - - return yup - .object(shape) - .test(isValidKey(key)) - .noUnknown(); - } else if (_.has(attribute, 'target')) { - const shape = getRelationValidator(attribute, relations); - - return yup - .object(shape) - .test(isValidKey(key)) - .noUnknown(); - } +const createSchema = (types, relations, { modelType } = {}) => { + const schema = yup.object({ + name: yup + .string() + .min(1) + .required('name.required'), + description: yup.string(), + connection: yup.string(), + collectionName: yup + .string() + .nullable() + .test(isValidCollectionName), + attributes: yup.lazy(attributes => { + return yup + .object() + .shape( + _.mapValues(attributes, (attribute, key) => { + if (FORBIDDEN_ATTRIBUTE_NAMES.includes(key)) { return yup.object().test({ - name: 'mustHaveTypeOrTarget', - message: 'Attribute must have either a type or a target', + name: 'forbiddenKeys', + message: `Attribute keys cannot be one of ${FORBIDDEN_ATTRIBUTE_NAMES.join( + ', ' + )}`, test: () => false, }); - }) - ) - .required('attributes.required'); - }), - }) - .noUnknown(); + } + + if (_.has(attribute, 'type')) { + const shape = { + type: yup + .string() + .oneOf(types) + .required(), + configurable: yup.boolean().nullable(), + private: yup.boolean().nullable(), + ...getTypeShape(attribute, { modelType }), + }; + + return yup + .object(shape) + .test(isValidKey(key)) + .noUnknown(); + } else if (_.has(attribute, 'target')) { + const shape = getRelationValidator(attribute, relations); + + return yup + .object(shape) + .test(isValidKey(key)) + .noUnknown(); + } + return yup.object().test({ + name: 'mustHaveTypeOrTarget', + message: 'Attribute must have either a type or a target', + test: () => false, + }); + }) + ) + .required('attributes.required'); + }), + }); + + if (modelType === modelTypes.CONTENT_TYPE) { + return schema + .shape({ + kind: yup + .string() + .oneOf(CONTENT_TYPE_KINDS) + .required('contentType.kind.required'), + }) + .noUnknown(); + } + + return schema.noUnknown(); +}; module.exports = createSchema; diff --git a/packages/strapi-plugin-content-type-builder/controllers/validation/remove-empty-defaults.js b/packages/strapi-plugin-content-type-builder/controllers/validation/remove-empty-defaults.js new file mode 100644 index 0000000000..aa197e14be --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/controllers/validation/remove-empty-defaults.js @@ -0,0 +1,30 @@ +'use strict'; + +const _ = require('lodash'); + +/** + * Convert zero length string on default attributes to undefined + */ +module.exports = data => { + if (_.has(data, 'attributes')) { + Object.keys(data.attributes).forEach(attribute => { + if (data.attributes[attribute].default === '') { + data.attributes[attribute].default = undefined; + } + }); + } + + if (_.has(data, 'components') && Array.isArray(data.components)) { + data.components.forEach(data => { + if (_.has(data, 'attributes') && _.has(data, 'uid')) { + Object.keys(data.attributes).forEach(attribute => { + if (data.attributes[attribute].default === '') { + data.attributes[attribute].default = undefined; + } + }); + } + }); + } + + return data; +}; diff --git a/packages/strapi-plugin-content-type-builder/services/ContentTypes.js b/packages/strapi-plugin-content-type-builder/services/ContentTypes.js index 7eeab3c87a..c20c173895 100644 --- a/packages/strapi-plugin-content-type-builder/services/ContentTypes.js +++ b/packages/strapi-plugin-content-type-builder/services/ContentTypes.js @@ -17,7 +17,7 @@ const { nameToSlug } = require('../utils/helpers'); * @param {Object} contentType */ const formatContentType = contentType => { - const { uid, plugin, connection, collectionName, info } = contentType; + const { uid, kind, plugin, connection, collectionName, info } = contentType; return { uid, @@ -26,6 +26,7 @@ const formatContentType = contentType => { name: _.get(info, 'name') || _.upperFirst(pluralize(uid)), description: _.get(info, 'description', ''), connection, + kind: kind || 'collectionType', collectionName, attributes: formatAttributes(contentType), }, diff --git a/packages/strapi-plugin-content-type-builder/services/schema-builder/content-type-builder.js b/packages/strapi-plugin-content-type-builder/services/schema-builder/content-type-builder.js index aa8efe08a8..d59ab2834d 100644 --- a/packages/strapi-plugin-content-type-builder/services/schema-builder/content-type-builder.js +++ b/packages/strapi-plugin-content-type-builder/services/schema-builder/content-type-builder.js @@ -80,6 +80,7 @@ module.exports = function createComponentBuilder() { contentType .setUID(uid) .set('connection', infos.connection || defaultConnection) + .set('kind', infos.kind) .set('collectionName', infos.collectionName || defaultCollectionName) .set(['info', 'name'], infos.name) .set(['info', 'description'], infos.description) @@ -197,9 +198,12 @@ module.exports = function createComponentBuilder() { } }); + // TODO: handle kind change => update routes.json file somehow + contentType .set('connection', infos.connection) .set('collectionName', infos.collectionName) + .set('kind', infos.kind) .set(['info', 'name'], infos.name) .set(['info', 'description'], infos.description) .setAttributes(this.convertAttributes(newAttributes)); diff --git a/packages/strapi-plugin-content-type-builder/services/schema-builder/schema-handler.js b/packages/strapi-plugin-content-type-builder/services/schema-builder/schema-handler.js index 0a5f704014..5caaa376c3 100644 --- a/packages/strapi-plugin-content-type-builder/services/schema-builder/schema-handler.js +++ b/packages/strapi-plugin-content-type-builder/services/schema-builder/schema-handler.js @@ -16,6 +16,7 @@ module.exports = function createSchemaHandler(infos) { dir, filename, schema: schema || { + kind: undefined, info: {}, options: {}, attributes: {},