Merge pull request #14244 from strapi/relations-main-view/get-entity-with-count

Relations main view/get entity with count
This commit is contained in:
Pierre Noël 2022-09-06 17:07:16 +02:00 committed by GitHub
commit ba401b93f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 126 additions and 103 deletions

View File

@ -46,7 +46,7 @@ module.exports = {
return ctx.forbidden();
}
const entity = await entityManager.findOneWithCreatorRoles(id, model);
const entity = await entityManager.findOneWithCreatorRolesAndCount(id, model);
if (!entity) {
return ctx.notFound();

View File

@ -39,24 +39,44 @@ const findCreatorRoles = (entity) => {
return [];
};
// TODO: define when we use this one vs basic populate
const getDeepPopulate = (uid, populate) => {
const getDeepPopulate = (
uid,
populate,
{ onlyMany = false, countMany = false, maxLevel = Infinity } = {},
level = 1
) => {
if (populate) {
return populate;
}
const { attributes } = strapi.getModel(uid);
if (level > maxLevel) {
return {};
}
return Object.keys(attributes).reduce((populateAcc, attributeName) => {
const attribute = attributes[attributeName];
const model = strapi.getModel(uid);
return Object.keys(model.attributes).reduce((populateAcc, attributeName) => {
const attribute = model.attributes[attributeName];
if (attribute.type === 'relation') {
populateAcc[attributeName] = true; // Only populate first level of relations
const isManyRelation = MANY_RELATIONS.includes(attribute.relation);
// always populate createdBy, updatedBy, localizations etc.
if (!isVisibleAttribute(model, attributeName)) {
populateAcc[attributeName] = true;
} else if (!onlyMany || isManyRelation) {
// Only populate one level of relations
populateAcc[attributeName] = countMany && isManyRelation ? { count: true } : true;
}
}
if (attribute.type === 'component') {
populateAcc[attributeName] = {
populate: getDeepPopulate(attribute.component, null),
populate: getDeepPopulate(
attribute.component,
null,
{ onlyMany, countMany, maxLevel },
level + 1
),
};
}
@ -67,7 +87,10 @@ const getDeepPopulate = (uid, populate) => {
if (attribute.type === 'dynamiczone') {
populateAcc[attributeName] = {
populate: (attribute.components || []).reduce((acc, componentUID) => {
return merge(acc, getDeepPopulate(componentUID, null));
return merge(
acc,
getDeepPopulate(componentUID, null, { onlyMany, countMany, maxLevel }, level + 1)
);
}, {}),
};
}
@ -76,39 +99,6 @@ const getDeepPopulate = (uid, populate) => {
}, {});
};
// TODO: define when we use this one vs deep populate
const getBasePopulate = (uid, populate) => {
if (populate) {
return populate;
}
const { attributes } = strapi.getModel(uid);
return Object.keys(attributes).filter((attributeName) => {
return ['relation', 'component', 'dynamiczone', 'media'].includes(
attributes[attributeName].type
);
});
};
const getCounterPopulate = (uid, populate) => {
const basePopulate = getBasePopulate(uid, populate);
const model = strapi.getModel(uid);
return basePopulate.reduce((populate, attributeName) => {
const attribute = model.attributes[attributeName];
if (MANY_RELATIONS.includes(attribute.relation) && isVisibleAttribute(model, attributeName)) {
populate[attributeName] = { count: true };
} else {
populate[attributeName] = true;
}
return populate;
}, {});
};
const addCreatedByRolesPopulate = (populate) => {
return {
...populate,
@ -138,18 +128,25 @@ module.exports = ({ strapi }) => ({
},
findPage(opts, uid, populate) {
const params = { ...opts, populate: getBasePopulate(uid, populate) };
const params = { ...opts, populate: getDeepPopulate(uid, populate, { maxLevel: 1 }) };
return strapi.entityService.findPage(uid, params);
},
findWithRelationCountsPage(opts, uid, populate) {
const counterPopulate = addCreatedByRolesPopulate(getCounterPopulate(uid, populate));
const params = { ...opts, populate: counterPopulate };
const counterPopulate = getDeepPopulate(uid, populate, { 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, { onlyMany: true, countMany: true });
const params = { populate: addCreatedByRolesPopulate(counterPopulate) };
return strapi.entityService.findOne(uid, id, params);
},
async findOne(id, uid, populate) {
const params = { populate: getDeepPopulate(uid, populate) };

View File

@ -133,61 +133,95 @@ describe('CM API', () => {
await builder.cleanup();
});
describe('Count relations', () => {
describe('Automatic count and populate relations', () => {
test('many-way', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
qs: {
sort: 'name:ASC',
},
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(getCollectorByName(res.body.results, 'Bernard').stamps.count).toBe(2);
expect(getCollectorByName(res.body.results, 'Isabelle').stamps.count).toBe(1);
expect(getCollectorByName(res.body.results, 'Emma').stamps.count).toBe(0);
// all relations are populated and xToMany relations are counted
expect(res.body.results).toMatchObject([
{
age: 25,
createdBy: null,
id: 1,
name: 'Bernard',
stamps: { count: 2 },
stamps_m2m: { count: 1 },
stamps_one_many: { count: 0 },
stamps_one_one: {
id: 1,
name: '1946',
},
stamps_one_way: {
id: 1,
name: '1946',
},
updatedBy: null,
},
{
age: 23,
createdBy: null,
id: 3,
name: 'Emma',
stamps: { count: 0 },
stamps_m2m: { count: 2 },
stamps_one_many: { count: 1 },
stamps_one_one: {
id: 3,
name: '1948',
},
stamps_one_way: {
id: 3,
name: '1948',
},
updatedBy: null,
},
{
age: 55,
createdBy: null,
id: 2,
name: 'Isabelle',
stamps: { count: 1 },
stamps_m2m: { count: 0 },
stamps_one_many: { count: 2 },
stamps_one_one: {
id: 2,
name: '1947',
},
stamps_one_way: {
id: 2,
name: '1947',
},
updatedBy: null,
},
]);
});
test('many-to-many (collector -> stamps)', async () => {
test('findOne', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
url: `/content-manager/collection-types/api::collector.collector/${data.collectors[0].id}`,
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(getCollectorByName(res.body.results, 'Bernard').stamps_m2m.count).toBe(1);
expect(getCollectorByName(res.body.results, 'Isabelle').stamps_m2m.count).toBe(0);
expect(getCollectorByName(res.body.results, 'Emma').stamps_m2m.count).toBe(2);
});
test('many-to-many (stamp -> collectors)', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::stamp.stamp',
// only xToMany relations are populated and counted
expect(res.body).toMatchObject({
age: 25,
id: 1,
name: 'Bernard',
stamps: { count: 2 },
stamps_m2m: { count: 1 },
stamps_one_many: { count: 0 },
createdBy: null,
updatedBy: null,
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(getStampByName(res.body.results, '1946').collectors.count).toBe(2);
expect(getStampByName(res.body.results, '1947').collectors.count).toBe(1);
expect(getStampByName(res.body.results, '1948').collectors.count).toBe(0);
});
test('one-to-many', async () => {
const res = await rq({
method: 'GET',
url: '/content-manager/collection-types/api::collector.collector',
});
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body.results)).toBe(true);
expect(res.body.results).toHaveLength(3);
expect(getCollectorByName(res.body.results, 'Bernard').stamps_one_many.count).toBe(0);
expect(getCollectorByName(res.body.results, 'Isabelle').stamps_one_many.count).toBe(2);
expect(getCollectorByName(res.body.results, 'Emma').stamps_one_many.count).toBe(1);
});
});

View File

@ -38,7 +38,7 @@ const deleteFixtures = async () => {
}
};
describe('Content Manager End to End', () => {
describe('Relations', () => {
beforeAll(async () => {
await builder
.addContentTypes(
@ -410,8 +410,7 @@ describe('Content Manager End to End', () => {
method: 'GET',
});
expect(Array.isArray(foundTag.articles)).toBeTruthy();
expect(foundTag.articles.length).toBe(2);
expect(foundTag.articles.count).toBe(2);
await rq({
url: '/content-manager/collection-types/api::article.article/actions/bulkDelete',
@ -426,8 +425,7 @@ describe('Content Manager End to End', () => {
method: 'GET',
});
expect(Array.isArray(foundTag2.articles)).toBeTruthy();
expect(foundTag2.articles.length).toBe(0);
expect(foundTag2.articles.count).toBe(0);
});
});
@ -750,7 +748,8 @@ describe('Content Manager End to End', () => {
});
});
test('Get article1 with cat3', async () => {
// TODO RELATIONS: reimplement following tests
test.skip('Get article1 with cat3', async () => {
const { body } = await rq({
url: `/content-manager/collection-types/api::article.article/${data.articles[0].id}/category`,
method: 'GET',
@ -761,7 +760,7 @@ describe('Content Manager End to End', () => {
});
});
test('Get article2 with cat2', async () => {
test.skip('Get article2 with cat2', async () => {
const { body } = await rq({
url: `/content-manager/collection-types/api::article.article/${data.articles[1].id}/category`,
method: 'GET',
@ -772,7 +771,7 @@ describe('Content Manager End to End', () => {
});
});
test('Get cat1 without relations', async () => {
test.skip('Get cat1 without relations', async () => {
const { body } = await rq({
url: `/content-manager/collection-types/api::category.category/${data.categories[0].id}/articles`,
method: 'GET',
@ -789,7 +788,7 @@ describe('Content Manager End to End', () => {
});
});
test('Get cat2 with article2', async () => {
test.skip('Get cat2 with article2', async () => {
const { body } = await rq({
url: `/content-manager/collection-types/api::category.category/${data.categories[1].id}/articles`,
method: 'GET',
@ -801,7 +800,7 @@ describe('Content Manager End to End', () => {
});
});
test('Get cat3 with article1', async () => {
test.skip('Get cat3 with article1', async () => {
const { body } = await rq({
url: `/content-manager/collection-types/api::category.category/${data.categories[2].id}/articles`,
method: 'GET',

View File

@ -84,12 +84,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
const query = transformParamsToQuery(uid, wrappedParams);
const { results, pagination } = await db.query(uid).findPage(query);
return {
results,
pagination,
};
return db.query(uid).findPage(query);
},
async findWithRelationCounts(uid, opts) {
@ -97,9 +92,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator })
const query = transformParamsToQuery(uid, wrappedParams);
const results = await db.query(uid).findMany(query);
return results;
return db.query(uid).findMany(query);
},
async findOne(uid, entityId, opts) {