import { CRUDEvents , LocalStorage } from "./storage" ; import { StoreChangeEvent } from "./storage" ; import { ModelSchema } from "./ModelSchema" ; import { PushStream , ObservablePushStream } from "./utils/PushStream" ; import { Filter } from "./filters" ; import { ModelReplicationConfig } from "./replication/api/ReplicationConfig" ; import invariant from "tiny-invariant" ; import { ModelChangeReplication } from "./replication/mutations/MutationsQueue" ; import { v4 as uuidv4 } from "uuid" ; import { createLogger } from "./utils/logger" ; import { FetchReplicator } from "./replication/fetch/FetchReplicator" ; const logger = createLogger ( "model" ) ; /**
 * Options that describe model field
 */ export interface FieldOptions { /** GraphQL type */ type : string ; /** GraphQL key */ key : string ; format ? : { } ; } const CLIENT_ID_PREFIX = "storeclient." ; /**
 * Defines the properties expected in the Fields object for a model
 */ export type Fields < T > = { [ P in keyof T ] : FieldOptions ; } ; /**
 * Model Config options
 */ export interface ModelConfig < T = unknown > { /**
     * Model name
     */ name : string ; /**
     * Model store name, defualts to `user_${name}`
     */ storeName ? : string ; /**
     * Model fields
     */ fields : Fields < T > ; } /**
 * Provides CRUD capabilities for a model
 */ export class Model < T = unknown > { public schema : ModelSchema < T > ; public replicationConfig : ModelReplicationConfig | undefined ; public replication ? : ModelChangeReplication ; public changeEventStream : PushStream < StoreChangeEvent > ; private storage : LocalStorage ; private replicator ? : FetchReplicator ; constructor ( schema , storage , replicationConfig ? ) { this . changeEventStream = new ObservablePushStream ( ) ; this . schema = schema ; this . storage = storage ; this . replicationConfig = replicationConfig ; this . storage . addStore ( this . schema ) ; } public getFields ( ) { return this . schema . getFields ( ) ; } public getName ( ) { return this . schema . getName ( ) ; } public getStoreName ( ) { return this . schema . getStoreName ( ) ; } public getSchema ( ) { return this . schema ; } public query ( filter ? ) { if ( ! filter ) { return this . storage . query ( this . schema . getStoreName ( ) ) ; } return this . storage . query ( this . schema . getStoreName ( ) , filter ) ; } public queryById ( id ) { return this . storage . queryById ( this . schema . getStoreName ( ) , this . schema . getPrimaryKey ( ) , id ) ; } public async save ( input ) { input = this . addPrimaryKeyIfNeeded ( input ) ; try { const data = await this . storage . save ( this . schema . getStoreName ( ) , input ) ; await this . replication ?. saveChangeForReplication ( this , data , CRUDEvents . ADD , this . storage ) ; const event = { eventType : CRUDEvents . ADD , data : [ data ] } ; this . changeEventStream . publish ( event ) ; return data ; } catch ( error ) { throw error ; } } public setReplicator ( replicator ) { this . replicator = replicator ; } public getReplicator ( ) { return this . replicator ; } param filter public startReplication ( filter ? ) { if ( filter ) { this . replicator ?. applyFilter ( filter ) ; } this . replicator ?. startReplication ( ) ; } param filter public applyFilter ( filter ) { this . replicator ?. applyFilter ( { filter } ) ; } param input public async saveOrUpdate ( input ) { input = this . addPrimaryKeyIfNeeded ( input ) ; try { const data = await this . storage . saveOrUpdate ( this . schema . getStoreName ( ) , this . schema . getPrimaryKey ( ) , input ) ; await this . replication ?. saveChangeForReplication ( this , data , CRUDEvents . ADD , this . storage ) ; const event = { eventType : CRUDEvents . ADD , data : [ data ] } ; this . changeEventStream . publish ( event ) ; return data ; } catch ( error ) { throw error ; } } param input param filter public async update ( input , filter ? ) { invariant ( filter , "filter needs to be provided for update" ) ; try { const data = await this . storage . update ( this . schema . getStoreName ( ) , input , filter ) ; await this . replication ?. saveChangeForReplication ( this , data , CRUDEvents . UPDATE , this . storage ) ; const event = { eventType : CRUDEvents . UPDATE , data } ; this . changeEventStream . publish ( event ) ; return data ; } catch ( error ) { throw error ; } } param input public async updateById ( input ) { const primaryKey = this . schema . getPrimaryKey ( ) ; invariant ( ( input as any ) [ primaryKey ] , "Missing primary key for update" ) ; try { const data = await this . storage . updateById ( this . schema . getStoreName ( ) , this . schema . getPrimaryKey ( ) , input ) ; await this . replication ?. saveChangeForReplication ( this , data , CRUDEvents . UPDATE , this . storage ) ; const event = { eventType : CRUDEvents . UPDATE , data : [ data ] } ; this . changeEventStream . publish ( event ) ; return data ; } catch ( error ) { throw error ; } } param filter public async remove ( filter ) { invariant ( filter , "filter needs to be provided for deletion" ) ; try { const primaryKey = this . schema . getPrimaryKey ( ) ; const data = await this . storage . remove ( this . schema . getStoreName ( ) , primaryKey , filter ) ; await this . replication ?. saveChangeForReplication ( this , data , CRUDEvents . DELETE , this . storage ) ; const event = { eventType : CRUDEvents . DELETE , data } ; this . changeEventStream . publish ( event ) ; return data ; } catch ( error ) { throw error ; } } param input public async removeById ( input ) { const primaryKey = this . schema . getPrimaryKey ( ) ; invariant ( ( input as any ) [ primaryKey ] , "Missing primary key for delete" ) ; try { const data = await this . storage . removeById ( this . schema . getStoreName ( ) , this . schema . getPrimaryKey ( ) , input ) ; await this . replication ?. saveChangeForReplication ( this , data , CRUDEvents . DELETE , this . storage ) ; const event = { eventType : CRUDEvents . DELETE , data : [ data ] } ; this . changeEventStream . publish ( event ) ; return data ; } catch ( error ) { throw error ; } } param eventType param listener public subscribe ( listener , eventTypes ? ) { return this . changeEventStream . subscribe ( ( event ) => { listener ( event ) ; } , ( event ) => { if ( eventTypes ) { return eventTypes . includes ( event . eventType ) ; } return true ; } ) ; } param result public async processDeltaChanges ( dataResult , type ? ) { if ( ! dataResult || dataResult . length === 0 ) { logger ( "Delta processing: No changes" ) ; return ; } const db = this . storage ; const store = this . schema . getStoreName ( ) ; const primaryKey = this . schema . getPrimaryKey ( ) ; for ( const item of dataResult ) { delete item . __typename ; let data ; let eventType ; if ( item . _deleted ) { logger ( "Delta processing: deleting item" ) ; data = await await db . removeById ( store , primaryKey , item ) ; eventType = CRUDEvents . DELETE ; } else { const exist = await db . queryById ( store , primaryKey , item [ primaryKey ] ) ; if ( exist ) { logger ( "Delta processing: updating item" ) ; eventType = CRUDEvents . UPDATE ; data = await db . updateById ( this . schema . getStoreName ( ) , primaryKey , item ) ; } else { logger ( "Delta processing: adding item" ) ; eventType = CRUDEvents . ADD ; data = await db . save ( this . schema . getStoreName ( ) , item ) ; } if ( ! data ) { logger ( "Failed to update items in database" ) ; return ; } } const event = { eventType , data : [ data ] } ; this . changeEventStream . publish ( event ) ; } } param result public async processSubscriptionChanges ( dataResult , type ) { if ( ! dataResult ) { logger ( "Subscription returned no result. If you see this something is wrong." ) ; return ; } try { delete dataResult . __typename ; logger ( "Retrieved object from subscription" ) ; const store = this . schema . getStoreName ( ) ; const primaryKey = this . schema . getPrimaryKey ( ) ; if ( type === CRUDEvents . ADD ) { await this . storage . save ( this . schema . getStoreName ( ) , dataResult ) ; } if ( type === CRUDEvents . UPDATE ) { await this . storage . updateById ( this . schema . getStoreName ( ) , primaryKey , dataResult ) ; } if ( type === CRUDEvents . DELETE ) { await await this . storage . removeById ( store , primaryKey , dataResult ) ; } const event = { eventType : type , data : [ dataResult ] } ; this . changeEventStream . publish ( event ) ; } catch ( error ) { logger ( "Error when processing subscription" + JSON . stringify ( error ) ) ; } } /**
     * Checks if model has client side id.
     * Usually this means that model was not replicated and id from the server was not assigned.
     */ public hasClientID ( ) { return this . schema . getPrimaryKey ( ) . startsWith ( CLIENT_ID_PREFIX ) ; } private addPrimaryKeyIfNeeded ( input ) { const primaryKey = this . schema . getPrimaryKey ( ) ; if ( ! input [ primaryKey ] ) { input [ primaryKey ] = CLIENT_ID_PREFIX + uuidv4 ( ) ; } return input ; } }