diff --git a/packages/core/content-manager/server/services/entity-manager.js b/packages/core/content-manager/server/services/entity-manager.js index a72ecc89f4..694fdca5f9 100644 --- a/packages/core/content-manager/server/services/entity-manager.js +++ b/packages/core/content-manager/server/services/entity-manager.js @@ -62,40 +62,40 @@ module.exports = ({ strapi }) => ({ return assoc(`${CREATED_BY_ATTRIBUTE}.roles`, roles, entity); }, - find(opts, uid, populate) { - const params = { ...opts, populate: getDeepPopulate(uid, populate) }; + find(opts, uid) { + const params = { ...opts, populate: getDeepPopulate(uid) }; return strapi.entityService.findMany(uid, params); }, - findPage(opts, uid, populate) { - const params = { ...opts, populate: getDeepPopulate(uid, populate, { maxLevel: 1 }) }; + findPage(opts, uid) { + const params = { ...opts, populate: getDeepPopulate(uid, { maxLevel: 1 }) }; return strapi.entityService.findPage(uid, params); }, - findWithRelationCountsPage(opts, uid, populate) { - const counterPopulate = getDeepPopulate(uid, populate, { countMany: true, maxLevel: 1 }); + findWithRelationCountsPage(opts, uid) { + const counterPopulate = getDeepPopulate(uid, { countMany: true, maxLevel: 1 }); const params = { ...opts, populate: addCreatedByRolesPopulate(counterPopulate) }; return strapi.entityService.findWithRelationCountsPage(uid, params); }, - findOneWithCreatorRolesAndCount(id, uid, populate) { - const counterPopulate = getDeepPopulate(uid, populate, { countMany: true, countOne: true }); + findOneWithCreatorRolesAndCount(id, uid) { + const counterPopulate = getDeepPopulate(uid, { countMany: true, countOne: true }); const params = { populate: addCreatedByRolesPopulate(counterPopulate) }; return strapi.entityService.findOne(uid, id, params); }, - async findOne(id, uid, populate) { - const params = { populate: getDeepPopulate(uid, populate) }; + async findOne(id, uid) { + const params = { populate: getDeepPopulate(uid) }; return strapi.entityService.findOne(uid, id, params); }, - async findOneWithCreatorRoles(id, uid, populate) { - const entity = await this.findOne(id, uid, populate); + async findOneWithCreatorRoles(id, uid) { + const entity = await this.findOne(id, uid); if (!entity) { return entity; @@ -114,7 +114,7 @@ module.exports = ({ strapi }) => ({ const params = { data: publishData, - populate: getDeepPopulate(uid, null, { countMany: true, countOne: true }), + populate: getDeepPopulate(uid, { countMany: true, countOne: true }), }; return strapi.entityService.create(uid, params); @@ -125,14 +125,14 @@ module.exports = ({ strapi }) => ({ const params = { data: publishData, - populate: getDeepPopulate(uid, null, { countMany: true, countOne: true }), + populate: getDeepPopulate(uid, { countMany: true, countOne: true }), }; return strapi.entityService.update(uid, entity.id, params); }, delete(entity, uid) { - const params = { populate: getDeepPopulate(uid, null, { countMany: true, countOne: true }) }; + const params = { populate: getDeepPopulate(uid, { countMany: true, countOne: true }) }; return strapi.entityService.delete(uid, entity.id, params); }, @@ -161,7 +161,7 @@ module.exports = ({ strapi }) => ({ const params = { data, - populate: getDeepPopulate(uid, null, { countMany: true, countOne: true }), + populate: getDeepPopulate(uid, { countMany: true, countOne: true }), }; return strapi.entityService.update(uid, entity.id, params); @@ -176,7 +176,7 @@ module.exports = ({ strapi }) => ({ const params = { data, - populate: getDeepPopulate(uid, null, { countMany: true, countOne: true }), + populate: getDeepPopulate(uid, { countMany: true, countOne: true }), }; return strapi.entityService.update(uid, entity.id, params); diff --git a/packages/core/content-manager/server/services/utils/__tests__/populate.test.js b/packages/core/content-manager/server/services/utils/__tests__/populate.test.js new file mode 100644 index 0000000000..25539f63a0 --- /dev/null +++ b/packages/core/content-manager/server/services/utils/__tests__/populate.test.js @@ -0,0 +1,152 @@ +'use strict'; + +const { getDeepPopulate } = require('../populate'); + +describe('Populate', () => { + const fakeModels = { + empty: { + modelName: 'Fake empty model', + attributes: {}, + }, + component: { + modelName: 'Fake component model', + attributes: { + componentAttrName: { + type: 'component', + component: 'empty', + }, + }, + }, + dynZone: { + modelName: 'Fake dynamic zone model', + attributes: { + dynZoneAttrName: { + type: 'dynamiczone', + components: ['empty', 'component'], + }, + }, + }, + relationOTM: { + modelName: 'Fake relation oneToMany model', + attributes: { + relationAttrName: { + type: 'relation', + relation: 'oneToMany', + }, + }, + }, + relationOTO: { + modelName: 'Fake relation oneToOne model', + attributes: { + relationAttrName: { + type: 'relation', + relation: 'oneToOne', + }, + }, + }, + media: { + modelName: 'Fake media model', + attributes: { + mediaAttrName: { + type: 'media', + }, + }, + }, + }; + + describe('getDeepPopulate', () => { + beforeEach(() => { + global.strapi = { + getModel: jest.fn((uid) => fakeModels[uid]), + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('with empty model', async () => { + const uid = 'empty'; + + const result = getDeepPopulate(uid); + + expect(result).toEqual({}); + }); + + test('with component model', async () => { + const uid = 'component'; + + const result = getDeepPopulate(uid); + + expect(result).toEqual({ + componentAttrName: { populate: {} }, + }); + }); + + test('with dynamic zone model', async () => { + const uid = 'dynZone'; + + const result = getDeepPopulate(uid); + + expect(result).toEqual({ + dynZoneAttrName: { + populate: { + componentAttrName: { + populate: {}, + }, + }, + }, + }); + }); + + test('with relation model - oneToMany', async () => { + const uid = 'relationOTM'; + + const result = getDeepPopulate(uid); + + expect(result).toEqual({ + relationAttrName: true, + }); + }); + + test('with relation model - oneToMany - with countMany', async () => { + const uid = 'relationOTM'; + + const result = getDeepPopulate(uid, { countMany: true }); + + expect(result).toEqual({ + relationAttrName: { count: true }, + }); + }); + + test('with relation model - oneToOne', async () => { + const uid = 'relationOTO'; + + const result = getDeepPopulate(uid); + + expect(result).toEqual({ + relationAttrName: true, + }); + }); + + test('with relation model - oneToOne - with countOne', async () => { + const uid = 'relationOTO'; + + const result = getDeepPopulate(uid, { countOne: true }); + + expect(result).toEqual({ + relationAttrName: { count: true }, + }); + }); + + test('with media model', async () => { + const uid = 'media'; + + const result = getDeepPopulate(uid); + + expect(result).toEqual({ + mediaAttrName: { populate: 'folder' }, + }); + }); + }); +}); diff --git a/packages/core/content-manager/server/services/utils/populate.js b/packages/core/content-manager/server/services/utils/populate.js index aaf029b9d7..4dab666d28 100644 --- a/packages/core/content-manager/server/services/utils/populate.js +++ b/packages/core/content-manager/server/services/utils/populate.js @@ -7,70 +7,123 @@ const { hasDraftAndPublish, isVisibleAttribute } = strapiUtils.contentTypes; const { isAnyToMany } = strapiUtils.relations; const { PUBLISHED_AT_ATTRIBUTE } = strapiUtils.contentTypes.constants; +/** + * Populate the model for relation + * @param {Object} attribute - Attribute containing a relation + * @param {String} attribute.relation - type of relation + * @param model - Model of the populated entity + * @param attributeName + * @param {Object} options - Options to apply while populating + * @param {Boolean} options.countMany + * @param {Boolean} options.countOne + * @returns {true|{count: true}} + */ +function getPopulateForRelation(attribute, model, attributeName, { countMany, countOne }) { + const isManyRelation = isAnyToMany(attribute); + + // always populate createdBy, updatedBy, localizations etc. + if (!isVisibleAttribute(model, attributeName)) { + return true; + } + if ((isManyRelation && countMany) || (!isManyRelation && countOne)) { + return { count: true }; + } + + return true; +} + +/** + * Populate the model for Dynamic Zone components + * @param {Object} attribute - Attribute containing the components + * @param {String[]} attribute.components - IDs of components + * @param {Object} options - Options to apply while populating + * @param {Boolean} options.countMany + * @param {Boolean} options.countOne + * @param {Number} options.maxLevel + * @param {Number} level + * @returns {{populate: Object}} + */ +function getPopulateForDZ(attribute, options, level) { + const populatedComponents = (attribute.components || []).map((componentUID) => + getDeepPopulate(componentUID, options, level + 1) + ); + + return { populate: populatedComponents.reduce(merge, {}) }; +} + +/** + * Get the populated value based on the type of the attribute + * @param {String} attributeName - Name of the attribute + * @param {Object} model - Model of the populated entity + * @param {Object} model.attributes + * @param {Object} options - Options to apply while populating + * @param {Boolean} options.countMany + * @param {Boolean} options.countOne + * @param {Number} options.maxLevel + * @param {Number} level + * @returns {Object} + */ +function getPopulateFor(attributeName, model, options, level) { + const attribute = model.attributes[attributeName]; + + switch (attribute.type) { + case 'relation': + return { + [attributeName]: getPopulateForRelation(attribute, model, attributeName, options), + }; + case 'component': + return { + [attributeName]: { + populate: getDeepPopulate(attribute.component, options, level + 1), + }, + }; + case 'media': + return { + [attributeName]: { populate: 'folder' }, + }; + case 'dynamiczone': + return { + [attributeName]: getPopulateForDZ(attribute, options, level), + }; + default: + return {}; + } +} + +/** + * Deeply populate a model based on UID + * @param {String} uid - Unique identifier of the model + * @param {Object} [options] - Options to apply while populating + * @param {Boolean} [options.countMany=false] + * @param {Boolean} [options.countOne=false] + * @param {Number} [options.maxLevel=Infinity] + * @param {Number} [level=1] - Current level of nested call + * @returns {Object} + */ const getDeepPopulate = ( uid, - populate, { countMany = false, countOne = false, maxLevel = Infinity } = {}, level = 1 ) => { - if (populate) { - return populate; - } - if (level > maxLevel) { return {}; } const model = strapi.getModel(uid); - return Object.keys(model.attributes).reduce((populateAcc, attributeName) => { - const attribute = model.attributes[attributeName]; - - if (attribute.type === 'relation') { - const isManyRelation = isAnyToMany(attribute); - // always populate createdBy, updatedBy, localizations etc. - if (!isVisibleAttribute(model, attributeName)) { - populateAcc[attributeName] = true; - } else if ((isManyRelation && countMany) || (!isManyRelation && countOne)) { - populateAcc[attributeName] = { count: true }; - } else { - populateAcc[attributeName] = true; - } - } - - if (attribute.type === 'component') { - populateAcc[attributeName] = { - populate: getDeepPopulate( - attribute.component, - null, - { countOne, countMany, maxLevel }, - level + 1 - ), - }; - } - - if (attribute.type === 'media') { - populateAcc[attributeName] = { populate: 'folder' }; - } - - if (attribute.type === 'dynamiczone') { - populateAcc[attributeName] = { - populate: (attribute.components || []).reduce((acc, componentUID) => { - return merge( - acc, - getDeepPopulate(componentUID, null, { countOne, countMany, maxLevel }, level + 1) - ); - }, {}), - }; - } - - return populateAcc; - }, {}); + return Object.keys(model.attributes).reduce( + (populateAcc, attributeName) => + merge( + populateAcc, + getPopulateFor(attributeName, model, { countMany, countOne, maxLevel }, level) + ), + {} + ); }; /** * getDeepPopulateDraftCount works recursively on the attributes of a model - * creating a populate object to count all the unpublished relations within the model + * creating a populated object to count all the unpublished relations within the model * These relations can be direct to this content type or contained within components/dynamic zones * @param {String} uid of the model * @returns {Object} result