From 72c7203e1842f3f7f33f30b15724ea2b84fd83d3 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Mon, 29 Jul 2019 18:00:01 +0200 Subject: [PATCH] Init populate cleanup --- .../getstarted/api/article/models/Article.js | 11 - .../api/article/models/Article.settings.json | 6 +- .../api/article/services/Article.js | 11 +- examples/getstarted/api/tag/services/Tag.js | 6 +- .../strapi-hook-bookshelf/lib/mount-models.js | 224 +++++++++++++----- packages/strapi-hook-bookshelf/lib/queries.js | 7 +- 6 files changed, 190 insertions(+), 75 deletions(-) diff --git a/examples/getstarted/api/article/models/Article.js b/examples/getstarted/api/article/models/Article.js index 9a690533ea..11b47b89e9 100644 --- a/examples/getstarted/api/article/models/Article.js +++ b/examples/getstarted/api/article/models/Article.js @@ -8,47 +8,36 @@ module.exports = { // Before saving a value. // Fired before an `insert` or `update` query. // beforeSave: async (model, attrs, options) => {}, - // After saving a value. // Fired after an `insert` or `update` query. // afterSave: async (model, response, options) => {}, - // Before fetching a value. // Fired before a `fetch` operation. // beforeFetch: async (model, columns, options) => {}, - // After fetching a value. // Fired after a `fetch` operation. // afterFetch: async (model, response, options) => {}, - // Before fetching all values. // Fired before a `fetchAll` operation. // beforeFetchAll: async (model, columns, options) => {}, - // After fetching all values. // Fired after a `fetchAll` operation. // afterFetchAll: async (model, response, options) => {}, - // Before creating a value. // Fired before an `insert` query. // beforeCreate: async (model, attrs, options) => {}, - // After creating a value. // Fired after an `insert` query. // afterCreate: async (model, attrs, options) => {}, - // Before updating a value. // Fired before an `update` query. // beforeUpdate: async (model, attrs, options) => {}, - // After updating a value. // Fired after an `update` query. // afterUpdate: async (model, attrs, options) => {}, - // Before destroying a value. // Fired before a `delete` query. // beforeDestroy: async (model, attrs, options) => {}, - // After destroying a value. // Fired after a `delete` query. // afterDestroy: async (model, attrs, options) => {} diff --git a/examples/getstarted/api/article/models/Article.settings.json b/examples/getstarted/api/article/models/Article.settings.json index 2df7be7631..956c779006 100644 --- a/examples/getstarted/api/article/models/Article.settings.json +++ b/examples/getstarted/api/article/models/Article.settings.json @@ -31,10 +31,7 @@ }, "enum": { "type": "enumeration", - "enum": [ - "morning,", - "noon" - ] + "enum": ["morning,", "noon"] }, "bool": { "type": "boolean" @@ -61,7 +58,6 @@ "via": "linkedArticles" }, "fb_cta": { - "required": true, "type": "group", "group": "cta_facebook", "repeatable": false diff --git a/examples/getstarted/api/article/services/Article.js b/examples/getstarted/api/article/services/Article.js index 8564ba30ea..237ec7688d 100644 --- a/examples/getstarted/api/article/services/Article.js +++ b/examples/getstarted/api/article/services/Article.js @@ -5,4 +5,13 @@ * to customize this service */ -module.exports = {}; +module.exports = { + find(params) { + // return strapi.query('article').find(params, { + // manyTags: () => {}, + // ['linkedTags.linkedArticles.pic']: () => {}, + // }); + + return strapi.query('article').find(params, ['ingredients']); + }, +}; diff --git a/examples/getstarted/api/tag/services/Tag.js b/examples/getstarted/api/tag/services/Tag.js index 8564ba30ea..3840fb4639 100644 --- a/examples/getstarted/api/tag/services/Tag.js +++ b/examples/getstarted/api/tag/services/Tag.js @@ -5,4 +5,8 @@ * to customize this service */ -module.exports = {}; +module.exports = { + find(params) { + return strapi.query('tag').find(params, null); + }, +}; diff --git a/packages/strapi-hook-bookshelf/lib/mount-models.js b/packages/strapi-hook-bookshelf/lib/mount-models.js index 2cce0a026b..c4a2baeebc 100644 --- a/packages/strapi-hook-bookshelf/lib/mount-models.js +++ b/packages/strapi-hook-bookshelf/lib/mount-models.js @@ -436,6 +436,15 @@ module.exports = ({ models, target, plugin = false }, ctx) => { }); }; + // Extract association except polymorphic. + const associations = definition.associations.filter( + association => association.nature.toLowerCase().indexOf('morph') === -1 + ); + // Extract polymorphic association. + const polymorphicAssociations = definition.associations.filter( + association => association.nature.toLowerCase().indexOf('morph') !== -1 + ); + // Update serialize to reformat data for polymorphic associations. loadedModel.serialize = function(options) { const attrs = _.clone(this.attributes); @@ -447,27 +456,16 @@ module.exports = ({ models, target, plugin = false }, ctx) => { const relations = this.relations; groupAttributes.forEach(key => { + const { repeatable } = definition.attributes[key]; if (relations[key]) { const groups = relations[key].toJSON().map(el => el.slice); - attrs[key] = - definition.attributes[key].repeatable === true - ? groups - : _.first(groups) || null; + attrs[key] = repeatable === true ? groups : _.first(groups) || null; + } else { + attrs[key] = repeatable === true ? [] : null; } }); - // Extract association except polymorphic. - const associations = definition.associations.filter( - association => - association.nature.toLowerCase().indexOf('morph') === -1 - ); - // Extract polymorphic association. - const polymorphicAssociations = definition.associations.filter( - association => - association.nature.toLowerCase().indexOf('morph') !== -1 - ); - polymorphicAssociations.map(association => { // Retrieve relation Bookshelf object. const relation = relations[association.alias]; @@ -489,7 +487,7 @@ module.exports = ({ models, target, plugin = false }, ctx) => { switch (association.nature) { case 'oneToManyMorph': attrs[association.alias] = - attrs[association.alias][model.collectionName]; + attrs[association.alias][model.collectionName] || null; break; case 'manyToManyMorph': attrs[association.alias] = attrs[association.alias].map( @@ -497,7 +495,8 @@ module.exports = ({ models, target, plugin = false }, ctx) => { ); break; case 'oneMorphToOne': - attrs[association.alias] = attrs[association.alias].related; + attrs[association.alias] = + attrs[association.alias].related || null; break; case 'manyMorphToOne': case 'manyMorphToMany': @@ -548,11 +547,12 @@ module.exports = ({ models, target, plugin = false }, ctx) => { }); const findModelByAssoc = ({ assoc }) => { - return assoc.plugin - ? strapi.plugins[assoc.plugin].models[ - assoc.collection || assoc.model - ] - : strapi.models[assoc.collection || assoc.model]; + 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 }) => { @@ -582,51 +582,165 @@ module.exports = ({ models, target, plugin = false }, ctx) => { } }; - const addPolymorphicRelated = path => { - const assoc = definition.associations.find( - assoc => assoc.alias === path || assoc.via === path - ); + // const addPolymorphicRelated = path => { + // const assoc = definition.associations.find( + // assoc => assoc.alias === path || assoc.via === path + // ); - if (assoc && isPolymorphic({ assoc })) { - return formatPolymorphicPopulate({ - assoc, - path, - }); - } + // if (assoc && isPolymorphic({ assoc })) { + // return formatPolymorphicPopulate({ + // assoc, + // path, + // }); + // } - let extraAssocs = []; - if (assoc) { - const assocModel = findModelByAssoc({ assoc }); + // let extraAssocs = []; + // if (assoc) { + // const assocModel = findModelByAssoc({ assoc }); - extraAssocs = assocModel.associations - .filter(assoc => isPolymorphic({ assoc })) - .map(assoc => - formatPolymorphicPopulate({ + // 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, - prefix: `${path}.`, - }) - ); - } + }); + } - return [path, ...extraAssocs]; - }; + 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) => { - 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), []); + // do not populate anything + if (options.withRelated === false) return; + + if (_.isNil(options.withRelated)) { + options.withRelated = [] + .concat(createGroupsPopulate()) + .concat(createAssociationPopulate()); + } else if (Array.isArray(options.withRelated)) { + 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 e08e69959d..80205fa358 100644 --- a/packages/strapi-hook-bookshelf/lib/queries.js +++ b/packages/strapi-hook-bookshelf/lib/queries.js @@ -58,7 +58,7 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) { } const entry = await model.forge(params).fetch({ - withRelated: populate || defaultPopulate, + withRelated: _.isNil(populate) ? defaultPopulate : populate, }); return entry ? entry.toJSON() : null; @@ -72,7 +72,10 @@ module.exports = function createQueryBuilder({ model, modelKey, strapi }) { return model .query(buildQuery({ model, filters })) - .fetchAll({ withRelated: populate || defaultPopulate, transacting }) + .fetchAll({ + withRelated: _.isNil(populate) ? defaultPopulate : populate, + transacting, + }) .then(results => results.toJSON()); }