From 4ac7a1485c7dcd83569951298606f487608806b1 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Mon, 29 Mar 2021 23:10:41 -0600 Subject: [PATCH] fix(): Add consistent sorting for aggregate queries --- .../typegoose/e2e/sub-task.resolver.spec.ts | 29 --------------- examples/typegoose/e2e/tag.resolver.spec.ts | 29 --------------- .../interfaces/aggregate-query.interface.ts | 12 ------- .../services/mongoose-query.service.spec.ts | 36 +++++++++---------- .../src/query/aggregate.builder.ts | 14 +++++++- .../src/query/filter-query.builder.ts | 36 ++++++++++++++----- .../src/services/mongoose-query.service.ts | 14 +++++--- .../src/services/reference-query.service.ts | 13 ++++--- .../src/query/filter-query.builder.ts | 25 ++++++++++--- .../services/typegoose-query.service.spec.ts | 36 +++++++++---------- .../src/query/aggregate.builder.ts | 16 +++++++-- .../src/query/filter-query.builder.ts | 36 ++++++++++++++----- .../src/services/reference-query.service.ts | 13 ++++--- .../src/services/typegoose-query-service.ts | 14 +++++--- .../src/query/filter-query.builder.ts | 11 ++++++ .../src/query/relation-query.builder.ts | 5 +++ 16 files changed, 192 insertions(+), 147 deletions(-) diff --git a/examples/typegoose/e2e/sub-task.resolver.spec.ts b/examples/typegoose/e2e/sub-task.resolver.spec.ts index 53c9656f2..70ea4a662 100644 --- a/examples/typegoose/e2e/sub-task.resolver.spec.ts +++ b/examples/typegoose/e2e/sub-task.resolver.spec.ts @@ -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 = 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') diff --git a/examples/typegoose/e2e/tag.resolver.spec.ts b/examples/typegoose/e2e/tag.resolver.spec.ts index b0fe48f10..97c9f74de 100644 --- a/examples/typegoose/e2e/tag.resolver.spec.ts +++ b/examples/typegoose/e2e/tag.resolver.spec.ts @@ -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 = 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') diff --git a/packages/core/src/interfaces/aggregate-query.interface.ts b/packages/core/src/interfaces/aggregate-query.interface.ts index 589321758..f5a2363ac 100644 --- a/packages/core/src/interfaces/aggregate-query.interface.ts +++ b/packages/core/src/interfaces/aggregate-query.interface.ts @@ -6,15 +6,3 @@ export type AggregateQuery = { min?: (keyof DTO)[]; groupBy?: (keyof DTO)[]; }; - -// const j = `invoiceAgg(filter: {}){ -// groupBy { -// currency -// created -// } -// max { -// amount -// date -// }; -// }`; -// diff --git a/packages/query-mongoose/__tests__/services/mongoose-query.service.spec.ts b/packages/query-mongoose/__tests__/services/mongoose-query.service.spec.ts index cd4206caf..a14bfe9b5 100644 --- a/packages/query-mongoose/__tests__/services/mongoose-query.service.spec.ts +++ b/packages/query-mongoose/__tests__/services/mongoose-query.service.spec.ts @@ -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, }, }, ]); diff --git a/packages/query-mongoose/src/query/aggregate.builder.ts b/packages/query-mongoose/src/query/aggregate.builder.ts index ebba2b9f7..c4ab8b7e0 100644 --- a/packages/query-mongoose/src/query/aggregate.builder.ts +++ b/packages/query-mongoose/src/query/aggregate.builder.ts @@ -99,9 +99,21 @@ export class AggregateBuilder { return null; } return fields.reduce((id: Record, 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}`; + } } diff --git a/packages/query-mongoose/src/query/filter-query.builder.ts b/packages/query-mongoose/src/query/filter-query.builder.ts index 6b73c3200..456cb865e 100644 --- a/packages/query-mongoose/src/query/filter-query.builder.ts +++ b/packages/query-mongoose/src/query/filter-query.builder.ts @@ -4,15 +4,18 @@ import { AggregateBuilder, MongooseGroupAndAggregate } from './aggregate.builder import { getSchemaKey } from './helpers'; import { WhereBuilder } from './where.builder'; -type MongooseSort = Record; +const MONGOOSE_SORT_DIRECTION: Record = { + [SortDirection.ASC]: 1, + [SortDirection.DESC]: -1, +}; +type MongooseSort = Record; type MongooseQuery = { filterQuery: FilterQuery; - options: { limit?: number; skip?: number; sort?: MongooseSort[] }; + options: { limit?: number; skip?: number; sort?: MongooseSort }; }; -type MongooseAggregateQuery = { - filterQuery: FilterQuery; +type MongooseAggregateQuery = MongooseQuery & { aggregate: MongooseGroupAndAggregate; }; /** @@ -37,6 +40,7 @@ export class FilterQueryBuilder { return { filterQuery: this.buildFilterQuery(filter), aggregate: this.aggregateBuilder.build(aggregate), + options: { sort: this.buildAggregateSorting(aggregate) }, }; } @@ -48,6 +52,7 @@ export class FilterQueryBuilder { return { filterQuery: this.buildIdFilterQuery(id, filter), aggregate: this.aggregateBuilder.build(aggregate), + options: { sort: this.buildAggregateSorting(aggregate) }, }; } @@ -74,9 +79,24 @@ export class FilterQueryBuilder { * Applies the ORDER BY clause to a `typeorm` QueryBuilder. * @param sorts - an array of SortFields to create the ORDER BY clause. */ - buildSorting(sorts?: SortField[]): MongooseSort[] { - return (sorts || []).map((sort) => ({ - [getSchemaKey(sort.field.toString())]: sort.direction === SortDirection.ASC ? 'asc' : 'desc', - })); + buildSorting(sorts?: SortField[]): MongooseSort | undefined { + if (!sorts) { + return undefined; + } + return sorts.reduce((sort: MongooseSort, sortField: SortField) => { + const field = getSchemaKey(sortField.field.toString()); + const direction = MONGOOSE_SORT_DIRECTION[sortField.direction]; + return { ...sort, [field]: direction }; + }, {}); + } + + private buildAggregateSorting(aggregate: AggregateQuery): 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] }; + }, {}); } } diff --git a/packages/query-mongoose/src/services/mongoose-query.service.ts b/packages/query-mongoose/src/services/mongoose-query.service.ts index afd0b832f..e6045e227 100644 --- a/packages/query-mongoose/src/services/mongoose-query.service.ts +++ b/packages/query-mongoose/src/services/mongoose-query.service.ts @@ -72,11 +72,15 @@ export class MongooseQueryService filter: Filter, aggregateQuery: AggregateQuery, ): Promise[]> { - const { aggregate, filterQuery } = this.filterQueryBuilder.buildAggregateQuery(aggregateQuery, filter); - const aggResult = (await this.Model.aggregate>([ - { $match: filterQuery }, - { $group: aggregate }, - ]).exec()) as Record[]; + 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>(aggPipeline).exec()) as Record< + string, + unknown + >[]; return AggregateBuilder.convertToAggregateResponse(aggResult); } diff --git a/packages/query-mongoose/src/services/reference-query.service.ts b/packages/query-mongoose/src/services/reference-query.service.ts index 28aa17d51..a5c4fa38d 100644 --- a/packages/query-mongoose/src/services/reference-query.service.ts +++ b/packages/query-mongoose/src/services/reference-query.service.ts @@ -63,13 +63,18 @@ export abstract class ReferenceQueryService { if (!refFilter) { return []; } - const { filterQuery, aggregate } = referenceQueryBuilder.buildAggregateQuery( + const { filterQuery, aggregate, options } = referenceQueryBuilder.buildAggregateQuery( assembler.convertAggregateQuery(aggregateQuery), refFilter, ); - const aggResult = (await relationModel - .aggregate>([{ $match: filterQuery }, { $group: aggregate }]) - .exec()) as Record[]; + const aggPipeline: unknown[] = [{ $match: filterQuery }, { $group: aggregate }]; + if (options.sort) { + aggPipeline.push({ $sort: options.sort ?? {} }); + } + const aggResult = (await relationModel.aggregate>(aggPipeline).exec()) as Record< + string, + unknown + >[]; return AggregateBuilder.convertToAggregateResponse(aggResult); } diff --git a/packages/query-sequelize/src/query/filter-query.builder.ts b/packages/query-sequelize/src/query/filter-query.builder.ts index a54186b0e..0f24e1231 100644 --- a/packages/query-sequelize/src/query/filter-query.builder.ts +++ b/packages/query-sequelize/src/query/filter-query.builder.ts @@ -89,6 +89,7 @@ export class FilterQueryBuilder>> { 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; } @@ -98,6 +99,7 @@ export class FilterQueryBuilder>> { 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; } @@ -191,7 +193,13 @@ export class FilterQueryBuilder>> { return qb; } - applyGroupBy(qb: T, groupBy?: (keyof Entity)[], alias?: string): T { + private applyAggregate

(opts: P, aggregate: AggregateQuery): P { + // eslint-disable-next-line no-param-reassign + opts.attributes = this.aggregateBuilder.build(aggregate).attributes; + return opts; + } + + applyGroupBy(qb: T, groupBy?: (keyof Entity)[]): T { if (!groupBy) { return qb; } @@ -203,10 +211,19 @@ export class FilterQueryBuilder>> { return qb; } - private applyAggregate

(opts: P, aggregate: AggregateQuery): P { + applyAggregateSorting(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( diff --git a/packages/query-typegoose/__tests__/services/typegoose-query.service.spec.ts b/packages/query-typegoose/__tests__/services/typegoose-query.service.spec.ts index b0310227d..a6d2cfbf1 100644 --- a/packages/query-typegoose/__tests__/services/typegoose-query.service.spec.ts +++ b/packages/query-typegoose/__tests__/services/typegoose-query.service.spec.ts @@ -235,54 +235,54 @@ describe('TypegooseQueryService', () => { 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, }, }, ]); diff --git a/packages/query-typegoose/src/query/aggregate.builder.ts b/packages/query-typegoose/src/query/aggregate.builder.ts index d73463aa3..f86057301 100644 --- a/packages/query-typegoose/src/query/aggregate.builder.ts +++ b/packages/query-typegoose/src/query/aggregate.builder.ts @@ -13,7 +13,7 @@ enum AggregateFuncs { } type Aggregate = Record>; type Group = { _id: Record | null }; -export type TypegooseGroupAndAggregate = (Aggregate & Group) | Record; +export type TypegooseGroupAndAggregate = Aggregate & Group; const AGG_REGEXP = /(avg|sum|count|max|min|group_by)_(.*)/; @@ -99,9 +99,21 @@ export class AggregateBuilder { return null; } return fields.reduce((id: Record, 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 DocumentType)[]): 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 DocumentType): string { + return `group_by_${field as string}`; + } } diff --git a/packages/query-typegoose/src/query/filter-query.builder.ts b/packages/query-typegoose/src/query/filter-query.builder.ts index 718b6d0bd..895fc8fbd 100644 --- a/packages/query-typegoose/src/query/filter-query.builder.ts +++ b/packages/query-typegoose/src/query/filter-query.builder.ts @@ -5,15 +5,18 @@ import { AggregateBuilder, TypegooseGroupAndAggregate } from './aggregate.builde import { getSchemaKey } from './helpers'; import { WhereBuilder } from './where.builder'; -type TypegooseSort = Record; +const TYPEGOOSE_SORT_DIRECTION: Record = { + [SortDirection.ASC]: 1, + [SortDirection.DESC]: -1, +}; +type TypegooseSort = Record; type TypegooseQuery = { filterQuery: FilterQuery Entity>; - options: { limit?: number; skip?: number; sort?: TypegooseSort[] }; + options: { limit?: number; skip?: number; sort?: TypegooseSort }; }; -type TypegooseAggregateQuery = { - filterQuery: FilterQuery; +type TypegooseAggregateQuery = TypegooseQuery & { aggregate: TypegooseGroupAndAggregate; }; /** @@ -41,6 +44,7 @@ export class FilterQueryBuilder { return { filterQuery: this.buildFilterQuery(filter), aggregate: this.aggregateBuilder.build(aggregate), + options: { sort: this.buildAggregateSorting(aggregate) }, }; } @@ -52,6 +56,7 @@ export class FilterQueryBuilder { return { filterQuery: this.buildIdFilterQuery(id, filter), aggregate: this.aggregateBuilder.build(aggregate), + options: { sort: this.buildAggregateSorting(aggregate) }, }; } @@ -78,9 +83,24 @@ export class FilterQueryBuilder { * Applies the ORDER BY clause to a `typeorm` QueryBuilder. * @param sorts - an array of SortFields to create the ORDER BY clause. */ - buildSorting(sorts?: SortField[]): TypegooseSort[] { - return (sorts || []).map((sort) => ({ - [getSchemaKey(sort.field.toString())]: sort.direction === SortDirection.ASC ? 'asc' : 'desc', - })); + buildSorting(sorts?: SortField[]): TypegooseSort | undefined { + if (!sorts) { + return undefined; + } + return sorts.reduce((sort: TypegooseSort, sortField: SortField) => { + const field = getSchemaKey(sortField.field.toString()); + const direction = TYPEGOOSE_SORT_DIRECTION[sortField.direction]; + return { ...sort, [field]: direction }; + }, {}); + } + + private buildAggregateSorting(aggregate: AggregateQuery>): TypegooseSort | undefined { + const aggregateGroupBy = this.aggregateBuilder.getGroupBySelects(aggregate.groupBy); + if (!aggregateGroupBy) { + return undefined; + } + return aggregateGroupBy.reduce((sort: TypegooseSort, sortField) => { + return { ...sort, [getSchemaKey(sortField)]: TYPEGOOSE_SORT_DIRECTION[SortDirection.ASC] }; + }, {}); } } diff --git a/packages/query-typegoose/src/services/reference-query.service.ts b/packages/query-typegoose/src/services/reference-query.service.ts index 726e2da86..f1a8bef50 100644 --- a/packages/query-typegoose/src/services/reference-query.service.ts +++ b/packages/query-typegoose/src/services/reference-query.service.ts @@ -75,13 +75,18 @@ export abstract class ReferenceQueryService { if (!refFilter) { return []; } - const { filterQuery, aggregate } = referenceQueryBuilder.buildAggregateQuery( + const { filterQuery, aggregate, options } = referenceQueryBuilder.buildAggregateQuery( assembler.convertAggregateQuery(aggregateQuery), refFilter, ); - const aggResult = (await relationModel - .aggregate>([{ $match: filterQuery }, { $group: { _id: null, ...aggregate } }]) - .exec()) as Record[]; + const aggPipeline: unknown[] = [{ $match: filterQuery }, { $group: aggregate }]; + if (options.sort) { + aggPipeline.push({ $sort: options.sort ?? {} }); + } + const aggResult = (await relationModel.aggregate>(aggPipeline).exec()) as Record< + string, + unknown + >[]; return AggregateBuilder.convertToAggregateResponse(aggResult); } diff --git a/packages/query-typegoose/src/services/typegoose-query-service.ts b/packages/query-typegoose/src/services/typegoose-query-service.ts index 50b230d2a..3ed179a61 100644 --- a/packages/query-typegoose/src/services/typegoose-query-service.ts +++ b/packages/query-typegoose/src/services/typegoose-query-service.ts @@ -56,11 +56,15 @@ export class TypegooseQueryService filter: Filter, aggregateQuery: AggregateQuery, ): Promise[]> { - const { aggregate, filterQuery } = this.filterQueryBuilder.buildAggregateQuery(aggregateQuery, filter); - const aggResult = (await this.Model.aggregate>([ - { $match: filterQuery }, - { $group: aggregate }, - ]).exec()) as Record[]; + 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>(aggPipeline).exec()) as Record< + string, + unknown + >[]; return AggregateBuilder.convertToAggregateResponse(aggResult); } diff --git a/packages/query-typeorm/src/query/filter-query.builder.ts b/packages/query-typeorm/src/query/filter-query.builder.ts index 06c3a68ff..0d20eba0b 100644 --- a/packages/query-typeorm/src/query/filter-query.builder.ts +++ b/packages/query-typeorm/src/query/filter-query.builder.ts @@ -78,6 +78,7 @@ export class FilterQueryBuilder { let qb = this.createQueryBuilder(); qb = this.applyAggregate(qb, aggregate, qb.alias); qb = this.applyFilter(qb, query.filter, qb.alias); + qb = this.applyAggregateSorting(qb, aggregate.groupBy, qb.alias); qb = this.applyGroupBy(qb, aggregate.groupBy, qb.alias); return qb; } @@ -182,6 +183,16 @@ export class FilterQueryBuilder { }, qb); } + applyAggregateSorting>(qb: T, groupBy?: (keyof Entity)[], alias?: string): T { + if (!groupBy) { + return qb; + } + return groupBy.reduce((prevQb, field) => { + const col = alias ? `${alias}.${field as string}` : `${field as string}`; + return prevQb.addOrderBy(col, 'ASC'); + }, qb); + } + /** * Create a `typeorm` SelectQueryBuilder which can be used as an entry point to create update, delete or insert * QueryBuilders. diff --git a/packages/query-typeorm/src/query/relation-query.builder.ts b/packages/query-typeorm/src/query/relation-query.builder.ts index 33c30a09c..aa184d6a1 100644 --- a/packages/query-typeorm/src/query/relation-query.builder.ts +++ b/packages/query-typeorm/src/query/relation-query.builder.ts @@ -115,6 +115,11 @@ export class RelationQueryBuilder { let relationBuilder = this.createRelationQueryBuilder(entity); relationBuilder = this.filterQueryBuilder.applyAggregate(relationBuilder, aggregateQuery, relationBuilder.alias); relationBuilder = this.filterQueryBuilder.applyFilter(relationBuilder, query.filter, relationBuilder.alias); + relationBuilder = this.filterQueryBuilder.applyAggregateSorting( + relationBuilder, + aggregateQuery.groupBy, + relationBuilder.alias, + ); relationBuilder = this.filterQueryBuilder.applyGroupBy( relationBuilder, aggregateQuery.groupBy,