import type { AnyEntity , Dictionary , EntityData , EntityMetadata , FilterQuery , Loaded , LoadedCollection , Populate , Primary } from '../typings' ; import { ArrayCollection } from './ArrayCollection' ; import { Utils } from '../utils/Utils' ; import { ValidationError } from '../errors' ; import type { QueryOrderMap } from '../enums' ; import { QueryOrder , ReferenceType } from '../enums' ; import { Reference } from './Reference' ; import type { Transaction } from '../connections/Connection' ; import type { FindOptions } from '../drivers/IDatabaseDriver' ; export interface MatchingOptions < T , P extends string = never > extends FindOptions < T , P > { where ? : FilterQuery < T > ; store ? : boolean ; ctx ? : Transaction ; } export class Collection < T , O = unknown > extends ArrayCollection < T , O > { private snapshot : T [ ] | undefined = [ ] ; private dirty = false ; private readonly ? : boolean ; private _populated = false ; private _lazyInitialized = false ; constructor ( owner , items ? , initialized = true ) { super ( owner , items ) ; this . initialized = ! ! items || initialized ; } /**
     * Creates new Collection instance, assigns it to the owning entity and sets the items to it (propagating them to their inverse sides)
     */ static create < T , O = any > ( owner , prop , items , initialized ) { const coll = new Collection < T , O > ( owner , items , initialized ) ; owner [ prop ] = coll as unknown as O [ keyof O ] ; if ( items ) { coll . set ( items ) ; } return coll ; } /**
     * Initializes the collection and returns the items
     */ async loadItems ( ) { if ( ! this . isInitialized ( true ) ) { await this . init ( ) ; } return super . getItems ( ) ; } /**
     * Gets the count of collection items from database instead of counting loaded items.
     * The value is cached, use `refresh = true` to force reload it.
     */ async loadCount ( refresh = false ) { if ( ! refresh && Utils . isDefined ( this . _count ) ) { return this . _count ! ; } const em = this . getEntityManager ( ) ; const pivotMeta = em . getMetadata ( ) . find ( this . property . pivotTable ) ! ; if ( ! em . getPlatform ( ) . usesPivotTable ( ) && this . property . reference === ReferenceType . MANY_TO_MANY ) { this . _count = this . length ; } else if ( this . property . pivotTable && ! ( this . property . inversedBy || this . property . mappedBy ) ) { this . _count = await em . count ( this . property . type , this . createLoadCountCondition ( { } as FilterQuery < T > , pivotMeta ) , { populate : [ { field : this . property . pivotTable } ] } ) ; } else { this . _count = await em . count ( this . property . type , this . createLoadCountCondition ( { } as FilterQuery < T > , pivotMeta ) ) ; } return this . _count ! ; } async matching < P extends string = never > ( options ) { const em = this . getEntityManager ( ) ; const { where , ctx , ... opts } = options ; opts . orderBy = this . createOrderBy ( opts . orderBy ) ; let items ; if ( this . property . reference === ReferenceType . MANY_TO_MANY && em . getPlatform ( ) . usesPivotTable ( ) ) { const map = await em . getDriver ( ) . loadFromPivotTable ( this . property , [ this . owner . __helper ! . __primaryKeys ] , where , opts . orderBy , ctx , options ) ; items = map [ this . owner . __helper ! . getSerializedPrimaryKey ( ) ] . map ( ( item ) => em . merge ( this . property . type , item , { convertCustomTypes : true } ) ) ; } else { items = await em . find ( this . property . type , this . createCondition ( where ) , opts ) ; } if ( options . store ) { this . hydrate ( items ) ; this . populated ( ) ; this . readonly = true ; } return items ; } /**
     * Returns the items (the collection must be initialized)
     */ getItems ( check = true ) { if ( check ) { this . checkInitialized ( ) ; } return super . getItems ( ) ; } toJSON ( ) { if ( ! this . isInitialized ( ) ) { return [ ] ; } return super . toJSON ( ) ; } add ( ... items ) { const unwrapped = items . map ( i => Reference . unwrapReference ( i ) ) ; unwrapped . forEach ( item => this . validateItemType ( item ) ) ; this . modify ( 'add' , unwrapped ) ; this . cancelOrphanRemoval ( unwrapped ) ; } set ( items ) { if ( ! this . initialized ) { this . initialized = true ; this . snapshot = undefined ; } super . set ( items ) ; } internal hydrate ( items ) { this . initialized = true ; super . hydrate ( items ) ; this . takeSnapshot ( ) ; } remove ( ... items ) { if ( items [ 0 ] instanceof Function ) { for ( const item of this . items ) { if ( items [ 0 ] ( item ) ) { this . remove ( item ) ; } } return ; } const unwrapped = items . map ( i => Reference . unwrapReference ( i as T ) ) ; this . modify ( 'remove' , unwrapped ) ; const em = this . getEntityManager ( unwrapped , false ) ; if ( this . property . orphanRemoval && em ) { for ( const item of unwrapped ) { em . getUnitOfWork ( ) . scheduleOrphanRemoval ( item ) ; } } } contains ( item , check = true ) { if ( check ) { this . checkInitialized ( ) ; } return super . contains ( item ) ; } count ( ) { this . checkInitialized ( ) ; return super . count ( ) ; } shouldPopulate ( ) { return this . _populated && ! this . _lazyInitialized ; } populated ( populated = true ) { this . _populated = populated ; this . _lazyInitialized = false ; } isDirty ( ) { return this . dirty ; } setDirty ( dirty = true ) { this . dirty = dirty ; } async init < P extends string = never > ( options = { } ) { if ( this . dirty ) { const items = [ ... this . items ] ; this . dirty = false ; await this . init ( options ) ; items . forEach ( i => this . add ( i ) ) ; return this as unknown as LoadedCollection < Loaded < T , P > > ; } const em = this . getEntityManager ( ) ; if ( ! this . initialized && this . property . reference === ReferenceType . MANY_TO_MANY && em . getPlatform ( ) . usesPivotTable ( ) ) { const map = await em . getDriver ( ) . loadFromPivotTable ( this . property , [ this . owner . __helper ! . __primaryKeys ] , options . where , options . orderBy ) ; this . hydrate ( map [ this . owner . __helper ! . getSerializedPrimaryKey ( ) ] . map ( ( item ) => em . merge ( this . property . type , item , { convertCustomTypes : true } ) ) ) ; this . _lazyInitialized = true ; return this as unknown as LoadedCollection < Loaded < T , P > > ; } if ( this . property . reference === ReferenceType . MANY_TO_MANY && ( this . property . owner || em . getPlatform ( ) . usesPivotTable ( ) ) && this . length === 0 ) { this . initialized = true ; this . dirty = false ; this . _lazyInitialized = true ; return this as unknown as LoadedCollection < Loaded < T , P > > ; } const where = this . createCondition ( options . where ) ; const order = [ ... this . items ] ; const customOrder = ! ! options . orderBy ; const orderBy = this . createOrderBy ( options . orderBy ) ; const items = await em . find ( this . property . type , where , { populate : options . populate , orderBy } ) ; if ( ! customOrder ) { this . reorderItems ( items , order ) ; } this . items . clear ( ) ; let i = 0 ; items . forEach ( item => { this . items . add ( item ) ; this [ i ++ ] = item ; } ) ; this . initialized = true ; this . dirty = false ; this . _lazyInitialized = true ; return this as unknown as LoadedCollection < Loaded < T , P > > ; } internal takeSnapshot ( ) { this . snapshot = [ ... this . items ] ; this . setDirty ( false ) ; } internal getSnapshot ( ) { return this . snapshot ; } private getEntityManager ( items = [ ] , required = true ) { let em = this . owner . __helper ! . __em ; if ( ! em ) { const item = ( items . concat ( ... this . items ) as AnyEntity < T > [ ] ) . find ( i => i ?. __helper ! . __em ) ; em = item ?. __helper ! . __em ; } if ( ! em && required ) { throw ValidationError . entityNotManaged ( this . owner ) ; } return em ; } private createCondition ( cond = { } as FilterQuery < T > ) { if ( this . property . reference === ReferenceType . ONE_TO_MANY ) { cond [ this . property . mappedBy ] = this . owner . __helper ! . getPrimaryKey ( ) ; } else { this . createManyToManyCondition ( cond ) ; } return cond ; } private createOrderBy ( orderBy = [ ] ) { if ( Utils . isEmpty ( orderBy ) && this . property . reference === ReferenceType . ONE_TO_MANY ) { const defaultOrder = this . property . referencedColumnNames . map ( name => { return { [ name ] : QueryOrder . ASC } ; } ) ; orderBy = this . property . orderBy || defaultOrder ; } return Utils . asArray ( orderBy ) ; } private createManyToManyCondition ( cond ) { if ( this . property . owner || this . property . pivotTable ) { const pk = this . property . targetMeta ! . primaryKeys [ 0 ] ; cond [ pk ] = { $in : [ ] } ; this . items . forEach ( ( item ) => cond [ pk ] . $in . push ( item . __helper ! . getPrimaryKey ( ) ) ) ; } else { cond [ this . property . mappedBy ] = this . owner . __helper ! . getPrimaryKey ( ) ; } } private createLoadCountCondition ( cond , pivotMeta ? ) { const val = this . owner . __meta ! . compositePK ? { $in : this . owner . __helper ! . __primaryKeys } : this . owner . __helper ! . getPrimaryKey ( ) ; if ( this . property . reference === ReferenceType . ONE_TO_MANY ) { cond [ this . property . mappedBy ] = val ; } else if ( pivotMeta && this . property . owner && ! this . property . inversedBy ) { const pivotProp1 = pivotMeta . properties [ this . property . type + '_inverse' ] ; const inverse = pivotProp1 . mappedBy ; const key = `${ this . property . pivotTable }.${ pivotMeta . properties [ inverse ] . name }` ; cond [ key ] = val ; } else { const key = this . property . owner ? this . property . inversedBy : this . property . mappedBy ; cond [ key ] = val ; } return cond ; } private modify ( method , items ) { if ( method === 'remove' ) { this . checkInitialized ( ) ; } this . validateModification ( items ) ; super [ method ] ( ... items ) ; this . setDirty ( ) ; } private checkInitialized ( ) { if ( ! this . isInitialized ( ) ) { throw new Error ( `Collection<${ this . property . type }> of entity ${ this . owner . constructor . name }[${ this . owner . __helper ! . getPrimaryKey ( ) }] not initialized` ) ; } } /**
     * re-orders items after searching with `$in` operator
     */ private reorderItems ( items , order ) { if ( this . property . reference === ReferenceType . MANY_TO_MANY && this . property . owner ) { items . sort ( ( a , b ) => order . indexOf ( a ) - order . indexOf ( b ) ) ; } } private cancelOrphanRemoval ( items ) { const em = this . getEntityManager ( items , false ) ; if ( ! em ) { return ; } for ( const item of items ) { em ! . getUnitOfWork ( ) . cancelOrphanRemoval ( item ) ; } } private validateItemType ( item ) { if ( ! Utils . isEntity ( item ) ) { throw ValidationError . notEntity ( this . owner , this . property , item ) ; } } private validateModification ( items ) { if ( this . readonly ) { throw ValidationError . cannotModifyReadonlyCollection ( this . owner , this . property ) ; } if ( this . property . pivotTable || ! this . property . mappedBy ) { return ; } const check = ( item ) => { if ( ! item || item . __helper ! . __initialized ) { return false ; } return ! item [ this . property . mappedBy ] && this . property . reference === ReferenceType . MANY_TO_MANY ; } ; if ( items . find ( item => check ( item ) ) ) { throw ValidationError . cannotModifyInverseCollection ( this . owner , this . property ) ; } } } Object . defineProperties ( Collection . prototype , { $ : { get ( ) { return this ; } } , get : { get ( ) { return ( ) => this ; } } , } ) ; export interface InitOptions < T , P extends string = never > { populate ? : Populate < T , P > ; orderBy ? : QueryOrderMap < T > | QueryOrderMap < T > [ ] ; where ? : FilterQuery < T > ; }