Edit CT schema

This commit is contained in:
Alexandre Bodin 2019-11-07 12:44:06 +01:00
parent c2df4bc404
commit 8d025a970d
9 changed files with 239 additions and 278 deletions

View File

@ -1,6 +1,6 @@
'use strict';
const validateComponentCategory = require('./validation/componentCategory');
const validateComponentCategory = require('./validation/component-category');
module.exports = {
async editCategory(ctx) {

View File

@ -11,7 +11,7 @@ const { nameToSlug } = require('../utils/helpers');
const {
validateContentTypeInput,
validateUpdateContentTypeInput,
} = require('./validation/contentType');
} = require('./validation/content-type');
module.exports = {
getContentTypes(ctx) {
@ -50,8 +50,8 @@ module.exports = {
return ctx.send({ error }, 400);
}
const slug = nameToSlug(body.name);
const uid = `application::${slug}.${slug}`;
const modelName = nameToSlug(body.name);
const uid = `application::${modelName}.${modelName}`;
if (_.has(strapi.contentTypes, uid)) {
return ctx.send({ error: 'contentType.alreadyExists' }, 400);
@ -62,9 +62,12 @@ module.exports = {
try {
const contentType = createContentTypeSchema(body);
await generateAPI(slug, contentType);
await generateAPI(modelName, contentType);
await generateReversedRelations({ attributes: body.attributes, slug });
await generateReversedRelations({
attributes: body.attributes,
modelName,
});
if (_.isEmpty(strapi.api)) {
strapi.emit('didCreateFirstContentType');
@ -111,46 +114,15 @@ module.exports = {
await writeContentType({ uid, schema: newSchema });
const updates = Object.keys(strapi.contentTypes).map(ctUID => {
const contentType = strapi.contentTypes[ctUID];
// delete all relations directed to the updated ct except for oneWay and manyWay
await deleteBidirectionalRelations(contentType);
const keysToDelete = Object.keys(
contentType.__schema__.attributes
).filter(key => {
const attr = contentType.__schema__.attributes[key];
if (
(attr.model === uid || attr.collection === uid) &&
attr.via &&
!Object.keys(newSchema.attributes).includes(attr.via)
) {
return true;
}
return false;
});
const keysToUpdate = [];
if (keysToDelete.length > 0 || keysToUpdate.length > 0) {
const newAttributes = _.omit(contentType.__schema__.attributes, [
keysToDelete,
]);
const newCTSchema = {
...contentType.__schema__,
attributes: newAttributes,
};
return writeContentType({ uid: ctUID, schema: newCTSchema });
}
await generateReversedRelations({
attributes: body.attributes,
modelName: contentType.modelName,
plugin: contentType.plugin,
});
await Promise.all(updates);
// TODO: clear relations to delete (diff old vs new attributes and delete the ones with via)
// TODO: update relations that changed
// TODO: add new relations (diff old vs new and add new attributes)
if (_.isEmpty(strapi.api)) {
strapi.emit('didCreateFirstContentType');
} else {
@ -174,45 +146,87 @@ module.exports = {
},
};
const generateReversedRelations = ({ attributes, slug, plugin }) => {
const deleteBidirectionalRelations = ({ modelName, plugin }) => {
const updates = Object.keys(strapi.contentTypes).map(uid => {
const { __schema__ } = strapi.contentTypes[uid];
const keysToDelete = Object.keys(__schema__.attributes).filter(key => {
const attr = __schema__.attributes[key];
const target = attr.model || attr.collection;
const sameModel = target === modelName;
const samePluginOrNoPlugin =
(attr.plugin && attr.plugin === plugin) || !attr.plugin;
const isBiDirectionnal = _.has(attr, 'via');
if (samePluginOrNoPlugin && sameModel && isBiDirectionnal) {
return true;
}
return false;
});
if (keysToDelete.length > 0) {
const newchema = {
...__schema__,
attributes: _.omit(__schema__.attributes, keysToDelete),
};
return writeContentType({ uid, schema: newchema });
}
});
return Promise.all(updates);
};
const buildReversedRelation = ({ key, attr, plugin, modelName }) => {
const targetAttributeOptions = {
via: key,
columnName: attr.targetColumnName,
plugin,
};
switch (attr.nature) {
case 'manyWay':
case 'oneWay':
return;
case 'oneToOne':
case 'oneToMany':
targetAttributeOptions.model = modelName;
break;
case 'manyToOne':
targetAttributeOptions.collection = modelName;
break;
case 'manyToMany': {
targetAttributeOptions.collection = modelName;
if (!targetAttributeOptions.dominant) {
targetAttributeOptions.dominant = true;
}
break;
}
default:
}
return targetAttributeOptions;
};
const generateReversedRelations = ({ attributes, modelName, plugin }) => {
const promises = Object.keys(attributes)
.filter(key => _.has(attributes[key], 'target'))
.map(key => {
const attr = attributes[key];
const target = strapi.contentTypes[attr.target];
const targetAttributeOptions = {
via: key,
columnName: attr.targetColumnName,
plugin,
};
switch (attr.nature) {
case 'manyWay':
case 'oneWay':
return;
case 'oneToOne':
case 'oneToMany':
targetAttributeOptions.model = slug;
break;
case 'manyToOne':
targetAttributeOptions.collection = slug;
break;
case 'manyToMany': {
targetAttributeOptions.collection = slug;
if (!targetAttributeOptions.dominant) {
targetAttributeOptions.dominant = true;
}
break;
}
default:
}
const schema = _.merge({}, target.__schema__, {
attributes: {
[attr.targetAttribute]: targetAttributeOptions,
[attr.targetAttribute]: buildReversedRelation({
key,
attr,
plugin,
modelName,
}),
},
});

View File

@ -1,15 +1,12 @@
'use strict';
const yup = require('yup');
const _ = require('lodash');
const formatYupErrors = require('./yup-formatter');
const { isValidName, isValidKey } = require('./common');
const { getTypeShape } = require('./types');
const getRelationValidator = require('./relations');
const createSchema = require('./model-schema');
const VALID_COMPONENT_RELATIONS = ['oneWay', 'manyWay'];
const VALID_COMPONENT_TYPES = [
const VALID_RELATIONS = ['oneWay', 'manyWay'];
const VALID_TYPES = [
// advanced types
'media',
@ -33,7 +30,7 @@ const VALID_COMPONENT_TYPES = [
];
const validateComponentInput = data => {
return componentSchema
return createSchema(VALID_TYPES, VALID_RELATIONS)
.validate(data, {
strict: true,
abortEarly: false,
@ -51,7 +48,7 @@ const validateUpdateComponentInput = data => {
});
}
return componentSchema
return createSchema(VALID_TYPES, VALID_RELATIONS)
.validate(data, {
strict: true,
abortEarly: false,
@ -59,65 +56,6 @@ const validateUpdateComponentInput = data => {
.catch(error => Promise.reject(formatYupErrors(error)));
};
const componentSchema = yup
.object({
name: yup
.string()
.min(1)
.required('name.required'),
icon: yup
.string()
.test(isValidName)
.required('icon.required'),
category: yup
.string()
.min(3)
.test(isValidName)
.required('category.required'),
description: yup.string(),
connection: yup.string(),
collectionName: yup
.string()
.nullable()
.test(isValidName),
attributes: yup.lazy(obj => {
return yup
.object()
.shape(
_.mapValues(obj, (value, key) => {
return yup.lazy(obj => {
let shape;
if (_.has(obj, 'type')) {
shape = {
type: yup
.string()
.oneOf(VALID_COMPONENT_TYPES)
.required(),
...getTypeShape(obj),
};
} else if (_.has(obj, 'target')) {
shape = getRelationValidator(obj, VALID_COMPONENT_RELATIONS);
} else {
return yup.object().test({
name: 'mustHaveTypeOrTarget',
message: 'Attribute must have either a type or a target',
test: () => false,
});
}
return yup
.object()
.shape(shape)
.test(isValidKey(key))
.noUnknown();
});
})
)
.required('attributes.required');
}),
})
.noUnknown();
module.exports = {
validateComponentInput,
validateUpdateComponentInput,

View File

@ -0,0 +1,70 @@
'use strict';
const _ = require('lodash');
const formatYupErrors = require('./yup-formatter');
const createSchema = require('./model-schema');
const VALID_RELATIONS = [
'oneWay',
'manyWay',
'oneToOne',
'oneToMany',
'manyToOne',
'manyToMany',
];
const VALID_TYPES = [
// advanced types
'media',
// scalar types
'string',
'text',
'richtext',
'json',
'enumeration',
'password',
'email',
'integer',
'biginteger',
'float',
'decimal',
'date',
'boolean',
// nested component
'component',
'dynamiczone',
];
const validateContentTypeInput = data => {
return createSchema(VALID_TYPES, VALID_RELATIONS)
.validate(data, {
strict: true,
abortEarly: false,
})
.catch(error => Promise.reject(formatYupErrors(error)));
};
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;
}
});
}
return createSchema(VALID_TYPES, VALID_RELATIONS)
.validate(data, {
strict: true,
abortEarly: false,
})
.catch(error => Promise.reject(formatYupErrors(error)));
};
module.exports = {
validateContentTypeInput,
validateUpdateContentTypeInput,
};

View File

@ -1,123 +0,0 @@
'use strict';
const yup = require('yup');
const _ = require('lodash');
const formatYupErrors = require('./yup-formatter');
const { isValidName, isValidKey } = require('./common');
const { getTypeShape } = require('./types');
const getRelationValidator = require('./relations');
const VALID_COMPONENT_RELATIONS = [
'oneWay',
'manyWay',
'oneToOne',
'oneToMany',
'manyToOne',
'manyToMany',
];
const VALID_COMPONENT_TYPES = [
// advanced types
'media',
// scalar types
'string',
'text',
'richtext',
'json',
'enumeration',
'password',
'email',
'integer',
'biginteger',
'float',
'decimal',
'date',
'boolean',
// nested component
'component',
'dynamiczone',
];
const validateContentTypeInput = data => {
return componentSchema
.validate(data, {
strict: true,
abortEarly: false,
})
.catch(error => Promise.reject(formatYupErrors(error)));
};
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;
}
});
}
return componentSchema
.validate(data, {
strict: true,
abortEarly: false,
})
.catch(error => Promise.reject(formatYupErrors(error)));
};
const componentSchema = yup
.object({
name: yup
.string()
.min(1)
.required('name.required'),
description: yup.string(),
connection: yup.string(),
collectionName: yup
.string()
.nullable()
.test(isValidName),
attributes: yup.lazy(obj => {
return yup
.object()
.shape(
_.mapValues(obj, (value, key) => {
return yup.lazy(obj => {
let shape;
if (_.has(obj, 'type')) {
shape = {
type: yup
.string()
.oneOf(VALID_COMPONENT_TYPES)
.required(),
...getTypeShape(obj),
};
} else if (_.has(obj, 'target')) {
shape = getRelationValidator(obj, VALID_COMPONENT_RELATIONS);
} else {
return yup.object().test({
name: 'mustHaveTypeOrTarget',
message: 'Attribute must have either a type or a target',
test: () => false,
});
}
return yup
.object()
.shape(shape)
.test(isValidKey(key))
.noUnknown();
});
})
)
.required('attributes.required');
}),
})
.noUnknown();
module.exports = {
validateContentTypeInput,
validateUpdateContentTypeInput,
};

View File

@ -0,0 +1,61 @@
'use strict';
const _ = require('lodash');
const yup = require('yup');
const { isValidName, isValidKey } = require('./common');
const { getTypeShape } = require('./types');
const getRelationValidator = require('./relations');
const createSchema = (types, relations) =>
yup
.object({
name: yup
.string()
.min(1)
.required('name.required'),
description: yup.string(),
connection: yup.string(),
collectionName: yup
.string()
.nullable()
.test(isValidName),
attributes: yup.lazy(attributes => {
return yup
.object()
.shape(
_.mapValues(attributes, (attribute, key) => {
if (_.has(attribute, 'type')) {
const shape = {
type: yup
.string()
.oneOf(types)
.required(),
...getTypeShape(attribute),
};
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');
}),
})
.noUnknown();
module.exports = createSchema;

View File

@ -1,6 +1,5 @@
'use strict';
const _ = require('lodash');
const yup = require('yup');
const { validators, isValidName } = require('./common');
@ -25,15 +24,6 @@ module.exports = (obj, validNatures) => {
? yup
.string()
.test(isValidName)
.test({
name: 'checkAvailableAttribute',
message: `The attribute '${obj.targetAttribute}' already exists in the target`,
test: value => {
const targetContentType = strapi.contentTypes[obj.target];
if (_.has(targetContentType.attributes, value)) return false;
return true;
},
})
.required()
: yup.string().test(isValidName),
targetColumnName: yup.string(),

View File

@ -5,6 +5,17 @@ const _ = require('lodash');
const { createController, createService } = require('../core-api');
const getURLFromSegments = require('../utils/url-from-segments');
const pickSchema = obj =>
_.cloneDeep(
_.pick(obj, [
'connection',
'collectionName',
'info',
'options',
'attributes',
])
);
module.exports = function(strapi) {
// Retrieve Strapi version.
strapi.config.uuid = _.get(strapi.config.info, 'strapi.uuid', '');
@ -49,7 +60,7 @@ module.exports = function(strapi) {
throw new Error(`Component ${key} is missing a collectionName attribute`);
Object.assign(component, {
__schema__: _.cloneDeep(component),
__schema__: pickSchema(component),
uid: key,
modelType: 'component',
globalId:
@ -63,7 +74,7 @@ module.exports = function(strapi) {
let model = strapi.api[apiName].models[modelName];
Object.assign(model, {
__schema__: _.cloneDeep(model),
__schema__: pickSchema(model),
modelType: 'contentType',
uid: `application::${apiName}.${modelName}`,
apiName,
@ -144,7 +155,7 @@ module.exports = function(strapi) {
let model = strapi.admin.models[key];
Object.assign(model, {
__schema__: _.cloneDeep(model),
__schema__: pickSchema(model),
modelType: 'contentType',
uid: `strapi::${key}`,
modelName: key,
@ -178,7 +189,7 @@ module.exports = function(strapi) {
let model = plugin.models[key];
Object.assign(model, {
__schema__: _.cloneDeep(model),
__schema__: pickSchema(model),
modelType: 'contentType',
modelName: key,
uid: `plugins::${pluginName}.${key}`,