Skip to content

Commit

Permalink
feat(aggregations,relations,core): Add relation aggregation to core
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-martin committed Jul 16, 2020
1 parent af075d2 commit a489588
Show file tree
Hide file tree
Showing 9 changed files with 345 additions and 33 deletions.
67 changes: 67 additions & 0 deletions packages/core/__tests__/services/assembler-query.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,73 @@ describe('AssemblerQueryService', () => {
});
});

describe('aggregateRelations', () => {
it('should transform the results for a single entity', () => {
const mockQueryService = mock<QueryService<TestEntity>>();
const assemblerService = new AssemblerQueryService(new TestAssembler(), instance(mockQueryService));
const aggQuery: AggregateQuery<TestDTO> = { count: ['foo'] };
const result: AggregateResponse<TestDTO> = { count: { foo: 1 } };
when(
mockQueryService.aggregateRelations(
TestDTO,
'test',
objectContaining({ bar: 'bar' }),
objectContaining({ foo: { eq: 'bar' } }),
aggQuery,
),
).thenResolve(result);

return expect(
assemblerService.aggregateRelations(TestDTO, 'test', { foo: 'bar' }, { foo: { eq: 'bar' } }, aggQuery),
).resolves.toEqual(result);
});

it('should transform the results for multiple entities', () => {
const mockQueryService = mock<QueryService<TestEntity>>();
const assemblerService = new AssemblerQueryService(new TestAssembler(), instance(mockQueryService));
const dto: TestDTO = { foo: 'bar' };
const entity: TestEntity = { bar: 'bar' };
const aggQuery: AggregateQuery<TestDTO> = { count: ['foo'] };
const result: AggregateResponse<TestDTO> = { count: { foo: 1 } };
when(
mockQueryService.aggregateRelations(
TestDTO,
'test',
deepEqual([entity]),
objectContaining({ foo: { eq: 'bar' } }),
aggQuery,
),
).thenCall((relationClass, relation, entities) => {
return Promise.resolve(
new Map<TestEntity, AggregateResponse<TestDTO>>([[entities[0], result]]),
);
});
return expect(
assemblerService.aggregateRelations(TestDTO, 'test', [{ foo: 'bar' }], { foo: { eq: 'bar' } }, aggQuery),
).resolves.toEqual(new Map([[dto, result]]));
});

it('should return an empty array for dtos with no aggregateRelations', () => {
const mockQueryService = mock<QueryService<TestEntity>>();
const assemblerService = new AssemblerQueryService(new TestAssembler(), instance(mockQueryService));
const dto: TestDTO = { foo: 'bar' };
const entity: TestEntity = { bar: 'bar' };
const aggQuery: AggregateQuery<TestDTO> = { count: ['foo'] };
when(
mockQueryService.aggregateRelations(
TestDTO,
'test',
deepEqual([entity]),
objectContaining({ foo: { eq: 'bar' } }),
aggQuery,
),
).thenResolve(new Map<TestEntity, AggregateResponse<TestDTO>>());
return expect(
assemblerService.aggregateRelations(TestDTO, 'test', [{ foo: 'bar' }], { foo: { eq: 'bar' } }, aggQuery),
).resolves.toEqual(new Map([[dto, {}]]));
});
});

describe('countRelations', () => {
it('should transform the results for a single entity', () => {
const mockQueryService = mock<QueryService<TestEntity>>();
Expand Down
6 changes: 6 additions & 0 deletions packages/core/__tests__/services/noop-query.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,10 @@ describe('NoOpQueryService', () => {
it('should throw a NotImplementedException when calling updateOne', () => {
return expect(instance.updateOne(1, { foo: 'bar' })).rejects.toThrow('updateOne is not implemented');
});

it('should throw a NotImplementedException when calling aggregateRelations', () => {
return expect(instance.aggregateRelations(TestType, 'test', new TestType(), {}, {})).rejects.toThrow(
'aggregateRelations is not implemented',
);
});
});
24 changes: 23 additions & 1 deletion packages/core/__tests__/services/proxy-query.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { mock, reset, instance, when } from 'ts-mockito';
import { QueryService, AggregateQuery } from '../../src';
import { ProxyQueryService } from '../../src/services/proxy-query.service';

describe('NoOpQueryService', () => {
describe('ProxyQueryService', () => {
class TestType {
foo!: string;
}
Expand Down Expand Up @@ -105,6 +105,28 @@ describe('NoOpQueryService', () => {
return expect(queryService.queryRelations(TestType, relationName, dtos, query)).resolves.toBe(result);
});

it('should proxy to the underlying service when calling aggregateRelations with one dto', () => {
const relationName = 'test';
const dto = new TestType();
const filter = {};
const aggQuery: AggregateQuery<TestType> = { count: ['foo'] };
const result = { count: { foo: 1 } };
when(mockQueryService.aggregateRelations(TestType, relationName, dto, filter, aggQuery)).thenResolve(result);
return expect(queryService.aggregateRelations(TestType, relationName, dto, filter, aggQuery)).resolves.toBe(result);
});

it('should proxy to the underlying service when calling aggregateRelations with many dtos', () => {
const relationName = 'test';
const dtos = [new TestType()];
const filter = {};
const aggQuery: AggregateQuery<TestType> = { count: ['foo'] };
const result = new Map([[{ foo: 'bar' }, { count: { foo: 1 } }]]);
when(mockQueryService.aggregateRelations(TestType, relationName, dtos, filter, aggQuery)).thenResolve(result);
return expect(queryService.aggregateRelations(TestType, relationName, dtos, filter, aggQuery)).resolves.toBe(
result,
);
});

it('should proxy to the underlying service when calling countRelations with one dto', () => {
const relationName = 'test';
const dto = new TestType();
Expand Down
64 changes: 63 additions & 1 deletion packages/core/__tests__/services/relation-query.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { mock, reset, instance, when, deepEqual } from 'ts-mockito';
import { QueryService, RelationQueryService } from '../../src';
import { AggregateQuery, QueryService, RelationQueryService } from '../../src';

describe('RelationQueryService', () => {
const mockQueryService: QueryService<TestType> = mock<QueryService<TestType>>();
Expand Down Expand Up @@ -124,6 +124,68 @@ describe('RelationQueryService', () => {
});
});

describe('#aggregateRelations', () => {
it('should proxy to the underlying service when calling queryRelations with one dto', async () => {
const relationName = 'test';
const dto = new TestType();
const result = { count: { foo: 1 } };
const filter = {};
const relationFilter = {};
const relationAggregateQuery: AggregateQuery<TestType> = { count: ['foo'] };
testRelationFn.mockReturnValue({ filter: relationFilter });
when(mockRelationService.aggregate(deepEqual(relationFilter), relationAggregateQuery)).thenResolve(result);
await expect(
queryService.aggregateRelations(TestType, relationName, dto, filter, relationAggregateQuery),
).resolves.toBe(result);
return expect(testRelationFn).toHaveBeenCalledWith(dto);
});

it('should proxy to the underlying service when calling queryRelations with many dtos', async () => {
const relationName = 'test';
const dtos = [new TestType()];
const relationResults = { count: { foo: 1 } };
const result = new Map([[dtos[0], relationResults]]);
const filter = {};
const relationFilter = {};
const relationAggregateQuery: AggregateQuery<TestType> = { count: ['foo'] };
testRelationFn.mockReturnValue({ filter: relationFilter });
when(mockRelationService.aggregate(deepEqual(relationFilter), relationAggregateQuery)).thenResolve(
relationResults,
);
return expect(
queryService.aggregateRelations(TestType, relationName, dtos, filter, relationAggregateQuery),
).resolves.toEqual(result);
});

it('should proxy to the underlying service when calling queryRelations with one dto and a unknown relation', () => {
const relationName = 'unknown';
const dto = new TestType();
const filter = {};
const aggregateQuery: AggregateQuery<TestType> = { count: ['foo'] };
const result = { count: { foo: 1 } };
when(mockQueryService.aggregateRelations(TestType, relationName, dto, filter, aggregateQuery)).thenResolve(
result,
);
return expect(queryService.aggregateRelations(TestType, relationName, dto, filter, aggregateQuery)).resolves.toBe(
result,
);
});

it('should proxy to the underlying service when calling queryRelations with many dtos and a unknown relation', () => {
const relationName = 'unknown';
const dtos = [new TestType()];
const filter = {};
const aggregateQuery: AggregateQuery<TestType> = { count: ['foo'] };
const result = new Map([[dtos[0], { count: { foo: 1 } }]]);
when(mockQueryService.aggregateRelations(TestType, relationName, dtos, filter, aggregateQuery)).thenResolve(
result,
);
return expect(
queryService.aggregateRelations(TestType, relationName, dtos, filter, aggregateQuery),
).resolves.toBe(result);
});
});

describe('#countRelations', () => {
it('should proxy to the underlying service when calling queryRelations with one dto', async () => {
const relationName = 'test';
Expand Down
45 changes: 45 additions & 0 deletions packages/core/src/services/assembler-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,49 @@ export class AssemblerQueryService<DTO, Entity> implements QueryService<DTO> {
this.queryService.updateOne(id, this.assembler.convertToEntity((update as unknown) as DTO)),
);
}

aggregateRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
dto: DTO,
filter: Filter<Relation>,
aggregate: AggregateQuery<Relation>,
): Promise<AggregateResponse<Relation>>;
aggregateRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
dtos: DTO[],
filter: Filter<Relation>,
aggregate: AggregateQuery<Relation>,
): Promise<Map<DTO, AggregateResponse<Relation>>>;
async aggregateRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
dto: DTO | DTO[],
filter: Filter<Relation>,
aggregate: AggregateQuery<Relation>,
): Promise<AggregateResponse<Relation> | Map<DTO, AggregateResponse<Relation>>> {
if (Array.isArray(dto)) {
const entities = this.assembler.convertToEntities(dto);
const relationMap = await this.queryService.aggregateRelations(
RelationClass,
relationName,
entities,
filter,
aggregate,
);
return entities.reduce((map, e, index) => {
const entry = relationMap.get(e) ?? {};
map.set(dto[index], entry);
return map;
}, new Map<DTO, AggregateResponse<Relation>>());
}
return this.queryService.aggregateRelations(
RelationClass,
relationName,
this.assembler.convertToEntity(dto),
filter,
aggregate,
);
}
}
42 changes: 32 additions & 10 deletions packages/core/src/services/noop-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,7 @@ export class NoOpQueryService<DTO> implements QueryService<DTO> {
return Promise.reject(new NotImplementedException('findById is not implemented'));
}

findRelation<Relation>(
RelationClass: Class<Relation>,
relationName: string,
entity: DTO,
): Promise<Relation | undefined>;
findRelation<Relation>(RelationClass: Class<Relation>, relationName: string, dto: DTO): Promise<Relation | undefined>;

findRelation<Relation>(
RelationClass: Class<Relation>,
Expand All @@ -60,7 +56,7 @@ export class NoOpQueryService<DTO> implements QueryService<DTO> {
findRelation<Relation>(
RelationClass: Class<Relation>,
relationName: string,
entity: DTO | DTO[],
dto: DTO | DTO[],
): Promise<(Relation | undefined) | Map<DTO, Relation | undefined>> {
return Promise.reject(new NotImplementedException('findRelation is not implemented'));
}
Expand All @@ -84,7 +80,7 @@ export class NoOpQueryService<DTO> implements QueryService<DTO> {
queryRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
entity: DTO,
dto: DTO,
query: Query<Relation>,
): Promise<Relation[]>;

Expand All @@ -98,7 +94,7 @@ export class NoOpQueryService<DTO> implements QueryService<DTO> {
queryRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
entity: DTO | DTO[],
dto: DTO | DTO[],
query: Query<Relation>,
): Promise<Relation[] | Map<DTO, Relation[]>> {
return Promise.reject(new NotImplementedException('queryRelations is not implemented'));
Expand All @@ -107,7 +103,7 @@ export class NoOpQueryService<DTO> implements QueryService<DTO> {
countRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
entity: DTO,
dto: DTO,
filter: Filter<Relation>,
): Promise<number>;

Expand All @@ -121,7 +117,7 @@ export class NoOpQueryService<DTO> implements QueryService<DTO> {
countRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
entity: DTO | DTO[],
dto: DTO | DTO[],
filter: Filter<Relation>,
): Promise<number | Map<DTO, number>> {
return Promise.reject(new NotImplementedException('countRelations is not implemented'));
Expand All @@ -146,4 +142,30 @@ export class NoOpQueryService<DTO> implements QueryService<DTO> {
updateOne<U extends DeepPartial<DTO>>(id: string | number, update: U): Promise<DTO> {
return Promise.reject(new NotImplementedException('updateOne is not implemented'));
}

aggregateRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
dto: DTO,
filter: Filter<Relation>,
aggregate: AggregateQuery<Relation>,
): Promise<AggregateResponse<Relation>>;

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

aggregateRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
dto: DTO | DTO[],
filter: Filter<Relation>,
aggregate: AggregateQuery<Relation>,
): Promise<AggregateResponse<Relation> | Map<DTO, AggregateResponse<Relation>>> {
return Promise.reject(new NotImplementedException('aggregateRelations is not implemented'));
}
}
27 changes: 27 additions & 0 deletions packages/core/src/services/proxy-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,31 @@ export class ProxyQueryService<DTO> implements QueryService<DTO> {
updateOne<U extends DeepPartial<DTO>>(id: string | number, update: U): Promise<DTO> {
return this.proxied.updateOne(id, update);
}

aggregateRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
dto: DTO,
filter: Filter<Relation>,
aggregate: AggregateQuery<Relation>,
): Promise<AggregateResponse<Relation>>;
aggregateRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
dtos: DTO[],
filter: Filter<Relation>,
aggregate: AggregateQuery<Relation>,
): Promise<Map<DTO, AggregateResponse<Relation>>>;
async aggregateRelations<Relation>(
RelationClass: Class<Relation>,
relationName: string,
dto: DTO | DTO[],
filter: Filter<Relation>,
aggregate: AggregateQuery<Relation>,
): Promise<AggregateResponse<Relation> | Map<DTO, AggregateResponse<Relation>>> {
if (Array.isArray(dto)) {
return this.proxied.aggregateRelations(RelationClass, relationName, dto, filter, aggregate);
}
return this.proxied.aggregateRelations(RelationClass, relationName, dto, filter, aggregate);
}
}
Loading

0 comments on commit a489588

Please sign in to comment.