import { EventEmitter } from '@angular/core' ; import { forkJoin , NEVER , Observable , of , throwError } from 'rxjs' ; import { map , tap } from 'rxjs/operators' ; import { EntityKey , ODataEntity , ODataEntityAnnotations , ODataEntityResource , ODataNavigationPropertyResource , ODataOptions , ODataPropertyResource , ODataQueryArgumentsOptions , ODataQueryOptionsHandler , ODataResource , ODataSingletonResource , } from '../resources' ; import { ODataStructuredType } from '../schema' ; import { Objects , Strings , Types } from '../utils' ; import { ODataCollection } from './collection' ; import { INCLUDE_DEEP , INCLUDE_SHALLOW , ModelOptions , ODataModelEvent , ODataModelField , ODataModelOptions , ODataModelRelation , } from './options' ; export class ODataModel < T > { static options : ModelOptions ; static meta : ODataModelOptions < any > ; static buildMeta < T > ( { options , schema , } ) { if ( options === undefined ) { let fields = schema . fields ( { include_navigation : true , include_parents : true } ) . reduce ( ( acc , f ) => { let name = f . name ; while ( RESERVED_FIELD_NAMES . includes ( name ) ) { name = name + '_' ; } return Object . assign ( acc , { [ name ] : { field : f . name , default : f . default , required : ! f . nullable , } , } ) ; } , { } ) ; options = { fields } ; } this . meta = new ODataModelOptions < T > ( { options , schema } ) ; } _parent : [ ODataModel < any > | ODataCollection < any , ODataModel < any > > , ODataModelField < any > | null ] | null = null ; _attributes : { [ name ] : any ; } = { } ; _changes : { [ name ] : any ; } = { } ; _relations : { [ name ] : ODataModelRelation < any > ; } = { } ; _resource : ODataResource < T > | null = null ; _annotations ! : ODataEntityAnnotations ; _reset : boolean = false ; _reparent : boolean = false ; _silent : boolean = false ; _meta : ODataModelOptions < T > ; events$ = new EventEmitter < ODataModelEvent < T > > ( ) ; constructor ( data = { } , { parent , resource , annots , reset = false , } = { } ) { const Klass = this . constructor as typeof ODataModel ; if ( Klass . meta === undefined ) throw new Error ( `Can't create model without metadata` ) ; this . _meta = Klass . meta ; this . _meta . bind ( this , { parent , resource , annots } ) ; ( < any > this ) [ this . _meta . cid ] = ( < any > data ) [ this . _meta . cid ] || Strings . uniqueId ( `${ Klass . meta . schema . name . toLowerCase ( ) }-` ) ; let attrs = this . annots ( ) . attributes < T > ( data , 'full' ) ; let defaults = this . defaults ( ) ; this . assign ( Objects . merge ( defaults , attrs ) , { reset } ) ; } resource ( ) { return ODataModelOptions . resource < T > ( this ) as ODataEntityResource < T > | ODataNavigationPropertyResource < T > | ODataPropertyResource < T > | ODataSingletonResource < T > ; } navigationProperty < N > ( name ) { const field = this . _meta . field ( name ) ; if ( field === undefined || ! field . navigation ) throw Error ( `Can't find navigation property ${ name }` ) ; const resource = this . resource ( ) ; if ( ! ( resource instanceof ODataEntityResource ) || ! resource . hasKey ( ) ) throw Error ( "Can't get navigation without ODataEntityResource with key" ) ; return field . resourceFactory < T , N > ( resource ) as ODataNavigationPropertyResource < N > ; } property < N > ( name ) { const field = this . _meta . field ( name ) ; if ( field === undefined || field . navigation ) throw Error ( `Can't find property ${ name }` ) ; const resource = this . resource ( ) ; if ( ! ( resource instanceof ODataEntityResource ) || ! resource . hasKey ( ) ) throw Error ( "Can't get property without ODataEntityResource with key" ) ; return field . resourceFactory < T , N > ( resource ) as ODataPropertyResource < N > ; } attach ( resource ) { return this . _meta . attach ( this , resource ) ; } schema ( ) { return this . _meta . schema ; } annots ( ) { return this . _annotations ; } key ( { field_mapping = false , resolve = true , } = { } ) { return this . _meta . resolveKey ( this , { field_mapping , resolve } ) ; } isNew ( ) { return this . key ( ) === undefined ; } isParentOf ( child ) { return ( child !== this && ODataModelOptions . chain ( child ) . some ( ( p ) => p [ 0 ] === this ) ) ; } referential ( field , { field_mapping = false , resolve = true , } = { } ) { return this . _meta . resolveReferential ( this , field , { field_mapping , resolve , } ) ; } referenced ( field , { field_mapping = false , resolve = true , } = { } ) { return this . _meta . resolveReferenced ( this , field , { field_mapping , resolve , } ) ; } _errors ? : { [ key ] : any ; } ; validate ( { method , navigation = false , } = { } ) { return this . _meta . validate ( this , { method , navigation } ) ; } isValid ( { method , navigation = false , } = { } ) { this . _errors = this . validate ( { method , navigation } ) ; if ( this . _errors !== undefined ) this . events$ . emit ( new ODataModelEvent ( 'invalid' , { model : this , value : this . _errors , options : { method } , } ) ) ; return this . _errors === undefined ; } defaults ( ) { return this . _meta . defaults ( ) || { } ; } toEntity ( { client_id = false , include_navigation = false , include_concurrency = false , include_computed = false , include_key = true , include_non_field = false , changes_only = false , field_mapping = false , chain = [ ] , } = { } ) { return this . _meta . toEntity ( this , { client_id , include_navigation , include_concurrency , include_computed , include_key , include_non_field , changes_only , field_mapping , chain , } ) ; } toJSON ( ) { return this . toEntity ( ) ; } attributes ( { changes_only = false , include_concurrency = false , include_computed = false , include_non_field = false , field_mapping = false , } = { } ) { return this . _meta . attributes ( this , { changes_only , include_concurrency , include_computed , include_non_field , field_mapping , } ) ; } set ( path , value ) { const pathArray = ( Types . isArray ( path ) ? path : ( path as string ) . match ( /([^[.\]])+/g ) ) as any [ ] ; if ( pathArray . length === 0 ) return undefined ; if ( pathArray . length > 1 ) { const model = ( < any > this ) [ pathArray [ 0 ] ] ; return model . set ( pathArray . slice ( 1 ) , value ) ; } if ( pathArray . length === 1 ) { return ( ( < any > this ) [ pathArray [ 0 ] ] = value ) ; } } get ( path ) { const pathArray = ( Types . isArray ( path ) ? path : ( path as string ) . match ( /([^[.\]])+/g ) ) as any [ ] ; if ( pathArray . length === 0 ) return undefined ; const value = ( < any > this ) [ pathArray [ 0 ] ] ; if ( pathArray . length > 1 && ( value instanceof ODataModel || value instanceof ODataCollection ) ) { return value . get ( pathArray . slice ( 1 ) ) ; } return value ; } reset ( { path , silent = false , } = { } ) { const pathArray = ( path === undefined ? [ ] : Types . isArray ( path ) ? path : ( path as string ) . match ( /([^[.\]])+/g ) ) as any [ ] ; const name = pathArray [ 0 ] ; const value = name !== undefined ? ( < any > this ) [ name ] : undefined ; if ( ODataModelOptions . isModel ( value ) || ODataModelOptions . isCollection ( value ) ) { value . reset ( { path : pathArray . slice ( 1 ) , silent } ) ; } else { this . _meta . reset ( this , { name : pathArray [ 0 ] , silent } ) ; } } assign ( entity , { reset = false , reparent = false , silent = false , } = { } ) { return this . _meta . assign ( this , entity , { reset , silent , reparent } ) ; } clone < M extends ODataModel < T > > ( ) { let Ctor = < typeof ODataModel > this . constructor ; return new Ctor ( this . toEntity ( INCLUDE_SHALLOW ) , { resource : this . resource ( ) as ODataResource < T > , annots : this . annots ( ) , } ) as M ; } private _request ( obs$ ) { this . events$ . emit ( new ODataModelEvent ( 'request' , { model : this , options : { observable : obs$ } , } ) ) ; return obs$ . pipe ( map ( ( { entity , annots } ) => { this . _annotations = annots ; this . assign ( annots . attributes < T > ( entity || { } , 'full' ) , { reset : true , } ) ; this . events$ . emit ( new ODataModelEvent ( 'sync' , { model : this , options : { entity , annots } , } ) ) ; return this ; } ) ) ; } fetch ( { ... options } = { } ) { let resource = this . resource ( ) ; if ( resource === undefined ) return throwError ( 'fetch: Resource is undefined' ) ; let obs$ ; if ( resource instanceof ODataEntityResource ) { if ( ! resource . hasKey ( ) ) return throwError ( "fetch: Can't fetch model without key" ) ; obs$ = resource . fetch ( options ) ; } else if ( resource instanceof ODataNavigationPropertyResource ) { obs$ = resource . fetch ( { responseType : 'entity' , ... options } ) ; } else { obs$ = ( resource as ODataPropertyResource < T > ) . fetch ( { responseType : 'entity' , ... options , } ) ; } return this . _request ( obs$ ) ; } save ( { method , navigation = false , validate = true , ... options } = { } ) { let resource = this . resource ( ) ; if ( resource === undefined ) return throwError ( 'save: Resource is undefined' ) ; if ( ! ( resource instanceof ODataEntityResource || resource instanceof ODataNavigationPropertyResource ) ) return throwError ( 'save: Resource type ODataEntityResource/ODataNavigationPropertyResource needed' ) ; if ( method === undefined && this . schema ( ) . isCompoundKey ( ) ) return throwError ( 'save: Composite key require a specific method, use create/update/modify' ) ; method = method || ( ! resource . hasKey ( ) ? 'create' : 'update' ) ; if ( resource instanceof ODataEntityResource && ( method === 'update' || method === 'modify' ) && ! resource . hasKey ( ) ) return throwError ( 'save: Update/Patch require entity key' ) ; if ( resource instanceof ODataNavigationPropertyResource || method === 'create' ) resource . clearKey ( ) ; let obs$ ; if ( ! validate || this . isValid ( { method , navigation } ) ) { const _entity = this . toEntity ( { changes_only : method === 'modify' , field_mapping : true , include_concurrency : true , include_navigation : navigation , } ) as T ; obs$ = ( method === 'create' ? resource . create ( _entity , options ) : method === 'modify' ? resource . modify ( _entity , { etag : this . annots ( ) . etag , ... options } ) : resource . update ( _entity , { etag : this . annots ( ) . etag , ... options } ) ) . pipe ( map ( ( { entity , annots } ) => ( { entity : entity || _entity , annots } ) ) ) ; } else { obs$ = throwError ( this . _errors ) ; } return this . _request ( obs$ ) ; } destroy ( { ... options } = { } ) { let resource = this . resource ( ) ; if ( resource === undefined ) return throwError ( 'destroy: Resource is undefined' ) ; if ( ! ( resource instanceof ODataEntityResource || resource instanceof ODataNavigationPropertyResource ) ) return throwError ( 'destroy: Resource type ODataEntityResource/ODataNavigationPropertyResource needed' ) ; if ( ! resource . hasKey ( ) ) return throwError ( "destroy: Can't destroy model without key" ) ; const _entity = this . toEntity ( { field_mapping : true } ) as T ; const obs$ = resource . destroy ( { etag : this . annots ( ) . etag , ... options } ) . pipe ( map ( ( { entity , annots } ) => ( { entity : entity || _entity , annots } ) ) ) ; return this . _request ( obs$ ) . pipe ( tap ( ( ) => this . events$ . emit ( new ODataModelEvent ( 'destroy' , { model : this } ) ) ) ) ; } param func query ( func ) { return this . _meta . query ( this , this . resource ( ) , func ) as this ; } param include_navigation returns hasChanged ( { include_navigation = false , } = { } ) { return this . _meta . hasChanged ( this , { include_navigation } ) ; } param ctx returns asEntity < R > ( ctx ) { return this . _meta . asEntity ( this , ctx ) ; } callFunction < P , R > ( name , params , responseType , { ... options } = { } ) { const resource = this . resource ( ) ; if ( ! ( resource instanceof ODataEntityResource ) || ! resource . hasKey ( ) ) return throwError ( "Can't call function without ODataEntityResource with key" ) ; const func = resource . function < P , R > ( name ) . query ( ( q ) => q . apply ( options ) ) ; switch ( responseType ) { case 'property' : return func . callProperty ( params , options ) ; case 'model' : return func . callModel ( params , options ) ; case 'collection' : return func . callCollection ( params , options ) ; default : return func . call ( params , { responseType , ... options } ) ; } } callAction < P , R > ( name , params , responseType , { ... options } = { } ) { const resource = this . resource ( ) ; if ( ! ( resource instanceof ODataEntityResource ) || ! resource . hasKey ( ) ) return throwError ( "Can't call action without ODataEntityResource with key" ) ; const action = resource . action < P , R > ( name ) . query ( ( q ) => q . apply ( options ) ) ; switch ( responseType ) { case 'property' : return action . callProperty ( params , options ) ; case 'model' : return action . callModel ( params , options ) ; case 'collection' : return action . callCollection ( params , options ) ; default : return action . call ( params , { responseType , ... options } ) ; } } cast < S > ( type ) { const resource = this . resource ( ) ; if ( ! ( resource instanceof ODataEntityResource ) ) throw new Error ( `Can't cast to derived model without ODataEntityResource` ) ; return resource . cast < S > ( type ) . asModel ( this . toEntity ( INCLUDE_DEEP ) , { annots : this . annots ( ) } ) ; } fetchNavigationProperty < S > ( name , responseType , { ... options } = { } ) { const nav = this . navigationProperty < S > ( name ) as ODataNavigationPropertyResource < S > ; nav . query ( ( q ) => q . apply ( options ) ) ; switch ( responseType ) { case 'model' : return nav . fetchModel ( options ) ; case 'collection' : return nav . fetchCollection ( options ) ; } } getValue < P > ( name , options ? ) { const field = this . _meta . field ( name ) ; if ( field === undefined || field . navigation ) throw Error ( `Can't find property ${ name }` ) ; let value = ( this as any ) [ name ] as P | ODataModel < P > | ODataCollection < P , ODataModel < P > > ; if ( value === undefined ) { const prop = field . resourceFactory ( this . resource ( ) ) as ODataPropertyResource < P > ; return field . collection ? prop . fetchCollection ( options ) . pipe ( tap ( ( c ) => this . assign ( { [ name ] : c } , { silent : true } ) ) ) : field . isStructuredType ( ) ? prop . fetchModel ( options ) . pipe ( tap ( ( c ) => this . assign ( { [ name ] : c } , { silent : true } ) ) ) : prop . fetchProperty ( options ) . pipe ( tap ( ( c ) => this . assign ( { [ name ] : c } , { silent : true } ) ) ) ; } return of ( value as P ) ; } setReference < N > ( name , model , options ? ) { const reference = ( this . navigationProperty < N > ( name ) as ODataNavigationPropertyResource < N > ) . reference ( ) ; const etag = this . annots ( ) . etag ; let obs$ = NEVER as Observable < any > ; if ( model instanceof ODataModel ) { obs$ = reference . set ( model . _meta . entityResource ( model ) as ODataEntityResource < N > , { etag , ... options } ) ; } else if ( model instanceof ODataCollection ) { obs$ = forkJoin ( model . models ( ) . map ( ( m ) => reference . add ( m . _meta . entityResource ( m ) as ODataEntityResource < N > , options ) ) ) ; } else if ( model === null ) { obs$ = reference . unset ( { etag , ... options } ) ; } this . events$ . emit ( new ODataModelEvent ( 'request' , { model : this , options : { observable : obs$ } , } ) ) ; return obs$ . pipe ( map ( ( model ) => { this . assign ( { [ name ] : model } ) ; this . events$ . emit ( new ODataModelEvent ( 'sync' , { model : this } ) ) ; return this ; } ) ) ; } getReference < P > ( name ) { const field = this . _meta . field ( name ) ; if ( field === undefined || ! field . navigation ) throw Error ( `Can't find navigation property ${ name }` ) ; let model = ( this as any ) [ name ] as ODataModel < P > | ODataCollection < P , ODataModel < P > > ; if ( model === undefined ) { const value = field . collection ? [ ] : this . referenced ( field ) ; model = field . modelCollectionFactory < T , P > ( { parent : this , value } ) ; ( this as any ) [ name ] = model ; } return model ; } equals ( other ) { const thisKey = this . key ( ) ; const otherKey = other . key ( ) ; return ( this === other || ( typeof this === typeof other && ( ( < any > this ) [ this . _meta . cid ] === ( < any > other ) [ this . _meta . cid ] || ( thisKey !== undefined && otherKey !== undefined && Types . isEqual ( thisKey , otherKey ) ) ) ) ) ; } get [ Symbol . toStringTag ] ( ) { return 'Model' ; } collection ( ) { return this . _parent !== null && ODataModelOptions . isCollection ( this . _parent [ 0 ] ) ? ( this . _parent [ 0 ] as ODataCollection < T , ODataModel < T > > ) : undefined ; } next ( ) { return this . collection ( ) ?. next ( this ) ; } prev ( ) { return this . collection ( ) ?. prev ( this ) ; } } export const RESERVED_FIELD_NAMES = Object . getOwnPropertyNames ( ODataModel . prototype ) ;