import { oneToManyDataLoader } from "../dataloaders/oneToManyDataLoader" ; import { Collection , currentlyInstantiatingEntity , ensureNotDeleted , Entity , EntityMetadata , getEm , getMetadata , IdOf , maybeResolveReferenceToId , } from "../index" ; import { remove } from "../utils" ; import { AbstractRelationImpl } from "./AbstractRelationImpl" ; import { ManyToOneReferenceImpl } from "./ManyToOneReference" ; import { RelationT , RelationU } from "./Relation" ; /** An alias for creating `OneToManyCollection`s. */ export function hasMany < T extends Entity , U extends Entity > ( otherMeta , fieldName , otherFieldName , otherColumnName ) { const entity = currentlyInstantiatingEntity as T ; return new OneToManyCollection ( entity , otherMeta , fieldName , otherFieldName , otherColumnName ) ; } export class OneToManyCollection < T extends Entity , U extends Entity > extends AbstractRelationImpl < U [ ] > implements Collection < T , U > { private loaded : U [ ] | undefined ; private addedBeforeLoaded : U [ ] = [ ] ; private isCascadeDelete : boolean ; constructor ( public entity , public otherMeta , public fieldName , public otherFieldName , public otherColumnName ) { super ( ) ; this . isCascadeDelete = getMetadata ( entity ) . config . __data . cascadeDeleteFields . includes ( fieldName as any ) ; } async load ( opts ? ) { ensureNotDeleted ( this . entity , { ignore : "pending" } ) ; if ( this . loaded === undefined ) { if ( this . entity . id === undefined ) { this . loaded = [ ] ; } else { this . loaded = await oneToManyDataLoader ( getEm ( this . entity ) , this ) . load ( this . entity . id ) ; } this . maybeAppendAddedBeforeLoaded ( ) ; } return this . filterDeleted ( this . loaded , opts ) ; } async find ( id ) { return ( await this . load ( ) ) . find ( ( u ) => u . id === id ) ; } get isLoaded ( ) : boolean { return this . loaded !== undefined ; } get get ( ) : U [ ] { return this . filterDeleted ( this . doGet ( ) , { withDeleted : false } ) ; } get getWithDeleted ( ) : U [ ] { return this . filterDeleted ( this . doGet ( ) , { withDeleted : true } ) ; } private doGet ( ) { ensureNotDeleted ( this . entity , { ignore : "pending" } ) ; if ( this . loaded === undefined ) { if ( this . entity . id === undefined ) { return this . addedBeforeLoaded ; } else { throw new Error ( "get was called when not preloaded" ) ; } } return this . loaded ; } set ( values ) { ensureNotDeleted ( this . entity ) ; if ( this . loaded === undefined ) { throw new Error ( "set was called when not loaded" ) ; } const loaded = [ ... this . loaded ] ; for ( const other of loaded ) { if ( ! values . includes ( other ) ) { this . remove ( other ) ; } } for ( const other of values ) { if ( ! loaded . includes ( other ) ) { this . add ( other ) ; } } } add ( other ) { ensureNotDeleted ( this . entity ) ; if ( this . loaded === undefined ) { if ( ! this . addedBeforeLoaded . includes ( other ) ) { this . addedBeforeLoaded . push ( other ) ; } } else { if ( ! this . loaded . includes ( other ) ) { this . loaded . push ( other ) ; } } this . getOtherRelation ( other ) . set ( this . entity ) ; } remove ( other , opts = { requireLoaded : true } ) { ensureNotDeleted ( this . entity , { ignore : "pending" } ) ; if ( this . loaded === undefined && opts . requireLoaded ) { throw new Error ( "remove was called when not loaded" ) ; } remove ( this . loaded || this . addedBeforeLoaded , other ) ; this . getOtherRelation ( other ) . set ( undefined ) ; } removeAll ( ) { ensureNotDeleted ( this . entity ) ; if ( this . loaded === undefined ) { throw new Error ( "removeAll was called when not loaded" ) ; } for ( const other of [ ... this . loaded ] ) { this . remove ( other ) ; } } setFromOpts ( others ) { this . loaded = [ ] ; others . forEach ( ( o ) => this . add ( o ) ) ; } initializeForNewEntity ( ) { if ( this . loaded === undefined ) { this . loaded = [ ] ; } } removeIfLoaded ( other ) { if ( this . loaded !== undefined ) { remove ( this . loaded , other ) ; } else { remove ( this . addedBeforeLoaded , other ) ; } } async refreshIfLoaded ( ) { if ( this . loaded !== undefined && this . entity . id !== undefined ) { this . loaded = await oneToManyDataLoader ( getEm ( this . entity ) , this ) . load ( this . entity . id ) ; } } maybeCascadeDelete ( ) { if ( this . isCascadeDelete ) { this . current ( { withDeleted : true } ) . forEach ( getEm ( this . entity ) . delete ) ; } } async cleanupOnEntityDeleted ( ) { const current = await this . load ( { withDeleted : true } ) ; current . forEach ( ( other ) => { const m2o = this . getOtherRelation ( other ) ; if ( maybeResolveReferenceToId ( m2o . current ( ) ) === this . entity . id ) { m2o . set ( undefined ) ; } } ) ; this . loaded = [ ] ; this . addedBeforeLoaded = [ ] ; } private maybeAppendAddedBeforeLoaded ( ) { if ( this . loaded ) { const newEntities = this . addedBeforeLoaded . filter ( ( e ) => ! this . loaded ?. includes ( e ) ) ; for ( const e of newEntities ) { this . loaded . push ( e ) ; } this . addedBeforeLoaded = [ ] ; } } current ( opts ? ) { return this . filterDeleted ( this . loaded || this . addedBeforeLoaded , opts ) ; } public toString ( ) { return `OneToManyCollection(entity: ${ this . entity }, fieldName: ${ this . fieldName }, otherType: ${ this . otherMeta . type }, otherFieldName: ${ this . otherFieldName })` ; } private filterDeleted ( entities , opts ? ) { return opts ?. withDeleted === true ? [ ... entities ] : entities . filter ( ( e ) => ! e . isDeletedEntity ) ; } /** Returns the other relation that points back at us, i.e. we're `Author.image` and this is `Image.author_id`. */ private getOtherRelation ( other ) { return ( other as U ) [ this . otherFieldName ] as any ; } [ RelationT ] : T = null ! ; [ RelationU ] : U = null ! ; }