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 {
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 =>

View File

@ -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({

View File

@ -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,

View File

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

View File

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

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
*/
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),
},

View File

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

View File

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