Skip to content

Commit

Permalink
fix(query-typeorm): Fixed group by for aggregated date fields
Browse files Browse the repository at this point in the history
  • Loading branch information
TriPSs committed Jul 7, 2022
1 parent 97cbfd2 commit 7ffeaf6
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 30 deletions.
2 changes: 1 addition & 1 deletion packages/query-typeorm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ptc-org/nestjs-query-typeorm",
"version": "1.0.0-alpha.4",
"version": "1.0.0-alpha.5",
"description": "Typeorm adapter for @ptc-org/nestjs-query-core",
"author": "doug-martin <[email protected]>",
"homepage": "https://github.com/tripss/nestjs-query#readme",
Expand Down
40 changes: 32 additions & 8 deletions packages/query-typeorm/src/query/aggregate.builder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SelectQueryBuilder } from 'typeorm';
import { Repository, SelectQueryBuilder } from 'typeorm';
import { AggregateQuery, AggregateResponse } from '@ptc-org/nestjs-query-core';
import { BadRequestException } from '@nestjs/common';
import { camelCase } from 'camel-case';
Expand All @@ -18,16 +18,18 @@ const AGG_REGEXP = /(AVG|SUM|COUNT|MAX|MIN|GROUP_BY)_(.*)/;
* Builds a WHERE clause from a Filter.
*/
export class AggregateBuilder<Entity> {
constructor(readonly repo: Repository<Entity>) {}

// eslint-disable-next-line @typescript-eslint/no-shadow
static async asyncConvertToAggregateResponse<Entity>(
public static async asyncConvertToAggregateResponse<Entity>(
responsePromise: Promise<Record<string, unknown>[]>
): Promise<AggregateResponse<Entity>[]> {
const aggResponse = await responsePromise;
return this.convertToAggregateResponse(aggResponse);
}

// eslint-disable-next-line @typescript-eslint/no-shadow
static getAggregateSelects<Entity>(query: AggregateQuery<Entity>): string[] {
public static getAggregateSelects<Entity>(query: AggregateQuery<Entity>): string[] {
return [...this.getAggregateGroupBySelects(query), ...this.getAggregateFuncSelects(query)];
}

Expand All @@ -52,17 +54,17 @@ export class AggregateBuilder<Entity> {
}

// eslint-disable-next-line @typescript-eslint/no-shadow
static getAggregateAlias<Entity>(func: AggregateFuncs, field: keyof Entity): string {
public static getAggregateAlias<Entity>(func: AggregateFuncs, field: keyof Entity): string {
return `${func}_${field as string}`;
}

// eslint-disable-next-line @typescript-eslint/no-shadow
static getGroupByAlias<Entity>(field: keyof Entity): string {
public static getGroupByAlias<Entity>(field: keyof Entity): string {
return `GROUP_BY_${field as string}`;
}

// eslint-disable-next-line @typescript-eslint/no-shadow
static convertToAggregateResponse<Entity>(rawAggregates: Record<string, unknown>[]): AggregateResponse<Entity>[] {
public static convertToAggregateResponse<Entity>(rawAggregates: Record<string, unknown>[]): AggregateResponse<Entity>[] {
return rawAggregates.map((response) => {
return Object.keys(response).reduce((agg: AggregateResponse<Entity>, resultField: string) => {
const matchResult = AGG_REGEXP.exec(resultField);
Expand All @@ -82,13 +84,27 @@ export class AggregateBuilder<Entity> {
});
}

/**
* Returns the corrected fields for orderBy and groupBy
*/
public getCorrectedField(alias: string, field: string) {
const col = alias ? `${alias}.${field}` : `${field}`;
const meta = this.repo.metadata.findColumnWithPropertyName(`${field}`);

if (meta && this.repo.manager.connection.driver.normalizeType(meta) === 'datetime') {
return `DATE(${col})`;
}

return col;
}

/**
* Builds a aggregate SELECT clause from a aggregate.
* @param qb - the `typeorm` SelectQueryBuilder
* @param aggregate - the aggregates to select.
* @param alias - optional alias to use to qualify an identifier
*/
build<Qb extends SelectQueryBuilder<Entity>>(qb: Qb, aggregate: AggregateQuery<Entity>, alias?: string): Qb {
public build<Qb extends SelectQueryBuilder<Entity>>(qb: Qb, aggregate: AggregateQuery<Entity>, alias?: string): Qb {
const selects = [
...this.createGroupBySelect(aggregate.groupBy, alias),
...this.createAggSelect(AggregateFuncs.COUNT, aggregate.count, alias),
Expand Down Expand Up @@ -118,9 +134,17 @@ export class AggregateBuilder<Entity> {
if (!fields) {
return [];
}

return fields.map((field) => {
const col = alias ? `${alias}.${field as string}` : (field as string);
return [`${col}`, AggregateBuilder.getGroupByAlias(field)];
const groupByAlias = AggregateBuilder.getGroupByAlias(field);

const meta = this.repo.metadata.findColumnWithPropertyName(`${field as string}`);
if (meta && this.repo.manager.connection.driver.normalizeType(meta) === 'datetime') {
return [`DATE(${col})`, groupByAlias];
}

return [`${col}`, groupByAlias];
});
}
}
45 changes: 25 additions & 20 deletions packages/query-typeorm/src/query/filter-query.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ interface Groupable<Entity> extends QueryBuilder<Entity> {
*/
interface Pageable<Entity> extends QueryBuilder<Entity> {
limit(limit?: number): this;

offset(offset?: number): this;

skip(skip?: number): this;

take(take?: number): this;
}

Expand All @@ -56,15 +59,15 @@ export class FilterQueryBuilder<Entity> {
constructor(
readonly repo: Repository<Entity>,
readonly whereBuilder: WhereBuilder<Entity> = new WhereBuilder<Entity>(),
readonly aggregateBuilder: AggregateBuilder<Entity> = new AggregateBuilder<Entity>()
readonly aggregateBuilder: AggregateBuilder<Entity> = new AggregateBuilder<Entity>(repo)
) {}

/**
* Create a `typeorm` SelectQueryBuilder with `WHERE`, `ORDER BY` and `LIMIT/OFFSET` clauses.
*
* @param query - the query to apply.
*/
select(query: Query<Entity>): SelectQueryBuilder<Entity> {
public select(query: Query<Entity>): SelectQueryBuilder<Entity> {
const hasRelations = this.filterHasRelations(query.filter);
let qb = this.createQueryBuilder();
qb = hasRelations
Expand All @@ -76,7 +79,7 @@ export class FilterQueryBuilder<Entity> {
return qb;
}

selectById(id: string | number | (string | number)[], query: Query<Entity>): SelectQueryBuilder<Entity> {
public selectById(id: string | number | (string | number)[], query: Query<Entity>): SelectQueryBuilder<Entity> {
const hasRelations = this.filterHasRelations(query.filter);
let qb = this.createQueryBuilder();
qb = hasRelations
Expand All @@ -89,12 +92,16 @@ export class FilterQueryBuilder<Entity> {
return qb;
}

aggregate(query: Query<Entity>, aggregate: AggregateQuery<Entity>): SelectQueryBuilder<Entity> {
public aggregate(query: Query<Entity>, aggregate: AggregateQuery<Entity>): SelectQueryBuilder<Entity> {
const hasRelations = this.filterHasRelations(query.filter);
let qb = this.createQueryBuilder();
qb = hasRelations
? this.applyRelationJoinsRecursive(qb, this.getReferencedRelationsRecursive(this.repo.metadata, query.filter))
: qb;
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);
qb = this.applyAggregateGroupBy(qb, aggregate.groupBy, qb.alias);
return qb;
}

Expand All @@ -103,7 +110,7 @@ export class FilterQueryBuilder<Entity> {
*
* @param query - the query to apply.
*/
delete(query: Query<Entity>): DeleteQueryBuilder<Entity> {
public delete(query: Query<Entity>): DeleteQueryBuilder<Entity> {
return this.applyFilter(this.repo.createQueryBuilder().delete(), query.filter);
}

Expand All @@ -112,7 +119,7 @@ export class FilterQueryBuilder<Entity> {
*
* @param query - the query to apply.
*/
softDelete(query: Query<Entity>): SoftDeleteQueryBuilder<Entity> {
public softDelete(query: Query<Entity>): SoftDeleteQueryBuilder<Entity> {
return this.applyFilter(this.repo.createQueryBuilder().softDelete() as SoftDeleteQueryBuilder<Entity>, query.filter);
}

Expand All @@ -121,7 +128,7 @@ export class FilterQueryBuilder<Entity> {
*
* @param query - the query to apply.
*/
update(query: Query<Entity>): UpdateQueryBuilder<Entity> {
public update(query: Query<Entity>): UpdateQueryBuilder<Entity> {
const qb = this.applyFilter(this.repo.createQueryBuilder().update(), query.filter);
return this.applySorting(qb, query.sorting);
}
Expand All @@ -132,7 +139,7 @@ export class FilterQueryBuilder<Entity> {
* @param paging - the Paging options.
* @param useSkipTake - if skip/take should be used instead of limit/offset.
*/
applyPaging<P extends Pageable<Entity>>(qb: P, paging?: Paging, useSkipTake?: boolean): P {
public applyPaging<P extends Pageable<Entity>>(qb: P, paging?: Paging, useSkipTake?: boolean): P {
if (!paging) {
return qb;
}
Expand All @@ -151,7 +158,7 @@ export class FilterQueryBuilder<Entity> {
* @param aggregate - the aggregates to select.
* @param alias - optional alias to use to qualify an identifier
*/
applyAggregate<Qb extends SelectQueryBuilder<Entity>>(qb: Qb, aggregate: AggregateQuery<Entity>, alias?: string): Qb {
public applyAggregate<Qb extends SelectQueryBuilder<Entity>>(qb: Qb, aggregate: AggregateQuery<Entity>, alias?: string): Qb {
return this.aggregateBuilder.build(qb, aggregate, alias);
}

Expand All @@ -162,7 +169,7 @@ export class FilterQueryBuilder<Entity> {
* @param filter - the filter.
* @param alias - optional alias to use to qualify an identifier
*/
applyFilter<Where extends WhereExpression>(qb: Where, filter?: Filter<Entity>, alias?: string): Where {
public applyFilter<Where extends WhereExpression>(qb: Where, filter?: Filter<Entity>, alias?: string): Where {
if (!filter) {
return qb;
}
Expand All @@ -175,7 +182,7 @@ export class FilterQueryBuilder<Entity> {
* @param sorts - an array of SortFields to create the ORDER BY clause.
* @param alias - optional alias to use to qualify an identifier
*/
applySorting<T extends Sortable<Entity>>(qb: T, sorts?: SortField<Entity>[], alias?: string): T {
public applySorting<T extends Sortable<Entity>>(qb: T, sorts?: SortField<Entity>[], alias?: string): T {
if (!sorts) {
return qb;
}
Expand All @@ -185,23 +192,21 @@ export class FilterQueryBuilder<Entity> {
}, qb);
}

applyGroupBy<T extends Groupable<Entity>>(qb: T, groupBy?: (keyof Entity)[], alias?: string): T {
public applyAggregateGroupBy<T extends Groupable<Entity>>(qb: T, groupBy?: (keyof Entity)[], alias?: string): T {
if (!groupBy) {
return qb;
}
return groupBy.reduce((prevQb, group) => {
const col = alias ? `${alias}.${group as string}` : `${group as string}`;
return prevQb.addGroupBy(col);
return groupBy.reduce((prevQb, field) => {
return prevQb.addGroupBy(this.aggregateBuilder.getCorrectedField(alias, field as string));
}, qb);
}

applyAggregateSorting<T extends Sortable<Entity>>(qb: T, groupBy?: (keyof Entity)[], alias?: string): T {
public applyAggregateSorting<T extends Sortable<Entity>>(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');
return prevQb.addOrderBy(this.aggregateBuilder.getCorrectedField(alias, field as string), 'ASC');
}, qb);
}

Expand All @@ -220,7 +225,7 @@ export class FilterQueryBuilder<Entity> {
*
* @returns the query builder for chaining
*/
applyRelationJoinsRecursive(
public applyRelationJoinsRecursive(
qb: SelectQueryBuilder<Entity>,
relationsMap?: NestedRecord,
alias?: string
Expand Down
6 changes: 5 additions & 1 deletion packages/query-typeorm/src/query/relation-query.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,11 @@ export class RelationQueryBuilder<Entity, Relation> {
aggregateQuery.groupBy,
relationBuilder.alias
);
relationBuilder = this.filterQueryBuilder.applyGroupBy(relationBuilder, aggregateQuery.groupBy, relationBuilder.alias);
relationBuilder = this.filterQueryBuilder.applyAggregateGroupBy(
relationBuilder,
aggregateQuery.groupBy,
relationBuilder.alias
);
return relationBuilder;
}

Expand Down

0 comments on commit 7ffeaf6

Please sign in to comment.