import { createHash } from 'crypto' ; import * as Debug from 'debug' ; import * as _ from 'lodash' ; import * as redis from 'redis' ; import * as traverse from 'traverse' ; import { v4 as uuid } from 'uuid' ; import { INohmPrefixes , NohmClass } from '.' ; import { LinkError } from './errors/LinkError' ; import { ValidationError } from './errors/ValidationError' ; import * as messageComposers from './eventComposers' ; import { callbackError , checkEqual } from './helpers' ; import { idGenerators } from './idGenerators' ; import { IChangeEventPayload , IDefaultEventPayload , IDictionary , ILinkOptions , ILinkSaveResult , IModelOptions , IModelPropertyDefinition , IModelPropertyDefinitions , IProperty , IPropertyDiff , IRelationChange , IRelationChangeEventPayload , ISaveOptions , ISearchOption , ISortOptions , IStructuredSearch , IUnlinkKeyMapItem , IValidationObject , IValidationResult , TLinkCallback , TValidationDefinition , ILinkOptionsWithName , } from './model.header' ; import { del , exec , exists , get , hgetall , mset , sadd , scard , set , setnx , sinter , sismember , smembers , } from './typed-redis-helper' ; import { validators } from './validators' ; export { IDictionary , ILinkOptions , IModelPropertyDefinition , IModelPropertyDefinitions , IModelOptions , ISortOptions , TLinkCallback , } ; export { NohmModel } ; const debug = Debug ( 'nohm:model' ) ; const debugPubSub = Debug ( 'nohm:pubSub' ) ; const indexNumberTypes = [ 'integer' , 'float' , 'timestamp' ] ; export type TAllowedEventNames = 'create' | 'save' | 'update' | 'remove' | 'link' | 'unlink' ; const eventActions = [ 'create' , 'update' , 'save' , 'remove' , 'unlink' , 'link' , ] ; const metaUpdating = [ ] ; type { redis . RedisClient } name memberof /**
 * Validation errors that were set during the last call to  NohmModel . (so also during save())
 *
 * The type is an object with property names as keys and then an array with validation
 * names of the validations that failed
 *
 *  type { Object . < string , Array < string > > } name memberof type { string } name memberof abstract class abstract class NohmModel < TProps extends IDictionary = IDictionary > { type { redis . RedisClient } public client : redis . RedisClient ; public errors : { [ key in keyof TProps ] : Array < string > ; } ; public meta : { inDb : boolean ; properties : IModelPropertyDefinitions ; version : string ; } ; public readonly modelName : string ; protected properties : Map < keyof TProps , IProperty > ; protected options : IModelOptions ; protected publish : null | boolean = null ; protected static readonly definitions : IModelPropertyDefinitions = { } ; protected abstract nohmClass : NohmClass ; private _id : null | string ; private _isLoaded : boolean ; private _isDirty : boolean ; private allPropertiesCache : { [ key in keyof TProps ] : any ; } & { id : any ; } ; private inDb : boolean ; private tmpUniqueKeys : Array < string > ; private relationChanges : Array < IRelationChange > ; constructor ( ) { this . _initOptions ( ) ; if ( ! this . client ) { throw new Error ( 'No redis client defined before initializing models.' ) ; } if ( ! this . meta . inDb ) { this . updateMeta ( this . options . metaCallback ) ; } this . properties = new Map ( ) ; this . allPropertiesCache = { id : null , } as any ; this . errors = { } as any ; const definitions = this . getDefinitions ( ) ; const propKeys = Object . keys ( definitions ) ; debug ( `Constructing model with these properties: %j` , propKeys ) ; propKeys . forEach ( ( key ) => { const definition = this . getDefinitions ( ) [ key ] ; let defaultValue = definition . defaultValue || 0 ; if ( typeof defaultValue === 'function' ) { defaultValue = defaultValue ( ) ; } this . properties . set ( key , { __numericIndex : false , __oldValue : null , __updated : false , value : undefined , } ) ; if ( typeof definition . type === 'function' ) { this . setProperty ( key , defaultValue , true ) ; } else { this . property ( key , defaultValue ) ; } this . __resetProp ( key ) ; this . errors [ key ] = [ ] ; } ) ; if ( this . options . methods ) { this . addMethods ( this . options . methods ) ; } if ( this . options . publish ) { this . publish = this . options . publish ; } if ( ! this . options . idGenerator ) { this . options . idGenerator = 'default' ; } this . relationChanges = [ ] ; this . tmpUniqueKeys = [ ] ; this . _id = null ; this . _isLoaded = false ; this . _isDirty = false ; this . inDb = false ; } private __resetProp ( property ) { const tmp = this . getProperty ( property ) ; tmp . __updated = false ; tmp . __oldValue = tmp . value ; type genericFunction = ( ... args : Array < any > ) => any ; let type = this . getDefinitions ( ) [ property ] . type ; if ( typeof type !== 'string' ) { type = '__notIndexed__' ; } tmp . __numericIndex = indexNumberTypes . indexOf ( type ) > - 1 ; } private addMethods ( methods ? ) { if ( methods ) { _ . each ( methods , ( method , name ) => { if ( typeof ( this as any ) [ name ] !== 'undefined' ) { const errorForStack = new Error ( 'Deprecation warning' ) ; setTimeout ( ( ) => { console . warn ( '\x1b[31m%s\x1b[0m' , `WARNING: Overwriting existing property/method '${ name }' in '${ this . modelName }' because of method definition.` ) ; console . warn ( '\x1b[31m%s\x1b[0m' , `DEPRECATED: Overwriting built-in methods is deprecated. Please migrate them to a different name. Here's a stack to help identify the problem:` , errorForStack . stack ) ; } , 1 ) ; ( this as any ) [ '_super_' + name ] = ( this as any ) [ name ] . bind ( this ) ; } debug ( `Adding method to model: %s` , method ) ; ( this as any ) [ name ] = method ; } ) ; } } private updateMeta ( callback ? ) { if ( metaUpdating . includes ( this . meta . version ) ) { return ; } process . nextTick ( async ( ) => { if ( metaUpdating . includes ( this . meta . version ) ) { return ; } metaUpdating . push ( this . meta . version ) ; const versionKey = this . rawPrefix ( ) . meta . version + this . modelName ; const idGeneratorKey = this . rawPrefix ( ) . meta . idGenerator + this . modelName ; const propertiesKey = this . rawPrefix ( ) . meta . properties + this . modelName ; try { const dbVersion = await get ( this . client , versionKey ) ; if ( this . meta . version !== dbVersion ) { const properties = traverse ( this . meta . properties ) . map ( ( x ) => { if ( typeof x === 'function' ) { return String ( x ) ; } else { return x ; } } ) ; const generator = this . options . idGenerator || 'default' ; debug ( `Setting meta for model '%s' with version '%s' and idGenerator '%s' to %j.` , this . modelName , this . meta . version , generator , properties ) ; await Promise . all ( [ set ( this . client , idGeneratorKey , generator . toString ( ) ) , set ( this . client , propertiesKey , JSON . stringify ( properties ) ) , set ( this . client , versionKey , this . meta . version ) , ] ) ; } this . meta . inDb = true ; const updatingIndex = metaUpdating . indexOf ( this . meta . version ) ; if ( updatingIndex !== - 1 ) { metaUpdating . splice ( updatingIndex , 1 ) ; } if ( typeof callback === 'function' ) { callback ( null , this . meta . version ) ; } } catch ( err ) { this . nohmClass . logError ( err ) ; if ( typeof callback === 'function' ) { callback ( err , this . meta . version ) ; } } } ) ; } protected protected abstract _initOptions ( ) ; protected protected abstract prefix ( prefix ) ; protected protected abstract rawPrefix ( ) ; protected generateMetaVersion ( ) { const hash = createHash ( 'sha1' ) ; const idGenerator = this . options . idGenerator || 'default' ; hash . update ( '' + JSON . stringify ( this . getDefinitions ( ) ) ) ; hash . update ( '' + JSON . stringify ( this . modelName ) ) ; hash . update ( idGenerator . toString ( ) ) ; return hash . digest ( 'hex' ) ; } public p ( keyOrValues , value ? ) { console . warn ( '\x1b[31m%s\x1b[0m' , 'DEPRECATED: Usage of NohmModel.p() is deprecated, use NohmModel.property() instead.' ) ; return this . property ( keyOrValues , value ) ; } public prop ( keyOrValues , value ? ) { console . warn ( '\x1b[31m%s\x1b[0m' , 'DEPRECATED: Usage of NohmModel.prop() is deprecated, use NohmModel.property() instead.' ) ; return this . property ( keyOrValues , value ) ; } private param { * } key returns { string } private isPropertyKey ( key ) { return typeof key === 'string' ; } public property < TProp extends keyof TProps > ( key ) ; public property < TProp extends keyof TProps > ( key , value ) ; public property ( valuesObject ) ; param { ( string | PropertyObject ) } keyOrValues param { * } value returns { ( any ) } public property < TProp extends keyof TProps > ( keyOrValues , value ? ) { if ( ! this . isPropertyKey ( keyOrValues ) ) { const obj = { } ; Object . keys ( keyOrValues ) . forEach ( ( key ) => { obj [ key ] = this . property ( key , keyOrValues [ key ] ) ; } ) ; return obj ; } if ( typeof value !== 'undefined' ) { debug ( `Setting property '%s' to value %o` , keyOrValues , value ) ; this . setProperty ( keyOrValues , value ) ; this . allPropertiesCache [ keyOrValues ] = this . property ( keyOrValues ) ; } const prop = this . getProperty ( keyOrValues ) ; let returnValue = prop . value ; if ( this . getDefinitions ( ) [ keyOrValues ] . type === 'json' ) { returnValue = JSON . parse ( returnValue ) ; } debug ( `Returning property '%s' with value %o` , keyOrValues , returnValue ) ; return returnValue ; } private getProperty ( key ) { const prop = this . properties . get ( key ) ; if ( ! prop ) { throw new Error ( `Invalid property key '${ key }'.` ) ; } return prop ; } private setProperty ( key , value , skipCast = false ) { const prop = this . getProperty ( key ) ; if ( prop . value !== value ) { if ( skipCast ) { prop . value = value ; this . allPropertiesCache [ key ] = value ; } else { prop . value = this . castProperty ( key , prop , value ) ; } prop . __updated = prop . value !== prop . __oldValue ; this . properties . set ( key , prop ) ; } } private castProperty ( key , prop , newValue ) { const type = this . getDefinitions ( ) [ key ] . type ; if ( typeof type === 'undefined' ) { return newValue ; } debug ( `Casting property '%s' with type %o` , key , type ) ; if ( typeof type === 'function' ) { return type . call ( this , String ( newValue ) , key , String ( prop . __oldValue ) ) ; } switch ( type . toLowerCase ( ) ) { case 'boolean' : case 'bool' : return newValue === 'false' ? false : ! ! newValue ; case 'string' : return ( ! ( newValue instanceof String ) || newValue . toString ( ) === '' ) && typeof newValue !== 'string' ? '' : newValue ; case 'integer' : case 'int' : return isNaN ( parseInt ( newValue , 10 ) ) ? 0 : parseInt ( newValue , 10 ) ; case 'float' : case 'number' : return isNaN ( parseFloat ( newValue ) ) ? 0 : parseFloat ( newValue ) ; case 'date' : case 'time' : case 'timestamp' : let castTimestamp ; if ( isNaN ( newValue ) && typeof newValue === 'string' ) { let timezoneOffset ; if ( newValue . match ( /Z$/ ) ) { timezoneOffset = 0 ; } else { const matches = newValue . match ( /(\+|\-)([\d]{1,2})\:([\d]{2})$/ ) ; if ( matches ) { const hours = parseInt ( matches [ 2 ] , 10 ) ; const minutes = parseInt ( matches [ 3 ] , 10 ) ; timezoneOffset = hours * 60 + minutes ; if ( matches [ 1 ] === '-' ) { timezoneOffset *= - 1 ; } newValue = newValue . substring ( 0 , newValue . length - matches [ 0 ] . length ) + 'Z' ; } else { timezoneOffset = new Date ( newValue ) . getTimezoneOffset ( ) ; } } castTimestamp = new Date ( newValue ) . getTime ( ) - timezoneOffset * 60 * 1000 ; } else { castTimestamp = parseInt ( newValue , 10 ) ; } return castTimestamp . toString ( ) ; case 'json' : if ( typeof newValue === 'object' ) { return JSON . stringify ( newValue ) ; } else { try { JSON . parse ( newValue ) ; return newValue ; } catch ( e ) { return JSON . stringify ( newValue ) ; } } default : return newValue ; } } example public propertyDiff ( key ? ) { if ( key ) { return [ this . onePropertyDiff ( key ) ] ; } else { const diffResult = [ ] ; for ( const [ iterationKey ] of this . properties ) { const diff = this . onePropertyDiff ( iterationKey ) ; if ( diff ) { diffResult . push ( diff ) ; } } return diffResult ; } } private onePropertyDiff ( key ) { const prop = this . getProperty ( key ) ; if ( prop . __updated ) { return { after : prop . value , before : prop . __oldValue , key , } ; } } param { string } key public propertyReset ( key ? ) { if ( key && ! this . properties . has ( key ) ) { throw new Error ( 'Invalid key specified for propertyReset' ) ; } debug ( `Resetting property '%s' (all if empty).` , key ) ; this . properties . forEach ( ( prop , innerKey ) => { if ( ! key || innerKey === key ) { prop . __updated = false ; prop . value = prop . __oldValue ; this . properties . set ( innerKey , prop ) ; this . allPropertiesCache [ innerKey ] = prop . __oldValue ; } } ) ; } /**
     *  Get all properties with values either as an array or as json (param true).
     */ public allProperties ( ) { return Object . assign ( { } , this . allPropertiesCache ) ; } param { ISaveOptions } options returns { Promise < void > } public async save ( options ? ) { callbackError ( ... arguments ) ; options = { silent : false , skip_validation_and_unique_indexes : false , ... options , } ; let action = 'update' ; if ( ! this . id ) { action = 'create' ; this . id = uuid ( ) ; } debug ( `Saving instance '%s.%s' with action '%s'.` , this . modelName , this . id , action ) ; let isValid = true ; if ( options . skip_validation_and_unique_indexes === false ) { isValid = await this . validate ( undefined , true ) ; if ( ! isValid ) { if ( action === 'create' ) { this . id = null ; } throw new ValidationError ( this . errors , this . modelName ) ; } } let numIdExisting = 0 ; if ( action !== 'create' ) { numIdExisting = await sismember ( this . client , this . prefix ( 'idsets' ) , this . id ) ; } if ( action === 'create' && numIdExisting === 0 ) { debug ( `Creating new instance '%s.%s' because action was '%s' and numIdExisting was %d.` , this . modelName , this . id , action , numIdExisting ) ; await this . create ( ) ; this . inDb = false ; } else { this . inDb = true ; } await this . update ( options ) ; this . inDb = true ; this . _isDirty = false ; this . _isLoaded = true ; } private async create ( ) { const id = await this . generateId ( ) ; await sadd ( this . client , this . prefix ( 'idsets' ) , id ) ; await this . setUniqueIds ( id ) ; this . id = id ; } private async generateId ( ) { let id = null ; let generator = this . options . idGenerator ; if ( typeof generator === 'function' ) { id = await generator . call ( this ) ; } else { if ( ! generator || ! idGenerators [ generator ] ) { generator = 'default' ; } id = await idGenerators [ generator ] . call ( idGenerators , this . client , this . prefix ( 'incrementalIds' ) ) ; } if ( typeof id !== 'string' ) { id = String ( id ) ; } if ( id . includes ( ':' ) ) { throw new Error ( 'Nohm IDs cannot contain the character ":". Please change your idGenerator!' ) ; } return id ; } /**
     * Sets the unique ids of all unique property values in this instance to the given id.
     * Warning: Only use this during create() when overwriting temporary ids!
     */ private async setUniqueIds ( id ) { const mSetArguments = [ ] ; for ( const [ key , prop ] of this . properties ) { const isUnique = ! ! this . getDefinitions ( ) [ key ] . unique ; const isEmptyString = prop . value === '' ; const isDirty = prop . __updated || ! this . inDb ; if ( isUnique && ! isEmptyString && isDirty ) { let value = this . property ( key ) ; if ( this . getDefinitions ( ) [ key ] . type === 'string' ) { value = value . toLowerCase ( ) ; } const prefix = this . prefix ( 'unique' ) ; mSetArguments . push ( `${ prefix }:${ key }:${ value }` , id ) ; } } if ( mSetArguments . length !== 0 ) { debug ( `Setting all unique indices of model '%s.%s' to new id '%s'.` , this . modelName , this . id , id ) ; return mset ( this . client , mSetArguments ) ; } } private async update ( options ) { if ( ! this . id ) { throw new Error ( 'Update was called without having an id set.' ) ; } const hmSetArguments = [ ] ; const client = this . client . multi ( ) ; const isCreate = ! this . inDb ; hmSetArguments . push ( `${ this . prefix ( 'hash' ) }:${ this . id }` ) ; for ( const [ key , prop ] of this . properties ) { if ( isCreate || prop . __updated ) { hmSetArguments . push ( key , prop . value ) ; } } if ( hmSetArguments . length > 1 ) { hmSetArguments . push ( '__meta_version' , this . meta . version ) ; client . hmset . apply ( client , hmSetArguments ) ; } await this . setIndices ( client ) ; await exec ( client ) ; const linkResults = await this . storeLinks ( options ) ; this . relationChanges = [ ] ; const linkFailures = linkResults . filter ( ( linkResult ) => ! linkResult . success ) ; if ( linkFailures . length > 0 ) { throw new LinkError ( linkFailures ) ; } let diff ; if ( this . getPublish ( ) ) { diff = this . propertyDiff ( ) ; } for ( const [ key ] of this . properties ) { this . __resetProp ( key ) ; } if ( ! options . silent ) { if ( isCreate ) { this . fireEvent ( 'create' ) ; } else { this . fireEvent ( 'update' , diff ) ; } this . fireEvent ( 'save' , diff ) ; } return linkResults ; } private async storeLinks ( options ) { const changeFns = this . relationChanges . map ( ( change ) => { return async ( ) => { change . options . silent = options . silent ; let returnArray = [ ] ; const saveResult = { child : change . object , error : null , parent : this , success : true , } ; try { if ( ! change . object . id ) { debug ( `Saving %sed '%s' instance from '%s.%s' with relation '%s' because it had no id.` , change . action , change . object . modelName , this . modelName , this . id , change . options . name ) ; await change . object . save ( options ) ; } await this . saveLinkRedis ( change ) ; try { if ( typeof change . callback === 'function' ) { change . callback . call ( this , change . action , this . modelName , change . options . name , change . object ) ; } } catch ( e ) { } } catch ( err ) { const isSubLinkError = err instanceof LinkError ; if ( ! isSubLinkError && typeof change . options . error === 'function' ) { try { change . options . error ( err , change . object ) ; } catch ( e ) { } } if ( isSubLinkError ) { returnArray = returnArray . concat ( err . errors ) ; } else { saveResult . success = false ; saveResult . error = err ; } } returnArray . push ( saveResult ) ; return returnArray ; } ; } ) ; let saveResults = [ ] ; for ( const [ , fn ] of changeFns . entries ( ) ) { saveResults = saveResults . concat ( await fn ( ) ) ; } return saveResults ; } private getRelationKey ( otherName , relationName ) { return `${ this . prefix ( 'relations' ) }:${ relationName }:${ otherName }:${ this . id }` ; } private async saveLinkRedis ( change ) { const foreignName = `${ change . options . name }Foreign` ; const command = change . action === 'link' ? 'sadd' : 'srem' ; const relationKeyPrefix = this . rawPrefix ( ) . relationKeys ; const multi = this . client . multi ( ) ; const toKey = this . getRelationKey ( change . object . modelName , change . options . name ) ; multi [ command ] ( `${ relationKeyPrefix }${ this . modelName }:${ this . id }` , toKey ) ; multi [ command ] ( toKey , change . object . stringId ( ) ) ; const fromKey = change . object . getRelationKey ( this . modelName , foreignName ) ; multi [ command ] ( `${ relationKeyPrefix }${ change . object . modelName }:${ change . object . id }` , fromKey ) ; multi [ command ] ( fromKey , this . stringId ( ) ) ; try { debug ( `Linking in redis.` , this . modelName , change . options . name , command ) ; await exec ( multi ) ; if ( ! change . options . silent ) { this . fireEvent ( change . action , change . object , change . options . name ) ; } } catch ( err ) { if ( change . options . error ) { change . options . error ( err , change . object ) ; } throw err ; } } private async setIndices ( multi ) { let oldValues = { } ; try { if ( this . inDb ) { oldValues = await this . getHashAll ( this . id ) ; } } catch ( e ) { if ( e . message !== 'not found' ) { throw e ; } } for ( const [ key , prop ] of this . properties ) { const isUnique = ! ! this . getDefinitions ( ) [ key ] . unique ; const isIndex = ! ! this . getDefinitions ( ) [ key ] . index ; const isInDb = oldValues [ key ] !== undefined ; const isDirty = prop . value !== oldValues [ key ] ; const oldValue = oldValues [ key ] ; if ( isUnique && isDirty && isInDb ) { let oldUniqueValue = oldValue ; if ( this . getDefinitions ( ) [ key ] . type === 'string' ) { oldUniqueValue = oldUniqueValue . toLowerCase ( ) ; } debug ( `Removing old unique '%s' from '%s.%s' because propUpdated: %o && this.inDb %o.` , key , this . modelName , this . id , prop . __updated , this . inDb ) ; multi . del ( `${ this . prefix ( 'unique' ) }:${ key }:${ oldUniqueValue }` , this . nohmClass . logError ) ; } if ( isIndex && isDirty ) { if ( prop . __numericIndex ) { debug ( `Adding numeric index '%s' to '%s.%s'.` , key , this . modelName , this . id , prop . __updated ) ; const scoredPrefix = this . prefix ( 'scoredindex' ) ; multi . zadd ( `${ scoredPrefix }:${ key }` , prop . value , this . stringId ( ) , this . nohmClass . logError ) ; } debug ( `Adding index '%s' to '%s.%s'; isInDb: '%s'; newValue: '%s'; oldValue: '%s'.` , key , this . modelName , this . stringId ( ) , isInDb , prop . value , oldValue ) ; const prefix = this . prefix ( 'index' ) ; if ( isInDb ) { multi . srem ( `${ prefix }:${ key }:${ oldValue }` , this . stringId ( ) , this . nohmClass . logError ) ; } multi . sadd ( `${ prefix }:${ key }:${ prop . value }` , this . stringId ( ) , this . nohmClass . logError ) ; } } } public valid ( property ? , setDirectly = false ) { console . warn ( '\x1b[31m%s\x1b[0m' , 'DEPRECATED: Usage of NohmModel.valid() is deprecated, use NohmModel.validate() instead.' ) ; return this . validate ( property , setDirectly ) ; } /**
     * Check if one or all properties are valid and optionally set the unique indices immediately.
     * If a property is invalid the  NohmModel object will be populated with error messages.
     *
     *  param { string } property param { boolean } setDirectly returns { Promise < boolean > } public async validate ( property ? , setDirectly = false ) { callbackError ( ... arguments ) ; const nonUniqueValidations = [ ] ; for ( const [ key , prop ] of this . properties ) { if ( ! property || property === key ) { this . errors [ key ] = [ ] ; nonUniqueValidations . push ( this . validateProperty ( key , prop ) ) ; } } let validationResults = await Promise . all < IValidationResult > ( nonUniqueValidations ) ; let valid = validationResults . some ( ( result ) => result . valid ) ; if ( ! valid ) { setDirectly = false ; } const uniqueValidations = [ ] ; for ( const [ key , prop ] of this . properties ) { if ( ! property || property === key ) { uniqueValidations . push ( this . checkUniques ( setDirectly , key , prop ) ) ; } } const uniqueValidationResults = await Promise . all < IValidationResult > ( uniqueValidations ) ; validationResults = validationResults . concat ( uniqueValidationResults ) ; validationResults . forEach ( ( result ) => { if ( ! this . errors [ result . key ] ) { this . errors [ result . key ] = [ ] ; } if ( ! result . valid ) { valid = false ; if ( ! result . errors || result . errors . length === 0 ) { throw new Error ( `Validation failed but didn't provide an error message. Property name: ${ result . key }.` ) ; } this . errors [ result . key ] = this . errors [ result . key ] . concat ( result . errors ) ; } } ) ; if ( setDirectly && valid === false ) { await this . clearTemporaryUniques ( ) ; } return valid ; } private async validateProperty ( key , property ) { const result = { key , valid : true , } ; const validations = this . getDefinitions ( ) [ key ] . validations ; if ( validations ) { const validatorOptions = { old : property . __oldValue , optional : false , trim : true , } ; const validationObjects = validations . map ( ( validator ) => this . getValidationObject ( validator , key ) ) ; const validationPromises = validationObjects . map ( async ( validationObject ) => { if ( validationObject . options . optional && ! property . value ) { return ; } const valid = await validationObject . validator . call ( this , property . value , { ... validatorOptions , ... validationObject . options , } ) ; if ( ! valid ) { result . valid = false ; if ( ! result . errors ) { result . errors = [ ] ; } result . errors . push ( validationObject . name ) ; } } ) ; await Promise . all ( validationPromises ) ; } return result ; } private getValidationObject ( validator , key ) { if ( typeof validator === 'function' ) { const funcName = validator . name || key ; return { name : `custom_${ funcName }` , options : { } , validator , } ; } else { if ( typeof validator === 'string' ) { return { name : validator , options : { } , validator : validators [ validator ] , } ; } else if ( validator && validator . name ) { return { name : validator . name , options : validator . options , validator : validators [ validator . name ] , } ; } else { throw new Error ( `Bad validation definition for model '${ this . modelName }' for validator '${ validator }'.` ) ; } } } private isUpdatedUnique ( key , property ) { const definition = this . getDefinitions ( ) [ key ] ; if ( ! definition || ! definition . unique ) { return false ; } if ( property . value === '' ) { return false ; } if ( ! property . __updated && this . inDb ) { return false ; } return true ; } private async isUniqueKeyFree ( key , setDirectly ) { let dbValue ; if ( setDirectly ) { dbValue = await setnx ( this . client , key , this . stringId ( ) ) ; } else { dbValue = await exists ( this . client , key ) ; } let isFreeUnique = false ; if ( setDirectly && dbValue === 1 ) { isFreeUnique = true ; this . tmpUniqueKeys . push ( key ) ; } else if ( ! setDirectly && dbValue === 0 ) { isFreeUnique = true ; } else if ( setDirectly && dbValue === 0 ) { const dbId = await get ( this . client , key ) ; if ( dbId === this . stringId ( ) ) { isFreeUnique = true ; } } debug ( `Checked unique '%s' for '%s.%s'. Result: '%s' because setDirectly: '%o' && dbValue: '%d'.` , key , this . modelName , this . id , isFreeUnique , setDirectly , dbValue ) ; return isFreeUnique ; } private getUniqueKey ( key , property ) { let uniqueValue = property . value ; if ( this . getDefinitions ( ) [ key ] . type === 'string' ) { uniqueValue = String . prototype . toLowerCase . call ( property . value ) ; } return `${ this . prefix ( 'unique' ) }:${ key }:${ uniqueValue }` ; } private async checkUniques ( setDirectly , key , property ) { const successReturn = { key , valid : true , } ; const isUpdatedUnique = this . isUpdatedUnique ( key , property ) ; if ( ! isUpdatedUnique ) { return successReturn ; } const uniqueKey = this . getUniqueKey ( key , property ) ; debug ( `Checking unique '%s' for '%s.%s' at '%s'.` , key , this . modelName , this . id , uniqueKey ) ; const isFree = await this . isUniqueKeyFree ( uniqueKey , setDirectly ) ; if ( ! isFree ) { return { errors : [ 'notUnique' ] , key , valid : false , } ; } return successReturn ; } private param { string } key param { IProperty } property returns { Promise < void > } private async clearTemporaryUniques ( ) { if ( this . tmpUniqueKeys . length > 0 ) { debug ( `Clearing temp uniques '%o' for '%s.%s'.` , this . tmpUniqueKeys , this . modelName , this . id ) ; const deletes = this . tmpUniqueKeys . map ( ( key ) => { return del ( this . client , key ) ; } ) ; await Promise . all ( deletes ) ; } } param { boolean } silent returns { Promise < void > } public async remove ( silent = false ) { callbackError ( ... arguments ) ; if ( ! this . id ) { throw new Error ( 'The instance you are trying to delete has no id.' ) ; } if ( ! this . inDb ) { await this . load ( this . id ) ; } debug ( `Removing '%s.%s'.` , this . modelName , this . id ) ; await this . deleteDbCall ( ) ; const oldId = this . id ; this . id = null ; if ( ! silent ) { this . fireEvent ( 'remove' , oldId ) ; } } private async deleteDbCall ( ) { const multi = this . client . multi ( ) ; multi . del ( `${ this . prefix ( 'hash' ) }:${ this . stringId ( ) }` ) ; multi . srem ( this . prefix ( 'idsets' ) , this . stringId ( ) ) ; this . properties . forEach ( ( prop , key ) => { if ( this . getDefinitions ( ) [ key ] . unique ) { let value = prop . __oldValue ; if ( this . getDefinitions ( ) [ key ] . type === 'string' ) { value = String ( value ) . toLowerCase ( ) ; } multi . del ( `${ this . prefix ( 'unique' ) }:${ key }:${ value }` ) ; } if ( this . getDefinitions ( ) [ key ] . index === true ) { multi . srem ( `${ this . prefix ( 'index' ) }:${ key }:${ prop . __oldValue }` , this . stringId ( ) ) ; } if ( prop . __numericIndex === true ) { multi . zrem ( `${ this . prefix ( 'scoredindex' ) }:${ key }` , this . stringId ( ) ) ; } } ) ; await this . unlinkAll ( multi ) ; await exec ( multi ) ; } param { * } id returns { Promise < boolean > } public async exists ( id ) { callbackError ( ... arguments ) ; return ! ! ( await sismember ( this . client , this . prefix ( 'idsets' ) , id ) ) ; } private async getHashAll ( id ) { const props = { } ; const values = await hgetall ( this . client , `${ this . prefix ( 'hash' ) }:${ id }` ) ; if ( values === null || Object . keys ( values ) . length === 0 ) { throw new Error ( 'not found' ) ; } Object . keys ( values ) . forEach ( ( key ) => { if ( key === '__meta_version' ) { return ; } if ( ! this . getDefinitions ( ) [ key ] ) { this . nohmClass . logError ( `A hash in the DB contained a key '${ key }' that is not in the model definition. This might be because of model changes or database corruption/intrusion.` ) ; return ; } props [ key ] = values [ key ] ; } ) ; return props ; } param { * } id returns { Object } Resolves with the return of  NohmModel . allProperties * of  NohmModel . allProperties after loading
     *  throws memberof public async load ( id ) { callbackError ( ... arguments ) ; if ( ! id ) { throw new Error ( 'No id passed to .load().' ) ; } debug ( `Loading '%s.%s' at '%s'.` , this . modelName , id ) ; const dbProps = await this . getHashAll ( id ) ; const definitions = this . getDefinitions ( ) ; Object . keys ( dbProps ) . forEach ( ( key ) => { if ( definitions [ key ] . load_pure ) { debug ( `Loading property '%s' from '%s.%s' as pure (no type casting).` , key , this . modelName , id ) ; this . setProperty ( key , dbProps [ key ] , true ) ; } else { this . property ( key , dbProps [ key ] ) ; } this . __resetProp ( key ) ; } ) ; this . id = id ; this . inDb = true ; this . _isDirty = false ; this . _isLoaded = true ; return this . allProperties ( ) ; } public link < T extends NohmModel > ( other , callback ? ) ; public link < T extends NohmModel > ( other , optionsOrNameOrCallback , callback ? ) ; example example param { NohmModel } other param { ( string | ILinkOptions | function ) } optionsOrNameOrCallback param { function } callback public link < T extends NohmModel > ( other , optionsOrNameOrCallback ? , callback ? ) { if ( typeof optionsOrNameOrCallback === 'function' ) { callback = optionsOrNameOrCallback ; optionsOrNameOrCallback = 'default' ; } else if ( typeof optionsOrNameOrCallback === 'undefined' ) { optionsOrNameOrCallback = 'default' ; } const options = this . getLinkOptions ( optionsOrNameOrCallback ) ; this . relationChanges . push ( { action : 'link' , callback , object : other , options , } ) ; debug ( `Set link for '%s.%s': %o` , this . modelName , this . id , this . relationChanges [ this . relationChanges . length - 1 ] ) ; } public unlink < T extends NohmModel > ( other , callback ? ) ; public unlink < T extends NohmModel > ( other , optionsOrNameOrCallback , callback ? ) ; example param { NohmModel } other param { ( string | ILinkOptions | function ) } optionsOrNameOrCallback param { function } callback public unlink < T extends NohmModel > ( other , optionsOrNameOrCallback ? , callback ? ) { if ( typeof optionsOrNameOrCallback === 'function' ) { callback = optionsOrNameOrCallback ; optionsOrNameOrCallback = 'default' ; } else if ( typeof optionsOrNameOrCallback === 'undefined' ) { optionsOrNameOrCallback = 'default' ; } const options = this . getLinkOptions ( optionsOrNameOrCallback ) ; this . relationChanges . forEach ( ( change , key ) => { const sameRelationChange = change . options . name === options . name && checkEqual ( change . object , other ) ; if ( sameRelationChange ) { this . relationChanges . splice ( key , 1 ) ; } } ) ; this . relationChanges . push ( { action : 'unlink' , callback , object : other , options , } ) ; debug ( `Set unlink for '%s.%s': %o` , this . modelName , this . id , this . relationChanges [ this . relationChanges . length - 1 ] ) ; } private getLinkOptions ( optionsOrName ) { if ( typeof optionsOrName === 'string' ) { return { name : optionsOrName , } ; } else { return { name : 'default' , ... optionsOrName , } ; } } private isMultiClient ( client ) { return client && typeof client . exec === 'function' ; } param { ( redis . RedisClient | redis . Multi ) } givenClient returns { Promise < void > } memberof public async unlinkAll ( givenClient ? ) { callbackError ( ... arguments ) ; let multi ; if ( this . isMultiClient ( givenClient ) ) { multi = givenClient ; } else if ( givenClient ) { multi = givenClient . multi ( ) ; } else { multi = this . client . multi ( ) ; } this . relationChanges = [ ] ; const relationKeysKey = `${ this . rawPrefix ( ) . relationKeys }${ this . modelName }:${ this . id }` ; const keys = await smembers ( this . client , relationKeysKey ) ; debug ( `Removing links for '%s.%s': %o.` , this . modelName , this . id , keys ) ; const others = keys . map ( ( key ) => { const matches = key . match ( /:([\w]*):([\w]*):[^:]+$/i ) ; if ( ! matches ) { throw new Error ( 'Malformed relation key found in the database! ' + key ) ; } const selfRelationName = matches [ 1 ] ; const otherModelName = matches [ 2 ] ; const namedMatches = matches [ 1 ] . match ( /^([\w]*)Foreign$/ ) ; const otherRelationName = namedMatches ? namedMatches [ 1 ] : matches [ 1 ] + 'Foreign' ; return { otherIdsKey : `${ this . rawPrefix ( ) . relations }${ otherModelName }:${ otherRelationName }:${ this . modelName }:` , ownIdsKey : `${ this . prefix ( 'relations' ) }:${ selfRelationName }:${ otherModelName }:${ this . id }` , } ; } ) ; const otherRelationIdsPromises = others . map ( ( item ) => this . removeIdFromOtherRelations ( multi , item ) ) ; await Promise . all ( otherRelationIdsPromises ) ; others . forEach ( ( item ) => multi . del ( item . ownIdsKey ) ) ; multi . del ( relationKeysKey ) ; if ( ! this . isMultiClient ( givenClient ) ) { await exec ( multi ) ; } } private async removeIdFromOtherRelations ( multi , item ) { const ids = await smembers ( this . client , item . ownIdsKey ) ; ids . forEach ( ( id ) => { multi . srem ( `${ item . otherIdsKey }${ id }` , this . stringId ( ) ) ; } ) ; } param { NohmModel } obj param { string } relationName returns { Promise < boolean > } public async belongsTo ( obj , relationName = 'default' ) { callbackError ( ... arguments ) ; if ( ! this . id || ! obj . id ) { throw new Error ( 'Calling belongsTo() even though either the object itself or the relation does not have an id.' ) ; } return ! ! ( await sismember ( this . client , this . getRelationKey ( obj . modelName , relationName ) , obj . id ) ) ; } param { string } otherModelName param { string } relationName returns { Promise < Array < any > > } public async getAll ( otherModelName , relationName = 'default' ) { if ( ! this . id ) { throw new Error ( `Calling getAll() even though this ${ this . modelName } has no id. Please load or save it first.` ) ; } const relationKey = this . getRelationKey ( otherModelName , relationName ) ; const ids = await smembers ( this . client , relationKey ) ; if ( ! Array . isArray ( ids ) ) { return [ ] ; } else { return ids ; } } param { string } otherModelName param { string } relationName returns { Promise < number > } public async numLinks ( otherModelName , relationName = 'default' ) { callbackError ( ... arguments ) ; if ( ! this . id ) { throw new Error ( `Calling numLinks() even though this ${ this . modelName } has no id. Please load or save it first.` ) ; } const relationKey = this . getRelationKey ( otherModelName , relationName ) ; return scard ( this . client , relationKey ) ; } see https param { ISearchOptions } searches returns { Promise < Array < any > > } public async find ( searches = { } ) { const structuredSearches = this . createStructuredSearchOptions ( searches ) ; const uniqueSearch = structuredSearches . find ( ( search ) => search . type === 'unique' ) ; if ( uniqueSearch ) { debug ( `Finding '%s's with uniques:\n%o.` , this . modelName , this . id , uniqueSearch ) ; return this . uniqueSearch ( uniqueSearch ) ; } const onlySets = structuredSearches . filter ( ( search ) => search . type === 'set' ) ; const onlyZSets = structuredSearches . filter ( ( search ) => search . type === 'zset' ) ; if ( onlySets . length === 0 && onlyZSets . length === 0 ) { return smembers ( this . client , `${ this . prefix ( 'idsets' ) }` ) ; } debug ( `Finding '%s's with these searches (sets, zsets):\n%o,\n%o.` , this . modelName , this . id , onlySets , onlyZSets ) ; const setPromises = this . setSearch ( onlySets ) ; const zSetPromises = this . zSetSearch ( onlyZSets ) ; const searchResults = await Promise . all ( [ setPromises , zSetPromises ] ) ; if ( onlySets . length !== 0 && onlyZSets . length !== 0 ) { const intersection = _ . intersection ( searchResults [ 0 ] , searchResults [ 1 ] ) ; return intersection ; } else { if ( onlySets . length !== 0 ) { return searchResults [ 0 ] ; } else { return searchResults [ 1 ] ; } } } private createStructuredSearchOptions ( searches ) { return Object . keys ( searches ) . map ( ( key ) => { const search = searches [ key ] ; if ( typeof search === 'undefined' ) { throw new Error ( 'Invalid find() options.' ) ; } const prop = this . getProperty ( key ) ; const definition = this . getDefinitions ( ) [ key ] ; const structuredSearch = { key , options : { } , type : 'undefined' , value : search , } ; if ( definition . unique ) { if ( definition . type === 'string' ) { if ( typeof search !== 'string' ) { throw new Error ( 'Invalid search parameters: Searching for a unique (type "string") with a non-string value is not supported.' ) ; } structuredSearch . value = search . toLowerCase ( ) ; } structuredSearch . type = 'unique' ; } else { if ( ! prop . __numericIndex && ! definition . index ) { throw new Error ( `Trying to search for non-indexed and non-unique property '${ key }' is not supported.` ) ; } const isDirectNumericSearch = ! isNaN ( parseInt ( search as string , 10 ) ) ; const isSimpleIndexSearch = ! prop . __numericIndex || isDirectNumericSearch ; if ( ! isSimpleIndexSearch && prop . __numericIndex ) { structuredSearch . type = 'zset' ; structuredSearch . options = search as Partial < ISearchOption > ; } else if ( definition . index === true ) { structuredSearch . type = 'set' ; } } return structuredSearch ; } ) ; } private async uniqueSearch ( options ) { const key = `${ this . prefix ( 'unique' ) }:${ options . key }:${ options . value }` ; const id = await get ( this . client , key ) ; if ( id ) { return [ id ] ; } else { return [ ] ; } } private async setSearch ( searches ) { const keys = searches . map ( ( search ) => { return `${ this . prefix ( 'index' ) }:${ search . key }:${ search . value }` ; } ) ; if ( keys . length === 0 ) { return [ ] ; } return sinter ( this . client , keys ) ; } private async zSetSearch ( searches ) { const singleSearches = await Promise . all ( searches . map ( ( search ) => this . singleZSetSearch ( search ) ) ) ; return _ . intersection ( ... singleSearches ) ; } private singleZSetSearch ( search ) { return new Promise ( ( resolve , reject ) => { const key = `${ this . prefix ( 'scoredindex' ) }:${ search . key }` ; let command = 'zrangebyscore' ; const options = { endpoints : '[]' , limit : - 1 , max : '+inf' , min : '-inf' , offset : 0 , ... search . options , } ; if ( ( options . min === '+inf' && options . max !== '+inf' ) || ( options . max === '-inf' && options . min !== '-inf' ) || parseFloat ( '' + options . min ) > parseFloat ( '' + options . max ) ) { command = 'zrevrangebyscore' ; } if ( options . endpoints === ')' ) { options . endpoints = '[)' ; } const endpoints = [ options . endpoints [ 0 ] === '(' ? '(' : '' , options . endpoints [ 1 ] === ')' ? '(' : '' , ] ; const callback = ( err , ids ) => { if ( err ) { reject ( err ) ; } else { resolve ( ids ) ; } } ; if ( options . limit ) { this . client [ command ] ( key , endpoints [ 0 ] + options . min , endpoints [ 1 ] + options . max , 'LIMIT' , options . offset , options . limit , callback ) ; } else { this . client [ command ] ( key , endpoints [ 0 ] + options . min , endpoints [ 1 ] + options . max , callback ) ; } } ) ; } see https param { Object } options param { ( Array < string > | false ) } ids returns { Promise < Array < string > > } public async sort ( options = { } , ids = false ) { callbackError ( ... arguments ) ; if ( ! Array . isArray ( ids ) ) { ids = false ; } if ( ids && ids . length === 0 ) { return [ ] ; } if ( Array . isArray ( ids ) && options === { } ) { return ids . map ( ( id ) => String ( id ) ) . sort ( ) ; } if ( ! options . field || ! this . properties . has ( options . field ) ) { throw new Error ( `Invalid field in ${ this . modelName }.sort() options: '${ options . field }'` ) ; } const fieldType = this . getDefinitions ( ) [ options . field ] . type ; const isIndexed = this . getDefinitions ( ) [ options . field ] . index ; const alpha = options . alpha || ( fieldType === 'string' ? 'ALPHA' : undefined ) ; const direction = options . direction ? options . direction : 'ASC' ; const scored = typeof fieldType === 'string' && isIndexed ? indexNumberTypes . includes ( fieldType ) : false ; let start = 0 ; let stop = 100 ; if ( Array . isArray ( options . limit ) && options . limit . length > 0 ) { start = options . limit [ 0 ] ; if ( scored ) { stop = options . limit [ 1 ] ? start + options . limit [ 1 ] : start + stop ; stop -- ; } else { stop = options . limit [ 1 ] || stop ; } } let idsetKey = this . prefix ( 'idsets' ) ; let zsetKey = `${ this . prefix ( 'scoredindex' ) }:${ options . field }` ; const client = this . client . multi ( ) ; let tmpKey = '' ; debug ( `Sorting '%s's with these options (this.id, alpha, direction, scored, start, stop, ids):` , this . modelName , this . id , alpha , direction , scored , start , stop , ids ) ; if ( ids ) { if ( scored ) { tmpKey = zsetKey + ':tmp_sort:' + + new Date ( ) + Math . ceil ( Math . random ( ) * 1000 ) ; const tempZaddArgs = [ tmpKey ] ; ids . forEach ( ( id ) => { tempZaddArgs . push ( '0' , id as string ) ; } ) ; client . zadd . apply ( client , tempZaddArgs ) ; client . zinterstore ( [ tmpKey , 2 , tmpKey , zsetKey ] ) ; zsetKey = tmpKey ; } else { tmpKey = idsetKey + ':tmp_sort:' + + new Date ( ) + Math . ceil ( Math . random ( ) * 1000 ) ; ids . unshift ( tmpKey ) ; client . sadd . apply ( client , ids as Array < string > ) ; client . sinterstore ( [ tmpKey , tmpKey , idsetKey ] ) ; idsetKey = tmpKey ; } } if ( scored ) { const method = direction && direction === 'DESC' ? 'zrevrange' : 'zrange' ; client [ method ] ( zsetKey , start , stop ) ; } else { const args = [ idsetKey , 'BY' , `${ this . prefix ( 'hash' ) }:*->${ options . field }` , 'LIMIT' , String ( start ) , String ( stop ) , direction , ] ; if ( alpha ) { args . push ( alpha ) ; } client . sort . apply ( client , args ) ; } if ( ids ) { client . del ( tmpKey ) ; } const replies = await exec < any > ( client ) ; let reply ; if ( ids ) { reply = replies . splice ( 2 , 1 ) [ 0 ] ; } else { reply = replies . splice ( 0 , 1 ) [ 0 ] ; } replies . forEach ( ( otherReply ) => { if ( otherReply instanceof Error ) { const errorMessage = otherReply . stack ? otherReply . stack : otherReply . message ; this . nohmClass . logError ( `Error during ${ this . modelName }.sort() multi.exec(): ${ errorMessage }` ) ; } } ) ; if ( reply instanceof Error ) { throw reply ; } else { return reply ; } } returns { Object } public getDefinitions ( ) { const definitions = Object . getPrototypeOf ( this ) . definitions ; if ( ! definitions ) { throw new Error ( `Model was not defined with proper static definitions: '${ this . modelName }'` ) ; } return definitions ; } private fireEvent ( event , ... args ) { if ( ! this . getPublish ( ) ) { return ; } if ( eventActions . indexOf ( event ) < 0 ) { const supported = eventActions . join ( ', ' ) ; this . nohmClass . logError ( 'Cannot fire an unsupported action. Was "' + event + '" ' + 'and must be one of ' + supported ) ; return ; } const composer = messageComposers [ event ] || messageComposers . defaultComposer ; const payload = composer . apply ( this , args ) ; const message = JSON . stringify ( payload ) ; debugPubSub ( `Firing event '%s' for '%s': %j.` , event , this . modelName , payload ) ; this . client . publish ( `${ this . prefix ( 'channel' ) }:${ event }` , message ) ; } private getPublish ( ) { if ( this . publish !== null ) { return this . publish ; } else { return this . nohmClass . getPublish ( ) ; } } public async subscribe < TPayloadProps extends TProps > ( eventName , callback ) ; public async subscribe < TPayloadProps extends TProps > ( eventName , callback ) ; public async subscribe < TPayloadProps extends TProps > ( eventName , callback ) ; param { string } eventName param { function } callback returns { Promise < void > } memberof public async subscribe < TPayloadProps extends TProps > ( eventName , callback ) { debugPubSub ( `Subscribing to event '%s' for '%s'.` , eventName , this . modelName ) ; await this . nohmClass . subscribeEvent ( `${ this . modelName }:${ eventName }` , callback ) ; } param { string } eventName param { function } callback returns { Promise < void > } memberof public async subscribeOnce ( eventName , callback ) { debugPubSub ( `Subscribing once to event '%s' for '%s'.` , eventName , this . modelName ) ; await this . nohmClass . subscribeEventOnce ( `${ this . modelName }:${ eventName }` , callback ) ; } param { string } eventName param { function } fn memberof public unsubscribeEvent ( eventName , fn ? ) { debugPubSub ( `Unsubscribing from event '%s' for '%s' with fn?: %s.` , eventName , this . modelName , fn ) ; this . nohmClass . unsubscribeEvent ( eventName , fn ) ; } get id ( ) : null | string { return this . _id ; } memberof set id ( id ) { if ( id === null ) { this . _id = null ; this . _isLoaded = false ; this . _isDirty = false ; this . allPropertiesCache . id = null ; return ; } const stringifiedId = String ( id ) ; if ( this . _id !== stringifiedId ) { this . _id = stringifiedId ; this . _isLoaded = false ; this . _isDirty = true ; this . allPropertiesCache . id = this . _id ; } } private stringId ( ) { return typeof this . _id === 'string' ? this . _id : '' ; } readonly type { boolean } memberof get isLoaded ( ) : boolean { return this . _isLoaded ; } /**
     * True if there are any unsaved changes. This is triggered by changing the id manually,
     * using .link()/.unlink() and changing properties from their stored state.
     */ get isDirty ( ) : boolean { if ( this . _isDirty ) { return true ; } if ( this . relationChanges . length > 0 ) { return true ; } const propDiffs = this . propertyDiff ( ) ; if ( propDiffs . length > 0 ) { return true ; } return false ; } } export default NohmModel ;