@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.
Files changed (55) hide show
  1. package/AGENTS.md +13 -0
  2. package/README.md +1 -0
  3. package/config/newrelic-config.js +118 -0
  4. package/index.js +9 -5
  5. package/lib/activeRecord/baseRecord.js +49 -3
  6. package/lib/apm/agent.js +22 -0
  7. package/lib/apm/index.js +54 -0
  8. package/lib/apm/utils.js +43 -0
  9. package/lib/http/client.js +16 -2
  10. package/lib/jobQueue/README.md +63 -0
  11. package/lib/jobQueue/abstractJob.js +103 -0
  12. package/lib/jobQueue/abstractJobComponent.js +37 -0
  13. package/lib/jobQueue/index.js +10 -0
  14. package/lib/jobQueue/jobImporter.js +19 -0
  15. package/lib/jobQueue/queue.js +161 -0
  16. package/lib/jobQueue/worker.js +273 -0
  17. package/lib/logger/README.md +33 -2
  18. package/lib/logger/index.js +117 -20
  19. package/lib/logger/serverMiddleware.js +41 -0
  20. package/lib/redis/redisClient.js +32 -24
  21. package/lib/server/controllerImporter.js +9 -47
  22. package/lib/server/server.js +34 -71
  23. package/lib/server/serverController.js +215 -23
  24. package/lib/utils/abstractAutoImporter.js +64 -0
  25. package/package.json +8 -6
  26. package/test/activeRecord/baseRecord.test.js +129 -0
  27. package/test/activeRecord/db/connection.test.js +4 -1
  28. package/test/activeRecord/db/connectionConfiguration.test.js +9 -3
  29. package/test/activeRecord/db/schema.test.js +217 -0
  30. package/test/activeRecord/helpers/baseRecordMocks.js +6 -4
  31. package/test/activeRecord/helpers/connection.js +0 -3
  32. package/test/activeRecord/helpers/connectionConfiguration.js +0 -8
  33. package/test/activeRecord/utils.test.js +72 -0
  34. package/test/apm/agent.test.js +56 -0
  35. package/test/apm/utils.test.js +121 -0
  36. package/test/helpers/env.js +33 -0
  37. package/test/http/client.test.js +3 -3
  38. package/test/jobQueue/abstractJob.test.js +307 -0
  39. package/test/jobQueue/abstractJobComponent.test.js +110 -0
  40. package/test/jobQueue/helpers/abstractJob.js +26 -0
  41. package/test/jobQueue/helpers/apmMock.js +20 -0
  42. package/test/jobQueue/helpers/bullmqMock.js +67 -0
  43. package/test/jobQueue/helpers/env.js +29 -0
  44. package/test/jobQueue/helpers/fixtureJobs/notAJob.js +8 -0
  45. package/test/jobQueue/helpers/fixtureJobs/testJobA.js +14 -0
  46. package/test/jobQueue/helpers/fixtureJobs/testJobB.js +14 -0
  47. package/test/jobQueue/helpers/loggerMock.js +43 -0
  48. package/test/jobQueue/helpers/worker.js +38 -0
  49. package/test/jobQueue/queue.test.js +320 -0
  50. package/test/jobQueue/worker.test.js +526 -0
  51. package/test/logger/index.test.js +61 -0
  52. package/test/logger/serverMiddleware.test.js +72 -0
  53. package/test/server/apiDocsGenerator.test.js +38 -0
  54. package/test/server/server.test.js +186 -4
  55. package/test/server/serverController.test.js +407 -5
@@ -1,9 +1,12 @@
1
- const { logger } = require('../logger');
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 logger() { return logger; }
49
- get rendered() { return this.#rendered }
50
- markRendered() { this.#rendered = true; }
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
- get statusCode() { return this._response.statusCode; }
53
- set statusCode(_status) { this._response.statusCode = _status; }
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
- if (await this.runAfterCallbacks(action) === false) return false;
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.#rendered = true;
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
- body = structuredClone(body);
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
- body[idx] = await this.processJSONBody(body[idx], options);
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
- body[key] = await this.processJSONBody(val, options);
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.js`;
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.js`;
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
- return actionNames.map((actionName => {
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": "1.4.5",
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
- "pino": "^9.6.0",
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
- process.env.NODE_ENV = Helpers.originalNodeEnv;
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
- process.env.DB_POOL_MIN = Helpers.originalEnv.dbPoolMin;
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