From 94c242f476590208fea8bc1734f1434a4e1ead43 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Tue, 11 Oct 2022 08:39:21 +0200 Subject: [PATCH] feat(medusa): Allow to filter customer groups by discount condition id (#2346) --- .../api/__tests__/admin/customer-groups.js | 274 ++++++++++-------- .../customer-groups/list-customer-groups.ts | 1 + .../medusa/src/repositories/customer-group.ts | 103 ++++++- .../medusa/src/services/customer-group.ts | 47 +-- packages/medusa/src/types/customer-groups.ts | 4 + packages/medusa/src/utils/repository.ts | 165 +++++++++++ 6 files changed, 446 insertions(+), 148 deletions(-) create mode 100644 packages/medusa/src/utils/repository.ts diff --git a/integration-tests/api/__tests__/admin/customer-groups.js b/integration-tests/api/__tests__/admin/customer-groups.js index 3609a8312e..a91a85fbdd 100644 --- a/integration-tests/api/__tests__/admin/customer-groups.js +++ b/integration-tests/api/__tests__/admin/customer-groups.js @@ -1,14 +1,29 @@ const path = require("path") +const { IdMap } = require("medusa-test-utils") + const setupServer = require("../../../helpers/setup-server") const { useApi } = require("../../../helpers/use-api") const { useDb, initDb } = require("../../../helpers/use-db") const customerSeeder = require("../../helpers/customer-seeder") const adminSeeder = require("../../helpers/admin-seeder") +const { + DiscountRuleType, + AllocationType, + DiscountConditionType, + DiscountConditionOperator, +} = require("@medusajs/medusa") +const { simpleDiscountFactory } = require("../../factories") jest.setTimeout(30000) +const adminReqConfig = { + headers: { + Authorization: "Bearer test_token", + }, +} + describe("/admin/customer-groups", () => { let medusaProcess let dbConnection @@ -43,11 +58,11 @@ describe("/admin/customer-groups", () => { name: "test group", } - const response = await api.post("/admin/customer-groups", payload, { - headers: { - Authorization: "Bearer test_token", - }, - }) + const response = await api.post( + "/admin/customer-groups", + payload, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.customer_group).toEqual( @@ -66,11 +81,7 @@ describe("/admin/customer-groups", () => { } await api - .post("/admin/customer-groups", payload, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .post("/admin/customer-groups", payload, adminReqConfig) .catch((err) => { expect(err.response.status).toEqual(422) expect(err.response.data.type).toEqual("duplicate_error") @@ -99,11 +110,10 @@ describe("/admin/customer-groups", () => { const id = "customer-group-1" - const deleteResponse = await api.delete(`/admin/customer-groups/${id}`, { - headers: { - Authorization: "Bearer test_token", - }, - }) + const deleteResponse = await api.delete( + `/admin/customer-groups/${id}`, + adminReqConfig + ) expect(deleteResponse.data).toEqual({ id: id, @@ -112,11 +122,7 @@ describe("/admin/customer-groups", () => { }) await api - .get(`/admin/customer-groups/${id}`, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get(`/admin/customer-groups/${id}`, adminReqConfig) .catch((error) => { expect(error.response.data.type).toEqual("not_found") expect(error.response.data.message).toEqual( @@ -134,11 +140,7 @@ describe("/admin/customer-groups", () => { const customerRes_preDeletion = await api.get( `/admin/customers/test-customer-delete-cg?expand=groups`, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) expect(customerRes_preDeletion.data.customer).toEqual( @@ -153,11 +155,7 @@ describe("/admin/customer-groups", () => { ) const deleteResponse = await api - .delete(`/admin/customer-groups/${id}`, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .delete(`/admin/customer-groups/${id}`, adminReqConfig) .catch((err) => console.log(err)) expect(deleteResponse.data).toEqual({ @@ -168,11 +166,7 @@ describe("/admin/customer-groups", () => { const customerRes = await api.get( `/admin/customers/test-customer-delete-cg?expand=groups`, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) expect(customerRes.data.customer).toEqual( @@ -198,11 +192,7 @@ describe("/admin/customer-groups", () => { const api = useApi() const response = await api - .get("/admin/customer-groups/test-group-5/customers", { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get("/admin/customer-groups/test-group-5/customers", adminReqConfig) .catch((err) => { console.log(err) }) @@ -246,11 +236,7 @@ describe("/admin/customer-groups", () => { const batchAddResponse = await api.post( "/admin/customer-groups/customer-group-1/customers/batch", payload, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) expect(batchAddResponse.status).toEqual(200) @@ -262,9 +248,7 @@ describe("/admin/customer-groups", () => { const getCustomerResponse = await api.get( "/admin/customers?expand=groups", - { - headers: { Authorization: "Bearer test_token" }, - } + adminReqConfig ) expect(getCustomerResponse.data.customers).toEqual( @@ -304,11 +288,7 @@ describe("/admin/customer-groups", () => { .post( "/admin/customer-groups/non-existing-customer-group-1/customers/batch", payload, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => { expect(err.response.data.type).toEqual("not_found") @@ -332,11 +312,7 @@ describe("/admin/customer-groups", () => { .post( "/admin/customer-groups/customer-group-1/customers/batch", payload_1, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => console.log(err)) @@ -355,11 +331,7 @@ describe("/admin/customer-groups", () => { .post( "/admin/customer-groups/customer-group-1/customers/batch", payload_2, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch((err) => { expect(err.response.data.type).toEqual("not_found") @@ -371,9 +343,7 @@ describe("/admin/customer-groups", () => { // check that customer-1 is only added once and that customer-2 is added correctly const getCustomerResponse = await api.get( "/admin/customers?expand=groups", - { - headers: { Authorization: "Bearer test_token" }, - } + adminReqConfig ) expect(getCustomerResponse.data.customers).toEqual( @@ -419,11 +389,11 @@ describe("/admin/customer-groups", () => { }, } - const response = await api.post(`/admin/customer-groups/${id}`, body, { - headers: { - Authorization: "Bearer test_token", - }, - }) + const response = await api.post( + `/admin/customer-groups/${id}`, + body, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.customer_group).toEqual( @@ -454,11 +424,11 @@ describe("/admin/customer-groups", () => { } const response = await api - .post(`/admin/customer-groups/${id}?expand=customers`, body, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .post( + `/admin/customer-groups/${id}?expand=customers`, + body, + adminReqConfig + ) .catch(console.log) expect(response.status).toEqual(200) @@ -490,11 +460,7 @@ describe("/admin/customer-groups", () => { const response = await api .get( `/admin/customer-groups?limit=5&offset=2&expand=customers&order=created_at`, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) .catch(console.log) @@ -510,11 +476,10 @@ describe("/admin/customer-groups", () => { it("retreive a list of customer groups filtered by name using `q` param", async () => { const api = useApi() - const response = await api.get(`/admin/customer-groups?q=vip-customers`, { - headers: { - Authorization: "Bearer test_token", - }, - }) + const response = await api.get( + `/admin/customer-groups?q=vip-customers`, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.count).toEqual(1) @@ -525,6 +490,90 @@ describe("/admin/customer-groups", () => { ) expect(response.data.customer_groups[0]).not.toHaveProperty("customers") }) + + it("lists customers in group filtered by discount condition id and count", async () => { + const api = useApi() + + const resCustomerGroup = await api.get( + "/admin/customer-groups", + adminReqConfig + ) + + const customerGroup1 = resCustomerGroup.data.customer_groups[0] + const customerGroup2 = resCustomerGroup.data.customer_groups[2] + + const buildDiscountData = (code, conditionId, groups) => { + return { + code, + rule: { + type: DiscountRuleType.PERCENTAGE, + value: 10, + allocation: AllocationType.TOTAL, + conditions: [ + { + id: conditionId, + type: DiscountConditionType.CUSTOMER_GROUPS, + operator: DiscountConditionOperator.IN, + customer_groups: groups, + }, + ], + }, + } + } + + const discountConditionId = IdMap.getId( + "discount-condition-customer-group-1" + ) + await simpleDiscountFactory( + dbConnection, + buildDiscountData("code-1", discountConditionId, [customerGroup1.id]) + ) + + const discountConditionId2 = IdMap.getId( + "discount-condition-customer-group-2" + ) + await simpleDiscountFactory( + dbConnection, + buildDiscountData("code-2", discountConditionId2, [customerGroup2.id]) + ) + + let res = await api.get( + `/admin/customer-groups?discount_condition_id=${discountConditionId}`, + adminReqConfig + ) + + expect(res.status).toEqual(200) + expect(res.data.customer_groups).toHaveLength(1) + expect(res.data.customer_groups).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: customerGroup1.id }), + ]) + ) + + res = await api.get( + `/admin/customer-groups?discount_condition_id=${discountConditionId2}`, + adminReqConfig + ) + + expect(res.status).toEqual(200) + expect(res.data.customer_groups).toHaveLength(1) + expect(res.data.customer_groups).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: customerGroup2.id }), + ]) + ) + + res = await api.get(`/admin/customer-groups`, adminReqConfig) + + expect(res.status).toEqual(200) + expect(res.data.customer_groups).toHaveLength(7) + expect(res.data.customer_groups).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: customerGroup1.id }), + expect.objectContaining({ id: customerGroup2.id }), + ]) + ) + }) }) describe("GET /admin/customer-groups/:id", () => { @@ -543,11 +592,10 @@ describe("/admin/customer-groups", () => { const id = "customer-group-1" - const response = await api.get(`/admin/customer-groups/${id}`, { - headers: { - Authorization: "Bearer test_token", - }, - }) + const response = await api.get( + `/admin/customer-groups/${id}`, + adminReqConfig + ) expect(response.status).toEqual(200) expect(response.data.customer_group).toEqual( @@ -566,11 +614,7 @@ describe("/admin/customer-groups", () => { const response = await api.get( `/admin/customer-groups/${id}?expand=customers`, - { - headers: { - Authorization: "Bearer test_token", - }, - } + adminReqConfig ) expect(response.status).toEqual(200) @@ -589,11 +633,7 @@ describe("/admin/customer-groups", () => { const id = "test-group-000" await api - .get(`/admin/customer-groups/${id}`, { - headers: { - Authorization: "Bearer test_token", - }, - }) + .get(`/admin/customer-groups/${id}`, adminReqConfig) .catch((err) => { expect(err.response.status).toEqual(404) expect(err.response.data.type).toEqual("not_found") @@ -624,9 +664,7 @@ describe("/admin/customer-groups", () => { const batchAddResponse = await api .delete("/admin/customer-groups/test-group-5/customers/batch", { - headers: { - Authorization: "Bearer test_token", - }, + ...adminReqConfig, data: payload, }) .catch((err) => console.log(err)) @@ -641,9 +679,7 @@ describe("/admin/customer-groups", () => { const getCustomerResponse = await api.get( "/admin/customers?expand=groups", - { - headers: { Authorization: "Bearer test_token" }, - } + adminReqConfig ) expect(getCustomerResponse.data.customers).toEqual( @@ -669,9 +705,7 @@ describe("/admin/customer-groups", () => { const batchAddResponse = await api .delete("/admin/customer-groups/test-group-5/customers/batch", { - headers: { - Authorization: "Bearer test_token", - }, + ...adminReqConfig, data: payload, }) .catch((err) => console.log(err)) @@ -686,9 +720,7 @@ describe("/admin/customer-groups", () => { const getCustomerResponse = await api.get( "/admin/customers/test-customer-7?expand=groups", - { - headers: { Authorization: "Bearer test_token" }, - } + adminReqConfig ) expect(getCustomerResponse.data.customer).toEqual( @@ -714,17 +746,13 @@ describe("/admin/customer-groups", () => { } await api.delete("/admin/customer-groups/test-group-5/customers/batch", { - headers: { - Authorization: "Bearer test_token", - }, + ...adminReqConfig, data: payload, }) // check that customer-1 is only added once and that customer-2 is added correctly const getCustomerResponse = await api - .get("/admin/customers?expand=groups", { - headers: { Authorization: "Bearer test_token" }, - }) + .get("/admin/customers?expand=groups", adminReqConfig) .catch((err) => console.log(err)) expect(getCustomerResponse.data.customers).toEqual( @@ -756,18 +784,14 @@ describe("/admin/customer-groups", () => { } await api.delete("/admin/customer-groups/test-group-5/customers/batch", { - headers: { - Authorization: "Bearer test_token", - }, + ...adminReqConfig, data: payload, }) const idempotentRes = await api.delete( "/admin/customer-groups/test-group-5/customers/batch", { - headers: { - Authorization: "Bearer test_token", - }, + ...adminReqConfig, data: payload, } ) diff --git a/packages/medusa/src/api/routes/admin/customer-groups/list-customer-groups.ts b/packages/medusa/src/api/routes/admin/customer-groups/list-customer-groups.ts index 892a631175..0126287668 100644 --- a/packages/medusa/src/api/routes/admin/customer-groups/list-customer-groups.ts +++ b/packages/medusa/src/api/routes/admin/customer-groups/list-customer-groups.ts @@ -15,6 +15,7 @@ import { Type } from "class-transformer" * - (query) q {string} Query used for searching customer group names. * - (query) offset=0 {integer} How many groups to skip in the result. * - (query) order {string} the field used to order the customer groups. + * - (query) discount_condition_id {string} The discount condition id on which to filter the customer groups. * - in: query * name: id * style: form diff --git a/packages/medusa/src/repositories/customer-group.ts b/packages/medusa/src/repositories/customer-group.ts index b7c5e6f438..95df1555e9 100644 --- a/packages/medusa/src/repositories/customer-group.ts +++ b/packages/medusa/src/repositories/customer-group.ts @@ -1,5 +1,30 @@ -import { DeleteResult, EntityRepository, In, Repository } from "typeorm" -import { CustomerGroup } from "../models/customer-group" +import { + DeleteResult, + EntityRepository, + FindOperator, + In, + Repository, + SelectQueryBuilder, +} from "typeorm" +import { CustomerGroup } from "../models" +import { ExtendedFindConfig, Writable } from "../types/common" +import { + getGroupedRelations, + mergeEntitiesWithRelations, + queryEntityWithIds, + queryEntityWithoutRelations, +} from "../utils/repository" + +export type DefaultWithoutRelations = Omit< + ExtendedFindConfig>>, + "relations" +> + +export type FindWithoutRelationsOptions = DefaultWithoutRelations & { + where: DefaultWithoutRelations["where"] & { + discount_condition_id?: string | FindOperator + } +} @EntityRepository(CustomerGroup) export class CustomerGroupRepository extends Repository { @@ -37,4 +62,78 @@ export class CustomerGroupRepository extends Repository { }) .execute() } + + public async findWithRelationsAndCount( + relations: string[] = [], + idsOrOptionsWithoutRelations: FindWithoutRelationsOptions = { where: {} } + ): Promise<[CustomerGroup[], number]> { + let count: number + let entities: CustomerGroup[] + if (Array.isArray(idsOrOptionsWithoutRelations)) { + entities = await this.findByIds(idsOrOptionsWithoutRelations, { + withDeleted: idsOrOptionsWithoutRelations.withDeleted ?? false, + }) + count = entities.length + } else { + const customJoinsBuilders: (( + qb: SelectQueryBuilder, + alias: string + ) => void)[] = [] + + if (idsOrOptionsWithoutRelations?.where?.discount_condition_id) { + const discountConditionId = + idsOrOptionsWithoutRelations?.where?.discount_condition_id + delete idsOrOptionsWithoutRelations?.where?.discount_condition_id + + customJoinsBuilders.push( + (qb: SelectQueryBuilder, alias: string) => { + qb.innerJoin( + "discount_condition_customer_group", + "dc_cg", + `dc_cg.customer_group_id = ${alias}.id AND dc_cg.condition_id = :dcId`, + { dcId: discountConditionId } + ) + } + ) + } + + const result = await queryEntityWithoutRelations( + this, + idsOrOptionsWithoutRelations, + true, + customJoinsBuilders + ) + entities = result[0] + count = result[1] + } + const entitiesIds = entities.map(({ id }) => id) + + if (entitiesIds.length === 0) { + // no need to continue + return [[], count] + } + + if (relations.length === 0) { + const toReturn = await this.findByIds( + entitiesIds, + idsOrOptionsWithoutRelations + ) + return [toReturn, toReturn.length] + } + + const groupedRelations = getGroupedRelations(relations) + const entitiesIdsWithRelations = await queryEntityWithIds( + this, + entitiesIds, + groupedRelations, + idsOrOptionsWithoutRelations.withDeleted, + idsOrOptionsWithoutRelations.select + ) + + const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) + const entitiesToReturn = + mergeEntitiesWithRelations(entitiesAndRelations) + + return [entitiesToReturn, count] + } } diff --git a/packages/medusa/src/services/customer-group.ts b/packages/medusa/src/services/customer-group.ts index 0748ae8307..c565ba7d94 100644 --- a/packages/medusa/src/services/customer-group.ts +++ b/packages/medusa/src/services/customer-group.ts @@ -1,17 +1,18 @@ import { MedusaError } from "medusa-core-utils" -import { DeepPartial, EntityManager, ILike, SelectQueryBuilder } from "typeorm" +import { DeepPartial, EntityManager, ILike } from "typeorm" import { CustomerService } from "." import { CustomerGroup } from ".." -import { CustomerGroupRepository } from "../repositories/customer-group" -import { FindConfig } from "../types/common" import { - CustomerGroupUpdate, - FilterableCustomerGroupProps, -} from "../types/customer-groups" + CustomerGroupRepository, + FindWithoutRelationsOptions, +} from "../repositories/customer-group" +import { FindConfig } from "../types/common" +import { CustomerGroupUpdate } from "../types/customer-groups" import { buildQuery, formatException, isDefined, + isString, PostgresError, setMetadata, } from "../utils" @@ -195,15 +196,14 @@ class CustomerGroupService extends TransactionBaseService { * @return the result of the find operation */ async list( - selector: FilterableCustomerGroupProps = {}, + selector: Partial & { + q?: string + discount_condition_id?: string + } = {}, config: FindConfig ): Promise { - const cgRepo: CustomerGroupRepository = this.manager_.getCustomRepository( - this.customerGroupRepository_ - ) - - const query = buildQuery(selector, config) - return await cgRepo.find(query) + const [customerGroups] = await this.listAndCount(selector, config) + return customerGroups } /** @@ -214,7 +214,10 @@ class CustomerGroupService extends TransactionBaseService { * @return the result of the find operation */ async listAndCount( - selector: FilterableCustomerGroupProps = {}, + selector: Partial & { + q?: string + discount_condition_id?: string + } = {}, config: FindConfig ): Promise<[CustomerGroup[], number]> { const cgRepo: CustomerGroupRepository = this.manager_.getCustomRepository( @@ -222,7 +225,7 @@ class CustomerGroupService extends TransactionBaseService { ) let q - if ("q" in selector) { + if (isString(selector.q)) { q = selector.q delete selector.q } @@ -230,13 +233,15 @@ class CustomerGroupService extends TransactionBaseService { const query = buildQuery(selector, config) if (q) { - const where = query.where + query.where.name = ILike(`%${q}%`) + } - delete where.name - - query.where = ((qb: SelectQueryBuilder): void => { - qb.where(where).andWhere([{ name: ILike(`%${q}%`) }]) - }) as any + if (query.where.discount_condition_id) { + const { relations, ...query_ } = query + return await cgRepo.findWithRelationsAndCount( + relations, + query_ as FindWithoutRelationsOptions + ) } return await cgRepo.findAndCount(query) diff --git a/packages/medusa/src/types/customer-groups.ts b/packages/medusa/src/types/customer-groups.ts index 7f6baae71c..10accad6d3 100644 --- a/packages/medusa/src/types/customer-groups.ts +++ b/packages/medusa/src/types/customer-groups.ts @@ -28,6 +28,10 @@ export class FilterableCustomerGroupProps { @ValidateNested() @Type(() => DateComparisonOperator) created_at?: DateComparisonOperator + + @IsString() + @IsOptional() + discount_condition_id?: string } export class CustomerGroupsBatchCustomer { diff --git a/packages/medusa/src/utils/repository.ts b/packages/medusa/src/utils/repository.ts new file mode 100644 index 0000000000..5c1eea4481 --- /dev/null +++ b/packages/medusa/src/utils/repository.ts @@ -0,0 +1,165 @@ +import { flatten, groupBy, map, merge } from "lodash" +import { Repository, SelectQueryBuilder } from "typeorm" +import { FindWithoutRelationsOptions } from "../repositories/customer-group" + +/** + * Custom query entity, it is part of the creation of a custom findWithRelationsAndCount needs. + * Allow to query the relations for the specified entity ids + * @param repository + * @param entityIds + * @param groupedRelations + * @param withDeleted + * @param select + */ +export async function queryEntityWithIds( + repository: Repository, + entityIds: string[], + groupedRelations: { [toplevel: string]: string[] }, + withDeleted = false, + select: (keyof T)[] = [] +): Promise { + const alias = repository.constructor.name + return await Promise.all( + Object.entries(groupedRelations).map(async ([toplevel, rels]) => { + let querybuilder = repository.createQueryBuilder(`${alias}`) + + if (select && select.length) { + querybuilder.select(select.map((f) => `${alias}.${f as string}`)) + } + + querybuilder = querybuilder.leftJoinAndSelect( + `${alias}.${toplevel}`, + toplevel + ) + + for (const rel of rels) { + const [_, rest] = rel.split(".") + if (!rest) { + continue + } + // Regex matches all '.' except the rightmost + querybuilder = querybuilder.leftJoinAndSelect( + rel.replace(/\.(?=[^.]*\.)/g, "__"), + rel.replace(".", "__") + ) + } + + if (withDeleted) { + querybuilder = querybuilder + .where(`${alias}.id IN (:...entitiesIds)`, { + entitiesIds: entityIds, + }) + .withDeleted() + } else { + querybuilder = querybuilder.where( + `${alias}.deleted_at IS NULL AND products.id IN (:...entitiesIds)`, + { + entitiesIds: entityIds, + } + ) + } + + return querybuilder.getMany() + }) + ).then(flatten) +} + +/** + * Custom query entity without relations, it is part of the creation of a custom findWithRelationsAndCount needs. + * Allow to query the entities without taking into account the relations. The relations will be queried separately + * using the queryEntityWithIds util + * @param repository + * @param optionsWithoutRelations + * @param shouldCount + * @param customJoinBuilders + */ +export async function queryEntityWithoutRelations( + repository: Repository, + optionsWithoutRelations: FindWithoutRelationsOptions, + shouldCount = false, + customJoinBuilders: (( + qb: SelectQueryBuilder, + alias: string + ) => void)[] = [] +): Promise<[T[], number]> { + const alias = repository.constructor.name + + const qb = repository + .createQueryBuilder(alias) + .select([`${alias}.id`]) + .skip(optionsWithoutRelations.skip) + .take(optionsWithoutRelations.take) + + if (optionsWithoutRelations.where) { + qb.where(optionsWithoutRelations.where) + } + + if (optionsWithoutRelations.order) { + const toSelect: string[] = [] + const parsed = Object.entries(optionsWithoutRelations.order).reduce( + (acc, [k, v]) => { + const key = `${alias}.${k}` + toSelect.push(key) + acc[key] = v + return acc + }, + {} + ) + qb.addSelect(toSelect) + qb.orderBy(parsed) + } + + for (const customJoinBuilder of customJoinBuilders) { + customJoinBuilder(qb, alias) + } + + if (optionsWithoutRelations.withDeleted) { + qb.withDeleted() + } + + let entities: T[] + let count = 0 + if (shouldCount) { + const result = await qb.getManyAndCount() + entities = result[0] + count = result[1] + } else { + entities = await qb.getMany() + } + + return [entities, count] +} + +/** + * Grouped the relation to the top level entity + * @param relations + */ +export function getGroupedRelations(relations: string[]): { + [toplevel: string]: string[] +} { + const groupedRelations: { [toplevel: string]: string[] } = {} + for (const rel of relations) { + const [topLevel] = rel.split(".") + if (groupedRelations[topLevel]) { + groupedRelations[topLevel].push(rel) + } else { + groupedRelations[topLevel] = [rel] + } + } + + return groupedRelations +} + +/** + * Merged the entities and relations that composed by the result of queryEntityWithIds and queryEntityWithoutRelations + * call + * @param entitiesAndRelations + */ +export function mergeEntitiesWithRelations( + entitiesAndRelations: Array> +): T[] { + const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id") + return map(entitiesAndRelationsById, (entityAndRelations) => + merge({}, ...entityAndRelations) + ) +}