import { SelectQueryBuilder } from 'typeorm' ; import { AggregateQuery , AggregateResponse } from '@nestjs-query/core' ; import { BadRequestException } from '@nestjs/common' ; import { camelCase } from 'camel-case' ; enum AggregateFuncs { AVG = 'AVG' , SUM = 'SUM' , COUNT = 'COUNT' , MAX = 'MAX' , MIN = 'MIN' } const AGG_REGEXP = /(AVG|SUM|COUNT|MAX|MIN|GROUP_BY)_(.*)/ ; internal export class AggregateBuilder < Entity > { static async asyncConvertToAggregateResponse < Entity > ( responsePromise ) { const aggResponse = await responsePromise ; return this . convertToAggregateResponse ( aggResponse ) ; } static getAggregateSelects < Entity > ( query ) { return [ ... this . getAggregateGroupBySelects ( query ) , ... this . getAggregateFuncSelects ( query ) ] ; } private static getAggregateGroupBySelects < Entity > ( query ) { return ( query . groupBy ?? [ ] ) . map ( ( f ) => this . getGroupByAlias ( f ) ) ; } private static getAggregateFuncSelects < Entity > ( query ) { const aggs = [ [ AggregateFuncs . COUNT , query . count ] , [ AggregateFuncs . SUM , query . sum ] , [ AggregateFuncs . AVG , query . avg ] , [ AggregateFuncs . MAX , query . max ] , [ AggregateFuncs . MIN , query . min ] , ] ; return aggs . reduce ( ( cols , [ func , fields ] ) => { const aliases = ( fields ?? [ ] ) . map ( ( f ) => this . getAggregateAlias ( func , f ) ) ; return [ ... cols , ... aliases ] ; } , [ ] as string [ ] ) ; } static getAggregateAlias < Entity > ( func , field ) { return `${ func }_${ field as string }` ; } static getGroupByAlias < Entity > ( field ) { return `GROUP_BY_${ field as string }` ; } static convertToAggregateResponse < Entity > ( rawAggregates ) { return rawAggregates . map ( ( response ) => { return Object . keys ( response ) . reduce ( ( agg , resultField ) => { const matchResult = AGG_REGEXP . exec ( resultField ) ; if ( ! matchResult ) { throw new Error ( 'Unknown aggregate column encountered.' ) ; } const [ matchedFunc , matchedFieldName ] = matchResult . slice ( 1 ) ; const aggFunc = camelCase ( matchedFunc . toLowerCase ( ) ) as keyof AggregateResponse < Entity > ; const fieldName = matchedFieldName as keyof Entity ; const aggResult = agg [ aggFunc ] || { } ; return { ... agg , [ aggFunc ] : { ... aggResult , [ fieldName ] : response [ resultField ] } , } ; } , { } ) ; } ) ; } param qb param aggregate param alias build < Qb extends SelectQueryBuilder < Entity > > ( qb , aggregate , alias ? ) { const selects = [ ... this . createGroupBySelect ( aggregate . groupBy , alias ) , ... this . createAggSelect ( AggregateFuncs . COUNT , aggregate . count , alias ) , ... this . createAggSelect ( AggregateFuncs . SUM , aggregate . sum , alias ) , ... this . createAggSelect ( AggregateFuncs . AVG , aggregate . avg , alias ) , ... this . createAggSelect ( AggregateFuncs . MAX , aggregate . max , alias ) , ... this . createAggSelect ( AggregateFuncs . MIN , aggregate . min , alias ) , ] ; if ( ! selects . length ) { throw new BadRequestException ( 'No aggregate fields found.' ) ; } const [ head , ... tail ] = selects ; return tail . reduce ( ( acc , [ select , selectAlias ] ) => acc . addSelect ( select , selectAlias ) , qb . select ( head [ 0 ] , head [ 1 ] ) ) ; } private createAggSelect ( func , fields ? , alias ? ) { if ( ! fields ) { return [ ] ; } return fields . map ( ( field ) => { const col = alias ? `${ alias }.${ field as string }` : ( field as string ) ; return [ `${ func }(${ col })` , AggregateBuilder . getAggregateAlias ( func , field ) ] ; } ) ; } private createGroupBySelect ( fields ? , alias ? ) { if ( ! fields ) { return [ ] ; } return fields . map ( ( field ) => { const col = alias ? `${ alias }.${ field as string }` : ( field as string ) ; return [ `${ col }` , AggregateBuilder . getGroupByAlias ( field ) ] ; } ) ; } }