Skip to content

Commit

Permalink
feat(aggregations,sequelize): Add relation aggregation to sequelize
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-martin committed Jul 16, 2020
1 parent a489588 commit 93e7c1b
Show file tree
Hide file tree
Showing 2 changed files with 254 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
88 changes: 86 additions & 2 deletions packages/query-sequelize/src/services/relation-query.service.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -67,6 +67,55 @@ export abstract class RelationQueryService<Entity extends Model> {
return assembler.convertToDTOs((relations as unknown) as Model[]);
}

async aggregateRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
entities: Entity[],
filter: Filter<Relation>,
aggregate: AggregateQuery<Relation>,
): Promise<Map<Entity, AggregateResponse<Relation>>>;

/**
* 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<Relation>(
RelationClass: Class<Relation>,
relationName: string,
dto: Entity,
filter: Filter<Relation>,
aggregate: AggregateQuery<Relation>,
): Promise<AggregateResponse<Relation>>;

async aggregateRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
dto: Entity | Entity[],
filter: Filter<Relation>,
aggregate: AggregateQuery<Relation>,
): Promise<AggregateResponse<Relation> | Map<Entity, AggregateResponse<Relation>>> {
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<Model>(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<string, unknown>),
);
return assembler.convertAggregateResponse(agg);
}

countRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
Expand Down Expand Up @@ -237,6 +286,41 @@ export abstract class RelationQueryService<Entity extends Model> {
}, Promise.resolve(new Map<Entity, Relation[]>()));
}

/**
* 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<Relation>(
RelationClass: Class<Relation>,
relationName: string,
entities: Entity[],
filter: Filter<Relation>,
aggregate: AggregateQuery<Relation>,
): Promise<Map<Entity, AggregateResponse<Relation>>> {
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<string, unknown>),
);
map.set(e, assembler.convertAggregateResponse(agg));
return map;
}, Promise.resolve(new Map<Entity, AggregateResponse<Relation>>()));
}

/**
* Query for an array of relations for multiple dtos.
* @param RelationClass - The class to serialize the relations into.
Expand Down

0 comments on commit 93e7c1b

Please sign in to comment.