diff --git a/packages/core/content-manager/server/tests/api/number-of-draft-relations.test.e2e.js b/packages/core/content-manager/server/tests/api/number-of-draft-relations.test.e2e.js index 384d3366cc..90f08f2c87 100644 --- a/packages/core/content-manager/server/tests/api/number-of-draft-relations.test.e2e.js +++ b/packages/core/content-manager/server/tests/api/number-of-draft-relations.test.e2e.js @@ -7,8 +7,15 @@ const { createAuthRequest } = require('../../../../../../test/helpers/request'); const builder = createTestBuilder(); let strapi; let rq; +const data = { + categories: [], + categoriesdp: { + published: [], + draft: [], + }, +}; -const product = { +const productModel = { displayName: 'Product', singularName: 'product', pluralName: 'products', @@ -25,6 +32,12 @@ const product = { target: 'api::categorydp.categorydp', targetAttribute: 'product', }, + onecategorydp: { + type: 'relation', + relation: 'oneToOne', + target: 'api::categorydp.categorydp', + targetAttribute: 'oneproduct', + }, categories: { type: 'relation', relation: 'oneToMany', @@ -38,6 +51,7 @@ const product = { comporep: { component: 'default.compo', type: 'component', + repeatable: true, }, dz: { components: ['default.compo'], @@ -46,7 +60,7 @@ const product = { }, }; -const categoryDP = { +const categoryDPModel = { displayName: 'Category Draft & Publish', singularName: 'categorydp', pluralName: 'categoriesdp', @@ -54,41 +68,41 @@ const categoryDP = { attributes: { name: { type: 'string', - unique: true, }, }, }; -const category = { +const categoryModel = { displayName: 'Category', singularName: 'category', pluralName: 'categories', attributes: { name: { type: 'string', - unique: true, }, }, }; -const compo = { +const compoModel = { displayName: 'compo', attributes: { name: { type: 'string', - required: true, }, categoriesdp: { type: 'relation', relation: 'oneToMany', target: 'api::categorydp.categorydp', - targetAttribute: 'product', }, categories: { type: 'relation', relation: 'oneToMany', target: 'api::category.category', - targetAttribute: 'product', + }, + onecategorydp: { + type: 'relation', + relation: 'oneToOne', + target: 'api::categorydp.categorydp', }, }, }; @@ -96,13 +110,45 @@ const compo = { describe('CM API - Basic', () => { beforeAll(async () => { await builder - .addContentTypes([categoryDP, category]) - .addComponent(compo) - .addContentTypes([product]) + .addContentTypes([categoryDPModel, categoryModel]) + .addComponent(compoModel) + .addContentTypes([productModel]) .build(); strapi = await createStrapiInstance(); rq = await createAuthRequest({ strapi }); + + const { body: category } = await rq({ + method: 'POST', + url: '/content-manager/collection-types/api::category.category', + body: { name: 'Food' }, + }); + data.categories.push(category); + + const { body: categoryPublished } = await rq({ + method: 'POST', + url: '/content-manager/collection-types/api::categorydp.categorydp', + body: { name: 'Food' }, + }); + await rq({ + method: 'POST', + url: `/content-manager/collection-types/api::categorydp.categorydp/${categoryPublished.id}/actions/publish`, + }); + data.categoriesdp.published.push(categoryPublished); + + const { body: categoryDraft1 } = await rq({ + method: 'POST', + url: '/content-manager/collection-types/api::categorydp.categorydp', + body: { name: 'Food' }, + }); + data.categoriesdp.draft.push(categoryDraft1); + + const { body: categoryDraft2 } = await rq({ + method: 'POST', + url: '/content-manager/collection-types/api::categorydp.categorydp', + body: { name: 'Food' }, + }); + data.categoriesdp.draft.push(categoryDraft2); }); afterAll(async () => { @@ -111,11 +157,40 @@ describe('CM API - Basic', () => { }); test('Return 0 when no relations are set', async () => { + const { body: product } = await rq({ + method: 'POST', + url: '/content-manager/collection-types/api::product.product', + body: { name: 'Pizza' }, + }); + + const { body } = await rq({ + method: 'GET', + url: `/content-manager/collection-types/api::product.product/${product.id}/actions/numberOfDraftRelations`, + }); + + expect(body.data).toBe(0); + }); + + test('Return 0 when only relations without d&p are set', async () => { const { body: product } = await rq({ method: 'POST', url: '/content-manager/collection-types/api::product.product', body: { name: 'Pizza', + onecategorydp: data.categories[0].id, + categories: [data.categories[0].id], + compo: { + onecategorydp: data.categories[0].id, + categories: [data.categories[0].id], + }, + comporep: [{ categories: [data.categories[0].id], onecategorydp: data.categories[0].id }], + dz: [ + { + __component: 'default.compo', + categories: [data.categories[0].id], + onecategorydp: data.categories[0].id, + }, + ], }, }); @@ -126,4 +201,159 @@ describe('CM API - Basic', () => { expect(body.data).toBe(0); }); + + test('Return 0 when relations without d&p are set & published relations only', async () => { + const categoryId = data.categories[0].id; + const publishedId = data.categoriesdp.published[0].id; + + const { body: product } = await rq({ + method: 'POST', + url: '/content-manager/collection-types/api::product.product', + body: { + name: 'Pizza', + onecategorydp: publishedId, + categories: [categoryId], + categoriesdp: [publishedId], + compo: { + onecategorydp: publishedId, + categories: [categoryId], + categoriesdp: [publishedId], + }, + comporep: [ + { onecategorydp: publishedId, categories: [categoryId], categoriesdp: [publishedId] }, + ], + dz: [ + { + __component: 'default.compo', + onecategorydp: publishedId, + categories: [categoryId], + categoriesdp: [publishedId], + }, + ], + }, + }); + + const { body } = await rq({ + method: 'GET', + url: `/content-manager/collection-types/api::product.product/${product.id}/actions/numberOfDraftRelations`, + }); + + expect(body.data).toBe(0); + }); + + test('Return 8 when there are 8 drafts (1 xToOne & 1 xToMany on ct, compo, comporep, dz)', async () => { + const categoryId = data.categories[0].id; + const draftId = data.categoriesdp.draft[0].id; + + const { body: product } = await rq({ + method: 'POST', + url: '/content-manager/collection-types/api::product.product', + body: { + name: 'Pizza', + onecategorydp: draftId, + categories: [categoryId], + categoriesdp: [draftId], + compo: { + onecategorydp: draftId, + categories: [categoryId], + categoriesdp: [draftId], + }, + comporep: [{ onecategorydp: draftId, categories: [categoryId], categoriesdp: [draftId] }], + dz: [ + { + __component: 'default.compo', + onecategorydp: draftId, + categories: [categoryId], + categoriesdp: [draftId], + }, + ], + }, + }); + + const { body } = await rq({ + method: 'GET', + url: `/content-manager/collection-types/api::product.product/${product.id}/actions/numberOfDraftRelations`, + }); + + expect(body.data).toBe(8); + }); + + test('Return 8 when there are 8 drafts (1 xToOne & 1/2 xToMany on ct, compo, comporep, dz)', async () => { + const categoryId = data.categories[0].id; + const draftId = data.categoriesdp.draft[0].id; + + const { body: product } = await rq({ + method: 'POST', + url: '/content-manager/collection-types/api::product.product', + body: { + name: 'Pizza', + onecategorydp: draftId, + categories: [categoryId], + categoriesdp: [draftId, categoryId], + compo: { + onecategorydp: draftId, + categories: [categoryId], + categoriesdp: [draftId, categoryId], + }, + comporep: [ + { onecategorydp: draftId, categories: [categoryId], categoriesdp: [draftId, categoryId] }, + ], + dz: [ + { + onecategorydp: draftId, + __component: 'default.compo', + categories: [categoryId], + categoriesdp: [draftId, categoryId], + }, + ], + }, + }); + + const { body } = await rq({ + method: 'GET', + url: `/content-manager/collection-types/api::product.product/${product.id}/actions/numberOfDraftRelations`, + }); + + expect(body.data).toBe(8); + }); + + test('Return 12 when there are 12 drafts (1 xToOne & 2 xToMany on ct, compo, comporep, dz)', async () => { + const categoryId = data.categories[0].id; + const draft1Id = data.categoriesdp.draft[0].id; + const draft2Id = data.categoriesdp.draft[1].id; + + const { body: product } = await rq({ + method: 'POST', + url: '/content-manager/collection-types/api::product.product', + body: { + name: 'Pizza', + onecategorydp: draft1Id, + categories: [categoryId], + categoriesdp: [draft1Id, draft2Id], + compo: { + onecategorydp: draft1Id, + categories: [categoryId], + categoriesdp: [draft1Id, draft2Id], + }, + comporep: [ + { onecategorydp: draft1Id, categories: [categoryId], categoriesdp: [draft1Id, draft2Id] }, + ], + dz: [ + { + onecategorydp: draft1Id, + __component: 'default.compo', + categories: [categoryId], + categoriesdp: [draft1Id, draft2Id], + }, + ], + }, + }); + + const { body } = await rq({ + method: 'GET', + url: `/content-manager/collection-types/api::product.product/${product.id}/actions/numberOfDraftRelations`, + }); + + expect(body.data).toBe(12); + }); }); diff --git a/packages/core/database/lib/query/helpers/populate/apply.js b/packages/core/database/lib/query/helpers/populate/apply.js index 0324b0ca22..2ae346d135 100644 --- a/packages/core/database/lib/query/helpers/populate/apply.js +++ b/packages/core/database/lib/query/helpers/populate/apply.js @@ -11,7 +11,7 @@ const { fromRow } = require('../transform'); * @returns */ const XtoOne = async (input, ctx) => { - const { attribute, attributeName, results, populateValue, targetMeta } = input; + const { attribute, attributeName, results, populateValue, targetMeta, isCount } = input; const { db, qb } = ctx; const fromTargetRow = (rowOrRows) => fromRow(targetMeta, rowOrRows); @@ -61,6 +61,41 @@ const XtoOne = async (input, ctx) => { results.map((r) => r[referencedColumnName]).filter((value) => !_.isNil(value)) ); + if (isCount) { + if (_.isEmpty(referencedValues)) { + results.forEach((result) => { + result[attributeName] = { count: 0 }; + }); + return; + } + + const rows = await qb + .init(populateValue) + .join({ + alias, + referencedTable: joinTable.name, + referencedColumn: joinTable.inverseJoinColumn.name, + rootColumn: joinTable.inverseJoinColumn.referencedColumn, + rootTable: qb.alias, + on: joinTable.on, + }) + .select([joinColAlias, qb.raw('count(*) AS count')]) + .where({ [joinColAlias]: referencedValues }) + .groupBy(joinColAlias) + .execute({ mapResults: false }); + + const map = rows.reduce((map, row) => { + map[row[joinColumnName]] = { count: Number(row.count) }; + return map; + }, {}); + + results.forEach((result) => { + result[attributeName] = map[result[referencedColumnName]] || { count: 0 }; + }); + + return; + } + if (_.isEmpty(referencedValues)) { results.forEach((result) => { result[attributeName] = null;