import { Observable , Subscription } from 'rxjs' ; import { finalize } from 'rxjs/operators' ; import { CID_FIELD_NAME , COMPUTED , DEFAULT_VERSION , OPTIMISTIC_CONCURRENCY , } from '../constants' ; import { ODataHelper } from '../helper' ; import { ODataParserOptions } from '../options' ; import { EntityKey , ODataEntitiesAnnotations , ODataEntityAnnotations , ODataEntityResource , ODataEntitySetResource , ODataNavigationPropertyResource , ODataPropertyResource , ODataQueryOptions , ODataQueryOptionsHandler , ODataResource , ODataSingletonResource , } from '../resources' ; import { ODataEntitySet , ODataStructuredType , ODataStructuredTypeFieldParser , } from '../schema' ; import { Options , OptionsHelper } from '../types' ; import { Objects , Types } from '../utils' ; import type { ODataCollection } from './collection' ; import type { ODataModel } from './model' ; export type ODataModelEventType = 'change' | 'reset' | 'update' | 'destroy' | 'add' | 'remove' | 'invalid' | 'request' | 'sync' | 'attach' ; export class ODataModelEvent < T > { name : ODataModelEventType ; bubbling : boolean = true ; model ? : ODataModel < T > ; collection ? : ODataCollection < T , ODataModel < T > > ; chain : [ ODataModel < any > | ODataCollection < any , ODataModel < any > > , string | number | null ] [ ] ; value ? : any ; previous ? : any ; options ? : any ; constructor ( name , { model , collection , previous , value , track , options , } = { } ) { this . name = name ; this . model = model ; this . collection = collection ; this . previous = previous ; this . value = value ; this . options = options ; this . chain = [ [ ( this . collection || this . model ) as ODataModel < any > | ODataCollection < any , ODataModel < any > > , track || null , ] , ] ; } stopPropagation ( ) { this . bubbling = false ; } push ( model , track ) { this . chain . splice ( 0 , 0 , [ model , track ] ) ; } visited ( model ) { return ( this . chain . some ( ( c ) => c [ 0 ] === model ) && this . chain [ this . chain . length - 1 ] [ 0 ] !== model ) ; } get path ( ) { return this . chain . map ( ( [ , track ] , index ) => typeof track === 'number' ? `[${ track }]` : typeof track === 'string' ? index === 0 ? track : `.${ track }` : '' ) . join ( '' ) ; } } export const BUBBLING = [ 'change' , 'reset' , 'update' , 'destroy' , 'add' , 'remove' , ] ; export const INCLUDE_SHALLOW = { include_concurrency : true , include_computed : true , include_key : true , } ; export const INCLUDE_DEEP = { include_navigation : true , include_non_field : true , ... INCLUDE_SHALLOW , } ; export enum ODataModelState { Added , Removed , Changed , Unchanged } export type ModelOptions = { cid ? : string ; fields : { [ name ] : ModelFieldOptions ; } ; } ; export type ModelFieldOptions = { field ? : string ; default ? : any ; required ? : boolean ; concurrency ? : boolean ; maxLength ? : number ; minLength ? : number ; min ? : number ; max ? : number ; pattern ? : RegExp ; } ; export function Model ( { cid = CID_FIELD_NAME } = { } ) { return < T extends { new ( ... args ) : { } ; } > ( constructor ) => { const Klass = < any > constructor ; if ( ! Klass . hasOwnProperty ( 'options' ) ) Klass . options = { fields : { } } as ModelOptions ; Klass . options . cid = cid ; return constructor ; } ; } export function ModelField ( { name , ... options } = { } ) { return ( target , key ) : void => { const Klass = target . constructor ; if ( ! Klass . hasOwnProperty ( 'options' ) ) Klass . options = { fields : { } } as ModelOptions ; options . field = name || key ; Klass . options . fields [ key ] = options ; } ; } export type ODataModelFieldOptions < F > = ModelFieldOptions & { name : string ; field : string ; parser : ODataStructuredTypeFieldParser < F > ; } ; export class ODataModelField < F > { name : string ; field : string ; parser : ODataStructuredTypeFieldParser < F > ; options : ODataModelOptions < any > ; meta ? : ODataModelOptions < any > ; default ? : any ; required : boolean ; concurrency : boolean ; maxLength ? : number ; minLength ? : number ; min ? : number ; max ? : number ; pattern ? : RegExp ; constructor ( options , { name , field , parser , ... opts } ) { this . options = options ; this . name = name ; this . field = field ; this . parser = parser ; this . default = opts . default || parser . default ; this . required = Boolean ( opts . required || ! parser . nullable ) ; this . concurrency = Boolean ( opts . concurrency ) ; this . maxLength = opts . maxLength || parser . maxLength ; this . minLength = opts . minLength ; this . min = opts . min ; this . max = opts . max ; this . pattern = opts . pattern ; } get api ( ) { return this . options . api ; } get type ( ) { return this . parser . type ; } get navigation ( ) { return Boolean ( this . parser . navigation ) ; } get collection ( ) { return Boolean ( this . parser . collection ) ; } annotatedValue < T > ( term ) { return this . parser . annotatedValue < T > ( term ) ; } configure ( { findOptionsForType , concurrency , options , } ) { this . meta = findOptionsForType ( this . parser . type ) ; if ( concurrency ) this . concurrency = concurrency ; if ( this . default !== undefined ) this . default = this . deserialize ( this . default , options ) ; } isKey ( ) { return this . parser . isKey ( ) ; } hasReferentials ( ) { return this . parser . hasReferentials ( ) ; } get referentials ( ) { return this . parser . referentials ; } isStructuredType ( ) { return this . parser . isStructuredType ( ) ; } structured ( ) { let structuredType = this . api . findStructuredTypeForType < F > ( this . parser . type ) ; if ( ! structuredType ) throw new Error ( `Could not find structured type for ${ this . parser . type }` ) ; return structuredType ; } isEnumType ( ) { return this . parser . isEnumType ( ) ; } enum ( ) { let enumType = this . api . findEnumTypeForType < F > ( this . parser . type ) ; if ( ! enumType ) throw new Error ( `Could not find enum type for ${ this . parser . type }` ) ; return enumType ; } validate ( value , { method , navigation = false , } = { } ) { if ( ODataModelOptions . isModel ( value ) ) { return ! value . isValid ( { method , navigation } ) ? value . _errors : undefined ; } else if ( ODataModelOptions . isCollection ( value ) ) { return value . models ( ) . some ( ( m ) => ! m . isValid ( { method , navigation } ) ) ? value . models ( ) . map ( ( m ) => m . _errors ) : undefined ; } else { const computed = this . annotatedValue < boolean > ( COMPUTED ) ; let errors = this . parser ?. validate ( value , { method , navigation } ) || [ ] ; if ( this . required && ( value === null || ( value === undefined && method !== 'modify' ) ) && ! ( computed && method === 'create' ) ) { errors [ 'push' ] ( `required` ) ; } if ( this . maxLength !== undefined && typeof value === 'string' && value . length > this . maxLength ) { errors [ 'push' ] ( `maxlength` ) ; } if ( this . minLength !== undefined && typeof value === 'string' && value . length < this . minLength ) { errors [ 'push' ] ( `minlength` ) ; } if ( this . min !== undefined && typeof value === 'number' && value < this . min ) { errors [ 'push' ] ( `min` ) ; } if ( this . max !== undefined && typeof value === 'number' && value > this . max ) { errors [ 'push' ] ( `max` ) ; } if ( this . pattern !== undefined && typeof value === 'string' && ! this . pattern . test ( value ) ) { errors [ 'push' ] ( `pattern` ) ; } return ! Types . isEmpty ( errors ) ? errors : undefined ; } } defaults ( ) { return this . isStructuredType ( ) && this . meta !== undefined ? this . meta . defaults ( ) : this . default ; } deserialize ( value , options ? ) { const parserOptions = options !== undefined ? new ODataParserOptions ( options ) : this . api . options ; return this . parser . deserialize ( value , parserOptions ) ; } serialize ( value , options ? ) { const parserOptions = options !== undefined ? new ODataParserOptions ( options ) : this . api . options ; return this . parser . serialize ( value , parserOptions ) ; } encode ( value , options ? ) { const parserOptions = options !== undefined ? new ODataParserOptions ( options ) : this . api . options ; return this . parser . encode ( value , parserOptions ) ; } resourceFactory < T , F > ( base ) { if ( ! ( base instanceof ODataEntityResource || base instanceof ODataNavigationPropertyResource || base instanceof ODataPropertyResource ) ) throw new Error ( "Can't build resource for non compatible base type" ) ; return this . navigation ? ( base as ODataEntityResource < T > ) . navigationProperty < F > ( this . parser . name ) : ( base as ODataEntityResource < T > ) . property < F > ( this . parser . name ) ; } annotationsFactory ( base ) { return this . parser . collection ? new ODataEntitiesAnnotations ( base . helper , base . property ( this . parser . name ) || { } ) : new ODataEntityAnnotations ( base . helper , base . property ( this . parser . name ) || { } ) ; } schemaFactory < T , F > ( base ) { return this . api . findStructuredTypeForType ( this . parser . type ) ; } modelCollectionFactory < T , F > ( { parent , value , reset , } ) { const annots = this . annotationsFactory ( parent . annots ( ) ) ; const Model = this . api . modelForType ( this . parser . type ) ; const Collection = this . api . collectionForType ( this . parser . type ) ; if ( Model === undefined || Collection === undefined ) throw Error ( `No model for ${ this . name }` ) ; return this . parser . collection ? ( new Collection ( ( value || [ ] ) as F [ ] , { annots : annots as ODataEntitiesAnnotations , parent : [ parent , this ] , reset , } ) as ODataCollection < F , ODataModel < F > > ) : ( new Model ( ( value || { } ) as F , { annots : annots as ODataEntityAnnotations , parent : [ parent , this ] , reset , } ) as ODataModel < F > ) ; } } export type ODataModelRelation < T > = { state : ODataModelState ; model ? : ODataModel < T > | ODataCollection < T , ODataModel < T > > | null ; field : ODataModelField < T > ; subscription ? : Subscription ; } ; export type ODataModelEntry < T , M extends ODataModel < T > > = { state : ODataModelState ; model : M ; key ? : EntityKey < T > | { [ name ] : any ; } ; subscription ? : Subscription ; } ; export class ODataModelOptions < T > { name : string ; cid : string ; base ? : string ; open ? : boolean ; private _fields : ODataModelField < any > [ ] ; schema : ODataStructuredType < T > ; entitySet ? : ODataEntitySet ; parent ? : ODataModelOptions < any > ; children : ODataModelOptions < any > [ ] = [ ] ; constructor ( { options , schema , } ) { this . name = schema . name ; this . base = schema . base ; this . open = schema . open ; this . schema = schema ; this . cid = options ?. cid || CID_FIELD_NAME ; this . _fields = Object . entries ( options . fields ) . map ( ( [ name , options ] ) => { const { field , ... opts } = options ; if ( field === undefined || name === undefined ) throw new Error ( 'Model Properties need name and field' ) ; const parser = this . schema . findFieldByName < T > ( field as keyof T ) ; if ( parser === undefined ) throw new Error ( `No parser for ${ field } with name = ${ name }` ) ; return new ODataModelField < T > ( this , { name , field , parser , ... opts } ) ; } ) ; } get api ( ) { return this . schema . api ; } type ( ) { return this . schema . type ( ) ; } isTypeOf ( type ) { return this . schema . isTypeOf ( type ) ; } findChildOptions ( predicate ) { if ( predicate ( this ) ) return this ; let match ; for ( let ch of this . children ) { match = ch . findChildOptions ( predicate ) ; if ( match !== undefined ) break ; } return match ; } configure ( { findOptionsForType , options , } ) { if ( this . base ) { const parent = findOptionsForType ( this . base ) as ODataModelOptions < any > ; parent . children . push ( this ) ; this . parent = parent ; } this . entitySet = this . api . findEntitySetForEntityType ( this . type ( ) ) ; let concurrencyFields = [ ] ; if ( this . entitySet !== undefined ) { concurrencyFields = this . entitySet . annotatedValue < string [ ] > ( OPTIMISTIC_CONCURRENCY ) || [ ] ; } this . _fields . forEach ( ( field ) => { let concurrency = concurrencyFields . indexOf ( field . field ) !== - 1 ; field . configure ( { findOptionsForType , concurrency , options , } ) ; } ) ; } fields ( { include_navigation = false , include_parents = true , } = { } ) { return [ ... ( include_parents && this . parent !== undefined ? this . parent . fields ( { include_navigation , include_parents } ) : [ ] ) , ... this . _fields . filter ( ( field ) => include_navigation || ! field . navigation ) , ] ; } field < F > ( name ) { let field = this . fields ( { include_parents : true , include_navigation : true , } ) . find ( ( modelField ) => modelField . name === name || modelField . field === name ) ; if ( field === undefined ) throw new Error ( `No field with name ${ name }` ) ; return field ; } attach ( self , resource ) { if ( self . _resource !== null && resource . type ( ) !== self . _resource . type ( ) && ! resource . isSubtypeOf ( self . _resource ) ) throw new Error ( `Can't reattach ${ resource . type ( ) } to ${ self . _resource . type ( ) }` ) ; const current = self . _resource ; if ( current === null || ! current . isEqualTo ( resource ) ) { self . _resource = resource ; self . events$ . emit ( new ODataModelEvent ( 'attach' , { model : self , previous : current , value : resource , } ) ) ; } } static chain ( child ) { const chain = [ ] as any [ ] ; let tuple = [ child , null ] ; while ( tuple !== null ) { const parent = tuple as [ ODataModel < any > | ODataCollection < any , ODataModel < any > > , ODataModelField < any > | null ] ; if ( chain . some ( ( p ) => p [ 0 ] === parent [ 0 ] ) ) break ; chain . splice ( 0 , 0 , parent ) ; tuple = tuple [ 0 ] . _parent ; } return chain ; } static resource < T > ( child ) { let resource = undefined ; for ( let [ model , field ] of ODataModelOptions . chain ( child ) ) { resource = resource || model . _resource || ( ODataModelOptions . isModel ( model ) ? ( model as ODataModel < any > ) . _meta . modelResourceFactory ( ) : ( model as ODataCollection < any , ODataModel < any > > ) . _model . meta . collectionResourceFactory ( ) ) ; if ( resource === undefined ) break ; if ( ODataModelOptions . isModel ( model ) ) { let key = ( model as ODataModel < any > ) . key ( { field_mapping : true , } ) as EntityKey < any > ; if ( key !== undefined ) resource = resource instanceof ODataEntitySetResource ? resource . entity ( key ) : ( resource as ODataEntityResource < T > ) . key ( key ) ; } if ( field === null ) { const query = model . _resource ?. cloneQuery < T > ( ) . toQueryArguments ( ) ; if ( query !== undefined ) resource . query ( ( q ) => q . apply ( query ) ) ; continue ; } resource = ( field as ODataModelField < any > ) . resourceFactory < any , any > ( resource as ODataResource < any > ) ; } if ( resource === undefined ) throw new Error ( `resource: Can't build resource for ${ child }` ) ; return resource ; } collectionResourceFactory ( query ? ) { if ( this . entitySet === undefined ) return undefined ; return ODataEntitySetResource . factory < T > ( this . api , { path : this . entitySet . name , schema : this . schema , query , } ) ; } modelResourceFactory ( query ? ) { const resource = this . collectionResourceFactory ( query ) ; if ( resource instanceof ODataEntitySetResource ) return resource . entity ( ) ; return resource as ODataEntityResource < T > | ODataNavigationPropertyResource < T > | ODataPropertyResource < T > | ODataSingletonResource < T > | undefined ; } entityResource ( self ) { let resource = this . modelResourceFactory ( self . _resource !== null ? self . _resource . cloneQuery ( ) : undefined ) ; if ( resource === undefined ) throw new Error ( `entityResource: Can't build resource for ${ self }` ) ; const key = self . key ( { field_mapping : true } ) as EntityKey < T > ; if ( key !== undefined ) return resource . key ( key ) ; return resource as ODataResource < T > ; } bind ( self , { parent , resource , annots , } = { } ) { if ( parent !== undefined ) { self . _parent = parent ; } resource = resource || this . modelResourceFactory ( ) ; if ( resource !== undefined ) { this . attach ( self , resource as ODataEntityResource < T > | ODataPropertyResource < T > | ODataNavigationPropertyResource < T > | ODataSingletonResource < T > ) ; } self . _annotations = annots || new ODataEntityAnnotations ( ODataHelper [ DEFAULT_VERSION ] ) ; const fields = this . fields ( { include_navigation : true , include_parents : true , } ) ; for ( let field of fields ) { Object . defineProperty ( self , field . name , { configurable : true , get : ( ) => this . _get ( self , field as ODataModelField < any > ) , set : ( value ) => this . _set ( self , field as ODataModelField < any > , value ) , } ) ; } } query ( self , resource , func ) { resource . query ( func ) ; this . attach ( self , resource ) ; return self ; } resolveKey ( value , { field_mapping = false , resolve = true , } = { } ) { const keyTypes = this . schema . keys ( { include_parents : true } ) ; const key = { } ; for ( var kt of keyTypes ) { let v = value as any ; let options = this as ODataModelOptions < any > ; let field ; for ( let name of kt . name . split ( '/' ) ) { if ( options === undefined ) break ; field = options . fields ( { include_parents : true } ) . find ( ( field ) => field . field === name ) ; if ( field !== undefined ) { v = Types . isPlainObject ( v ) || ODataModelOptions . isModel ( v ) ? v [ field . name ] : v ; options = field . meta as ODataModelOptions < any > ; } } if ( field === undefined ) return undefined ; let name = field_mapping ? field . field : field . name ; if ( kt . alias !== undefined ) name = kt . alias ; key [ name ] = v ; } if ( Types . isEmpty ( key ) ) return undefined ; return resolve ? Objects . resolveKey ( key ) : key ; } resolveReferential ( value , field , { field_mapping = false , resolve = true , } = { } ) { const referential = { } ; for ( var ref of field . referentials ) { let from = this . fields ( { include_parents : true } ) . find ( ( p ) => p . field === ref . referencedProperty ) ; let to = field . options . fields ( { include_parents : true } ) . find ( ( field ) => field . field === ref . property ) ; if ( from !== undefined && to !== undefined ) { let name = field_mapping ? to . field : to . name ; referential [ name ] = value && ( value as any ) [ from . name ] ; } } if ( Types . isEmpty ( referential ) ) return undefined ; return resolve ? Objects . resolveKey ( referential , { single : false } ) : referential ; } resolveReferenced ( value , field , { field_mapping = false , resolve = true , } = { } ) { const referenced = { } ; for ( var ref of field . referentials ) { let from = this . fields ( { include_parents : true } ) . find ( ( field ) => field . field === ref . property ) ; let to = ( field . meta as ODataModelOptions < any > ) . fields ( { include_parents : true } ) . find ( ( field ) => field . field === ref . referencedProperty ) ; if ( from !== undefined && to !== undefined ) { let name = field_mapping ? to . field : to . name ; referenced [ name ] = value && ( value as any ) [ from . name ] ; } } if ( Types . isEmpty ( referenced ) ) return undefined ; return resolve ? Objects . resolveKey ( referenced , { single : false } ) : referenced ; } validate ( self , { method , navigation = false , } = { } ) { const errors = this . fields ( { include_parents : true , include_navigation : navigation , } ) . reduce ( ( acc , field ) => { let value = ( self as any ) [ field . name ] ; let errs = field . validate ( value , { method } ) ; return errs !== undefined ? Object . assign ( acc , { [ field . name ] : errs } ) : acc ; } , { } ) ; return ! Types . isEmpty ( errors ) ? errors : undefined ; } defaults ( ) { const defs = this . fields ( ) . reduce ( ( acc , field ) => { let value = field . defaults ( ) ; return value !== undefined ? Object . assign ( acc , { [ field . name ] : value } ) : acc ; } , { } ) ; return ! Types . isEmpty ( defs ) ? defs : undefined ; } hasChanged ( self , { include_navigation = false } = { } ) { return ( ! Types . isEmpty ( self . _changes ) || Object . values ( self . _relations ) . filter ( ( { field } ) => ! field . navigation || include_navigation ) . some ( ( { field , state , model } ) => ( ! include_navigation || field . navigation ) && ( state === ODataModelState . Changed || ( model != undefined && model . hasChanged ( { include_navigation } ) ) ) ) ) ; } asEntity < R , M extends ODataModel < T > > ( self , func ) { const parent = self . _parent ; self . _parent = null ; const result = func ( self ) ; if ( result instanceof Observable ) { return ( result as any ) . pipe ( finalize ( ( ) => ( self . _parent = parent ) ) ) ; } else { self . _parent = parent ; return result ; } } toEntity ( self , { 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 = [ ] , } = { } ) { let attrs = self . attributes ( { changes_only , field_mapping , include_concurrency , include_computed , include_non_field , } ) ; let relations = Object . entries ( self . _relations ) . filter ( ( [ , { model } ] ) => chain . every ( ( c ) => c !== model ) ) . filter ( ( [ , { model , state } ] ) => ! changes_only || ( changes_only && ( state === ODataModelState . Changed || ( model != undefined && model . hasChanged ( { include_navigation } ) ) ) ) ) . filter ( ( [ , { field , model } ] ) => ( include_navigation && field . navigation && model !== null ) || ! field . navigation ) . map ( ( [ k , { model , field , state } ] ) => { let changesOnly = changes_only && state !== ODataModelState . Changed && ! ! field . navigation ; let includeKey = include_key && ! ! field . navigation ; if ( ODataModelOptions . isModel ( model ) ) { return [ k , ( model as ODataModel < any > ) . toEntity ( { client_id , include_navigation , include_concurrency , include_non_field , field_mapping , changes_only : changesOnly , include_key : includeKey , chain : [ self , ... chain ] , } ) , ] ; } else if ( ODataModelOptions . isCollection ( model ) ) { return [ k , ( model as ODataCollection < any , ODataModel < any > > ) . toEntities ( { client_id , include_navigation , include_concurrency , include_non_field , field_mapping , changes_only : changesOnly , include_key : includeKey , chain : [ self , ... chain ] , } ) , ] ; } return [ k , model ] ; } ) . reduce ( ( acc , [ k , v ] ) => { const name = field_mapping ? this . fields ( ) . find ( ( field ) => field . name === k ) ?. field || k : k ; return Object . assign ( acc , { [ name ] : v } ) ; } , { } ) ; let entity = { ... attrs , ... relations } ; if ( client_id ) { ( < any > entity ) [ this . cid ] = ( < any > self ) [ this . cid ] ; } if ( include_key ) { entity = { ... entity , ... ( this . resolveKey ( self , { field_mapping , resolve : false } ) as { } ) , } ; } if ( self . _parent !== null && ( ( ODataModelOptions . isModel ( self . _parent [ 0 ] ) && ( self . _parent [ 1 ] as ODataModelField < any > ) . meta !== self . _meta ) || ( ODataModelOptions . isCollection ( self . _parent [ 0 ] ) && ( self . _parent [ 0 ] as ODataCollection < any , ODataModel < any > > ) . _model . meta !== self . _meta ) ) ) { this . api . options . helper . type ( entity , this . schema . type ( ) ) ; } return entity ; } attributes ( self , { changes_only = false , include_concurrency = false , include_computed = false , include_non_field = false , field_mapping = false , } = { } ) { const fieldAttrs = this . fields ( ) . reduce ( ( acc , f ) => { const isChanged = f . name in self . _changes ; const name = field_mapping ? f . field : f . name ; const computed = f . annotatedValue < boolean > ( COMPUTED ) ; const value = isChanged ? self . _changes [ f . name ] : self . _attributes [ f . name ] ; if ( f . concurrency && include_concurrency ) { return Object . assign ( acc , { [ name ] : value } ) ; } else if ( computed && include_computed ) { return Object . assign ( acc , { [ name ] : value } ) ; } else if ( changes_only && isChanged ) { return Object . assign ( acc , { [ name ] : value } ) ; } else if ( ! changes_only && ! f . concurrency && ! computed ) { return Object . assign ( acc , { [ name ] : value } ) ; } else { return acc ; } } , { } ) ; if ( ! include_non_field ) return fieldAttrs ; const names = Object . keys ( fieldAttrs ) ; const nonFieldAttrs = Object . entries ( self ) . filter ( ( [ k ] ) => names . indexOf ( k ) === - 1 && ! k . startsWith ( '_' ) && ! k . endsWith ( '$' ) ) . reduce ( ( acc , [ k , v ] ) => Object . assign ( acc , { [ k ] : v } ) , { } ) ; return { ... fieldAttrs , ... nonFieldAttrs } ; } reset ( self , { name , silent = false } = { } ) { if ( ! Types . isEmpty ( name ) && ( name as string ) in self . _changes ) { const value = self . _attributes [ name as string ] ; const previous = self . _changes [ name as string ] ; delete self . _changes [ name as string ] ; if ( ! silent ) { self . events$ . emit ( new ODataModelEvent ( 'change' , { model : self , track : name , value , previous , } ) ) ; } } else if ( Types . isEmpty ( name ) ) { const entries = Object . entries ( self . _changes ) ; self . _changes = { } ; if ( ! silent ) { entries . forEach ( ( entry ) => { self . events$ . emit ( new ODataModelEvent ( 'change' , { track : entry [ 0 ] , model : self , value : self . _attributes [ entry [ 0 ] ] , previous : entry [ 1 ] , } ) ) ; } ) ; } } } assign ( self , entity , { reset = false , reparent = false , silent = false , } = { } ) { self . _reset = reset ; self . _reparent = reparent ; self . _silent = silent ; const changes = [ ] ; const model = self as any ; Object . entries ( entity ) . filter ( ( [ , value ] ) => value !== undefined ) . forEach ( ( [ key , value ] ) => { const field = this . fields ( { include_parents : true , include_navigation : true , } ) . find ( ( field ) => ( self . _reset && field . field === key ) || field . name === key ) ; if ( field !== undefined ) { if ( this . _set ( self , field , value ) ) { changes . push ( field . name ) ; } } else { const current = model [ key ] ; model [ key ] = value ; if ( current !== value ) changes . push ( key ) ; } } ) ; if ( ( ! self . _silent && changes . length > 0 ) || self . _reset ) { self . events$ . emit ( new ODataModelEvent ( self . _reset ? 'reset' : 'update' , { model : self , options : { changes } , } ) ) ; } self . _reset = false ; self . _reparent = false ; self . _silent = false ; } static isModel ( obj ) { return Types . rawType ( obj ) === 'Model' ; } static isCollection ( obj ) { return Types . rawType ( obj ) === 'Collection' ; } private updateCollection < F > ( self , field , collection , value ) { collection . _annotations = field . annotationsFactory ( self . annots ( ) ) as ODataEntitiesAnnotations ; collection . assign ( value as T [ ] | { [ name ] : any ; } [ ] , { reset : self . _reset , reparent : self . _reparent , silent : self . _silent , } ) ; return collection . hasChanged ( ) ; } private updateModel < F > ( self , field , model , value ) { model . _annotations = field . annotationsFactory ( self . annots ( ) ) as ODataEntityAnnotations ; model . assign ( value as F | { [ name ] : any ; } , { reset : self . _reset , reparent : self . _reparent , silent : self . _silent , } ) ; return model . hasChanged ( ) ; } private _get < F > ( self , field ) { if ( field . isStructuredType ( ) ) { return self . _relations [ field . name ] ?. model ; } else { return this . attributes ( self , { include_concurrency : true , include_computed : true , } ) [ field . name ] ; } } private _setStructured < F > ( self , field , value ) { let changed = false ; if ( ! ( field . name in self . _relations ) ) { self . _relations [ field . name ] = { state : ODataModelState . Unchanged , field : field , } ; } const relation = self . _relations [ field . name ] ; const currentModel = relation . model ; if ( value === null ) { relation . model = value as null ; changed = currentModel !== value ; this . _unsubscribe ( self , relation ) ; } else if ( ODataModelOptions . isCollection ( currentModel ) ) { if ( ODataModelOptions . isCollection ( value ) ) { if ( ( currentModel as ODataCollection < F , ODataModel < F > > ) . equals ( value as ODataCollection < F , ODataModel < F > > ) ) { changed = false ; } this . _unsubscribe ( self , relation ) ; relation . model = value as ODataCollection < F , ODataModel < F > > ; this . _subscribe ( self , relation ) ; if ( self . _reparent ) relation . model . _parent = [ self , field ] ; changed = true ; } else if ( Types . isArray ( value ) ) { changed = this . updateCollection < F > ( self , field , currentModel as ODataCollection < F , ODataModel < F > > , value as T [ ] | { [ name ] : any ; } [ ] ) ; } } else if ( ODataModelOptions . isModel ( currentModel ) ) { if ( ODataModelOptions . isModel ( value ) ) { if ( ( currentModel as ODataModel < F > ) . equals ( value as ODataModel < F > ) ) { changed = false ; } this . _unsubscribe ( self , relation ) ; relation . model = value as ODataModel < F > ; this . _subscribe ( self , relation ) ; if ( self . _reparent ) relation . model . _parent = [ self , field ] ; changed = true ; } else if ( Types . isPlainObject ( value ) ) { changed = this . updateModel < F > ( self , field , currentModel as ODataModel < F > , value as F | { [ name ] : any ; } ) ; } } else { relation . model = ODataModelOptions . isCollection ( value ) || ODataModelOptions . isModel ( value ) ? ( value as ODataModel < F > | ODataCollection < F , ODataModel < F > > ) : field . modelCollectionFactory < T , F > ( { parent : self , value : value , reset : self . _reset , } ) ; changed = true ; this . _subscribe ( self , relation ) ; if ( self . _reparent ) relation . model . _parent = [ self , field ] ; } if ( ! ODataModelOptions . isCollection ( relation . model ) ) { var ref = field . meta ?. resolveReferential ( relation . model , field ) ; if ( ref !== undefined ) { Object . entries ( ref ) . forEach ( ( [ k , v ] ) => this . _setValue ( self , k , v , false ) ) ; } } relation . state = self . _reset || ! changed ? ODataModelState . Unchanged : ODataModelState . Changed ; if ( ! self . _silent && changed ) { self . events$ . emit ( new ODataModelEvent ( 'change' , { track : field . name , model : self , value : relation . model , previous : currentModel , } ) ) ; } return changed ; } private _setValue < F > ( self , field , value , key ? ) { let changed = false ; const attrs = this . attributes ( self , { include_concurrency : true , include_computed : true , } ) ; const currentValue = attrs [ field ] ; changed = ! Types . isEqual ( currentValue , value ) ; if ( self . _reset ) { delete self . _changes [ field ] ; self . _attributes [ field ] = value ; } else if ( Types . isEqual ( value , self . _attributes [ field ] ) ) { delete self . _changes [ field ] ; } else if ( changed ) { self . _changes [ field ] = value ; } if ( ! self . _silent && changed ) { self . events$ . emit ( new ODataModelEvent ( 'change' , { track : field , model : self , value , previous : currentValue , options : { key } , } ) ) ; } return changed ; } private _set < F > ( self , field , value ) { return field . isStructuredType ( ) ? this . _setStructured ( self , field , value ) : this . _setValue ( self , field . name , value , field . isKey ( ) ) ; } private _unsubscribe < F > ( self , relation ) { if ( relation . subscription ) { relation . subscription . unsubscribe ( ) ; delete relation . subscription ; } } private _subscribe < F > ( self , relation ) { if ( relation . subscription ) { throw new Error ( 'Subscription already exists' ) ; } if ( relation . model == null ) { throw new Error ( 'Subscription model is null' ) ; } relation . subscription = relation . model . events$ . subscribe ( ( event ) => { if ( BUBBLING . indexOf ( event . name ) !== - 1 && event . bubbling && ! event . visited ( self ) ) { if ( event . model === relation . model ) { if ( event . name === 'change' && relation . field . navigation && event . options ?. key ) { var ref = ( relation . model as ODataModel < any > ) . referential ( relation . field ) ; if ( ref !== undefined ) { Object . entries ( ref ) . forEach ( ( [ k , v ] ) => this . _setValue ( self , k , v , false ) ) ; } } } event . push ( self , relation . field . name ) ; self . events$ . emit ( event ) ; } } ) ; } }