From fd54e71baf13bbd79b78d7ad2ad2e96d717262d0 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Tue, 30 Jul 2019 11:22:44 +0200 Subject: [PATCH] bookshelf clean populate --- .../api/article/services/Article.js | 7 +- .../strapi-hook-bookshelf/lib/mount-models.js | 356 +++++++++--------- packages/strapi-hook-bookshelf/lib/queries.js | 14 +- 3 files changed, 176 insertions(+), 201 deletions(-) diff --git a/examples/getstarted/api/article/services/Article.js b/examples/getstarted/api/article/services/Article.js index 237ec7688d..418622a9d2 100644 --- a/examples/getstarted/api/article/services/Article.js +++ b/examples/getstarted/api/article/services/Article.js @@ -7,11 +7,6 @@ module.exports = { find(params) { - // return strapi.query('article').find(params, { - // manyTags: () => {}, - // ['linkedTags.linkedArticles.pic']: () => {}, - // }); - - return strapi.query('article').find(params, ['ingredients']); + return strapi.query('article').find(params); }, }; diff --git a/packages/strapi-hook-bookshelf/lib/mount-models.js b/packages/strapi-hook-bookshelf/lib/mount-models.js index c4a2baeebc..2b731af3ef 100644 --- a/packages/strapi-hook-bookshelf/lib/mount-models.js +++ b/packages/strapi-hook-bookshelf/lib/mount-models.js @@ -461,8 +461,6 @@ module.exports = ({ models, target, plugin = false }, ctx) => { const groups = relations[key].toJSON().map(el => el.slice); attrs[key] = repeatable === true ? groups : _.first(groups) || null; - } else { - attrs[key] = repeatable === true ? [] : null; } }); @@ -523,6 +521,176 @@ 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 populateGroup = key => { + let paths = []; + const group = strapi.groups[definition.attributes[key].group]; + const assocs = (group.associations || []).filter( + assoc => assoc.autoPopulate === true + ); + + // paths.push(`${key}.slice`); + assocs.forEach(assoc => { + if (isPolymorphic({ assoc })) { + const rel = formatPolymorphicPopulate({ + assoc, + path: assoc.alias, + prefix: `${key}.slice.`, + }); + + paths.push(rel); + } else { + paths.push(`${key}.slice.${assoc.alias}`); + } + }); + + return paths; + }; + + const createGroupsPopulate = () => { + const groupsToPopulate = groupAttributes.reduce((acc, key) => { + const attribute = definition.attributes[key]; + const autoPopulate = _.get(attribute, ['autoPopulate'], true); + + if (autoPopulate === true) { + return acc.concat(populateGroup(key)); + } + return acc; + }, []); + + return groupsToPopulate; + }; + + const isGroup = (def, key) => + _.get(def, ['attributes', key, 'type']) === 'group'; + + 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 groups 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 (isGroup(tmpModel, part)) { + tmpModel = strapi.groups[tmpModel.attributes[part].group]; + // add group path and there relations / images + const path = `${prefix}${part}.slice`; + + 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() { const lifecycle = { @@ -546,201 +714,21 @@ module.exports = ({ models, target, plugin = false }, ctx) => { } }); - 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 addPolymorphicRelated = path => { - // const assoc = definition.associations.find( - // assoc => assoc.alias === path || assoc.via === path - // ); - - // if (assoc && isPolymorphic({ assoc })) { - // return formatPolymorphicPopulate({ - // assoc, - // path, - // }); - // } - - // 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 [path, ...extraAssocs]; - // }; - - function 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), []); - } - - function populateGroup(key) { - let paths = []; - const group = strapi.groups[definition.attributes[key].group]; - const assocs = (group.associations || []).filter( - assoc => assoc.autoPopulate === true - ); - - assocs.forEach(assoc => { - if (isPolymorphic({ assoc })) { - const rel = formatPolymorphicPopulate({ - assoc, - path: assoc.alias, - prefix: `${key}.slice.`, - }); - - paths.push(rel); - } else { - paths.push(`${key}.slice.${assoc.alias}`); - } - }); - - paths.push(`${key}.slice`); - - return paths; - } - - function createGroupsPopulate() { - const groupsToPopulate = groupAttributes.reduce((acc, key) => { - const attribute = definition.attributes[key]; - const autoPopulate = _.get(attribute, ['autoPopulate'], true); - - if (autoPopulate === true) { - return acc.concat(populateGroup(key)); - } - return acc; - }, []); - - return groupsToPopulate; - } - - const isGroup = key => groupAttributes.includes(key); - - function formatPopulateOptions(populate) { - // if groups are no - return populate - .reduce((acc, opt) => { - if (typeof opt === 'string') { - // split in parts and check if some parts of the path are morph or groups and update them - const parts = opt.split('.'); - - if (parts.length === 1) { - if (isGroup(opt)) { - // add group path and there relations / images - return acc.concat(populateGroup(opt)); - } - } - } - - if (typeof opt === 'object' && opt !== null) { - return acc.concat(opt); - } - - return acc; - }, []) - .reduce((acc, val) => acc.concat(val), []); - } - // 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(createGroupsPopulate()) .concat(createAssociationPopulate()); - } else if (Array.isArray(options.withRelated)) { + } else { options.withRelated = formatPopulateOptions(options.withRelated); - } else if (_.isObject(options.withRelated)) { - options.withRelated = formatPopulateOptions([options.withRelated]); } - // if (_.isArray(options.withRelated)) { - // options.withRelated = options.withRelated - // .concat(groupAttributes.map(key => `${key}.slice`)) - // .map(addPolymorphicRelated) - // .reduce((acc, paths) => acc.concat(paths), []); - // } else { - // options.withRelated = groupAttributes - // .map(key => `${key}.slice`) - // .map(addPolymorphicRelated) - // .reduce((acc, paths) => acc.concat(paths), []); - // } - }); - - this.on('fetching fetching:collection', () => { return _.isFunction(target[model.toLowerCase()]['beforeFetchAll']) ? target[model.toLowerCase()]['beforeFetchAll'] : Promise.resolve(); diff --git a/packages/strapi-hook-bookshelf/lib/queries.js b/packages/strapi-hook-bookshelf/lib/queries.js index 80205fa358..d3ab910086 100644 --- a/packages/strapi-hook-bookshelf/lib/queries.js +++ b/packages/strapi-hook-bookshelf/lib/queries.js @@ -19,11 +19,6 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) { return model.attributes[key].type === 'group'; }); - // default relations to populate - const defaultPopulate = model.associations - .filter(ast => ast.autoPopulate !== false) - .map(ast => ast.alias); - // Returns an object with relation keys only to create relations in DB const pickRelations = values => { return _.pick(values, assocKeys); @@ -58,7 +53,7 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) { } const entry = await model.forge(params).fetch({ - withRelated: _.isNil(populate) ? defaultPopulate : populate, + withRelated: populate, }); return entry ? entry.toJSON() : null; @@ -73,7 +68,7 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) { return model .query(buildQuery({ model, filters })) .fetchAll({ - withRelated: _.isNil(populate) ? defaultPopulate : populate, + withRelated: populate, transacting, }) .then(results => results.toJSON()); @@ -199,9 +194,6 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) { // Convert `params` object to filters compatible with Bookshelf. const filters = modelUtils.convertParams(modelKey, params); - // Select field to populate. - const withRelated = populate || defaultPopulate; - return model .query(qb => { buildSearchQuery(qb, model, params); @@ -219,7 +211,7 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) { } }) .fetchAll({ - withRelated, + withRelated: populate, }); }