Add kind property to content-type-builder

Signed-off-by: Alexandre Bodin <bodin.alex@gmail.com>
This commit is contained in:
Alexandre Bodin 2020-01-14 18:04:07 +01:00
parent ff1b249d31
commit 57122c4acb
9 changed files with 211 additions and 123 deletions

View File

@ -5,10 +5,19 @@ const _ = require('lodash');
const { const {
validateContentTypeInput, validateContentTypeInput,
validateUpdateContentTypeInput, validateUpdateContentTypeInput,
validateKind,
} = require('./validation/content-type'); } = require('./validation/content-type');
module.exports = { module.exports = {
getContentTypes(ctx) { async getContentTypes(ctx) {
const { kind } = ctx.query;
try {
await validateKind(kind);
} catch (error) {
return ctx.send({ error }, 400);
}
const contentTypeService = const contentTypeService =
strapi.plugins['content-type-builder'].services.contenttypes; strapi.plugins['content-type-builder'].services.contenttypes;
@ -17,6 +26,13 @@ module.exports = {
if (uid.startsWith('strapi::')) return false; if (uid.startsWith('strapi::')) return false;
if (uid === 'plugins::upload.file') return false; // TODO: add a flag in the content type instead 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; return true;
}) })
.map(uid => .map(uid =>

View File

@ -6,6 +6,7 @@ const yup = require('yup');
const { isValidCategoryName, isValidIcon } = require('./common'); const { isValidCategoryName, isValidIcon } = require('./common');
const formatYupErrors = require('./yup-formatter'); const formatYupErrors = require('./yup-formatter');
const createSchema = require('./model-schema'); const createSchema = require('./model-schema');
const removeEmptyDefaults = require('./remove-empty-defaults');
const { modelTypes, DEFAULT_TYPES } = require('./constants'); const { modelTypes, DEFAULT_TYPES } = require('./constants');
const VALID_RELATIONS = ['oneWay', 'manyWay']; const VALID_RELATIONS = ['oneWay', 'manyWay'];
@ -63,26 +64,7 @@ const validateComponentInput = data => {
}; };
const validateUpdateComponentInput = data => { const validateUpdateComponentInput = data => {
// convert zero length string on default attributes to undefined removeEmptyDefaults(data);
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;
}
});
}
});
}
return yup return yup
.object({ .object({

View File

@ -3,6 +3,9 @@
const CONTENT_TYPE = 'CONTENT_TYPE'; const CONTENT_TYPE = 'CONTENT_TYPE';
const COMPONENT = 'COMPONENT'; const COMPONENT = 'COMPONENT';
const SINGLE_TYPE = 'singleType';
const COLLECTION_TYPE = 'collectionType';
const DEFAULT_TYPES = [ const DEFAULT_TYPES = [
// advanced types // advanced types
'media', 'media',
@ -28,8 +31,15 @@ const DEFAULT_TYPES = [
const FORBIDDEN_ATTRIBUTE_NAMES = ['__component', '__contentType']; const FORBIDDEN_ATTRIBUTE_NAMES = ['__component', '__contentType'];
const CONTENT_TYPE_KINDS = [SINGLE_TYPE, COLLECTION_TYPE];
module.exports = { module.exports = {
DEFAULT_TYPES, DEFAULT_TYPES,
CONTENT_TYPE_KINDS,
typeKinds: {
SINGLE_TYPE,
COLLECTION_TYPE,
},
modelTypes: { modelTypes: {
CONTENT_TYPE, CONTENT_TYPE,
COMPONENT, COMPONENT,

View File

@ -2,35 +2,66 @@
const _ = require('lodash'); const _ = require('lodash');
const yup = require('yup'); const yup = require('yup');
const formatYupErrors = require('./yup-formatter'); const formatYupErrors = require('./yup-formatter');
const createSchema = require('./model-schema'); const createSchema = require('./model-schema');
const removeEmptyDefaults = require('./remove-empty-defaults');
const { nestedComponentSchema } = require('./component'); const { nestedComponentSchema } = require('./component');
const { modelTypes, DEFAULT_TYPES } = require('./constants'); const {
modelTypes,
DEFAULT_TYPES,
CONTENT_TYPE_KINDS,
typeKinds,
} = require('./constants');
const VALID_RELATIONS = [ /**
'oneWay', * Allowed relation per type kind
'manyWay', */
'oneToOne', const VALID_RELATIONS = {
'oneToMany', [typeKinds.SINGLE_TYPE]: ['oneWay', 'manyWay'],
'manyToOne', [typeKinds.COLLECTION_TYPE]: [
'manyToMany', 'oneWay',
]; 'manyWay',
'oneToOne',
'oneToMany',
'manyToOne',
'manyToMany',
],
};
/**
* Allowed types
*/
const VALID_TYPES = [...DEFAULT_TYPES, 'component', 'dynamiczone']; 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 const contentTypeSchema = createSchema(
.object({ VALID_TYPES,
contentType: contentTypeSchema.required().noUnknown(), VALID_RELATIONS[kind] || [],
components: nestedComponentSchema, {
}) modelType: modelTypes.CONTENT_TYPE,
.noUnknown(); }
);
return yup
.object({
contentType: contentTypeSchema.required().noUnknown(),
components: nestedComponentSchema,
})
.noUnknown();
};
/**
* Validator for content type creation
*/
const validateContentTypeInput = data => { const validateContentTypeInput = data => {
return createContentTypeSchema return createContentTypeSchema(data)
.validate(data, { .validate(data, {
strict: true, strict: true,
abortEarly: false, abortEarly: false,
@ -38,29 +69,13 @@ const validateContentTypeInput = data => {
.catch(error => Promise.reject(formatYupErrors(error))); .catch(error => Promise.reject(formatYupErrors(error)));
}; };
/**
* Validator for content type edition
*/
const validateUpdateContentTypeInput = data => { const validateUpdateContentTypeInput = data => {
// convert zero length string on default attributes to undefined removeEmptyDefaults(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)) { return createContentTypeSchema(data)
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
.validate(data, { .validate(data, {
strict: true, strict: true,
abortEarly: false, abortEarly: false,
@ -68,7 +83,20 @@ const validateUpdateContentTypeInput = data => {
.catch(error => Promise.reject(formatYupErrors(error))); .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 = { module.exports = {
validateContentTypeInput, validateContentTypeInput,
validateUpdateContentTypeInput, validateUpdateContentTypeInput,
validateKind,
}; };

View File

@ -3,72 +3,88 @@
const _ = require('lodash'); const _ = require('lodash');
const yup = require('yup'); 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 { isValidCollectionName, isValidKey } = require('./common');
const { getTypeShape } = require('./types'); const { getTypeShape } = require('./types');
const getRelationValidator = require('./relations'); const getRelationValidator = require('./relations');
const createSchema = (types, relations, { modelType } = {}) => const createSchema = (types, relations, { modelType } = {}) => {
yup const schema = yup.object({
.object({ name: yup
name: yup .string()
.string() .min(1)
.min(1) .required('name.required'),
.required('name.required'), description: yup.string(),
description: yup.string(), connection: yup.string(),
connection: yup.string(), collectionName: yup
collectionName: yup .string()
.string() .nullable()
.nullable() .test(isValidCollectionName),
.test(isValidCollectionName), attributes: yup.lazy(attributes => {
attributes: yup.lazy(attributes => { return yup
return yup .object()
.object() .shape(
.shape( _.mapValues(attributes, (attribute, key) => {
_.mapValues(attributes, (attribute, key) => { if (FORBIDDEN_ATTRIBUTE_NAMES.includes(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();
}
return yup.object().test({ return yup.object().test({
name: 'mustHaveTypeOrTarget', name: 'forbiddenKeys',
message: 'Attribute must have either a type or a target', message: `Attribute keys cannot be one of ${FORBIDDEN_ATTRIBUTE_NAMES.join(
', '
)}`,
test: () => false, test: () => false,
}); });
}) }
)
.required('attributes.required'); if (_.has(attribute, 'type')) {
}), const shape = {
}) type: yup
.noUnknown(); .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; module.exports = createSchema;

View File

@ -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;
};

View File

@ -17,7 +17,7 @@ const { nameToSlug } = require('../utils/helpers');
* @param {Object} contentType * @param {Object} contentType
*/ */
const formatContentType = contentType => { const formatContentType = contentType => {
const { uid, plugin, connection, collectionName, info } = contentType; const { uid, kind, plugin, connection, collectionName, info } = contentType;
return { return {
uid, uid,
@ -26,6 +26,7 @@ const formatContentType = contentType => {
name: _.get(info, 'name') || _.upperFirst(pluralize(uid)), name: _.get(info, 'name') || _.upperFirst(pluralize(uid)),
description: _.get(info, 'description', ''), description: _.get(info, 'description', ''),
connection, connection,
kind: kind || 'collectionType',
collectionName, collectionName,
attributes: formatAttributes(contentType), attributes: formatAttributes(contentType),
}, },

View File

@ -80,6 +80,7 @@ module.exports = function createComponentBuilder() {
contentType contentType
.setUID(uid) .setUID(uid)
.set('connection', infos.connection || defaultConnection) .set('connection', infos.connection || defaultConnection)
.set('kind', infos.kind)
.set('collectionName', infos.collectionName || defaultCollectionName) .set('collectionName', infos.collectionName || defaultCollectionName)
.set(['info', 'name'], infos.name) .set(['info', 'name'], infos.name)
.set(['info', 'description'], infos.description) .set(['info', 'description'], infos.description)
@ -197,9 +198,12 @@ module.exports = function createComponentBuilder() {
} }
}); });
// TODO: handle kind change => update routes.json file somehow
contentType contentType
.set('connection', infos.connection) .set('connection', infos.connection)
.set('collectionName', infos.collectionName) .set('collectionName', infos.collectionName)
.set('kind', infos.kind)
.set(['info', 'name'], infos.name) .set(['info', 'name'], infos.name)
.set(['info', 'description'], infos.description) .set(['info', 'description'], infos.description)
.setAttributes(this.convertAttributes(newAttributes)); .setAttributes(this.convertAttributes(newAttributes));

View File

@ -16,6 +16,7 @@ module.exports = function createSchemaHandler(infos) {
dir, dir,
filename, filename,
schema: schema || { schema: schema || {
kind: undefined,
info: {}, info: {},
options: {}, options: {},
attributes: {}, attributes: {},