@ti-engine/core 1.0.14 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,18 +1,44 @@
1
1
  # ti-engine changelog
2
2
 
3
+ ## Version 1.1.0
4
+
5
+ * feat(redis integration): implement better process for fetching and storing the remote Redis server settings and enabled features
6
+ * feat(redis integration): add getter method `serverVersion` that returns the Redis server version
7
+ * feat(redis integration): add support for RedisJSON functionality
8
+ * feat(redis integration): add getter method `isJSONSupported` to verify if this module is enabled/supported in Redis server
9
+ * feat(redis integration): add separate enum for override modes `TiRedisOverrideMode`
10
+ * feat(redis integration): add support for executing any single command in Redis that is otherwise unsupported by the underlying framework. Use method `callCommand` for that in case you need to access something more exotic or currently unimplemented
11
+ * feat(cache): add support for working with JSON values. Currently supporting operations `set`, `get`, and `append array` with more coming in future versions
12
+ * feat(cache): add support for setting manual expiration of values
13
+ * feat(message tracer): change log level of trace entries from `DEBUG` to `NOTICE` for all events
14
+ * feat(message tracer): add functionality for storing the trace entries in the `cache` in JSON format. This is the default behavior and will always work. Traces can later be retrieved from the `cache` for further processing by specialized application
15
+ * feat(message tracer): add new property `traceTimestamp` to the trace entries as it may differ from the actual message timestamp
16
+ * feat(config): add new setting `messageExchange.traceExpirationTime` that controls the preservation time for all trace entries. Default is one hour and will be refreshed every time a new trace is generated. Setting this to `0` will disable
17
+ expiration altogether
18
+ * feat(config): add new setting `messageExchange.traceRepository` that specifies the name of the key in `cache` that will store the trace entries. The default one should be sufficient for most cases
19
+ * feat(config): change default of setting `messageExchange.traceLogEnabled` to `false`. With this change trace entries will not be written to the standard log output
20
+ * feat(exceptions): add new exception code `1006` as `E_GEN_FEATURE_UNSUPPORTED` to indicate functionality that is unsupported by the system or integrated application
21
+ * refactor(config)!: correct the name of the ENV variable controlling `memoryCache.user` to `TI_MEMORY_CACHE_USER` from previous `MEMORY_CACHE_USER`
22
+ * build(npm): update npm development dependencies to their latest versions
23
+ * docs: update some JSDoc types and other entries to reflect the current state of the code
24
+
3
25
  ## Version 1.0.14
26
+
4
27
  * feat(message exchange): improve the behavior of `onConnectionRecovered` and `onConnectionDisrupted` events in modules `cache`, `message-handler` and `service-executor` to prevent cross-activation or deactivation
5
28
  * feat(redis integration): improve Redis integration for greater stability, support of Redis Cloud, and support for Redis 7
6
29
  * feat(config): enable setting `auditing.logDetails` by default for all logs
7
30
  * docs: update general documentation
8
31
 
9
32
  ## Version 1.0.13
33
+
10
34
  * build(npm): update npm dependencies to their latest versions
11
35
 
12
36
  ## Version 1.0.12
37
+
13
38
  * build(npm): update npm dependencies to their latest versions
14
39
 
15
40
  ## Version 1.0.11
41
+
16
42
  * feat(message exchange): implement message tampering and insertion protection
17
43
  * feat(config): provide option to turn on/off the message tampering and insertion protection
18
44
  * feat(config): provide option to turn on/off message tracing
@@ -21,4 +47,5 @@
21
47
  * docs: add a change log
22
48
 
23
49
  ## Version 1.0.0
50
+
24
51
  * feat: create initial version of the framework
package/bin/settings.json CHANGED
@@ -23,7 +23,9 @@
23
23
  "messageStore": "ti:messages:store",
24
24
  "securityHashEnabled": true,
25
25
  "securityHashKey": "23e7bdc7-a793-41f9-856e-6760332f0c73",
26
- "traceLogEnabled": true
26
+ "traceExpirationTime": 3600,
27
+ "traceLogEnabled": false,
28
+ "traceRepository": "ti:messages:trace"
27
29
  },
28
30
  "serviceConfig": {
29
31
  "executionTimeout": 180000,
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -7,6 +7,8 @@ const _ = require( "lodash" );
7
7
  const tools = require( "#tools" );
8
8
  const config = require( "#config" );
9
9
  const logger = require( "#logger" );
10
+ const exceptions = require( "#exceptions" );
11
+ const cache = require( "#cache" );
10
12
 
11
13
  /**
12
14
  * @typedef {Object} TiTraceEntry
@@ -14,13 +16,18 @@ const logger = require( "#logger" );
14
16
  * @property {string} dispatchEvent
15
17
  * @property {string} fromAddress
16
18
  * @property {string} messageID
17
- * @property {string} messageSnapshot
19
+ * @property {Object} messageSnapshot
18
20
  * @property {string} messageState
19
21
  * @property {string} messageType
20
22
  * @property {string} toAddress
21
23
  * @property {string} traceID
24
+ * @property {number} traceTimestamp
22
25
  */
23
26
 
27
+ const traceRoot = {
28
+ trace: []
29
+ };
30
+
24
31
  /**
25
32
  * Enum for listing message types.
26
33
  *
@@ -96,22 +103,28 @@ let formatLogEntry = ( traceEntry ) => {
96
103
  };
97
104
 
98
105
  /**
99
- * Used to obscure sensitive data in the message snapshot and convert it to string.
106
+ * Used to obscure sensitive data in the message, remove the payload, and return a snapshot.
100
107
  *
101
108
  * @method
102
109
  * @param {Message} message
103
- * @returns {string}
110
+ * @returns {Message}
104
111
  * @private
105
112
  */
106
113
  let obscureSensitiveData = ( message ) => {
107
- let messageSnapshot = tools.stringifyJSON( message );
108
- return _.replace( messageSnapshot, /("\w*?pin\w*?"|"\w*?pass\w*?"|"\w*?otp\w*?"):"(.*?)"/gmi, "\"SENSITIVE_PROPERTY\":\"OBSCURED_BY_SYSTEM\"" );
114
+ /** @type Message */
115
+ let messageSnapshot = tools.parseJSON( _.replace( tools.stringifyJSON( message ), /("\w*?pin\w*?"|"\w*?pass\w*?"|"\w*?otp\w*?"):"(.*?)"/gmi, "\"SENSITIVE_PROPERTY\":\"OBSCURED_BY_SYSTEM\"" ) );
116
+ delete messageSnapshot.payload;
117
+ return messageSnapshot;
109
118
  };
110
119
 
111
120
  /**
112
121
  * Used to create a trace entry for the provided {@link Message} and parameters.
113
122
  * <br/>
114
- * NOTE: With the exception of failed message delivery, trace events are logged with severity level DEBUG.
123
+ * NOTE: By default all trace events are stored in the memory cache for further processing and analysis. The
124
+ * location is configured in the MESSAGE_EXCHANGE_TRACE_REPOSITORY setting.
125
+ * <br/>
126
+ * NOTE: Trace events are logged with severity level NOTICE or ERROR for failed dispatches. They still might be
127
+ * filtered out if the minimum log level setting is set too high.
115
128
  *
116
129
  * @method
117
130
  * @param {Message} message The message to trace.
@@ -124,6 +137,10 @@ module.exports.recordTraceEntry = ( message, messageType, dispatchEvent, message
124
137
  // depending on whether the message comes as request or response, the from and to addresses will be opposite:
125
138
  let source = message.source.route + "." + message.source.instanceID;
126
139
  let destination = message.destination.route + ( ( message.destination.instanceID != null ) ? "." + message.destination.instanceID : "" );
140
+ let messageSnapshot = obscureSensitiveData( message );
141
+ delete messageSnapshot.chainID;
142
+ delete messageSnapshot.messageID;
143
+ let currentDate = new Date();
127
144
 
128
145
  /** @type TiTraceEntry */
129
146
  let traceEntry = {
@@ -131,16 +148,34 @@ module.exports.recordTraceEntry = ( message, messageType, dispatchEvent, message
131
148
  dispatchEvent: tools.getEnumName( dispatchEventEnum, dispatchEvent ),
132
149
  fromAddress: ( messageType === messageTypeEnum.MESSAGE_REQUEST ) ? source : destination,
133
150
  messageID: message.messageID,
134
- messageSnapshot: obscureSensitiveData( message ),
151
+ messageSnapshot: messageSnapshot,
135
152
  messageState: tools.getEnumName( messageStateEnum, messageState ),
136
153
  messageType: tools.getEnumName( messageTypeEnum, messageType ),
137
154
  toAddress: ( messageType === messageTypeEnum.MESSAGE_REQUEST ) ? destination : source,
155
+ traceTimestamp: currentDate.getTime(),
138
156
  traceID: tools.getUUID()
139
157
  };
140
158
 
159
+ // only write the trace in the general log if this is enabled:
141
160
  if ( config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_LOG_ENABLED ) === true ) {
142
- createLogEntry( traceEntry, ( dispatchEvent === dispatchEventEnum.FAILED ) ? logger.logSeverity.ERROR : logger.logSeverity.DEBUG );
161
+ createLogEntry( traceEntry, ( dispatchEvent === dispatchEventEnum.FAILED ) ? logger.logSeverity.ERROR : logger.logSeverity.NOTICE );
143
162
  }
144
163
 
145
- // TODO Feature: Functionality that can dispatch the trace entry to a configurable database and/or monitoring system.
164
+ // add the trace entry to the repository in the memory cache:
165
+ cache.setJSON( config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_REPOSITORY ), traceRoot, "$", 1 ).then( () => {
166
+ return cache.arrayAppendJSON( config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_REPOSITORY ), traceEntry, "$.trace" );
167
+ } ).then( () => {
168
+ // this will refresh the expiration time for the trace repository on each new record:
169
+ let expiration = config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_EXPIRATION_TIME );
170
+ return ( expiration > 0 ) ? cache.expireValue( config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_REPOSITORY ), expiration ) : expiration;
171
+ } ).catch( ( error ) => {
172
+ if ( error.code === exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED ) {
173
+ cache.addToSet( config.getSetting( config.setting.MESSAGE_EXCHANGE_TRACE_REPOSITORY ), traceEntry ).catch( ( error ) => {
174
+ logger.log( `Failed to add message trace entry to the trace repository. While this will not prevent the application from running, it might still be a sign of a more serious problem!`, logger.logSeverity.WARNING, error );
175
+ } );
176
+ } else {
177
+ logger.log( `Failed to add message trace entry to the trace repository. While this will not prevent the application from running, it might still be a sign of a more serious problem!`, logger.logSeverity.WARNING, error );
178
+ }
179
+ } );
180
+
146
181
  };
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -19,6 +19,7 @@ const _ = require( "lodash" );
19
19
  let cacheCommandsEnum = tools.enum( {
20
20
  ADD_TO_SET: [ "sadd", "add to set", "https://redis.io/commands/sadd" ],
21
21
  DELETE_VALUE: [ "del", "delete value", "https://redis.io/commands/del" ],
22
+ EXPIRE: [ "expire", "expire", "https://redis.io/commands/expire" ],
22
23
  GET_ALL_FROM_SET: [ "smembers", "get all set members", "https://redis.io/commands/smembers" ],
23
24
  GET_VALUE: [ "get", "get value", "https://redis.io/commands/get" ],
24
25
  HASH_GET: [ "hget", "hash get", "https://redis.io/commands/hget" ],
@@ -27,6 +28,9 @@ let cacheCommandsEnum = tools.enum( {
27
28
  HASH_SET: [ "hset", "", "https://redis.io/commands/hset" ],
28
29
  HASH_SET_MANY: [ "hmset", "", "https://redis.io/commands/hmset" ],
29
30
  IS_SET_MEMBER: [ "sismember", "", "https://redis.io/commands/sismember" ],
31
+ JSON_ARRAY_APPEND: [ "json.arrappend", "", "https://redis.io/commands/json.arrappend" ],
32
+ JSON_GET: [ "json.get", "", "https://redis.io/commands/json.get" ],
33
+ JSON_SET: [ "json.set", "", "https://redis.io/commands/json.set" ],
30
34
  KEYS: [ "keys", "", "https://redis.io/commands/keys" ],
31
35
  LIST_PUSH: [ "lpush", "list push", "https://redis.io/commands/lpush" ],
32
36
  LIST_POP_TAIL_BLOCKING: [ "brpop", "list pop tail blocking", "https://redis.io/commands/brpop" ],
@@ -36,10 +40,26 @@ let cacheCommandsEnum = tools.enum( {
36
40
  UNION_OF_SETS: [ "sunion", "union of sets", "https://redis.io/commands/sunion" ]
37
41
  } );
38
42
 
43
+ /**
44
+ * Enum for listing the Redis key override modes.
45
+ *
46
+ * @readonly
47
+ * @enum {string}
48
+ */
49
+ let cacheOverrideModeEnum = tools.enum( {
50
+ DEFAULT: [ "", "default", "Standard Redis behaviour when setting new key." ],
51
+ NX: [ "nx", "nx", "Sets the key only if it does not already exist." ],
52
+ XX: [ "xx", "xx", "Sets the key only if it already exists." ]
53
+ } );
54
+
39
55
  /**
40
56
  * @typedef {string} TiRedisCommand
41
57
  */
42
58
  module.exports.cacheCommands = cacheCommandsEnum;
59
+ /**
60
+ * @typedef {string} TiRedisOverrideMode
61
+ */
62
+ module.exports.cacheOverrideMode = cacheOverrideModeEnum;
43
63
 
44
64
  /**
45
65
  * Used to create a Redis Cache client.
@@ -53,6 +73,8 @@ class RedisClient {
53
73
  #retryMaxInterval = 1000;
54
74
  #retryMaxAttempts = undefined;
55
75
  #redisClient = undefined;
76
+ #serverInfo = {};
77
+ #serverFeatures = {};
56
78
  #connectionObservers = [];
57
79
 
58
80
  /**
@@ -101,13 +123,40 @@ class RedisClient {
101
123
  this.#redisClient = new Redis( options );
102
124
 
103
125
  this.#redisClient.on( "ready", () => {
104
- let serverInfo = this.#redisClient.serverInfo || {};
105
- logger.log( `Connection to Redis server ${ host }:${ port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO, serverInfo );
126
+ logger.log( `Connection to Redis server ${ host }:${ port } (re)established by client '${ this.identifier }' and is ready to be used.`, logger.logSeverity.INFO );
106
127
 
107
128
  // notify all connection observers about this event:
108
129
  _.forEach( this.#connectionObservers, ( connectionObservers ) => {
109
130
  connectionObservers.onConnectionRecovered( this.#clientIdentifier );
110
131
  } );
132
+
133
+ // fetch the server information and store it:
134
+ this.#redisClient.info().then( ( result ) => {
135
+ this.#serverInfo = {};
136
+ if ( _.isString( result ) ) {
137
+ let rawData = _.split( result, "\r\n" );
138
+ _.forEach( rawData, ( entry ) => {
139
+ let details = _.split( entry, ":" );
140
+ if ( !_.startsWith( details[ 0 ], "#" ) && details[ 0 ] !== "" && details[ 0 ] ) {
141
+ if ( _.isNaN( _.toNumber( details[ 1 ] ) ) ) {
142
+ this.#serverInfo[ details[ 0 ] ] = details[ 1 ];
143
+ } else {
144
+ this.#serverInfo[ details[ 0 ] ] = _.toNumber( details[ 1 ] );
145
+ }
146
+ }
147
+ } );
148
+ }
149
+ return this.#redisClient.module( "LIST" );
150
+ } ).then( ( result ) => {
151
+ this.#serverFeatures = {};
152
+ if ( _.isArray( result ) ) {
153
+ _.forEach( result, ( entry ) => {
154
+ this.#serverFeatures[ entry[ 1 ] ] = entry[ 3 ];
155
+ } );
156
+ }
157
+ } ).catch( ( error ) => {
158
+ logger.log( `Failed to fetch server information by client '${ this.identifier }'!`, logger.logSeverity.WARNING, error );
159
+ } );
111
160
  } );
112
161
  this.#redisClient.on( "error", ( error ) => {
113
162
  logger.log( `Error received in Redis client '${ this.identifier }'.`, logger.logSeverity.ERROR, error );
@@ -137,6 +186,28 @@ class RedisClient {
137
186
  return this.#clientIdentifier;
138
187
  }
139
188
 
189
+ /**
190
+ * Used to return the Redis server version.
191
+ *
192
+ * @property
193
+ * @return {number}
194
+ * @public
195
+ */
196
+ get serverVersion() {
197
+ return this.#serverInfo[ "redis_version" ];
198
+ }
199
+
200
+ /**
201
+ * Verify if Redis server supports JSON data types.
202
+ *
203
+ * @property
204
+ * @returns {boolean}
205
+ * @public
206
+ */
207
+ get isJSONSupported() {
208
+ return !_.isNil( this.#serverFeatures[ "ReJSON" ] );
209
+ }
210
+
140
211
  /**
141
212
  * Used to register a new {@link ConnectionObserver} for events related to the Redis connection state.
142
213
  *
@@ -172,6 +243,7 @@ class RedisClient {
172
243
 
173
244
  /**
174
245
  * Used to send a new blocking command to Redis.
246
+ * <br/>
175
247
  * WARNING: This will reserve the client connection until a result is received.
176
248
  *
177
249
  * @method
@@ -235,6 +307,28 @@ class RedisClient {
235
307
  } );
236
308
  }
237
309
 
310
+ /**
311
+ * Used to execute any Redis command in an unmanaged way.
312
+ * <br/>
313
+ * WARNING: Use this only if there is no other implemented function in this module and the command
314
+ * you want to execute is not supported by the 'multi' Redis command (implemented in {@link RedisClient.executeCommands}).
315
+ * Make sure to handle the result as it will be returned raw.
316
+ *
317
+ * @method
318
+ * @param {string[]} commandArguments
319
+ * @returns {Promise<Object>}
320
+ * @public
321
+ */
322
+ callCommand( commandArguments ) {
323
+ return new Promise( ( resolve, reject ) => {
324
+ this.#redisClient[ "call" ].apply( this.#redisClient, commandArguments ).then( ( result ) => {
325
+ resolve( result );
326
+ } ).catch( ( error ) => {
327
+ reject( exceptions.raise( error ) );
328
+ } );
329
+ } );
330
+ }
331
+
238
332
  }
239
333
 
240
334
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ti-engine/core",
3
- "version": "1.0.14",
3
+ "version": "1.1.0",
4
4
  "description": "The ti-engine is an open source, free to use - both for personal and commercial projects - framework for the creation of microservice-based solutions using node.js.",
5
5
  "author": "Boris Kostadinov <kostadinov.boris@gmail.com>",
6
6
  "license": "ISC",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "optionalDependencies": {
52
52
  "@google-cloud/error-reporting": "^3.0.5",
53
- "zeromq": "^6.0.0-beta.17"
53
+ "zeromq": "^6.0.0-beta.19"
54
54
  },
55
55
  "repository": {
56
56
  "type": "git",
package/utils/cache.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -278,6 +278,32 @@ class CommonMemoryCache extends ConnectionObserver {
278
278
  } );
279
279
  }
280
280
 
281
+ /**
282
+ * Used to set expiration in seconds to an existing key.
283
+ * <br/>
284
+ * NOTE: For performance optimization reasons, only use this only if the Redis command does not itself support the 'EX' argument.
285
+ *
286
+ * @method
287
+ * @param {string} key
288
+ * @param {number} seconds
289
+ * @returns {Promise<number>} This will resolve with the seconds as provided initially by the caller.
290
+ * @public
291
+ */
292
+ expireValue( key, seconds ) {
293
+ return new Promise( ( resolve, reject ) => {
294
+ if ( this.#isOperational === true ) {
295
+ let commandExpire = [ redis.cacheCommands.EXPIRE, key, seconds ];
296
+ this.#redisClient.executeCommands( [ commandExpire ] ).then( () => {
297
+ resolve( seconds );
298
+ } ).catch( ( error ) => {
299
+ reject( error );
300
+ } );
301
+ } else {
302
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
303
+ }
304
+ } );
305
+ }
306
+
281
307
  /**
282
308
  * Used to add the specified values to a list.
283
309
  *
@@ -313,7 +339,7 @@ class CommonMemoryCache extends ConnectionObserver {
313
339
  *
314
340
  * @method
315
341
  * @param {string} key
316
- * @param {string} value
342
+ * @param {string|Object} value
317
343
  * @returns {Promise}
318
344
  * @public
319
345
  */
@@ -516,6 +542,104 @@ class CommonMemoryCache extends ConnectionObserver {
516
542
  }
517
543
  } );
518
544
  }
545
+
546
+ /**
547
+ * Used to store a JSON variable.
548
+ * <br/>
549
+ * NOTE: Requires ReJSON module installed on server to work.
550
+ *
551
+ * @method
552
+ * @param {string} key
553
+ * @param {Object} value
554
+ * @param {string} [path='&']
555
+ * @param {number} [overrideMode=0] By default this allows full override for existing keys.
556
+ * Option 1 will set the key only if it doesn't already exist. Option 2 will set it only if it already exists.
557
+ * @returns {Promise}
558
+ * @public
559
+ */
560
+ setJSON( key, value, path = "$", overrideMode = 0 ) {
561
+ return new Promise( ( resolve, reject ) => {
562
+ if ( this.#isOperational === true ) {
563
+ if ( this.#redisClient.isJSONSupported ) {
564
+ let commandArguments = [ redis.cacheCommands.JSON_SET, key, path, tools.stringifyJSON( value ) ];
565
+ if ( overrideMode !== 0 ) {
566
+ commandArguments.push( overrideMode === 1 ? redis.cacheOverrideMode.NX : redis.cacheOverrideMode.XX );
567
+ }
568
+ this.#redisClient.callCommand( commandArguments ).then( () => {
569
+ resolve();
570
+ } ).catch( ( error ) => {
571
+ reject( error );
572
+ } );
573
+ } else {
574
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED ), { details: "No RedisJSON module installed on server." } );
575
+ }
576
+ } else {
577
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
578
+ }
579
+ } );
580
+ }
581
+
582
+ /**
583
+ * Used to fetch a JSON variable.
584
+ * <br/>
585
+ * NOTE: Requires ReJSON module installed on server to work.
586
+ *
587
+ * @method
588
+ * @param {string} key
589
+ * @param {string} path
590
+ * @returns {Promise<Object>}
591
+ * @public
592
+ */
593
+ getJSON( key, path = "$" ) {
594
+ return new Promise( ( resolve, reject ) => {
595
+ if ( this.#isOperational === true ) {
596
+ if ( this.#redisClient.isJSONSupported ) {
597
+ let commandArguments = [ redis.cacheCommands.JSON_GET, key, path ];
598
+ this.#redisClient.callCommand( commandArguments ).then( ( result ) => {
599
+ resolve( tools.parseJSON( result ) );
600
+ } ).catch( ( error ) => {
601
+ reject( error );
602
+ } );
603
+ } else {
604
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED ), { details: "No RedisJSON module installed on server." } );
605
+ }
606
+ } else {
607
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
608
+ }
609
+ } );
610
+ }
611
+
612
+ /**
613
+ * Used to add an item to a JSON array. That array needs to exist already.
614
+ * <br/>
615
+ * NOTE: Requires ReJSON module installed on server to work.
616
+ *
617
+ * @method
618
+ * @param {string} key
619
+ * @param {Object} value
620
+ * @param {string} path
621
+ * @returns {Promise}
622
+ * @public
623
+ */
624
+ arrayAppendJSON( key, value, path = "$" ) {
625
+ return new Promise( ( resolve, reject ) => {
626
+ if ( this.#isOperational === true ) {
627
+ if ( this.#redisClient.isJSONSupported ) {
628
+ let commandArguments = [ redis.cacheCommands.JSON_ARRAY_APPEND, key, path, tools.stringifyJSON( value ) ];
629
+ this.#redisClient.callCommand( commandArguments ).then( () => {
630
+ resolve();
631
+ } ).catch( ( error ) => {
632
+ reject( error );
633
+ } );
634
+ } else {
635
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_FEATURE_UNSUPPORTED ), { details: "No RedisJSON module installed on server." } );
636
+ }
637
+ } else {
638
+ reject( exceptions.raise( exceptions.exceptionCode.E_GEN_SYSTEM_CACHE_UNAVAILABLE ) );
639
+ }
640
+ } );
641
+ }
642
+
519
643
  }
520
644
 
521
645
  const instance = new CommonMemoryCache();
package/utils/config.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -27,6 +27,7 @@ const tools = require( "#tools" );
27
27
  * @property {EnvironmentVariable} env.TI_MEMORY_CACHE_REDIS_DB
28
28
  * @property {EnvironmentVariable} env.TI_MEMORY_CACHE_REDIS_HOST
29
29
  * @property {EnvironmentVariable} env.TI_MEMORY_CACHE_REDIS_PORT
30
+ * @property {EnvironmentVariable} env.TI_MEMORY_CACHE_USER
30
31
  * @property {EnvironmentVariable} env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED
31
32
  * @property {EnvironmentVariable} env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_KEY
32
33
  * @property {EnvironmentVariable} env.TI_MESSAGE_EXCHANGE_TRACE_LOG_ENABLED
@@ -76,7 +77,9 @@ const tools = require( "#tools" );
76
77
  * @property {string} messageStore
77
78
  * @property {boolean} securityHashEnabled
78
79
  * @property {string} securityHashKey
80
+ * @property {number} traceExpirationTime
79
81
  * @property {boolean} traceLogEnabled
82
+ * @property {string} traceRepository
80
83
  */
81
84
 
82
85
  /**
@@ -110,7 +113,9 @@ let settingsEnum = tools.enum( {
110
113
  MESSAGE_EXCHANGE_MESSAGE_STORE: [ "messageExchange.messageStore", "messageStore", "" ],
111
114
  MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED: [ "messageExchange.securityHashEnabled", "securityHashEnabled", "" ],
112
115
  MESSAGE_EXCHANGE_SECURITY_HASH_KEY: [ "messageExchange.securityHashKey", "securityHashKey", "" ],
116
+ MESSAGE_EXCHANGE_TRACE_EXPIRATION_TIME: [ "messageExchange.traceExpirationTime", "traceExpirationTime", "" ],
113
117
  MESSAGE_EXCHANGE_TRACE_LOG_ENABLED: [ "messageExchange.traceLogEnabled", "traceLogEnabled", "" ],
118
+ MESSAGE_EXCHANGE_TRACE_REPOSITORY: [ "messageExchange.traceRepository", "traceRepository", "" ],
114
119
  SERVICE_EXECUTION_TIMEOUT: [ "serviceConfig.executionTimeout", "executionTimeout", "" ],
115
120
  SERVICE_HEALTH_CHECK_ADDRESS: [ "serviceConfig.healthCheckAddress", "healthCheckAddress", "" ],
116
121
  SERVICE_HEALTH_CHECK_INTERVAL: [ "serviceConfig.healthCheckInterval", "healthCheckInterval", "" ],
@@ -138,7 +143,7 @@ if ( settings.memoryCache ) {
138
143
  settings.memoryCache.redisDB = ( process.env.TI_MEMORY_CACHE_REDIS_DB !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_DB : settings.memoryCache.redisDB;
139
144
  settings.memoryCache.redisHost = ( process.env.TI_MEMORY_CACHE_REDIS_HOST !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_HOST : settings.memoryCache.redisHost;
140
145
  settings.memoryCache.redisPort = ( process.env.TI_MEMORY_CACHE_REDIS_PORT !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_PORT : settings.memoryCache.redisPort;
141
- settings.memoryCache.user = ( process.env.MEMORY_CACHE_USER !== undefined ) ? process.env.MEMORY_CACHE_USER : settings.memoryCache.user;
146
+ settings.memoryCache.user = ( process.env.TI_MEMORY_CACHE_USER !== undefined ) ? process.env.TI_MEMORY_CACHE_USER : settings.memoryCache.user;
142
147
  }
143
148
  if ( settings.messageExchange ) {
144
149
  settings.messageExchange.securityHashEnabled = ( process.env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED !== undefined ) ? tools.toBool( process.env.TI_MESSAGE_EXCHANGE_SECURITY_HASH_ENABLED ) : settings.messageExchange.securityHashEnabled;
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
@@ -21,6 +21,7 @@ let exceptionCodeEnum = tools.enum( {
21
21
  E_GEN_INVALID_SERVICE_DOMAIN_NAME: [ 1003, "invalid service domain name", "Invalid or no service domain name provided at microservice startup." ],
22
22
  E_GEN_SYSTEM_CACHE_UNAVAILABLE: [ 1004, "system cache unavailable", "The system cache required for proper engine operation is unavailable." ],
23
23
  E_GEN_BAD_SERVICE_HANDLER: [ 1005, "bad service handler", "The provided service handler is not a proper function." ],
24
+ E_GEN_FEATURE_UNSUPPORTED: [ 1006, "feature unsupported", "The requested feature is not supported by current configuration or version." ],
24
25
  /** Security & Administration related exceptions - codes under 2xxx */
25
26
  E_SEC_INVALID_AUTH_TOKEN: [ 2000, "invalid auth token", "Invalid authorization token provided." ],
26
27
  E_SEC_INVALID_EXPIRED_SESSION: [ 2001, "invalid or expired session", "Invalid or expired session encountered." ],
package/utils/logger.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5
 
package/utils/tools.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * SPDX-FileCopyrightText: © 2021 Boris Kostadinov <kostadinov.boris@gmail.com>
2
+ * SPDX-FileCopyrightText: © 2021-2023 Boris Kostadinov <kostadinov.boris@gmail.com>
3
3
  * SPDX-License-Identifier: ICU
4
4
  */
5
5