@vida-global/core 1.4.5 → 2.0.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/AGENTS.md +13 -0
- package/README.md +1 -0
- package/config/newrelic-config.js +118 -0
- package/index.js +9 -5
- package/lib/activeRecord/baseRecord.js +49 -3
- package/lib/apm/agent.js +22 -0
- package/lib/apm/index.js +54 -0
- package/lib/apm/utils.js +43 -0
- package/lib/http/client.js +16 -2
- package/lib/jobQueue/README.md +63 -0
- package/lib/jobQueue/abstractJob.js +103 -0
- package/lib/jobQueue/abstractJobComponent.js +37 -0
- package/lib/jobQueue/index.js +10 -0
- package/lib/jobQueue/jobImporter.js +19 -0
- package/lib/jobQueue/queue.js +161 -0
- package/lib/jobQueue/worker.js +273 -0
- package/lib/logger/README.md +33 -2
- package/lib/logger/index.js +117 -20
- package/lib/logger/serverMiddleware.js +41 -0
- package/lib/redis/redisClient.js +32 -24
- package/lib/server/controllerImporter.js +9 -47
- package/lib/server/server.js +34 -71
- package/lib/server/serverController.js +215 -23
- package/lib/utils/abstractAutoImporter.js +64 -0
- package/package.json +8 -6
- package/test/activeRecord/baseRecord.test.js +129 -0
- package/test/activeRecord/db/connection.test.js +4 -1
- package/test/activeRecord/db/connectionConfiguration.test.js +9 -3
- package/test/activeRecord/db/schema.test.js +217 -0
- package/test/activeRecord/helpers/baseRecordMocks.js +6 -4
- package/test/activeRecord/helpers/connection.js +0 -3
- package/test/activeRecord/helpers/connectionConfiguration.js +0 -8
- package/test/activeRecord/utils.test.js +72 -0
- package/test/apm/agent.test.js +56 -0
- package/test/apm/utils.test.js +121 -0
- package/test/helpers/env.js +33 -0
- package/test/http/client.test.js +3 -3
- package/test/jobQueue/abstractJob.test.js +307 -0
- package/test/jobQueue/abstractJobComponent.test.js +110 -0
- package/test/jobQueue/helpers/abstractJob.js +26 -0
- package/test/jobQueue/helpers/apmMock.js +20 -0
- package/test/jobQueue/helpers/bullmqMock.js +67 -0
- package/test/jobQueue/helpers/env.js +29 -0
- package/test/jobQueue/helpers/fixtureJobs/notAJob.js +8 -0
- package/test/jobQueue/helpers/fixtureJobs/testJobA.js +14 -0
- package/test/jobQueue/helpers/fixtureJobs/testJobB.js +14 -0
- package/test/jobQueue/helpers/loggerMock.js +43 -0
- package/test/jobQueue/helpers/worker.js +38 -0
- package/test/jobQueue/queue.test.js +320 -0
- package/test/jobQueue/worker.test.js +526 -0
- package/test/logger/index.test.js +61 -0
- package/test/logger/serverMiddleware.test.js +72 -0
- package/test/server/apiDocsGenerator.test.js +38 -0
- package/test/server/server.test.js +186 -4
- package/test/server/serverController.test.js +407 -5
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
const
|
|
1
|
+
const APM = require('../apm');
|
|
2
2
|
const { camelize, singularize } = require('inflection');
|
|
3
3
|
const Errors = require('./errors');
|
|
4
|
+
const { logger } = require('../logger');
|
|
5
|
+
const nodeUtil = require('util');
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
const AUTH_CALLBACK_NAME = 'authenticateRequest';
|
|
9
|
+
const actionStore = new WeakMap();
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
class VidaServerController {
|
|
@@ -12,18 +15,22 @@ class VidaServerController {
|
|
|
12
15
|
#beforeCallbacks = [];
|
|
13
16
|
#beforeCallbacksToSkip = [];
|
|
14
17
|
#callbacksSetUp = false;
|
|
18
|
+
#isStreaming = false;
|
|
19
|
+
#logger;
|
|
15
20
|
#params;
|
|
16
21
|
#rendered = false;
|
|
17
22
|
#request;
|
|
18
23
|
#response;
|
|
24
|
+
#streamAbortController;
|
|
19
25
|
|
|
20
26
|
|
|
21
|
-
constructor(request, response) {
|
|
27
|
+
constructor(request, response, serverLogger) {
|
|
22
28
|
if (this.constructor == VidaServerController) {
|
|
23
29
|
throw new Error("ServerControllers must be subclasses of VidaServerController");
|
|
24
30
|
}
|
|
25
31
|
this.#request = request;
|
|
26
32
|
this.#response = response;
|
|
33
|
+
this.#logger = (serverLogger || logger).child(this.requestId);
|
|
27
34
|
|
|
28
35
|
this.#applyCallbacks();
|
|
29
36
|
this.#setupStatusRenderers();
|
|
@@ -41,16 +48,33 @@ class VidaServerController {
|
|
|
41
48
|
return structuredClone(this.#params);
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
get requestId() { return this._request.id; }
|
|
44
52
|
get requestHeaders() { return structuredClone(this._request.headers || {}); }
|
|
45
53
|
get responseHeaders() { return this._response.headers; };
|
|
46
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; }
|
|
47
58
|
get contentType() { return this.requestHeaders['content-type']; }
|
|
48
|
-
get
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
}
|
|
51
68
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
}
|
|
54
78
|
|
|
55
79
|
|
|
56
80
|
#processRequestData(key) {
|
|
@@ -80,6 +104,7 @@ class VidaServerController {
|
|
|
80
104
|
***********************************************************************************************/
|
|
81
105
|
async performRequest(action) {
|
|
82
106
|
try {
|
|
107
|
+
APM.setControllerName(this.constructor.name, action);
|
|
83
108
|
await this.#performRequest(action)
|
|
84
109
|
} catch(err) {
|
|
85
110
|
await this.#handleError(err);
|
|
@@ -88,7 +113,7 @@ class VidaServerController {
|
|
|
88
113
|
|
|
89
114
|
|
|
90
115
|
async #performRequest(action) {
|
|
91
|
-
await this.setupRequestState();
|
|
116
|
+
await APM.startSegment('setupRequestState', true, () => this.setupRequestState());
|
|
92
117
|
|
|
93
118
|
if (this.rendered) {
|
|
94
119
|
this.#response.end();
|
|
@@ -98,7 +123,7 @@ class VidaServerController {
|
|
|
98
123
|
const responseBody = await this.#performAction(action);
|
|
99
124
|
|
|
100
125
|
if (!this.rendered) {
|
|
101
|
-
await this.render(responseBody || {});
|
|
126
|
+
await APM.startSegment('render', true, () => this.render(responseBody || {}));
|
|
102
127
|
}
|
|
103
128
|
|
|
104
129
|
this.#response.end();
|
|
@@ -106,13 +131,14 @@ class VidaServerController {
|
|
|
106
131
|
|
|
107
132
|
|
|
108
133
|
async #performAction(action) {
|
|
109
|
-
if (await this.runBeforeCallbacks(action) === false) return false;
|
|
134
|
+
if (await APM.startSegment('beforeCallbacks', true, () => this.runBeforeCallbacks(action)) === false) return false;
|
|
110
135
|
|
|
111
136
|
const parameters = this.constructor.parametersForAction(action);
|
|
112
|
-
if (parameters) await this.validateParameters(parameters);
|
|
137
|
+
if (parameters) await APM.startSegment('validateParameters', true, () => this.validateParameters(parameters));
|
|
113
138
|
|
|
114
|
-
const responseBody = await this[action]();
|
|
115
|
-
|
|
139
|
+
const responseBody = await APM.startSegment(`action:${action}`, true, () => this[action]());
|
|
140
|
+
|
|
141
|
+
if (await APM.startSegment('afterCallbacks', true, () => this.runAfterCallbacks(action)) === false) return false;
|
|
116
142
|
|
|
117
143
|
return responseBody;
|
|
118
144
|
}
|
|
@@ -140,14 +166,24 @@ class VidaServerController {
|
|
|
140
166
|
message = err.message;
|
|
141
167
|
}
|
|
142
168
|
await this.renderServerError(message);
|
|
169
|
+
|
|
170
|
+
this.reportError(err);
|
|
143
171
|
}
|
|
144
172
|
}
|
|
145
173
|
|
|
146
174
|
|
|
175
|
+
reportError(err) {
|
|
176
|
+
APM.reportError(err);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
147
180
|
/***********************************************************************************************
|
|
148
181
|
* RESPONSE RENDERING
|
|
149
182
|
***********************************************************************************************/
|
|
150
183
|
async render(body, options={}) {
|
|
184
|
+
if (this.rendered) return;
|
|
185
|
+
if (this.#isStreaming) return;
|
|
186
|
+
|
|
151
187
|
if (typeof body == 'string') {
|
|
152
188
|
this._response.send(body);
|
|
153
189
|
} else {
|
|
@@ -159,7 +195,7 @@ class VidaServerController {
|
|
|
159
195
|
body = this.formatJSONBody(body, errors, options);
|
|
160
196
|
this._response.json(body);
|
|
161
197
|
}
|
|
162
|
-
this
|
|
198
|
+
this.markRendered();
|
|
163
199
|
}
|
|
164
200
|
|
|
165
201
|
|
|
@@ -196,27 +232,29 @@ class VidaServerController {
|
|
|
196
232
|
if (body === undefined) return null;
|
|
197
233
|
if (!body || typeof body != 'object') return body;
|
|
198
234
|
|
|
199
|
-
|
|
235
|
+
let processedBody;
|
|
200
236
|
|
|
201
237
|
if (body.toApiResponse) {
|
|
202
238
|
const formattedBody = await body.toApiResponse(options);
|
|
203
239
|
return await this.processJSONBody(formattedBody, options);
|
|
204
240
|
|
|
205
241
|
} else if (Array.isArray(body)) {
|
|
242
|
+
const processed = [];
|
|
206
243
|
for (const idx in body) {
|
|
207
|
-
|
|
244
|
+
processed.push(await this.processJSONBody(body[idx], options));
|
|
208
245
|
}
|
|
246
|
+
return processed;
|
|
209
247
|
|
|
210
248
|
} else if (body.constructor == Date) {
|
|
211
249
|
return body.getTime();
|
|
212
250
|
|
|
213
251
|
} else {
|
|
252
|
+
const processed = {};
|
|
214
253
|
for (let [key, val] of Object.entries(body)) {
|
|
215
|
-
|
|
254
|
+
processed[key] = await this.processJSONBody(val, options);
|
|
216
255
|
}
|
|
256
|
+
return processed;
|
|
217
257
|
}
|
|
218
|
-
|
|
219
|
-
return body;
|
|
220
258
|
}
|
|
221
259
|
|
|
222
260
|
|
|
@@ -324,10 +362,82 @@ class VidaServerController {
|
|
|
324
362
|
}
|
|
325
363
|
|
|
326
364
|
|
|
365
|
+
setHeader(header, value) {
|
|
366
|
+
this.#response.setHeader(header, value);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
flushHeaders() {
|
|
371
|
+
this.#response.flushHeaders();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
get rendered() { return this.#rendered; }
|
|
376
|
+
markRendered() { this.#rendered = true; }
|
|
377
|
+
get writableEnded() { return this.#response.writableEnded; }
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
/***********************************************************************************************
|
|
381
|
+
* STREAM
|
|
382
|
+
***********************************************************************************************/
|
|
383
|
+
async streamSseResponse(handler, errorHandler, abortController=null) {
|
|
384
|
+
if (this.#isStreaming) throw new StreamInProgressError();
|
|
385
|
+
this.#isStreaming = true;
|
|
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();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
writeStreamEvent(evt, data) {
|
|
424
|
+
if (!this.#isStreaming) throw new NoActiveStreamError();
|
|
425
|
+
if (this.#streamAbortController?.signal?.aborted || this.writableEnded) return;
|
|
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
|
+
|
|
327
436
|
/***********************************************************************************************
|
|
328
437
|
* HELPERS
|
|
329
438
|
***********************************************************************************************/
|
|
330
439
|
static autoLoadHelpers() {
|
|
440
|
+
this.actions; // make sure that actions are cached before loading helpers
|
|
331
441
|
const { StaticMethods, InstanceMethods, Accessors } = this._autoLoadModule(this.autoLoadHelperPath);
|
|
332
442
|
|
|
333
443
|
if (StaticMethods) Object.assign(this, StaticMethods);
|
|
@@ -360,12 +470,12 @@ class VidaServerController {
|
|
|
360
470
|
|
|
361
471
|
|
|
362
472
|
static get autoLoadHelperPath() {
|
|
363
|
-
return `${process.cwd()}/lib/controllers${this.routePrefix}Controller
|
|
473
|
+
return `${process.cwd()}/lib/controllers${this.routePrefix}Controller`;
|
|
364
474
|
}
|
|
365
475
|
|
|
366
476
|
|
|
367
477
|
static get autoLoadDocumentationPath() {
|
|
368
|
-
return `${process.cwd()}/lib/controllers/_docs${this.routePrefix}Controller
|
|
478
|
+
return `${process.cwd()}/lib/controllers/_docs${this.routePrefix}Controller`;
|
|
369
479
|
}
|
|
370
480
|
|
|
371
481
|
|
|
@@ -456,6 +566,44 @@ class VidaServerController {
|
|
|
456
566
|
}
|
|
457
567
|
|
|
458
568
|
|
|
569
|
+
/***********************************************************************************************
|
|
570
|
+
* REQUEST PARAMETERS
|
|
571
|
+
***********************************************************************************************/
|
|
572
|
+
hasParam(name) {
|
|
573
|
+
return Object.prototype.hasOwnProperty.call(this.params, name);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
coerceBoolean(value, { fallback } = {}) {
|
|
578
|
+
if (typeof value === 'boolean') return value;
|
|
579
|
+
if (typeof value !== 'string') return fallback;
|
|
580
|
+
const normalized = value.trim().toLowerCase();
|
|
581
|
+
if (!normalized) return fallback;
|
|
582
|
+
if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on') return true;
|
|
583
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off') return false;
|
|
584
|
+
return fallback;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
coerceInteger(value, { min, max, fallback, nonNegative } = {}) {
|
|
589
|
+
if (value === null || value === undefined) return fallback;
|
|
590
|
+
if (typeof value === 'string' && value.trim() === '') return fallback;
|
|
591
|
+
const parsed = Number(value);
|
|
592
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
593
|
+
let next = Math.floor(parsed);
|
|
594
|
+
if (nonNegative && next < 0) return fallback;
|
|
595
|
+
if (Number.isFinite(min)) next = Math.max(min, next);
|
|
596
|
+
if (Number.isFinite(max)) next = Math.min(max, next);
|
|
597
|
+
return next;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
coerceObject(value, { fallback = {} } = {}) {
|
|
602
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
|
603
|
+
return fallback;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
|
|
459
607
|
/***********************************************************************************************
|
|
460
608
|
* VALIDATIONS
|
|
461
609
|
***********************************************************************************************/
|
|
@@ -531,6 +679,12 @@ class VidaServerController {
|
|
|
531
679
|
}
|
|
532
680
|
|
|
533
681
|
|
|
682
|
+
validateIsBoolean(value, bool) {
|
|
683
|
+
if (typeof value == 'boolean' && value === bool) return;
|
|
684
|
+
return `must be ${bool}`;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
|
|
534
688
|
async validateFunction(value, fnc) {
|
|
535
689
|
const error = await fnc.call(this, value);
|
|
536
690
|
if (error) return error;
|
|
@@ -551,10 +705,14 @@ class VidaServerController {
|
|
|
551
705
|
* {getFooBar: '/ban/:accountIdd/foo/:userId/bar'}`
|
|
552
706
|
***********************************************************************************************/
|
|
553
707
|
static get actions() {
|
|
708
|
+
if (actionStore.has(this)) return actionStore.get(this);
|
|
554
709
|
const actionNames = this.actionNames;
|
|
555
|
-
|
|
710
|
+
const actions = actionNames.map((actionName => {
|
|
556
711
|
return this._constructAction(actionName);
|
|
557
712
|
}).bind(this));
|
|
713
|
+
|
|
714
|
+
actionStore.set(this, actions);
|
|
715
|
+
return actions;
|
|
558
716
|
}
|
|
559
717
|
|
|
560
718
|
|
|
@@ -670,15 +828,49 @@ class VidaServerController {
|
|
|
670
828
|
static authenticationDocumentation(actionName) {
|
|
671
829
|
return {apiKeyAuth: []};
|
|
672
830
|
}
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
/***********************************************************************************************
|
|
834
|
+
* SECURITY
|
|
835
|
+
***********************************************************************************************/
|
|
836
|
+
secureCompare(a, b) {
|
|
837
|
+
const aBuf = Buffer.from(a || '', 'utf8');
|
|
838
|
+
const bBuf = Buffer.from(b || '', 'utf8');
|
|
839
|
+
if (aBuf.length !== bBuf.length) return false;
|
|
840
|
+
return crypto.timingSafeEqual(aBuf, bBuf);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
/***********************************************************************************************
|
|
845
|
+
* APM
|
|
846
|
+
***********************************************************************************************/
|
|
847
|
+
addApmAttribute(key, value) {
|
|
848
|
+
APM.addCustomAttribute(key, value)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
setApmUserId(id) {
|
|
853
|
+
APM.setUserId(id)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
startApmSegment(name, record, handler) {
|
|
858
|
+
return APM.startSegment(name, record, handler);
|
|
859
|
+
}
|
|
673
860
|
}
|
|
674
861
|
|
|
675
862
|
|
|
863
|
+
class StreamInProgressError extends Error {}
|
|
864
|
+
class NoActiveStreamError extends Error {}
|
|
865
|
+
|
|
676
866
|
const errorClasses = Object.values(Errors).filter(cls => cls == Errors.ServerError || cls.prototype instanceof Errors.AbstractServerError);
|
|
677
867
|
VidaServerController.prototype.Errors = Object.fromEntries(errorClasses.map(cls => {
|
|
678
868
|
return [cls.name.replace('Error', ''), cls]
|
|
679
869
|
}));
|
|
680
870
|
|
|
681
871
|
|
|
682
|
-
module.exports = {
|
|
872
|
+
module.exports = {
|
|
873
|
+
NoActiveStreamError,
|
|
874
|
+
StreamInProgressError,
|
|
683
875
|
VidaServerController
|
|
684
876
|
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AbstractAutoImporter {
|
|
5
|
+
#directories;
|
|
6
|
+
#imports;
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
constructor(directories) {
|
|
10
|
+
if (!Array.isArray(directories)) {
|
|
11
|
+
directories = [directories];
|
|
12
|
+
}
|
|
13
|
+
this.#directories = directories;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
get imports() {
|
|
18
|
+
if (!this.#imports) {
|
|
19
|
+
this.#runImport();
|
|
20
|
+
}
|
|
21
|
+
return this.#imports;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
#runImport() {
|
|
26
|
+
this.#imports = [];
|
|
27
|
+
this.#directories.forEach(dir => {
|
|
28
|
+
this.#importDirectory(dir, dir);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
#importDirectory(dir, topLevelDirectory) {
|
|
34
|
+
fs.readdirSync(dir).forEach((f => {
|
|
35
|
+
const filePath = `${dir}/${f}`;
|
|
36
|
+
if (fs.lstatSync(filePath).isDirectory()) {
|
|
37
|
+
this.#importDirectory(filePath, topLevelDirectory);
|
|
38
|
+
} else if (f.endsWith('.js')) {
|
|
39
|
+
this.#importFromFile(filePath, dir, topLevelDirectory);
|
|
40
|
+
}
|
|
41
|
+
}).bind(this));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
#importFromFile(filePath, dir, topLevelDirectory) {
|
|
46
|
+
const exports = Object.values(require(filePath));
|
|
47
|
+
exports.forEach(obj => {
|
|
48
|
+
if (this.shouldImport(obj)) {
|
|
49
|
+
const directoryPrefix = dir.replace(topLevelDirectory, '');
|
|
50
|
+
this.processImport(obj, directoryPrefix);
|
|
51
|
+
this.#imports.push(obj);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
shouldImport(obj) {}
|
|
58
|
+
processImport(obj) {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
AbstractAutoImporter
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vida-global/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Core libraries for supporting Vida development",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "ISC",
|
|
@@ -18,19 +18,21 @@
|
|
|
18
18
|
"setupFilesAfterEnv": ["jest-extended/all"]
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"bullmq": "^5.0.0",
|
|
21
22
|
"commander": "^13.1.0",
|
|
22
23
|
"express": "^4.21.2",
|
|
24
|
+
"express-request-id": "1.4.1",
|
|
25
|
+
"express-winston": "^4.0.0",
|
|
23
26
|
"mustache-express": "^1.2.8",
|
|
27
|
+
"newrelic": "^13.0.0",
|
|
24
28
|
"pg": "^8.16.3",
|
|
25
|
-
"
|
|
26
|
-
"pino-http": "^10.4.0",
|
|
27
|
-
"pino-pretty": "^13.0.0",
|
|
28
|
-
"redis": "^5.0.0",
|
|
29
|
+
"redis": "^4.0.0",
|
|
29
30
|
"response-time": "^2.3.3",
|
|
30
31
|
"sequelize": "^6.37.7",
|
|
31
32
|
"sequelize-cli": "^6.6.3",
|
|
32
33
|
"socket.io": "^4.4.0",
|
|
33
|
-
"@vida-global/release": "^1.0.0"
|
|
34
|
+
"@vida-global/release": "^1.0.0",
|
|
35
|
+
"winston": "^3.0.0"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
36
38
|
"jest": "^29.0.0",
|
|
@@ -126,6 +126,135 @@ describe('BaseRecord', () => {
|
|
|
126
126
|
});
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
describe('BaseRecord.configureVirtualAccessors', () => {
|
|
130
|
+
it ('adds a VIRTUAL column when a _virtualGet method is defined on the subclass', () => {
|
|
131
|
+
const getter = jest.fn();
|
|
132
|
+
class User extends BaseRecord {}
|
|
133
|
+
User.prototype._virtualGetFullName = getter;
|
|
134
|
+
User.initialize();
|
|
135
|
+
|
|
136
|
+
expect(Model.init).toHaveBeenCalledWith(
|
|
137
|
+
expect.objectContaining({
|
|
138
|
+
fullName: { type: Sequelize.DataTypes.VIRTUAL, get: getter }
|
|
139
|
+
}),
|
|
140
|
+
expect.anything()
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it ('adds a VIRTUAL column when a _virtualSet method is defined on the subclass', () => {
|
|
145
|
+
const setter = jest.fn();
|
|
146
|
+
class User extends BaseRecord {}
|
|
147
|
+
User.prototype._virtualSetFullName = setter;
|
|
148
|
+
User.initialize();
|
|
149
|
+
|
|
150
|
+
expect(Model.init).toHaveBeenCalledWith(
|
|
151
|
+
expect.objectContaining({
|
|
152
|
+
fullName: { type: Sequelize.DataTypes.VIRTUAL, set: setter }
|
|
153
|
+
}),
|
|
154
|
+
expect.anything()
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it ('merges _virtualGet and _virtualSet into the same VIRTUAL column', () => {
|
|
159
|
+
const getter = jest.fn();
|
|
160
|
+
const setter = jest.fn();
|
|
161
|
+
class User extends BaseRecord {}
|
|
162
|
+
User.prototype._virtualGetFullName = getter;
|
|
163
|
+
User.prototype._virtualSetFullName = setter;
|
|
164
|
+
User.initialize();
|
|
165
|
+
|
|
166
|
+
expect(Model.init).toHaveBeenCalledWith(
|
|
167
|
+
expect.objectContaining({
|
|
168
|
+
fullName: { type: Sequelize.DataTypes.VIRTUAL, get: getter, set: setter }
|
|
169
|
+
}),
|
|
170
|
+
expect.anything()
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it ('picks up virtual accessors defined on a parent class', () => {
|
|
175
|
+
const getter = jest.fn();
|
|
176
|
+
class Person extends BaseRecord {}
|
|
177
|
+
Person.prototype._virtualGetDisplayName = getter;
|
|
178
|
+
class Employee extends Person {}
|
|
179
|
+
Employee.initialize();
|
|
180
|
+
|
|
181
|
+
expect(Model.init).toHaveBeenCalledWith(
|
|
182
|
+
expect.objectContaining({
|
|
183
|
+
displayName: { type: Sequelize.DataTypes.VIRTUAL, get: getter }
|
|
184
|
+
}),
|
|
185
|
+
expect.anything()
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it ('lets a subclass override a virtual accessor defined on a parent class', () => {
|
|
190
|
+
const parentGetter = jest.fn();
|
|
191
|
+
const childGetter = jest.fn();
|
|
192
|
+
class Person extends BaseRecord {}
|
|
193
|
+
Person.prototype._virtualGetDisplayName = parentGetter;
|
|
194
|
+
class Employee extends Person {}
|
|
195
|
+
Employee.prototype._virtualGetDisplayName = childGetter;
|
|
196
|
+
Employee.initialize();
|
|
197
|
+
|
|
198
|
+
expect(Model.init).toHaveBeenCalledWith(
|
|
199
|
+
expect.objectContaining({
|
|
200
|
+
displayName: { type: Sequelize.DataTypes.VIRTUAL, get: childGetter }
|
|
201
|
+
}),
|
|
202
|
+
expect.anything()
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it ('merges a parent _virtualGet with a child _virtualSet on the same property', () => {
|
|
207
|
+
const parentGetter = jest.fn();
|
|
208
|
+
const childSetter = jest.fn();
|
|
209
|
+
class Person extends BaseRecord {}
|
|
210
|
+
Person.prototype._virtualGetDisplayName = parentGetter;
|
|
211
|
+
class Employee extends Person {}
|
|
212
|
+
Employee.prototype._virtualSetDisplayName = childSetter;
|
|
213
|
+
Employee.initialize();
|
|
214
|
+
|
|
215
|
+
expect(Model.init).toHaveBeenCalledWith(
|
|
216
|
+
expect.objectContaining({
|
|
217
|
+
displayName: { type: Sequelize.DataTypes.VIRTUAL, get: parentGetter, set: childSetter }
|
|
218
|
+
}),
|
|
219
|
+
expect.anything()
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it ('does not pull in non-virtual methods from any ancestor', () => {
|
|
224
|
+
class Person extends BaseRecord {
|
|
225
|
+
someOtherMethod() {}
|
|
226
|
+
}
|
|
227
|
+
class Employee extends Person {}
|
|
228
|
+
Employee.initialize();
|
|
229
|
+
|
|
230
|
+
const [schemaArg] = Model.init.mock.calls[0];
|
|
231
|
+
expect(schemaArg).not.toHaveProperty('someOtherMethod');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it ('does not walk past Model.prototype when collecting accessors', () => {
|
|
235
|
+
class User extends BaseRecord {}
|
|
236
|
+
Model.prototype._virtualGetShouldBeIgnored = jest.fn();
|
|
237
|
+
User.initialize();
|
|
238
|
+
|
|
239
|
+
const [schemaArg] = Model.init.mock.calls[0];
|
|
240
|
+
expect(schemaArg).not.toHaveProperty('shouldBeIgnored');
|
|
241
|
+
|
|
242
|
+
delete Model.prototype._virtualGetShouldBeIgnored;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it ('leaves existing schema columns untouched while adding the virtual entry', () => {
|
|
246
|
+
const getter = jest.fn();
|
|
247
|
+
class User extends BaseRecord {}
|
|
248
|
+
User.prototype._virtualGetFullName = getter;
|
|
249
|
+
User.initialize();
|
|
250
|
+
|
|
251
|
+
const [schemaArg] = Model.init.mock.calls[0];
|
|
252
|
+
expect(schemaArg[Helpers.colName]).toEqual({ type: Sequelize.DataTypes[Helpers.dataTypeKey2] });
|
|
253
|
+
expect(schemaArg.fullName).toEqual({ type: Sequelize.DataTypes.VIRTUAL, get: getter });
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
|
|
129
258
|
describe('BaseRecord.tableName', () => {
|
|
130
259
|
it ('defaults to the puralized version of the class name', () => {
|
|
131
260
|
class User extends BaseRecord {}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const { Connection } = require('../../../lib/activeRecord/db/connection');
|
|
2
2
|
const { ConnectionConfiguration } = require('../../../lib/activeRecord/db/connectionConfiguration');
|
|
3
3
|
const Helpers = require('../helpers/connection');
|
|
4
|
+
const { snapshotEnv } = require('../../helpers/env');
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
let fetchConfigsSpy;
|
|
8
|
+
let restoreEnv;
|
|
7
9
|
|
|
8
10
|
jest.mock('sequelize', () => {
|
|
9
11
|
const { MockSequelize } = require('../helpers/fixtures');
|
|
@@ -11,6 +13,7 @@ jest.mock('sequelize', () => {
|
|
|
11
13
|
});
|
|
12
14
|
|
|
13
15
|
beforeEach(() => {
|
|
16
|
+
restoreEnv = snapshotEnv(['NODE_ENV']);
|
|
14
17
|
fetchConfigsSpy = jest.spyOn(ConnectionConfiguration, '_fetchAllConfigs');
|
|
15
18
|
fetchConfigsSpy.mockImplementation(Helpers.buildConfigs);
|
|
16
19
|
});
|
|
@@ -18,7 +21,7 @@ beforeEach(() => {
|
|
|
18
21
|
afterEach(() => {
|
|
19
22
|
fetchConfigsSpy.mockRestore();
|
|
20
23
|
Connection.clearConnectionsCache();
|
|
21
|
-
|
|
24
|
+
restoreEnv();
|
|
22
25
|
});
|
|
23
26
|
|
|
24
27
|
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
const { ConnectionConfiguration } = require('../../../lib/activeRecord/db/connectionConfiguration');
|
|
2
2
|
const Helpers = require('../helpers/connectionConfiguration');
|
|
3
|
+
const { snapshotEnv } = require('../../helpers/env');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
let restoreEnv;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
restoreEnv = snapshotEnv(['DB_POOL_MIN', 'DB_POOL_MAX', 'NODE_ENV']);
|
|
10
|
+
});
|
|
3
11
|
|
|
4
12
|
|
|
5
13
|
afterEach(() => {
|
|
6
14
|
Helpers.fetchConfigsSpy.mockReset();
|
|
7
|
-
|
|
8
|
-
process.env.DB_POOL_MAX = Helpers.originalEnv.dbPoolMax;
|
|
9
|
-
process.env.NODE_ENV = Helpers.originalEnv.nodeEnv;
|
|
15
|
+
restoreEnv();
|
|
10
16
|
});
|
|
11
17
|
|
|
12
18
|
|