@vida-global/core 1.2.4 → 1.3.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 (46) hide show
  1. package/.github/workflows/npm-test.yml +24 -0
  2. package/index.js +1 -1
  3. package/lib/{active_record → activeRecord}/README.md +3 -3
  4. package/lib/{active_record → activeRecord}/baseRecord.js +11 -2
  5. package/lib/http/README.md +2 -2
  6. package/lib/server/README.md +181 -20
  7. package/lib/server/errors.js +28 -0
  8. package/lib/server/index.js +3 -3
  9. package/lib/server/server.js +7 -52
  10. package/lib/server/serverController.js +169 -23
  11. package/package.json +1 -1
  12. package/scripts/{active_record → activeRecord}/migrate.js +1 -1
  13. package/test/{active_record → activeRecord}/baseRecord.test.js +46 -90
  14. package/test/activeRecord/db/connection.test.js +149 -0
  15. package/test/activeRecord/db/connectionConfiguration.test.js +128 -0
  16. package/test/activeRecord/db/migrator.test.js +144 -0
  17. package/test/activeRecord/db/queryInterface.test.js +48 -0
  18. package/test/activeRecord/helpers/baseRecord.js +32 -0
  19. package/test/activeRecord/helpers/baseRecordMocks.js +59 -0
  20. package/test/activeRecord/helpers/connection.js +28 -0
  21. package/test/activeRecord/helpers/connectionConfiguration.js +32 -0
  22. package/test/activeRecord/helpers/fixtures.js +39 -0
  23. package/test/activeRecord/helpers/migrator.js +78 -0
  24. package/test/activeRecord/helpers/queryInterface.js +29 -0
  25. package/test/http/client.test.js +61 -239
  26. package/test/http/error.test.js +23 -47
  27. package/test/http/helpers/client.js +80 -0
  28. package/test/http/helpers/error.js +31 -0
  29. package/test/server/helpers/autoload/TmpWithHelpersController.js +17 -0
  30. package/test/server/helpers/serverController.js +13 -0
  31. package/test/server/serverController.test.js +357 -11
  32. package/test/active_record/db/connection.test.js +0 -221
  33. package/test/active_record/db/connectionConfiguration.test.js +0 -184
  34. package/test/active_record/db/migrator.test.js +0 -266
  35. package/test/active_record/db/queryInterface.test.js +0 -66
  36. /package/lib/{active_record → activeRecord}/db/connection.js +0 -0
  37. /package/lib/{active_record → activeRecord}/db/connectionConfiguration.js +0 -0
  38. /package/lib/{active_record → activeRecord}/db/importSchema.js +0 -0
  39. /package/lib/{active_record → activeRecord}/db/migration.js +0 -0
  40. /package/lib/{active_record → activeRecord}/db/migrationTemplate.js +0 -0
  41. /package/lib/{active_record → activeRecord}/db/migrationVersion.js +0 -0
  42. /package/lib/{active_record → activeRecord}/db/migrator.js +0 -0
  43. /package/lib/{active_record → activeRecord}/db/queryInterface.js +0 -0
  44. /package/lib/{active_record → activeRecord}/db/schema.js +0 -0
  45. /package/lib/{active_record → activeRecord}/index.js +0 -0
  46. /package/lib/{active_record → activeRecord}/utils.js +0 -0
@@ -1,5 +1,10 @@
1
- const { logger } = require('../logger');
2
- const { camelize } = require('inflection');
1
+ const { logger } = require('../logger');
2
+ const { camelize, singularize } = require('inflection');
3
+ const{ AuthorizationError,
4
+ ForbiddenError,
5
+ NotFoundError,
6
+ ServerError,
7
+ ValidationError } = require('./errors');
3
8
 
4
9
 
5
10
  class VidaServerController {
@@ -70,6 +75,71 @@ class VidaServerController {
70
75
  }
71
76
 
72
77
 
78
+ /***********************************************************************************************
79
+ * PERFORM ACTION
80
+ ***********************************************************************************************/
81
+ async performRequest(action) {
82
+ try {
83
+ await this.#performRequest(action)
84
+ } catch(err) {
85
+ await this.#handleError(err);
86
+ }
87
+ }
88
+
89
+
90
+ async #performRequest(action) {
91
+ await this.setupRequestState();
92
+
93
+ if (this.rendered) {
94
+ this.#response.end();
95
+ return;
96
+ }
97
+
98
+ this.setupCallbacks();
99
+
100
+ const responseBody = await this.#performAction(action);
101
+
102
+ if (!this.rendered) {
103
+ await this.render(responseBody || {});
104
+ }
105
+
106
+ this.#response.end();
107
+ }
108
+
109
+
110
+ async #performAction(action) {
111
+ if (await this.runBeforeCallbacks(action) === false) return false;
112
+ const responseBody = await this[action]();
113
+ if (await this.runAfterCallbacks(action) === false) return false;
114
+
115
+ return responseBody;
116
+ }
117
+
118
+
119
+ async #handleError(err) {
120
+ if (err instanceof AuthorizationError) {
121
+ await this.renderUnauthorizedResponse(err.message);
122
+
123
+ } else if (err instanceof ForbiddenError) {
124
+ await this.renderForbiddenResponse(err.message);
125
+
126
+ } else if (err instanceof NotFoundError) {
127
+ await this.renderNotFoundResponse(err.message);
128
+
129
+ } else if (err instanceof ValidationError) {
130
+ await this.renderErrors(err.message, err.errors);
131
+
132
+ } else {
133
+ this.statusCode = 500;
134
+ await this.render({error: err.message});
135
+ if (process.env.NODE_ENV == 'development') console.log(err);
136
+ }
137
+ }
138
+
139
+
140
+ /***********************************************************************************************
141
+ * RESPONSE RENDERING
142
+ ***********************************************************************************************/
73
143
  async render(body, options={}) {
74
144
  if (typeof body == 'string') {
75
145
  this._response.send(body);
@@ -81,6 +151,46 @@ class VidaServerController {
81
151
  }
82
152
 
83
153
 
154
+ async renderErrors(message, fields) {
155
+ this.statusCode = 400;
156
+
157
+ const errors = {message: null, fields: {}};
158
+
159
+ if (message && !fields && typeof message == 'object') {
160
+ fields = message;
161
+ message = null;
162
+ }
163
+
164
+ if (message) errors.message = message;
165
+ if (fields) {
166
+ for (const [field, values] of Object.entries(fields)) {
167
+ if (!Array.isArray(values)) fields[field] = [values];
168
+ }
169
+ errors.fields = fields;
170
+ }
171
+
172
+ await this.render({ errors });
173
+ }
174
+
175
+
176
+ async renderNotFoundResponse(message=null) {
177
+ this.statusCode = 404;
178
+ await this.render({ message });
179
+ }
180
+
181
+
182
+ async renderUnauthorizedResponse(message=null) {
183
+ this.statusCode = 401;
184
+ await this.render({ message });
185
+ }
186
+
187
+
188
+ async renderForbiddenResponse(message=null) {
189
+ this.statusCode = 403;
190
+ await this.render({ message });
191
+ }
192
+
193
+
84
194
  async formatJSONBody(body, options) {
85
195
  body = await this._formatJSONBody(body, options);
86
196
  if (options.standardize === false) return body;
@@ -122,6 +232,8 @@ class VidaServerController {
122
232
  return 'bad request'
123
233
  case 401:
124
234
  return 'unauthorized';
235
+ case 403:
236
+ return 'forbidden';
125
237
  case 404:
126
238
  return 'not found';
127
239
  case 500:
@@ -130,21 +242,34 @@ class VidaServerController {
130
242
  }
131
243
 
132
244
 
133
- async renderErrors(errors) {
134
- this.statusCode = 400;
135
- await this.render({ errors });
245
+ /***********************************************************************************************
246
+ * HELPERS
247
+ ***********************************************************************************************/
248
+ static autoLoadHelpers() {
249
+ try {
250
+ this._autoLoadHelpers()
251
+ } catch(err) {
252
+ if (err.message.includes(`Cannot find module '${this.autoLoadHelperPath}'`)) return;
253
+ throw err;
254
+ }
136
255
  }
137
256
 
138
257
 
139
- async renderNotFoundResponse() {
140
- this.statusCode = 404;
141
- await this.render();
258
+ static _autoLoadHelpers() {
259
+ const { InstanceMethods, Accessors } = require(this.autoLoadHelperPath);
260
+
261
+ if (InstanceMethods) Object.assign(this.prototype, InstanceMethods);
262
+
263
+ if (Accessors) {
264
+ for (const [attrName, attrAccessors] of Object.entries(Accessors)) {
265
+ Object.defineProperty(this.prototype, attrName, attrAccessors);
266
+ }
267
+ }
142
268
  }
143
269
 
144
270
 
145
- async renderUnauthorizedResponse() {
146
- this.statusCode = 401;
147
- await this.render();
271
+ static get autoLoadHelperPath() {
272
+ return `${process.cwd()}/lib/controllers${this.routePrefix}Controller.js`;
148
273
  }
149
274
 
150
275
 
@@ -222,7 +347,7 @@ class VidaServerController {
222
347
  if (parameterErrors.length) errors[parameterName] = parameterErrors;
223
348
  }
224
349
 
225
- if (Object.keys(errors).length) throw new ValidationError(errors);
350
+ if (Object.keys(errors).length) throw new ValidationError('', errors);
226
351
  }
227
352
 
228
353
 
@@ -264,6 +389,11 @@ class VidaServerController {
264
389
  }
265
390
 
266
391
 
392
+ validateRegex(value, regex) {
393
+ if (!regex.test(value)) return `must match the pattern ${regex}`;
394
+ }
395
+
396
+
267
397
  validateFunction(value, fnc) {
268
398
  const error = fnc(value);
269
399
  if (error) return error;
@@ -272,6 +402,7 @@ class VidaServerController {
272
402
 
273
403
  /***********************************************************************************************
274
404
  * ACTION SETUP
405
+ ************************************************************************************************
275
406
  * The controller looks for any methods that fit the pattern of `{httpMethod}ActionName`
276
407
  * (e.g. getFooBar) and prepares server routes for them. By default, a method `getFooBar` on a
277
408
  * `BazController` would have an endpoint of `GET /baz/fooBar`. The server will also prepend
@@ -304,7 +435,26 @@ class VidaServerController {
304
435
 
305
436
  static _routeForAction(method, path, routePrefix, actionName) {
306
437
  if (this.routes[actionName]) return this.routes[actionName];
307
- if (path == 'Index') return routePrefix;
438
+
439
+ // If the path starts with Record, use :id as a path parameter
440
+ /*
441
+ class UsersController {
442
+ // Get /users
443
+ async getIndex() {}
444
+
445
+ // Get /user/:id
446
+ async getRecord() {}
447
+
448
+ // Get /user/:id/status
449
+ async getRecordStatus() {}
450
+ }
451
+ */
452
+ if (/^Record($|[A-Z])/.test(path)) {
453
+ routePrefix = `${singularize(routePrefix)}/:id`;
454
+ path = path == 'Record' ? 'Index' : path.replace(/^Record/, '');
455
+ }
456
+
457
+ if (path == 'Index') return routePrefix;
308
458
 
309
459
  path = path.charAt(0).toLowerCase() + path.slice(1);
310
460
  return `${routePrefix}/${path}`;
@@ -337,19 +487,15 @@ class VidaServerController {
337
487
  }
338
488
 
339
489
 
340
- class AuthorizationError extends Error {}
341
- class ValidationError extends Error {
342
- #errors;
343
- constructor(errors) {
344
- super();
345
- this.#errors = errors;
346
- }
347
- get errors() { return structuredClone(this.#errors) };
490
+ VidaServerController.prototype.Errors = {
491
+ Authorization: AuthorizationError,
492
+ Forbidden: ForbiddenError,
493
+ NotFound: NotFoundError,
494
+ Server: ServerError,
495
+ Validation: ValidationError
348
496
  }
349
497
 
350
498
 
351
499
  module.exports = {
352
- AuthorizationError,
353
- ValidationError,
354
500
  VidaServerController
355
501
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vida-global/core",
3
- "version": "1.2.4",
3
+ "version": "1.3.0",
4
4
  "description": "Core libraries for supporting Vida development",
5
5
  "author": "",
6
6
  "license": "ISC",
@@ -1,4 +1,4 @@
1
- const migration = require('../../lib/active_record/db/migration');
1
+ const migration = require('../../lib/activeRecord/db/migration');
2
2
  const { Command } = require('commander');
3
3
 
4
4
 
@@ -1,80 +1,30 @@
1
- const { BaseRecord } = require('../../lib/active_record/baseRecord');
2
- const { Connection} = require('../../lib/active_record/db/connection');
3
- const { ConnectionConfiguration } = require('../../lib/active_record/db/connectionConfiguration');
4
- const { getActiveRecordSchema } = require('../../lib/active_record/db/schema');
5
- const importSchema = require('../../lib/active_record/db/importSchema');
6
- const { Model, Sequelize } = require('sequelize');
7
- const { redisClientFactory } = require('../../lib/redis');
8
- const TestHelpers = require('@vida-global/test-helpers');
9
- const { camelize } = require('inflection');
10
-
11
-
12
- const connectionConfig = {database: ' ', host: ' ', password: ' ', username: ' '};
13
- const configSpy = jest.spyOn(ConnectionConfiguration, '_fetchAllConfigs');
14
- configSpy.mockImplementation(() => ({default: {test: connectionConfig}}));
15
-
16
- const mockSequelize = new (class MockSequelize {})();
17
- const connectionSpy = jest.spyOn(Connection.prototype, '_sequelize', 'get');
18
- connectionSpy.mockImplementation(() => mockSequelize);
1
+ const { BaseRecord } = require('../../lib/activeRecord/baseRecord');
2
+ const { getActiveRecordSchema } = require('../../lib/activeRecord/db/schema');
3
+ const importSchema = require('../../lib/activeRecord/db/importSchema');
4
+ const { Model, Sequelize } = require('sequelize');
5
+ const { redisClientFactory } = require('../../lib/redis');
6
+ const TestHelpers = require('@vida-global/test-helpers');
7
+ const { camelize } = require('inflection');
8
+ const Helpers = require('./helpers/baseRecord');
19
9
 
20
10
 
21
11
  jest.mock('sequelize', () => {
22
- const TestHelpers = require('@vida-global/test-helpers');
23
- class MockModel {
24
- constructor(data, opts={}) {
25
- this.dataValues = data || {}
26
- this.options = opts;
27
- for (const [k,v] of Object.entries(this.dataValues)) {
28
- this[k] = v;
29
- }
30
- };
31
- static addHook = jest.fn()
32
- static init = jest.fn()
33
- static findByPk = jest.fn();
34
- static findAll = jest.fn();
35
- static primaryKeyAttribute = TestHelpers.Faker.Text.randomString();
36
- toJSON() { return this.dataValues }
37
- }
38
-
39
- MockModel.findAll.mockImplementation(() => []);
40
-
41
- const dataTypeKey1 = TestHelpers.Faker.Text.randomString();
42
- const dataTypeValue1 = TestHelpers.Faker.Text.randomString();
43
- const dataTypeKey2 = TestHelpers.Faker.Text.randomString();
44
- const dataTypeValue2 = TestHelpers.Faker.Text.randomString();
45
- const Sequelize = {DataTypes: {
46
- [dataTypeKey1]: {types: {postgres: [dataTypeValue1]}, key: dataTypeKey1},
47
- [dataTypeKey2]: {types: {postgres: [dataTypeValue2]}, key: dataTypeKey2},
48
- postgres: {}
49
- }};
50
- return { Model: MockModel, Sequelize } ;
12
+ const { MockModel, MockSequelize } = require('./helpers/baseRecordMocks');
13
+ return { Model: MockModel, Sequelize: MockSequelize } ;
51
14
  });
52
15
 
53
- const dataTypeKey1 = Object.keys(Sequelize.DataTypes)[0];
54
- const dataTypeKey2 = Object.keys(Sequelize.DataTypes)[1];
55
- const dataTypeValue1 = Sequelize.DataTypes[dataTypeKey1].types.postgres[0];
56
- const dataTypeValue2 = Sequelize.DataTypes[dataTypeKey2].types.postgres[0];
57
- const col1Name = `${TestHelpers.Faker.Text.randomString()}_${TestHelpers.Faker.Text.randomString()}_${TestHelpers.Faker.Text.randomString()}`;
58
-
59
-
60
16
  jest.mock('../../lib/redis', () => {
61
- const client = {
62
- del: jest.fn(),
63
- mGet: jest.fn(),
64
- mSet: jest.fn(),
65
- }
66
- const redisClientFactory = () => client;
67
-
68
- return { redisClientFactory };
17
+ const { mockRedisClientFactory } = require('./helpers/baseRecordMocks');
18
+ return { redisClientFactory: mockRedisClientFactory };
69
19
  });
70
20
 
71
- jest.mock('../../lib/active_record/db/importSchema', () => jest.fn());
72
- const schema = {created_at: {type: dataTypeValue1},
73
- updated_at: {type: dataTypeValue2},
74
- [col1Name]: {type: dataTypeValue2}
75
- };
76
- importSchema.mockImplementation(() => structuredClone(schema));
77
- beforeEach(() => importSchema.mockImplementation(() => structuredClone(schema)));
21
+ jest.mock('../../lib/activeRecord/db/importSchema', () => jest.fn());
22
+ importSchema.mockImplementation(() => structuredClone(Helpers.schema));
23
+
24
+
25
+ beforeEach(() => {
26
+ importSchema.mockImplementation(() => structuredClone(Helpers.schema));
27
+ })
78
28
 
79
29
 
80
30
  afterEach(() => {
@@ -94,9 +44,9 @@ describe('BaseRecord', () => {
94
44
  it ('calls `getBaseRecordSchema` and passes the response to init', () => {
95
45
  class User extends BaseRecord {}
96
46
  User.initialize();
97
- const tableDetails = {created_at: {type: Sequelize.DataTypes[dataTypeKey1]},
98
- updated_at: {type: Sequelize.DataTypes[dataTypeKey2]},
99
- [col1Name]: {type: Sequelize.DataTypes[dataTypeKey2]}
47
+ const tableDetails = {created_at: {type: Sequelize.DataTypes[Helpers.dataTypeKey1]},
48
+ updated_at: {type: Sequelize.DataTypes[Helpers.dataTypeKey2]},
49
+ [Helpers.colName]: {type: Sequelize.DataTypes[Helpers.dataTypeKey2]}
100
50
  };
101
51
  expect(importSchema).toHaveBeenCalledTimes(1);
102
52
  expect(Model.init).toHaveBeenCalledTimes(1);
@@ -105,7 +55,7 @@ describe('BaseRecord', () => {
105
55
  createdAt: 'created_at',
106
56
  deletedAt: 'deleted_at',
107
57
  modelName: 'User',
108
- sequelize: mockSequelize,
58
+ sequelize: new Helpers.MockSequelize(),
109
59
  tableName: 'users',
110
60
  updatedAt: 'updated_at'
111
61
  });
@@ -120,7 +70,7 @@ describe('BaseRecord', () => {
120
70
  createdAt: 'created_at',
121
71
  deletedAt: 'deleted_at',
122
72
  modelName: 'User',
123
- sequelize: mockSequelize,
73
+ sequelize: new Helpers.MockSequelize(),
124
74
  tableName: 'users',
125
75
  timestamps: false,
126
76
  updatedAt: 'updated_at'
@@ -148,17 +98,17 @@ describe('BaseRecord', () => {
148
98
 
149
99
  it ('allows for overriding getters and setters', () => {
150
100
  class User extends BaseRecord {}
151
- const getterMethodName = `_get${camelize(col1Name)}`;
152
- const setterMethodName = `_set${camelize(col1Name)}`;
101
+ const getterMethodName = `_get${camelize(Helpers.colName)}`;
102
+ const setterMethodName = `_set${camelize(Helpers.colName)}`;
153
103
  const getter = jest.fn();
154
104
  const setter = jest.fn();
155
105
  User.prototype[getterMethodName] = getter;
156
106
  User.prototype[setterMethodName] = setter;
157
107
  User.initialize();
158
- const tableDetails = {created_at: {type: Sequelize.DataTypes[dataTypeKey1]},
159
- updated_at: {type: Sequelize.DataTypes[dataTypeKey2]},
160
- [col1Name]: {
161
- type: Sequelize.DataTypes[dataTypeKey2],
108
+ const tableDetails = {created_at: {type: Sequelize.DataTypes[Helpers.dataTypeKey1]},
109
+ updated_at: {type: Sequelize.DataTypes[Helpers.dataTypeKey2]},
110
+ [Helpers.colName]: {
111
+ type: Sequelize.DataTypes[Helpers.dataTypeKey2],
162
112
  get: getter,
163
113
  set: setter,
164
114
  }};
@@ -167,7 +117,7 @@ describe('BaseRecord', () => {
167
117
  createdAt: 'created_at',
168
118
  deletedAt: 'deleted_at',
169
119
  modelName: 'User',
170
- sequelize: mockSequelize,
120
+ sequelize: new Helpers.MockSequelize(),
171
121
  tableName: 'users',
172
122
  updatedAt: 'updated_at'
173
123
  });
@@ -535,27 +485,33 @@ describe('BaseRecord', () => {
535
485
  });
536
486
 
537
487
 
538
- describe('BaseRecord._afterSaveCacheHook', () => {
539
- it ('is called as an after save hook for cached models', () => {
540
- expect(Person.addHook).toHaveBeenCalledTimes(0);
541
- Person.initialize();
542
- expect(Person.addHook).toHaveBeenCalledTimes(1);
543
- expect(Person.addHook).toHaveBeenCalledWith('afterSave', Person._afterSaveCacheHook);
488
+ const hooks = ['Save', 'Destroy'];
489
+ describe.each(hooks)('BaseRecord._after%sCacheHook', (hook) => {
490
+ class Cacheable extends BaseRecord {
491
+ static get isCacheable() { return true; }
492
+ }
493
+ const hookMethod = `_after${hook}CacheHook`;
494
+
495
+ it (`is called as an after ${hook.toLowerCase()} hook for cached models`, () => {
496
+ expect(Cacheable.addHook).toHaveBeenCalledTimes(0);
497
+ Cacheable.initialize();
498
+ expect(Cacheable.addHook).toHaveBeenCalledTimes(2);
499
+ expect(Cacheable.addHook).toHaveBeenCalledWith(`after${hook}`, Cacheable[hookMethod]);
544
500
  });
545
501
 
546
502
  it ('is not called as an after save hook for non cached models', () => {
547
503
  class Foo extends BaseRecord {}
548
504
  Foo.initialize();
549
- expect(Person.addHook).toHaveBeenCalledTimes(0);
505
+ expect(Cacheable.addHook).toHaveBeenCalledTimes(0);
550
506
  });
551
507
 
552
508
  it ('calls BaseRecord#clearSelfCache and BaseRecord#updateCache', async () => {
553
- const person = new Person();
509
+ const person = new Cacheable();
554
510
  const clearSpy = jest.spyOn(person, 'clearSelfCache');
555
511
  const updateSpy = jest.spyOn(person, 'updateCache');
556
512
  const options = {};
557
513
 
558
- await Person._afterSaveCacheHook(person,options);
514
+ await Cacheable[hookMethod](person,options);
559
515
 
560
516
  expect(clearSpy).toHaveBeenCalledTimes(1);
561
517
  expect(updateSpy).toHaveBeenCalledTimes(1);
@@ -0,0 +1,149 @@
1
+ const { Connection } = require('../../../lib/activeRecord/db/connection');
2
+ const { ConnectionConfiguration } = require('../../../lib/activeRecord/db/connectionConfiguration');
3
+ const Helpers = require('../helpers/connection');
4
+
5
+
6
+ let fetchConfigsSpy;
7
+
8
+ jest.mock('sequelize', () => {
9
+ const { MockSequelize } = require('../helpers/fixtures');
10
+ return { Sequelize: MockSequelize };
11
+ });
12
+
13
+ beforeEach(() => {
14
+ fetchConfigsSpy = jest.spyOn(ConnectionConfiguration, '_fetchAllConfigs');
15
+ fetchConfigsSpy.mockImplementation(Helpers.buildConfigs);
16
+ });
17
+
18
+ afterEach(() => {
19
+ fetchConfigsSpy.mockRestore();
20
+ Connection.clearConnectionsCache();
21
+ process.env.NODE_ENV = Helpers.originalNodeEnv;
22
+ });
23
+
24
+
25
+ describe('Connection', () => {
26
+ describe('Connection#_sequelize', () => {
27
+ it ('caches connections per database id', () => {
28
+ const conn1 = new Connection();
29
+ const conn2 = new Connection();
30
+ const conn3 = new Connection(Helpers.databaseId1);
31
+
32
+ expect(conn1._sequelize).toBe(conn2._sequelize);
33
+ expect(conn1._sequelize).not.toBe(conn3._sequelize);
34
+ });
35
+
36
+
37
+ describe('settings (postgres)', () => {
38
+ it.each([
39
+ ['default', () => new Connection(), Helpers.defaultConfig],
40
+ ['secondary', () => new Connection(Helpers.databaseId1), Helpers.secondaryConfig],
41
+ ])('maps %s postgres config into sequelize options', (_label, connectionFactory, expectedConfig) => {
42
+ const connection = connectionFactory();
43
+ const options = connection._sequelize.options;
44
+
45
+ expect(options.database).toEqual(expectedConfig.database);
46
+ expect(options.dialect).toEqual('postgres');
47
+ expect(options.host).toEqual(expectedConfig.host);
48
+ expect(options.password).toEqual(expectedConfig.password);
49
+ expect(options.port).toEqual(expectedConfig.port);
50
+ expect(options.username).toEqual(expectedConfig.username);
51
+ });
52
+
53
+ it ('requires ssl when configured', () => {
54
+ const conn = new Connection(Helpers.databaseId2);
55
+
56
+ expect(conn._sequelize.options.dialectOptions).toEqual({ ssl: { require: true } });
57
+ });
58
+
59
+ it.each([
60
+ ['development', undefined],
61
+ ['production', false],
62
+ ])('sets logging for %s', (environment, expectedLogging) => {
63
+ process.env.NODE_ENV = environment;
64
+ const conn = new Connection();
65
+
66
+ expect(conn._sequelize.options.logging).toBe(expectedLogging);
67
+ });
68
+
69
+ it ('sets the configured pool or defaults', () => {
70
+ const conn1 = new Connection();
71
+ const conn2 = new Connection(Helpers.databaseId1);
72
+
73
+ expect(conn1._sequelize.options.pool).toEqual(Helpers.defaultConfig.pool);
74
+ expect(conn2._sequelize.options.pool).toEqual({ min: 0, max: 5 });
75
+ });
76
+
77
+ it ('configures underscored naming', () => {
78
+ const conn = new Connection();
79
+
80
+ expect(conn._sequelize.options.define.underscored).toBeTruthy();
81
+ });
82
+
83
+ it ('configures replication when readers are present', () => {
84
+ const replicatedConfig = Helpers.buildPostgresConfig({
85
+ readers: [Helpers.buildPostgresConfig(), Helpers.buildPostgresConfig()],
86
+ });
87
+ fetchConfigsSpy.mockImplementation(() => ({ default: { test: replicatedConfig } }));
88
+
89
+ const conn = new Connection();
90
+ const options = conn._sequelize.options;
91
+
92
+ expect(options.database).toBe(undefined);
93
+ expect(options.host).toBe(undefined);
94
+ expect(options.port).toBe(undefined);
95
+ expect(options.username).toBe(undefined);
96
+ expect(options.password).toBe(undefined);
97
+ expect(options.replication).toEqual({
98
+ write: {
99
+ database: replicatedConfig.database,
100
+ host: replicatedConfig.host,
101
+ password: replicatedConfig.password,
102
+ port: replicatedConfig.port,
103
+ username: replicatedConfig.username,
104
+ },
105
+ read: replicatedConfig.readers,
106
+ });
107
+ });
108
+ });
109
+
110
+
111
+ describe('settings (sqlite)', () => {
112
+ it ('maps sqlite options', () => {
113
+ const conn = new Connection('sqlite');
114
+
115
+ expect(conn._sequelize.options.dialect).toEqual('sqlite');
116
+ expect(conn._sequelize.options.storage).toEqual(
117
+ `${process.cwd()}/config/db/database.test.sqlite`,
118
+ );
119
+ });
120
+ });
121
+ });
122
+
123
+
124
+ describe('Connection#close', () => {
125
+ it ('closes a single connection', () => {
126
+ const conn = new Connection();
127
+ conn._sequelize; // reference it to initiate the connection
128
+
129
+ conn.close();
130
+
131
+ expect(conn._sequelize.close).toHaveBeenCalledTimes(1);
132
+ });
133
+ });
134
+
135
+
136
+ describe('Connection.closeAll', () => {
137
+ it ('closes all cached connections', () => {
138
+ const defaultConnection = new Connection();
139
+ const secondaryConnection = new Connection(Helpers.databaseId1);
140
+ defaultConnection._sequelize; // reference it to initiate the connection
141
+ secondaryConnection._sequelize; // reference it to initiate the connection
142
+
143
+ Connection.closeAll();
144
+
145
+ expect(defaultConnection._sequelize.close).toHaveBeenCalledTimes(1);
146
+ expect(secondaryConnection._sequelize.close).toHaveBeenCalledTimes(1);
147
+ });
148
+ });
149
+ });