@ti-engine/core 1.3.5 → 1.3.8

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
@@ -2,6 +2,31 @@
2
2
 
3
3
  This document will contain the list of changes made to the framework. The format is based on the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
4
4
 
5
+ ## Version 1.3.8
6
+ * fix(tester): fix the relative paths of various files in the tester service. The configuration now assumes that the working directory is the root of the tester package instead of the repository itself
7
+ * build(npm): update npm dependencies to their latest versions
8
+ * docs: fix some outdated information in the `README.md` file and add some more details
9
+
10
+ ## Version 1.3.7
11
+ * feat(exceptions): add new exception code `E_SEC_UNRECOGNIZED_AUTH_METHOD`
12
+ * fix(config): fix potential issue with `TI_GCLOUD_ENABLED` parsing as boolean
13
+ * docs: adjust some of the JSDoc descriptions and definitions
14
+
15
+ ## Version 1.3.6
16
+ * feat(exceptions): add new exception code `E_GEN_INVALID_ARGUMENT_TYPE`
17
+ * feat(exceptions): add public enum with all HTTP codes exported as `httpCode` and typedef `TiHttpCode`. Use it as the type for the `httpCode` exception property
18
+ * feat(tools): add `description` property to enum objects
19
+ * feat(tools): add `contains` property to enum objects
20
+ * feat(tools)!: change enum factory behavior to create a copy of the seed object instead of modifying it (may break code relying on seed object mutation)
21
+ * refactor(exceptions): invalid `httpCode` inputs are coerced to `undefined`; `Exception.asJSON` includes `httpCode` only when defined
22
+ * refactor(tools): deprecate method `getEnumName` in favor of `enum.name`; removal planned for 1.4.0
23
+ * refactor(tools)!: remove method `createCSVFile` as it is unnecessary for the framework's operation. It also eliminates the dependency from `fs-extra` package
24
+ * refactor(tools)!: disallow reserved keys as enum names; make enum properties non-enumerable and freeze the enum object to enforce immutability (may affect code relying on enumeration/extension)
25
+ * fix(tools): fix a bug in the `RetryPolicy` class that caused the `maxAttempts` property to be ignored
26
+ * build(npm): update npm dependencies to their latest versions
27
+ * build(npm): remove `fs-extra` package as it is not used by the framework
28
+ * build(npm)!: bump the minimum supported Node.js version to 18.0.0
29
+
5
30
  ## Version 1.3.5
6
31
  * fix(redis integration): fix a duplicated log entry on connection ready event if multiple observers are registered
7
32
  * feat(service caller)!: change log level of error result in `process` method from `ERROR` to `DEBUG`. Implementers are expected to handle this and decide if the error should be propagated further or not
package/README.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # ti-engine core
2
2
 
3
+ ![GitHub top language](https://img.shields.io/github/languages/top/Belleal/ti-engine)
4
+ ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/Belleal/ti-engine)
5
+ ![npms.io (scoped package)](https://img.shields.io/npms-io/maintenance-score/%40ti-engine/core)
6
+ ![npms.io (scoped package)](https://img.shields.io/npms-io/popularity-score/%40ti-engine/core)
7
+ ![npms.io (scoped package)](https://img.shields.io/npms-io/quality-score/%40ti-engine/core)
8
+
9
+ ![Logo](https://github.com/Belleal/ti-engine/blob/master/packages/core/docs/ti-engine-icon.ico)
10
+
3
11
  Flexible framework for the creation of microservices with [node.js](https://nodejs.org/).
4
12
 
5
13
  ## Introduction
@@ -27,15 +35,10 @@ Being a messaging system, the **ti-engine** relies on a message broker for the a
27
35
 
28
36
  To run the basic **ti-engine** framework, you will need a couple of things:
29
37
 
30
- * A local [node.js installation](https://nodejs.org/en/download/) with a minimum version of **14.17.0**
38
+ * A local [node.js installation](https://nodejs.org/en/download/) with a minimum version of **18.0.0**
31
39
  * A local or remote [Redis cache installation](https://redis.io/download) with a minimum version of **5.0.14**
32
40
 
33
- If you are working under Windows 10+ OS and you need to install Redis, take a look at this [guide](https://redis.com/blog/redis-on-windows-10/). You could also use [Redis Cloud](https://app.redislabs.com/) for development as it offers a free basic account. You can configure your connection to a remote Redis server using the following ENV variables:
34
-
35
- * `TI_MEMORY_CACHE_AUTH_KEY` can be used to provide the Redis password if there is any at all.
36
- * `TI_MEMORY_CACHE_REDIS_DB` can be used to specify the Redis DB you want to use. Make sure to set the correct number as, for example, Redis Cloud only uses DB `0`.
37
- * `TI_MEMORY_CACHE_REDIS_HOST` can be used to provide the remote host. This can be an IP or URL depending on your setup.
38
- * `TI_MEMORY_CACHE_REDIS_PORT` can be used to provide the remote port. By default, Redis uses `6379` however many implementations might use a custom port that needs to be specified in the connection settings.
41
+ If you are working under Windows 10+ OS and you need to install Redis, take a look at this [guide](https://redis.com/blog/redis-on-windows-10/). You could also use [Redis Cloud](https://app.redislabs.com/) for development as it offers a free basic account.
39
42
 
40
43
  To get the framework itself, use the command `npm install @ti-engine/core`. And to include it directly in your package.json dependencies execute `npm install @ti-engine/core --save-prod`.
41
44
 
@@ -44,13 +47,15 @@ To get the framework itself, use the command `npm install @ti-engine/core`. And
44
47
  To start using the **ti-engine**, you will have to make sure that all prerequisites are available and operational. However, before we get to the fun part, you need to also consider a couple of crucial things while working with this framework:
45
48
 
46
49
  1. The runtime configuration of the framework can be customized using ENV variables. These can be provided to node.js in all the standard ways, but there is also an option to include an `.env` file.
47
- 2. It loads your framework-related custom scripts and files dynamically, but it always assumes their provided paths are relative to the _current working directory_ (i.e., it uses `process.cwd()` to resolve the file paths). Be mindful of that whenever you declare file paths in the various settings.
50
+ 2. It loads your framework-related custom scripts and files dynamically, but it always assumes their provided paths are relative to the _current working directory_ (i.e., it uses `process.cwd()` to resolve the file paths). Be mindful of that whenever you declare relative file paths in the various settings.
51
+
52
+ ### Running the tester module
48
53
 
49
54
  Once you have everything else ready, you should download the **ti-engine** tester module with the command `npm install @ti-engine/tester`. The tester module packages an example microservice that shows the basic approach for using the framework. To make sure everything is working properly, you should try and start the tester service:
50
55
 
51
56
  1. Open a command prompt and navigate to the directory of the tester module; it should be something like that:
52
57
  `[path to your project]/node_modules/@ti-engine/tester`
53
- 2. Execute the following command `node ../core/bin/start-instance.js`. Keep in mind that the working directory for the node process has to be the one specified in point 1. Otherwise, you'll get errors that certain files cannot be found and loaded.
58
+ 2. Execute the following command `node ../core/bin/start-instance.js`. Keep in mind that the working directory for the node process has to be the one specified in point 1. Otherwise, you'll get errors that certain files cannot be found and loaded. Also, this configuration assumes that you have an unprotected local Redis server running on the default port. If you have a different setup, you can provide the host and port via ENV variables. We'll cover that in the section [Configuring for a remote Redis server](#configuring-for-a-remote-redis-server).
54
59
  3. If everything was done properly, you should see the following output:
55
60
 
56
61
  ```text
@@ -80,20 +85,21 @@ At the start of the output log, you can see a `NOTICE` that tells you a couple o
80
85
  * The instance name - in this case `ti-tester-service`. In the terminology of the framework, this is also known as a _service domain name_.
81
86
  * The _instance identifier_. It is an uuid string with a `ti-` prefix, that is generated by the framework at process start. It can and will be used to trace the messages during their movement through the microservice ecosystem. But more on that later.
82
87
 
83
- The following five `INFO` lines inform you about the successful connections to Redis. The default configuration assumes that your Redis is running on localhost and uses the default port. If you have a different setup, you can provide the host and port via ENV variables. We'll cover that in the section [Using the framework](#using-the-framework).
88
+ The following five `INFO` lines inform you about the successful connections to Redis. Remember, the default configuration assumes that your Redis is running on localhost, requires no password, and uses the default port.
84
89
 
85
90
  Following that come a couple of `INFO` lines that inform you about the microservice interface state. The framework starts with the process of _business services_ registration within the service domain of the microservice `ti-tester-service` and successfully adds two such services. The necessary information for this is read from a JSON config file included in the package. We'll get into more details on what this all means in the section [Creating a microservice](#creating-a-microservice).
86
91
 
87
92
  Once the initialization sequence has completed, the framework informs you that the microservice instance has started successfully with a `NOTICE` entry.
88
93
 
89
- If the framework encountered an error during initialization instead, you would see something like this instead:
94
+ NOTE: If the framework encountered an error during initialization, you would see something like this instead:
90
95
 
91
96
  ```text
92
97
  [timestamp]: [instance-id] - notice - Starting new instance of type 'ti-tester-service' with instance ID '[instance-id]'.
93
98
  [timestamp]: [instance-id] - alert - Error detected in the instance startup script!
99
+ » [information about the error]
94
100
  ```
95
101
 
96
- Finally, you should see a sequence of test execution statements with their results in JSON format. These are the results of three business service calls that are part of the default tester microservice. The final `NOTICE` should indicate that all three tests have been completed successfully.
102
+ Finally, you should see a sequence of test execution statements with their results in JSON format. These are the results of three business service calls that are part of the default tester microservice. The final `NOTICE` should indicate that all three tests out of three have been completed successfully.
97
103
 
98
104
  You can now kill the node process which should show you the following two lines:
99
105
 
@@ -108,6 +114,18 @@ The tester module gets its starting configuration from an `.env` file included i
108
114
 
109
115
  Before moving on, also take a good look at the file `bin/start-instance.js`. It should give you an idea of how the process of starting and stopping a microservice operates. In most cases this file should be enough as a starting script for your **ti-engine** based microservice applications. You can, of course, create your own starting script, but then you'll have to consider all necessary steps to properly handle the microservice instance.
110
116
 
117
+ ### Configuring for a remote Redis server
118
+
119
+ You can configure your connection to a remote Redis server using the following ENV variables:
120
+
121
+ * `TI_MEMORY_CACHE_AUTH_KEY` can be used to provide the Redis password if there is any at all.
122
+ * `TI_MEMORY_CACHE_REDIS_DB` can be used to specify the Redis DB you want to use. Make sure to set the correct number as, for example, Redis Cloud only uses DB `0`.
123
+ * `TI_MEMORY_CACHE_REDIS_HOST` can be used to provide the remote host. This can be an IP or URL depending on your setup.
124
+ * `TI_MEMORY_CACHE_REDIS_PORT` can be used to provide the remote port. By default, Redis uses `6379` however many implementations might use a custom port that needs to be specified in the connection settings.
125
+ * `TI_MEMORY_CACHE_USER` can be used to specify the Redis username if this is supported by your Redis implementation.
126
+
127
+ The easiest way to configure these variables is to edit the `.env` file included in the package. You might have to do that if you want to run the tester module successfully with a remote Redis server.
128
+
111
129
  ## Architecture
112
130
 
113
131
  The architectural approach for the **ti-engine** is done in _tiers_ with lower tiers being unaware of the tiers above them. The framework prefers a high level of abstraction in all its tiers and provides many options for customization and extension. While the language is JavaScript, the structuring of the framework follows the OOP principles, and you will find a lot of abstract classes and methods that require you to implement them. These are always marked with the `@abstract` annotation but if you happen to miss one, the framework will raise an `E_GEN_ABSTRACT_METHOD_CALL` exception when you try to use it in your solution.
@@ -132,7 +150,7 @@ For now, take a look at the following diagram:
132
150
 
133
151
  It shows the standard flow of a message exchange between one sender and _n_ identical message receivers. The sender splits each message into an _envelope_ and a _payload_, then stores the payload in the shared cache and enqueues the envelope in the requests (destination) queue. Receivers can subscribe to that queue to fetch enqueued messages and process their contents. During the fetch sequence a receiver assembles the full message by getting the payload from the storage. This process is depicted by the blue flow lines.
134
152
 
135
- After the processing is done, the message payload is modified, and the receiver sends the message back to the original sender using the same mechanism. It again splits the message into an envelope and a payload, stores the payload in the storage and enqueues the envelope in the sender response (source) queue. The sender will then assemble the message back and process the contained results. This process is depicted by the red flow lines.
153
+ After the processing is done, the message payload is modified, and the receiver sends the message back to the original sender using the same mechanism. It again splits the message into an envelope and a payload, stores the payload in the storage, and enqueues the envelope in the sender response (source) queue. The sender will then assemble the message back and process the contained results. This process is depicted by the red flow lines.
136
154
 
137
155
  In this scenario the framework uses _Redis lists_ as queues for the message envelopes and _Redis hash_ as message payload storage. The splitting between envelope and payload is done to avoid unnecessary transportation of potentially large volumes of operational data between the microservices. Other message brokers might use a slightly different approach, but they should still adhere to the same logical flow.
138
156
 
@@ -163,15 +181,15 @@ This tier comprises the actual implementation of your application. Its structure
163
181
  * It needs to take care of any type of stateful behavior like user sessions or transactions.
164
182
  * It needs to act as the primary interface between users and your application, thus handling access management and user interactions.
165
183
 
166
- Depending on the type of software you are building, tier 3 can be an API Gateway, a Web application, backend for a Mobile application or anything like that.
184
+ Depending on the type of software you are building, tier 3 can be an API Gateway, a Web application, backend for a Mobile application, or anything like that.
167
185
 
168
186
  ## Creating a microservice
169
187
 
170
188
  Now let's walk through the process of creating a microservice with **ti-engine**. We'll start with analyzing the contents of the tester module. Then we'll proceed with creating a new microservice that can call one of the business services in the default `ti-tester-service`.
171
189
 
172
- ### The ti-tester microservice
190
+ ### Deconstructing the ti-tester microservice
173
191
 
174
- If you managed to execute the initial framework test as explained in the [Getting started](#getting-started) section, you should already be familiar with the default tester microservice. Here we'll dissect its contents even further.
192
+ If you managed to execute the initial framework test as explained in the [Running the tester module](#running-the-tester-module) section, you should already be familiar with the default ti-tester microservice. Here we'll dissect its contents even further.
175
193
 
176
194
  Let's take a look at the files and file structure first (only relevant items are shown):
177
195
 
@@ -181,6 +199,7 @@ bin
181
199
  ↳ v1
182
200
  ↳ service1.js
183
201
  ↳ service2.js
202
+ ↳ more-labels.json
184
203
  ↳ tester-service.js
185
204
  ↳ tester-service.json
186
205
  .env
@@ -189,43 +208,45 @@ package.json
189
208
 
190
209
  You don't have to follow the exact same folder structure as most of the paths can be defined via ENV parameters and in the configuration file. However, having a good clean structure helps when organizing your work in more complex projects.
191
210
 
192
- In the default tester microservice all application files are located inside the `bin` folder. Outside you have only the `package.json` and the `.env` files which can be considered more of a configuration for the node process rather than part of the application itself. Nevertheless, let's start with them:
211
+ In the tester module all application files are located inside the `bin` folder. Outside you have only the `package.json` and the `.env` files which can be considered more of a configuration for the node process rather than part of the application itself. Nevertheless, let's start with them:
193
212
 
194
213
  #### package.json contents
195
214
 
196
215
  ```json
197
216
  {
198
217
  "name": "@ti-engine/tester",
199
- "version": "...",
200
- "description": "...",
201
- "author": "...",
202
- "license": "ISC",
218
+ "version": "[tester module version]",
219
+ "description": "[tester module description]",
220
+ "author": "[tester module author]",
221
+ "license": "GPL-3.0-or-later",
203
222
  "dependencies": {
204
223
  "@ti-engine/core": "latest"
205
224
  },
206
225
  "engines": {
207
- "node": ">=14.17.0"
226
+ "node": "[min required node version]"
208
227
  }
209
228
  }
210
229
  ```
211
230
 
212
- Apart from the standard information properties there are only two important entries here: `"@ti-engine/core": "latest"` and `"node": ">=14.17.0"`. The dependency on the core of the framework is set to `latest`, but as with any other npm library you should set this to a specific version when releasing on production. The minimum node version should also reflect the minimum requirements of your application and can be adjusted accordingly, but it should not go below the minimum version required by **ti-engine**.
231
+ Apart from the standard information properties there are only two important entries here: `"@ti-engine/core": "[min required core version]"` and `"node": "[min required node version]"`. The dependency on the core of the framework is set to `latest`, but as with any other npm library you should set this to a specific version when releasing on production. The minimum node version should also reflect the minimum requirements of your application and can be adjusted accordingly, but it should not go below the minimum version required by the **ti-engine** itself.
213
232
 
214
233
  #### .env contents
215
234
 
216
235
  ```text
236
+ TI_INSTANCE_NAME=ti-tester-service
217
237
  TI_INSTANCE_CLASS=bin/tester-service.js
218
238
  TI_INSTANCE_CONFIG=bin/tester-service.json
219
- TI_INSTANCE_NAME=tester-service
220
239
  TI_AUDITING_LOG_MIN_LEVEL=200
240
+ TI_LOCALIZATION_LABELS_PATH=bin/more-labels.json
221
241
  ```
222
242
 
223
- The ENV initialization file provides the minimal settings for the proper tester microservice operation. The first three are usually _mandatory_ for every microservice you create, while the last is provided for the needs of the tester demonstration. Let's review them and see what they do:
243
+ The ENV initialization file provides the minimal settings for the proper tester microservice operation. The first three are usually _mandatory_ for every microservice you create, while the last two are provided for the needs of the tester demonstration. Let's review them and see what they do:
224
244
 
225
- * `TI_INSTANCE_CLASS` specifies the relative path to the implementation of the `ServiceInstance` framework class—in this case a `ServiceProvider`. As stated above, the path is relative to the working directory of the `node` process. This variable is mandatory for every microservice you create with the **ti-engine**. If it is not provided, the microservice won't be able to start at all, and you will get an exception.
226
- * `TI_INSTANCE_CONFIG` specifies the relative path to the configuration data for the microservice. We'll delve into the specific settings below. Technically, you can omit this variable, and the microservice will still start successfully with an empty configuration. There are very few cases, however, where this would be applicable.
227
245
  * `TI_INSTANCE_NAME` is the _service domain_ name provided for the microservice. It has to be _unique_ in the context of the microservice ecosystem. If not provided, the framework will attempt to extract this information from the name of the implementation file. That is not a recommended approach, though, as it might cause hard to identify errors later.
228
- * `TI_AUDITING_LOG_MIN_LEVEL` specifies the minimum log level that should be sent to the log output stream. With a setting of `200` (corresponding to INFO) we filter out all `DEFAULT(0)` and `DEBUG(100)` entries as we don't need them for the tester microservice.
246
+ * `TI_INSTANCE_CLASS` specifies the relative path to the implementation of the `ServiceInstance` framework class—in this case a `ServiceProvider`. As stated above, the path is relative to the working directory of the `node.js` process. This variable is mandatory for every microservice you create with the **ti-engine**. If it is not provided, the microservice won't be able to start at all, and you will get an exception.
247
+ * `TI_INSTANCE_CONFIG` specifies the relative path to the configuration data for the microservice. We'll delve into the specific settings below. Technically, you can omit this variable, and the microservice will still start successfully with an empty configuration. There are very few cases, however, where this would be applicable.
248
+ * `TI_AUDITING_LOG_MIN_LEVEL` specifies the minimum log level that should be sent to the log output stream. With a setting of `200` (corresponding to `INFO`) we filter out all `DEFAULT (0)` and `DEBUG (100)` entries as we don't need them for the tester microservice.
249
+ * `TI_LOCALIZATION_LABELS_PATH` specifies the relative path to the additional localization labels file. This is optional and can be omitted if you don't need custom labels for your microservice.
229
250
 
230
251
  You can find the full list of available ENV variables and what they do below in [Using the framework](#using-the-framework) section.
231
252
 
@@ -236,10 +257,13 @@ Now let's look inside the `bin` folder. The two files there are the ones specifi
236
257
  * Method `onStart` overrides the base one from the parent class and is invoked automatically by the framework once initialization of the microservice is complete. In this case the method invokes the execution of the test sequence just once, and then the microservice remains dormant but active.
237
258
  * Method `reportHealthy` overrides but essentially just calls the same base method. Its only purpose here is to draw your attention to its existence and the possibility to implement your own health status reporting functionality if you want.
238
259
  * Method `verifyAccess` also overrides the base method and shows a very basic example of how to implement user access verification on business service level. Each time a service in the `ti-tester-service` is called, the framework will trigger this method and will only allow processing if there is a non-undefined value inside the `authToken` variable.
239
- * Method `#executeTests` is a custom private method that contains the test sequence itself. It is called by the `onStart` method just once per microservice start. Inside you can see two examples of calling a business service—in both cases the tester microservice is calling itself. In a more practical situation, however, these calls would be directed towards other service domains.
260
+ * Method `#executeTests` is a custom private method that contains the test execution sequence itself. It is called by the `onStart` method just once per microservice start.
261
+ * Method `#assertService` is an assertion wrapper around a standard service call request. Inside it, you can see how a business service is invoked and how the results should be handled.
240
262
 
241
263
  The file `tester-service.json` contains framework configuration for the tester microservice. It will be automatically loaded inside the `ServiceInstance` class during initialization and will already be available inside the `onStart` method for usage. In this case the configuration is related to the two business services that will be provided by the microservice. More on this topic will be covered in the section [Using the framework](#using-the-framework). For now pay attention to the `serviceFile` parameter and that it once again provides a relative path to the actual file containing the business logic.
242
264
 
265
+ The file `more-labels.json` contains one additional localization label for the tester microservice and is only used to demonstrate how to add custom labels to the microservice. For more information on localization, see the section [Localization](#localization).
266
+
243
267
  The final two files are located in `bin/services/v1/` folder. They contain the definitions and business logic of the two business services that will be loaded at initialization time and provided by the tester microservice. In this case `service1.js` contains a basic service that returns the current timestamp. The `service2.js` file contains a slightly more complex example of a service calling another service (in this case `service1`) before also returning two timestamps taken at the beginning and end of execution. Pay attention to the way the methods inside are declared and exported as this is the proper way to do this while using the **ti-engine** framework. Once again, we'll delve into the details and specifics of creating business services in the section [Using the framework](#using-the-framework).
244
268
 
245
269
  ### Creating your own microservice
@@ -247,9 +271,9 @@ The final two files are located in `bin/services/v1/` folder. They contain the d
247
271
  Now that we've seen the structure of the tester microservice, let's create a new one and make it call `service2`. Let's use the same file structure as for the tester. Create a folder `my-service` and in it create a `package.json` file. Make sure to include a dependency to `"@ti-engine/core": "latest"` in it. After that create a `.env` file and add the following entries in it:
248
272
 
249
273
  ```text
274
+ TI_INSTANCE_NAME=my-service
250
275
  TI_INSTANCE_CLASS=bin/my-service.js
251
276
  TI_INSTANCE_CONFIG=bin/my-service.json
252
- TI_INSTANCE_NAME=my-service
253
277
  TI_AUDITING_LOG_MIN_LEVEL=200
254
278
  ```
255
279
 
@@ -320,9 +344,9 @@ Now let's start the new microservice with `node .\node_modules\@ti-engine\core\b
320
344
 
321
345
  ```text
322
346
  [timestamp]: [instance-id] - notice - Starting new instance of type 'my-service' with instance ID '[instance-id]'.
323
- [timestamp]: [instance-id] - info - Connection to Redis server 127.0.0.1:6379 (re)established by client 'connection-msg-responses-in' and is ready to be used.
324
- [timestamp]: [instance-id] - info - Connection to Redis server 127.0.0.1:6379 (re)established by client 'connection-msg-requests-out' and is ready to be used.
325
- [timestamp]: [instance-id] - info - Connection to Redis server 127.0.0.1:6379 (re)established by client 'system-cache' and is ready to be used.
347
+ [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'system-cache' and is ready to be used.
348
+ [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-responses-in' and is ready to be used.
349
+ [timestamp]: [instance-id] - info - Connection to Redis server '127.0.0.1:6379' established by client 'connection-msg-requests-out' and is ready to be used.
326
350
  ```
327
351
 
328
352
  If you haven't started anything else, this is all you should see at this point.
@@ -331,10 +355,10 @@ Now without exiting this node process, let's start the original tester microserv
331
355
 
332
356
  ```text
333
357
  ...
334
- [timestamp]: [instance-id] - notice - Execution of service2 result:
335
- » {"isSuccessful":true,"payload":{"s1Timestamp":[timestamp],"s2TimestampStart":[timestamp],"s2TimestampEnd":[timestamp]}}
336
358
  [timestamp]: [instance-id] - notice - Instance [instance-id] started successfully.
337
359
  » {"nodeVersion":[node-version]}
360
+ [timestamp]: [instance-id] - notice - Execution of service2 result:
361
+ » {"isSuccessful":true,"payload":{"s1Timestamp":[timestamp],"s2TimestampStart":[timestamp],"s2TimestampEnd":[timestamp]}}
338
362
  ```
339
363
 
340
364
  This means the service call processing was successful and a result was returned to `my-service`. Because we made the receiving of the result blocking and part of the initialization sequence, the new microservice did not report successful startup until it received that response from `ti-tester-service`.
@@ -1,99 +1,105 @@
1
- {
2
- "system": {
3
- "exceptions": {
4
- "0": {
5
- "en": "Unidentified error encountered or unrecognized exception code provided."
6
- },
7
- "1000": {
8
- "en": "Error thrown by internal JS source."
9
- },
10
- "1001": {
11
- "en": "Attempt to construct an abstract class detected."
12
- },
13
- "1002": {
14
- "en": "Attempt to call an abstract method detected."
15
- },
16
- "1003": {
17
- "en": "Invalid or no service domain name provided at microservice startup."
18
- },
19
- "1004": {
20
- "en": "The system cache required for proper engine operation is unavailable."
21
- },
22
- "1005": {
23
- "en": "The provided service handler is not a proper function."
24
- },
25
- "1006": {
26
- "en": "The requested feature is not supported by current configuration or version."
27
- },
28
- "2000": {
29
- "en": "Invalid authorization token provided."
30
- },
31
- "2001": {
32
- "en": "Invalid or expired session encountered."
33
- },
34
- "2002": {
35
- "en": "Attempt for unauthorized access detected."
36
- },
37
- "2003": {
38
- "en": "The system detected tampering with the message received via message exchange."
39
- },
40
- "3000": {
41
- "en": "General error during cross-application communication."
42
- },
43
- "3001": {
44
- "en": "The message sender instance is currently unavailable."
45
- },
46
- "3002": {
47
- "en": "The execution of a service could not complete within the allowed timeout."
48
- },
49
- "3003": {
50
- "en": "The specified service is not found in the service registry."
51
- },
52
- "3004": {
53
- "en": "The specified service is not found in the service definition interface."
54
- },
55
- "3005": {
56
- "en": "No handler found in the interface for the specified service or service version."
57
- },
58
- "3006": {
59
- "en": "The message receiver instance is currently unavailable."
60
- },
61
- "3007": {
62
- "en": "The message exchange is irrevocably broken and cannot be used any longer."
63
- },
64
- "3010": {
65
- "en": "Connection retry attempts exceeded the configured limit."
66
- },
67
- "4000": {
68
- "en": "The request method is not recognized or not supported."
69
- },
70
- "4001": {
71
- "en": "The request URI is not recognized or not supported."
72
- },
73
- "4002": {
74
- "en": "The request body is not recognized or not supported."
75
- },
76
- "4003": {
77
- "en": "The request query is not recognized or not supported."
78
- },
79
- "4004": {
80
- "en": "The request headers are not recognized or not supported."
81
- },
82
- "4005": {
83
- "en": "The request parameters are not recognized or not supported."
84
- },
85
- "4006": {
86
- "en": "The request format is not recognized or not supported."
87
- },
88
- "4007": {
89
- "en": "The request content type is not recognized or not supported."
90
- },
91
- "4008": {
92
- "en": "The request content length is not recognized or not supported."
93
- },
94
- "4009": {
95
- "en": "The request content encoding is not recognized or not supported."
96
- }
97
- }
98
- }
1
+ {
2
+ "system": {
3
+ "exceptions": {
4
+ "0": {
5
+ "en": "Unidentified error encountered or unrecognized exception code provided."
6
+ },
7
+ "1000": {
8
+ "en": "Error thrown by internal JS source."
9
+ },
10
+ "1001": {
11
+ "en": "Attempt to construct an abstract class detected."
12
+ },
13
+ "1002": {
14
+ "en": "Attempt to call an abstract method detected."
15
+ },
16
+ "1003": {
17
+ "en": "Invalid or no service domain name provided at microservice startup."
18
+ },
19
+ "1004": {
20
+ "en": "The system cache required for proper engine operation is unavailable."
21
+ },
22
+ "1005": {
23
+ "en": "The provided service handler is not a proper function."
24
+ },
25
+ "1006": {
26
+ "en": "The requested feature is not supported by current configuration or version."
27
+ },
28
+ "1007": {
29
+ "en": "The provided argument is not of the expected type."
30
+ },
31
+ "2000": {
32
+ "en": "Invalid authorization token provided."
33
+ },
34
+ "2001": {
35
+ "en": "Invalid or expired session encountered."
36
+ },
37
+ "2002": {
38
+ "en": "Attempt for unauthorized access detected."
39
+ },
40
+ "2003": {
41
+ "en": "The system detected tampering with the message received via message exchange."
42
+ },
43
+ "2004": {
44
+ "en": "The requested authentication method is not recognized or supported."
45
+ },
46
+ "3000": {
47
+ "en": "General error during cross-application communication."
48
+ },
49
+ "3001": {
50
+ "en": "The message sender instance is currently unavailable."
51
+ },
52
+ "3002": {
53
+ "en": "The execution of a service could not complete within the allowed timeout."
54
+ },
55
+ "3003": {
56
+ "en": "The specified service is not found in the service registry."
57
+ },
58
+ "3004": {
59
+ "en": "The specified service is not found in the service definition interface."
60
+ },
61
+ "3005": {
62
+ "en": "No handler found in the interface for the specified service or service version."
63
+ },
64
+ "3006": {
65
+ "en": "The message receiver instance is currently unavailable."
66
+ },
67
+ "3007": {
68
+ "en": "The message exchange is irrevocably broken and cannot be used any longer."
69
+ },
70
+ "3010": {
71
+ "en": "Connection retry attempts exceeded the configured limit."
72
+ },
73
+ "4000": {
74
+ "en": "The request method is not recognized or not supported."
75
+ },
76
+ "4001": {
77
+ "en": "The request URI is not recognized or not supported."
78
+ },
79
+ "4002": {
80
+ "en": "The request body is not recognized or not supported."
81
+ },
82
+ "4003": {
83
+ "en": "The request query is not recognized or not supported."
84
+ },
85
+ "4004": {
86
+ "en": "The request headers are not recognized or not supported."
87
+ },
88
+ "4005": {
89
+ "en": "The request parameters are not recognized or not supported."
90
+ },
91
+ "4006": {
92
+ "en": "The request format is not recognized or not supported."
93
+ },
94
+ "4007": {
95
+ "en": "The request content type is not recognized or not supported."
96
+ },
97
+ "4008": {
98
+ "en": "The request content length is not recognized or not supported."
99
+ },
100
+ "4009": {
101
+ "en": "The request content encoding is not recognized or not supported."
102
+ }
103
+ }
104
+ }
99
105
  }
@@ -95,8 +95,8 @@ class MessageDispatcher {
95
95
  retry.onFailedAttempt( ( error ) => {
96
96
  logger.log( `Failed to send message request with chain ID: ${ message.chainID }`, logger.logSeverity.WARNING, error );
97
97
  } );
98
- retry.onRetry( ( attempt ) => {
99
- logger.log( `Retrying to send message response with chain ID: ${ message.chainID }. This is attempt ${ attempt }...`, logger.logSeverity.NOTICE );
98
+ retry.onRetry( ( attempt, error ) => {
99
+ logger.log( `Retrying to send message response with chain ID: ${ message.chainID }. This is attempt ${ attempt }...`, logger.logSeverity.NOTICE, ( error ) ? { error: tools.errorToJSON( error ) } : undefined );
100
100
  } );
101
101
 
102
102
  messageTracer.instance.recordTraceEntry( message, messageTracer.messageType.MESSAGE_REQUEST, messageTracer.dispatchEvent.SENT, messageTracer.messageState.PENDING );
@@ -125,8 +125,8 @@ class MessageDispatcher {
125
125
  retry.onFailedAttempt( ( error ) => {
126
126
  logger.log( `Failed to send message response with chain ID: ${ message.chainID }`, logger.logSeverity.WARNING, error );
127
127
  } );
128
- retry.onRetry( ( attempt ) => {
129
- logger.log( `Retrying to send message response with chain ID: ${ message.chainID }. This is attempt ${ attempt }...`, logger.logSeverity.NOTICE );
128
+ retry.onRetry( ( attempt, error ) => {
129
+ logger.log( `Retrying to send message response with chain ID: ${ message.chainID }. This is attempt ${ attempt }...`, logger.logSeverity.NOTICE, ( error ) ? { error: tools.errorToJSON( error ) } : undefined );
130
130
  } );
131
131
 
132
132
  messageTracer.instance.recordTraceEntry( message, messageTracer.messageType.MESSAGE_RESPONSE, messageTracer.dispatchEvent.SENT, messageTracer.messageState.PROCESSED );
@@ -30,6 +30,7 @@ const cache = require( "#cache" );
30
30
  const traceRoot = {
31
31
  trace: []
32
32
  };
33
+ const UNKNOWN_TOKEN = "UNKNOWN";
33
34
 
34
35
  /**
35
36
  * Enum for listing message types.
@@ -155,12 +156,12 @@ class MessageTracer {
155
156
  /** @type TiTraceEntry */
156
157
  let traceEntry = {
157
158
  chainID: message.chainID,
158
- dispatchEvent: tools.getEnumName( dispatchEventEnum, dispatchEvent ),
159
+ dispatchEvent: dispatchEventEnum.name( dispatchEvent, UNKNOWN_TOKEN ),
159
160
  fromAddress: ( messageType === messageTypeEnum.MESSAGE_REQUEST ) ? source : destination,
160
161
  messageID: message.messageID,
161
162
  messageSnapshot: messageSnapshot,
162
- messageState: tools.getEnumName( messageStateEnum, messageState ),
163
- messageType: tools.getEnumName( messageTypeEnum, messageType ),
163
+ messageState: messageStateEnum.name( messageState, UNKNOWN_TOKEN ),
164
+ messageType: messageTypeEnum.name( messageType, UNKNOWN_TOKEN ),
164
165
  toAddress: ( messageType === messageTypeEnum.MESSAGE_REQUEST ) ? destination : source,
165
166
  traceTimestamp: currentDate.getTime(),
166
167
  traceID: tools.getUUID()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ti-engine/core",
3
- "version": "1.3.5",
3
+ "version": "1.3.8",
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": "GPL-3.0-or-later",
@@ -47,11 +47,10 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "blake2": "^5.0.0",
50
- "@dotenvx/dotenvx": "^1.49.0",
51
- "fs-extra": "^11.3.1",
50
+ "@dotenvx/dotenvx": "^1.51.0",
52
51
  "lodash": "^4.17.21",
53
52
  "node-schedule": "^2.1.1",
54
- "ioredis": "^5.7.0"
53
+ "ioredis": "^5.8.0"
55
54
  },
56
55
  "optionalDependencies": {
57
56
  "@google-cloud/error-reporting": "^3.0.5",
@@ -62,6 +61,6 @@
62
61
  "url": "git+https://github.com/Belleal/ti-engine.git"
63
62
  },
64
63
  "engines": {
65
- "node": ">=14.17.0"
64
+ "node": ">=18.0.0"
66
65
  }
67
66
  }
package/utils/config.js CHANGED
@@ -112,6 +112,7 @@ const tools = require( "#tools" );
112
112
  *
113
113
  * @readonly
114
114
  * @enum {string} Keys of this ENUM are strings.
115
+ * @typedef {string} TiSetting
115
116
  */
116
117
  const settingsEnum = tools.enum( {
117
118
  AUDITING_LOG_CONSOLE_ENABLED: [ "auditing.logConsoleEnabled", "logConsoleEnabled", "" ],
@@ -144,9 +145,6 @@ const settingsEnum = tools.enum( {
144
145
  OPERATION_MODE: [ "operationMode", "operationMode", "" ]
145
146
  } );
146
147
 
147
- /**
148
- * @typedef {string} TiSetting
149
- */
150
148
  module.exports.setting = settingsEnum;
151
149
 
152
150
  /** @type {SettingsMain} */
@@ -165,11 +163,11 @@ if ( settings.localization ) {
165
163
  }
166
164
  if ( settings.memoryCache ) {
167
165
  settings.memoryCache.authKey = ( process.env.TI_MEMORY_CACHE_AUTH_KEY !== undefined ) ? process.env.TI_MEMORY_CACHE_AUTH_KEY : settings.memoryCache.authKey;
168
- settings.memoryCache.redisDB = ( process.env.TI_MEMORY_CACHE_REDIS_DB !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_DB : settings.memoryCache.redisDB;
166
+ settings.memoryCache.redisDB = ( process.env.TI_MEMORY_CACHE_REDIS_DB !== undefined ) ? Number( process.env.TI_MEMORY_CACHE_REDIS_DB ) : settings.memoryCache.redisDB;
169
167
  settings.memoryCache.redisHost = ( process.env.TI_MEMORY_CACHE_REDIS_HOST !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_HOST : settings.memoryCache.redisHost;
170
- settings.memoryCache.redisPort = ( process.env.TI_MEMORY_CACHE_REDIS_PORT !== undefined ) ? process.env.TI_MEMORY_CACHE_REDIS_PORT : settings.memoryCache.redisPort;
171
- settings.memoryCache.retryMaxAttempts = ( process.env.TI_MEMORY_CACHE_RETRY_MAX_ATTEMPTS !== undefined ) ? process.env.TI_MEMORY_CACHE_RETRY_MAX_ATTEMPTS : settings.memoryCache.retryMaxAttempts;
172
- settings.memoryCache.retryMaxInterval = ( process.env.TI_MEMORY_CACHE_RETRY_MAX_INTERVAL !== undefined ) ? process.env.TI_MEMORY_CACHE_RETRY_MAX_INTERVAL : settings.memoryCache.retryMaxInterval;
168
+ settings.memoryCache.redisPort = ( process.env.TI_MEMORY_CACHE_REDIS_PORT !== undefined ) ? Number( process.env.TI_MEMORY_CACHE_REDIS_PORT ) : settings.memoryCache.redisPort;
169
+ settings.memoryCache.retryMaxAttempts = ( process.env.TI_MEMORY_CACHE_RETRY_MAX_ATTEMPTS !== undefined ) ? Number( process.env.TI_MEMORY_CACHE_RETRY_MAX_ATTEMPTS ) : settings.memoryCache.retryMaxAttempts;
170
+ settings.memoryCache.retryMaxInterval = ( process.env.TI_MEMORY_CACHE_RETRY_MAX_INTERVAL !== undefined ) ? Number( process.env.TI_MEMORY_CACHE_RETRY_MAX_INTERVAL ) : settings.memoryCache.retryMaxInterval;
173
171
  settings.memoryCache.user = ( process.env.TI_MEMORY_CACHE_USER !== undefined ) ? process.env.TI_MEMORY_CACHE_USER : settings.memoryCache.user;
174
172
  }
175
173
  if ( settings.messageExchange ) {
@@ -178,8 +176,8 @@ if ( settings.messageExchange ) {
178
176
  settings.messageExchange.traceLogEnabled = ( process.env.TI_MESSAGE_EXCHANGE_TRACE_LOG_ENABLED !== undefined ) ? tools.toBool( process.env.TI_MESSAGE_EXCHANGE_TRACE_LOG_ENABLED ) : settings.messageExchange.traceLogEnabled;
179
177
  }
180
178
 
181
- // make sure GCloud is enabled before trying to set it up:
182
- if ( process.env.TI_GCLOUD_ENABLED === true && settings.gcloudIntegration ) {
179
+ // Make sure GCloud is enabled before trying to set it up:
180
+ if ( tools.toBool( process.env.TI_GCLOUD_ENABLED ) === true && settings.gcloudIntegration ) {
183
181
  settings.gcloudIntegration.apiKey = ( process.env.TI_GCLOUD_API_KEY !== undefined ) ? process.env.TI_GCLOUD_API_KEY : settings.gcloudIntegration.apiKey;
184
182
  settings.gcloudIntegration.projectID = ( process.env.TI_GCLOUD_PROJECT_ID !== undefined ) ? process.env.TI_GCLOUD_PROJECT_ID : settings.gcloudIntegration.projectID;
185
183
  }
@@ -14,6 +14,7 @@ const tools = require( "#tools" );
14
14
  *
15
15
  * @readonly
16
16
  * @enum {number}
17
+ * @typedef {number} TiExceptionCode
17
18
  */
18
19
  const exceptionCodeEnum = tools.enum( {
19
20
  E_UNKNOWN_ERROR: [ 0, "unknown error", "Unidentified error encountered or unrecognized exception code provided." ],
@@ -25,11 +26,13 @@ const exceptionCodeEnum = tools.enum( {
25
26
  E_GEN_SYSTEM_CACHE_UNAVAILABLE: [ 1004, "system cache unavailable", "The system cache required for proper engine operation is unavailable." ],
26
27
  E_GEN_BAD_SERVICE_HANDLER: [ 1005, "bad service handler", "The provided service handler is not a proper function." ],
27
28
  E_GEN_FEATURE_UNSUPPORTED: [ 1006, "feature unsupported", "The requested feature is not supported by current configuration or version." ],
29
+ E_GEN_INVALID_ARGUMENT_TYPE: [ 1007, "invalid argument type", "The provided argument is not of the expected type." ],
28
30
  /** Security & Administration exceptions - codes under 2xxx */
29
31
  E_SEC_INVALID_AUTH_TOKEN: [ 2000, "invalid auth token", "Invalid authorization token provided." ],
30
32
  E_SEC_INVALID_EXPIRED_SESSION: [ 2001, "invalid or expired session", "Invalid or expired session encountered." ],
31
33
  E_SEC_UNAUTHORIZED_ACCESS: [ 2002, "unauthorized access", "Attempt for unauthorized access detected." ],
32
34
  E_SEC_MESSAGE_TAMPERING_DETECTED: [ 2003, "message tampering detected", "The system detected tampering with the message received via message exchange." ],
35
+ E_SEC_UNRECOGNIZED_AUTH_METHOD: [ 2004, "unrecognized auth method", "The requested authentication method is not recognized or supported." ],
33
36
  /** Cross-Application Communication exceptions - codes under 3xxx */
34
37
  E_COM_GENERAL_ERROR: [ 3000, "general communication error", "General error during cross-application communication." ],
35
38
  E_COM_MESSAGE_SENDER_UNAVAILABLE: [ 3001, "message sender unavailable", "The message sender instance is currently unavailable." ],
@@ -53,10 +56,83 @@ const exceptionCodeEnum = tools.enum( {
53
56
  E_WEB_INVALID_REQUEST_CONTENT_ENCODING: [ 4009, "invalid request content encoding", "The request content encoding is not recognized or not supported." ]
54
57
  } );
55
58
 
59
+ module.exports.exceptionCode = exceptionCodeEnum;
60
+
56
61
  /**
57
- * @typedef {number} TiExceptionCode
62
+ * Enum for listing all HTTP codes.
63
+ *
64
+ * @readonly
65
+ * @enum {number}
66
+ * @typedef {number} TiHttpCode
58
67
  */
59
- module.exports.exceptionCode = exceptionCodeEnum;
68
+ const httpCodeEnum = tools.enum( {
69
+ /** 1xx informational response */
70
+ C_100: [ 100, "Continue", "The server has received the request headers and the client should proceed to send the request body." ],
71
+ C_101: [ 101, "Switching Protocols", "The requester has asked the server to switch protocols and the server has agreed to do so." ],
72
+ C_102: [ 102, "Processing", "This code indicates that the server has received and is processing the request, but no response is available yet." ],
73
+ C_103: [ 103, "Early Hints", "Used to return some response headers before final HTTP message." ],
74
+ /** 2xx success */
75
+ C_200: [ 200, "OK", "Standard response for successful HTTP requests." ],
76
+ C_201: [ 201, "Created", "The request has been fulfilled, resulting in the creation of a new resource." ],
77
+ C_202: [ 202, "Accepted", "The request has been accepted for processing, but the processing has not been completed." ],
78
+ C_203: [ 203, "Non-Authoritative Information", "The server is a transforming proxy that received a 200 OK from its origin, but is returning a modified version of the origin's response." ],
79
+ C_204: [ 204, "No Content", "The server successfully processed the request, and is not returning any content." ],
80
+ C_205: [ 205, "Reset Content", "The server successfully processed the request, asks that the requester reset its document view, and is not returning any content." ],
81
+ C_206: [ 206, "Partial Content", "The server is delivering only part of the resource (byte serving) due to a range header sent by the client." ],
82
+ C_207: [ 207, "Multi-Status", "The message body that follows is by default an XML message and can contain a number of separate response codes, depending on how many sub-requests were made." ],
83
+ C_208: [ 208, "Already Reported", "The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response, and are not being included again." ],
84
+ C_226: [ 226, "IM Used", "The server has fulfilled a request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance." ],
85
+ /** 3xx redirection */
86
+ C_300: [ 300, "Multiple Choices", "Indicates multiple options for the resource from which the client may choose." ],
87
+ C_301: [ 301, "Moved Permanently", "This and all future requests should be directed to the given URI." ],
88
+ C_302: [ 302, "Found", "Tells the client to look at (browse to) another URL." ],
89
+ C_303: [ 303, "See Other", "The response to the request can be found under another URI using the GET method." ],
90
+ C_304: [ 304, "Not Modified", "Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match." ],
91
+ C_307: [ 307, "Temporary Redirect", "In this case, the request should be repeated with another URI; however, future requests should still use the original URI." ],
92
+ C_308: [ 308, "Permanent Redirect", "This and all future requests should be directed to the given URI. 308 parallels the behavior of 301, but does not allow the HTTP method to change." ],
93
+ /** 4xx client errors */
94
+ C_400: [ 400, "Bad Request", "The server cannot or will not process the request due to an apparent client error." ],
95
+ C_401: [ 401, "Unauthorized", "Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided." ],
96
+ C_403: [ 403, "Forbidden", "The request contained valid data and was understood by the server, but the server is refusing action. This may be due to the user not having the necessary permissions for a resource or needing an account of some sort, or attempting a prohibited action." ],
97
+ C_404: [ 404, "Not Found", "The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible." ],
98
+ C_405: [ 405, "Method Not Allowed", "A request method is not supported for the requested resource; for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource." ],
99
+ C_406: [ 406, "Not Acceptable", "The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request." ],
100
+ C_407: [ 407, "Proxy Authentication Required", "The client must first authenticate itself with the proxy." ],
101
+ C_408: [ 408, "Request Timeout", "The server timed out waiting for the request." ],
102
+ C_409: [ 409, "Conflict", "Indicates that the request could not be processed because of conflict in the current state of the resource, such as an edit conflict between multiple simultaneous updates." ],
103
+ C_410: [ 410, "Gone", "Indicates that the resource requested was previously in use but is no longer available and will not be available again." ],
104
+ C_411: [ 411, "Length Required", "The request did not specify the length of its content, which is required by the requested resource." ],
105
+ C_412: [ 412, "Precondition Failed", "The server does not meet one of the preconditions that the requester put on the request header fields." ],
106
+ C_413: [ 413, "Payload Too Large", "The request is larger than the server is willing or able to process." ],
107
+ C_414: [ 414, "URI Too Long", "The URI provided was too long for the server to process." ],
108
+ C_415: [ 415, "Unsupported Media Type", "The request entity has a media type which the server or resource does not support." ],
109
+ C_416: [ 416, "Range Not Satisfiable", "The client has asked for a portion of the file (byte serving), but the server cannot supply that portion." ],
110
+ C_417: [ 417, "Expectation Failed", "The server cannot meet the requirements of the Expect request-header field." ],
111
+ C_421: [ 421, "Misdirected Request", "The request was directed at a server that is not able to produce a response (for example because of connection reuse)." ],
112
+ C_422: [ 422, "Unprocessable Content", "The request was well-formed (i.e., syntactically correct) but could not be processed." ],
113
+ C_423: [ 423, "Locked", "The resource that is being accessed is locked." ],
114
+ C_424: [ 424, "Failed Dependency", "The request failed because it depended on another request and that request failed." ],
115
+ C_425: [ 425, "Too Early", "Indicates that the server is unwilling to risk processing a request that might be replayed." ],
116
+ C_426: [ 426, "Upgrade Required", "The client should switch to a different protocol such as TLS/1.3, given in the Upgrade header field." ],
117
+ C_428: [ 428, "Precondition Required", "The origin server requires the request to be conditional." ],
118
+ C_429: [ 429, "Too Many Requests", "The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes." ],
119
+ C_431: [ 431, "Request Header Fields Too Large", "The server is unwilling to process the request because either an individual header field, or all the header fields collectively, are too large." ],
120
+ C_451: [ 451, "Unavailable For Legal Reasons", "A server operator has received a legal demand to deny access to a resource or to a set of resources that includes the requested resource." ],
121
+ /** 5xx server errors */
122
+ C_500: [ 500, "Internal Server Error", "A generic error message, given when an unexpected condition was encountered and no more specific message is suitable." ],
123
+ C_501: [ 501, "Not Implemented", "The server either does not recognize the request method, or it lacks the ability to fulfil the request." ],
124
+ C_502: [ 502, "Bad Gateway", "The server was acting as a gateway or proxy and received an invalid response from the upstream server." ],
125
+ C_503: [ 503, "Service Unavailable", "The server cannot handle the request (because it is overloaded or down for maintenance)." ],
126
+ C_504: [ 504, "Gateway Timeout", "The server was acting as a gateway or proxy and did not receive a timely response from the upstream server." ],
127
+ C_505: [ 505, "HTTP Version Not Supported", "The server does not support the HTTP version used in the request." ],
128
+ C_506: [ 506, "Variant Also Negotiates", "Transparent content negotiation for the request results in a circular reference." ],
129
+ C_507: [ 507, "Insufficient Storage", "The server is unable to store the representation needed to complete the request." ],
130
+ C_508: [ 508, "Loop Detected", "The server detected an infinite loop while processing the request." ],
131
+ C_510: [ 510, "Not Extended", "Further extensions to the request are required for the server to fulfil it." ],
132
+ C_511: [ 511, "Network Authentication Required", "The client needs to authenticate to gain network access. Intended for use by intercepting proxies used to control access to the network." ]
133
+ } );
134
+
135
+ module.exports.httpCode = httpCodeEnum;
60
136
 
61
137
  const labelPath = "system.exceptions.";
62
138
 
@@ -68,28 +144,27 @@ const labelPath = "system.exceptions.";
68
144
  */
69
145
  class Exception {
70
146
 
71
- #id = undefined;
72
- #code = undefined;
73
- #httpCode = undefined;
74
- #label = undefined;
75
- #description = undefined;
76
- #data = undefined;
147
+ #id;
148
+ #code;
149
+ #httpCode;
150
+ #label;
151
+ #description;
152
+ #data;
77
153
 
78
154
  /**
79
155
  * @constructor
80
156
  * @param {string} id The unique ID to be assigned to this exception.
81
157
  * @param {TiExceptionCode} exceptionCode An unique exception identifier. If this is not recognized, the default error code will be used instead.
82
- * @param {Object} [data] Any additional data to insert into the exception.
83
- * @param {string} [description] Description of the exception.
158
+ * @param {Object} [data={}] Any additional data to insert into the exception.
159
+ * @param {string} [description=undefined] Description of the exception.
84
160
  */
85
161
  constructor( id, exceptionCode, data, description ) {
86
- exceptionCode = ( exceptionCodeEnum.properties[ exceptionCode ] ) ? exceptionCode : module.exports.exceptionCode.E_UNKNOWN_ERROR;
162
+ exceptionCode = exceptionCodeEnum.contains( exceptionCode ) ? exceptionCode : exceptionCodeEnum.E_UNKNOWN_ERROR;
87
163
 
88
164
  this.#id = id;
89
165
  this.#code = exceptionCode;
90
- this.#httpCode = undefined;
91
- this.#label = labelPath + exceptionCode;
92
- this.#description = description || exceptionCodeEnum.properties[ exceptionCode ].description;
166
+ this.#label = labelPath + String( exceptionCode );
167
+ this.#description = description || exceptionCodeEnum.description( exceptionCode );
93
168
  this.#data = data || {};
94
169
  }
95
170
 
@@ -121,7 +196,7 @@ class Exception {
121
196
  * HTTP error code if relevant.
122
197
  *
123
198
  * @property
124
- * @returns {number}
199
+ * @returns {TiHttpCode}
125
200
  * @public
126
201
  */
127
202
  get httpCode() {
@@ -132,11 +207,15 @@ class Exception {
132
207
  * HTTP error code if relevant.
133
208
  *
134
209
  * @property
135
- * @param {number} httpCode
210
+ * @param {TiHttpCode} httpCode
136
211
  * @public
137
212
  */
138
213
  set httpCode( httpCode ) {
139
- this.#httpCode = httpCode;
214
+ if ( httpCodeEnum.contains( httpCode ) ) {
215
+ this.#httpCode = httpCode;
216
+ } else {
217
+ this.#httpCode = undefined;
218
+ }
140
219
  }
141
220
 
142
221
  /**
@@ -192,14 +271,19 @@ class Exception {
192
271
  * @public
193
272
  */
194
273
  asJSON( includeData = true ) {
195
- return {
274
+ let json = {
196
275
  id: this.id,
197
276
  code: this.code,
198
- httpCode: this.httpCode,
199
277
  label: this.label,
200
- description: this.description,
201
- data: ( includeData === true ) ? this.data : undefined
278
+ description: this.description
202
279
  };
280
+ if ( this.#httpCode !== undefined ) {
281
+ json.httpCode = this.#httpCode;
282
+ }
283
+ if ( includeData === true ) {
284
+ json.data = this.#data;
285
+ }
286
+ return json;
203
287
  }
204
288
  }
205
289
 
@@ -218,17 +302,25 @@ module.exports.raise = ( source, data, exceptionID ) => {
218
302
  let exception;
219
303
 
220
304
  if ( source instanceof Error ) {
221
- exception = new Exception( exceptionID || tools.getUUID(), module.exports.exceptionCode.E_GEN_JS_INTERNAL_ERROR, tools.errorToJSON( source ) );
305
+ exception = new Exception( exceptionID || tools.getUUID(), exceptionCodeEnum.E_GEN_JS_INTERNAL_ERROR, tools.errorToJSON( source ) );
222
306
  } else if ( source instanceof Exception ) {
223
307
  exception = source;
224
308
  } else if ( _.isString( source ) ) {
225
- exception = new Exception( exceptionID || tools.getUUID(), module.exports.exceptionCode.E_GEN_JS_INTERNAL_ERROR, {
309
+ exception = new Exception( exceptionID || tools.getUUID(), exceptionCodeEnum.E_GEN_JS_INTERNAL_ERROR, {
226
310
  message: source
227
311
  } );
228
312
  } else if ( _.isObjectLike( source ) ) {
229
- exception = new Exception( exceptionID || ( source.id || tools.getUUID() ), source.code || module.exports.exceptionCode.E_GEN_JS_INTERNAL_ERROR, source.data, source.description );
313
+ exception = new Exception(
314
+ exceptionID || ( source.id || tools.getUUID() ),
315
+ source.code || exceptionCodeEnum.E_GEN_JS_INTERNAL_ERROR,
316
+ source.data,
317
+ source.description
318
+ );
319
+ if ( httpCodeEnum.contains( source.httpCode ) ) {
320
+ exception.httpCode = source.httpCode;
321
+ }
230
322
  } else {
231
- exception = new Exception( exceptionID || tools.getUUID(), ( exceptionCodeEnum.properties[ source ] ) ? source : module.exports.exceptionCode.E_UNKNOWN_ERROR );
323
+ exception = new Exception( exceptionID || tools.getUUID(), ( exceptionCodeEnum.contains( source ) ) ? source : exceptionCodeEnum.E_UNKNOWN_ERROR );
232
324
  }
233
325
 
234
326
  // Merge the default exception data with the additional one if it's provided:
package/utils/logger.js CHANGED
@@ -42,7 +42,7 @@ module.exports.logSeverity = logSeverityEnum;
42
42
  * @returns {string}
43
43
  */
44
44
  module.exports.getSeverityName = ( severity ) => {
45
- return tools.getEnumName( logSeverityEnum, severity, "unknown" );
45
+ return logSeverityEnum.name( severity, "unknown" );
46
46
  };
47
47
 
48
48
  /**
package/utils/tools.js CHANGED
@@ -7,19 +7,21 @@
7
7
  */
8
8
 
9
9
  const _ = require( "lodash" );
10
- const fs = require( "fs-extra" );
11
10
  const crypto = require( "node:crypto" );
12
11
 
13
12
  /**
14
13
  * @typedef {Object} TiEnumValue
15
14
  * @property {number|string} value
16
15
  * @property {string} name
17
- * @property {string} description
16
+ * @property {string} [description]
18
17
  */
19
18
 
20
19
  /**
21
20
  * @typedef {Object} TiEnum
22
21
  * @property {Object.<number|string,TiEnumValue>} properties
22
+ * @property {function( (number|string), [string] ): (string|undefined)} name
23
+ * @property {function( (number|string), [string] ): (string|undefined)} description
24
+ * @property {function( (number|string) ): boolean} contains
23
25
  */
24
26
 
25
27
  /**
@@ -38,38 +40,113 @@ module.exports.getUUID = () => {
38
40
  *
39
41
  * @method
40
42
  * @param {Object} seed
41
- * @returns {Object}
43
+ * @returns {Object} This is a {@link TiEnum} object. Setting the proper reference here would unfortunately break IDE support.
42
44
  * @public
43
45
  */
44
46
  module.exports.enum = ( seed ) => {
45
- let properties = {};
47
+ const enumObject = Object.create( null );
48
+ const properties = Object.create( null );
49
+ const reserved = new Set( [ "properties", "name", "description", "contains", "__proto__", "prototype", "constructor" ] );
46
50
 
47
51
  _.forOwn( seed, ( value, key ) => {
48
- if ( value instanceof Array ) {
49
- seed[ key ] = value[ 0 ];
50
- properties[ value[ 0 ] ] = {
51
- value: value[ 0 ],
52
- name: value[ 1 ],
53
- description: value[ 2 ]
54
- };
52
+ if ( !reserved.has( key ) ) {
53
+ if ( Array.isArray( value ) ) {
54
+ enumObject[ key ] = value[ 0 ];
55
+ properties[ value[ 0 ] ] = {
56
+ value: value[ 0 ],
57
+ name: value[ 1 ],
58
+ description: value[ 2 ]
59
+ };
60
+ } else {
61
+ enumObject[ key ] = value;
62
+ properties[ value ] = {
63
+ value: value,
64
+ name: key
65
+ };
66
+ }
67
+ }
68
+ } );
69
+ Object.values( properties ).forEach( Object.freeze );
70
+ Object.freeze( properties );
71
+
72
+ /**
73
+ * Used to get the name of an {@link TiEnumValue} if such value exists.
74
+ *
75
+ * @method
76
+ * @param {number|string} value
77
+ * @param {string} [placeholder=undefined] If provided it will be returned when the enum value does not have a name defined.
78
+ * @returns {string|undefined}
79
+ * @public
80
+ */
81
+ const name = ( value, placeholder = undefined ) => {
82
+ return ( properties[ value ] ) ? properties[ value ].name : placeholder;
83
+ };
84
+
85
+ /**
86
+ * Used to get the description of an {@link TiEnumValue} if such value exists.
87
+ *
88
+ * @method
89
+ * @param {number|string} value
90
+ * @param {string} [placeholder=undefined] If provided it will be returned when the enum value does not have a description defined.
91
+ * @returns {string|undefined}
92
+ * @public
93
+ */
94
+ const description = ( value, placeholder = undefined ) => {
95
+ if ( !properties[ value ] ) {
96
+ return placeholder;
55
97
  } else {
56
- properties[ value ] = {
57
- value: value,
58
- name: key.toLowerCase(),
59
- description: ""
60
- };
98
+ return ( properties[ value ].description !== undefined ) ? properties[ value ].description : placeholder;
99
+ }
100
+ };
101
+
102
+ /**
103
+ * Used to check if the provided value is contained in the provided {@link TiEnum} list.
104
+ *
105
+ * @method
106
+ * @param {number|string} value
107
+ * @returns {boolean}
108
+ * @public
109
+ */
110
+ const contains = ( value ) => {
111
+ return !!( properties[ value ] );
112
+ };
113
+
114
+ Object.defineProperties( enumObject, {
115
+ contains: {
116
+ enumerable: false,
117
+ configurable: false,
118
+ writable: false,
119
+ value: contains
120
+ },
121
+ description: {
122
+ enumerable: false,
123
+ configurable: false,
124
+ writable: false,
125
+ value: description
126
+ },
127
+ name: {
128
+ enumerable: false,
129
+ configurable: false,
130
+ writable: false,
131
+ value: name
132
+ },
133
+ properties: {
134
+ enumerable: false,
135
+ configurable: false,
136
+ writable: false,
137
+ value: properties
61
138
  }
62
139
  } );
63
- seed.properties = properties;
140
+ Object.freeze( enumObject );
64
141
 
65
- Object.freeze( seed );
66
- return seed;
142
+ return enumObject;
67
143
  };
68
144
 
69
145
  /**
70
146
  * Used to get the name of an {@link TiEnum} value if such exists.
71
147
  *
72
148
  * @method
149
+ * @deprecated Use the 'name' property of the provided {@link TiEnum} instead.
73
150
  * @param {TiEnum} enumList
74
151
  * @param {number|string} enumValue
75
152
  * @param {string} [placeholder=undefined] If provided it will be returned when the enum value does not have a name defined.
@@ -81,7 +158,7 @@ module.exports.getEnumName = ( enumList, enumValue, placeholder = undefined ) =>
81
158
  };
82
159
 
83
160
  /**
84
- * Convert an Error to JSON object.
161
+ * Convert an Error to a JSON object.
85
162
  * <br/>
86
163
  * NOTE: If the value provided is not an error, then it will just be cloned.
87
164
  *
@@ -123,7 +200,7 @@ module.exports.toBool = ( value ) => {
123
200
  };
124
201
 
125
202
  /**
126
- * Will return UTC date string in format YYYY-MM-DD from the provided date.
203
+ * Will return a UTC date string in format YYYY-MM-DD from the provided date.
127
204
  *
128
205
  * @method
129
206
  * @param {Date} date
@@ -131,15 +208,14 @@ module.exports.toBool = ( value ) => {
131
208
  * @public
132
209
  */
133
210
  module.exports.getUTCDateString = ( date ) => {
134
- let year = date.getUTCFullYear();
135
- let month = ( "00" + ( date.getUTCMonth() + 1 ) ).match( /\d{2}$/ );
136
- let day = ( "00" + date.getUTCDate() ).match( /\d{2}$/ );
137
-
138
- return String( year + "-" + month + "-" + day );
211
+ const year = date.getUTCFullYear();
212
+ const month = String( date.getUTCMonth() + 1 ).padStart( 2, "0" );
213
+ const day = String( date.getUTCDate() ).padStart( 2, "0" );
214
+ return `${ year }-${ month }-${ day }`;
139
215
  };
140
216
 
141
217
  /**
142
- * Will return UTC time string in format hh:mm:ss.MMM from the provided date.
218
+ * Will return a UTC time string in format hh:mm:ss, or hh:mm:ss.MMM when useMilliseconds is true.
143
219
  *
144
220
  * @method
145
221
  * @param {Date} date
@@ -147,13 +223,12 @@ module.exports.getUTCDateString = ( date ) => {
147
223
  * @returns {string}
148
224
  * @public
149
225
  */
150
- module.exports.getUTCTimeString = ( date, useMilliseconds ) => {
151
- let hours = ( "00" + date.getUTCHours() ).match( /\d{2}$/ );
152
- let minutes = ( "00" + date.getUTCMinutes() ).match( /\d{2}$/ );
153
- let seconds = ( "00" + date.getUTCSeconds() ).match( /\d{2}$/ );
154
- let milliseconds = ( "000" + date.getUTCMilliseconds() ).match( /\d{3}$/ );
155
-
156
- return String( hours + ":" + minutes + ":" + seconds + ( ( useMilliseconds ) ? "." + milliseconds : "" ) );
226
+ module.exports.getUTCTimeString = ( date, useMilliseconds = false ) => {
227
+ const hours = String( date.getUTCHours() ).padStart( 2, "0" );
228
+ const minutes = String( date.getUTCMinutes() ).padStart( 2, "0" );
229
+ const seconds = String( date.getUTCSeconds() ).padStart( 2, "0" );
230
+ const milliseconds = useMilliseconds ? `${ String( date.getUTCMilliseconds() ).padStart( 3, "0" ) }` : "";
231
+ return `${ hours }:${ minutes }:${ seconds }${ useMilliseconds ? `.${ milliseconds }` : "" }`;
157
232
  };
158
233
 
159
234
  /**
@@ -181,7 +256,7 @@ module.exports.getUTCTimeString = ( date, useMilliseconds ) => {
181
256
  *
182
257
  * @method
183
258
  * @param {Object} object
184
- * @param {function} [replacer]
259
+ * @param {function( Object ): Object} [replacer]
185
260
  * @returns {Object}
186
261
  * @public
187
262
  */
@@ -315,7 +390,7 @@ module.exports.retrocycle = ( $ ) => {
315
390
  *
316
391
  * @method
317
392
  * @param {Object} value
318
- * @return {string}
393
+ * @returns {string|*}
319
394
  * @public
320
395
  */
321
396
  module.exports.stringifyJSON = ( value ) => {
@@ -345,7 +420,7 @@ module.exports.isJsonString = ( string ) => {
345
420
  *
346
421
  * @method
347
422
  * @param {string} value
348
- * @return {Object}
423
+ * @returns {Object|string}
349
424
  * @public
350
425
  */
351
426
  module.exports.parseJSON = ( value ) => {
@@ -364,7 +439,7 @@ module.exports.parseJSON = ( value ) => {
364
439
  *
365
440
  * @param {Object} input
366
441
  * @recursion
367
- * @return {string|null}
442
+ * @returns {string|null}
368
443
  * @public
369
444
  */
370
445
  module.exports.decomposeJSON = ( input ) => {
@@ -399,50 +474,6 @@ module.exports.decomposeJSON = ( input ) => {
399
474
  return decomposed;
400
475
  };
401
476
 
402
- /**
403
- * Used to create a CSV file from the provided data.
404
- *
405
- * @method
406
- * @param {Object[]} data
407
- * @param {string} filePath
408
- * @param {string} fileName
409
- * @return {Promise}
410
- * @public
411
- */
412
- module.exports.createCSVFile = ( data, filePath, fileName ) => {
413
- return new Promise( ( resolve, reject ) => {
414
- let fileData = "";
415
- if ( data && data.length > 0 ) {
416
- let keys = [];
417
- _.forOwn( data[ 0 ], ( value, key ) => {
418
- keys.push( key );
419
- } );
420
- keys.sort();
421
-
422
- _.forEach( keys, ( key, idx ) => {
423
- fileData += key + ( ( idx < keys.length - 1 ) ? "," : "" );
424
- } );
425
- fileData += "\n";
426
-
427
- _.forEach( data, ( entry ) => {
428
- _.forEach( keys, ( key, idx ) => {
429
- fileData += entry[ key ] + ( ( idx < keys.length - 1 ) ? "," : "" );
430
- } );
431
- fileData += "\n";
432
- } );
433
- }
434
-
435
- fs.ensureDir( filePath ).then( () => {
436
- const fullPath = filePath + "/" + Date.now() + "-" + fileName + ".csv";
437
- return fs.appendFile( fullPath, fileData );
438
- } ).then( () => {
439
- resolve();
440
- } ).catch( ( error ) => {
441
- reject( error );
442
- } );
443
- } );
444
- };
445
-
446
477
  /**
447
478
  * Used to create retry policy for the execution of an operation.
448
479
  *
@@ -457,8 +488,12 @@ class RetryPolicy {
457
488
 
458
489
  /**
459
490
  * @constructor
491
+ * @param {number} maxAttempts The maximum number of attempts to execute the operation.
460
492
  */
461
493
  constructor( maxAttempts ) {
494
+ if ( !Number.isInteger( maxAttempts ) || maxAttempts < 1 ) {
495
+ throw new TypeError( "maxAttempts must be a positive integer" );
496
+ }
462
497
  this.#maxAttempts = maxAttempts;
463
498
  }
464
499
 
@@ -468,13 +503,13 @@ class RetryPolicy {
468
503
  * Used to start execution of the provided operation.
469
504
  *
470
505
  * @method
471
- * @param {Object} context The context in which the operation will be executed (i.e. this reference).
472
- * @param {function} operation Operation to be executed; has to return a Promise.
473
- * @param {Array} params The arguments to be provided to the operation upon execution.
506
+ * @param {Object} context The context in which the operation will be executed (i.e., this reference).
507
+ * @param {function( ...* ): Promise<*>} operation Operation to be executed; must return a Promise.
508
+ * @param {Array<*>} [params=[]] The arguments to be provided to the operation upon execution.
474
509
  * @returns {Promise}
475
510
  * @public
476
511
  */
477
- execute( context, operation, params ) {
512
+ execute( context, operation, params = [] ) {
478
513
  return this.#retry( context, operation, params, 1, undefined );
479
514
  }
480
515
 
@@ -495,7 +530,7 @@ class RetryPolicy {
495
530
  * Used to register a method that will be automatically called on each execution retry (after the initial one).
496
531
  *
497
532
  * @method
498
- * @param {function( number )} action The current attempt number will be provided as an argument.
533
+ * @param {function( number, (Error|undefined) )} action The current attempt and last error are provided.
499
534
  * @public
500
535
  */
501
536
  onRetry( action ) {
@@ -511,8 +546,8 @@ class RetryPolicy {
511
546
  *
512
547
  * @method
513
548
  * @param {Object} context
514
- * @param {function} operation
515
- * @param {Array} params
549
+ * @param {function( ...* ): Promise<*>} operation Operation to be executed; must return a Promise.
550
+ * @param {Array<*>} params The arguments to be provided to the operation upon execution.
516
551
  * @param {number} attempt
517
552
  * @param {Error} error
518
553
  * @returns {Promise}
@@ -523,14 +558,25 @@ class RetryPolicy {
523
558
  return Promise.reject( error );
524
559
  } else {
525
560
  if ( attempt > 1 && this.#onRetry ) {
526
- this.#onRetry( attempt );
527
- }
528
- return operation.apply( context, params ).catch( error => {
529
- if ( this.#onFailedAttempt ) {
530
- this.#onFailedAttempt( error );
561
+ try {
562
+ this.#onRetry( attempt, error );
563
+ } catch ( _ ) { /* ignore observer errors */
531
564
  }
532
- return this.#retry( context, operation, params, ( attempt - 1 ), error );
533
- } );
565
+ }
566
+ return Promise
567
+ .resolve()
568
+ .then( () => {
569
+ return operation.apply( context, params );
570
+ } )
571
+ .catch( ( error ) => {
572
+ if ( this.#onFailedAttempt ) {
573
+ try {
574
+ this.#onFailedAttempt( error );
575
+ } catch ( _ ) { /* ignore observer errors */
576
+ }
577
+ }
578
+ return this.#retry( context, operation, params, ( attempt + 1 ), error );
579
+ } );
534
580
  }
535
581
  }
536
582