import type { AnyEntity , Dictionary , EntityMetadata , EntityProperty , FilterQuery , PopulateOptions , Primary } from '../typings' ; import type { EntityManager } from '../EntityManager' ; import { QueryHelper } from '../utils/QueryHelper' ; import { Utils } from '../utils/Utils' ; import { ValidationError } from '../errors' ; import type { Collection } from './Collection' ; import type { LockMode , QueryOrderMap } from '../enums' ; import { LoadStrategy , QueryOrder , ReferenceType } from '../enums' ; import { Reference } from './Reference' ; import type { EntityField , FindOptions } from '../drivers/IDatabaseDriver' ; import type { MetadataStorage } from '../metadata/MetadataStorage' ; import type { Platform } from '../platforms/Platform' ; export type EntityLoaderOptions < T , P extends string = never > = { where ? : FilterQuery < T > ; fields ? : readonly EntityField < T , P > [ ] ; orderBy ? : QueryOrderMap < T > | QueryOrderMap < T > [ ] ; refresh ? : boolean ; validate ? : boolean ; lookup ? : boolean ; convertCustomTypes ? : boolean ; ignoreLazyScalarProperties ? : boolean ; filters ? : Dictionary < boolean | Dictionary > | string [ ] | boolean ; strategy ? : LoadStrategy ; lockMode ? : Exclude < LockMode , LockMode . OPTIMISTIC > ; } ; export class EntityLoader { private readonly metadata = this . em . getMetadata ( ) ; private readonly driver = this . em . getDriver ( ) ; constructor ( private readonly em ) { } /**
     * Loads specified relations in batch. This will execute one query for each relation, that will populate it on all of the specified entities.
     */ async populate < T extends AnyEntity < T > , P extends string = never > ( entityName , entities , populate , options ) { if ( entities . length === 0 || populate === false ) { return ; } options . where ??= { } as FilterQuery < T > ; options . orderBy ??= { } ; options . filters ??= { } ; options . lookup ??= true ; options . validate ??= true ; options . refresh ??= false ; options . convertCustomTypes ??= true ; populate = this . normalizePopulate < T > ( entityName , populate , options . strategy , options . lookup ) ; const invalid = populate . find ( ( { field } ) => ! this . em . canPopulate ( entityName , field ) ) ; if ( options . validate && invalid ) { throw ValidationError . invalidPropertyName ( entityName , invalid . field ) ; } entities . forEach ( e => e . __helper ! . __serializationContext . populate = e . __helper ! . __serializationContext . populate ?? populate as PopulateOptions < T > [ ] ) ; for ( const pop of populate ) { await this . populateField < T > ( entityName , entities , pop , options as Required < EntityLoaderOptions < T > > ) ; } } normalizePopulate < T > ( entityName , populate , strategy ? , lookup = true ) { if ( populate === true || populate . some ( p => p . all ) ) { populate = this . lookupAllRelationships ( entityName ) ; } else { populate = Utils . asArray ( populate ) ; } if ( lookup ) { populate = this . lookupEagerLoadedRelationships ( entityName , populate , strategy ) ; } populate . forEach ( p => { if ( ! p . field . includes ( '.' ) ) { return ; } const [ f , ... parts ] = p . field . split ( '.' ) ; p . field = f ; p . children = p . children || [ ] ; const prop = this . metadata . find ( entityName ) ! . properties [ f ] ; p . children . push ( this . expandNestedPopulate ( prop . type , parts , p . strategy , p . all ) ) ; } ) ; return this . mergeNestedPopulate ( populate ) ; } /**
     * merge multiple populates for the same entity with different children
     */ private mergeNestedPopulate < T > ( populate ) { const tmp = populate . reduce ( ( ret , item ) => { if ( ! ret [ item . field ] ) { ret [ item . field ] = item ; return ret ; } if ( ! ret [ item . field ] . children && item . children ) { ret [ item . field ] . children = item . children ; } else if ( ret [ item . field ] . children && item . children ) { ret [ item . field ] . children ! . push ( ... item . children ! ) ; } return ret ; } , { } as Dictionary < PopulateOptions < T > > ) ; return Object . values ( tmp ) . map ( item => { if ( item . children ) { item . children = this . mergeNestedPopulate < T > ( item . children ) ; } return item ; } ) ; } /**
     * Expands `books.perex` like populate to use `children` array instead of the dot syntax
     */ private expandNestedPopulate < T > ( entityName , parts , strategy ? , all ? ) { const meta = this . metadata . find ( entityName ) ! ; const field = parts . shift ( ) ! ; const prop = meta . properties [ field ] ; const ret = { field , strategy , all } as PopulateOptions < T > ; if ( parts . length > 0 ) { ret . children = [ this . expandNestedPopulate ( prop . type , parts , strategy ) ] ; } return ret ; } /**
     * preload everything in one call (this will update already existing references in IM)
     */ private async populateMany < T extends AnyEntity < T > > ( entityName , entities , populate , options ) { const field = populate . field as keyof T ; const meta = this . metadata . find < T > ( entityName ) ! ; const prop = meta . properties [ field as string ] as EntityProperty ; if ( prop . reference === ReferenceType . SCALAR && prop . lazy ) { const filtered = entities . filter ( e => options . refresh || e [ prop . name ] === undefined ) ; if ( options . ignoreLazyScalarProperties || filtered . length === 0 ) { return entities ; } const pk = Utils . getPrimaryKeyHash ( meta . primaryKeys ) ; const ids = Utils . unique ( filtered . map ( e => Utils . getPrimaryKeyValues ( e , meta . primaryKeys , true ) ) ) ; const where = this . mergePrimaryCondition ( ids , pk , options , meta , this . metadata , this . driver . getPlatform ( ) ) ; const { filters , convertCustomTypes , lockMode , strategy } = options ; await this . em . find ( meta . className , where as FilterQuery < T > , { filters , convertCustomTypes , lockMode , strategy , fields : [ prop . name ] as never , } ) ; return entities ; } if ( prop . reference === ReferenceType . EMBEDDED ) { return [ ] ; } entities . forEach ( entity => { const value = entity [ field ] ; if ( Utils . isEntity ( value , true ) ) { ( value as AnyEntity ) . __helper ! . populated ( ) ; } else if ( Utils . isCollection ( value ) ) { value . populated ( ) ; } } ) ; const filtered = this . filterCollections < T > ( entities , field , options . refresh ) ; const innerOrderBy = Utils . asArray ( options . orderBy ) . filter ( orderBy => Utils . isObject ( orderBy [ prop . name ] ) ) . map ( orderBy => orderBy [ prop . name ] ) ; if ( prop . reference === ReferenceType . MANY_TO_MANY && this . driver . getPlatform ( ) . usesPivotTable ( ) ) { return this . findChildrenFromPivotTable < T > ( filtered , prop , options , innerOrderBy , populate ) ; } const where = await this . extractChildCondition ( options , prop ) ; const data = await this . findChildren < T > ( entities , prop , populate , { ... options , where , orderBy : innerOrderBy ! } ) ; this . initializeCollections < T > ( filtered , prop , field , data ) ; return data ; } private initializeCollections < T extends AnyEntity < T > > ( filtered , prop , field , children ) { if ( prop . reference === ReferenceType . ONE_TO_MANY ) { this . initializeOneToMany < T > ( filtered , children , prop , field ) ; } if ( prop . reference === ReferenceType . MANY_TO_MANY && ! prop . owner && ! this . driver . getPlatform ( ) . usesPivotTable ( ) ) { this . initializeManyToMany < T > ( filtered , children , prop , field ) ; } } private initializeOneToMany < T extends AnyEntity < T > > ( filtered , children , prop , field ) { for ( const entity of filtered ) { const items = children . filter ( child => { if ( prop . targetMeta ! . properties [ prop . mappedBy ] . mapToPk ) { return child [ prop . mappedBy ] as unknown === entity . __helper ! . getPrimaryKey ( ) ; } return Reference . unwrapReference ( child [ prop . mappedBy ] ) as unknown === entity ; } ) ; ( entity [ field ] as unknown as Collection < AnyEntity > ) . hydrate ( items ) ; } } private initializeManyToMany < T extends AnyEntity < T > > ( filtered , children , prop , field ) { for ( const entity of filtered ) { const items = children . filter ( child => ( child [ prop . mappedBy ] as unknown as Collection < AnyEntity > ) . contains ( entity ) ) ; ( entity [ field ] as unknown as Collection < AnyEntity > ) . hydrate ( items ) ; } } private async findChildren < T extends AnyEntity < T > > ( entities , prop , populate , options ) { const children = this . getChildReferences < T > ( entities , prop , options . refresh ) ; const meta = this . metadata . find ( prop . type ) ! ; let fk = Utils . getPrimaryKeyHash ( meta . primaryKeys ) ; if ( prop . reference === ReferenceType . ONE_TO_MANY || ( prop . reference === ReferenceType . MANY_TO_MANY && ! prop . owner ) ) { fk = meta . properties [ prop . mappedBy ] . name ; } if ( prop . reference === ReferenceType . ONE_TO_ONE && ! prop . owner && populate . strategy !== LoadStrategy . JOINED && ! this . em . config . get ( 'autoJoinOneToOneOwner' ) ) { children . length = 0 ; children . push ( ... entities ) ; fk = meta . properties [ prop . mappedBy ] . name ; } if ( children . length === 0 ) { return [ ] ; } const ids = Utils . unique ( children . map ( e => Utils . getPrimaryKeyValues ( e , e . __meta ! . primaryKeys , true ) ) ) ; const where = this . mergePrimaryCondition < T > ( ids , fk , options , meta , this . metadata , this . driver . getPlatform ( ) ) ; const fields = this . buildFields ( options . fields , prop ) ; const { refresh , filters , convertCustomTypes , lockMode , strategy } = options ; return this . em . find < T > ( prop . type , where as FilterQuery < T > , { refresh , filters , convertCustomTypes , lockMode , orderBy : [ ... Utils . asArray ( options . orderBy ) , ... Utils . asArray ( prop . orderBy ) , { [ fk ] : QueryOrder . ASC } ] as QueryOrderMap < T > [ ] , populate : populate . children as never ?? populate . all , strategy , fields : fields . length > 0 ? fields : undefined , } ) as Promise < T [ ] > ; } private mergePrimaryCondition < T > ( ids , pk , options , meta , metadata , platform ) { const cond1 = QueryHelper . processWhere ( { [ pk ] : { $in : ids } } , meta . name ! , metadata , platform , ! options . convertCustomTypes ) ; return options . where ! [ pk ] ? { $and : [ cond1 , options . where ] } : { ... cond1 , ... ( options . where as Dictionary ) } ; } private async populateField < T extends AnyEntity < T > > ( entityName , entities , populate , options ) { const prop = this . metadata . find ( entityName ) ! . properties [ populate . field ] as EntityProperty < T > ; if ( prop . reference === ReferenceType . SCALAR && ! prop . lazy ) { return ; } if ( ! populate . children ) { return void await this . populateMany < T > ( entityName , entities , populate , options ) ; } await this . populateMany < T > ( entityName , entities , populate , options ) ; const children = [ ] ; for ( const entity of entities ) { if ( Utils . isEntity ( entity [ populate . field ] ) ) { children . push ( entity [ populate . field ] ) ; } else if ( Reference . isReference ( entity [ populate . field ] ) ) { children . push ( entity [ populate . field ] . unwrap ( ) ) ; } else if ( Utils . isCollection ( entity [ populate . field ] ) ) { children . push ( ... entity [ populate . field ] . getItems ( ) ) ; } else if ( entity [ populate . field ] && prop . reference === ReferenceType . EMBEDDED ) { children . push ( ... Utils . asArray ( entity [ populate . field ] ) ) ; } } const filtered = Utils . unique ( children ) ; const fields = this . buildFields ( options . fields , prop ) ; const innerOrderBy = Utils . asArray ( options . orderBy ) . filter ( orderBy => Utils . isObject ( orderBy [ prop . name ] ) ) . map ( orderBy => orderBy [ prop . name ] ) ; const { refresh , filters , ignoreLazyScalarProperties } = options ; await this . populate < T > ( prop . type , filtered , populate . children , { where : await this . extractChildCondition ( options , prop , false ) as FilterQuery < T > , orderBy : innerOrderBy as QueryOrderMap < T > [ ] , fields : fields . length > 0 ? fields : undefined , validate : false , lookup : false , refresh , filters , ignoreLazyScalarProperties , } ) ; } private async findChildrenFromPivotTable < T extends AnyEntity < T > > ( filtered , prop , options , orderBy ? , populate ? ) { const ids = filtered . map ( ( e ) => e . __helper ! . __primaryKeys ) ; const refresh = options . refresh ; const where = await this . extractChildCondition ( options , prop , true ) ; const fields = this . buildFields ( options . fields , prop ) ; const options2 = { ... options } as FindOptions < T > ; delete options2 . limit ; delete options2 . offset ; options2 . fields = ( fields . length > 0 ? fields : undefined ) as EntityField < T > [ ] ; options2 . populate = ( populate ?. children ?? [ ] ) as never ; if ( prop . customType ) { ids . forEach ( ( id , idx ) => ids [ idx ] = QueryHelper . processCustomType < T > ( prop , id as FilterQuery < T > , this . driver . getPlatform ( ) ) as Primary < T > [ ] ) ; } const map = await this . driver . loadFromPivotTable ( prop , ids , where , orderBy , this . em . getTransactionContext ( ) , options2 ) ; const children = [ ] ; for ( const entity of filtered ) { const items = map [ entity . __helper ! . getSerializedPrimaryKey ( ) ] . map ( item => { const entity = this . em . getEntityFactory ( ) . create < T > ( prop . type , item , { refresh , merge : true , convertCustomTypes : true } ) ; return this . em . getUnitOfWork ( ) . registerManaged < T > ( entity , item , { refresh , loaded : true } ) ; } ) ; ( entity [ prop . name ] as unknown as Collection < AnyEntity > ) . hydrate ( items ) ; children . push ( ... items ) ; } return children ; } private async extractChildCondition < T > ( options , prop , filters = false ) { const subCond = Utils . isPlainObject ( options . where [ prop . name as string ] ) ? options . where [ prop . name as string ] : { } ; const meta2 = this . metadata . find ( prop . type ) ! ; const pk = Utils . getPrimaryKeyHash ( meta2 . primaryKeys ) ; [ '$and' , '$or' ] . forEach ( op => { if ( options . where [ op ] ) { const child = options . where [ op ] . map ( ( cond ) => cond [ prop . name ] ) . filter ( ( sub ) => sub != null && ! ( Utils . isPlainObject ( sub ) && Object . keys ( sub ) . every ( key => Utils . isOperator ( key , false ) ) ) ) . map ( ( cond ) => { if ( Utils . isPrimaryKey ( cond ) ) { return { [ pk ] : cond } ; } return cond ; } ) ; if ( child . length > 0 ) { subCond [ op ] = child ; } } } ) ; const operators = Object . keys ( subCond ) . filter ( key => Utils . isOperator ( key , false ) ) ; if ( operators . length > 0 ) { operators . forEach ( op => { subCond [ pk ] ??= { } ; subCond [ pk ] [ op ] = subCond [ op ] ; delete subCond [ op ] ; } ) ; } if ( filters ) { return this . em . applyFilters ( prop . type , subCond , options . filters , 'read' ) ; } return subCond ; } private buildFields < T , P extends string > ( fields = [ ] , prop ) { return fields . reduce ( ( ret , f ) => { if ( Utils . isPlainObject ( f ) ) { Object . keys ( f ) . filter ( ff => ff === prop . name ) . forEach ( ff => ret . push ( ... f [ ff ] as EntityField < T > [ ] ) ) ; } else if ( f . toString ( ) . includes ( '.' ) ) { const parts = f . toString ( ) . split ( '.' ) ; const propName = parts . shift ( ) ; const childPropName = parts . join ( '.' ) as EntityField < T > ; if ( propName === prop . name ) { ret . push ( childPropName ) ; } } return ret ; } , [ ] as EntityField < T > [ ] ) ; } private getChildReferences < T extends AnyEntity < T > > ( entities , prop , refresh ) { const filtered = this . filterCollections ( entities , prop . name , refresh ) ; const children = [ ] ; if ( prop . reference === ReferenceType . ONE_TO_MANY ) { children . push ( ... filtered . map ( e => ( e [ prop . name ] as unknown as Collection < T > ) . owner ) ) ; } else if ( prop . reference === ReferenceType . MANY_TO_MANY && prop . owner ) { children . push ( ... filtered . reduce ( ( a , b ) => [ ... a , ... ( b [ prop . name ] as unknown as Collection < AnyEntity > ) . getItems ( ) ] , [ ] as AnyEntity [ ] ) ) ; } else if ( prop . reference === ReferenceType . MANY_TO_MANY ) { children . push ( ... filtered ) ; } else { children . push ( ... this . filterReferences ( entities , prop . name , refresh ) ) ; } return children ; } private filterCollections < T extends AnyEntity < T > > ( entities , field , refresh ) { if ( refresh ) { return entities . filter ( e => e [ field ] ) ; } return entities . filter ( e => Utils . isCollection ( e [ field ] ) && ! ( e [ field ] as unknown as Collection < AnyEntity > ) . isInitialized ( true ) ) ; } private filterReferences < T extends AnyEntity < T > > ( entities , field , refresh ) { const children = entities . filter ( e => Utils . isEntity ( e [ field ] , true ) ) ; if ( refresh ) { return children . map ( e => Reference . unwrapReference ( e [ field ] ) ) ; } return children . filter ( e => ! ( e [ field ] as AnyEntity ) . __helper ! . __initialized ) . map ( e => Reference . unwrapReference ( e [ field ] ) ) ; } private lookupAllRelationships < T > ( entityName ) { const ret = [ ] ; const meta = this . metadata . find ( entityName ) ! ; meta . relations . forEach ( prop => { ret . push ( { field : prop . name , strategy : LoadStrategy . SELECT_IN , all : true , } ) ; } ) ; return ret ; } private lookupEagerLoadedRelationships < T > ( entityName , populate , strategy ? , prefix = '' , visited = [ ] ) { if ( visited . includes ( entityName ) ) { return [ ] ; } visited . push ( entityName ) ; const meta = this . metadata . find ( entityName ) ! ; const ret = prefix === '' ? [ ... populate ] : [ ] ; meta . relations . filter ( prop => prop . eager || populate . some ( p => p . field === prop . name ) ) . forEach ( prop => { const prefixed = prefix ? `${ prefix }.${ prop . name }` : prop . name ; const nestedPopulate = populate . find ( p => p . field === prop . name ) ?. children ?? [ ] ; const nested = this . lookupEagerLoadedRelationships ( prop . type , nestedPopulate , strategy , prefixed , visited ) ; if ( nested . length > 0 ) { ret . push ( ... nested ) ; } else { ret . push ( { field : prefixed , strategy : strategy ?? prop . strategy ?? this . em . config . get ( 'loadStrategy' ) , } ) ; } } ) ; return ret ; } }