Skip to content

Commit

Permalink
fix(): Add consistent sorting for aggregate queries
Browse files Browse the repository at this point in the history
  • Loading branch information
doug-martin committed Mar 31, 2021
1 parent 1e82baa commit 4ac7a14
Show file tree
Hide file tree
Showing 16 changed files with 192 additions and 147 deletions.
29 changes: 0 additions & 29 deletions examples/typegoose/e2e/sub-task.resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,35 +186,6 @@ describe('SubTaskResolver (typegoose - e2e)', () => {
expect(edges.map((e) => e.node)).toEqual(toGraphqlSubTasks(SUB_TASKS.slice(0, 3)));
}));

// it(`should allow querying on todoItem`, () => {
// return request(app.getHttpServer())
// .post('/graphql')
// .send({
// operationName: null,
// variables: {},
// query: `{
// subTasks(filter: { todoItem: { title: { like: "Create Entity%" } } }) {
// ${pageInfoField}
// ${edgeNodes(subTaskFields)}
// totalCount
// }
// }`,
// })
// .expect(200)
// .then(({ body }) => {
// const { edges, pageInfo, totalCount }: CursorConnectionType<SubTaskDTO> = body.data.subTasks;
// expect(pageInfo).toEqual({
// endCursor: 'eyJ0eXBlIjoia2V5c2V0IiwiZmllbGRzIjpbeyJmaWVsZCI6ImlkIiwidmFsdWUiOjl9XX0=',
// hasNextPage: false,
// hasPreviousPage: false,
// startCursor: 'eyJ0eXBlIjoia2V5c2V0IiwiZmllbGRzIjpbeyJmaWVsZCI6ImlkIiwidmFsdWUiOjR9XX0=',
// });
// expect(totalCount).toBe(6);
// expect(edges).toHaveLength(6);
// expect(edges.map((e) => e.node)).toEqual(SUB_TASKS.slice(3, 9));
// });
// });

it(`should allow sorting`, () =>
request(app.getHttpServer())
.post('/graphql')
Expand Down
29 changes: 0 additions & 29 deletions examples/typegoose/e2e/tag.resolver.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,35 +193,6 @@ describe('TagResolver (typegoose - e2e)', () => {
expect(edges.map((e) => e.node)).toEqual(TAGS.slice(0, 3));
}));

// it(`should allow querying on todoItems`, () => {
// return request(app.getHttpServer())
// .post('/graphql')
// .send({
// operationName: null,
// variables: {},
// query: `{
// tags(filter: { todoItems: { title: { like: "Create Entity%" } } }, sorting: [{field: id, direction: ASC}]) {
// ${pageInfoField}
// ${edgeNodes(tagFields)}
// totalCount
// }
// }`,
// })
// .expect(200)
// .then(({ body }) => {
// const { edges, pageInfo, totalCount }: CursorConnectionType<TagDTO> = body.data.tags;
// expect(pageInfo).toEqual({
// endCursor: 'eyJ0eXBlIjoia2V5c2V0IiwiZmllbGRzIjpbeyJmaWVsZCI6ImlkIiwidmFsdWUiOjV9XX0=',
// hasNextPage: false,
// hasPreviousPage: false,
// startCursor: 'eyJ0eXBlIjoia2V5c2V0IiwiZmllbGRzIjpbeyJmaWVsZCI6ImlkIiwidmFsdWUiOjF9XX0=',
// });
// expect(totalCount).toBe(3);
// expect(edges).toHaveLength(3);
// expect(edges.map((e) => e.node)).toEqual([TAGS[0], TAGS[2], TAGS[4]]);
// });
// });

it(`should allow sorting`, () =>
request(app.getHttpServer())
.post('/graphql')
Expand Down
12 changes: 0 additions & 12 deletions packages/core/src/interfaces/aggregate-query.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,3 @@ export type AggregateQuery<DTO> = {
min?: (keyof DTO)[];
groupBy?: (keyof DTO)[];
};

// const j = `invoiceAgg(filter: {}){
// groupBy {
// currency
// created
// }
// max {
// amount
// date
// };
// }`;
//
Original file line number Diff line number Diff line change
Expand Up @@ -240,54 +240,54 @@ describe('MongooseQueryService', () => {
return expect(queryResult).toEqual([
{
groupBy: {
boolType: true,
boolType: false,
},
avg: {
numberType: 6,
numberType: 5,
},
count: {
id: 5,
},
max: {
dateType: TEST_ENTITIES[9].dateType,
numberType: 10,
stringType: 'foo8',
dateType: TEST_ENTITIES[8].dateType,
numberType: 9,
stringType: 'foo9',
id: expect.any(ObjectId),
},
min: {
dateType: TEST_ENTITIES[1].dateType,
numberType: 2,
stringType: 'foo10',
dateType: TEST_ENTITIES[0].dateType,
numberType: 1,
stringType: 'foo1',
id: expect.any(ObjectId),
},
sum: {
numberType: 30,
numberType: 25,
},
},
{
groupBy: {
boolType: false,
boolType: true,
},
avg: {
numberType: 5,
numberType: 6,
},
count: {
id: 5,
},
max: {
dateType: TEST_ENTITIES[8].dateType,
numberType: 9,
stringType: 'foo9',
dateType: TEST_ENTITIES[9].dateType,
numberType: 10,
stringType: 'foo8',
id: expect.any(ObjectId),
},
min: {
dateType: TEST_ENTITIES[0].dateType,
numberType: 1,
stringType: 'foo1',
dateType: TEST_ENTITIES[1].dateType,
numberType: 2,
stringType: 'foo10',
id: expect.any(ObjectId),
},
sum: {
numberType: 25,
numberType: 30,
},
},
]);
Expand Down
14 changes: 13 additions & 1 deletion packages/query-mongoose/src/query/aggregate.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,21 @@ export class AggregateBuilder<Entity extends Document> {
return null;
}
return fields.reduce((id: Record<string, string>, field) => {
const aggAlias = `group_by_${field as string}`;
const aggAlias = this.getGroupByAlias(field);
const fieldAlias = `$${getSchemaKey(String(field))}`;
return { ...id, [aggAlias]: fieldAlias };
}, {});
}

getGroupBySelects(fields?: (keyof Entity)[]): string[] | undefined {
if (!fields) {
return undefined;
}
// append _id so it pulls the sort from the _id field
return (fields ?? []).map((f) => `_id.${this.getGroupByAlias(f)}`);
}

private getGroupByAlias(field: keyof Entity): string {
return `group_by_${field as string}`;
}
}
36 changes: 28 additions & 8 deletions packages/query-mongoose/src/query/filter-query.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import { AggregateBuilder, MongooseGroupAndAggregate } from './aggregate.builder
import { getSchemaKey } from './helpers';
import { WhereBuilder } from './where.builder';

type MongooseSort = Record<string, 'asc' | 'desc'>;
const MONGOOSE_SORT_DIRECTION: Record<SortDirection, 1 | -1> = {
[SortDirection.ASC]: 1,
[SortDirection.DESC]: -1,
};

type MongooseSort = Record<string, 1 | -1>;
type MongooseQuery<Entity extends Document> = {
filterQuery: FilterQuery<Entity>;
options: { limit?: number; skip?: number; sort?: MongooseSort[] };
options: { limit?: number; skip?: number; sort?: MongooseSort };
};

type MongooseAggregateQuery<Entity extends Document> = {
filterQuery: FilterQuery<Entity>;
type MongooseAggregateQuery<Entity extends Document> = MongooseQuery<Entity> & {
aggregate: MongooseGroupAndAggregate;
};
/**
Expand All @@ -37,6 +40,7 @@ export class FilterQueryBuilder<Entity extends Document> {
return {
filterQuery: this.buildFilterQuery(filter),
aggregate: this.aggregateBuilder.build(aggregate),
options: { sort: this.buildAggregateSorting(aggregate) },
};
}

Expand All @@ -48,6 +52,7 @@ export class FilterQueryBuilder<Entity extends Document> {
return {
filterQuery: this.buildIdFilterQuery(id, filter),
aggregate: this.aggregateBuilder.build(aggregate),
options: { sort: this.buildAggregateSorting(aggregate) },
};
}

Expand All @@ -74,9 +79,24 @@ export class FilterQueryBuilder<Entity extends Document> {
* Applies the ORDER BY clause to a `typeorm` QueryBuilder.
* @param sorts - an array of SortFields to create the ORDER BY clause.
*/
buildSorting(sorts?: SortField<Entity>[]): MongooseSort[] {
return (sorts || []).map((sort) => ({
[getSchemaKey(sort.field.toString())]: sort.direction === SortDirection.ASC ? 'asc' : 'desc',
}));
buildSorting(sorts?: SortField<Entity>[]): MongooseSort | undefined {
if (!sorts) {
return undefined;
}
return sorts.reduce((sort: MongooseSort, sortField: SortField<Entity>) => {
const field = getSchemaKey(sortField.field.toString());
const direction = MONGOOSE_SORT_DIRECTION[sortField.direction];
return { ...sort, [field]: direction };
}, {});
}

private buildAggregateSorting(aggregate: AggregateQuery<Entity>): MongooseSort | undefined {
const aggregateGroupBy = this.aggregateBuilder.getGroupBySelects(aggregate.groupBy);
if (!aggregateGroupBy) {
return undefined;
}
return aggregateGroupBy.reduce((sort: MongooseSort, sortField) => {
return { ...sort, [getSchemaKey(sortField)]: MONGOOSE_SORT_DIRECTION[SortDirection.ASC] };
}, {});
}
}
14 changes: 9 additions & 5 deletions packages/query-mongoose/src/services/mongoose-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,15 @@ export class MongooseQueryService<Entity extends Document>
filter: Filter<Entity>,
aggregateQuery: AggregateQuery<Entity>,
): Promise<AggregateResponse<Entity>[]> {
const { aggregate, filterQuery } = this.filterQueryBuilder.buildAggregateQuery(aggregateQuery, filter);
const aggResult = (await this.Model.aggregate<Record<string, unknown>>([
{ $match: filterQuery },
{ $group: aggregate },
]).exec()) as Record<string, unknown>[];
const { aggregate, filterQuery, options } = this.filterQueryBuilder.buildAggregateQuery(aggregateQuery, filter);
const aggPipeline: unknown[] = [{ $match: filterQuery }, { $group: aggregate }];
if (options.sort) {
aggPipeline.push({ $sort: options.sort ?? {} });
}
const aggResult = (await this.Model.aggregate<Record<string, unknown>>(aggPipeline).exec()) as Record<
string,
unknown
>[];
return AggregateBuilder.convertToAggregateResponse(aggResult);
}

Expand Down
13 changes: 9 additions & 4 deletions packages/query-mongoose/src/services/reference-query.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,18 @@ export abstract class ReferenceQueryService<Entity extends Document> {
if (!refFilter) {
return [];
}
const { filterQuery, aggregate } = referenceQueryBuilder.buildAggregateQuery(
const { filterQuery, aggregate, options } = referenceQueryBuilder.buildAggregateQuery(
assembler.convertAggregateQuery(aggregateQuery),
refFilter,
);
const aggResult = (await relationModel
.aggregate<Record<string, unknown>>([{ $match: filterQuery }, { $group: aggregate }])
.exec()) as Record<string, unknown>[];
const aggPipeline: unknown[] = [{ $match: filterQuery }, { $group: aggregate }];
if (options.sort) {
aggPipeline.push({ $sort: options.sort ?? {} });
}
const aggResult = (await relationModel.aggregate<Record<string, unknown>>(aggPipeline).exec()) as Record<
string,
unknown
>[];
return AggregateBuilder.convertToAggregateResponse(aggResult);
}

Expand Down
25 changes: 21 additions & 4 deletions packages/query-sequelize/src/query/filter-query.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export class FilterQueryBuilder<Entity extends Model<Entity, Partial<Entity>>> {
let opts: FindOptions = { raw: true };
opts = this.applyAggregate(opts, aggregate);
opts = this.applyFilter(opts, query.filter);
opts = this.applyAggregateSorting(opts, aggregate.groupBy);
opts = this.applyGroupBy(opts, aggregate.groupBy);
return opts;
}
Expand All @@ -98,6 +99,7 @@ export class FilterQueryBuilder<Entity extends Model<Entity, Partial<Entity>>> {
let opts: FindOptions = { joinTableAttributes: [], raw: true } as FindOptions;
opts = this.applyAggregate(opts, aggregate);
opts = this.applyFilter(opts, query.filter);
opts = this.applyAggregateSorting(opts, aggregate.groupBy);
opts = this.applyGroupBy(opts, aggregate.groupBy);
return opts;
}
Expand Down Expand Up @@ -191,7 +193,13 @@ export class FilterQueryBuilder<Entity extends Model<Entity, Partial<Entity>>> {
return qb;
}

applyGroupBy<T extends Groupable>(qb: T, groupBy?: (keyof Entity)[], alias?: string): T {
private applyAggregate<P extends Projectable>(opts: P, aggregate: AggregateQuery<Entity>): P {
// eslint-disable-next-line no-param-reassign
opts.attributes = this.aggregateBuilder.build(aggregate).attributes;
return opts;
}

applyGroupBy<T extends Groupable>(qb: T, groupBy?: (keyof Entity)[]): T {
if (!groupBy) {
return qb;
}
Expand All @@ -203,10 +211,19 @@ export class FilterQueryBuilder<Entity extends Model<Entity, Partial<Entity>>> {
return qb;
}

private applyAggregate<P extends Projectable>(opts: P, aggregate: AggregateQuery<Entity>): P {
applyAggregateSorting<T extends Sortable>(qb: T, groupBy?: (keyof Entity)[]): T {
if (!groupBy) {
return qb;
}
// eslint-disable-next-line no-param-reassign
opts.attributes = this.aggregateBuilder.build(aggregate).attributes;
return opts;
qb.order = groupBy.map(
(field): OrderItem => {
const colName = this.model.rawAttributes[field as string].field;
const col = sequelize.col(colName ?? (field as string));
return [col, 'ASC'];
},
);
return qb;
}

private applyAssociationIncludes<Opts extends FindOptions | CountOptions>(
Expand Down
Loading

0 comments on commit 4ac7a14

Please sign in to comment.