import { Filter , Paging , Query , SortField , getFilterFields , AggregateQuery } from '@nestjs-query/core' ; import { DeleteQueryBuilder , QueryBuilder , Repository , SelectQueryBuilder , UpdateQueryBuilder , WhereExpression , EntityMetadata , } from 'typeorm' ; import { SoftDeleteQueryBuilder } from 'typeorm/query-builder/SoftDeleteQueryBuilder' ; import { AggregateBuilder } from './aggregate.builder' ; import { WhereBuilder } from './where.builder' ; import merge from 'lodash.merge' ; internal interface Sortable < Entity > extends QueryBuilder < Entity > { addOrderBy ( sort , order ? , nulls ? ) : this ; } interface Groupable < Entity > extends QueryBuilder < Entity > { addGroupBy ( groupBy ) : this ; } internal interface Pageable < Entity > extends QueryBuilder < Entity > { limit ( limit ? ) : this ; offset ( offset ? ) : this ; skip ( skip ? ) : this ; take ( take ? ) : this ; } internal interface R < T > { [ keys ] : T ; } internal export type NestedRecord = R < NestedRecord > ; internal export class FilterQueryBuilder < Entity > { constructor ( readonly repo , readonly whereBuilder = new WhereBuilder < Entity > ( ) , readonly aggregateBuilder = new AggregateBuilder < Entity > ( ) ) { } param query select ( query ) { 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 . applyFilter ( qb , query . filter , qb . alias ) ; qb = this . applySorting ( qb , query . sorting , qb . alias ) ; qb = this . applyPaging ( qb , query . paging , hasRelations ) ; return qb ; } selectById ( id , query ) { const hasRelations = this . filterHasRelations ( query . filter ) ; let qb = this . createQueryBuilder ( ) ; qb = hasRelations ? this . applyRelationJoinsRecursive ( qb , this . getReferencedRelationsRecursive ( this . repo . metadata , query . filter ) ) : qb ; qb = qb . andWhereInIds ( id ) ; qb = this . applyFilter ( qb , query . filter , qb . alias ) ; qb = this . applySorting ( qb , query . sorting , qb . alias ) ; qb = this . applyPaging ( qb , query . paging , hasRelations ) ; return qb ; } aggregate ( query , aggregate ) { 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 ; } param query delete ( query ) { return this . applyFilter ( this . repo . createQueryBuilder ( ) . delete ( ) , query . filter ) ; } param query softDelete ( query ) { return this . applyFilter ( this . repo . createQueryBuilder ( ) . softDelete ( ) as SoftDeleteQueryBuilder < Entity > , query . filter ) ; } param query update ( query ) { const qb = this . applyFilter ( this . repo . createQueryBuilder ( ) . update ( ) , query . filter ) ; return this . applySorting ( qb , query . sorting ) ; } param qb param paging param useSkipTake applyPaging < P extends Pageable < Entity > > ( qb , paging ? , useSkipTake ? ) { if ( ! paging ) { return qb ; } if ( useSkipTake ) { return qb . take ( paging . limit ) . skip ( paging . offset ) ; } return qb . limit ( paging . limit ) . offset ( paging . offset ) ; } param qb param aggregate param alias applyAggregate < Qb extends SelectQueryBuilder < Entity > > ( qb , aggregate , alias ? ) { return this . aggregateBuilder . build ( qb , aggregate , alias ) ; } param qb param filter param alias applyFilter < Where extends WhereExpression > ( qb , filter ? , alias ? ) { if ( ! filter ) { return qb ; } return this . whereBuilder . build ( qb , filter , this . getReferencedRelationsRecursive ( this . repo . metadata , filter ) , alias ) ; } param qb param sorts param alias applySorting < T extends Sortable < Entity > > ( qb , sorts ? , alias ? ) { if ( ! sorts ) { return qb ; } return sorts . reduce ( ( prevQb , { field , direction , nulls } ) => { const col = alias ? `${ alias }.${ field as string }` : `${ field as string }` ; return prevQb . addOrderBy ( col , direction , nulls ) ; } , qb ) ; } applyGroupBy < T extends Groupable < Entity > > ( qb , groupBy ? , alias ? ) { if ( ! groupBy ) { return qb ; } return groupBy . reduce ( ( prevQb , group ) => { const col = alias ? `${ alias }.${ group as string }` : `${ group as string }` ; return prevQb . addGroupBy ( col ) ; } , qb ) ; } applyAggregateSorting < T extends Sortable < Entity > > ( qb , groupBy ? , alias ? ) { 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.
     */ private createQueryBuilder ( ) { return this . repo . createQueryBuilder ( ) ; } param qb param relationsMap returns applyRelationJoinsRecursive ( qb , relationsMap ? , alias ? ) { if ( ! relationsMap ) { return qb ; } const referencedRelations = Object . keys ( relationsMap ) ; return referencedRelations . reduce ( ( rqb , relation ) => { return this . applyRelationJoinsRecursive ( rqb . leftJoin ( `${ alias ?? rqb . alias }.${ relation }` , relation ) , relationsMap [ relation ] , relation ) ; } , qb ) ; } param filter private returns filterHasRelations ( filter ? ) { if ( ! filter ) { return false ; } return this . getReferencedRelations ( filter ) . length > 0 ; } private getReferencedRelations ( filter ) { const { relationNames } = this ; const referencedFields = getFilterFields ( filter ) ; return referencedFields . filter ( ( f ) => relationNames . includes ( f ) ) ; } getReferencedRelationsRecursive ( metadata , filter = { } ) { const referencedFields = Array . from ( new Set ( Object . keys ( filter ) as ( keyof Filter < unknown > ) [ ] ) ) ; return referencedFields . reduce ( ( prev , curr ) => { const currFilterValue = filter [ curr ] ; if ( ( curr === 'and' || curr === 'or' ) && currFilterValue ) { for ( const subFilter of currFilterValue ) { prev = merge ( prev , this . getReferencedRelationsRecursive ( metadata , subFilter ) ) ; } } const referencedRelation = metadata . relations . find ( ( r ) => r . propertyName === curr ) ; if ( ! referencedRelation ) return prev ; return { ... prev , [ curr ] : merge ( ( prev as NestedRecord ) [ curr ] , this . getReferencedRelationsRecursive ( referencedRelation . inverseEntityMetadata , currFilterValue ) ) , } ; } , { } ) ; } private get relationNames ( ) : string [ ] { return this . repo . metadata . relations . map ( ( r ) => r . propertyName ) ; } }