module import * as yup from 'yup' ; import { InvalidIdError } from '@worldsibu/convector-core-errors' ; import { BaseStorage } from '@worldsibu/convector-core-storage' ; import { Validate } from '../src/validate.decorator' ; import { getDefaults } from '../src/default.decorator' ; import { Required , ensureRequired } from '../src/required.decorator' ; import { getPropertiesValidation , getValidatedProperties } from '../src/validate.decorator' ; export type RequiredKeys < T > = { [ K in keyof T ] - ? : string extends K ? never : number extends K ? never : { } extends Pick < T , K > ? never : K ; } extends { [ _ in keyof T ] - ? : infer U ; } ? U extends keyof T ? U : never : never ; export type OptionalKeys < T > = { [ K in keyof T ] - ? : string extends K ? never : number extends K ? never : { } extends Pick < T , K > ? K : never ; } extends { [ _ in keyof T ] - ? : infer U ; } ? U extends keyof T ? U : never : never ; export declare type FlatConvectorModel < T > = { [ L in Exclude < OptionalKeys < T > , keyof ConvectorModel < any > > ] ? : T [ L ] ; } & { [ L in Exclude < RequiredKeys < T > , keyof ConvectorModel < any > > ] : T [ L ] ; } ; export interface History < T > { value : T ; txId : string ; timestamp : number ; } /**
 * This class is intended to be inherited by all the models of the application.
 *
 * It provides the underlying communication with the [[BaseStorage]].
 */ export abstract class ConvectorModel < T extends ConvectorModel < any > > { private static type = 'io.convector.model' ; public static schema < T extends ConvectorModel < any > > ( this ) { return yup . object < FlatConvectorModel < T > & { id : string ; type : string ; } > ( ) . shape ( { id : yup . string ( ) . required ( ) , type : yup . string ( ) , ... getPropertiesValidation ( this . prototype ) } as any ) ; } param this param id param type public static async getOne < T extends ConvectorModel < any > > ( this , id , type ? , storageOptions ? ) { type = type || this ; const content = await BaseStorage . current . get ( id , storageOptions ) ; const model = new type ( content ) ; if ( ( content && model ) && content . type !== new type ( '' ) . type ) { throw new Error ( `Possible ID collision, element ${ id } of type ${ content . type } is not ${ new type ( '' ) . type }` ) ; } return model ; } param this param type param args public static async query < T > ( type , ... args ) ; public static async query < T > ( this , ... args ) { let type = this ; if ( args [ 0 ] && 'type' in args [ 0 ] && args [ 0 ] . type === ConvectorModel . type ) { type = args . shift ( ) ; } const content = await BaseStorage . current . query ( ... args ) ; return Array . isArray ( content ) ? content . map ( c => new type ( c ) ) : new type ( content ) ; } param this param type public static async getAll < T extends ConvectorModel < any > > ( this , type ? ) { type = type || new this ( '' ) . type ; return await ConvectorModel . query ( this , { selector : { type } } ) as T [ ] ; } /**
     * This field is [[Required]] and [[Validate]]d using a string schema
     *
     * Represents the key used to store the model in the blockchain
     */ @ Required ( ) @ Validate ( yup . string ( ) ) public id : string ; /**
     * This field must be provided by the extender class.
     *
     * It should be [[Required]] and [[ReadOnly]]
     *
     * We normally use a domain name patter for type names, i.e.: `io.worldsibu.example.user`
     */ public abstract readonly type : string ; /**
     * The constructor can be called in multiple ways.
     *
     * - As an empty box where you instantiate one and start adding data
     * - As a data fetcher, providing the ID and using [[ConvectorModel.fetch]]
     * - As a formatter, you just pass an object of any shape into the constructor
     *    and it will trim the remaining content and leave what's important
     */ constructor ( ) ; constructor ( id ) ; constructor ( content ) ; constructor ( content ? ) { if ( ! content ) { return ; } if ( typeof content === 'string' ) { this . id = content ; return ; } this . assign ( content ) ; } /**
     * Given one model loaded into the instance,
     * update its content with the object passed in the param.
     *
     * Store the result after the update occurs.
     */ public async update ( content ) { this . assign ( content ) ; await this . save ( ) ; } /**
     * Invokes the [[BaseStorage.get]] method to retrieve the model from storage.
     */ public async fetch ( storageOptions ? ) { const content = await BaseStorage . current . get ( this . id , storageOptions ) as ConvectorModel < T > ; if ( content . type !== this . type ) { throw new Error ( `Possible ID collision, element ${ this . id } of type ${ content . type } is not ${ this . type }` ) ; } this . assign ( content as T ) ; } public async history ( ) { const history = await BaseStorage . current . history ( this . id ) ; return history . map ( item => ( { txId : item . tx_id , value : new ( this . constructor as new ( content ) => T ) ( item . value ) , timestamp : item . timestamp } ) ) ; } param storageOptions public async save ( storageOptions ? ) { this . assign ( getDefaults ( this ) , true ) ; if ( ! ensureRequired ( this ) ) { if ( ! this . id ) { throw new Error ( `Model ${ this . type } is missing the 'id' property \n${ JSON . stringify ( this ) }` ) ; } else { throw new Error ( `Model ${ this . type } is not complete\n${ JSON . stringify ( this ) }.
        Check your model definition for more details.` ) ; } } InvalidIdError . test ( this . id ) ; await BaseStorage . current . set ( this . id , this , storageOptions ) ; } /**
     * Make a copy of this model
     */ public clone ( ) { return new ( this . constructor as new ( content ) => T ) ( Object . assign ( { } , this ) ) ; } param skipEmpty public toJSON ( skipEmpty = false ) { let protos = [ ] ; let children = this ; do { children = Object . getPrototypeOf ( children ) ; protos . push ( children ) ; } while ( children [ '__proto__' ] . constructor . name !== ConvectorModel . name ) ; const descriptors = protos . reduce ( ( result , proto ) => [ ... result , ... Object . keys ( proto ) . map ( key => [ key , Object . getOwnPropertyDescriptor ( proto , key ) ] ) ] , [ ] ) ; const base = Object . keys ( this ) . concat ( 'id' ) . filter ( k => ! k . startsWith ( '_' ) ) . filter ( k => ! skipEmpty || ! ( this [ k ] === undefined || this [ k ] === null ) ) . reduce ( ( result , key ) => ( { ... result , [ key ] : this [ key ] } ) , { } ) ; return descriptors . reduce ( ( result , [ key , desc ] ) => { const hasGetter = desc && typeof desc . get === 'function' ; if ( hasGetter ) { result [ key ] = desc . get . call ( this ) ; } if ( skipEmpty && ( result [ key ] === undefined || result [ key ] === null ) ) { delete result [ key ] ; } if ( result [ key ] instanceof ConvectorModel ) { result [ key ] = result [ key ] . toJSON ( true ) ; } return result ; } , base ) ; } /**
     * Delete a model and persist the changes into the blockchain.
     *
     * Notice that there's no such a concept as **delete** in the blockchain,
     * so what this does is to remove all the reachable references to the model.
     */ public async delete ( storageOptions ? ) { await BaseStorage . current . delete ( this . id , storageOptions ) ; } hidden param defaults private assign ( content , defaults = false ) { const validated = [ 'id' , 'type' , ... getValidatedProperties ( this ) ] ; const filteredContent = Object . keys ( content ) . map ( key => key . replace ( /^_/ , '' ) ) . filter ( key => validated . indexOf ( key ) >= 0 ) . reduce ( ( result , key ) => ( { ... result , [ key ] : content [ key ] !== undefined ? content [ key ] : content [ '_' + key ] } ) , { } ) ; const afterDefaults = defaults ? this . toJSON ( true ) : { } ; Object . assign ( this , filteredContent , afterDefaults ) ; } }