import Resource , { DetailOpts } from './index' ; import assert from 'assert' ; import { AttributeError } from './exceptions' ; import { getContentTypeWeak } from './util' ; export type RelatedObjectValue = string | string [ ] | number | number [ ] | Record < string , any > | Record < string , any > [ ] ; export type CollectionValue = Record < string , any > [ ] ; export default class RelatedManager < T extends typeof Resource = typeof Resource > { to : T ; value : RelatedObjectValue ; many : boolean = false ; /**
     * Is `true` when `resolve()` is called and first page of results loads up to `this.batchSize` objects
     */ resolved : boolean = false ; /**
     * Deferred promises when `this.resolve()` hits the max requests in `this.batchSize`
     */ deferred : ( ( ) => Promise < InstanceType < T > > ) [ ] = [ ] ; /**
     * List of stringified Primary Keys, even if `this.value` is a list of objects, or Resource instances
     */ primaryKeys : string [ ] ; /**
     * When sending `this.resolve()`, only send out the first `n` requests where `n` is `this.batchSize`. You
     * can call `this.all()` to recursively get all objects
     */ batchSize : number = Infinity ; _resources : Record < string , InstanceType < T > > = { } ; constructor ( to , value ) { assert ( typeof to === 'function' , `RelatedManager expected first parameter to be Resource class, received "${ to }". Please double check related definitions on class.` ) ; this . to = to ; this . value = value ; this . many = Array . isArray ( value ) ; this . primaryKeys = this . getPrimaryKeys ( ) ; if ( ! this . value || ( this . many && ! Object . keys ( this . value ) . length ) ) { this . resolved = true ; } } /**
     * Check if values exist on manager
     */ hasValues ( ) { if ( this . many ) { return ( this . value as any [ ] ) . length > 0 ; } return Boolean ( this . value ) ; } canAutoResolve ( ) { let value = this . value as any ; let isObject = Object === this . getValueContentType ( ) ; let hasIds = this . primaryKeys . length > 0 ; if ( this . many ) { return isObject && hasIds && this . primaryKeys . length === value . length ; } return isObject && hasIds ; } returns getValueContentType ( ) { return getContentTypeWeak ( this . value ) ; } returns getPrimaryKeys ( ) { if ( ! Boolean ( this . value ) || ( Array . isArray ( this . value ) && ! this . value . length ) ) { return [ ] ; } let contentType = this . getValueContentType ( ) ; let iterValue = this . value as any [ ] ; if ( this . many ) { if ( contentType === Resource ) { return iterValue . map ( ( resource ) => this . getIdFromResource ( resource ) ) ; } else if ( this . many && contentType === Object ) { return iterValue . map ( ( record ) => this . getIdFromObject ( record ) ) ; } else { return this . value as string [ ] ; } } else { if ( contentType === Resource ) { return [ this . getIdFromResource ( this . value as InstanceType < T > ) ] ; } else if ( contentType === Object ) { return [ this . getIdFromObject ( this . value ) ] ; } else { return [ this . value as string ] ; } } } param object getIdFromObject ( object ) { return String ( object [ this . to . uniqueKey ] ) ; } param resource getIdFromResource ( resource ) { return resource . id ; } param id getOne ( id , options ? ) { return this . to . detail < T > ( id , options ) . then ( ( resource ) => { assert ( resource . getConstructor ( ) . toResourceName ( ) == this . to . toResourceName ( ) , `Related class detail() returned invalid instance: ${ resource . toResourceName ( ) } (returned) !== ${ this . to . toResourceName ( ) } (expected)` ) ; this . _resources [ resource . id ] = resource ; return resource ; } ) ; } param index getOneAtIndex ( index ) { return this . getOne ( this . primaryKeys [ index ] ) ; } /**
     * Get all loaded resources relevant to this relation
     * Like manager.resources getter except it won't throw an AttributeError and will return with any loaded resources if its ID is listed in `this.primaryKeys`
     */ getAllLoaded ( ) { try { return this . resources ; } catch ( e ) { if ( e . name === 'AttributeError' ) { let cachedObjects = [ ] ; for ( let id of this . primaryKeys ) { let cached = this . to . getCached ( id ) ; if ( cached ) { cachedObjects . push ( cached . resource as InstanceType < T > ) ; } } return cachedObjects ; } else { throw e ; } } } param options async resolve ( options ? ) { const promises = [ ] ; this . deferred = [ ] ; for ( let i in this . primaryKeys ) { let pk = this . primaryKeys [ i ] ; if ( Number ( i ) > this . batchSize ) { this . deferred . push ( this . getOne . bind ( this , pk , options ) ) ; } else { promises . push ( this . getOne ( pk , options ) ) ; } } await Promise . all ( promises ) ; this . resolved = true ; return Object . values ( this . resources ) ; } async next ( options ? ) { const promises = [ ] ; if ( ! this . resolved ) { return await this . resolve ( options ) ; } this . deferred . splice ( 0 , this . batchSize ) . forEach ( ( deferredFn ) => { promises . push ( deferredFn ( ) ) ; } ) ; return await Promise . all ( promises ) ; } param options async all ( options ? ) { await this . next ( options ) ; if ( this . deferred . length ) { return await this . all ( options ) ; } else { return Object . values ( this . resources ) ; } } resolveFromObjectValue ( ) { let Ctor = this . to ; let value = this . value as any ; let contentType = this . getValueContentType ( ) ; let newResources = { } ; assert ( Object === contentType , `Expected RelatedResource.value to be an Object. Received ${ contentType }` ) ; try { if ( this . many ) { for ( let i in value ) { let resource = new Ctor ( value [ i ] ) as InstanceType < T > ; assert ( ! ! resource . id , `RelatedResource.value[${ i }] does not have an ID.` ) ; newResources [ resource . id ] = resource ; } } else { let resource = new Ctor ( value ) as InstanceType < T > ; assert ( ! ! resource . id , `RelatedResource value does not have an ID.` ) ; newResources [ this . getIdFromObject ( value ) ] = new Ctor ( value ) as InstanceType < T > ; } this . resolved = true ; Object . assign ( this . _resources , newResources ) ; return true ; } catch ( e ) { throw e ; } } param resource add ( resource ) { assert ( this . many , `Related Manager "many" must be true to add()` ) ; assert ( resource . id , `Resource must be saved before adding to Related Manager` ) ; assert ( resource . getConstructor ( ) === this . to , `Related Manager add() expected ${ this . to . toResourceName ( ) }, received ${ resource . getConstructor ( ) . toResourceName ( ) }` ) ; const ContentCtor = this . getValueContentType ( ) ; var value ; if ( ContentCtor === Object ) { value = resource . toJSON ( ) ; } else if ( ContentCtor === Number || ContentCtor === String ) { value = resource . id ; } ; ( this . value as any [ ] ) . push ( value ) ; this . _resources [ resource . id ] = resource ; } param value fromValue < T extends typeof RelatedManager > ( this , value ) { let Ctor = < T > this . constructor ; return new Ctor ( this . to , value ) as InstanceType < T > ; } /**
     * Getter -- get `this._resources` but make sure we've actually retrieved the objects first
     * Throws AttributeError if `this.resolve()` hasn't finished
     */ get resources ( ) : InstanceType < T > [ ] { if ( ! this . resolved ) { throw new AttributeError ( `Can't read results of ${ this . constructor . name }[resources], ${ this . to . toResourceName ( ) } must resolve() first` ) ; } const allObjects = Object . values ( this . _resources ) ; return allObjects ; } /**
     * Getter -- Same as manager.resources except returns first node
     */ get resource ( ) : InstanceType < T > { return this . resources [ 0 ] ; } get length ( ) : number { return this . primaryKeys . length ; } toString ( ) { return this . primaryKeys . join ( ', ' ) ; } toJSON ( ) { return JSON . parse ( JSON . stringify ( this . value ) ) ; } }