import * as Debug from 'debug' ; import { EventEmitter } from 'events' ; import * as redis from 'redis' ; import { LinkError } from './errors/LinkError' ; import { ValidationError } from './errors/ValidationError' ; import { getPrefix , INohmPrefixes } from './helpers' ; import { IMiddlewareOptions , middleware , TRequestHandler } from './middleware' ; import { IDictionary , ILinkOptions , IModelOptions , IModelPropertyDefinition , IModelPropertyDefinitions , ISortOptions , NohmModel , TLinkCallback , } from './model' ; import { boolProperty , dateProperty , floatProperty , IChangeEventPayload , IDefaultEventPayload , integerProperty , IRelationChangeEventPayload , ISearchOption , IStaticMethods , jsonProperty , numberProperty , stringProperty , timeProperty , timestampProperty , TTypedDefinitions , } from './model.header' ; import { psubscribe , punsubscribe , keys , del } from './typed-redis-helper' ; export { boolProperty , dateProperty , floatProperty , IChangeEventPayload , IDefaultEventPayload , IDictionary , ILinkOptions , IModelOptions , IModelPropertyDefinition , IModelPropertyDefinitions , INohmPrefixes , integerProperty , IRelationChangeEventPayload , ISortOptions , IStaticMethods , jsonProperty , LinkError , NohmModelExtendable as NohmModel , numberProperty , stringProperty , timeProperty , timestampProperty , TLinkCallback , TTypedDefinitions , ValidationError , } ; export { nohm , nohm as Nohm } ; const debug = Debug ( 'nohm:index' ) ; const debugPubSub = Debug ( 'nohm:pubSub' ) ; const PUBSUB_ALL_PATTERN = '*:*' ; abstract class NohmModelExtendable < TProps = any > extends NohmModel < TProps > { public client : redis . RedisClient ; protected nohmClass : NohmClass ; protected protected _initOptions ( ) { throw new Error ( 'Class is not extended properly. Use the return Nohm.register() instead of your class directly.' ) ; } protected protected prefix ( _prefix ) { throw new Error ( 'Class is not extended properly. Use the return Nohm.register() instead of your class directly.' ) ; } protected protected rawPrefix ( ) { throw new Error ( 'Class is not extended properly. Use the return Nohm.register() instead of your class directly.' ) ; } } export interface INohmOptions { prefix ? : string ; client ? : redis . RedisClient ; meta ? : boolean ; publish ? : boolean | redis . RedisClient ; } export type Constructor < T > = new ( ... args ) => T ; function staticImplements < T > ( ) { return ( _constructor ) => { } ; } namespace namespace example class export class NohmClass { /**
     * The redis prefixed key object.
     * Defaults to prefixing with 'nohm' which then creates keys like 'nohm:idsets:someModel'.
     */ public prefix : INohmPrefixes ; /**
     * The current global nohm redis client
     */ public client : redis . RedisClient ; public readonly LinkError = LinkError ; public readonly ValidationError = ValidationError ; /**
     * Whether to store the meta values about models.
     * This is used for example by the admin app.
     * Defaults to true.
     */ private meta : boolean ; private publish : boolean = false ; private publishClient : redis . RedisClient ; private isPublishSubscribed : boolean ; private publishEventEmitter : EventEmitter ; private modelCache : { [ name ] : Constructor < NohmModel < any > > ; } ; private extraValidators : Array < string > ; constructor ( { prefix , client , meta , publish } ) { debug ( 'Creating NohmClass.' , arguments ) ; this . setPrefix ( prefix ) ; if ( client ) { this . setClient ( client ) ; } this . modelCache = { } ; this . extraValidators = [ ] ; this . meta = meta || true ; this . isPublishSubscribed = false ; if ( typeof publish !== 'undefined' ) { if ( typeof publish !== 'boolean' ) { this . setPublish ( true ) ; this . setPubSubClient ( publish ) ; } else { this . setPublish ( publish ) ; } } } /**
     * Set the Nohm global redis client.
     * Note: this will not affect models that have a client set on their own.
     */ public setPrefix ( prefix = 'nohm' ) { debug ( 'Setting new prefix.' , prefix ) ; this . prefix = getPrefix ( prefix ) ; } /**
     * Set the Nohm global redis client.
     * Note: this will not affect models that have a client set on their own.
     */ public setClient ( client ? ) { debug ( 'Setting new redis client. Connected: %s; Address: %s.' , client && ( client . connected || ( client as any ) . status === 'ready' ) , client && ( client as any ) . address ) ; if ( client && ! ( client . connected || ( client as any ) . status === 'ready' ) ) { this . logError ( `WARNING: setClient() received a redis client that is not connected yet.
Consider waiting for an established connection before setting it. Status (if ioredis): ${ ( client as any ) . status }
, connected (if node_redis): ${ client . connected }` ) ; } else if ( ! client ) { client = redis . createClient ( ) ; } this . client = client ; } public logError ( err ) { if ( err ) { console . error ( { message : err , name : 'Nohm Error' , } ) ; } } param { string } modelName param { IModelDefinitions } options param { boolean } temp returns { NohmStaticModel } public model < TAdditionalMethods > ( modelName , options , temp = false ) { if ( ! modelName ) { this . logError ( 'When creating a new model you have to provide a name!' ) ; } debug ( 'Registering new model using model().' , modelName , options , temp ) ; const self = this ; let metaVersion = '' ; class @ staticImplements < IStaticMethods < CreatedClass > > ( ) class CreatedClass extends NohmModelExtendable { memberof type { RedisClient } public client = self . client ; protected nohmClass = self ; protected options = options ; memberof type { string } public readonly modelName = modelName ; ignore memberof constructor ( ) { super ( ) ; if ( self . meta ) { if ( ! metaVersion ) { metaVersion = this . generateMetaVersion ( ) ; } this . meta = { inDb : false , properties : this . options . properties , version : metaVersion , } ; } } protected _initOptions ( ) { this . options = options || { properties : { } } ; Object . getPrototypeOf ( this ) . definitions = this . options . properties ; this . meta = { inDb : false , properties : { } , version : '' , } ; if ( ! this . client ) { this . client = self . client ; } } protected prefix ( prefix ) { return self . prefix [ prefix ] + modelName ; } protected rawPrefix ( ) { return self . prefix ; } static param { * } id throws alias memberof returns { Promise < NohmModel > } public static async load < P extends NohmModel > ( id ) { const model = await self . factory < P > ( modelName ) ; await model . load ( id ) ; return model ; } static param { Array < string > } ids alias memberof returns { Promise < NohmModel > } public static async loadMany < P extends NohmModel > ( ids ) { if ( ! Array . isArray ( ids ) || ids . length === 0 ) { return [ ] ; } const loadPromises = ids . map ( async ( id ) => { try { return await self . factory ( modelName , id ) ; } catch ( err ) { if ( err && err . message === 'not found' ) { return ; } else { throw err ; } } } ) ; const loadedModels = await Promise . all ( loadPromises ) ; return loadedModels . filter < P > ( ( model ) : model is P => typeof model !== 'undefined' ) ; } static param { ISearchOptions } searches alias memberof returns { Promise < Array < NohmModel > > } public static async findAndLoad < P extends NohmModel , TProps extends IDictionary > ( searches ? ) { const dummy = await self . factory < P > ( modelName ) ; const ids = await dummy . find ( searches ) ; if ( ids . length === 0 ) { return [ ] ; } const loadPromises = ids . map ( ( id ) => { return self . factory < P > ( modelName , id ) ; } ) ; return Promise . all ( loadPromises ) ; } static see NohmModel . sort param { ISortOptions < IDictionary > } sortOptions alias memberof returns { Promise < Array < string > > } public static async sort ( sortOptions = { } , ids = false ) { const dummy = await self . factory ( modelName ) ; return dummy . sort ( sortOptions , ids ) ; } static see NohmModel . find param { ISearchOptions } searches alias memberof returns { Promise < Array < string > > } public static async find < TProps extends IDictionary > ( searches = { } ) { const dummy = await self . factory ( modelName ) ; return dummy . find ( searches ) ; } static param { * } id alias memberof returns { Promise < NohmModel > } public static async remove ( id , silent ? ) { const model = await self . factory ( modelName ) ; model . id = id ; await model . remove ( silent ) ; } } if ( ! temp ) { this . modelCache [ modelName ] = CreatedClass ; } return CreatedClass as any ; } param { NohmModel } subClass param { boolean } temp returns { NohmStaticModel } example public register < T extends Constructor < NohmModelExtendable < IDictionary > > > ( subClass , temp = false ) { const self = this ; const modelName = ( subClass as any ) . modelName ; if ( ! modelName ) { throw new Error ( 'A class passed to nohm.register() did not have static a modelName property.' ) ; } if ( ! ( subClass as any ) . definitions ) { throw new Error ( 'A class passed to nohm.register() did not have static property definitions.' ) ; } debug ( 'Registering new model using register().' , modelName , ( subClass as any ) . definitions , temp ) ; let metaVersion = '' ; @ staticImplements < IStaticMethods < CreatedClass > > ( ) class CreatedClass extends subClass { protected nohmClass = self ; public readonly modelName = modelName ; constructor ( ... args ) { super ( ... args ) ; if ( self . meta ) { if ( ! metaVersion ) { metaVersion = this . generateMetaVersion ( ) ; } this . meta = { inDb : false , properties : this . options . properties , version : metaVersion , } ; } } protected _initOptions ( ) { this . options = { idGenerator : ( subClass as any ) . idGenerator , properties : { } , } ; if ( ! this . client ) { this . client = self . client ; } this . meta = { inDb : false , properties : { } , version : '' , } ; if ( ! this . options . idGenerator ) { this . options . idGenerator = 'default' ; } } protected prefix ( prefix ) { return self . prefix [ prefix ] + this . modelName ; } protected rawPrefix ( ) { return self . prefix ; } public getDefinitions ( ) { const definitions = ( CreatedClass as any ) . definitions ; if ( ! definitions ) { throw new Error ( `Model was not defined with proper static definitions: '${ modelName }'` ) ; } return definitions ; } param { * } id returns { Promise < NohmModel | void > } public static async load < P extends NohmModel > ( id ) { const model = await self . factory < P > ( modelName ) ; await model . load ( id ) ; return model ; } param { Array < string > } ids returns { Promise < NohmModel > } public static async loadMany < P extends NohmModel > ( ids ) { if ( ! Array . isArray ( ids ) || ids . length === 0 ) { return [ ] ; } const loadPromises = ids . map ( async ( id ) => { try { return await self . factory ( modelName , id ) ; } catch ( err ) { if ( err && err . message === 'not found' ) { return ; } else { throw err ; } } } ) ; const loadedModels = await Promise . all ( loadPromises ) ; return loadedModels . filter < P > ( ( model ) : model is P => typeof model !== 'undefined' ) ; } param { ISearchOptions } searches returns { Promise < Array < NohmModel > > } public static async findAndLoad < P extends NohmModel , TProps extends IDictionary > ( searches ? ) { const dummy = await self . factory < P > ( modelName ) ; const ids = await dummy . find ( searches ) ; if ( ids . length === 0 ) { return [ ] ; } const loadPromises = ids . map ( ( id ) => { return self . factory < P > ( dummy . modelName , id ) ; } ) ; return Promise . all ( loadPromises ) ; } see NohmModel . sort static param { ISortOptions < IDictionary > } sortOptions returns { Promise < Array < string > > } public static async sort ( options = { } , ids = false ) { const dummy = await self . factory ( modelName ) ; return dummy . sort ( options , ids ) ; } see NohmModel . find static param { ISearchOptions } searches returns { Promise < Array < string > > } public static async find < TProps extends IDictionary > ( searches = { } ) { const dummy = await self . factory ( modelName ) ; return dummy . find ( searches ) ; } param { * } id returns { Promise < NohmModel > } public static async remove ( id , silent ? ) { const model = await self . factory ( modelName ) ; model . id = id ; await model . remove ( silent ) ; } } if ( ! temp ) { this . modelCache [ modelName ] = CreatedClass ; } return CreatedClass ; } returns { Array < NohmModelStatic > } public getModels ( ) { return this . modelCache ; } param { string } name param { * } id returns { Promise < NohmModel > } throws throws memberof public async factory < T extends NohmModel < any > > ( name , id ? ) { if ( typeof arguments [ 1 ] === 'function' || typeof arguments [ 2 ] === 'function' ) { throw new Error ( 'Not implemented: factory does not support callback method anymore.' ) ; } else { debug ( `Factory is creating a new instance of '%s' with id %s.` , name , id ) ; const model = this . modelCache [ name ] ; if ( ! model ) { throw new Error ( `Model '${ name }' not found.` ) ; } const instance = new model ( ) as T ; if ( id ) { await instance . load ( id ) ; return instance ; } else { return instance ; } } } param { Object } client public async purgeDb ( client = this . client ) { async function delKeys ( prefix ) { const foundKeys = await keys ( client , prefix + '*' ) ; if ( foundKeys . length === 0 ) { return ; } await del ( client , foundKeys ) ; } const deletes = [ ] ; debug ( `PURGING DATABASE!` , client && client . connected , client && ( client as any ) . address , this . prefix ) ; Object . keys ( this . prefix ) . forEach ( ( key ) => { const prefix = ( this . prefix as any ) [ key ] ; if ( typeof prefix === 'object' ) { Object . keys ( prefix ) . forEach ( ( innerKey ) => { const innerPrefix = prefix [ innerKey ] ; deletes . push ( delKeys ( innerPrefix ) ) ; } ) ; } else { deletes . push ( delKeys ( prefix ) ) ; } } ) ; await Promise . all ( deletes ) ; } public setExtraValidations ( files ) { debug ( `Setting extra validation files` , files ) ; if ( ! Array . isArray ( files ) ) { files = [ files ] ; } files . forEach ( ( path ) => { if ( this . extraValidators . indexOf ( path ) === - 1 ) { this . extraValidators . push ( path ) ; const validators = require ( path ) ; Object . keys ( validators ) . forEach ( ( _name ) => { } ) ; } } ) ; } public getExtraValidatorFileNames ( ) { return this . extraValidators ; } public middleware ( options ) { return middleware ( options , this ) ; } public getPublish ( ) { return this . publish ; } public setPublish ( publish ) { debug ( `Setting publish mode to '%o'.` , ! ! publish ) ; this . publish = ! ! publish ; } public getPubSubClient ( ) { return this . publishClient ; } public setPubSubClient ( client ) { debug ( `Setting pubSub client. Connected: '%s'; Address: '%s'.` , client && client . connected , client && ( client as any ) . address ) ; this . publishClient = client ; return this . initPubSub ( ) ; } private async initPubSub ( ) { if ( ! this . getPubSubClient ( ) ) { throw new Error ( 'A second redis client must set via nohm.setPubSubClient before using pub/sub methods.' ) ; } else if ( this . isPublishSubscribed === true ) { return ; } this . publishEventEmitter = new EventEmitter ( ) ; this . publishEventEmitter . setMaxListeners ( 0 ) ; this . isPublishSubscribed = true ; await psubscribe ( this . publishClient , this . prefix . channel + PUBSUB_ALL_PATTERN ) ; debugPubSub ( `Redis PSUBSCRIBE for '%s'.` , this . prefix . channel + PUBSUB_ALL_PATTERN ) ; this . publishClient . on ( 'pmessage' , ( _pattern , channel , message ) => { const suffix = channel . slice ( this . prefix . channel . length ) ; const parts = suffix . match ( /([^:]+)/g ) ; if ( ! parts ) { this . logError ( `An erroneous channel has been captured: ${ channel }.` ) ; return ; } const modelName = parts [ 0 ] ; const action = parts [ 1 ] ; let payload = { } ; try { payload = message ? JSON . parse ( message ) : { } ; debugPubSub ( `Redis published message for model '%s' with action '%s' and message: '%j'.` , modelName , action , payload ) ; } catch ( e ) { this . logError ( `A published message is not valid JSON. Was : "${ message }"` ) ; return ; } this . publishEventEmitter . emit ( `${ modelName }:${ action }` , payload ) ; } ) ; } public async subscribeEvent ( eventName , callback ) { await this . initPubSub ( ) ; debugPubSub ( `Redis subscribing to event '%s'.` , eventName ) ; this . publishEventEmitter . on ( eventName , callback ) ; } public async subscribeEventOnce ( eventName , callback ) { await this . initPubSub ( ) ; debugPubSub ( `Redis subscribing once to event '%s'.` , eventName ) ; this . publishEventEmitter . once ( eventName , callback ) ; } public unsubscribeEvent ( eventName , fn ? ) { if ( this . publishEventEmitter ) { debugPubSub ( `Redis unsubscribing from event '%s' with fn?: %s.` , eventName , fn ) ; if ( ! fn ) { this . publishEventEmitter . removeAllListeners ( eventName ) ; } else { this . publishEventEmitter . removeListener ( eventName , fn ) ; } } } public async closePubSub ( ) { if ( this . isPublishSubscribed === true ) { debugPubSub ( `Redis PUNSUBSCRIBE for '%s'.` , this . prefix . channel + PUBSUB_ALL_PATTERN ) ; this . isPublishSubscribed = false ; await punsubscribe ( this . publishClient , this . prefix . channel + PUBSUB_ALL_PATTERN ) ; } return this . publishClient ; } } const nohm = new NohmClass ( { } ) ; export default nohm ;