@vida-global/core 2.0.0 → 2.0.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/README.md +8 -7
- package/config/newrelic-config.js +2 -0
- package/lib/activeRecord/README.md +220 -111
- package/lib/http/README.md +84 -19
- package/lib/jobQueue/README.md +139 -46
- package/lib/logger/README.md +48 -5
- package/lib/server/README.md +282 -129
- package/lib/server/controllerImporter.js +1 -1
- package/lib/server/controllerMixins/callbacks.js +132 -0
- package/lib/server/controllerMixins/documentation.js +36 -0
- package/lib/server/controllerMixins/renderer.js +315 -0
- package/lib/server/controllerMixins/requestDetails.js +82 -0
- package/lib/server/controllerMixins/routing.js +108 -0
- package/lib/server/controllerMixins/validations.js +114 -0
- package/lib/server/server.js +2 -0
- package/lib/server/serverController.js +75 -672
- package/package.json +8 -5
- package/test/server/serverController.test.js +868 -22
- package/test/apm/agent.test.js +0 -56
- package/test/apm/utils.test.js +0 -121
|
@@ -1,27 +1,16 @@
|
|
|
1
|
-
const APM
|
|
2
|
-
const { camelize
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const AUTH_CALLBACK_NAME = 'authenticateRequest';
|
|
9
|
-
const actionStore = new WeakMap();
|
|
1
|
+
const APM = require('../apm');
|
|
2
|
+
const { camelize } = require('inflection');
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const Errors = require('./errors');
|
|
5
|
+
const { logger } = require('../logger');
|
|
10
6
|
|
|
11
7
|
|
|
12
8
|
class VidaServerController {
|
|
13
|
-
#afterCallbacks = [];
|
|
14
|
-
#afterCallbacksToSkip = [];
|
|
15
|
-
#beforeCallbacks = [];
|
|
16
|
-
#beforeCallbacksToSkip = [];
|
|
17
|
-
#callbacksSetUp = false;
|
|
18
|
-
#isStreaming = false;
|
|
19
9
|
#logger;
|
|
20
10
|
#params;
|
|
21
|
-
#rendered
|
|
11
|
+
#rendered = false;
|
|
22
12
|
#request;
|
|
23
13
|
#response;
|
|
24
|
-
#streamAbortController;
|
|
25
14
|
|
|
26
15
|
|
|
27
16
|
constructor(request, response, serverLogger) {
|
|
@@ -32,75 +21,18 @@ class VidaServerController {
|
|
|
32
21
|
this.#response = response;
|
|
33
22
|
this.#logger = (serverLogger || logger).child(this.requestId);
|
|
34
23
|
|
|
35
|
-
this
|
|
36
|
-
this
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
get _request() { return this.#request };
|
|
41
|
-
get _response() { return this.#response };
|
|
42
|
-
|
|
43
|
-
get params() {
|
|
44
|
-
if (!this.#params) {
|
|
45
|
-
const body = Buffer.isBuffer(this._request.body) ? {} : this.#processRequestData('body');
|
|
46
|
-
this.#params = {...this._request.params, ...this._request.query, ...body};
|
|
47
|
-
}
|
|
48
|
-
return structuredClone(this.#params);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
get requestId() { return this._request.id; }
|
|
52
|
-
get requestHeaders() { return structuredClone(this._request.headers || {}); }
|
|
53
|
-
get responseHeaders() { return this._response.headers; };
|
|
54
|
-
get requestBody() { return this._request.body; }
|
|
55
|
-
get requestMethod() { return this._request.method; }
|
|
56
|
-
get requestIp() { return this._request.ip; }
|
|
57
|
-
get url() { return this._request.originalUrl; }
|
|
58
|
-
get contentType() { return this.requestHeaders['content-type']; }
|
|
59
|
-
get userAgent() { return this.requestHeaders['user-agent']; }
|
|
60
|
-
|
|
61
|
-
get statusCode() { return this._response.statusCode; }
|
|
62
|
-
set statusCode(_status) { this._response.statusCode = _status; }
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
get logger() {
|
|
66
|
-
return this.#logger;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
get bearerToken() {
|
|
71
|
-
const auth = this.requestHeaders.authorization;
|
|
72
|
-
if (!auth) return null;
|
|
73
|
-
|
|
74
|
-
const match = /^\s*bearer\s+(.+)$/i.exec(auth);
|
|
75
|
-
if (!match) return null;
|
|
76
|
-
return match[1].trim();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
#processRequestData(key) {
|
|
81
|
-
const data = this._request[key] || {};
|
|
82
|
-
const processedData = {};
|
|
83
|
-
for (let [paramName, paramValue] of Object.entries(data)) {
|
|
84
|
-
processedData[paramName] = this.#processedRequestDataValue(paramValue)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return processedData;
|
|
24
|
+
this._applyCallbacks();
|
|
25
|
+
this._setupStatusRenderers();
|
|
88
26
|
}
|
|
89
27
|
|
|
90
28
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (!/^\[|{/.test(value)) return value;
|
|
95
|
-
return JSON.parse(value);
|
|
96
|
-
} catch(err) {
|
|
97
|
-
return value;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
29
|
+
get _request() { return this.#request }
|
|
30
|
+
get _response() { return this.#response }
|
|
31
|
+
get logger() { return this.#logger }
|
|
100
32
|
|
|
101
33
|
|
|
102
34
|
/***********************************************************************************************
|
|
103
|
-
* PERFORM
|
|
35
|
+
* PERFORM REQUEST
|
|
104
36
|
***********************************************************************************************/
|
|
105
37
|
async performRequest(action) {
|
|
106
38
|
try {
|
|
@@ -113,32 +45,30 @@ class VidaServerController {
|
|
|
113
45
|
|
|
114
46
|
|
|
115
47
|
async #performRequest(action) {
|
|
116
|
-
await
|
|
117
|
-
|
|
118
|
-
if (this.rendered) {
|
|
119
|
-
this.#response.end();
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
48
|
+
await this.#setupRequestState()
|
|
49
|
+
if (this.rendered) return this.#response.end();
|
|
122
50
|
|
|
123
51
|
const responseBody = await this.#performAction(action);
|
|
124
52
|
|
|
125
|
-
if (!this.rendered) {
|
|
126
|
-
await APM.startSegment('render', true, () => this.render(responseBody || {}));
|
|
127
|
-
}
|
|
53
|
+
if (!this.rendered) await this.render(responseBody || {});
|
|
128
54
|
|
|
129
55
|
this.#response.end();
|
|
130
56
|
}
|
|
131
57
|
|
|
132
58
|
|
|
59
|
+
async #setupRequestState() {
|
|
60
|
+
return await APM.startSegment('setupRequestState', true, () => this.setupRequestState?.());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
133
64
|
async #performAction(action) {
|
|
134
|
-
if (await
|
|
65
|
+
if (!await this._runCallbacks('before', action)) return false;
|
|
135
66
|
|
|
136
|
-
|
|
137
|
-
if (parameters) await APM.startSegment('validateParameters', true, () => this.validateParameters(parameters));
|
|
67
|
+
await APM.startSegment('validateParameters', true, () => this.validateAction(action));
|
|
138
68
|
|
|
139
69
|
const responseBody = await APM.startSegment(`action:${action}`, true, () => this[action]());
|
|
140
70
|
|
|
141
|
-
if (await
|
|
71
|
+
if (!await this._runCallbacks('after', action)) return false;
|
|
142
72
|
|
|
143
73
|
return responseBody;
|
|
144
74
|
}
|
|
@@ -172,287 +102,41 @@ class VidaServerController {
|
|
|
172
102
|
}
|
|
173
103
|
|
|
174
104
|
|
|
175
|
-
reportError(err) {
|
|
176
|
-
APM.reportError(err);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
/***********************************************************************************************
|
|
181
|
-
* RESPONSE RENDERING
|
|
182
|
-
***********************************************************************************************/
|
|
183
|
-
async render(body, options={}) {
|
|
184
|
-
if (this.rendered) return;
|
|
185
|
-
if (this.#isStreaming) return;
|
|
186
|
-
|
|
187
|
-
if (typeof body == 'string') {
|
|
188
|
-
this._response.send(body);
|
|
189
|
-
} else {
|
|
190
|
-
body = await this.processJSONBody(body, options);
|
|
191
|
-
|
|
192
|
-
const errors = body.errors || null;
|
|
193
|
-
delete body.errors;
|
|
194
|
-
|
|
195
|
-
body = this.formatJSONBody(body, errors, options);
|
|
196
|
-
this._response.json(body);
|
|
197
|
-
}
|
|
198
|
-
this.markRendered();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
async renderErrors(message, fields) {
|
|
203
|
-
const errors = {message: null, fields: {}};
|
|
204
|
-
|
|
205
|
-
if (message && !fields && typeof message == 'object') {
|
|
206
|
-
fields = message;
|
|
207
|
-
message = null;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (fields) {
|
|
211
|
-
for (const [field, values] of Object.entries(fields)) {
|
|
212
|
-
if (!Array.isArray(values)) fields[field] = [values];
|
|
213
|
-
}
|
|
214
|
-
errors.fields = fields;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
await this.renderBadRequest(message, { errors });
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
formatJSONBody(body, errors, options) {
|
|
222
|
-
if (options.standardize === false) return body;
|
|
223
|
-
|
|
224
|
-
const response = {data: body, status: this.statusText};
|
|
225
|
-
if (errors) response.errors = errors;
|
|
226
|
-
|
|
227
|
-
return response;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
async processJSONBody(body, options) {
|
|
232
|
-
if (body === undefined) return null;
|
|
233
|
-
if (!body || typeof body != 'object') return body;
|
|
234
|
-
|
|
235
|
-
let processedBody;
|
|
236
|
-
|
|
237
|
-
if (body.toApiResponse) {
|
|
238
|
-
const formattedBody = await body.toApiResponse(options);
|
|
239
|
-
return await this.processJSONBody(formattedBody, options);
|
|
240
|
-
|
|
241
|
-
} else if (Array.isArray(body)) {
|
|
242
|
-
const processed = [];
|
|
243
|
-
for (const idx in body) {
|
|
244
|
-
processed.push(await this.processJSONBody(body[idx], options));
|
|
245
|
-
}
|
|
246
|
-
return processed;
|
|
247
|
-
|
|
248
|
-
} else if (body.constructor == Date) {
|
|
249
|
-
return body.getTime();
|
|
250
|
-
|
|
251
|
-
} else {
|
|
252
|
-
const processed = {};
|
|
253
|
-
for (let [key, val] of Object.entries(body)) {
|
|
254
|
-
processed[key] = await this.processJSONBody(val, options);
|
|
255
|
-
}
|
|
256
|
-
return processed;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
get statusText() {
|
|
262
|
-
switch(this.statusCode) {
|
|
263
|
-
case 200:
|
|
264
|
-
return 'ok';
|
|
265
|
-
case 201:
|
|
266
|
-
return 'created';
|
|
267
|
-
case 202:
|
|
268
|
-
return 'accepted';
|
|
269
|
-
case 204:
|
|
270
|
-
return 'no content';
|
|
271
|
-
case 301:
|
|
272
|
-
return 'moved permanently';
|
|
273
|
-
case 302:
|
|
274
|
-
return 'found';
|
|
275
|
-
case 304:
|
|
276
|
-
return 'not modified';
|
|
277
|
-
case 307:
|
|
278
|
-
return 'temporary redirect';
|
|
279
|
-
case 308:
|
|
280
|
-
return 'permanent redirect';
|
|
281
|
-
case 400:
|
|
282
|
-
return 'bad request'
|
|
283
|
-
case 401:
|
|
284
|
-
return 'unauthorized';
|
|
285
|
-
case 402:
|
|
286
|
-
return 'payment required';
|
|
287
|
-
case 403:
|
|
288
|
-
return 'forbidden';
|
|
289
|
-
case 404:
|
|
290
|
-
return 'not found';
|
|
291
|
-
case 405:
|
|
292
|
-
return 'method not allowed';
|
|
293
|
-
case 408:
|
|
294
|
-
return 'request timeout';
|
|
295
|
-
case 409:
|
|
296
|
-
return 'conflict';
|
|
297
|
-
case 410:
|
|
298
|
-
return 'gone';
|
|
299
|
-
case 413:
|
|
300
|
-
return 'content too large';
|
|
301
|
-
case 415:
|
|
302
|
-
return 'unsupported media type';
|
|
303
|
-
case 422:
|
|
304
|
-
return 'unprocessable content';
|
|
305
|
-
case 423:
|
|
306
|
-
return 'locked';
|
|
307
|
-
case 429:
|
|
308
|
-
return 'too many requests';
|
|
309
|
-
case 500:
|
|
310
|
-
return 'server error';
|
|
311
|
-
case 501:
|
|
312
|
-
return 'not implemented';
|
|
313
|
-
case 502:
|
|
314
|
-
return 'bad gateway';
|
|
315
|
-
case 503:
|
|
316
|
-
return 'service unavailable';
|
|
317
|
-
case 504:
|
|
318
|
-
return 'gateway timeout';
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
#setupStatusRenderers() {
|
|
324
|
-
const successStatuses = {
|
|
325
|
-
CreationSuccessful: 201,
|
|
326
|
-
Accepted: 202,
|
|
327
|
-
NoConent: 204,
|
|
328
|
-
MovedPermanently: 301,
|
|
329
|
-
Found: 302,
|
|
330
|
-
NotModified: 304,
|
|
331
|
-
TemporaryRedirect: 307,
|
|
332
|
-
PermanentRedirect: 308,
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
for (let [type, statusCode] of Object.entries(Errors.Statuses)) {
|
|
336
|
-
if (type == 'Server') type = 'ServerError';
|
|
337
|
-
this[`render${type}`] = async function(message=null, body=null) {
|
|
338
|
-
return await this.#renderError(statusCode, message, body);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
for (let [type, statusCode] of Object.entries(successStatuses)) {
|
|
343
|
-
this[`render${type}`] = async function(body=null) {
|
|
344
|
-
return await this.#renderSuccess(statusCode, body);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
async #renderError(statusCode, message, body) {
|
|
351
|
-
this.statusCode = statusCode;
|
|
352
|
-
body = structuredClone(body || {});
|
|
353
|
-
body.errors = body.errors || {};
|
|
354
|
-
body.errors.message = message || null;
|
|
355
|
-
return await this.render(body);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
async #renderSuccess(statusCode, body) {
|
|
360
|
-
this.statusCode = statusCode;
|
|
361
|
-
return await this.render(body);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
setHeader(header, value) {
|
|
366
|
-
this.#response.setHeader(header, value);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
flushHeaders() {
|
|
371
|
-
this.#response.flushHeaders();
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
|
|
375
105
|
get rendered() { return this.#rendered; }
|
|
376
106
|
markRendered() { this.#rendered = true; }
|
|
377
107
|
get writableEnded() { return this.#response.writableEnded; }
|
|
108
|
+
get isStreaming() { return Boolean(this._isStreaming); }
|
|
378
109
|
|
|
379
110
|
|
|
380
111
|
/***********************************************************************************************
|
|
381
|
-
*
|
|
112
|
+
* MIXINS
|
|
382
113
|
***********************************************************************************************/
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
let closeHandler;
|
|
388
|
-
if (abortController) {
|
|
389
|
-
this.#streamAbortController = abortController;
|
|
390
|
-
closeHandler = () => abortController.abort();
|
|
391
|
-
this.#request.on('close', closeHandler);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
this.#setSseHeaders();
|
|
395
|
-
this.markRendered();
|
|
396
|
-
try {
|
|
397
|
-
await handler.bind(this)();
|
|
398
|
-
} catch(err) {
|
|
399
|
-
await errorHandler(err);
|
|
400
|
-
throw err;
|
|
401
|
-
} finally {
|
|
402
|
-
this.writeStreamEvent(null, '[DONE]');
|
|
403
|
-
this.#response.end();
|
|
404
|
-
|
|
405
|
-
this.#isStreaming = false;
|
|
406
|
-
this.#streamAbortController = null;
|
|
407
|
-
|
|
408
|
-
if (closeHandler) this.#request.off('close', closeHandler);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
#setSseHeaders() {
|
|
414
|
-
this.statusCode = 200;
|
|
415
|
-
this.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
416
|
-
this.setHeader('Cache-Control', 'no-cache');
|
|
417
|
-
this.setHeader('Connection', 'keep-alive');
|
|
418
|
-
this.setHeader('X-Accel-Buffering', 'no');
|
|
419
|
-
this.flushHeaders();
|
|
114
|
+
static autoLoadMixins() {
|
|
115
|
+
this.actions; // make sure that actions are cached before loading helpers
|
|
116
|
+
const mixin = this._autoLoadModule(this.mixinDirectory);
|
|
117
|
+
this.loadMixin(mixin);
|
|
420
118
|
}
|
|
421
119
|
|
|
422
120
|
|
|
423
|
-
|
|
424
|
-
if (
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (evt) {
|
|
428
|
-
this.#response.write(`event: ${evt}\n`);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const msg = `data: ${data}\n\n`;
|
|
432
|
-
this.#response.write(msg);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
/***********************************************************************************************
|
|
437
|
-
* HELPERS
|
|
438
|
-
***********************************************************************************************/
|
|
439
|
-
static autoLoadHelpers() {
|
|
440
|
-
this.actions; // make sure that actions are cached before loading helpers
|
|
441
|
-
const { StaticMethods, InstanceMethods, Accessors } = this._autoLoadModule(this.autoLoadHelperPath);
|
|
121
|
+
static loadMixin(mixin) {
|
|
122
|
+
if (typeof mixin == 'function') mixin = mixin();
|
|
123
|
+
const { InstanceMethods, StaticMethods, Accessors={}, StaticAccessors={} } = mixin;
|
|
442
124
|
|
|
443
125
|
if (StaticMethods) Object.assign(this, StaticMethods);
|
|
444
126
|
if (InstanceMethods) Object.assign(this.prototype, InstanceMethods);
|
|
445
127
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
128
|
+
for (const [attrName, attrAccessors] of Object.entries(Accessors)) {
|
|
129
|
+
Object.defineProperty(this.prototype, attrName, attrAccessors);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const [attrName, attrAccessors] of Object.entries(StaticAccessors)) {
|
|
133
|
+
Object.defineProperty(this, attrName, attrAccessors);
|
|
450
134
|
}
|
|
451
135
|
}
|
|
452
136
|
|
|
453
137
|
|
|
454
138
|
static autoLoadDocumentation() {
|
|
455
|
-
const docs = this._autoLoadModule(this.
|
|
139
|
+
const docs = this._autoLoadModule(this.documentationDirectory);
|
|
456
140
|
Object.assign(this, docs);
|
|
457
141
|
}
|
|
458
142
|
|
|
@@ -469,103 +153,16 @@ class VidaServerController {
|
|
|
469
153
|
}
|
|
470
154
|
|
|
471
155
|
|
|
472
|
-
static get
|
|
156
|
+
static get mixinDirectory() {
|
|
473
157
|
return `${process.cwd()}/lib/controllers${this.routePrefix}Controller`;
|
|
474
158
|
}
|
|
475
159
|
|
|
476
160
|
|
|
477
|
-
static get
|
|
161
|
+
static get documentationDirectory() {
|
|
478
162
|
return `${process.cwd()}/lib/controllers/_docs${this.routePrefix}Controller`;
|
|
479
163
|
}
|
|
480
164
|
|
|
481
165
|
|
|
482
|
-
/***********************************************************************************************
|
|
483
|
-
* CALLBACKS
|
|
484
|
-
***********************************************************************************************/
|
|
485
|
-
async setupRequestState() {}
|
|
486
|
-
setupCallbacks() {}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
#applyCallbacks() {
|
|
490
|
-
// set up the callbacks when the instance is constructed and prevent it from being called again
|
|
491
|
-
this.setupCallbacks();
|
|
492
|
-
this.setupCallbacks = undefined;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
beforeCallback(callback, options={}) {
|
|
497
|
-
this.#addCallback(callback, options, this.#beforeCallbacks);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
afterCallback(callback, options={}) {
|
|
502
|
-
this.#addCallback(callback, options, this.#afterCallbacks);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
skipBeforeCallback(callback, options={}) {
|
|
507
|
-
this.#addCallback(callback, options, this.#beforeCallbacksToSkip);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
skipAfterCallback(callback, options={}) {
|
|
512
|
-
this.#addCallback(callback, options, this.#afterCallbacksToSkip);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
#addCallback(callback, options, callbacksList) {
|
|
517
|
-
callbacksList.push({ callback, options });
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
async runBeforeCallbacks(actionName) {
|
|
522
|
-
return await this.#runCallbacks(this.#beforeCallbacks, this.#beforeCallbacksToSkip, actionName);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
async runAfterCallbacks(actionName) {
|
|
527
|
-
return await this.#runCallbacks(this.#afterCallbacks, this.#afterCallbacksToSkip, actionName);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
async #runCallbacks(callbacksList, callbacksToSkipList, actionName) {
|
|
532
|
-
let toSkip = callbacksToSkipList.filter(({ callback, options }) => {
|
|
533
|
-
return this.#shouldRunCallback(actionName, options);
|
|
534
|
-
});
|
|
535
|
-
toSkip = toSkip.map(({ callback }) => callback);
|
|
536
|
-
|
|
537
|
-
for (const { callback, options } of callbacksList) {
|
|
538
|
-
if (!this.#shouldRunCallback(actionName, options)) continue;
|
|
539
|
-
if (toSkip.includes(callback)) continue;
|
|
540
|
-
const result = await this.#runCallback(callback)
|
|
541
|
-
if (result === false) return false;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
return true;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
async #runCallback(callback) {
|
|
549
|
-
if (typeof callback == 'function') return await callback.call(this);
|
|
550
|
-
return await this[callback]();
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
#shouldRunCallback(actionName, options) {
|
|
555
|
-
if (options.only) {
|
|
556
|
-
if (Array.isArray(options.only)) return options.only.includes(actionName);
|
|
557
|
-
return options.only == actionName;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
if (options.except) {
|
|
561
|
-
if (Array.isArray(options.except)) return !options.except.includes(actionName);
|
|
562
|
-
return options.except != actionName;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
return true;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
|
|
569
166
|
/***********************************************************************************************
|
|
570
167
|
* REQUEST PARAMETERS
|
|
571
168
|
***********************************************************************************************/
|
|
@@ -574,6 +171,13 @@ class VidaServerController {
|
|
|
574
171
|
}
|
|
575
172
|
|
|
576
173
|
|
|
174
|
+
coerceString(value, { fallback = '' } = {}) {
|
|
175
|
+
if (typeof value !== 'string') return fallback;
|
|
176
|
+
const trimmed = value.trim();
|
|
177
|
+
return trimmed || fallback;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
577
181
|
coerceBoolean(value, { fallback } = {}) {
|
|
578
182
|
if (typeof value === 'boolean') return value;
|
|
579
183
|
if (typeof value !== 'string') return fallback;
|
|
@@ -604,232 +208,6 @@ class VidaServerController {
|
|
|
604
208
|
}
|
|
605
209
|
|
|
606
210
|
|
|
607
|
-
/***********************************************************************************************
|
|
608
|
-
* VALIDATIONS
|
|
609
|
-
***********************************************************************************************/
|
|
610
|
-
async validateParameters(validations) {
|
|
611
|
-
const errors = {};
|
|
612
|
-
for (const [parameterName, parameterValidations] of Object.entries(validations)) {
|
|
613
|
-
const parameterErrors = await this.validateParameter(this.params[parameterName], parameterValidations);
|
|
614
|
-
if (parameterErrors.length) errors[parameterName] = parameterErrors;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
if (Object.keys(errors).length) throw new Errors.ValidationError('', errors);
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
async validateParameter(value, parameterValidations) {
|
|
622
|
-
const errors = [];
|
|
623
|
-
|
|
624
|
-
if (parameterValidations?.optional && value === undefined) return errors;
|
|
625
|
-
|
|
626
|
-
for (const [validationType, validationOptions] of Object.entries(parameterValidations)) {
|
|
627
|
-
const validationMethod = `validate${camelize(validationType)}`;
|
|
628
|
-
if (!this[validationMethod]) continue;
|
|
629
|
-
const error = await this[validationMethod](value, validationOptions);
|
|
630
|
-
if (error) errors.push(error);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
return errors;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
validatePresence(value) {
|
|
638
|
-
if (!value) return 'required';
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
validateIsInteger(value, options) {
|
|
643
|
-
const num = parseFloat(value);
|
|
644
|
-
if (!/^-?\d+$/.test(value) || isNaN(num) || !Number.isInteger(num)) return 'must be an integer';
|
|
645
|
-
|
|
646
|
-
if (options?.gte !== undefined) {
|
|
647
|
-
if (num < options.gte) return `must be greater than or equal to ${options.gte}`;
|
|
648
|
-
}
|
|
649
|
-
if (options?.lte !== undefined) {
|
|
650
|
-
if (num > options.lte) return `must be less than or equal to ${options.lte}`;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
validateIsString(value, options) {
|
|
656
|
-
if (typeof value !== 'string') return 'must be a string';
|
|
657
|
-
|
|
658
|
-
if (options?.length?.gte !== undefined) {
|
|
659
|
-
if (value.length < options.length.gte) return `must be greater than or equal to ${options.length.gte} characters`;
|
|
660
|
-
}
|
|
661
|
-
if (options?.length?.lte !== undefined) {
|
|
662
|
-
if (value.length > options.length.lte) return `must be fewer than or equal to ${options.length.lte} characters`;
|
|
663
|
-
}
|
|
664
|
-
if (options?.regex !== undefined) {
|
|
665
|
-
if (!options.regex.test(value)) return `must match ${options.regex}`;
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
validateIsDateTime(value) {
|
|
671
|
-
if (new Date(value) == 'Invalid Date') return 'invalid date';
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
validateIsEnum(value, options) {
|
|
676
|
-
if (!options.enums.includes(value)) {
|
|
677
|
-
return options.error || `must be one of [${options.enums.filter(Boolean).join(', ')}]`;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
validateIsBoolean(value, bool) {
|
|
683
|
-
if (typeof value == 'boolean' && value === bool) return;
|
|
684
|
-
return `must be ${bool}`;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
async validateFunction(value, fnc) {
|
|
689
|
-
const error = await fnc.call(this, value);
|
|
690
|
-
if (error) return error;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
/***********************************************************************************************
|
|
695
|
-
* ACTION SETUP
|
|
696
|
-
************************************************************************************************
|
|
697
|
-
* The controller looks for any methods that fit the pattern of `{httpMethod}ActionName`
|
|
698
|
-
* (e.g. getFooBar) and prepares server routes for them. By default, a method `getFooBar` on a
|
|
699
|
-
* `BazController` would have an endpoint of `GET /baz/fooBar`. The server will also prepend
|
|
700
|
-
* directory names, so if `bazController.js` is nested within the `ban` directory
|
|
701
|
-
* (e.g. `ban/bazController.js`), the endpoint will be `GET /ban/baz/fooBar`.
|
|
702
|
-
*
|
|
703
|
-
* Default routing logic can be overriden by defining custom routes in the `routes` getter. To
|
|
704
|
-
* provider a custom route for `getFooBar`, `routes` should return an object like:
|
|
705
|
-
* {getFooBar: '/ban/:accountIdd/foo/:userId/bar'}`
|
|
706
|
-
***********************************************************************************************/
|
|
707
|
-
static get actions() {
|
|
708
|
-
if (actionStore.has(this)) return actionStore.get(this);
|
|
709
|
-
const actionNames = this.actionNames;
|
|
710
|
-
const actions = actionNames.map((actionName => {
|
|
711
|
-
return this._constructAction(actionName);
|
|
712
|
-
}).bind(this));
|
|
713
|
-
|
|
714
|
-
actionStore.set(this, actions);
|
|
715
|
-
return actions;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
static _constructAction(actionName) {
|
|
720
|
-
const routePrefix = this.routePrefix
|
|
721
|
-
const action = {};
|
|
722
|
-
const match = actionName.match(this.actionRegExp);
|
|
723
|
-
|
|
724
|
-
action.method = match.groups.method.toUpperCase();
|
|
725
|
-
action.action = actionName;
|
|
726
|
-
action.path = this._routeForAction(action.method, match.groups.path, routePrefix, actionName);
|
|
727
|
-
return action;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
static _routeForAction(method, path, routePrefix, actionName) {
|
|
732
|
-
if (this.routes[actionName]) return this.routes[actionName];
|
|
733
|
-
|
|
734
|
-
// If the path starts with Record, use :id as a path parameter
|
|
735
|
-
/*
|
|
736
|
-
class UsersController {
|
|
737
|
-
// Get /users
|
|
738
|
-
async getIndex() {}
|
|
739
|
-
|
|
740
|
-
// Get /user/:id
|
|
741
|
-
async getRecord() {}
|
|
742
|
-
|
|
743
|
-
// Get /user/:id/status
|
|
744
|
-
async getRecordStatus() {}
|
|
745
|
-
}
|
|
746
|
-
*/
|
|
747
|
-
if (/^Record($|[A-Z])/.test(path)) {
|
|
748
|
-
routePrefix = `${singularize(routePrefix)}/:id`;
|
|
749
|
-
path = path == 'Record' ? 'Index' : path.replace(/^Record/, '');
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
if (path == 'Index') return routePrefix;
|
|
753
|
-
|
|
754
|
-
path = path.charAt(0).toLowerCase() + path.slice(1);
|
|
755
|
-
return `${routePrefix}/${path}`;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
static get routePrefix() {
|
|
760
|
-
const controllerName = this.name.replace(/Controller$/, '');
|
|
761
|
-
const controllerPrefix = controllerName.charAt(0).toLowerCase() + controllerName.slice(1);
|
|
762
|
-
|
|
763
|
-
return `${this.directoryPrefix || ''}/${controllerPrefix}`;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
static get routes() {
|
|
768
|
-
return {};
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
static get actionNames() {
|
|
773
|
-
const methodNames = Object.getOwnPropertyNames(this.prototype);
|
|
774
|
-
const nameRegExp = this.actionRegExp;
|
|
775
|
-
return methodNames.filter(name => nameRegExp.test(name));
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
static get actionRegExp() {
|
|
780
|
-
return new RegExp(/^(?<method>get|post|put|delete|patch|head)(?<path>[A-Z][a-zA-Z]+)$/);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
/***********************************************************************************************
|
|
785
|
-
* DOCUMENTATION
|
|
786
|
-
***********************************************************************************************/
|
|
787
|
-
static parametersForAction(actionName) {
|
|
788
|
-
const methodName = this.parametersMethodForAction(actionName);
|
|
789
|
-
if (this[methodName]) return this[methodName]();
|
|
790
|
-
return null;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
static parametersMethodForAction(actionName) {
|
|
795
|
-
return `parametersFor${camelize(actionName)}`;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
static documentationForAction(actionName) {
|
|
800
|
-
const methodName = this.documentationMethodForAction(actionName);
|
|
801
|
-
if (this[methodName]) return this[methodName]();
|
|
802
|
-
return null;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
static documentationMethodForAction(actionName) {
|
|
807
|
-
return `document${camelize(actionName)}`;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
isAuthenticatedAction(actionName) {
|
|
812
|
-
const { options } = this.#beforeCallbacks.find(({ callback, options }) => callback == AUTH_CALLBACK_NAME)
|
|
813
|
-
return Boolean(options) && this.#shouldRunCallback(actionName, options);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
static isAuthenticatedAction(actionName) {
|
|
818
|
-
const controller = new this();
|
|
819
|
-
return controller.isAuthenticatedAction(actionName);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
static _authenticationDocumentation(actionName) {
|
|
824
|
-
if (!this.isAuthenticatedAction(actionName)) return undefined;
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
static authenticationDocumentation(actionName) {
|
|
829
|
-
return {apiKeyAuth: []};
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
|
|
833
211
|
/***********************************************************************************************
|
|
834
212
|
* SECURITY
|
|
835
213
|
***********************************************************************************************/
|
|
@@ -857,11 +235,36 @@ class VidaServerController {
|
|
|
857
235
|
startApmSegment(name, record, handler) {
|
|
858
236
|
return APM.startSegment(name, record, handler);
|
|
859
237
|
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
reportError(err) {
|
|
241
|
+
APM.reportError(err);
|
|
242
|
+
}
|
|
860
243
|
}
|
|
861
244
|
|
|
862
245
|
|
|
863
|
-
|
|
864
|
-
|
|
246
|
+
/***************************************************************************************************
|
|
247
|
+
* MIXINS
|
|
248
|
+
***************************************************************************************************/
|
|
249
|
+
const CallbacksMixin = require('./controllerMixins/callbacks');
|
|
250
|
+
const DocumentationMixin = require('./controllerMixins/documentation');
|
|
251
|
+
const RendererMixin = require('./controllerMixins/renderer');
|
|
252
|
+
const RequestDetailsMixin = require('./controllerMixins/requestDetails');
|
|
253
|
+
const RoutingMixin = require('./controllerMixins/routing');
|
|
254
|
+
const ValidationMixin = require('./controllerMixins/validations');
|
|
255
|
+
|
|
256
|
+
VidaServerController.loadMixin(CallbacksMixin);
|
|
257
|
+
VidaServerController.loadMixin(DocumentationMixin);
|
|
258
|
+
VidaServerController.loadMixin(RequestDetailsMixin);
|
|
259
|
+
VidaServerController.loadMixin(RendererMixin);
|
|
260
|
+
VidaServerController.loadMixin(RoutingMixin);
|
|
261
|
+
VidaServerController.loadMixin(ValidationMixin);
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
/***************************************************************************************************
|
|
265
|
+
* ERRORS
|
|
266
|
+
***************************************************************************************************/
|
|
267
|
+
const { NoActiveStreamError, StreamInProgressError } = RendererMixin;
|
|
865
268
|
|
|
866
269
|
const errorClasses = Object.values(Errors).filter(cls => cls == Errors.ServerError || cls.prototype instanceof Errors.AbstractServerError);
|
|
867
270
|
VidaServerController.prototype.Errors = Object.fromEntries(errorClasses.map(cls => {
|