Merge pull request #14977 from strapi/refactor/deep-populate-entity-manager

refactor(entity-manager): getDeepPopulate
This commit is contained in:
Nathan Pichon 2022-12-08 09:57:28 +01:00 committed by GitHub
commit ed5c21e5c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 271 additions and 66 deletions

View File

@ -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);

View File

@ -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' },
});
});
});
});

View File

@ -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