@vida-global/core 1.3.1 → 1.3.3
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/.github/workflows/npm-publish.yml +17 -7
- package/.github/workflows/npm-test.yml +5 -3
- package/index.js +8 -13
- package/lib/redis/index.js +1 -1
- package/lib/server/README.md +1 -1
- package/lib/server/apiDocsGenerator.js +44 -13
- package/lib/server/controllerImporter.js +11 -9
- package/lib/server/index.js +2 -0
- package/lib/server/server.js +76 -69
- package/lib/server/serverController.js +169 -57
- package/package.json +2 -2
- package/test/server/server.test.js +5 -3
- package/test/server/serverController.test.js +202 -66
|
@@ -7,14 +7,16 @@ const{ AuthorizationError,
|
|
|
7
7
|
ValidationError } = require('./errors');
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
const AUTH_CALLBACK_NAME = 'authenticateRequest';
|
|
11
|
+
|
|
12
|
+
|
|
10
13
|
class VidaServerController {
|
|
11
14
|
#afterCallbacks = [];
|
|
12
15
|
#beforeCallbacks = [];
|
|
16
|
+
#callbacksSetUp = false;
|
|
13
17
|
#params;
|
|
14
18
|
#rendered = false;
|
|
15
19
|
#request;
|
|
16
|
-
#requestBody;
|
|
17
|
-
#requestQuery;
|
|
18
20
|
#response;
|
|
19
21
|
|
|
20
22
|
|
|
@@ -24,6 +26,8 @@ class VidaServerController {
|
|
|
24
26
|
}
|
|
25
27
|
this.#request = request;
|
|
26
28
|
this.#response = response;
|
|
29
|
+
|
|
30
|
+
this.#applyCallbacks();
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
|
|
@@ -32,15 +36,15 @@ class VidaServerController {
|
|
|
32
36
|
|
|
33
37
|
get params() {
|
|
34
38
|
if (!this.#params) {
|
|
35
|
-
const body = this.#processRequestData('body');
|
|
36
|
-
|
|
37
|
-
this.#params = {...this._request.params, ...body, ...query};
|
|
39
|
+
const body = Buffer.isBuffer(this._request.body) ? {} : this.#processRequestData('body');
|
|
40
|
+
this.#params = {...this._request.params, ...this._request.query, ...body};
|
|
38
41
|
}
|
|
39
42
|
return structuredClone(this.#params);
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
get requestHeaders() { return structuredClone(this._request.headers || {}); }
|
|
43
46
|
get responseHeaders() { return this._response.headers; };
|
|
47
|
+
get requestBody() { return this._request.body; }
|
|
44
48
|
get contentType() { return this.requestHeaders['content-type']; }
|
|
45
49
|
get logger() { return logger; }
|
|
46
50
|
get rendered() { return this.#rendered }
|
|
@@ -51,23 +55,20 @@ class VidaServerController {
|
|
|
51
55
|
|
|
52
56
|
|
|
53
57
|
#processRequestData(key) {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const processedData = {};
|
|
59
|
-
this[instanceVarName] = processedData;
|
|
60
|
-
for (let [paramName, paramValue] of Object.entries(data)) {
|
|
61
|
-
processedData[paramName] = this.#processedRequestDataValue(paramValue)
|
|
62
|
-
}
|
|
58
|
+
const data = this._request[key] || {};
|
|
59
|
+
const processedData = {};
|
|
60
|
+
for (let [paramName, paramValue] of Object.entries(data)) {
|
|
61
|
+
processedData[paramName] = this.#processedRequestDataValue(paramValue)
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
return
|
|
64
|
+
return processedData;
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
|
|
69
68
|
#processedRequestDataValue(value) {
|
|
69
|
+
return value;
|
|
70
70
|
try {
|
|
71
|
+
if (!/^\[|{/.test(value)) return value;
|
|
71
72
|
return JSON.parse(value);
|
|
72
73
|
} catch(err) {
|
|
73
74
|
return value;
|
|
@@ -95,8 +96,6 @@ class VidaServerController {
|
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
this.setupCallbacks();
|
|
99
|
-
|
|
100
99
|
const responseBody = await this.#performAction(action);
|
|
101
100
|
|
|
102
101
|
if (!this.rendered) {
|
|
@@ -109,6 +108,10 @@ class VidaServerController {
|
|
|
109
108
|
|
|
110
109
|
async #performAction(action) {
|
|
111
110
|
if (await this.runBeforeCallbacks(action) === false) return false;
|
|
111
|
+
|
|
112
|
+
const parameters = this.constructor.parametersForAction(action);
|
|
113
|
+
if (parameters) await this.validateParameters(parameters);
|
|
114
|
+
|
|
112
115
|
const responseBody = await this[action]();
|
|
113
116
|
if (await this.runAfterCallbacks(action) === false) return false;
|
|
114
117
|
|
|
@@ -127,12 +130,17 @@ class VidaServerController {
|
|
|
127
130
|
await this.renderNotFoundResponse(err.message);
|
|
128
131
|
|
|
129
132
|
} else if (err instanceof ValidationError) {
|
|
130
|
-
await this.renderErrors(err.message, err.
|
|
133
|
+
await this.renderErrors(err.message, err.fields);
|
|
131
134
|
|
|
132
135
|
} else {
|
|
133
|
-
this.
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
+
this._response.error = err;
|
|
137
|
+
let message;
|
|
138
|
+
if (typeof err == 'string') {
|
|
139
|
+
message = err;
|
|
140
|
+
} else {
|
|
141
|
+
message = err.message;
|
|
142
|
+
}
|
|
143
|
+
await this.renderServerErrorResponse(message);
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
146
|
|
|
@@ -144,7 +152,12 @@ class VidaServerController {
|
|
|
144
152
|
if (typeof body == 'string') {
|
|
145
153
|
this._response.send(body);
|
|
146
154
|
} else {
|
|
147
|
-
body = await this.
|
|
155
|
+
body = await this.processJSONBody(body, options);
|
|
156
|
+
|
|
157
|
+
const errors = body.errors || null;
|
|
158
|
+
delete body.errors;
|
|
159
|
+
|
|
160
|
+
body = this.formatJSONBody(body, errors, options);
|
|
148
161
|
this._response.json(body);
|
|
149
162
|
}
|
|
150
163
|
this.#rendered = true;
|
|
@@ -173,42 +186,54 @@ class VidaServerController {
|
|
|
173
186
|
}
|
|
174
187
|
|
|
175
188
|
|
|
189
|
+
async renderUnauthorizedResponse(message=null) {
|
|
190
|
+
return await this.#renderError(401, message);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async renderForbiddenResponse(message=null) {
|
|
195
|
+
return await this.#renderError(403, message);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
176
199
|
async renderNotFoundResponse(message=null) {
|
|
177
|
-
|
|
178
|
-
await this.render({ message });
|
|
200
|
+
return await this.#renderError(404, message);
|
|
179
201
|
}
|
|
180
202
|
|
|
181
203
|
|
|
182
|
-
async
|
|
183
|
-
this
|
|
184
|
-
await this.render({ message });
|
|
204
|
+
async renderServerErrorResponse(message=null) {
|
|
205
|
+
return await this.#renderError(500, message);
|
|
185
206
|
}
|
|
186
207
|
|
|
187
208
|
|
|
188
|
-
async
|
|
189
|
-
this.statusCode =
|
|
190
|
-
|
|
209
|
+
async #renderError(statusCode, message) {
|
|
210
|
+
this.statusCode = statusCode;
|
|
211
|
+
const body = {errors: {message: message || null}};
|
|
212
|
+
return await this.render(body);
|
|
191
213
|
}
|
|
192
214
|
|
|
193
215
|
|
|
194
|
-
|
|
195
|
-
body = await this._formatJSONBody(body, options);
|
|
216
|
+
formatJSONBody(body, errors, options) {
|
|
196
217
|
if (options.standardize === false) return body;
|
|
197
|
-
|
|
218
|
+
|
|
219
|
+
const response = {data: body, status: this.statusText};
|
|
220
|
+
if (errors) response.errors = errors;
|
|
221
|
+
|
|
222
|
+
return response;
|
|
198
223
|
}
|
|
199
224
|
|
|
200
225
|
|
|
201
|
-
async
|
|
226
|
+
async processJSONBody(body, options) {
|
|
202
227
|
if (body === undefined) return null;
|
|
203
228
|
if (!body || typeof body != 'object') return body;
|
|
204
229
|
|
|
205
230
|
if (body.toApiResponse) {
|
|
206
231
|
const formattedBody = await body.toApiResponse(options);
|
|
207
|
-
return await this.
|
|
232
|
+
return await this.processJSONBody(formattedBody, options);
|
|
208
233
|
|
|
209
234
|
} else if (Array.isArray(body)) {
|
|
210
235
|
for (const idx in body) {
|
|
211
|
-
body[idx] = await this.
|
|
236
|
+
body[idx] = await this.processJSONBody(body[idx], options);
|
|
212
237
|
}
|
|
213
238
|
|
|
214
239
|
} else if (body.constructor == Date) {
|
|
@@ -216,7 +241,7 @@ class VidaServerController {
|
|
|
216
241
|
|
|
217
242
|
} else {
|
|
218
243
|
for (let [key, val] of Object.entries(body)) {
|
|
219
|
-
body[key] = await this.
|
|
244
|
+
body[key] = await this.processJSONBody(val, options);
|
|
220
245
|
}
|
|
221
246
|
}
|
|
222
247
|
|
|
@@ -246,17 +271,7 @@ class VidaServerController {
|
|
|
246
271
|
* HELPERS
|
|
247
272
|
***********************************************************************************************/
|
|
248
273
|
static autoLoadHelpers() {
|
|
249
|
-
|
|
250
|
-
this._autoLoadHelpers()
|
|
251
|
-
} catch(err) {
|
|
252
|
-
if (err.message.includes(`Cannot find module '${this.autoLoadHelperPath}'`)) return;
|
|
253
|
-
throw err;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
static _autoLoadHelpers() {
|
|
259
|
-
const { InstanceMethods, Accessors } = require(this.autoLoadHelperPath);
|
|
274
|
+
const { InstanceMethods, Accessors } = this._autoLoadModule(this.autoLoadHelperPath);
|
|
260
275
|
|
|
261
276
|
if (InstanceMethods) Object.assign(this.prototype, InstanceMethods);
|
|
262
277
|
|
|
@@ -268,11 +283,34 @@ class VidaServerController {
|
|
|
268
283
|
}
|
|
269
284
|
|
|
270
285
|
|
|
286
|
+
static autoLoadDocumentation() {
|
|
287
|
+
const docs = this._autoLoadModule(this.autoLoadDocumentationPath);
|
|
288
|
+
Object.assign(this, docs);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
static _autoLoadModule(path) {
|
|
293
|
+
try {
|
|
294
|
+
return require(path);
|
|
295
|
+
} catch(err) {
|
|
296
|
+
const errorText = `Cannot find module '${path}'`;
|
|
297
|
+
if (!err.message.includes(errorText)) throw err;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
271
304
|
static get autoLoadHelperPath() {
|
|
272
305
|
return `${process.cwd()}/lib/controllers${this.routePrefix}Controller.js`;
|
|
273
306
|
}
|
|
274
307
|
|
|
275
308
|
|
|
309
|
+
static get autoLoadDocumentationPath() {
|
|
310
|
+
return `${process.cwd()}/lib/controllers/_docs${this.routePrefix}Controller.js`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
276
314
|
/***********************************************************************************************
|
|
277
315
|
* CALLBACKS
|
|
278
316
|
***********************************************************************************************/
|
|
@@ -280,6 +318,13 @@ class VidaServerController {
|
|
|
280
318
|
setupCallbacks() {}
|
|
281
319
|
|
|
282
320
|
|
|
321
|
+
#applyCallbacks() {
|
|
322
|
+
// set up the callbacks when the instance is constructed and prevent it from being called again
|
|
323
|
+
this.setupCallbacks();
|
|
324
|
+
this.setupCallbacks = undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
283
328
|
beforeCallback(callback, options={}) {
|
|
284
329
|
this.#addCallback(callback, options, this.#beforeCallbacks);
|
|
285
330
|
}
|
|
@@ -340,7 +385,7 @@ class VidaServerController {
|
|
|
340
385
|
/***********************************************************************************************
|
|
341
386
|
* VALIDATIONS
|
|
342
387
|
***********************************************************************************************/
|
|
343
|
-
|
|
388
|
+
validateParameters(validations) {
|
|
344
389
|
const errors = {};
|
|
345
390
|
for (const [parameterName, parameterValidations] of Object.entries(validations)) {
|
|
346
391
|
const parameterErrors = this.validateParameter(this.params[parameterName], parameterValidations);
|
|
@@ -353,8 +398,12 @@ class VidaServerController {
|
|
|
353
398
|
|
|
354
399
|
validateParameter(value, parameterValidations) {
|
|
355
400
|
const errors = [];
|
|
401
|
+
|
|
402
|
+
if (parameterValidations?.optional && value === undefined) return errors;
|
|
403
|
+
|
|
356
404
|
for (const [validationType, validationOptions] of Object.entries(parameterValidations)) {
|
|
357
405
|
const validationMethod = `validate${camelize(validationType)}`;
|
|
406
|
+
if (!this[validationMethod]) continue;
|
|
358
407
|
const error = this[validationMethod](value, validationOptions);
|
|
359
408
|
if (error) errors.push(error);
|
|
360
409
|
}
|
|
@@ -370,10 +419,29 @@ class VidaServerController {
|
|
|
370
419
|
|
|
371
420
|
validateIsInteger(value, options) {
|
|
372
421
|
const num = parseFloat(value);
|
|
373
|
-
if (
|
|
374
|
-
|
|
422
|
+
if (!/^-?\d+$/.test(value) || isNaN(num) || !Number.isInteger(num)) return 'must be an integer';
|
|
423
|
+
|
|
424
|
+
if (options?.gte !== undefined) {
|
|
375
425
|
if (num < options.gte) return `must be greater than or equal to ${options.gte}`;
|
|
376
426
|
}
|
|
427
|
+
if (options?.lte !== undefined) {
|
|
428
|
+
if (num > options.lte) return `must be less than or equal to ${options.lte}`;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
validateIsString(value, options) {
|
|
434
|
+
if (typeof value !== 'string') return 'must be a string';
|
|
435
|
+
|
|
436
|
+
if (options?.length?.gte !== undefined) {
|
|
437
|
+
if (value.length < options.length.gte) return `must be greater than or equal to ${options.length.gte} characters`;
|
|
438
|
+
}
|
|
439
|
+
if (options?.length?.lte !== undefined) {
|
|
440
|
+
if (value.length > options.length.lte) return `must be fewer than or equal to ${options.length.lte} characters`;
|
|
441
|
+
}
|
|
442
|
+
if (options?.regex !== undefined) {
|
|
443
|
+
if (!options.regex.test(value)) return `must match ${options.regex}`;
|
|
444
|
+
}
|
|
377
445
|
}
|
|
378
446
|
|
|
379
447
|
|
|
@@ -389,11 +457,6 @@ class VidaServerController {
|
|
|
389
457
|
}
|
|
390
458
|
|
|
391
459
|
|
|
392
|
-
validateRegex(value, regex) {
|
|
393
|
-
if (!regex.test(value)) return `must match the pattern ${regex}`;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
460
|
validateFunction(value, fnc) {
|
|
398
461
|
const error = fnc(value);
|
|
399
462
|
if (error) return error;
|
|
@@ -482,7 +545,56 @@ class VidaServerController {
|
|
|
482
545
|
|
|
483
546
|
|
|
484
547
|
static get actionRegExp() {
|
|
485
|
-
return new RegExp(/^(?<method>get|post|put|delete|patch)(?<path>[A-Z][a-zA-Z]+)$/);
|
|
548
|
+
return new RegExp(/^(?<method>get|post|put|delete|patch|head)(?<path>[A-Z][a-zA-Z]+)$/);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
/***********************************************************************************************
|
|
553
|
+
* DOCUMENTATION
|
|
554
|
+
***********************************************************************************************/
|
|
555
|
+
static parametersForAction(actionName) {
|
|
556
|
+
const methodName = this.parametersMethodForAction(actionName);
|
|
557
|
+
if (this[methodName]) return this[methodName]();
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
static parametersMethodForAction(actionName) {
|
|
563
|
+
return `parametersFor${camelize(actionName)}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
static documentationForAction(actionName) {
|
|
568
|
+
const methodName = this.documentationMethodForAction(actionName);
|
|
569
|
+
if (this[methodName]) return this[methodName]();
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
static documentationMethodForAction(actionName) {
|
|
575
|
+
return `document${camelize(actionName)}`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
isAuthenticatedAction(actionName) {
|
|
580
|
+
const { options } = this.#beforeCallbacks.find(({ callback, options }) => callback == AUTH_CALLBACK_NAME)
|
|
581
|
+
return Boolean(options) && this.#shouldRunCallback(actionName, options);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
static isAuthenticatedAction(actionName) {
|
|
586
|
+
const controller = new this();
|
|
587
|
+
return controller.isAuthenticatedAction(actionName);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
static _authenticationDocumentation(actionName) {
|
|
592
|
+
if (!this.isAuthenticatedAction(actionName)) return undefined;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
static authenticationDocumentation(actionName) {
|
|
597
|
+
return {apiKeyAuth: []};
|
|
486
598
|
}
|
|
487
599
|
}
|
|
488
600
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vida-global/core",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "Core libraries for supporting Vida development",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "ISC",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@vida-global/release": "^1.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"jest": "^
|
|
36
|
+
"jest": "^29.0.0",
|
|
37
37
|
"jest-express": "^1.12.0",
|
|
38
38
|
"jest-extended": "^7.0.0",
|
|
39
39
|
"@vida-global/test-helpers": "^1.0.1"
|
|
@@ -8,12 +8,14 @@ const { VidaServer } = require('../../lib/server');
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
jest.mock('express', () => {
|
|
11
|
+
const express = require('jest-express');
|
|
11
12
|
const { Response } = require('jest-express/lib/response');
|
|
12
13
|
|
|
13
|
-
// missing functions in mock
|
|
14
|
-
Response.prototype.on
|
|
14
|
+
// missing functions in mock express
|
|
15
|
+
Response.prototype.on = jest.fn();
|
|
16
|
+
express.raw = jest.fn();
|
|
15
17
|
|
|
16
|
-
return
|
|
18
|
+
return express;
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
|