import { Query , Class , AssemblerFactory , Filter , AggregateQuery , AggregateResponse , ModifyRelationOptions , FindRelationOptions , GetByIdOptions , } from '@nestjs-query/core' ; import { Repository , RelationQueryBuilder as TypeOrmRelationQueryBuilder , ObjectLiteral } from 'typeorm' ; import lodashFilter from 'lodash.filter' ; import lodashOmit from 'lodash.omit' ; import { AggregateBuilder , EntityIndexRelation , FilterQueryBuilder , RelationQueryBuilder } from '../query' ; interface RelationMetadata { type : string | Function ; isOneToOne : boolean ; isManyToOne : boolean ; } internal export abstract class RelationQueryService < Entity > { abstract filterQueryBuilder : FilterQueryBuilder < Entity > ; abstract EntityClass : Class < Entity > ; abstract repo : Repository < Entity > ; abstract getById ( id , opts ? ) ; param RelationClass param relationName param entities param query async queryRelations < Relation > ( RelationClass , relationName , entities , query ) ; param RelationClass param dto param relationName param query async queryRelations < Relation > ( RelationClass , relationName , dto , query ) ; async queryRelations < Relation > ( RelationClass , relationName , dto , query ) { if ( Array . isArray ( dto ) ) { return this . batchQueryRelations ( RelationClass , relationName , dto , query ) ; } const assembler = AssemblerFactory . getAssembler ( RelationClass , this . getRelationEntity ( relationName ) ) ; const relationQueryBuilder = this . getRelationQueryBuilder ( relationName ) ; return assembler . convertAsyncToDTOs ( relationQueryBuilder . select ( dto , assembler . convertQuery ( query ) ) . getMany ( ) ) ; } async aggregateRelations < Relation > ( RelationClass , relationName , entities , filter , aggregate ) ; async aggregateRelations < Relation > ( RelationClass , relationName , dto , filter , aggregate ) ; async aggregateRelations < Relation > ( RelationClass , relationName , dto , filter , aggregate ) { if ( Array . isArray ( dto ) ) { return this . batchAggregateRelations ( RelationClass , relationName , dto , filter , aggregate ) ; } const assembler = AssemblerFactory . getAssembler ( RelationClass , this . getRelationEntity ( relationName ) ) ; const relationQueryBuilder = this . getRelationQueryBuilder ( relationName ) ; const aggResponse = await AggregateBuilder . asyncConvertToAggregateResponse ( relationQueryBuilder . aggregate ( dto , assembler . convertQuery ( { filter } ) , assembler . convertAggregateQuery ( aggregate ) ) . getRawMany < Record < string , unknown > > ( ) ) ; return aggResponse . map ( ( agg ) => assembler . convertAggregateResponse ( agg ) ) ; } async countRelations < Relation > ( RelationClass , relationName , entities , filter ) ; async countRelations < Relation > ( RelationClass , relationName , dto , filter ) ; async countRelations < Relation > ( RelationClass , relationName , dto , filter ) { if ( Array . isArray ( dto ) ) { return this . batchCountRelations ( RelationClass , relationName , dto , filter ) ; } const assembler = AssemblerFactory . getAssembler ( RelationClass , this . getRelationEntity ( relationName ) ) ; const relationQueryBuilder = this . getRelationQueryBuilder ( relationName ) ; return relationQueryBuilder . select ( dto , assembler . convertQuery ( { filter } ) ) . getCount ( ) ; } param RelationClass param relationName param dtos param opts async findRelation < Relation > ( RelationClass , relationName , dtos , opts ? ) ; param RelationClass param dto param relationName param opts async findRelation < Relation > ( RelationClass , relationName , dto , opts ? ) ; async findRelation < Relation > ( RelationClass , relationName , dto , opts ? ) { if ( Array . isArray ( dto ) ) { return this . batchFindRelations ( RelationClass , relationName , dto , opts ) ; } const assembler = AssemblerFactory . getAssembler ( RelationClass , this . getRelationEntity ( relationName ) ) ; const relationEntity = await this . getRelationQueryBuilder ( relationName ) . select ( dto , { filter : opts ?. filter , paging : { limit : 1 } } ) . getOne ( ) ; return relationEntity ? assembler . convertToDTO ( relationEntity ) : undefined ; } param id param relationName param relationIds param opts async addRelations < Relation > ( relationName , id , relationIds , opts ? ) { const entity = await this . getById ( id , opts ) ; const relations = await this . getRelations ( relationName , relationIds , opts ?. relationFilter ) ; if ( ! this . foundAllRelations ( relationIds , relations ) ) { throw new Error ( `Unable to find all ${ relationName } to add to ${ this . EntityClass . name }` ) ; } await this . createTypeormRelationQueryBuilder ( entity , relationName ) . add ( relationIds ) ; return entity ; } param id param relationName param relationIds param opts async setRelations < Relation > ( relationName , id , relationIds , opts ? ) { const entity = await this . getById ( id , opts ) ; const relations = await this . getRelations ( relationName , relationIds , opts ?. relationFilter ) ; if ( relationIds . length ) { if ( ! this . foundAllRelations ( relationIds , relations ) ) { throw new Error ( `Unable to find all ${ relationName } to set on ${ this . EntityClass . name }` ) ; } } const relationQueryBuilder = this . getRelationQueryBuilder ( relationName ) ; const existingRelations = await relationQueryBuilder . select ( entity , { filter : opts ?. relationFilter } ) . getMany ( ) ; await this . createTypeormRelationQueryBuilder ( entity , relationName ) . addAndRemove ( relations , existingRelations ) ; return entity ; } param id param relationName param relationId param opts async setRelation < Relation > ( relationName , id , relationId , opts ? ) { const entity = await this . getById ( id , opts ) ; const relation = ( await this . getRelations ( relationName , [ relationId ] , opts ?. relationFilter ) ) [ 0 ] ; if ( ! relation ) { throw new Error ( `Unable to find ${ relationName } to set on ${ this . EntityClass . name }` ) ; } await this . createTypeormRelationQueryBuilder ( entity , relationName ) . set ( relationId ) ; return entity ; } param id param relationName param relationIds param opts async removeRelations < Relation > ( relationName , id , relationIds , opts ? ) { const entity = await this . getById ( id , opts ) ; const relations = await this . getRelations ( relationName , relationIds , opts ?. relationFilter ) ; if ( ! this . foundAllRelations ( relationIds , relations ) ) { throw new Error ( `Unable to find all ${ relationName } to remove from ${ this . EntityClass . name }` ) ; } await this . createTypeormRelationQueryBuilder ( entity , relationName ) . remove ( relationIds ) ; return entity ; } param id param relationName param relationId async removeRelation < Relation > ( relationName , id , relationId , opts ? ) { const entity = await this . getById ( id , opts ) ; const relation = ( await this . getRelations ( relationName , [ relationId ] , opts ?. relationFilter ) ) [ 0 ] ; if ( ! relation ) { throw new Error ( `Unable to find ${ relationName } to remove from ${ this . EntityClass . name }` ) ; } const meta = this . getRelationMeta ( relationName ) ; if ( meta . isOneToOne || meta . isManyToOne ) { await this . createTypeormRelationQueryBuilder ( entity , relationName ) . set ( null ) ; } else { await this . createTypeormRelationQueryBuilder ( entity , relationName ) . remove ( relationId ) ; } return entity ; } getRelationQueryBuilder < Relation > ( name ) { return new RelationQueryBuilder ( this . repo , name ) ; } param RelationClass param entities param relationName param query private async batchQueryRelations < Relation > ( RelationClass , relationName , entities , query ) { const assembler = AssemblerFactory . getAssembler ( RelationClass , this . getRelationEntity ( relationName ) ) ; const relationQueryBuilder = this . getRelationQueryBuilder ( relationName ) ; const convertedQuery = assembler . convertQuery ( query ) ; const entityRelations = await relationQueryBuilder . batchSelect ( entities , convertedQuery ) . getRawAndEntities ( ) ; return entityRelations . raw . reduce ( ( results , rawRelation ) => { const index = rawRelation . __nestjsQuery__entityIndex__ ; const e = entities [ index ] ; const relationDtos = assembler . convertToDTOs ( this . getRelationsFromPrimaryKeys ( relationQueryBuilder , rawRelation , entityRelations . entities ) ) ; return results . set ( e , [ ... ( results . get ( e ) ?? [ ] ) , ... relationDtos ] ) ; } , new Map < Entity , Relation [ ] > ( ) ) ; } param RelationClass param entities param relationName param query private async batchAggregateRelations < Relation > ( RelationClass , relationName , entities , filter , aggregate ) { const assembler = AssemblerFactory . getAssembler ( RelationClass , this . getRelationEntity ( relationName ) ) ; const relationQueryBuilder = this . getRelationQueryBuilder < Relation > ( relationName ) ; const convertedQuery = assembler . convertQuery ( { filter } ) ; const rawAggregates = await relationQueryBuilder . batchAggregate ( entities , convertedQuery , aggregate ) . getRawMany < EntityIndexRelation < Record < string , unknown > > > ( ) ; return rawAggregates . reduce ( ( results , relationAgg ) => { const index = relationAgg . __nestjsQuery__entityIndex__ ; const e = entities [ index ] ; const resultingAgg = results . get ( e ) ?? [ ] ; results . set ( e , [ ... resultingAgg , ... AggregateBuilder . convertToAggregateResponse ( [ lodashOmit ( relationAgg , relationQueryBuilder . entityIndexColName ) , ] ) , ] ) ; return results ; } , new Map < Entity , AggregateResponse < Relation > [ ] > ( ) ) ; } param RelationClass param entities param relationName param filter private async batchCountRelations < Relation > ( RelationClass , relationName , entities , filter ) { const assembler = AssemblerFactory . getAssembler ( RelationClass , this . getRelationEntity ( relationName ) ) ; const relationQueryBuilder = this . getRelationQueryBuilder ( relationName ) ; const convertedQuery = assembler . convertQuery ( { filter } ) ; const entityRelations = await Promise . all ( entities . map ( ( e ) => relationQueryBuilder . select ( e , convertedQuery ) . getCount ( ) ) ) ; return entityRelations . reduce ( ( results , relationCount , index ) => { const e = entities [ index ] ; results . set ( e , relationCount ) ; return results ; } , new Map < Entity , number > ( ) ) ; } param RelationClass param dtos param relationName param query private async batchFindRelations < Relation > ( RelationClass , relationName , dtos , opts ? ) { const batchResults = await this . batchQueryRelations ( RelationClass , relationName , dtos , { paging : { limit : 1 } , filter : opts ?. filter , } ) ; const results = new Map < Entity , Relation > ( ) ; batchResults . forEach ( ( relation , dto ) => { results . set ( dto , relation [ 0 ] ) ; } ) ; return results ; } private createTypeormRelationQueryBuilder ( entity , relationName ) { return this . repo . createQueryBuilder ( ) . relation ( relationName ) . of ( entity ) ; } private getRelationMeta ( relationName ) { const relationMeta = this . repo . metadata . relations . find ( ( r ) => r . propertyName === relationName ) ; if ( ! relationMeta ) { throw new Error ( `Unable to find relation ${ relationName } on ${ this . EntityClass . name }` ) ; } return relationMeta ; } private getRelationEntity ( relationName ) { const relationMeta = this . getRelationMeta ( relationName ) ; if ( typeof relationMeta . type === 'string' ) { return this . repo . manager . getRepository ( relationMeta . type ) . target as Class < unknown > ; } return relationMeta . type as Class < unknown > ; } private getRelationsFromPrimaryKeys < Relation > ( relationBuilder , rawResult , relations ) { const pks = relationBuilder . getRelationPrimaryKeysPropertyNameAndColumnsName ( ) ; const filter = pks . reduce ( ( keys , key ) => ( { ... keys , [ key . propertyName ] : rawResult [ key . columnName ] } ) , { } as Partial < Entity > ) ; return lodashFilter ( relations , filter ) as Relation [ ] ; } private getRelations < Relation > ( relationName , ids , filter ? ) { const relationQueryBuilder = this . getRelationQueryBuilder < Relation > ( relationName ) . filterQueryBuilder ; return relationQueryBuilder . selectById ( ids , { filter } ) . getMany ( ) ; } private foundAllRelations < Relation > ( relationIds , relations ) { return new Set ( [ ... relationIds ] ) . size === relations . length ; } }