From 1e28d8b9d9d5d5062d6ee09ee4e09fc1d2a0c01a Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Fri, 8 Nov 2019 13:25:15 +0100 Subject: [PATCH] Update component servie --- .../api/address/models/Address.settings.json | 2 +- .../controllers/Components.js | 57 +-- .../controllers/ContentTypes.js | 354 ++---------------- .../services/Components.js | 345 ++++++++--------- .../services/ContentTypes.js | 322 ++++++++++++++++ packages/strapi/lib/core/load-components.js | 1 + 6 files changed, 542 insertions(+), 539 deletions(-) create mode 100644 packages/strapi-plugin-content-type-builder/services/ContentTypes.js diff --git a/examples/getstarted/api/address/models/Address.settings.json b/examples/getstarted/api/address/models/Address.settings.json index 51fe9b4d17..990fae7f5b 100755 --- a/examples/getstarted/api/address/models/Address.settings.json +++ b/examples/getstarted/api/address/models/Address.settings.json @@ -30,4 +30,4 @@ "model": "country" } } -} \ No newline at end of file +} diff --git a/packages/strapi-plugin-content-type-builder/controllers/Components.js b/packages/strapi-plugin-content-type-builder/controllers/Components.js index a940bd0d83..4372735fff 100644 --- a/packages/strapi-plugin-content-type-builder/controllers/Components.js +++ b/packages/strapi-plugin-content-type-builder/controllers/Components.js @@ -1,9 +1,13 @@ 'use strict'; + const { validateComponentInput, validateUpdateComponentInput, } = require('./validation/component'); +const _ = require('lodash'); +const componentService = require('../services/Components'); + /** * Components controller */ @@ -15,8 +19,9 @@ module.exports = { * @param {Object} ctx - koa context */ async getComponents(ctx) { - const service = strapi.plugins['content-type-builder'].services.components; - const data = service.getComponents(); + const data = Object.keys(strapi.components).map(uid => { + return componentService.formatComponent(strapi.components[uid]); + }); ctx.send({ data }); }, @@ -29,14 +34,13 @@ module.exports = { async getComponent(ctx) { const { uid } = ctx.params; - const service = strapi.plugins['content-type-builder'].services.components; - const component = service.getComponent(uid); + const component = strapi.components[uid]; if (!component) { return ctx.send({ error: 'component.notFound' }, 404); } - ctx.send({ data: component }); + ctx.send({ data: componentService.formatComponent(component) }); }, /** @@ -53,16 +57,18 @@ module.exports = { return ctx.send({ error }, 400); } - const service = strapi.plugins['content-type-builder'].services.components; - const uid = service.createComponentUID(body); + const uid = componentService.createComponentUID(body); - if (service.getComponent(uid)) { + if (_.has(strapi.components, uid)) { return ctx.send({ error: 'component.alreadyExists' }, 400); } strapi.reload.isWatching = false; - const newComponent = await service.createComponent({ uid, infos: body }); + const newComponent = await componentService.createComponent({ + uid, + infos: body, + }); strapi.reload(); @@ -78,8 +84,7 @@ module.exports = { const { uid } = ctx.params; const { body } = ctx.request; - const service = strapi.plugins['content-type-builder'].services.components; - const component = service.getComponent(uid); + const component = strapi.components[uid]; if (!component) { return ctx.send({ error: 'component.notFound' }, 404); @@ -91,23 +96,26 @@ module.exports = { return ctx.send({ error }, 400); } - const newUID = service.createComponentUID(body); - if (newUID !== uid && service.getComponent(newUID)) { + const newUID = componentService.editComponentUID(body); + if (newUID !== uid && _.has(strapi.components, newUID)) { return ctx.send({ error: 'new.component.alreadyExists' }, 400); } strapi.reload.isWatching = false; - const updatedComponent = await service.updateComponent({ - newUID, - component, + const updatedComponent = await componentService.updateComponent({ + uid, infos: body, }); - await service.updateComponentInModels(component.uid, updatedComponent.uid); - strapi.reload(); + await componentService.updateComponentInModels( + component.uid, + updatedComponent.uid + ); - ctx.send({ data: updatedComponent }, 200); + setImmediate(() => strapi.reload()); + + ctx.send({ data: updatedComponent }); }, /** @@ -118,8 +126,7 @@ module.exports = { async deleteComponent(ctx) { const { uid } = ctx.params; - const service = strapi.plugins['content-type-builder'].services.components; - const component = service.getComponent(uid); + const component = strapi.components[uid]; if (!component) { return ctx.send({ error: 'component.notFound' }, 404); @@ -127,11 +134,11 @@ module.exports = { strapi.reload.isWatching = false; - await service.deleteComponent(component); - await service.deleteComponentInModels(component.uid); + await componentService.deleteComponentInModels(component.uid); + await componentService.deleteComponent(component); - strapi.reload(); + setImmediate(() => strapi.reload()); - ctx.send({ data: { uid } }, 200); + ctx.send({ data: { uid } }); }, }; diff --git a/packages/strapi-plugin-content-type-builder/controllers/ContentTypes.js b/packages/strapi-plugin-content-type-builder/controllers/ContentTypes.js index 1e407f3b51..58a67f551e 100644 --- a/packages/strapi-plugin-content-type-builder/controllers/ContentTypes.js +++ b/packages/strapi-plugin-content-type-builder/controllers/ContentTypes.js @@ -1,18 +1,15 @@ 'use strict'; const _ = require('lodash'); -const pluralize = require('pluralize'); -const fse = require('fs-extra'); -const path = require('path'); -const generator = require('strapi-generate'); -const { formatAttributes, convertAttributes } = require('../utils/attributes'); const { nameToSlug } = require('../utils/helpers'); const { validateContentTypeInput, validateUpdateContentTypeInput, } = require('./validation/content-type'); +const contentTypeService = require('../services/ContentTypes'); + module.exports = { getContentTypes(ctx) { const contentTypes = Object.keys(strapi.contentTypes) @@ -22,7 +19,9 @@ module.exports = { return true; }) - .map(uid => formatContentType(strapi.contentTypes[uid])); + .map(uid => + contentTypeService.formatContentType(strapi.contentTypes[uid]) + ); ctx.send({ data: contentTypes, @@ -38,7 +37,7 @@ module.exports = { return ctx.send({ error: 'contentType.notFound' }, 404); } - ctx.send({ data: formatContentType(contentType) }); + ctx.send({ data: contentTypeService.formatContentType(contentType) }); }, async createContentType(ctx) { @@ -60,11 +59,11 @@ module.exports = { strapi.reload.isWatching = false; try { - const contentType = createContentTypeSchema(body); + const contentType = contentTypeService.createContentTypeSchema(body); - await generateAPI(modelName, contentType); + await contentTypeService.generateAPI(modelName, contentType); - await generateReversedRelations({ + await contentTypeService.generateReversedRelations({ attributes: body.attributes, modelName, }); @@ -84,11 +83,14 @@ module.exports = { setImmediate(() => strapi.reload()); - ctx.send({ - data: { - uid, + ctx.send( + { + data: { + uid, + }, }, - }); + 201 + ); }, async updateContentType(ctx) { @@ -110,14 +112,17 @@ module.exports = { strapi.reload.isWatching = false; try { - const newSchema = updateContentTypeSchema(contentType.__schema__, body); + const newSchema = contentTypeService.updateContentTypeSchema( + contentType.__schema__, + body + ); - await writeContentType({ uid, schema: newSchema }); + await contentTypeService.writeContentType({ uid, schema: newSchema }); // delete all relations directed to the updated ct except for oneWay and manyWay - await deleteBidirectionalRelations(contentType); + await contentTypeService.deleteBidirectionalRelations(contentType); - await generateReversedRelations({ + await contentTypeService.generateReversedRelations({ attributes: body.attributes, modelName: contentType.modelName, plugin: contentType.plugin, @@ -128,12 +133,9 @@ module.exports = { } else { strapi.emit('didCreateContentType'); } - } catch (e) { - strapi.log.error(e); - strapi.emit('didNotCreateContentType', e); - return ctx.badRequest(null, [ - { messages: [{ id: 'request.error.model.write' }] }, - ]); + } catch (error) { + strapi.emit('didNotCreateContentType', error); + throw error; } setImmediate(() => strapi.reload()); @@ -160,8 +162,8 @@ module.exports = { strapi.reload.isWatching = false; - await removeContentType(contentType); - await deleteAllRelations(contentType); + await contentTypeService.deleteAllRelations(contentType); + await contentTypeService.removeContentType(contentType); setImmediate(() => strapi.reload()); @@ -172,303 +174,3 @@ module.exports = { }); }, }; - -const deleteAllRelations = ({ modelName, plugin }) => { - const contentTypeUpdates = 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; - - if (samePluginOrNoPlugin && sameModel) { - return true; - } - - return false; - }); - - if (keysToDelete.length > 0) { - const newchema = { - ...__schema__, - attributes: _.omit(__schema__.attributes, keysToDelete), - }; - - return writeContentType({ uid, schema: newchema }); - } - }); - - const componentUpdates = Object.keys(strapi.components).map(uid => { - const { __schema__ } = strapi.components[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; - - if (samePluginOrNoPlugin && sameModel) { - return true; - } - - return false; - }); - - if (keysToDelete.length > 0) { - const newchema = { - ...__schema__, - attributes: _.omit(__schema__.attributes, keysToDelete), - }; - - return strapi.plugins[ - 'content-type-builder' - ].services.components.writeComponent({ uid, schema: newchema }); - } - }); - - return Promise.all([...contentTypeUpdates, ...componentUpdates]); -}; - -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 schema = _.merge({}, target.__schema__, { - attributes: { - [attr.targetAttribute]: buildReversedRelation({ - key, - attr, - plugin, - modelName, - }), - }, - }); - - return writeContentType({ uid: attr.target, schema }); - }); - - return Promise.all(promises); -}; - -const removeContentType = async ({ uid }) => { - const { apiName, __filename__ } = strapi.contentTypes[uid]; - - const baseName = path.basename(__filename__, '.settings.json'); - const apiFolder = path.join(strapi.dir, 'api', apiName); - - const deleteFile = async filePath => { - const fileName = path.basename(filePath); - - if (_.startsWith(_.toLower(fileName), _.toLower(baseName) + '.')) { - await fse.remove(filePath); - } - - if (fileName === 'routes.json') { - const { routes } = await fse.readJSON(filePath); - - const clearedRoutes = routes.filter(route => { - return !_.startsWith( - _.toLower(route.handler), - _.toLower(baseName) + '.' - ); - }); - - if (clearedRoutes.length === 0) { - await fse.remove(filePath); - } else { - await fse.writeJSON( - filePath, - { - routes: clearedRoutes, - }, - { - spaces: 2, - } - ); - } - } - }; - - const recursiveRemoveFiles = async folder => { - const filesName = await fse.readdir(folder); - - for (const fileName of filesName) { - const filePath = path.join(folder, fileName); - - const stat = await fse.stat(filePath); - - if (stat.isDirectory()) { - await recursiveRemoveFiles(filePath); - } else { - await deleteFile(filePath); - } - } - - const files = await fse.readdir(folder); - if (files.length === 0) { - await fse.remove(folder); - } - }; - - await recursiveRemoveFiles(apiFolder); -}; - -const writeContentType = async ({ uid, schema }) => { - const { plugin, apiName, __filename__ } = strapi.contentTypes[uid]; - - let fileDir; - if (plugin) { - fileDir = `./extensions/${plugin}/models`; - } else { - fileDir = `./api/${apiName}/models`; - } - - const filePath = path.join(strapi.dir, fileDir, __filename__); - - await fse.ensureFile(filePath); - return fse.writeFile(filePath, JSON.stringify(schema, null, 2)); -}; - -const formatContentType = contentType => { - const { uid, plugin, connection, collectionName, info } = contentType; - - return { - uid, - plugin, - schema: { - name: _.get(info, 'name') || _.upperFirst(pluralize(uid)), - description: _.get(info, 'description', ''), - connection, - collectionName, - attributes: formatAttributes(contentType), - }, - }; -}; - -const createContentTypeSchema = infos => ({ - connection: - infos.connection || - _.get( - strapi, - ['config', 'currentEnvironment', 'database', 'defaultConnection'], - 'default' - ), - collectionName: - infos.collectionName || `${_.snakeCase(pluralize(infos.name))}`, - info: { - name: infos.name, - description: infos.description, - }, - attributes: convertAttributes(infos.attributes), -}); - -const updateContentTypeSchema = (old, infos) => ({ - ...old, - connection: infos.connection || old.connection, - collectionName: infos.collectionName || old.collectionName, - info: { - name: infos.name || old.info.name, - description: infos.description || old.info.description, - }, - // TODO: keep old params like autoMigration, private, configurable - attributes: convertAttributes(infos.attributes), -}); - -const generateAPI = (name, contentType) => { - // create api - return new Promise((resolve, reject) => { - const scope = { - generatorType: 'api', - id: name, - name, - rootPath: strapi.dir, - args: { - displayName: contentType.info.name, - description: contentType.info.description, - connection: contentType.connection, - collectionName: contentType.collectionName, - attributes: contentType.attributes, - }, - }; - - generator(scope, { - success: () => resolve(), - error: err => reject(err), - }); - }); -}; diff --git a/packages/strapi-plugin-content-type-builder/services/Components.js b/packages/strapi-plugin-content-type-builder/services/Components.js index 58c22a2165..957bba50ec 100644 --- a/packages/strapi-plugin-content-type-builder/services/Components.js +++ b/packages/strapi-plugin-content-type-builder/services/Components.js @@ -5,36 +5,17 @@ const _ = require('lodash'); const fse = require('fs-extra'); const pluralize = require('pluralize'); +const contentTypeService = require('./ContentTypes'); const { formatAttributes, convertAttributes } = require('../utils/attributes'); const { nameToSlug } = require('../utils/helpers'); -/** - * Returns a list of all available components with formatted attributes - */ -const getComponents = () => { - return Object.keys(strapi.components).map(uid => { - return formatComponent(uid, strapi.components[uid]); - }); -}; - -/** - * Returns a component by uid - * @param {string} uid - component's UID - */ -const getComponent = uid => { - const component = strapi.components[uid]; - if (!component) return null; - - return formatComponent(uid, component); -}; - /** * Formats a component attributes * @param {string} uid - string * @param {Object} component - strapi component model */ -const formatComponent = (uid, component) => { - const { connection, collectionName, info, category } = component; +const formatComponent = component => { + const { uid, connection, collectionName, info, category } = component; return { uid, @@ -58,7 +39,12 @@ const formatComponent = (uid, component) => { async function createComponent({ uid, infos }) { const schema = createSchema(infos); - await writeSchema({ uid, schema }); + await writeSchema({ + category: nameToSlug(infos.category), + name: nameToSlug(infos.name), + schema, + }); + return { uid }; } @@ -67,49 +53,43 @@ async function createComponent({ uid, infos }) { * @param {Object} component * @param {Object} infos */ -async function updateComponent({ component, newUID, infos }) { - const { uid, schema: oldSchema } = component; +async function updateComponent({ component, infos }) { + const { uid, __schema__: oldSchema } = component; // don't update collectionName if not provided const updatedSchema = { - info: { - icon: infos.icon, - name: infos.name, - description: infos.description || oldSchema.description, - }, + ...oldSchema, connection: infos.connection || oldSchema.connection, collectionName: infos.collectionName || oldSchema.collectionName, + info: { + name: infos.name || oldSchema.info.name, + icon: infos.icon || oldSchema.info.icon, + description: infos.description || oldSchema.info.description, + }, attributes: convertAttributes(infos.attributes), }; - if (uid !== newUID) { - await deleteSchema(uid); + await editSchema({ uid, schema: updatedSchema }); - if (_.has(strapi.plugins, ['content-manager', 'services', 'components'])) { - await _.get(strapi.plugins, [ - 'content-manager', - 'services', - 'components', - ]).updateUID(uid, newUID); + if (component.category !== infos.category) { + const oldDir = path.join(strapi.dir, 'components', component.category); + const newDir = path.join(strapi.dir, 'components', infos.category); + + await fse.move( + path.join(oldDir, component.__filename__), + path.join(newDir, component.__filename__) + ); + + const list = await fse.readdir(oldDir); + if (list.length === 0) { + await fse.remove(oldDir); } - await writeSchema({ - uid: newUID, - schema: updatedSchema, - }); - - const [category] = uid.split('.'); - - const categoryDir = path.join(strapi.dir, 'components', category); - const categoryCompos = await fse.readdir(categoryDir); - if (categoryCompos.length === 0) { - await fse.remove(categoryDir); - } - - return { uid: newUID }; + return { + uid: `${infos.category}.${component.modelName}`, + }; } - await writeSchema({ uid, schema: updatedSchema }); return { uid }; } @@ -164,14 +144,27 @@ async function deleteComponent(component) { /** * Writes a component schema file */ -async function writeSchema({ uid, schema }) { - const [category, filename] = uid.split('.'); - const categoryDir = path.join(strapi.dir, 'components', category); +async function writeSchema({ category, name, schema }) { + const filePath = path.join( + strapi.dir, + 'components', + category, + `${name}.json` + ); - await fse.ensureDir(categoryDir); + await fse.ensureFile(filePath); + await fse.writeJSON(filePath, schema, { spaces: 2 }); +} - const filepath = path.join(categoryDir, `${filename}.json`); - await fse.writeFile(filepath, JSON.stringify(schema, null, 2)); +/** + * Edit a component schema file + */ +async function editSchema({ uid, schema }) { + const { category, __filename__ } = strapi.components[uid]; + const filePath = path.join(strapi.dir, 'components', category, __filename__); + + await fse.ensureFile(filePath); + await fse.writeJSON(filePath, schema, { spaces: 2 }); } /** @@ -179,156 +172,131 @@ async function writeSchema({ uid, schema }) { * @param {string} ui */ async function deleteSchema(uid) { - const [category, filename] = uid.split('.'); - await strapi.fs.removeAppFile(`components/${category}/${filename}.json`); + const { category, __filename__ } = strapi.components[uid]; + await strapi.fs.removeAppFile(`components/${category}/${__filename__}`); } const updateComponentInModels = (oldUID, newUID) => { - const contentTypeService = - strapi.plugins['content-type-builder'].services.contenttypebuilder; + const contentTypeUpdates = Object.keys(strapi.contentTypes).map(uid => { + const { __schema__: oldSchema } = strapi.contentTypes[uid]; - const updateModels = (models, { plugin } = {}) => { - Object.keys(models).forEach(modelKey => { - const model = models[modelKey]; + const componentsToUpdate = Object.keys(oldSchema.attributes).reduce( + (acc, key) => { + if ( + oldSchema.attributes[key].type === 'component' && + oldSchema.attributes[key].component === oldUID + ) { + acc.push(key); + } - const attributesToModify = Object.keys(model.attributes).reduce( - (acc, key) => { - if ( - model.attributes[key].type === 'component' && - model.attributes[key].component === oldUID - ) { - acc.push(key); - } + return acc; + }, + [] + ); - return acc; - }, - [] - ); - - const dynamicoznesToUpdate = Object.keys(model.attributes).filter(key => { + const dynamiczonesToUpdate = Object.keys(oldSchema.attributes).filter( + key => { return ( - model.attributes[key].type === 'dynamiczone' && - model.attributes[key].components.includes(oldUID) + oldSchema.attributes[key].type === 'dynamiczone' && + oldSchema.attributes[key].components.includes(oldUID) ); - }, []); + }, + [] + ); - if (attributesToModify.length > 0) { - const modelJSON = contentTypeService.readModel(modelKey, { - plugin, - api: model.apiName, - }); + if (componentsToUpdate.length > 0 || dynamiczonesToUpdate.length > 0) { + const newSchema = _.cloneDeep(oldSchema); - attributesToModify.forEach(key => { - modelJSON.attributes[key].component = newUID; - }); - - dynamicoznesToUpdate.forEach(key => { - modelJSON.attributes[key] = { - ...modelJSON.attributes[key], - components: modelJSON.attributes[key].components.map(val => - val !== oldUID ? val : newUID - ), - }; - }); - - contentTypeService.writeModel(modelKey, modelJSON, { - plugin, - api: model.apiName, - }); - } - }); - }; - - updateModels(strapi.models); - - Object.keys(strapi.plugins).forEach(pluginKey => { - updateModels(strapi.plugins[pluginKey].models, { plugin: pluginKey }); - }); - - Object.keys(strapi.components).forEach(uid => { - const component = strapi.components[uid]; - - const componentsToRemove = Object.keys(component.attributes).filter(key => { - return ( - component.attributes[key].type === 'component' && - component.attributes[key].component === oldUID - ); - }, []); - - if (componentsToRemove.length > 0) { - const newSchema = { - info: component.info, - connection: component.connection, - collectionName: component.collectionName, - attributes: component.attributes, - }; - - componentsToRemove.forEach(key => { + componentsToUpdate.forEach(key => { newSchema.attributes[key].component = newUID; }); - writeSchema({ uid, schema: newSchema }); + dynamiczonesToUpdate.forEach(key => { + newSchema.attributes[key].components = oldSchema.attributes[ + key + ].components.map(val => (val !== oldUID ? val : newUID)); + }); + + return contentTypeService.writeContentType({ uid, schema: newSchema }); } + + return Promise.resolve(); }); + + const componentUpdates = Object.keys(strapi.components).map(uid => { + const { __schema__: oldSchema } = strapi.components[uid]; + + const componentsToUpdate = Object.keys(oldSchema.attributes).filter(key => { + return ( + oldSchema.attributes[key].type === 'component' && + oldSchema.attributes[key].component === oldUID + ); + }, []); + + if (componentsToUpdate.length > 0) { + const newSchema = { + ...oldSchema, + }; + + componentsToUpdate.forEach(key => { + newSchema.attributes[key].component = newUID; + }); + + return editSchema({ uid, schema: newSchema }); + } + + return Promise.resolve(); + }); + + return Promise.all([...contentTypeUpdates, ...componentUpdates]); }; const deleteComponentInModels = async componentUID => { - const [category] = componentUID.split('.'); - const contentTypeService = - strapi.plugins['content-type-builder'].services.contenttypebuilder; + const component = strapi.components[componentUID]; - const updateModels = (models, { plugin } = {}) => { - Object.keys(models).forEach(modelKey => { - const model = models[modelKey]; + const contentTypeUpdates = Object.keys(strapi.contentTypes).map(uid => { + const { __schema__: oldSchema } = strapi.contentTypes[uid]; - const componentsToRemove = Object.keys(model.attributes).filter(key => { + const componentsToRemove = Object.keys(oldSchema.attributes).filter(key => { + return ( + oldSchema.attributes[key].type === 'component' && + oldSchema.attributes[key].component === componentUID + ); + }, []); + + const dynamiczonesToUpdate = Object.keys(oldSchema.attributes).filter( + key => { return ( - model.attributes[key].type === 'component' && - model.attributes[key].component === componentUID + oldSchema.attributes[key].type === 'dynamiczone' && + oldSchema.attributes[key].components.includes(componentUID) ); - }, []); + }, + [] + ); - const dynamicoznesToUpdate = Object.keys(model.attributes).filter(key => { - return ( - model.attributes[key].type === 'dynamiczone' && - model.attributes[key].components.includes(componentUID) - ); - }, []); + if (componentsToRemove.length > 0 || dynamiczonesToUpdate.length > 0) { + const newSchema = _.cloneDeep(oldSchema); - if (componentsToRemove.length > 0 || dynamicoznesToUpdate.length > 0) { - const modelJSON = contentTypeService.readModel(modelKey, { - plugin, - api: model.apiName, - }); + componentsToRemove.forEach(key => { + delete newSchema.attributes[key]; + }); - componentsToRemove.forEach(key => { - delete modelJSON.attributes[key]; - }); + dynamiczonesToUpdate.forEach(key => { + newSchema.attributes[key] = { + ...newSchema.attributes[key], + components: newSchema.attributes[key].components.filter( + val => val !== componentUID + ), + }; + }); - dynamicoznesToUpdate.forEach(key => { - modelJSON.attributes[key] = { - ...modelJSON.attributes[key], - components: modelJSON.attributes[key].components.filter( - val => val !== componentUID - ), - }; - }); + return contentTypeService.writeContentType({ uid, schema: newSchema }); + } - contentTypeService.writeModel(modelKey, modelJSON, { - plugin, - api: model.apiName, - }); - } - }); - }; - - updateModels(strapi.models); - - Object.keys(strapi.plugins).forEach(pluginKey => { - updateModels(strapi.plugins[pluginKey].models, { plugin: pluginKey }); + return Promise.resolve(); }); - Object.keys(strapi.components).forEach(uid => { + const componentUpdates = Object.keys(strapi.components).map(uid => { const component = strapi.components[uid]; const componentsToRemove = Object.keys(component.attributes).filter(key => { @@ -350,25 +318,28 @@ const deleteComponentInModels = async componentUID => { delete newSchema.attributes[key]; }); - writeSchema({ uid, schema: newSchema }); + return editSchema({ uid, schema: newSchema }); } + + return Promise.resolve(); }); - const categoryDir = path.join(strapi.dir, 'components', category); - const categoryCompos = await fse.readdir(categoryDir); - if (categoryCompos.length === 0) { + await Promise.all([...contentTypeUpdates, ...componentUpdates]); + + const categoryDir = path.join(strapi.dir, 'components', component.category); + const list = await fse.readdir(categoryDir); + if (list.length === 0) { await fse.remove(categoryDir); } }; module.exports = { - getComponents, - getComponent, createComponent, createComponentUID, updateComponent, deleteComponent, - writeComponent: writeSchema, + editSchema, + formatComponent, // export for testing only createSchema, diff --git a/packages/strapi-plugin-content-type-builder/services/ContentTypes.js b/packages/strapi-plugin-content-type-builder/services/ContentTypes.js new file mode 100644 index 0000000000..4d028a61ec --- /dev/null +++ b/packages/strapi-plugin-content-type-builder/services/ContentTypes.js @@ -0,0 +1,322 @@ +'use strict'; + +const path = require('path'); +const _ = require('lodash'); +const pluralize = require('pluralize'); +const fse = require('fs-extra'); +const generator = require('strapi-generate'); + +const componentService = require('./Components'); +const { formatAttributes, convertAttributes } = require('../utils/attributes'); + +const deleteAllRelations = ({ modelName, plugin }) => { + const contentTypeUpdates = 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; + + if (samePluginOrNoPlugin && sameModel) { + return true; + } + + return false; + }); + + if (keysToDelete.length > 0) { + const newchema = { + ...__schema__, + attributes: _.omit(__schema__.attributes, keysToDelete), + }; + + return writeContentType({ uid, schema: newchema }); + } + }); + + const componentUpdates = Object.keys(strapi.components).map(uid => { + const { __schema__ } = strapi.components[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; + + if (samePluginOrNoPlugin && sameModel) { + return true; + } + + return false; + }); + + if (keysToDelete.length > 0) { + const newchema = { + ...__schema__, + attributes: _.omit(__schema__.attributes, keysToDelete), + }; + + return componentService.editSchema({ uid, schema: newchema }); + } + }); + + return Promise.all([...contentTypeUpdates, ...componentUpdates]); +}; + +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 schema = _.merge({}, target.__schema__, { + attributes: { + [attr.targetAttribute]: buildReversedRelation({ + key, + attr, + plugin, + modelName, + }), + }, + }); + + return writeContentType({ uid: attr.target, schema }); + }); + + return Promise.all(promises); +}; + +const removeContentType = async ({ uid }) => { + const { apiName, __filename__ } = strapi.contentTypes[uid]; + + const baseName = path.basename(__filename__, '.settings.json'); + const apiFolder = path.join(strapi.dir, 'api', apiName); + + const deleteFile = async filePath => { + const fileName = path.basename(filePath); + + if (_.startsWith(_.toLower(fileName), _.toLower(baseName) + '.')) { + await fse.remove(filePath); + } + + if (fileName === 'routes.json') { + const { routes } = await fse.readJSON(filePath); + + const clearedRoutes = routes.filter(route => { + return !_.startsWith( + _.toLower(route.handler), + _.toLower(baseName) + '.' + ); + }); + + if (clearedRoutes.length === 0) { + await fse.remove(filePath); + } else { + await fse.writeJSON( + filePath, + { + routes: clearedRoutes, + }, + { + spaces: 2, + } + ); + } + } + }; + + const recursiveRemoveFiles = async folder => { + const filesName = await fse.readdir(folder); + + for (const fileName of filesName) { + const filePath = path.join(folder, fileName); + + const stat = await fse.stat(filePath); + + if (stat.isDirectory()) { + await recursiveRemoveFiles(filePath); + } else { + await deleteFile(filePath); + } + } + + const files = await fse.readdir(folder); + if (files.length === 0) { + await fse.remove(folder); + } + }; + + await recursiveRemoveFiles(apiFolder); +}; + +const writeContentType = async ({ uid, schema }) => { + const { plugin, apiName, __filename__ } = strapi.contentTypes[uid]; + + let fileDir; + if (plugin) { + fileDir = `./extensions/${plugin}/models`; + } else { + fileDir = `./api/${apiName}/models`; + } + + const filePath = path.join(strapi.dir, fileDir, __filename__); + + await fse.ensureFile(filePath); + return fse.writeFile(filePath, JSON.stringify(schema, null, 2)); +}; + +const formatContentType = contentType => { + const { uid, plugin, connection, collectionName, info } = contentType; + + return { + uid, + plugin, + schema: { + name: _.get(info, 'name') || _.upperFirst(pluralize(uid)), + description: _.get(info, 'description', ''), + connection, + collectionName, + attributes: formatAttributes(contentType), + }, + }; +}; + +const createContentTypeSchema = infos => ({ + connection: + infos.connection || + _.get( + strapi, + ['config', 'currentEnvironment', 'database', 'defaultConnection'], + 'default' + ), + collectionName: + infos.collectionName || `${_.snakeCase(pluralize(infos.name))}`, + info: { + name: infos.name, + description: infos.description, + }, + attributes: convertAttributes(infos.attributes), +}); + +const updateContentTypeSchema = (old, infos) => ({ + ...old, + connection: infos.connection || old.connection, + collectionName: infos.collectionName || old.collectionName, + info: { + name: infos.name || old.info.name, + description: infos.description || old.info.description, + }, + // TODO: keep old params like autoMigration, private, configurable + attributes: convertAttributes(infos.attributes), +}); + +const generateAPI = (name, contentType) => { + // create api + return new Promise((resolve, reject) => { + const scope = { + generatorType: 'api', + id: name, + name, + rootPath: strapi.dir, + args: { + displayName: contentType.info.name, + description: contentType.info.description, + connection: contentType.connection, + collectionName: contentType.collectionName, + attributes: contentType.attributes, + }, + }; + + generator(scope, { + success: () => resolve(), + error: err => reject(err), + }); + }); +}; + +module.exports = { + generateAPI, + createContentTypeSchema, + updateContentTypeSchema, + + deleteAllRelations, + deleteBidirectionalRelations, + generateReversedRelations, + + formatContentType, + writeContentType, + removeContentType, +}; diff --git a/packages/strapi/lib/core/load-components.js b/packages/strapi/lib/core/load-components.js index 7ba603456f..648d3e2972 100644 --- a/packages/strapi/lib/core/load-components.js +++ b/packages/strapi/lib/core/load-components.js @@ -17,6 +17,7 @@ module.exports = async ({ dir }) => { Object.keys(map[category]).forEach(key => { acc[`${category}.${key}`] = Object.assign(map[category][key], { category, + modelName: key, }); }); return acc;