@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.
- package/.github/workflows/npm-test.yml +24 -0
- package/index.js +1 -1
- package/lib/{active_record → activeRecord}/README.md +3 -3
- package/lib/{active_record → activeRecord}/baseRecord.js +11 -2
- package/lib/http/README.md +2 -2
- package/lib/server/README.md +181 -20
- package/lib/server/errors.js +28 -0
- package/lib/server/index.js +3 -3
- package/lib/server/server.js +7 -52
- package/lib/server/serverController.js +169 -23
- package/package.json +1 -1
- package/scripts/{active_record → activeRecord}/migrate.js +1 -1
- package/test/{active_record → activeRecord}/baseRecord.test.js +46 -90
- package/test/activeRecord/db/connection.test.js +149 -0
- package/test/activeRecord/db/connectionConfiguration.test.js +128 -0
- package/test/activeRecord/db/migrator.test.js +144 -0
- package/test/activeRecord/db/queryInterface.test.js +48 -0
- package/test/activeRecord/helpers/baseRecord.js +32 -0
- package/test/activeRecord/helpers/baseRecordMocks.js +59 -0
- package/test/activeRecord/helpers/connection.js +28 -0
- package/test/activeRecord/helpers/connectionConfiguration.js +32 -0
- package/test/activeRecord/helpers/fixtures.js +39 -0
- package/test/activeRecord/helpers/migrator.js +78 -0
- package/test/activeRecord/helpers/queryInterface.js +29 -0
- package/test/http/client.test.js +61 -239
- package/test/http/error.test.js +23 -47
- package/test/http/helpers/client.js +80 -0
- package/test/http/helpers/error.js +31 -0
- package/test/server/helpers/autoload/TmpWithHelpersController.js +17 -0
- package/test/server/helpers/serverController.js +13 -0
- package/test/server/serverController.test.js +357 -11
- package/test/active_record/db/connection.test.js +0 -221
- package/test/active_record/db/connectionConfiguration.test.js +0 -184
- package/test/active_record/db/migrator.test.js +0 -266
- package/test/active_record/db/queryInterface.test.js +0 -66
- /package/lib/{active_record → activeRecord}/db/connection.js +0 -0
- /package/lib/{active_record → activeRecord}/db/connectionConfiguration.js +0 -0
- /package/lib/{active_record → activeRecord}/db/importSchema.js +0 -0
- /package/lib/{active_record → activeRecord}/db/migration.js +0 -0
- /package/lib/{active_record → activeRecord}/db/migrationTemplate.js +0 -0
- /package/lib/{active_record → activeRecord}/db/migrationVersion.js +0 -0
- /package/lib/{active_record → activeRecord}/db/migrator.js +0 -0
- /package/lib/{active_record → activeRecord}/db/queryInterface.js +0 -0
- /package/lib/{active_record → activeRecord}/db/schema.js +0 -0
- /package/lib/{active_record → activeRecord}/index.js +0 -0
- /package/lib/{active_record → activeRecord}/utils.js +0 -0
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
const { 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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
146
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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,80 +1,30 @@
|
|
|
1
|
-
const { BaseRecord }
|
|
2
|
-
const {
|
|
3
|
-
const
|
|
4
|
-
const {
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const {
|
|
8
|
-
const
|
|
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
|
|
23
|
-
|
|
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
|
|
62
|
-
|
|
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/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
importSchema.mockImplementation(() => structuredClone(schema));
|
|
77
|
-
|
|
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
|
-
[
|
|
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:
|
|
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:
|
|
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(
|
|
152
|
-
const setterMethodName = `_set${camelize(
|
|
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
|
-
[
|
|
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:
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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(
|
|
505
|
+
expect(Cacheable.addHook).toHaveBeenCalledTimes(0);
|
|
550
506
|
});
|
|
551
507
|
|
|
552
508
|
it ('calls BaseRecord#clearSelfCache and BaseRecord#updateCache', async () => {
|
|
553
|
-
const person = new
|
|
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
|
|
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
|
+
});
|