@go-mailer/jarvis 1.0.0 → 2.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/README.md CHANGED
@@ -1,2 +1,256 @@
1
1
  # jarvis-node
2
- A multipurpose helper package for our node apps.
2
+ A multipurpose helper package for our node apps. This package contains helpers for the following:
3
+
4
+ * Authentication
5
+ * Feature flag control
6
+ * Logging & Monitoring
7
+ * Environment variable management
8
+ * Request Query processing
9
+
10
+ ## **Installation**
11
+
12
+ yarn install @go-mailer/jarvis
13
+ ## **Authentication**
14
+ In Go-Mailer, there are two major types of authentication:
15
+
16
+
17
+ 1. **JWT authentication**:
18
+ is employed in simple username/password log-ins and is the primary method of user authentication in the system.
19
+ We use the `jsonwetoken` package for [JWT](https://jwt.io/) authentication.
20
+
21
+ const { Authenticator } = require('@go-mailer/jarvis');
22
+ const { JWTAuth } = Authenticator;
23
+
24
+ app.use('/route', JWTAuth, route_handler);
25
+ 2. **API authentication**:
26
+ is done via in-house authentication methods. The [IAM](https://github.com/go-mailer-ltd/iam) app is responsible
27
+ for managing user identities, authorizations and API keys. This package makes simple API calls to the IAM app to
28
+ verify the provided API keys. There are two ways API keys can be authenticated:
29
+
30
+ * **As a Query Parameter**: Here the API key is passed as a request query parameter - typically for applications
31
+ which intend to authenticate via `GET` HTTP method.
32
+ * **As an Authentication Header**: This is default way to pass API keys and security tokens in HTTP requests.
33
+
34
+
35
+ const { Authenticator } = require('@go-mailer/jarvis');
36
+ const { APIAuth } = Authenticator;
37
+ const { defaultAuth, paramAuth } = APIAuth();
38
+
39
+ app.use('/api-route', defaultAuth, handler); //API auth
40
+ app.use('/api-param-route', paramAuth, handler); //Query param auth
41
+
42
+ ## **Feature Flag Control**
43
+ Go-Mailer uses [Go-Flags](https://go-flags.com) to control application feature visibility. This gives us the ability to
44
+ build our application's feature continuosly without worry about the customers seeing or using them before release.
45
+
46
+ `async FeatureFlag.verify(<flage_name>, <criteria>)`: used to verify whether or not a feature is turned ON or OFF.
47
+ Returns `true` if feature is enabled and `false` otherwise.
48
+
49
+ * **flag_name** `string`: **required**
50
+
51
+ This is the name of the feature to be checked as defined on the [Go-Flags app](https://go-flags.com).
52
+
53
+ * **criteria** `Object`: **required**
54
+ This is a key -> value pair of criteria with which the status of the flag should be checked.
55
+ These criteria are matched against those configure on the [Go-Flags app](https://go-flags.com).
56
+
57
+ const { FeatureFlag } = require('@go-mailer/jarvis');
58
+ FeatureFlag.verify(<flag_name>: String, <criteria>: Object ).then((result) => {
59
+ // do anything with result.
60
+ });
61
+
62
+ ## **Logging**
63
+ Logging is essential to the success of any application. The ability to collect information about the application's processes is crucial
64
+ to making engineering and business decisions as well as resolving any issues that may arise in the system. Jarvis provides logging capabilities. Currently, the logging application we utilize is [Logtail](https://logtail.com/).
65
+
66
+ There are two kinds of Loggers provided by Jarvis:
67
+
68
+ 1. Request Logger
69
+ 2. Process Logger
70
+
71
+ ### **Request Logger**
72
+
73
+ This middleware generates logs each time a request passes through the system. It should be placed before any routes are defined.
74
+
75
+ const { RequestLogger } = require('@go-mailer/jarvis');
76
+ app.use('/', RequestLogger);
77
+
78
+ ### **Process Logger**
79
+
80
+ This logger is used to keep various kinds of logs from different parts of the system. There are two kinds of process logs recorded: `error` & `info`.
81
+
82
+ 1. **error** logs are recorded whenever an exception is thrown or some anormally is detected or encountered.
83
+ 2. **info** logs are strictly informational. They are typically used to provide visibility through the system for things
84
+ like system health, process paths, etc.
85
+
86
+ ```
87
+ const { ProcessLogger } = require('@go-mailer/jarvis');
88
+ const logger = new ProcessLogger(<service_name>: String);
89
+
90
+ // error logging
91
+ logger.error(<error_object>: Error, <method_name>: String);
92
+
93
+ // info logging
94
+ logger.error(<message_to_be_logged>: String, <method_name>: String);
95
+ ```
96
+
97
+ 1. **`<service_name>`**: This is the name of the service or part of the system where the log is generated. For example, if the log was generated in the Mailing controller, this would have the value `MailingController`.
98
+ 2. **`<error_object>`**: This is the instance of the Error that was thrown and caught.
99
+ 3. **`<message_to_be_logged>`**: For informational logs, this is the information to be logged.
100
+ 4. **`<method_name>`**: This is the method/function within which the log was generated.
101
+
102
+ .
103
+ ## **Environment variable management**
104
+ Jarvis also provides environment variable management. This enables applications to set and retrieve environment variables more seamlessly.
105
+ 1. **`set(config: Object)`**: allows for the configuration of new variables. This does not override environment variables.
106
+
107
+ * `config: Object`: a key-value set that specifies application or instance specific criteria to the environment namespace.
108
+
109
+ .
110
+ 2. **`fetch(variable_name: String, is_required: Boolean)`**: retrieves the specified variable.
111
+
112
+ * `variable_name: String`: The name of the variable to retrieve.
113
+ * `is_required: Boolean`: Specifies whether or not the variable to be retrieved is required. If set to `true`, an Exception would be thrown if no value exists. default: `false`
114
+
115
+ ```
116
+ const { EnvVar } = require('@go-mailer/jarvis');
117
+
118
+ // set environment variable(s)
119
+ EnvVar.set({
120
+ APP_NAME: 'Mailing'
121
+ });
122
+
123
+ // retrieve environment variable
124
+ EnvVar.fetch('APP_NAME', true); // required
125
+ EnvVar.fetch('APP_NAME'); // not required
126
+ ```
127
+ ## **Request Query processing**
128
+ Jarvis provides a query processor that converts request queries into MongoDB compatible queries. Request Queries are processed based on keywords and the use of special characters. A request query is the part of the request path that comes after the question mark (?). The `QueryBuilder.buildQuery()` is used for request processing.
129
+
130
+ GET http://go-mailer.com?<query_part>
131
+
132
+ ```
133
+ QueryBuilder.buildQuery(options: Object) : {
134
+ count: boolean
135
+ fields_to_return: string
136
+ limit: number
137
+ seek_conditions: MongooseQueryObject
138
+ skip: number
139
+ sort_condition: string
140
+ }
141
+ ```
142
+
143
+ ### **Keywords**:
144
+ Keywords in request queries are used to prepare the following queries. Given the sample records in the database:
145
+ ```
146
+ [{
147
+ name: "Nathan",
148
+ age: 21,
149
+ sex: 'M'
150
+ }, {
151
+ name: "Anita",
152
+ age: 15,
153
+ sex: 'F'
154
+ }, {
155
+ name: "Manchang",
156
+ age: 25,
157
+ sex: 'M'
158
+ }]
159
+ ```
160
+ 1. **Field inclusion** queries: when the keyword `return_only` is used the query, ONLY the properties that are contained in the value that is assigned to this keyword WILL be returned. For example:
161
+
162
+ ```
163
+ // given the request
164
+ GET https://go-mailer.com?return_only=name,age
165
+
166
+ // sample response will return only the `age` and `name` properties on the records
167
+ [{
168
+ name: "Nathan",
169
+ age: 21,
170
+ }, {
171
+ name: "Anita",
172
+ age: 15,
173
+ }, {
174
+ name: "Manchang",
175
+ age: 25,
176
+ }]
177
+ ```
178
+ 2. **Field exclusion** queries: when the keyword `exclude_only` is used the query, ONLY the properties that are NOT contained in the value that is assigned to this keyword WILL be returned. For example:
179
+
180
+ ```
181
+ // given the request
182
+ GET https://go-mailer.com?exclude_only=name,age
183
+
184
+ // sample response will return only the `sex` property on the records.
185
+ [{
186
+ sex: 'M'
187
+ }, {
188
+ sex: 'F'
189
+ }, {
190
+ sex: 'M'
191
+ }]
192
+ ```
193
+ 3. **Pagination** queries: when the keywords `page` & `population` are specified in the query, the result set would be paginated. NOTE that the first page number is `0`. For example:
194
+ ```
195
+ // given the request
196
+ GET https://go-mailer.com?page=0&population=2
197
+
198
+ // sample response. First two matching objects
199
+ [{
200
+ name: "Nathan",
201
+ age: 21,
202
+ sex: 'M'
203
+ }, {
204
+ name: "Anita",
205
+ age: 15,
206
+ sex: 'F'
207
+ }]
208
+ ```
209
+ 4. **Sort** queries: we can specify sort conditions by using the `sort_by` keyword in the query. The value would contain the property with which we would like to sort. Default sort mode is `ASC`. If you want `DESC`, specify the value with a minus sign (`-`):
210
+ ```
211
+ // ASCending (age)
212
+ GET https://go-mailer.com?sort_by=age
213
+
214
+ // DESCending (-age)
215
+ GET https://go-mailer.com?sort_by=-age
216
+ ```
217
+ 5. **count**: To return a count of items that match the query, the `count` keyword is used.
218
+
219
+ ```
220
+ GET https://go-mailer.com?count=1
221
+ ```
222
+
223
+ ### **Special Characters**
224
+ The use of special characters in query values indicate the need to prepare the following queries:
225
+ 1. **OR** queries: Whenever a comma separated list is used as a keyword value. This is hand
226
+
227
+ ```
228
+ // return records where 'name' is 'Nathan' OR 'Michael'
229
+ GET https://go-mailer.com?name=Nathan,Michael
230
+ ```
231
+ 3. **NOR** queries: Whenever the values of a query field are separated by an exclamation mark (!), a NOR query is built.
232
+
233
+ ```
234
+ // return records where 'name' is neither 'Nathan' NOR 'Michael'
235
+ GET https://go-mailer.com?name=Nathan!Michael
236
+ ```
237
+ 4. **Range selection** queries: To support range queries, the tilde character (~) is used in between values. currently, ranges only work with integer values
238
+ ```
239
+ // get all records where age is between 12 and 34 (inclusive)
240
+ GET https://go-mailer.com?age=12~34
241
+
242
+ // get all records less than 34
243
+ GET https://go-mailer.com?age=~34
244
+
245
+ // get all records greater than 34
246
+ GET https://go-mailer.com?age=34~
247
+ ```
248
+
249
+ ### **Wildcard**
250
+ The `QueryBuilder` closure also provides a `buildWildcardOptions()` function that prepares a mongoose regular expression query object for text matching.
251
+ ```
252
+ QueryBuilder.buildWildcardOptions(key_list: string, value) : MongooseQueryObject
253
+ ```
254
+
255
+ * `key_list: string`: is a comma-separated list of properties against which value is sought
256
+ * `value: any`: is the value being sought for.
package/index.js CHANGED
@@ -0,0 +1,7 @@
1
+ const EnvVar = require('./lib/env')
2
+ const FeatureFlag = require('./lib/flag')
3
+ const QueryBuilder = require('./lib/query')
4
+ const HTTP = require('./lib/middlewares/http')
5
+ const Authenticator = require('./lib/middlewares/auth')
6
+ const { RequestLogger, ProcessLogger } = require('./lib/middlewares/logger')
7
+ module.exports = { RequestLogger, ProcessLogger, Authenticator, EnvVar, FeatureFlag, HTTP, QueryBuilder }
@@ -0,0 +1,27 @@
1
+ const axios = require("axios");
2
+ const Env = require("../env");
3
+ const BASE_URI = Env.fetch("GO_FLAGS_URI", true);
4
+ const API_KEY = Env.fetch("GO_FLAGS_KEY", true);
5
+
6
+ const verifyFeatureFlag = async (flag_name, criteria = {}) => {
7
+ const { error, payload } = (
8
+ await axios.post(
9
+ `${BASE_URI}/api/v1/flags/${flag_name}`,
10
+ {
11
+ payload: criteria,
12
+ environment: Env.fetch("NODE_ENV", true)
13
+ },
14
+ {
15
+ headers: {
16
+ authorization: `Bearer ${API_KEY}`,
17
+ },
18
+ }
19
+ )
20
+ ).data;
21
+
22
+ if (error) throw new Error(error);
23
+
24
+ return payload.is_permitted;
25
+ };
26
+
27
+ module.exports = { verifyFeatureFlag };
@@ -0,0 +1,42 @@
1
+ const axios = require("axios");
2
+ const Env = require("../env");
3
+ const IAM_URI = Env.fetch("IAM_SERVICE_URI", true);
4
+ const DEFAULT_TOKEN = Env.fetch("DEFAULT_TOKEN", true);
5
+
6
+ const verifyAPIKey = async (key) => {
7
+ const { error, payload } = (
8
+ await axios.get(`${IAM_URI}/keys/verify/${key}`, {
9
+ headers: {
10
+ authorization: `Bearer ${DEFAULT_TOKEN}`,
11
+ },
12
+ })
13
+ ).data;
14
+
15
+ if (error) throw new Error("Unauthorized");
16
+
17
+ return payload.org_id;
18
+ };
19
+
20
+ const verifyFeatureFlag = async (flag_name, criteria = {}) => {
21
+ const { error, payload } = (
22
+ await axios.post(
23
+ `${IAM_URI}/flags`,
24
+ {
25
+ criteria,
26
+ environment: Env.fetch("NODE_ENV"),
27
+ name: flag_name,
28
+ },
29
+ {
30
+ headers: {
31
+ authorization: `Bearer ${DEFAULT_TOKEN}`,
32
+ },
33
+ }
34
+ )
35
+ ).data;
36
+
37
+ if (error) throw new Error(error);
38
+
39
+ return payload.is_permitted;
40
+ };
41
+
42
+ module.exports = { verifyAPIKey, verifyFeatureFlag };
package/lib/env.js CHANGED
@@ -1,12 +1,24 @@
1
- require('dotenv').config()
1
+ require("dotenv").config();
2
2
 
3
3
  /** */
4
4
  class EnvVar {
5
- fetch (var_name = '', is_required = false) {
6
- const value = process.env[var_name]
7
- if (is_required && !var_name) throw new Error(`Required EnvVar ${var_name} not found`)
8
- return value
5
+ constructor() {
6
+ this.config = {};
7
+ }
8
+
9
+ set(config = {}) {
10
+ this.config = {
11
+ ...this.config,
12
+ ...config,
13
+ };
14
+ }
15
+
16
+ fetch(var_name = "", is_required = false) {
17
+ if (!var_name) throw new Error(`Variable name is required.`);
18
+ const value = process.env[var_name] || this.config[var_name];
19
+ if (is_required && !value) throw new Error(`Required EnvVar ${var_name} not found`);
20
+ return value;
9
21
  }
10
22
  }
11
23
 
12
- module.exports = new EnvVar()
24
+ module.exports = new EnvVar();
package/lib/flag.js ADDED
@@ -0,0 +1,18 @@
1
+ const { verifyFeatureFlag } = require('./clients/go-flags')
2
+ const { ProcessLogger } = require('./middlewares/logger')
3
+ const flagLogger = new ProcessLogger('FeatureFlag')
4
+
5
+ const verify = async (flag_name = '', criteria = {}) => {
6
+ try {
7
+ if (!flag_name) throw new Error('Unspecified flag name');
8
+ if (!Object.keys(criteria)) throw new Error('Unspecified criteria');
9
+
10
+ const result = await verifyFeatureFlag(flag_name, criteria);
11
+ return result
12
+ } catch (e) {
13
+ flagLogger.error(e, 'verify')
14
+ return false
15
+ }
16
+ }
17
+
18
+ module.exports = { verify }
@@ -1,116 +1,73 @@
1
1
  /**
2
2
  * User Authentication Middleware
3
3
  */
4
- const jwt = require("jsonwebtoken");
5
- const { verify_api_key } = require("../clients/iam");
6
- const RootService = require("../services/_root");
7
- const rootService = new RootService();
8
- const { JWT_ISSUER, JWT_SECRET, DEFAULT_TOKEN: GM_TOKEN } = require("../../.config");
9
-
10
- const { app_logger } = require("../utilities/logger");
11
- const logger = app_logger("Authentication Middleware");
12
-
13
- class Authentication {
14
- async authenticate_user(request, response, next) {
15
- try {
16
- const { authorization } = request.headers;
17
- if (!authorization) {
18
- return next(rootService.process_failed_response("Unauthorized", 403));
19
- }
20
-
21
- const [, token] = authorization.split(" ");
22
- if (!token) {
23
- return next(rootService.process_failed_response("Unauthorized", 403));
24
- }
25
4
 
26
- if (token === GM_TOKEN) {
27
- request.tenant_id = { $exists: true };
28
- return next();
29
- }
5
+ const jwt = require("jsonwebtoken");
6
+ const Env = require("../env");
7
+ const Errors = require("./errors");
8
+ const { ProcessLogger } = require("./logger");
9
+ const { verifyAPIKey } = require("../clients/iam");
10
+ const authLogger = new ProcessLogger("Authenticator");
30
11
 
31
- const verified_data = await jwt.verify(token, JWT_SECRET, {
32
- issuer: JWT_ISSUER,
33
- });
12
+ // helpers
13
+ const extract_token = (headers) => {
14
+ const { authorization } = headers;
15
+ if (!authorization) throw new Error("Unauthorized");
34
16
 
35
- const { tenant_id, is_admin } = verified_data;
36
- request.tenant_id = tenant_id;
37
- if (is_admin) process_request(request);
17
+ const [, token] = authorization.split(" ");
18
+ if (!token) throw new Error("Unauthorized");
38
19
 
39
- next();
40
- } catch (e) {
41
- logger.error(`[Auth Error] ${e.message}`);
42
- next(rootService.process_failed_response("Unauthorized", 403));
43
- }
44
- }
45
- }
20
+ return token;
21
+ };
46
22
 
47
- const authenticate_api_key = async (request, response, next) => {
23
+ // main
24
+ const JWTAuth = async (request, response, next) => {
48
25
  try {
49
- const { authorization } = request.headers;
50
- if (!authorization) {
51
- return next(rootService.process_failed_response("Unauthorized", 403));
52
- }
53
-
54
- const [, api_key] = authorization.split(" ");
55
- if (!api_key) {
56
- return next(rootService.process_failed_response("Unauthorized", 403));
57
- }
58
-
59
- if (api_key === GM_TOKEN) {
60
- request.tenant_id = request.body.tenant_id;
26
+ // env vars
27
+ const DEFAULT_TOKEN = Env.fetch("DEFAULT_TOKEN", true);
28
+ const ISSUER = Env.fetch("JWT_ISSUER", true);
29
+ const SECRET = Env.fetch("JWT_SECRET", true);
30
+
31
+ const token = extract_token(request.headers);
32
+ if (token === DEFAULT_TOKEN) {
33
+ // inter-service requests
34
+ request.is_service_request = true;
35
+ // typically scope requests by tenant_id
36
+ request.tenant_id = request.body.tenant_id || request.query.tenant_id || { $exists: true };
61
37
  return next();
62
38
  }
63
39
 
64
- const payload = await verify_api_key(api_key);
65
- request.tenant_id = payload.org_id;
40
+ const { tenant_id, is_admin } = await jwt.verify(token, SECRET, { issuer: ISSUER });
41
+ request.is_admin = !!is_admin;
42
+ request.tenant_id = is_admin ? { $exists: true } : tenant_id;
43
+
66
44
  next();
67
45
  } catch (e) {
68
- logger.error(`${e.message}`, "authenticate_api_key");
69
- next(rootService.process_failed_response("Unauthorized", 403));
46
+ authLogger.error(e, "JWTAuth");
47
+ return response.status(403).json(Errors.UNAUTHORIZED);
70
48
  }
71
49
  };
72
50
 
73
- /** All requests not made from the 'admin console' must be scoped by tenant */
74
- const process_request = (request) => {
75
- const { query, params, body } = request;
76
- let tenant_id = { $exists: true };
77
- if (query.tenant_id) tenant_id = query.tenant_id;
78
- if (params.tenant_id) tenant_id = params.tenant_id;
79
- if (body.tenant_id) tenant_id = body.tenant_id;
80
-
81
- request.tenant_id = tenant_id;
51
+ const authenticateParamKey = async (request, response, next) => {
52
+ try {
53
+ const { api_key: token } = request.params;
54
+ request.tenant_id = await verifyAPIKey(token);
55
+ next();
56
+ } catch (e) {
57
+ authLogger.error(e, "authenticateParamKey");
58
+ return response.status(403).json(Errors.UNAUTHORIZED);
59
+ }
82
60
  };
83
61
 
84
- const authenticate_user = async (request, __, next) => {
62
+ const authenticateBearerKey = async (request, response, next) => {
85
63
  try {
86
- const { authorization } = request.headers;
87
- if (!authorization) {
88
- return next(rootService.process_failed_response("Unauthorized", 403));
89
- }
90
-
91
- const [, token] = authorization.split(" ");
92
- if (!token) {
93
- return next(rootService.process_failed_response("Unauthorized", 403));
94
- }
95
-
96
- if (token === GM_TOKEN) {
97
- request.tenant_id = { $exists: true };
98
- return next();
99
- }
100
-
101
- const verified_data = await jwt.verify(token, JWT_SECRET, {
102
- issuer: JWT_ISSUER,
103
- });
104
-
105
- const { tenant_id, is_admin } = verified_data;
106
- request.tenant_id = tenant_id;
107
- if (is_admin) process_request(request);
108
-
64
+ const token = extract_token(request.headers);
65
+ request.tenant_id = await verifyAPIKey(token);
109
66
  next();
110
67
  } catch (e) {
111
- logger.error(`[Auth Error] ${e.message}`);
112
- next(rootService.process_failed_response("Unauthorized", 403));
68
+ authLogger.error(e, "authenticateBearerKey");
69
+ return response.status(403).json(Errors.UNAUTHORIZED);
113
70
  }
114
71
  };
115
72
 
116
- module.exports = { authenticate_api_key, authenticate_user };
73
+ module.exports = { authenticateBearerKey, authenticateParamKey, JWTAuth };
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ UNAUTHORIZED: {
3
+ payload: null,
4
+ error: 'Unauthorized',
5
+ status_code: 403
6
+ }
7
+ }
@@ -0,0 +1,52 @@
1
+ /** **/
2
+ const { ProcessLogger } = require("./logger");
3
+ const HTTPLogger = new ProcessLogger("HTTPSetup");
4
+
5
+ module.exports = {
6
+ handle404(_, __, next) {
7
+ const return_data = {
8
+ status_code: 404,
9
+ success: false,
10
+ error: "Resource not found",
11
+ payload: null,
12
+ };
13
+
14
+ next(return_data);
15
+ },
16
+
17
+ handleError(error, __, response, ____) {
18
+ // Log errors
19
+ if (error.error) {
20
+ HTTPLogger.info(error.error, "handleError");
21
+ } else {
22
+ HTTPLogger.error(error, "handleError");
23
+ }
24
+
25
+ // return error
26
+ return response.status(error.status_code || 500).json({
27
+ success: false,
28
+ status_code: error.status_code || 500,
29
+ error: error.error || "Internal Server Error",
30
+ payload: null,
31
+ });
32
+ },
33
+
34
+ processResponse(request, response, next) {
35
+ if (!request.payload) return next();
36
+
37
+ const { status_code } = request.payload;
38
+ return response.status(status_code).json(request.payload);
39
+ },
40
+
41
+ setupRequest(request, response, next) {
42
+ request.headers["access-control-allow-origin"] = "*";
43
+ request.headers["access-control-allow-headers"] = "*";
44
+
45
+ if (request.method === "OPTIONS") {
46
+ request.headers["access-control-allow-methods"] = "GET, POST, PUT, PATCH, DELETE";
47
+ response.status(200).json();
48
+ }
49
+
50
+ next();
51
+ },
52
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @author Oguntuberu Nathan O. <nateoguns.work@gmail.com>
3
+ **/
4
+
5
+ const Env = require("../env");
6
+ const { randomUUID } = require("crypto");
7
+ const { Logtail } = require("@logtail/node");
8
+ const LOGTAIL_SECRET = Env.fetch("LOGTAIL_SECRET", true);
9
+ const logtail = new Logtail(LOGTAIL_SECRET);
10
+
11
+ function RequestLogger() {
12
+ const app_name = Env.fetch("APP_NAME", true);
13
+ return (request, response, next) => {
14
+ if (!request.request_id) request.request_id = randomUUID();
15
+
16
+ //
17
+ const {
18
+ query,
19
+ params,
20
+ headers: { host, origin, "user-agent": user_agent, "sec-ch-ua-platform": os, referer },
21
+ request_id,
22
+ tenant_id,
23
+ } = request;
24
+
25
+ response.on("finish", () => {
26
+ const {
27
+ _parsedUrl: { pathname },
28
+ httpVersion,
29
+ _startTime,
30
+ _remoteAddress,
31
+ payload,
32
+ } = response.req;
33
+ const { statusCode, statusMessage } = response.req.res;
34
+ const duration = Date.now() - Date.parse(_startTime);
35
+ const log = {
36
+ app_name,
37
+ request_id,
38
+ tenant_id,
39
+ query,
40
+ params,
41
+ host,
42
+ origin,
43
+ user_agent,
44
+ os,
45
+ referer,
46
+ httpVersion,
47
+ _remoteAddress,
48
+ pathname,
49
+ duration,
50
+ type: "request",
51
+ status_code: payload ? payload.status_code : statusCode,
52
+ };
53
+
54
+ let error = payload ? payload.error : statusMessage;
55
+ if (error) {
56
+ log.error = error;
57
+ logtail.error(pathname, log);
58
+ } else {
59
+ logtail.info(pathname, log);
60
+ }
61
+
62
+ logtail.flush();
63
+ });
64
+
65
+ next();
66
+ }
67
+ }
68
+
69
+ class ProcessLogger {
70
+ constructor(service = "System") {
71
+ this.service = service;
72
+ }
73
+
74
+ error(error, method = "unspecified_method") {
75
+ logtail.error(`${this.service}:${method}:${error.message}`, {
76
+ app_name: Env.fetch("APP_NAME", true),
77
+ type: "process",
78
+ message: error.message,
79
+ trace: error.stack,
80
+ });
81
+ logtail.flush();
82
+ }
83
+
84
+ info(info, method = "unspecified_method") {
85
+ logtail.info(`${this.service}:${method}:${info}`, {
86
+ app_name: Env.fetch("APP_NAME", true),
87
+ type: "process",
88
+ message: info,
89
+ });
90
+ logtail.flush();
91
+ }
92
+ }
93
+
94
+ module.exports = { RequestLogger, ProcessLogger };
95
+
package/lib/query.js CHANGED
@@ -1,29 +1,10 @@
1
- /** */
2
- const check_if_value_is_integer = (value) => {
3
- return !isNaN(Number(value))
4
- }
5
-
6
- exports.build_query = (options) => {
1
+ const buildQuery = (options) => {
7
2
  const seek_conditions = {}
8
- const sort_condition = options.sort_by
9
- ? this.build_sort_order_string(options.sort_by)
10
- : ''
11
- const fields_to_return = options.return_only
12
- ? this.build_return_fields_string(options.return_only)
13
- : ''
3
+ const sort_condition = options.sort_by ? buildSortOrderString(options.sort_by) : ''
4
+ const fields_to_return = options.return_only ? buildReturnFieldsString(options.return_only) : ''
14
5
  const count = options.count || false
15
6
 
16
- let skip = 0
17
- let limit = Number.MAX_SAFE_INTEGER
18
-
19
- if (options.page && options.population) {
20
- const pagination = this.determine_pagination(
21
- options.page,
22
- options.population
23
- )
24
- limit = pagination.limit
25
- skip = pagination.skip
26
- }
7
+ const { skip, limit } = determinePagination(options.page, options.population)
27
8
 
28
9
  /** Delete sort and return fields */
29
10
  delete options.count
@@ -33,24 +14,17 @@ exports.build_query = (options) => {
33
14
  delete options.sort_by
34
15
 
35
16
  Object.keys(options).forEach((field) => {
36
- const field_value =
37
- typeof options[field] === 'number'
38
- ? options[field].toString()
39
- : options[field]
17
+ const field_value = options[field].toLowerCase()
40
18
  let condition
41
19
 
42
- if (typeof field_value === 'string') {
43
- if (field_value.includes(':')) {
44
- condition = this.build_in_query(field_value)
45
- } else if (field_value.includes('!')) {
46
- condition = this.build_nor_query(field_value)
47
- } else if (field_value.includes('~')) {
48
- condition = this.build_range_query(field_value)
49
- } else {
50
- condition = this.build_or_query(field_value)
51
- }
20
+ if (field_value.includes(':')) {
21
+ condition = buildInQuery(field_value)
22
+ } else if (field_value.includes('!')) {
23
+ condition = buildNorQuery(field_value)
24
+ } else if (field_value.includes('~')) {
25
+ condition = buildRangeQuery(field_value)
52
26
  } else {
53
- condition = field_value
27
+ condition = buildOrQuery(field_value)
54
28
  }
55
29
 
56
30
  seek_conditions[field] = { ...condition }
@@ -66,74 +40,71 @@ exports.build_query = (options) => {
66
40
  }
67
41
  }
68
42
 
69
- exports.build_in_query = (value) => {
43
+ const buildInQuery = (value) => {
70
44
  const values = value.split(':')
71
45
  return {
72
- $in: [
73
- ...values.map((value) =>
74
- check_if_value_is_integer(value) ? Number(value) : value
75
- )
76
- ]
46
+ $in: [...values]
77
47
  }
78
48
  }
79
49
 
80
- exports.build_nor_query = (value) => {
50
+ const buildNorQuery = (value) => {
81
51
  const values = value.split('!')
82
52
  return {
83
- $nin: [
84
- ...values
85
- .slice(1)
86
- .map((value) =>
87
- check_if_value_is_integer(value) ? Number(value) : value
88
- )
89
- ]
53
+ $nin: [...values.slice(1)]
90
54
  }
91
55
  }
92
56
 
93
- exports.build_or_query = (value) => {
57
+ const buildOrQuery = (value) => {
94
58
  const values = value.split(',')
95
59
  return {
96
- $in: [
97
- ...values.map((value) =>
98
- check_if_value_is_integer(value) ? Number(value) : value
99
- )
100
- ]
60
+ $in: [...values]
101
61
  }
102
62
  }
103
63
 
104
- exports.build_range_query = (value) => {
105
- const values = value.split('-')
64
+ const buildRangeQuery = (value) => {
65
+ const [min, max] = value.split('~')
106
66
  return {
107
- $gte: values[0] ? Number(values[0]) : Number.MIN_SAFE_INTEGER,
108
- $lte: values[1] ? Number(values[1]) : Number.MAX_SAFE_INTEGER
67
+ $gte: min ? Number(min) : Number.MIN_SAFE_INTEGER,
68
+ $lte: max ? Number(max) : Number.MAX_SAFE_INTEGER
109
69
  }
110
70
  }
111
71
 
112
- exports.build_return_fields_string = (value) => {
113
- const fields = value.split(',')
114
- return fields.join(' ')
72
+ const buildReturnFieldsString = (value) => {
73
+ return value.replace(/,/gi, ' ')
115
74
  }
116
75
 
117
- exports.build_sort_order_string = (value) => {
118
- const values = value.split(',')
119
- return values.join(' ')
76
+ const buildSortOrderString = (value) => {
77
+ return value.replace(/,/gi, ' ')
120
78
  }
121
79
 
122
- exports.build_wildcard_options = (key_list, value) => {
80
+ const buildWildcardOptions = (key_list, value) => {
123
81
  const keys = key_list.split(',')
82
+
124
83
  return {
125
84
  $or: keys.map((key) => ({
126
85
  [key]: {
127
- $regex: `${value}`,
86
+ $regex: value || '',
128
87
  $options: 'i'
129
88
  }
130
89
  }))
131
90
  }
132
91
  }
133
92
 
134
- exports.determine_pagination = (page, population) => {
93
+ const determinePagination = (page = 0, population = Number.MAX_SAFE_INTEGER) => {
135
94
  return {
136
95
  limit: Number(population),
137
96
  skip: page * population
138
97
  }
139
98
  }
99
+
100
+ module.exports = {
101
+ buildInQuery,
102
+ buildNorQuery,
103
+ buildOrQuery,
104
+ buildQuery,
105
+ buildRangeQuery,
106
+ buildReturnFieldsString,
107
+ buildSortOrderString,
108
+ buildWildcardOptions,
109
+ determinePagination
110
+ }
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@go-mailer/jarvis",
3
- "version": "1.0.0",
3
+ "version": "2.1.0",
4
4
  "main": "index.js",
5
- "repository": "git@noguntuberu:go-mailer-ltd/jarvis-node.git",
5
+ "repository": "git@github.com:go-mailer-ltd/jarvis-node.git",
6
6
  "author": "Nathan Oguntuberu <nateoguns.work@gmail.com>",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
9
  "@logtail/node": "^0.3.3",
10
10
  "axios": "^1.3.4",
11
11
  "dotenv": "^16.0.3",
12
- "jsonwebtoken": "^9.0.0",
13
- "morgan": "^1.10.0",
14
- "winston": "^3.8.2"
12
+ "jsonwebtoken": "^9.0.0"
13
+ },
14
+ "devDependencies": {
15
+ "chai": "^4.3.7",
16
+ "mocha": "^10.2.0"
15
17
  }
16
18
  }
package/lib/logger.js DELETED
@@ -1,126 +0,0 @@
1
- /**
2
- * @author Oguntuberu Nathan O. <nateoguns.work@gmail.com>
3
- **/
4
-
5
- const envVar = require('./env')
6
- const { Logtail } = require("@logtail/node");
7
- const logtail = new Logtail("oTNABUxtJb6erTRpZ2jhJhEs");
8
-
9
- const request_logger = (request, __, next) => {
10
-
11
- }
12
-
13
- /** MORGAN */
14
- const { createWriteStream } = require('fs')
15
- const { resolve } = require('path')
16
-
17
- /** REQUEST LOG */
18
- const morgan = require('morgan')
19
- const request_log_format = '[:date[web] :remote-addr :remote-user ] :method :url HTTP/:http-version :referrer - :user-agent | :status :response-time ms'
20
-
21
- const request_log_stream = createWriteStream(resolve(__dirname, '../../logs/request.log'), { flags: 'a' })
22
- const morgan_logger = morgan(request_log_format, { stream: request_log_stream })
23
-
24
- /** WINSTON */
25
- const {
26
- createLogger,
27
- format,
28
- transports
29
- } = require('winston')
30
-
31
- const {
32
- colorize,
33
- combine,
34
- printf,
35
- timestamp
36
- } = format
37
-
38
- const log_transports = {
39
- client_log: new transports.File({ level: 'error', filename: 'logs/client.log' }),
40
- console: new transports.Console({ level: 'warn' }),
41
- combined_log: new transports.File({ level: 'info', filename: 'logs/combined.log' }),
42
- error_log: new transports.File({ level: 'error', filename: 'logs/error.log' }),
43
- exception_log: new transports.File({ filename: 'logs/exception.log' }),
44
- mailer_log: new transports.File({ level: 'error', filename: 'logs/mailer.log' }),
45
- stream_log: new transports.File({ level: 'error', filename: 'logs/stream.log' })
46
- }
47
-
48
- const log_format = printf(({ level, message, timestamp }) => `[${timestamp} : ${level}] - ${message}`)
49
-
50
- const logger = createLogger({
51
- transports: [
52
- log_transports.console,
53
- log_transports.combined_log,
54
- log_transports.error_log
55
- ],
56
- exceptionHandlers: [
57
- log_transports.exception_log
58
- ],
59
- exitOnError: false,
60
- format: combine(
61
- colorize(),
62
- timestamp(),
63
- log_format
64
- )
65
- })
66
-
67
- const client_logger = createLogger({
68
- transports: [
69
- log_transports.console,
70
- log_transports.client_log
71
- ],
72
- exitOnError: false,
73
- format: combine(
74
- colorize(),
75
- timestamp(),
76
- log_format
77
- )
78
- })
79
-
80
- const mail_logger = createLogger({
81
- transports: [
82
- log_transports.console,
83
- log_transports.mailer_log
84
- ],
85
- exitOnError: false,
86
- format: combine(
87
- colorize(),
88
- timestamp(),
89
- log_format
90
- )
91
- })
92
-
93
- const stream_logger = createLogger({
94
- transports: [
95
- log_transports.console,
96
- log_transports.stream_log
97
- ],
98
- exitOnError: false,
99
- format: combine(
100
- colorize(),
101
- timestamp(),
102
- log_format
103
- )
104
- })
105
-
106
- const log = console.log
107
- const app_logger = (service = 'System') => {
108
- const console = (message, method = 'unspecified_method') => {
109
- const formatted_message = `[${service} ${method}()]: ${message}`
110
- log(formatted_message)
111
- }
112
-
113
- const error = (message, method = 'unspecified_method') => {
114
- const formatted_message = `[${service} ${method}()]: ${message}`
115
- logger.error(`${formatted_message}`)
116
- }
117
-
118
- const info = (message, method = 'unspecified_method') => {
119
- const formatted_message = `[${service} ${method}()]: ${message}`
120
- logger.info(`${formatted_message} ${message}`)
121
- }
122
-
123
- return { console, error, info }
124
- }
125
-
126
- module.exports = { app_logger, client_logger, logger, mail_logger, morgan: morgan_logger, stream_logger }
@@ -1,50 +0,0 @@
1
- /**
2
- * @author Oguntuberu Nathan O. <nateoguns.work@gmail.com>
3
- **/
4
- const { app_logger } = require('../utilities/logger')
5
- const logger = app_logger('HTTP Middleware')
6
-
7
- module.exports = {
8
- handle_404 (request, response, next) {
9
- const return_data = {
10
- status_code: 404,
11
- success: false,
12
- error: 'Resource not found',
13
- payload: null
14
- }
15
-
16
- next(return_data)
17
- },
18
-
19
- handle_error (error, request, response, next) {
20
- // Log errors
21
- logger.error(error.error || error.message, 'handle_error')
22
-
23
- // return error
24
- return response.status(error.status_code || 500).json({
25
- success: false,
26
- status_code: error.status_code || 500,
27
- error: error.error || 'Internal Server Error',
28
- payload: null
29
- })
30
- },
31
-
32
- process_response (request, response, next) {
33
- if (!request.payload) return next()
34
-
35
- const { status_code } = request.payload
36
- return response.status(status_code).json(request.payload)
37
- },
38
-
39
- setup_request (request, response, next) {
40
- request.headers['access-control-allow-origin'] = '*'
41
- request.headers['access-control-allow-headers'] = '*'
42
-
43
- if (request.method === 'OPTIONS') {
44
- request.headers['access-control-allow-methods'] = 'GET, POST, PUT, PATCH, DELETE'
45
- response.status(200).json()
46
- }
47
-
48
- next()
49
- }
50
- }
package/test.js DELETED
File without changes