diff --git a/packages/query-sequelize/__tests__/services/sequelize-query.service.spec.ts b/packages/query-sequelize/__tests__/services/sequelize-query.service.spec.ts index eb7635933..2a6b68a71 100644 --- a/packages/query-sequelize/__tests__/services/sequelize-query.service.spec.ts +++ b/packages/query-sequelize/__tests__/services/sequelize-query.service.spec.ts @@ -283,6 +283,174 @@ describe('SequelizeQueryService', (): void => { }); }); + describe('#aggregateRelations', () => { + describe('with one entity', () => { + it('call select and return the result', async () => { + const queryService = moduleRef.get(TestEntityService); + const aggResult = await queryService.aggregateRelations( + TestRelation, + 'testRelations', + TestEntity.build(PLAIN_TEST_ENTITIES[0]), + { relationName: { isNot: null } }, + { count: ['testRelationPk'] }, + ); + return expect(aggResult).toEqual({ + count: { + testRelationPk: 3, + }, + }); + }); + }); + + describe('with multiple entities', () => { + it('call select and return the result', async () => { + const entities = PLAIN_TEST_ENTITIES.slice(0, 3).map((pe) => TestEntity.build(pe)); + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.aggregateRelations( + TestRelation, + 'testRelations', + entities, + { relationName: { isNot: null } }, + { + count: ['testRelationPk', 'relationName', 'testEntityId'], + min: ['testRelationPk', 'relationName', 'testEntityId'], + max: ['testRelationPk', 'relationName', 'testEntityId'], + }, + ); + + expect(queryResult.size).toBe(3); + expect(queryResult).toEqual( + new Map([ + [ + entities[0], + { + count: { + relationName: 3, + testEntityId: 3, + testRelationPk: 3, + }, + max: { + relationName: 'foo1-test-relation', + testEntityId: 'test-entity-1', + testRelationPk: 'test-relations-test-entity-1-3', + }, + min: { + relationName: 'foo1-test-relation', + testEntityId: 'test-entity-1', + testRelationPk: 'test-relations-test-entity-1-1', + }, + }, + ], + [ + entities[1], + { + count: { + relationName: 3, + testEntityId: 3, + testRelationPk: 3, + }, + max: { + relationName: 'foo2-test-relation', + testEntityId: 'test-entity-2', + testRelationPk: 'test-relations-test-entity-2-3', + }, + min: { + relationName: 'foo2-test-relation', + testEntityId: 'test-entity-2', + testRelationPk: 'test-relations-test-entity-2-1', + }, + }, + ], + [ + entities[2], + { + count: { + relationName: 3, + testEntityId: 3, + testRelationPk: 3, + }, + max: { + relationName: 'foo3-test-relation', + testEntityId: 'test-entity-3', + testRelationPk: 'test-relations-test-entity-3-3', + }, + min: { + relationName: 'foo3-test-relation', + testEntityId: 'test-entity-3', + testRelationPk: 'test-relations-test-entity-3-1', + }, + }, + ], + ]), + ); + }); + + it('should return an empty array if no results are found.', async () => { + const entities: TestEntity[] = [ + PLAIN_TEST_ENTITIES[0] as TestEntity, + { testEntityPk: 'does-not-exist' } as TestEntity, + ]; + const queryService = moduleRef.get(TestEntityService); + const queryResult = await queryService.aggregateRelations( + TestRelation, + 'testRelations', + entities, + { relationName: { isNot: null } }, + { + count: ['testRelationPk', 'relationName', 'testEntityId'], + min: ['testRelationPk', 'relationName', 'testEntityId'], + max: ['testRelationPk', 'relationName', 'testEntityId'], + }, + ); + + expect(queryResult).toEqual( + new Map([ + [ + entities[0], + { + count: { + relationName: 3, + testEntityId: 3, + testRelationPk: 3, + }, + max: { + relationName: 'foo1-test-relation', + testEntityId: 'test-entity-1', + testRelationPk: 'test-relations-test-entity-1-3', + }, + min: { + relationName: 'foo1-test-relation', + testEntityId: 'test-entity-1', + testRelationPk: 'test-relations-test-entity-1-1', + }, + }, + ], + [ + { testEntityPk: 'does-not-exist' } as TestEntity, + { + count: { + relationName: 0, + testEntityId: 0, + testRelationPk: 0, + }, + max: { + relationName: null, + testEntityId: null, + testRelationPk: null, + }, + min: { + relationName: null, + testEntityId: null, + testRelationPk: null, + }, + }, + ], + ]), + ); + }); + }); + }); + describe('#countRelations', () => { describe('with one entity', () => { it('call count and return the result', async () => { diff --git a/packages/query-sequelize/src/services/relation-query.service.ts b/packages/query-sequelize/src/services/relation-query.service.ts index 24df6ad7b..f57299c66 100644 --- a/packages/query-sequelize/src/services/relation-query.service.ts +++ b/packages/query-sequelize/src/services/relation-query.service.ts @@ -1,7 +1,7 @@ -import { Query, Class, AssemblerFactory, Filter } from '@nestjs-query/core'; +import { Query, Class, AssemblerFactory, Filter, AggregateQuery, AggregateResponse } from '@nestjs-query/core'; import { Model, ModelCtor } from 'sequelize-typescript'; import { ModelCtor as SequelizeModelCtor } from 'sequelize'; -import { FilterQueryBuilder } from '../query'; +import { AggregateBuilder, FilterQueryBuilder } from '../query'; interface SequelizeAssociation { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -67,6 +67,55 @@ export abstract class RelationQueryService { return assembler.convertToDTOs((relations as unknown) as Model[]); } + async aggregateRelations( + RelationClass: Class, + relationName: string, + entities: Entity[], + filter: Filter, + aggregate: AggregateQuery, + ): Promise>>; + + /** + * Query for an array of relations. + * @param RelationClass - The class to serialize the relations into. + * @param dto - The dto to query relations for. + * @param relationName - The name of relation to query for. + * @param query - A query to filter, page and sort relations. + */ + async aggregateRelations( + RelationClass: Class, + relationName: string, + dto: Entity, + filter: Filter, + aggregate: AggregateQuery, + ): Promise>; + + async aggregateRelations( + RelationClass: Class, + relationName: string, + dto: Entity | Entity[], + filter: Filter, + aggregate: AggregateQuery, + ): Promise | Map>> { + if (Array.isArray(dto)) { + return this.batchAggregateRelations(RelationClass, relationName, dto, filter, aggregate); + } + const relationEntity = this.getRelationEntity(relationName); + const assembler = AssemblerFactory.getAssembler(RelationClass, relationEntity); + const relationQueryBuilder = this.getRelationQueryBuilder(relationEntity); + const results = ((await this.ensureIsEntity(dto).$get( + relationName as keyof Entity, + relationQueryBuilder.aggregateOptions( + assembler.convertQuery({ filter }), + assembler.convertAggregateQuery(aggregate), + ), + )) as unknown) as Model[]; + const [agg] = results.map((r) => + AggregateBuilder.convertToAggregateResponse(r.get({ plain: true }) as Record), + ); + return assembler.convertAggregateResponse(agg); + } + countRelations( RelationClass: Class, relationName: string, @@ -237,6 +286,41 @@ export abstract class RelationQueryService { }, Promise.resolve(new Map())); } + /** + * Query for an array of relations for multiple dtos. + * @param RelationClass - The class to serialize the relations into. + * @param entities - The entities to query relations for. + * @param relationName - The name of relation to query for. + * @param query - A query to filter, page or sort relations. + */ + private async batchAggregateRelations( + RelationClass: Class, + relationName: string, + entities: Entity[], + filter: Filter, + aggregate: AggregateQuery, + ): Promise>> { + const relationEntity = this.getRelationEntity(relationName); + const assembler = AssemblerFactory.getAssembler(RelationClass, relationEntity); + const relationQueryBuilder = this.getRelationQueryBuilder(relationEntity); + const findOptions = relationQueryBuilder.aggregateOptions( + assembler.convertQuery({ filter }), + assembler.convertAggregateQuery(aggregate), + ); + return entities.reduce(async (mapPromise, e) => { + const map = await mapPromise; + const results = ((await this.ensureIsEntity(e).$get( + relationName as keyof Entity, + findOptions, + )) as unknown) as Model[]; + const [agg] = results.map((r) => + AggregateBuilder.convertToAggregateResponse(r.get({ plain: true }) as Record), + ); + map.set(e, assembler.convertAggregateResponse(agg)); + return map; + }, Promise.resolve(new Map>())); + } + /** * Query for an array of relations for multiple dtos. * @param RelationClass - The class to serialize the relations into.