diff --git a/packages/strapi-generate-api/templates/mongoose/service.template b/packages/strapi-generate-api/templates/mongoose/service.template index b96c0fd9c0..7d5e8ac232 100644 --- a/packages/strapi-generate-api/templates/mongoose/service.template +++ b/packages/strapi-generate-api/templates/mongoose/service.template @@ -10,6 +10,144 @@ // Public dependencies. const _ = require('lodash'); +const buildTempFieldPath = field => { + return `__${field}`; +}; + +const restoreRealFieldPath = (field, prefix) => { + return `${prefix}${field}`; +}; + +export const generateLookupStage = ( + strapiModel, + { whitelistedPopulate = null, prefixPath = '' } = {} +) => { + const result = strapiModel.associations + .filter(ast => { + if (whitelistedPopulate) { + return _.includes(whitelistedPopulate, ast.alias); + } + return ast.autoPopulate; + }) + .reduce((acc, ast) => { + const model = ast.plugin + ? strapi.plugins[ast.plugin].models[ast.collection || ast.model] + : strapi.models[ast.collection || ast.model]; + + const from = model.collectionName; + const isDominantAssociation = ast.dominant || !!ast.model; + + const _localField = !isDominantAssociation + ? '_id' + : ast.via === strapiModel.collectionName || ast.via === 'related' + ? '_id' + : ast.alias; + + const localField = `${prefixPath}${_localField}`; + + const foreignField = ast.filter + ? `${ast.via}.ref` + : isDominantAssociation + ? ast.via === strapiModel.collectionName + ? ast.via + : '_id' + : ast.via === strapiModel.collectionName + ? '_id' + : ast.via; + + // Add the juncture like the `.populate()` function + const asTempPath = buildTempFieldPath(ast.alias, prefixPath); + const asRealPath = restoreRealFieldPath(ast.alias, prefixPath); + acc.push({ + $lookup: { + from, + localField, + foreignField, + as: asTempPath, + }, + }); + + // Unwind the relation's result if only one is expected + if (ast.type === 'model') { + acc.push({ + $unwind: { + path: `$${asTempPath}`, + preserveNullAndEmptyArrays: true, + }, + }); + } + + // Preserve relation field if it is empty + acc.push({ + $addFields: { + [asRealPath]: { + $ifNull: [`$${asTempPath}`, null], + }, + }, + }); + + // Remove temp field + acc.push({ + $project: { + [asTempPath]: 0, + }, + }); + + return acc; + }, []); + + return result; +}; + +export const generateMatchStage = ( + strapiModel, + filters, + { prefixPath = '' } = {} +) => { + const result = _.chain(filters) + .get('relations') + .reduce((acc, relationFilters, relationName) => { + const association = strapiModel.associations.find( + a => a.alias === relationName + ); + + // Ignore association if it's not been found + if (!association) { + return acc; + } + + const model = association.plugin + ? strapi.plugins[association.plugin].models[ + association.collection || association.model + ] + : strapi.models[association.collection || association.model]; + + _.forEach(relationFilters, (value, key) => { + if (key !== 'relations') { + acc.push({ + $match: { [`${prefixPath}${relationName}.${key}`]: value }, + }); + } else { + const nextPrefixedPath = `${prefixPath}${relationName}.`; + acc.push( + ...generateLookupStage(model, { + whitelistedPopulate: _.keys(value), + prefixPath: nextPrefixedPath, + }), + ...generateMatchStage(model, relationFilters, { + prefixPath: nextPrefixedPath, + }) + ); + } + }); + return acc; + }, []) + .value(); + + return result; +}; + + module.exports = { /** @@ -22,84 +160,17 @@ module.exports = { // Convert `params` object to filters compatible with Mongo. const filters = strapi.utils.models.convertParams('<%= globalID.toLowerCase() %>', params); - // Select field to populate. - const populate = <%= globalID %>.associations - .filter(ast => ast.autoPopulate) - .reduce((acc, ast) => { - // Strapi Model - const model = ast.plugin - ? strapi.plugins[ast.plugin].models[ast.collection || ast.model] - : strapi.models[ast.collection || ast.model]; + // Generate stages. + const populate = generateLookupStage(<%= globalID %>); + const match = generateMatchStage(<%= globalID %>, filters); - const from = model.collectionName; - const as = ast.alias; - const localField = ast.dominant ? '_id' : ast.via === <%= globalID %>.collectionName || ast.via === 'related' ? '_id' : ast.alias; - const foreignField = ast.filter ? `${ast.via}.ref` : - ast.dominant ? - (ast.via === <%= globalID %>.collectionName ? ast.via : '_id') : - (ast.via === <%= globalID %>.collectionName ? '_id' : ast.via); - - // Add the juncture like the `.populate()` function - acc.push({ - $lookup: { - from, - localField, - foreignField, - as, - } - }); - - // Unwind the relation's result if only one is expected - if (ast.type === 'model') { - acc.push({ - $unwind: { - path: `$${ast.alias}`, - preserveNullAndEmptyArrays: true - } - }); - } - - // Preserve relation field if it is empty - acc.push({ - $addFields: { - [ast.alias]: { - $ifNull: [`$${ast.alias}`, null] - } - } - }); - - // Filtrate the result depending of params - if (filters.relations) { - Object.keys(filters.relations).forEach( - (relationName) => { - if (ast.alias === relationName) { - const association = <%= globalID %>.associations.find(a => a.alias === relationName); - if (association) { - const relation = filters.relations[relationName]; - - Object.keys(relation).forEach( - (filter) => { - acc.push({ - $match: { [`${relationName}.${filter}`]: relation[filter] } - }); - } - ); - } - } - } - ); - } - - return acc; - }, []); - - const result = <%= globalID %> - .aggregate([ - { - $match: filters.where - }, - ...populate, - ]) + const result = <%= globalID %>.aggregate([ + { + $match: filters.where, // Direct relation filter + }, + ...populate, // Nested-Population + ...match, // Nested relation filter + ]) .skip(filters.start) .limit(filters.limit);