Skip to content

Commit

Permalink
feat(aggregations): Add aggregations interfaces
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-martin committed Jul 16, 2020
1 parent 7de81a7 commit d67e733
Show file tree
Hide file tree
Showing 16 changed files with 248 additions and 18 deletions.
25 changes: 24 additions & 1 deletion packages/core/__tests__/assemblers/abstract.assembler.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { transformQuery, Query, AbstractAssembler, Assembler } from '../../src';
import {
transformQuery,
Query,
AbstractAssembler,
Assembler,
AggregateQuery,
AggregateResponse,
transformAggregateQuery,
transformAggregateResponse,
} from '../../src';

describe('ClassTransformerAssembler', () => {
class TestDTO {
Expand Down Expand Up @@ -35,6 +44,20 @@ describe('ClassTransformerAssembler', () => {
lastName: 'last',
});
}

convertAggregateQuery(aggregate: AggregateQuery<TestDTO>): AggregateQuery<TestEntity> {
return transformAggregateQuery(aggregate, {
firstName: 'first',
lastName: 'last',
});
}

convertAggregateResponse(aggregate: AggregateResponse<TestEntity>): AggregateResponse<TestDTO> {
return transformAggregateResponse(aggregate, {
first: 'firstName',
last: 'lastName',
});
}
}

const testDTO: TestDTO = { firstName: 'foo', lastName: 'bar' };
Expand Down
36 changes: 29 additions & 7 deletions packages/core/__tests__/services/assembler-query.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { mock, instance, objectContaining, when, deepEqual } from 'ts-mockito';
import { AbstractAssembler, AssemblerQueryService, Query, QueryService, transformQuery } from '../../src';
import {
AbstractAssembler,
AggregateQuery,
AggregateResponse,
AssemblerQueryService,
Query,
QueryService,
transformAggregateQuery,
transformAggregateResponse,
transformQuery,
} from '../../src';

describe('AssemblerQueryService', () => {
class TestDTO {
Expand All @@ -15,12 +25,6 @@ describe('AssemblerQueryService', () => {
super(TestDTO, TestEntity);
}

convertQuery(query: Query<TestDTO>): Query<TestEntity> {
return transformQuery(query, {
foo: 'bar',
});
}

convertToDTO(entity: TestEntity): TestDTO {
return {
foo: entity.bar,
Expand All @@ -32,6 +36,24 @@ describe('AssemblerQueryService', () => {
bar: dto.foo,
};
}

convertQuery(query: Query<TestDTO>): Query<TestEntity> {
return transformQuery(query, {
foo: 'bar',
});
}

convertAggregateQuery(aggregate: AggregateQuery<TestDTO>): AggregateQuery<TestEntity> {
return transformAggregateQuery(aggregate, {
foo: 'bar',
});
}

convertAggregateResponse(aggregate: AggregateResponse<TestEntity>): AggregateResponse<TestDTO> {
return transformAggregateResponse(aggregate, {
bar: 'foo',
});
}
}

describe('query', () => {
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/assemblers/abstract.assembler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Class } from '../common';
import { Query } from '../interfaces';
import { AggregateQuery, Query, AggregateResponse } from '../interfaces';
import { getCoreMetadataStorage } from '../metadata';
import { Assembler } from './assembler';

Expand Down Expand Up @@ -39,6 +39,10 @@ export abstract class AbstractAssembler<DTO, Entity> implements Assembler<DTO, E

abstract convertQuery(query: Query<DTO>): Query<Entity>;

abstract convertAggregateQuery(aggregate: AggregateQuery<DTO>): AggregateQuery<Entity>;

abstract convertAggregateResponse(aggregate: AggregateResponse<Entity>): AggregateResponse<DTO>;

convertToDTOs(entities: Entity[]): DTO[] {
return entities.map((e) => this.convertToDTO(e));
}
Expand Down
14 changes: 13 additions & 1 deletion packages/core/src/assemblers/assembler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Class } from '../common';
import { Query } from '../interfaces';
import { AggregateQuery, AggregateResponse, Query } from '../interfaces';
import { getCoreMetadataStorage } from '../metadata';

export interface Assembler<DTO, Entity> {
Expand All @@ -21,6 +21,18 @@ export interface Assembler<DTO, Entity> {
*/
convertQuery(query: Query<DTO>): Query<Entity>;

/**
* Convert a DTO query to an entity query.
* @param aggregate - the aggregate query to convert.
*/
convertAggregateQuery(aggregate: AggregateQuery<DTO>): AggregateQuery<Entity>;

/**
* Convert a Entity aggregate response query to an dto aggregate.
* @param aggregate - the aggregate query to convert.
*/
convertAggregateResponse(aggregate: AggregateResponse<Entity>): AggregateResponse<DTO>;

/**
* Convert an array of entities to a an of DTOs
* @param entities - the entities to convert.
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/assemblers/class-transformer.assembler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { plainToClass } from 'class-transformer';
import { Query } from '../interfaces';
import { AggregateQuery, AggregateResponse, Query } from '../interfaces';
import { getCoreMetadataStorage } from '../metadata';
import { AbstractAssembler } from './abstract.assembler';
import { Class } from '../common';
Expand All @@ -20,6 +20,14 @@ export abstract class ClassTransformerAssembler<DTO, Entity> extends AbstractAss
return query as Query<Entity>;
}

convertAggregateQuery(aggregate: AggregateQuery<DTO>): AggregateQuery<Entity> {
return (aggregate as unknown) as AggregateQuery<Entity>;
}

convertAggregateResponse(aggregate: AggregateResponse<Entity>): AggregateResponse<DTO> {
return aggregate as AggregateResponse<DTO>;
}

// eslint-disable-next-line @typescript-eslint/ban-types
convert<T>(cls: Class<T>, obj: object): T {
const deserializer = getCoreMetadataStorage().getAssemblerDeserializer(cls);
Expand Down
76 changes: 76 additions & 0 deletions packages/core/src/helpers/aggregate.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { AggregateQuery, AggregateResponse, NumberAggregate } from '../interfaces';
import { QueryFieldMap } from './query.helpers';

const convertAggregateQueryFields = <From, To>(
fieldMap: QueryFieldMap<From, To>,
fields?: (keyof From)[],
): (keyof To)[] | undefined => {
if (!fields) {
return fields;
}
return fields.map((fromField) => {
const otherKey = fieldMap[fromField];
if (!otherKey) {
throw new Error(`No corresponding field found for '${fromField as string}' when transforming aggregateQuery`);
}
return otherKey as keyof To;
});
};

const convertAggregateNumberFields = <From, To>(
fieldMap: QueryFieldMap<From, To>,
response?: NumberAggregate<From>,
): NumberAggregate<To> | undefined => {
if (!response) {
return response;
}
return Object.keys(response).reduce((toResponse, fromField) => {
const otherKey = fieldMap[fromField as keyof From] as keyof To;
if (!otherKey) {
throw new Error(`No corresponding field found for '${fromField}' when transforming aggregateQuery`);
}
return { ...toResponse, [otherKey]: response[fromField as keyof From] };
}, {} as Record<keyof To, number>);
};

const convertAggregateFields = <From, To>(
fieldMap: QueryFieldMap<From, To>,
response?: Partial<From>,
): Partial<To> | undefined => {
if (!response) {
return response;
}
return Object.keys(response).reduce((toResponse, fromField) => {
const otherKey = fieldMap[fromField as keyof From] as keyof To;
if (!otherKey) {
throw new Error(`No corresponding field found for '${fromField}' when transforming aggregateQuery`);
}
return { ...toResponse, [otherKey]: response[fromField as keyof From] };
}, {} as Partial<To>);
};

export const transformAggregateQuery = <From, To>(
query: AggregateQuery<From>,
fieldMap: QueryFieldMap<From, To>,
): AggregateQuery<To> => {
return {
count: convertAggregateQueryFields(fieldMap, query.count),
sum: convertAggregateQueryFields(fieldMap, query.sum),
avg: convertAggregateQueryFields(fieldMap, query.avg),
max: convertAggregateQueryFields(fieldMap, query.max),
min: convertAggregateQueryFields(fieldMap, query.min),
};
};

export const transformAggregateResponse = <From, To>(
response: AggregateResponse<From>,
fieldMap: QueryFieldMap<From, To>,
): AggregateResponse<To> => {
return {
count: convertAggregateNumberFields(fieldMap, response.count),
sum: convertAggregateNumberFields(fieldMap, response.sum),
avg: convertAggregateNumberFields(fieldMap, response.avg),
max: convertAggregateFields(fieldMap, response.max),
min: convertAggregateFields(fieldMap, response.min),
};
};
1 change: 1 addition & 0 deletions packages/core/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export {
transformSort,
getFilterFields,
} from './query.helpers';
export { transformAggregateQuery, transformAggregateResponse } from './aggregate.helpers';
4 changes: 2 additions & 2 deletions packages/core/src/helpers/query.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import merge from 'lodash.merge';
import { Filter, Query, SortField } from '../interfaces';
import { FilterBuilder } from './filter.builder';

export type QueryFieldMap<From, To> = {
[F in keyof From]?: keyof To;
export type QueryFieldMap<From, To, T extends keyof To = keyof To> = {
[F in keyof From]?: T;
};

export const transformSort = <From, To>(
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ export {
NoOpQueryService,
QueryServiceRelation,
} from './services';
export { transformFilter, transformQuery, transformSort, applyFilter, getFilterFields, QueryFieldMap } from './helpers';
export {
transformFilter,
transformQuery,
transformSort,
applyFilter,
getFilterFields,
QueryFieldMap,
transformAggregateQuery,
transformAggregateResponse,
} from './helpers';
export {
ClassTransformerAssembler,
DefaultAssembler,
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/interfaces/aggregate-query.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type AggregateQuery<DTO> = {
count?: (keyof DTO)[];
sum?: (keyof DTO)[];
avg?: (keyof DTO)[];
max?: (keyof DTO)[];
min?: (keyof DTO)[];
};
15 changes: 15 additions & 0 deletions packages/core/src/interfaces/aggregate-response.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export type NumberAggregate<DTO> = {
[K in keyof DTO]?: number;
};

export type TypeAggregate<DTO> = {
[K in keyof DTO]?: DTO[K];
};

export type AggregateResponse<DTO> = {
count?: NumberAggregate<DTO>;
sum?: NumberAggregate<DTO>;
avg?: NumberAggregate<DTO>;
max?: TypeAggregate<DTO>;
min?: TypeAggregate<DTO>;
};
2 changes: 2 additions & 0 deletions packages/core/src/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export * from './query.inteface';
export * from './sort-field.interface';
export * from './update-many-response.interface';
export * from './delete-many-response.interface';
export * from './aggregate-response.interface';
export * from './aggregate-query.interface';
17 changes: 16 additions & 1 deletion packages/core/src/services/assembler-query.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Assembler } from '../assemblers';
import { Class, DeepPartial } from '../common';
import { DeleteManyResponse, Filter, Query, UpdateManyResponse } from '../interfaces';
import {
AggregateQuery,
AggregateResponse,
DeleteManyResponse,
Filter,
Query,
UpdateManyResponse,
} from '../interfaces';
import { QueryService } from './query.service';

export class AssemblerQueryService<DTO, Entity> implements QueryService<DTO> {
Expand Down Expand Up @@ -46,6 +53,14 @@ export class AssemblerQueryService<DTO, Entity> implements QueryService<DTO> {
return this.assembler.convertAsyncToDTOs(this.queryService.query(this.assembler.convertQuery(query)));
}

async aggregate(filter: Filter<DTO>, aggregate: AggregateQuery<DTO>): Promise<AggregateResponse<DTO>> {
const aggregateResponse = await this.queryService.aggregate(
this.assembler.convertQuery({ filter }).filter || {},
this.assembler.convertAggregateQuery(aggregate),
);
return this.assembler.convertAggregateResponse(aggregateResponse);
}

count(filter: Filter<DTO>): Promise<number> {
return this.queryService.count(this.assembler.convertQuery({ filter }).filter || {});
}
Expand Down
13 changes: 12 additions & 1 deletion packages/core/src/services/noop-query.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { NotImplementedException } from '@nestjs/common';
import { Filter, UpdateManyResponse, Query, DeleteManyResponse } from '../interfaces';
import {
Filter,
UpdateManyResponse,
Query,
DeleteManyResponse,
AggregateQuery,
AggregateResponse,
} from '../interfaces';
import { QueryService } from './query.service';
import { DeepPartial, Class } from '../common';

Expand Down Expand Up @@ -66,6 +73,10 @@ export class NoOpQueryService<DTO> implements QueryService<DTO> {
return Promise.reject(new NotImplementedException('query is not implemented'));
}

aggregate(filter: Filter<DTO>, aggregate: AggregateQuery<DTO>): Promise<AggregateResponse<DTO>> {
return Promise.reject(new NotImplementedException('aggregate is not implemented'));
}

count(filter: Filter<DTO>): Promise<number> {
return Promise.reject(new NotImplementedException('count is not implemented'));
}
Expand Down
13 changes: 12 additions & 1 deletion packages/core/src/services/proxy-query.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Class, DeepPartial } from '../common';
import { DeleteManyResponse, Filter, Query, UpdateManyResponse } from '../interfaces';
import {
AggregateQuery,
AggregateResponse,
DeleteManyResponse,
Filter,
Query,
UpdateManyResponse,
} from '../interfaces';
import { QueryService } from './query.service';

export class ProxyQueryService<DTO> implements QueryService<DTO> {
Expand Down Expand Up @@ -150,6 +157,10 @@ export class ProxyQueryService<DTO> implements QueryService<DTO> {
return this.proxied.query(query);
}

aggregate(filter: Filter<DTO>, query: AggregateQuery<DTO>): Promise<AggregateResponse<DTO>> {
return this.proxied.aggregate(filter, query);
}

count(filter: Filter<DTO>): Promise<number> {
return this.proxied.count(filter);
}
Expand Down
Loading

0 comments on commit d67e733

Please sign in to comment.