@vida-global/core 1.4.6 → 2.0.1

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 (50) hide show
  1. package/README.md +1 -0
  2. package/config/newrelic-config.js +118 -0
  3. package/index.js +9 -5
  4. package/lib/activeRecord/baseRecord.js +49 -3
  5. package/lib/apm/agent.js +22 -0
  6. package/lib/apm/index.js +54 -0
  7. package/lib/apm/utils.js +43 -0
  8. package/lib/http/client.js +16 -2
  9. package/lib/jobQueue/README.md +63 -0
  10. package/lib/jobQueue/abstractJob.js +103 -0
  11. package/lib/jobQueue/abstractJobComponent.js +37 -0
  12. package/lib/jobQueue/index.js +10 -0
  13. package/lib/jobQueue/jobImporter.js +19 -0
  14. package/lib/jobQueue/queue.js +161 -0
  15. package/lib/jobQueue/worker.js +273 -0
  16. package/lib/logger/README.md +1 -1
  17. package/lib/logger/index.js +33 -15
  18. package/lib/logger/serverMiddleware.js +14 -12
  19. package/lib/redis/redisClient.js +32 -24
  20. package/lib/server/controllerImporter.js +9 -47
  21. package/lib/server/server.js +30 -13
  22. package/lib/server/serverController.js +100 -23
  23. package/lib/utils/abstractAutoImporter.js +64 -0
  24. package/package.json +4 -2
  25. package/test/activeRecord/baseRecord.test.js +129 -0
  26. package/test/activeRecord/db/connection.test.js +4 -1
  27. package/test/activeRecord/db/connectionConfiguration.test.js +9 -3
  28. package/test/activeRecord/helpers/baseRecordMocks.js +6 -4
  29. package/test/activeRecord/helpers/connection.js +0 -3
  30. package/test/activeRecord/helpers/connectionConfiguration.js +0 -8
  31. package/test/apm/agent.test.js +56 -0
  32. package/test/apm/utils.test.js +121 -0
  33. package/test/helpers/env.js +33 -0
  34. package/test/http/client.test.js +3 -3
  35. package/test/jobQueue/abstractJob.test.js +307 -0
  36. package/test/jobQueue/abstractJobComponent.test.js +110 -0
  37. package/test/jobQueue/helpers/abstractJob.js +26 -0
  38. package/test/jobQueue/helpers/apmMock.js +20 -0
  39. package/test/jobQueue/helpers/bullmqMock.js +67 -0
  40. package/test/jobQueue/helpers/env.js +29 -0
  41. package/test/jobQueue/helpers/fixtureJobs/notAJob.js +8 -0
  42. package/test/jobQueue/helpers/fixtureJobs/testJobA.js +14 -0
  43. package/test/jobQueue/helpers/fixtureJobs/testJobB.js +14 -0
  44. package/test/jobQueue/helpers/loggerMock.js +43 -0
  45. package/test/jobQueue/helpers/worker.js +38 -0
  46. package/test/jobQueue/queue.test.js +320 -0
  47. package/test/jobQueue/worker.test.js +526 -0
  48. package/test/logger/index.test.js +61 -0
  49. package/test/logger/serverMiddleware.test.js +6 -7
  50. package/test/server/serverController.test.js +226 -10
@@ -1,61 +1,23 @@
1
- const fs = require('fs');
1
+ const { AbstractAutoImporter } = require('../utils/abstractAutoImporter');
2
2
  const { VidaServerController } = require('./serverController');
3
3
 
4
4
 
5
- class ControllerImporter {
6
- #controllerDirectories;
7
- #controllerClasses;
8
-
9
-
10
- constructor(controllerDirectories) {
11
- if (!Array.isArray(controllerDirectories)) {
12
- controllerDirectories = [controllerDirectories];
13
- }
14
- this.#controllerDirectories = controllerDirectories;
15
- }
16
-
17
-
5
+ class ControllerImporter extends AbstractAutoImporter {
18
6
  get controllerClasses() {
19
- if (!this.#controllerClasses) {
20
- this.#importControllerClasses();
21
- }
22
- return this.#controllerClasses;
7
+ return this.imports;
23
8
  }
24
9
 
25
10
 
26
- #importControllerClasses() {
27
- this.#controllerClasses = [];
28
- this.#controllerDirectories.forEach(dir => {
29
- this.#importDirectory(dir, dir);
30
- });
11
+ shouldImport(obj) {
12
+ return obj.prototype instanceof VidaServerController;
31
13
  }
32
14
 
33
15
 
34
- #importDirectory(dir, topLevelDirectory) {
35
- fs.readdirSync(dir).forEach((f => {
36
- const filePath = `${dir}/${f}`;
37
- if (fs.lstatSync(filePath).isDirectory()) {
38
- this.#importDirectory(filePath, topLevelDirectory);
39
- } else if (f.endsWith('.js')) {
40
- this.#importFromFile(filePath, dir, topLevelDirectory);
41
- }
42
- }).bind(this));
16
+ processImport(obj, directoryPrefix) {
17
+ obj.directoryPrefix = directoryPrefix;
18
+ obj.autoLoadHelpers();
19
+ obj.autoLoadDocumentation();
43
20
  }
44
-
45
-
46
- #importFromFile(filePath, dir, topLevelDirectory) {
47
- const exports = Object.values(require(filePath));
48
- exports.forEach(_export => {
49
- if (_export.prototype instanceof VidaServerController) {
50
- const directoryPrefix = dir.replace(topLevelDirectory, '');
51
- _export.directoryPrefix = directoryPrefix;
52
- _export.autoLoadHelpers();
53
- _export.autoLoadDocumentation();
54
- this.#controllerClasses.push(_export);
55
- }
56
- });
57
- }
58
-
59
21
  }
60
22
 
61
23
 
@@ -1,3 +1,4 @@
1
+ const APM = require('../apm');
1
2
  const { ControllerImporter } = require('./controllerImporter');
2
3
  const express = require('express');
3
4
  const { logger } = require('../logger');
@@ -10,6 +11,9 @@ const { SystemController } = require('./systemController');
10
11
  const requestID = require( 'express-request-id');
11
12
 
12
13
 
14
+ logger.addScope('server');
15
+
16
+
13
17
  class VidaServer {
14
18
  #controllerClasses;
15
19
  #_expressServer;
@@ -31,7 +35,7 @@ class VidaServer {
31
35
  await this.registerControllers();
32
36
 
33
37
  this.#httpServer.listen(this.#port, this.#host, () => {
34
- logger.info(`Server is running on port ${this.#port}`);
38
+ this.logger.info(`${this.constructor.name} running on port ${this.#port}`);
35
39
  if (callback) callback();
36
40
  });
37
41
  }
@@ -39,7 +43,7 @@ class VidaServer {
39
43
 
40
44
  get host() { return this.#host; }
41
45
  get port() { return this.#port; }
42
- get logger() { return logger.http; }
46
+ get logger() { return logger.server; }
43
47
 
44
48
 
45
49
  /***********************************************************************************************
@@ -106,7 +110,7 @@ class VidaServer {
106
110
 
107
111
 
108
112
  get loggingMiddleware() {
109
- return loggingMiddleware.middleware
113
+ return loggingMiddleware.middleware(this.logger);
110
114
  }
111
115
 
112
116
 
@@ -116,7 +120,7 @@ class VidaServer {
116
120
 
117
121
 
118
122
  get connectionAbortedMiddleware() {
119
- return async function (err, req, res, next) {
123
+ return async function connectionAborted(err, req, res, next) {
120
124
  if (err && err.code === 'ECONNABORTED') {
121
125
  return res.status(400).end(); // Don't process this error any further to avoid its logging
122
126
  }
@@ -180,7 +184,7 @@ class VidaServer {
180
184
  const method = action.method.toLowerCase();
181
185
  const requestHandler = this.requestHandler(action.action, controllerCls)
182
186
  if (process.env.NODE_ENV != 'test') {
183
- logger.verbose(`ROUTE: ${method.toUpperCase().padEnd(6)} ${action.path}`);
187
+ this.logger.verbose(`ROUTE: ${method.toUpperCase().padEnd(6)} ${action.path}`);
184
188
  }
185
189
  this['_'+method](action.path, requestHandler);
186
190
  }
@@ -195,24 +199,29 @@ class VidaServer {
195
199
 
196
200
 
197
201
  requestHandler(action, controllerCls) {
198
- return async function(request, response) {
199
- const controllerInstance = this.buildController(controllerCls, request, response);
200
- request.controller = controllerInstance;
201
- request.action = action;
202
- await controllerInstance.performRequest(action);
203
- }.bind(this);
202
+ // Implemented with this object access for APM segment tracking
203
+ const server = this;
204
+ const handler = {
205
+ async requestHandler(request, response) {
206
+ const controllerInstance = server.buildController(controllerCls, request, response);
207
+ request.controller = controllerInstance;
208
+ request.action = action;
209
+ await controllerInstance.performRequest(action);
210
+ }
211
+ }.requestHandler;
212
+ return handler;
204
213
  }
205
214
 
206
215
 
207
216
  buildController(controllerCls, request, response) {
208
- return new controllerCls(request, response);
217
+ return new controllerCls(request, response, this.logger);
209
218
  }
210
219
 
211
220
 
212
221
  get controllerClasses() {
213
222
  if (!this.#controllerClasses) {
214
223
  const controllerImporter = new ControllerImporter(this.controllerDirectories);
215
- this.#controllerClasses = controllerImporter.controllerClasses;
224
+ this.#controllerClasses = controllerImporter.controllerClasses.filter(cls => !cls.name.startsWith('Abstract'));
216
225
  }
217
226
 
218
227
  return this.#controllerClasses;
@@ -236,6 +245,14 @@ class VidaServer {
236
245
  }
237
246
 
238
247
 
248
+ /***********************************************************************************************
249
+ * APM
250
+ ***********************************************************************************************/
251
+ startApmSegment(name, record, handler) {
252
+ APM.startSegment(name, record, handler);
253
+ }
254
+
255
+
239
256
  /***********************************************************************************************
240
257
  * DEPRECATED PASS THROUGH METHODS
241
258
  ***********************************************************************************************/
@@ -1,10 +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');
4
5
  const nodeUtil = require('util');
5
6
 
6
7
 
7
8
  const AUTH_CALLBACK_NAME = 'authenticateRequest';
9
+ const actionStore = new WeakMap();
8
10
 
9
11
 
10
12
  class VidaServerController {
@@ -22,12 +24,13 @@ class VidaServerController {
22
24
  #streamAbortController;
23
25
 
24
26
 
25
- constructor(request, response) {
27
+ constructor(request, response, serverLogger) {
26
28
  if (this.constructor == VidaServerController) {
27
29
  throw new Error("ServerControllers must be subclasses of VidaServerController");
28
30
  }
29
31
  this.#request = request;
30
32
  this.#response = response;
33
+ this.#logger = (serverLogger || logger).child(this.requestId);
31
34
 
32
35
  this.#applyCallbacks();
33
36
  this.#setupStatusRenderers();
@@ -59,11 +62,8 @@ class VidaServerController {
59
62
  set statusCode(_status) { this._response.statusCode = _status; }
60
63
 
61
64
 
62
- get logger() {
63
- if (!this.#logger) {
64
- this.#logger = logger.http.createChild(this.requestId);
65
- }
66
- return this.#logger;
65
+ get logger() {
66
+ return this.#logger;
67
67
  }
68
68
 
69
69
 
@@ -104,6 +104,7 @@ class VidaServerController {
104
104
  ***********************************************************************************************/
105
105
  async performRequest(action) {
106
106
  try {
107
+ APM.setControllerName(this.constructor.name, action);
107
108
  await this.#performRequest(action)
108
109
  } catch(err) {
109
110
  await this.#handleError(err);
@@ -112,7 +113,7 @@ class VidaServerController {
112
113
 
113
114
 
114
115
  async #performRequest(action) {
115
- await this.setupRequestState();
116
+ await APM.startSegment('setupRequestState', true, () => this.setupRequestState());
116
117
 
117
118
  if (this.rendered) {
118
119
  this.#response.end();
@@ -122,7 +123,7 @@ class VidaServerController {
122
123
  const responseBody = await this.#performAction(action);
123
124
 
124
125
  if (!this.rendered) {
125
- await this.render(responseBody || {});
126
+ await APM.startSegment('render', true, () => this.render(responseBody || {}));
126
127
  }
127
128
 
128
129
  this.#response.end();
@@ -130,13 +131,14 @@ class VidaServerController {
130
131
 
131
132
 
132
133
  async #performAction(action) {
133
- if (await this.runBeforeCallbacks(action) === false) return false;
134
+ if (await APM.startSegment('beforeCallbacks', true, () => this.runBeforeCallbacks(action)) === false) return false;
134
135
 
135
136
  const parameters = this.constructor.parametersForAction(action);
136
- if (parameters) await this.validateParameters(parameters);
137
+ if (parameters) await APM.startSegment('validateParameters', true, () => this.validateParameters(parameters));
137
138
 
138
- const responseBody = await this[action]();
139
- 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;
140
142
 
141
143
  return responseBody;
142
144
  }
@@ -164,10 +166,17 @@ class VidaServerController {
164
166
  message = err.message;
165
167
  }
166
168
  await this.renderServerError(message);
169
+
170
+ this.reportError(err);
167
171
  }
168
172
  }
169
173
 
170
174
 
175
+ reportError(err) {
176
+ APM.reportError(err);
177
+ }
178
+
179
+
171
180
  /***********************************************************************************************
172
181
  * RESPONSE RENDERING
173
182
  ***********************************************************************************************/
@@ -223,29 +232,29 @@ class VidaServerController {
223
232
  if (body === undefined) return null;
224
233
  if (!body || typeof body != 'object') return body;
225
234
 
226
- if (nodeUtil.types.isProxy(body)) body = {...body};
227
-
228
- body = structuredClone(body);
235
+ let processedBody;
229
236
 
230
237
  if (body.toApiResponse) {
231
238
  const formattedBody = await body.toApiResponse(options);
232
239
  return await this.processJSONBody(formattedBody, options);
233
240
 
234
241
  } else if (Array.isArray(body)) {
242
+ const processed = [];
235
243
  for (const idx in body) {
236
- body[idx] = await this.processJSONBody(body[idx], options);
244
+ processed.push(await this.processJSONBody(body[idx], options));
237
245
  }
246
+ return processed;
238
247
 
239
248
  } else if (body.constructor == Date) {
240
249
  return body.getTime();
241
250
 
242
251
  } else {
252
+ const processed = {};
243
253
  for (let [key, val] of Object.entries(body)) {
244
- body[key] = await this.processJSONBody(val, options);
254
+ processed[key] = await this.processJSONBody(val, options);
245
255
  }
256
+ return processed;
246
257
  }
247
-
248
- return body;
249
258
  }
250
259
 
251
260
 
@@ -428,6 +437,7 @@ class VidaServerController {
428
437
  * HELPERS
429
438
  ***********************************************************************************************/
430
439
  static autoLoadHelpers() {
440
+ this.actions; // make sure that actions are cached before loading helpers
431
441
  const { StaticMethods, InstanceMethods, Accessors } = this._autoLoadModule(this.autoLoadHelperPath);
432
442
 
433
443
  if (StaticMethods) Object.assign(this, StaticMethods);
@@ -460,12 +470,12 @@ class VidaServerController {
460
470
 
461
471
 
462
472
  static get autoLoadHelperPath() {
463
- return `${process.cwd()}/lib/controllers${this.routePrefix}Controller.js`;
473
+ return `${process.cwd()}/lib/controllers${this.routePrefix}Controller`;
464
474
  }
465
475
 
466
476
 
467
477
  static get autoLoadDocumentationPath() {
468
- return `${process.cwd()}/lib/controllers/_docs${this.routePrefix}Controller.js`;
478
+ return `${process.cwd()}/lib/controllers/_docs${this.routePrefix}Controller`;
469
479
  }
470
480
 
471
481
 
@@ -556,6 +566,51 @@ class VidaServerController {
556
566
  }
557
567
 
558
568
 
569
+ /***********************************************************************************************
570
+ * REQUEST PARAMETERS
571
+ ***********************************************************************************************/
572
+ hasParam(name) {
573
+ return Object.prototype.hasOwnProperty.call(this.params, name);
574
+ }
575
+
576
+
577
+ coerceString(value, { fallback = '' } = {}) {
578
+ if (typeof value !== 'string') return fallback;
579
+ const trimmed = value.trim();
580
+ return trimmed || fallback;
581
+ }
582
+
583
+
584
+ coerceBoolean(value, { fallback } = {}) {
585
+ if (typeof value === 'boolean') return value;
586
+ if (typeof value !== 'string') return fallback;
587
+ const normalized = value.trim().toLowerCase();
588
+ if (!normalized) return fallback;
589
+ if (normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on') return true;
590
+ if (normalized === 'false' || normalized === '0' || normalized === 'no' || normalized === 'off') return false;
591
+ return fallback;
592
+ }
593
+
594
+
595
+ coerceInteger(value, { min, max, fallback, nonNegative } = {}) {
596
+ if (value === null || value === undefined) return fallback;
597
+ if (typeof value === 'string' && value.trim() === '') return fallback;
598
+ const parsed = Number(value);
599
+ if (!Number.isFinite(parsed)) return fallback;
600
+ let next = Math.floor(parsed);
601
+ if (nonNegative && next < 0) return fallback;
602
+ if (Number.isFinite(min)) next = Math.max(min, next);
603
+ if (Number.isFinite(max)) next = Math.min(max, next);
604
+ return next;
605
+ }
606
+
607
+
608
+ coerceObject(value, { fallback = {} } = {}) {
609
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value;
610
+ return fallback;
611
+ }
612
+
613
+
559
614
  /***********************************************************************************************
560
615
  * VALIDATIONS
561
616
  ***********************************************************************************************/
@@ -657,10 +712,14 @@ class VidaServerController {
657
712
  * {getFooBar: '/ban/:accountIdd/foo/:userId/bar'}`
658
713
  ***********************************************************************************************/
659
714
  static get actions() {
715
+ if (actionStore.has(this)) return actionStore.get(this);
660
716
  const actionNames = this.actionNames;
661
- return actionNames.map((actionName => {
717
+ const actions = actionNames.map((actionName => {
662
718
  return this._constructAction(actionName);
663
719
  }).bind(this));
720
+
721
+ actionStore.set(this, actions);
722
+ return actions;
664
723
  }
665
724
 
666
725
 
@@ -787,6 +846,24 @@ class VidaServerController {
787
846
  if (aBuf.length !== bBuf.length) return false;
788
847
  return crypto.timingSafeEqual(aBuf, bBuf);
789
848
  }
849
+
850
+
851
+ /***********************************************************************************************
852
+ * APM
853
+ ***********************************************************************************************/
854
+ addApmAttribute(key, value) {
855
+ APM.addCustomAttribute(key, value)
856
+ }
857
+
858
+
859
+ setApmUserId(id) {
860
+ APM.setUserId(id)
861
+ }
862
+
863
+
864
+ startApmSegment(name, record, handler) {
865
+ return APM.startSegment(name, record, handler);
866
+ }
790
867
  }
791
868
 
792
869
 
@@ -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.6",
3
+ "version": "2.0.1",
4
4
  "description": "Core libraries for supporting Vida development",
5
5
  "author": "",
6
6
  "license": "ISC",
@@ -18,13 +18,15 @@
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",
23
24
  "express-request-id": "1.4.1",
24
25
  "express-winston": "^4.0.0",
25
26
  "mustache-express": "^1.2.8",
27
+ "newrelic": "^13.0.0",
26
28
  "pg": "^8.16.3",
27
- "redis": "^5.0.0",
29
+ "redis": "^4.0.0",
28
30
  "response-time": "^2.3.3",
29
31
  "sequelize": "^6.37.7",
30
32
  "sequelize-cli": "^6.6.3",
@@ -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