import { CollectionProxy , RecordProxy , NullProxy } from "./proxies" ; import { ValidationErrorBuilder } from "./util/validation-error-builder" ; import { refreshJWT } from "./util/refresh-jwt" ; import relationshipIdentifiersFor from "./util/relationship-identifiers" ; import { Request , RequestVerbs , JsonapiResponse } from "./request" ; import { WritePayload } from "./util/write-payload" ; import { flipEnumerable , getNonEnumerables } from "./util/enumerables" ; import { CredentialStorage , InMemoryStorageBackend , StorageBackend } from "./credential-storage" ; import { IDMap } from "./id-map" ; import { deserialize , deserializeInstance } from "./util/deserialize" ; import { Attribute } from "./attribute" ; import DirtyChecker from "./util/dirty-check" ; import { Scope , WhereClause , SortScope , FieldArg , StatsScope , IncludeScope } from "./scope" ; import { JsonapiTypeRegistry } from "./jsonapi-type-registry" ; import { camelize , underscore , dasherize } from "inflected" ; import { ILogger , logger as defaultLogger } from "./logger" ; import { MiddlewareStack , BeforeFilter , AfterFilter } from "./middleware-stack" ; import { Omit } from "./util/omit" ; import { EventBus } from "./event-bus" ; import { JsonapiResource , JsonapiResponseDoc , JsonapiResourceIdentifier } from "./jsonapi-spec" ; import { cloneDeep } from "./util/clonedeep" ; import { nonenumerable } from "./util/decorators" ; import { IncludeScopeHash } from "./util/include-directive" ; import { ValidationErrors } from "./validation-errors" ; export type KeyCaseValue = "dash" | "camel" | "snake" ; export interface KeyCase { server : KeyCaseValue ; client : KeyCaseValue ; } export interface ModelConfiguration { baseUrl : string ; apiNamespace : string ; jsonapiType : string ; endpoint : string ; credentialStorageBackend : StorageBackend ; jwt : string ; jwtStorage : string | false ; keyCase : KeyCase ; strictAttributes : boolean ; logger : ILogger ; } export type ModelConfigurationOptions = Partial < ModelConfiguration > ; export type ModelIdFields = "id" | "temp_id" ; export type ModelAttrs < K extends keyof T , T extends JSORMBase > = { [ P in K ] ? : T [ P ] ; } & Partial < Record < ModelIdFields , string > > ; export type ModelAttrChanges < T > = { [ P in keyof T ] ? : T [ P ] [ ] ; } & Partial < Record < ModelIdFields , string [ ] > > ; export type ModelRecord < T extends JSORMBase > = ModelAttrs < keyof ( Omit < T , keyof JSORMBase > ) , T > ; export type ModelAttributeChangeSet < T extends JSORMBase > = ModelAttrChanges < Omit < T , keyof JSORMBase > > ; export interface SaveOptions { with ? : IncludeScope ; } export type ExtendedModel < Superclass extends typeof JSORMBase , Attributes , Methods , Prototype = Superclass [ "prototype" ] & Attributes & Methods > = { new ( attrs ? ) : Prototype ; prototype : Prototype ; } & Superclass ; export type AttrMap < T > = { [ P in keyof T ] : Attribute < T [ P ] > ; } ; export type DefaultAttrs = Record < string , any > ; export interface DefaultMethods < V > { [ key ] : ( this : V , ... args : any [ ] ) => any ; } export interface ExtendOptions < M , Attributes = DefaultAttrs , Methods = DefaultMethods < M > > { static ? : ModelConfigurationOptions ; attrs ? : AttrMap < Attributes > ; methods ? : ThisType < M & Attributes & Methods > & Methods ; } export const applyModelConfig = < T extends typeof JSORMBase > ( ModelClass , config ) : void => { let k ; config = { ... config } ; if ( config . credentialStorageBackend ) { ModelClass . credentialStorageBackend = config . credentialStorageBackend ; delete config . jwtStorage ; } if ( config . jwtStorage ) { ModelClass . jwtStorage = config . jwtStorage ; delete config . jwtStorage ; } if ( config . jwt ) { ModelClass . setJWT ( config . jwt ) ; delete config . jwt ; } for ( k in config ) { if ( config . hasOwnProperty ( k ) ) { ModelClass [ k ] = config [ k ] ; } } if ( ModelClass . isBaseClass === undefined ) { ModelClass . setAsBase ( ) ; } else if ( ModelClass . isBaseClass === true ) { ModelClass . isBaseClass = false ; } } ; export class JSORMBase { static baseUrl = "http://please-set-a-base-url.com" ; static apiNamespace = "/" ; static jsonapiType ? : string ; static endpoint : string ; static isBaseClass : boolean ; static keyCase : KeyCase = { server : "snake" , client : "camel" } ; static strictAttributes : boolean = false ; static logger : ILogger = defaultLogger ; static sync : boolean = false ; static attributeList : Record < string , Attribute > = { } ; static extendOptions : any ; static parentClass : typeof JSORMBase ; static currentClass : typeof JSORMBase = JSORMBase ; static beforeFetch : BeforeFilter | undefined ; static afterFetch : AfterFilter | undefined ; private static _typeRegistry : JsonapiTypeRegistry ; private static _IDMap : IDMap ; private static _middlewareStack : MiddlewareStack ; private static _credentialStorageBackend : StorageBackend ; private static _credentialStorage : CredentialStorage ; private static _jwtStorage : string | false = "jwt" ; static get credentialStorage ( ) : CredentialStorage { return this . _credentialStorage ; } static set jwtStorage ( val ) { if ( val !== this . _jwtStorage ) { this . _jwtStorage = val ; this . credentialStorageBackend = this . _credentialStorageBackend ; } } static get jwtStorage ( ) : string | false { return this . _jwtStorage ; } static set credentialStorageBackend ( backend ) { this . _credentialStorageBackend = backend ; this . _credentialStorage = new CredentialStorage ( this . jwtStorage || 'jwt' , this . _credentialStorageBackend ) ; } static get credentialStorageBackend ( ) : StorageBackend { return this . _credentialStorageBackend ; } static initializeCredentialStorage ( ) { if ( this . jwtStorage && typeof localStorage !== "undefined" ) { this . credentialStorageBackend = localStorage ; } else { this . credentialStorageBackend = new InMemoryStorageBackend ( ) ; } } static readonly isJSORMModel : boolean = true ; static fromJsonapi ( resource , payload ) { return deserialize ( this . typeRegistry , resource , payload ) ; } static inherited ( subclass ) { subclass . parentClass = this ; subclass . currentClass = subclass ; subclass . prototype . klass = subclass ; subclass . attributeList = cloneDeep ( subclass . attributeList ) ; } static setAsBase ( ) { this . isBaseClass = true ; this . jsonapiType = undefined ; if ( ! this . typeRegistry ) { this . typeRegistry = new JsonapiTypeRegistry ( this ) ; } if ( ! this . middlewareStack ) { this . _middlewareStack = new MiddlewareStack ( ) ; } if ( ! this . _IDMap ) { this . _IDMap = new IDMap ( ) ; } } static isSubclassOf ( maybeSuper ) { let current = this . currentClass ; while ( current ) { if ( current === maybeSuper ) { return true ; } current = current . parentClass ; } return false ; } static get baseClass ( ) { let current = this . currentClass ; while ( current ) { if ( current . isBaseClass ) { return current ; } current = current . parentClass ; } } static get store ( ) : IDMap { if ( this . baseClass === undefined ) { throw new Error ( `No base class for ${ this . name }` ) ; } return this . baseClass . _IDMap ; } static get typeRegistry ( ) : JsonapiTypeRegistry { if ( this . baseClass === undefined ) { throw new Error ( `No base class for ${ this . name }` ) ; } return this . baseClass . _typeRegistry ; } static set typeRegistry ( registry ) { if ( ! this . isBaseClass ) { throw new Error ( "Cannot set a registry on a non-base class" ) ; } this . _typeRegistry = registry ; } static registerType ( ) { if ( ! this . jsonapiType ) { return ; } const existingType = this . typeRegistry . get ( this . jsonapiType ) ; if ( existingType ) { if ( this . isSubclassOf ( existingType ) ) { return ; } } this . typeRegistry . register ( this . jsonapiType , this ) ; } static extend < T extends typeof JSORMBase , ExtendedAttrs , Methods , SuperType = T > ( this , options ) { class Subclass extends ( < ExtendedModel < typeof JSORMBase , { } , { } > > this ) { } this . inherited ( < any > Subclass ) ; const attrs = { } ; if ( options . attrs ) { for ( const key in options . attrs ) { if ( options . attrs . hasOwnProperty ( key ) ) { const attr = options . attrs [ key ] ; if ( ! attr . name ) { attr . name = key ; } attr . owner = < any > Subclass ; attrs [ key ] = attr ; } } } Subclass . attributeList = Object . assign ( { } , Subclass . attributeList , attrs ) ; applyModelConfig ( Subclass , options . static || { } ) ; Subclass . registerType ( ) ; if ( options . methods ) { for ( const methodName in options . methods ) { if ( options . methods . hasOwnProperty ( methodName ) ) { ; ( < any > Subclass . prototype ) [ methodName ] = options . methods [ methodName ] ; } } } return < any > Subclass ; } id ? : string ; temp_id ? : string ; stale : boolean = false ; storeKey : string = "" ; @ nonenumerable afterSync ? : ( diff : Record < string , any > ) => any | undefined ; @ nonenumerable relationships : Record < string , JSORMBase | JSORMBase [ ] > = { } ; @ nonenumerable klass ! : typeof JSORMBase ; @ nonenumerable private _persisted : boolean = false ; @ nonenumerable private _markedForDestruction : boolean = false ; @ nonenumerable private _markedForDisassociation : boolean = false ; @ nonenumerable private _originalRelationships : Record < string , JsonapiResourceIdentifier [ ] > = { } ; @ nonenumerable private _attributes ! : ModelRecord < this > ; @ nonenumerable private _originalAttributes : ModelRecord < this > ; @ nonenumerable private __meta__ : any ; @ nonenumerable private _errors : ValidationErrors < this > = { } ; constructor ( attrs ? ) { this . _initializeAttributes ( ) ; this . assignAttributes ( attrs ) ; this . _originalAttributes = cloneDeep ( this . _attributes ) ; this . _originalRelationships = this . relationshipResourceIdentifiers ( Object . keys ( this . relationships ) ) ; } private _initializeAttributes ( ) { this . _attributes = { } ; this . _copyPrototypeDescriptors ( ) ; } private _copyPrototypeDescriptors ( ) { const attrs = this . klass . attributeList ; for ( const key in attrs ) { if ( attrs . hasOwnProperty ( key ) ) { const attr = attrs [ key ] ; Object . defineProperty ( this , key , attr . descriptor ( ) ) ; } } ; [ "errors" , "isPersisted" , "isMarkedForDestruction" , "isMarkedForDisassociation" ] . forEach ( property => { const descriptor = Object . getOwnPropertyDescriptor ( JSORMBase . prototype , property ) ; if ( descriptor ) { Object . defineProperty ( this , property , descriptor ) ; } } ) ; } isType ( jsonapiType ) { return this . klass . jsonapiType === jsonapiType ; } get isPersisted ( ) : boolean { return this . _persisted ; } set isPersisted ( val ) { this . _persisted = val ; if ( ! ! val ) this . reset ( ) ; } _onStoreChange ? : ( event : any , attrs : any ) => void ; private onStoreChange ( ) { if ( this . _onStoreChange ) return this . _onStoreChange ; this . _onStoreChange = ( event , attrs ) => { let diff = { } as any ; Object . keys ( attrs ) . forEach ( k => { let self = this as any ; let changes = this . changes ( ) as any ; if ( self [ k ] !== attrs [ k ] && ! changes [ k ] ) { diff [ k ] = [ self [ k ] , attrs [ k ] ] ; self [ k ] = attrs [ k ] ; self . _originalAttributes [ k ] = attrs [ k ] ; } } ) ; let hasDiff = Object . keys ( diff ) . length > 0 ; if ( hasDiff && typeof this . afterSync !== "undefined" ) { this . afterSync ( diff ) ; } } ; return this . _onStoreChange ; } unlisten ( ) { if ( ! this . klass . sync ) return ; if ( this . storeKey ) { EventBus . removeEventListener ( this . storeKey , this . onStoreChange ( ) ) ; } Object . keys ( this . relationships ) . forEach ( k => { let related = this . relationships [ k ] ; if ( related ) { if ( Array . isArray ( related ) ) { related . forEach ( r => r . unlisten ( ) ) ; } else { related . unlisten ( ) ; } } } ) ; } listen ( ) { if ( ! this . klass . sync ) return ; if ( ! this . _onStoreChange ) { EventBus . addEventListener ( this . storeKey , this . onStoreChange ( ) ) ; } } reset ( ) { if ( this . klass . sync ) { this . klass . store . updateOrCreate ( this ) ; this . listen ( ) ; } this . _originalAttributes = cloneDeep ( this . _attributes ) ; this . _originalRelationships = this . relationshipResourceIdentifiers ( Object . keys ( this . relationships ) ) ; } get isMarkedForDestruction ( ) : boolean { return this . _markedForDestruction ; } set isMarkedForDestruction ( val ) { this . _markedForDestruction = val ; } get isMarkedForDisassociation ( ) : boolean { return this . _markedForDisassociation ; } set isMarkedForDisassociation ( val ) { this . _markedForDisassociation = val ; } get attributes ( ) : Record < string , any > { return this . _attributes ; } set attributes ( attrs ) { this . _attributes = { } ; this . assignAttributes ( attrs ) ; } get stored ( ) { return this . klass . store . find ( this ) ; } get typedAttributes ( ) : ModelRecord < this > { return this . _attributes ; } relationship ( name ) { return this . relationships [ name ] ; } assignAttributes ( attrs ? ) { if ( ! attrs ) { return ; } for ( const key in attrs ) { if ( attrs . hasOwnProperty ( key ) ) { let attributeName = key ; attributeName = this . klass . deserializeKey ( key ) ; if ( key === "id" || this . klass . attributeList [ attributeName ] ) { ; ( < any > this ) [ attributeName ] = attrs [ key ] ; } else if ( this . klass . strictAttributes ) { throw new Error ( `Unknown attribute: ${ key }` ) ; } } } } setMeta ( metaObj ) { this . __meta__ = metaObj ; } relationshipResourceIdentifiers ( relationNames ) { return relationshipIdentifiersFor ( this , relationNames ) ; } fromJsonapi ( resource , payload , includeDirective = { } ) { return deserializeInstance ( this , resource , payload , includeDirective ) ; } get resourceIdentifier ( ) : JsonapiResourceIdentifier { if ( this . klass . jsonapiType === undefined ) { throw new Error ( "Cannot build resource identifier for class. No JSONAPI Type specified." ) ; } return { id : this . id , type : this . klass . jsonapiType } ; } get errors ( ) : ValidationErrors < this > { return this . _errors ; } set errors ( errs ) { this . _errors = errs ; } get hasError ( ) { return Object . keys ( this . _errors ) . length > 1 ; } clearErrors ( ) { this . errors = { } ; } isDirty ( relationships ? ) { const dc = new DirtyChecker ( this ) ; return dc . check ( relationships ) ; } changes ( ) { const dc = new DirtyChecker ( this ) ; return dc . dirtyAttributes ( ) ; } hasDirtyRelation ( relationName , relatedModel ) { const dc = new DirtyChecker ( this ) ; return dc . checkRelation ( relationName , relatedModel ) ; } dup ( ) { let attrs = Object . assign ( { } , this . attributes , this . relationships ) ; let cloned = new this . klass ( attrs ) as any ; cloned . id = this . id ; cloned . isPersisted = this . isPersisted ; cloned . isMarkedForDestruction = this . isMarkedForDestruction ; cloned . isMarkedForDisassociation = this . isMarkedForDisassociation ; cloned . errors = Object . assign ( { } , this . errors ) ; return cloned ; } static fetchOptions ( ) { const options = { headers : { Accept : "application/vnd.api+json" , [ "Content-Type" ] : "application/vnd.api+json" } as any } ; const jwt = this . getJWT ( ) ; if ( jwt ) { options . headers . Authorization = this . generateAuthHeader ( jwt ) ; } return options ; } static url ( id ? ) { const endpoint = this . endpoint || `/${ this . jsonapiType }` ; let base = `${ this . fullBasePath ( ) }${ endpoint }` ; if ( id ) { base = `${ base }/${ id }` ; } return base ; } static fullBasePath ( ) { return `${ this . baseUrl }${ this . apiNamespace }` ; } static get middlewareStack ( ) : MiddlewareStack { if ( this . baseClass ) { const stack = this . baseClass . _middlewareStack ; if ( this . beforeFetch || this . afterFetch ) { const before = this . beforeFetch ? [ this . beforeFetch ] : stack . beforeFilters ; const after = this . afterFetch ? [ this . afterFetch ] : stack . afterFilters ; return new MiddlewareStack ( before , after ) ; } else { return stack ; } } else { return new MiddlewareStack ( ) ; } } static set middlewareStack ( stack ) { this . _middlewareStack = stack ; } static scope < I extends typeof JSORMBase > ( this ) { return new Scope ( this ) ; } static first < I extends typeof JSORMBase > ( this ) { return this . scope ( ) . first ( ) ; } static all < I extends typeof JSORMBase > ( this ) { return this . scope ( ) . all ( ) ; } static find < I extends typeof JSORMBase > ( this , id ) { return this . scope ( ) . find ( id ) ; } static where < I extends typeof JSORMBase > ( this , clause ) { return this . scope ( ) . where ( clause ) ; } static page < I extends typeof JSORMBase > ( this , pageNum ) { return this . scope ( ) . page ( pageNum ) ; } static per < I extends typeof JSORMBase > ( this , size ) { return this . scope ( ) . per ( size ) ; } static extraParams < I extends typeof JSORMBase > ( this , clause ) { return this . scope ( ) . extraParams ( clause ) ; } static order < I extends typeof JSORMBase > ( this , clause ) { return this . scope ( ) . order ( clause ) ; } static select < I extends typeof JSORMBase > ( this , clause ) { return this . scope ( ) . select ( clause ) ; } static selectExtra < I extends typeof JSORMBase > ( this , clause ) { return this . scope ( ) . selectExtra ( clause ) ; } static stats < I extends typeof JSORMBase > ( this , clause ) { return this . scope ( ) . stats ( clause ) ; } static includes < I extends typeof JSORMBase > ( this , clause ) { return this . scope ( ) . includes ( clause ) ; } static merge < I extends typeof JSORMBase > ( this , obj ) { return this . scope ( ) . merge ( obj ) ; } static setJWT ( token ) { this . credentialStorage . setJWT ( token ) ; } static getJWT ( ) { return this . credentialStorage . getJWT ( ) ; } static get jwt ( ) : string | undefined { return this . getJWT ( ) ; } static set jwt ( token ) { this . setJWT ( token ) ; } static generateAuthHeader ( jwt ) { return `Token token="${ jwt }"` ; } static getJWTOwner ( ) { this . logger . warn ( "JSORMBase#getJWTOwner() is deprecated. Use #baseClass property instead" ) ; return this . baseClass ; } static serializeKey ( key ) { switch ( this . keyCase . server ) { case "dash" : { return dasherize ( underscore ( key ) ) ; } case "snake" : { return underscore ( key ) ; } case "camel" : { return camelize ( underscore ( key ) , false ) ; } } } static deserializeKey ( key ) { switch ( this . keyCase . client ) { case "dash" : { return dasherize ( underscore ( key ) ) ; } case "snake" : { return underscore ( key ) ; } case "camel" : { return camelize ( underscore ( key ) , false ) ; } } } async destroy ( ) { const url = this . klass . url ( this . id ) ; const verb = "delete" ; const request = new Request ( this . _middleware ( ) , this . klass . logger ) ; let response ; try { response = await request . delete ( url , this . _fetchOptions ( ) ) ; } catch ( err ) { throw err ; } let base = this . klass . baseClass as typeof JSORMBase ; base . store . destroy ( this ) ; return await this . _handleResponse ( response , ( ) => { this . isPersisted = false ; } ) ; } async save ( options = { } ) { let url = this . klass . url ( ) ; let verb = "post" ; const request = new Request ( this . _middleware ( ) , this . klass . logger ) ; const payload = new WritePayload ( this , options . with ) ; let response ; if ( this . isPersisted ) { url = this . klass . url ( this . id ) ; verb = "patch" ; } this . clearErrors ( ) ; const json = payload . asJSON ( ) ; try { response = await request [ verb ] ( url , json , this . _fetchOptions ( ) ) ; } catch ( err ) { throw err ; } return await this . _handleResponse ( response , ( ) => { this . fromJsonapi ( response . jsonPayload . data , response . jsonPayload , payload . includeDirective ) ; payload . postProcess ( ) ; } ) ; } private async _handleResponse ( response , callback ) { refreshJWT ( this . klass , response ) ; if ( response . status === 422 ) { ValidationErrorBuilder . apply ( this , response . jsonPayload ) ; return false ; } else { callback ( ) ; return true ; } } private _fetchOptions ( ) { return this . klass . fetchOptions ( ) ; } private _middleware ( ) { return this . klass . middlewareStack ; } resetRelationTracking ( includeDirective ) { this . _originalRelationships = this . relationshipResourceIdentifiers ( Object . keys ( includeDirective ) ) ; } } ; ( < any > JSORMBase . prototype ) . klass = JSORMBase ; JSORMBase . initializeCredentialStorage ( ) ; export const isModelClass = ( arg ) : arg is typeof JSORMBase => { if ( ! arg ) { return false ; } return arg . currentClass && arg . currentClass . isJSORMModel ; } ; export const isModelInstance = ( arg ) : arg is JSORMBase => { if ( ! arg ) { return false ; } return isModelClass ( arg . constructor . currentClass ) ; } ;