diff --git a/packages/strapi-connector-bookshelf/lib/generate-component-relations.js b/packages/strapi-connector-bookshelf/lib/generate-component-relations.js index bb4ce0bdd2..7275ec2848 100644 --- a/packages/strapi-connector-bookshelf/lib/generate-component-relations.js +++ b/packages/strapi-connector-bookshelf/lib/generate-component-relations.js @@ -7,7 +7,7 @@ const { getComponentAttributes } = require('./utils/attributes'); const createComponentModels = async ({ model, definition, ORM, GLOBALS }) => { const { collectionName, primaryKey } = definition; - const componentAttributes = getComponentAttributes(definition.attributes); + const componentAttributes = getComponentAttributes(definition); if (componentAttributes.length > 0) { // create component model @@ -59,7 +59,7 @@ const createComponentModels = async ({ model, definition, ORM, GLOBALS }) => { const createComponentJoinTables = async ({ definition, ORM }) => { const { collectionName, primaryKey } = definition; - const componentAttributes = getComponentAttributes(definition.attributes); + const componentAttributes = getComponentAttributes(definition); if (componentAttributes.length > 0) { const joinTable = `${collectionName}_components`; diff --git a/packages/strapi-connector-bookshelf/lib/mount-models.js b/packages/strapi-connector-bookshelf/lib/mount-models.js index d2a7d8a456..e2d07df265 100644 --- a/packages/strapi-connector-bookshelf/lib/mount-models.js +++ b/packages/strapi-connector-bookshelf/lib/mount-models.js @@ -11,8 +11,25 @@ const { createComponentModels, } = require('./generate-component-relations'); +const populateFetch = require('./populate'); + const PIVOT_PREFIX = '_pivot_'; +const LIFECYCLES = { + creating: 'beforeCreate', + created: 'afterCreate', + destroying: 'beforeDestroy', + destroyed: 'afterDestroy', + updating: 'beforeUpdate', + updated: 'afterUpdate', + fetching: 'beforeFetch', + 'fetching:collection': 'beforeFetchAll', + fetched: 'afterFetch', + 'fetched:collection': 'afterFetchAll', + saving: 'beforeSave', + saved: 'afterSave', +}; + const getDatabaseName = connection => { const dbName = _.get(connection.settings, 'database'); const dbSchema = _.get(connection.settings, 'schema', 'public'); @@ -570,202 +587,12 @@ module.exports = ({ models, target, plugin = false }, ctx) => { return attrs; }; - const findModelByAssoc = ({ assoc }) => { - const target = assoc.collection || assoc.model; - return assoc.plugin === 'admin' - ? strapi.admin.models[target] - : assoc.plugin - ? strapi.plugins[assoc.plugin].models[target] - : strapi.models[target]; - }; - - const isPolymorphic = ({ assoc }) => { - return assoc.nature.toLowerCase().indexOf('morph') !== -1; - }; - - const formatPolymorphicPopulate = ({ assoc, path, prefix = '' }) => { - if (_.isString(path) && path === assoc.via) { - return { [`related.${assoc.via}`]: () => {} }; - } else if (_.isString(path) && path === assoc.alias) { - // MorphTo side. - if (assoc.related) { - return { [`${prefix}${assoc.alias}.related`]: () => {} }; - } - - // oneToMorph or manyToMorph side. - // Retrieve collection name because we are using it to build our hidden model. - const model = findModelByAssoc({ assoc }); - - return { - [`${prefix}${assoc.alias}.${model.collectionName}`]: function( - query - ) { - query.orderBy('created_at', 'desc'); - }, - }; - } - }; - - const createAssociationPopulate = () => { - return definition.associations - .filter(ast => ast.autoPopulate !== false) - .map(assoc => { - if (isPolymorphic({ assoc })) { - return formatPolymorphicPopulate({ - assoc, - path: assoc.alias, - }); - } - - let path = assoc.alias; - let extraAssocs = []; - if (assoc) { - const assocModel = findModelByAssoc({ assoc }); - - extraAssocs = assocModel.associations - .filter(assoc => isPolymorphic({ assoc })) - .map(assoc => - formatPolymorphicPopulate({ - assoc, - path: assoc.alias, - prefix: `${path}.`, - }) - ); - } - - return [assoc.alias, ...extraAssocs]; - }) - .reduce((acc, val) => acc.concat(val), []); - }; - - const populateComponent = key => { - const attr = definition.attributes[key]; - - if (attr.type === 'dynamiczone') return [`${key}.component`]; - - let paths = []; - const component = strapi.components[attr.component]; - - const assocs = (component.associations || []).filter( - assoc => assoc.autoPopulate === true - ); - - // paths.push(`${key}.component`); - assocs.forEach(assoc => { - if (isPolymorphic({ assoc })) { - const rel = formatPolymorphicPopulate({ - assoc, - path: assoc.alias, - prefix: `${key}.component.`, - }); - - paths.push(rel); - } else { - paths.push(`${key}.component.${assoc.alias}`); - } - }); - - return [`${key}.component`, ...paths]; - }; - - const createComponentsPopulate = () => { - const componentsToPopulate = componentAttributes.reduce((acc, key) => { - const attribute = definition.attributes[key]; - const autoPopulate = _.get(attribute, ['autoPopulate'], true); - - if (autoPopulate === true) { - return acc.concat(populateComponent(key)); - } - return acc; - }, []); - - return componentsToPopulate; - }; - - const isComponent = (def, key) => - _.get(def, ['attributes', key, 'type']) === 'component'; - - const formatPopulateOptions = withRelated => { - if (!Array.isArray(withRelated)) withRelated = [withRelated]; - - const obj = withRelated.reduce((acc, key) => { - if (_.isString(key)) { - acc[key] = () => {}; - return acc; - } - - return _.extend(acc, key); - }, {}); - - // if components are no - const finalObj = Object.keys(obj).reduce((acc, key) => { - // check the key path and update it if necessary nothing more - const parts = key.split('.'); - - let newKey; - let prefix = ''; - let tmpModel = definition; - for (let part of parts) { - if (isComponent(tmpModel, part)) { - tmpModel = strapi.components[tmpModel.attributes[part].component]; - // add component path and there relations / images - const path = `${prefix}${part}.component`; - - newKey = path; - prefix = `${path}.`; - continue; - } - - const assoc = tmpModel.associations.find( - association => association.alias === part - ); - - if (!assoc) return acc; - - tmpModel = findModelByAssoc({ assoc }); - - if (isPolymorphic({ assoc })) { - const path = formatPolymorphicPopulate({ - assoc, - path: assoc.alias, - prefix, - }); - - return _.extend(acc, path); - } - - newKey = `${prefix}${part}`; - prefix = `${newKey}.`; - } - - acc[newKey] = obj[key]; - return acc; - }, {}); - - return [finalObj]; - }; - // Initialize lifecycle callbacks. loadedModel.initialize = function() { // Load bookshelf plugin arguments from model options this.constructor.__super__.initialize.apply(this, arguments); - const lifecycle = { - creating: 'beforeCreate', - created: 'afterCreate', - destroying: 'beforeDestroy', - destroyed: 'afterDestroy', - updating: 'beforeUpdate', - updated: 'afterUpdate', - fetching: 'beforeFetch', - 'fetching:collection': 'beforeFetchAll', - fetched: 'afterFetch', - 'fetched:collection': 'afterFetchAll', - saving: 'beforeSave', - saved: 'afterSave', - }; - - _.forEach(lifecycle, (fn, key) => { + _.forEach(LIFECYCLES, (fn, key) => { if (_.isFunction(target[model.toLowerCase()][fn])) { this.on(key, target[model.toLowerCase()][fn]); } @@ -774,19 +601,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => { // Update withRelated level to bypass many-to-many association for polymorphic relationshiips. // Apply only during fetching. this.on('fetching fetching:collection', (instance, attrs, options) => { - // do not populate anything - if (options.withRelated === false) return; - if (options.isEager === true) return; - - if (_.isNil(options.withRelated)) { - options.withRelated = [] - .concat(createComponentsPopulate()) - .concat(createAssociationPopulate()); - } else if (_.isEmpty(options.withRelated)) { - options.withRelated = createComponentsPopulate(); - } else { - options.withRelated = formatPopulateOptions(options.withRelated); - } + populateFetch(definition, options); return _.isFunction(target[model.toLowerCase()]['beforeFetchAll']) ? target[model.toLowerCase()]['beforeFetchAll'] diff --git a/packages/strapi-connector-bookshelf/lib/populate.js b/packages/strapi-connector-bookshelf/lib/populate.js new file mode 100644 index 0000000000..4b975136c7 --- /dev/null +++ b/packages/strapi-connector-bookshelf/lib/populate.js @@ -0,0 +1,186 @@ +'use strict'; + +const _ = require('lodash'); +const { getComponentAttributes } = require('./utils/attributes'); +const { findModelByAssoc, isPolymorphic } = require('./utils/associations'); + +/** + * Create utilities to populate a model on fetch + */ + +const populateFetch = (definition, options) => { + // do not populate anything + if (options.withRelated === false) return; + if (options.isEager === true) return; + + if (_.isNil(options.withRelated)) { + options.withRelated = [] + .concat(createComponentsPopulate(definition)) + .concat(createAssociationPopulate(definition)); + } else if (_.isEmpty(options.withRelated)) { + options.withRelated = createComponentsPopulate(definition); + } else { + options.withRelated = formatPopulateOptions( + definition, + options.withRelated + ); + } +}; + +const isComponent = (def, key) => + _.get(def, ['attributes', key, 'type']) === 'component'; + +const createAssociationPopulate = definition => { + return definition.associations + .filter(ast => ast.autoPopulate !== false) + .map(assoc => { + if (isPolymorphic({ assoc })) { + return formatPolymorphicPopulate({ + assoc, + path: assoc.alias, + }); + } + + let path = assoc.alias; + let extraAssocs = []; + if (assoc) { + const assocModel = findModelByAssoc({ assoc }); + + extraAssocs = assocModel.associations + .filter(assoc => isPolymorphic({ assoc })) + .map(assoc => + formatPolymorphicPopulate({ + assoc, + path: assoc.alias, + prefix: `${path}.`, + }) + ); + } + + return [assoc.alias, ...extraAssocs]; + }) + .reduce((acc, val) => acc.concat(val), []); +}; + +const formatPopulateOptions = (definition, withRelated) => { + if (!Array.isArray(withRelated)) withRelated = [withRelated]; + + const obj = withRelated.reduce((acc, key) => { + if (_.isString(key)) { + acc[key] = () => {}; + return acc; + } + + return _.extend(acc, key); + }, {}); + + // if components are no + const finalObj = Object.keys(obj).reduce((acc, key) => { + // check the key path and update it if necessary nothing more + const parts = key.split('.'); + + let newKey; + let prefix = ''; + let tmpModel = definition; + for (let part of parts) { + if (isComponent(tmpModel, part)) { + tmpModel = strapi.components[tmpModel.attributes[part].component]; + // add component path and there relations / images + const path = `${prefix}${part}.component`; + + newKey = path; + prefix = `${path}.`; + continue; + } + + const assoc = tmpModel.associations.find( + association => association.alias === part + ); + + if (!assoc) return acc; + + tmpModel = findModelByAssoc({ assoc }); + + if (isPolymorphic({ assoc })) { + const path = formatPolymorphicPopulate({ + assoc, + path: assoc.alias, + prefix, + }); + + return _.extend(acc, path); + } + + newKey = `${prefix}${part}`; + prefix = `${newKey}.`; + } + + acc[newKey] = obj[key]; + return acc; + }, {}); + + return [finalObj]; +}; + +const populateComponent = (key, attr) => { + if (attr.type === 'dynamiczone') return [`${key}.component`]; + + let paths = []; + const component = strapi.components[attr.component]; + + const assocs = (component.associations || []).filter( + assoc => assoc.autoPopulate === true + ); + + // paths.push(`${key}.component`); + assocs.forEach(assoc => { + if (isPolymorphic({ assoc })) { + const rel = formatPolymorphicPopulate({ + assoc, + path: assoc.alias, + prefix: `${key}.component.`, + }); + + paths.push(rel); + } else { + paths.push(`${key}.component.${assoc.alias}`); + } + }); + + return [`${key}.component`, ...paths]; +}; + +const createComponentsPopulate = definition => { + return getComponentAttributes(definition).reduce((acc, key) => { + const attribute = definition.attributes[key]; + const autoPopulate = _.get(attribute, ['autoPopulate'], true); + + if (autoPopulate === true) { + return acc.concat(populateComponent(key, attribute)); + } + return acc; + }, []); +}; + +const formatPolymorphicPopulate = ({ assoc, path, prefix = '' }) => { + if (_.isString(path) && path === assoc.via) { + return { [`related.${assoc.via}`]: () => {} }; + } else if (_.isString(path) && path === assoc.alias) { + // MorphTo side. + if (assoc.related) { + return { [`${prefix}${assoc.alias}.related`]: () => {} }; + } + + // oneToMorph or manyToMorph side. + // Retrieve collection name because we are using it to build our hidden model. + const model = findModelByAssoc({ assoc }); + + return { + [`${prefix}${assoc.alias}.${model.collectionName}`]: function(query) { + query.orderBy('created_at', 'desc'); + }, + }; + } +}; + +module.exports = populateFetch; diff --git a/packages/strapi-connector-bookshelf/lib/utils/associations.js b/packages/strapi-connector-bookshelf/lib/utils/associations.js new file mode 100644 index 0000000000..3f00b6848a --- /dev/null +++ b/packages/strapi-connector-bookshelf/lib/utils/associations.js @@ -0,0 +1,19 @@ +'use strict'; + +const findModelByAssoc = ({ assoc }) => { + const target = assoc.collection || assoc.model; + return assoc.plugin === 'admin' + ? strapi.admin.models[target] + : assoc.plugin + ? strapi.plugins[assoc.plugin].models[target] + : strapi.models[target]; +}; + +const isPolymorphic = ({ assoc }) => { + return assoc.nature.toLowerCase().indexOf('morph') !== -1; +}; + +module.exports = { + findModelByAssoc, + isPolymorphic, +}; diff --git a/packages/strapi-connector-bookshelf/lib/utils/attributes.js b/packages/strapi-connector-bookshelf/lib/utils/attributes.js index 1f7fdc42f6..17ac916bea 100644 --- a/packages/strapi-connector-bookshelf/lib/utils/attributes.js +++ b/packages/strapi-connector-bookshelf/lib/utils/attributes.js @@ -3,9 +3,9 @@ /** * Returns the attribute keys of the component related attributes */ -function getComponentAttributes(attributes) { - return Object.keys(attributes).filter(key => - ['component', 'dynamiczone'].includes(attributes[key].type) +function getComponentAttributes(definition) { + return Object.keys(definition.attributes).filter(key => + ['component', 'dynamiczone'].includes(definition.attributes[key].type) ); }