@vida-global/core 1.4.1 → 1.4.2
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/index.js +13 -1
- package/lib/server/errors.js +65 -12
- package/lib/server/index.js +3 -2
- package/lib/server/serverController.js +39 -65
- package/package.json +1 -1
- package/test/server/errors.test.js +54 -0
- package/test/server/serverController.test.js +46 -8
package/index.js
CHANGED
|
@@ -5,10 +5,22 @@ const ActiveRecord = require('./lib/activeRecord');
|
|
|
5
5
|
const redisLibs = require('./lib/redis');
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
const {
|
|
9
|
+
AbstractServerError,
|
|
10
|
+
ApiDocsGenerator,
|
|
11
|
+
VidaServer,
|
|
12
|
+
VidaServerController,
|
|
13
|
+
} = serverLibs;
|
|
14
|
+
const serverErrorsToExport = Object.values(serverLibs).filter(obj => obj.prototype instanceof AbstractServerError);
|
|
15
|
+
|
|
16
|
+
|
|
8
17
|
module.exports = {
|
|
9
18
|
ActiveRecord,
|
|
10
19
|
...httpLibs,
|
|
11
20
|
logger,
|
|
12
21
|
...redisLibs,
|
|
13
|
-
...
|
|
22
|
+
...serverErrorsToExport,
|
|
23
|
+
ApiDocsGenerator,
|
|
24
|
+
VidaServer,
|
|
25
|
+
VidaServerController,
|
|
14
26
|
};
|
package/lib/server/errors.js
CHANGED
|
@@ -1,16 +1,58 @@
|
|
|
1
|
-
|
|
1
|
+
const Statuses = {
|
|
2
|
+
BadRequest: 400,
|
|
3
|
+
Unauthorized: 401,
|
|
4
|
+
PaymentRequired: 402,
|
|
5
|
+
Forbidden: 403,
|
|
6
|
+
NotFound: 404,
|
|
7
|
+
MethodNotAllowed: 405,
|
|
8
|
+
RequestTimeout: 408,
|
|
9
|
+
Conflict: 409,
|
|
10
|
+
Gone: 410,
|
|
11
|
+
ContentTooLarge: 413,
|
|
12
|
+
UnsupportedMediaType: 415,
|
|
13
|
+
UnprocessableContent: 422,
|
|
14
|
+
Locked: 423,
|
|
15
|
+
TooManyRequests: 429,
|
|
16
|
+
Server: 500,
|
|
17
|
+
NotImplemented: 501,
|
|
18
|
+
BadGateway: 502,
|
|
19
|
+
ServiceUnavailable: 503,
|
|
20
|
+
GatewayTimeout: 504,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const classNameForErrorType = (type) => `${type}Error`;
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AbstractServerError extends Error {
|
|
2
29
|
#message;
|
|
3
|
-
|
|
30
|
+
#body;
|
|
31
|
+
constructor(message, body) {
|
|
4
32
|
super();
|
|
5
33
|
this.#message = message;
|
|
34
|
+
this.#body = body || null;
|
|
6
35
|
}
|
|
7
36
|
get message() { return this.#message; };
|
|
37
|
+
get body() { return this.#body; };
|
|
38
|
+
get type() {
|
|
39
|
+
const clsName = this.constructor.name;
|
|
40
|
+
return clsName == 'ServerError' ? 'ServerError' : clsName.replace('Error', '')
|
|
41
|
+
}
|
|
8
42
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const ErrorClasses = {};
|
|
46
|
+
for (const [type, statusCode] of Object.entries(Statuses)) {
|
|
47
|
+
const clsName = classNameForErrorType(type);
|
|
48
|
+
const cls = class extends AbstractServerError {};
|
|
49
|
+
ErrorClasses[clsName] = cls
|
|
50
|
+
Object.defineProperty(cls, 'name', { value: clsName });
|
|
51
|
+
Object.defineProperty(cls, 'statusCode', { value: statusCode });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ValidationError extends AbstractServerError {
|
|
14
56
|
#fields;
|
|
15
57
|
constructor(message, fields) {
|
|
16
58
|
super(message);
|
|
@@ -20,11 +62,22 @@ class ValidationError extends ServerError {
|
|
|
20
62
|
}
|
|
21
63
|
|
|
22
64
|
|
|
65
|
+
function errorForStatusCode(code) {
|
|
66
|
+
for (const [type, aCode] of Object.entries(Statuses)) {
|
|
67
|
+
if (code == aCode) {
|
|
68
|
+
return ErrorClasses[classNameForErrorType(type)];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
23
76
|
module.exports = {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
77
|
+
...ErrorClasses,
|
|
78
|
+
AuthorizationError: ErrorClasses.UnauthorizedError, // Deprecated
|
|
79
|
+
errorForStatusCode,
|
|
80
|
+
AbstractServerError,
|
|
81
|
+
Statuses,
|
|
29
82
|
ValidationError
|
|
30
83
|
}
|
package/lib/server/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const { VidaServer } = require('./server');
|
|
2
2
|
const { VidaServerController } = require('./serverController');
|
|
3
|
-
const
|
|
3
|
+
const Errors = require('./errors');
|
|
4
4
|
const { ApiDocsGenerator } = require('./apiDocsGenerator');
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
module.exports = {
|
|
7
8
|
ApiDocsGenerator,
|
|
8
|
-
...
|
|
9
|
+
...Errors,
|
|
9
10
|
VidaServer,
|
|
10
11
|
VidaServerController,
|
|
11
12
|
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
const { logger } = require('../logger');
|
|
2
2
|
const { camelize, singularize } = require('inflection');
|
|
3
|
-
const
|
|
4
|
-
ConflictError,
|
|
5
|
-
ForbiddenError,
|
|
6
|
-
NotFoundError,
|
|
7
|
-
ServerError,
|
|
8
|
-
ValidationError } = require('./errors');
|
|
3
|
+
const Errors = require('./errors');
|
|
9
4
|
|
|
10
5
|
|
|
11
6
|
const AUTH_CALLBACK_NAME = 'authenticateRequest';
|
|
@@ -122,25 +117,22 @@ class VidaServerController {
|
|
|
122
117
|
|
|
123
118
|
|
|
124
119
|
async #handleError(err) {
|
|
125
|
-
if (err instanceof AuthorizationError) {
|
|
126
|
-
await this.renderUnauthorized(err.message);
|
|
120
|
+
if (err instanceof Errors.AuthorizationError) {
|
|
121
|
+
await this.renderUnauthorized(err.message, err.body);
|
|
127
122
|
|
|
128
|
-
} else if (err instanceof
|
|
129
|
-
await this.renderConflict(err.message);
|
|
130
|
-
|
|
131
|
-
} else if (err instanceof ForbiddenError) {
|
|
132
|
-
await this.renderForbidden(err.message);
|
|
133
|
-
|
|
134
|
-
} else if (err instanceof NotFoundError) {
|
|
135
|
-
await this.renderNotFound(err.message);
|
|
136
|
-
|
|
137
|
-
} else if (err instanceof ValidationError) {
|
|
123
|
+
} else if (err instanceof Errors.ValidationError) {
|
|
138
124
|
await this.renderErrors(err.message, err.fields);
|
|
139
125
|
|
|
126
|
+
} else if (err instanceof Errors.AbstractServerError) {
|
|
127
|
+
const renderer = `render${err.type}`;
|
|
128
|
+
await this[renderer](err.message, err.body);
|
|
129
|
+
|
|
140
130
|
} else {
|
|
141
131
|
this._response.error = err;
|
|
142
|
-
let message;
|
|
143
|
-
if (
|
|
132
|
+
let message = null;
|
|
133
|
+
if (process.env.NODE_ENV != 'development') {
|
|
134
|
+
// don't show error details outside of development
|
|
135
|
+
} else if (typeof err == 'string') {
|
|
144
136
|
message = err;
|
|
145
137
|
} else {
|
|
146
138
|
message = err.message;
|
|
@@ -170,8 +162,6 @@ class VidaServerController {
|
|
|
170
162
|
|
|
171
163
|
|
|
172
164
|
async renderErrors(message, fields) {
|
|
173
|
-
this.statusCode = 400;
|
|
174
|
-
|
|
175
165
|
const errors = {message: null, fields: {}};
|
|
176
166
|
|
|
177
167
|
if (message && !fields && typeof message == 'object') {
|
|
@@ -179,7 +169,6 @@ class VidaServerController {
|
|
|
179
169
|
message = null;
|
|
180
170
|
}
|
|
181
171
|
|
|
182
|
-
if (message) errors.message = message;
|
|
183
172
|
if (fields) {
|
|
184
173
|
for (const [field, values] of Object.entries(fields)) {
|
|
185
174
|
if (!Array.isArray(values)) fields[field] = [values];
|
|
@@ -187,15 +176,7 @@ class VidaServerController {
|
|
|
187
176
|
errors.fields = fields;
|
|
188
177
|
}
|
|
189
178
|
|
|
190
|
-
await this.
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
async renderServerError(message=null) {
|
|
195
|
-
if (process.env.NODE_ENV != 'development') {
|
|
196
|
-
message = null;
|
|
197
|
-
}
|
|
198
|
-
return await this.#renderError(500, message);
|
|
179
|
+
await this.renderBadRequest(message, { errors });
|
|
199
180
|
}
|
|
200
181
|
|
|
201
182
|
|
|
@@ -213,6 +194,8 @@ class VidaServerController {
|
|
|
213
194
|
if (body === undefined) return null;
|
|
214
195
|
if (!body || typeof body != 'object') return body;
|
|
215
196
|
|
|
197
|
+
body = structuredClone(body);
|
|
198
|
+
|
|
216
199
|
if (body.toApiResponse) {
|
|
217
200
|
const formattedBody = await body.toApiResponse(options);
|
|
218
201
|
return await this.processJSONBody(formattedBody, options);
|
|
@@ -285,27 +268,19 @@ class VidaServerController {
|
|
|
285
268
|
return 'too many requests';
|
|
286
269
|
case 500:
|
|
287
270
|
return 'server error';
|
|
271
|
+
case 501:
|
|
272
|
+
return 'not implemented';
|
|
273
|
+
case 502:
|
|
274
|
+
return 'bad gateway';
|
|
275
|
+
case 503:
|
|
276
|
+
return 'service unavailable';
|
|
277
|
+
case 504:
|
|
278
|
+
return 'gateway timeout';
|
|
288
279
|
}
|
|
289
280
|
}
|
|
290
281
|
|
|
291
282
|
|
|
292
283
|
#setupStatusRenderers() {
|
|
293
|
-
const errorStatuses = {
|
|
294
|
-
Unauthorized: 401,
|
|
295
|
-
PaymentRequired: 402,
|
|
296
|
-
Forbidden: 403,
|
|
297
|
-
NotFound: 404,
|
|
298
|
-
MethodNotAllowed: 405,
|
|
299
|
-
RequestTimeout: 408,
|
|
300
|
-
Conflict: 409,
|
|
301
|
-
Gone: 410,
|
|
302
|
-
ContentTooLarge: 413,
|
|
303
|
-
UnsupportedMediaType: 415,
|
|
304
|
-
UnprocessableContent: 422,
|
|
305
|
-
Locked: 423,
|
|
306
|
-
TooManyRequests: 429,
|
|
307
|
-
}
|
|
308
|
-
|
|
309
284
|
const successStatuses = {
|
|
310
285
|
CreationSuccessful: 201,
|
|
311
286
|
Accepted: 202,
|
|
@@ -317,23 +292,26 @@ class VidaServerController {
|
|
|
317
292
|
PermanentRedirect: 308,
|
|
318
293
|
}
|
|
319
294
|
|
|
320
|
-
for (
|
|
321
|
-
|
|
322
|
-
|
|
295
|
+
for (let [type, statusCode] of Object.entries(Errors.Statuses)) {
|
|
296
|
+
if (type == 'Server') type = 'ServerError';
|
|
297
|
+
this[`render${type}`] = async function(message=null, body=null) {
|
|
298
|
+
return await this.#renderError(statusCode, message, body);
|
|
323
299
|
}
|
|
324
300
|
}
|
|
325
301
|
|
|
326
|
-
for (
|
|
327
|
-
this[`render${
|
|
328
|
-
return await this.#renderSuccess(statusCode,
|
|
302
|
+
for (let [type, statusCode] of Object.entries(successStatuses)) {
|
|
303
|
+
this[`render${type}`] = async function(body=null) {
|
|
304
|
+
return await this.#renderSuccess(statusCode, body);
|
|
329
305
|
}
|
|
330
306
|
}
|
|
331
307
|
}
|
|
332
308
|
|
|
333
309
|
|
|
334
|
-
async #renderError(statusCode, message) {
|
|
310
|
+
async #renderError(statusCode, message, body) {
|
|
335
311
|
this.statusCode = statusCode;
|
|
336
|
-
|
|
312
|
+
body = structuredClone(body || {});
|
|
313
|
+
body.errors = body.errors || {};
|
|
314
|
+
body.errors.message = message || null;
|
|
337
315
|
return await this.render(body);
|
|
338
316
|
}
|
|
339
317
|
|
|
@@ -470,7 +448,7 @@ class VidaServerController {
|
|
|
470
448
|
if (parameterErrors.length) errors[parameterName] = parameterErrors;
|
|
471
449
|
}
|
|
472
450
|
|
|
473
|
-
if (Object.keys(errors).length) throw new ValidationError('', errors);
|
|
451
|
+
if (Object.keys(errors).length) throw new Errors.ValidationError('', errors);
|
|
474
452
|
}
|
|
475
453
|
|
|
476
454
|
|
|
@@ -677,14 +655,10 @@ class VidaServerController {
|
|
|
677
655
|
}
|
|
678
656
|
|
|
679
657
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
NotFound: NotFoundError,
|
|
685
|
-
Server: ServerError,
|
|
686
|
-
Validation: ValidationError
|
|
687
|
-
}
|
|
658
|
+
const errorClasses = Object.values(Errors).filter(cls => cls == Errors.ServerError || cls.prototype instanceof Errors.AbstractServerError);
|
|
659
|
+
VidaServerController.prototype.Errors = Object.fromEntries(errorClasses.map(cls => {
|
|
660
|
+
return [cls.name.replace('Error', ''), cls]
|
|
661
|
+
}));
|
|
688
662
|
|
|
689
663
|
|
|
690
664
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const Errors = require('../../lib/server/errors');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const Statuses = [
|
|
5
|
+
['BadRequest', 400],
|
|
6
|
+
['Unauthorized', 401],
|
|
7
|
+
['PaymentRequired', 402],
|
|
8
|
+
['Forbidden', 403],
|
|
9
|
+
['NotFound', 404],
|
|
10
|
+
['MethodNotAllowed', 405],
|
|
11
|
+
['RequestTimeout', 408],
|
|
12
|
+
['Conflict', 409],
|
|
13
|
+
['Gone', 410],
|
|
14
|
+
['ContentTooLarge', 413],
|
|
15
|
+
['UnsupportedMediaType', 415],
|
|
16
|
+
['UnprocessableContent', 422],
|
|
17
|
+
['Locked', 423],
|
|
18
|
+
['TooManyRequests', 429],
|
|
19
|
+
['Server', 500],
|
|
20
|
+
['NotImplemented', 501],
|
|
21
|
+
['BadGateway', 502],
|
|
22
|
+
['ServiceUnavailable', 503],
|
|
23
|
+
['GatewayTimeout', 504],
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
describe.each(Statuses)('describe %s', (type, statusCode) => {
|
|
28
|
+
const cls = Errors.errorForStatusCode(statusCode);
|
|
29
|
+
|
|
30
|
+
describe('statusCode', () => {
|
|
31
|
+
it (`equals ${statusCode}`, () => {
|
|
32
|
+
expect(cls.statusCode).toEqual(statusCode);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
describe('type', () => {
|
|
38
|
+
const typeStr = type == 'Server' ? 'ServerError' : type;
|
|
39
|
+
it (`equals ${typeStr}`, () => {
|
|
40
|
+
expect(new cls().type).toEqual(typeStr);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
describe('errorForStatusCode', () => {
|
|
48
|
+
it.each(Statuses)('returns a %sError for %s', (type, statusCode) => {
|
|
49
|
+
const clsName = `${type}Error`;
|
|
50
|
+
const cls = Errors[clsName];
|
|
51
|
+
expect(Errors.errorForStatusCode(statusCode)).toEqual(cls);
|
|
52
|
+
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -464,17 +464,39 @@ describe('VidaServerController', () => {
|
|
|
464
464
|
|
|
465
465
|
describe('#performRequest', () => {
|
|
466
466
|
const errorHandlers = [
|
|
467
|
-
['renderUnauthorized',
|
|
468
|
-
['
|
|
469
|
-
['
|
|
470
|
-
['
|
|
471
|
-
['
|
|
472
|
-
|
|
467
|
+
['renderUnauthorized', 'AuthorizationError'],
|
|
468
|
+
['renderErrors', 'ValidationError'],
|
|
469
|
+
['renderBadRequest', 'BadRequestError'],
|
|
470
|
+
['renderUnauthorized', 'UnauthorizedError'],
|
|
471
|
+
['renderPaymentRequired', 'PaymentRequiredError'],
|
|
472
|
+
['renderForbidden', 'ForbiddenError'],
|
|
473
|
+
['renderNotFound', 'NotFoundError'],
|
|
474
|
+
['renderMethodNotAllowed', 'MethodNotAllowedError'],
|
|
475
|
+
['renderRequestTimeout', 'RequestTimeoutError'],
|
|
476
|
+
['renderConflict', 'ConflictError'],
|
|
477
|
+
['renderGone', 'GoneError'],
|
|
478
|
+
['renderContentTooLarge', 'ContentTooLargeError'],
|
|
479
|
+
['renderUnsupportedMediaType', 'UnsupportedMediaTypeError'],
|
|
480
|
+
['renderUnprocessableContent', 'UnprocessableContentError'],
|
|
481
|
+
['renderLocked', 'LockedError'],
|
|
482
|
+
['renderTooManyRequests', 'TooManyRequestsError'],
|
|
483
|
+
['renderServerError', 'ServerError'],
|
|
484
|
+
['renderNotImplemented', 'NotImplementedError'],
|
|
485
|
+
['renderBadGateway', 'BadGatewayError'],
|
|
486
|
+
['renderServiceUnavailable', 'ServiceUnavailableError'],
|
|
487
|
+
['renderGatewayTimeout', 'GatewayTimeoutError'],
|
|
488
|
+
];
|
|
473
489
|
|
|
474
490
|
it.each(errorHandlers)('calls %s when %s is thrown', async (handlerName, errorClsName) => {
|
|
475
491
|
const controller = new FooController({}, {});
|
|
492
|
+
const key = TestHelpers.Faker.Text.randomString();
|
|
493
|
+
const value = TestHelpers.Faker.Text.randomString();
|
|
494
|
+
const body = {[key]: value};
|
|
476
495
|
const message = TestHelpers.Faker.Text.randomString();
|
|
477
|
-
|
|
496
|
+
|
|
497
|
+
if (!Errors[errorClsName]) console.log(errorClsName);
|
|
498
|
+
|
|
499
|
+
const error = new Errors[errorClsName](message, body);
|
|
478
500
|
if (handlerName == 'renderErrors') error.errors = randomPayload();
|
|
479
501
|
|
|
480
502
|
controller[handlerName] = jest.fn();
|
|
@@ -485,7 +507,7 @@ describe('VidaServerController', () => {
|
|
|
485
507
|
if (handlerName == 'renderErrors') {
|
|
486
508
|
expect(controller[handlerName]).toHaveBeenCalledWith(message, error.fields);
|
|
487
509
|
} else {
|
|
488
|
-
expect(controller[handlerName]).toHaveBeenCalledWith(message);
|
|
510
|
+
expect(controller[handlerName]).toHaveBeenCalledWith(message, body);
|
|
489
511
|
}
|
|
490
512
|
});
|
|
491
513
|
|
|
@@ -611,6 +633,11 @@ describe('VidaServerController', () => {
|
|
|
611
633
|
['renderUnprocessableContent', 422],
|
|
612
634
|
['renderLocked', 423],
|
|
613
635
|
['renderTooManyRequests', 429],
|
|
636
|
+
['renderServerError', 500],
|
|
637
|
+
['renderNotImplemented', 501],
|
|
638
|
+
['renderBadGateway', 502],
|
|
639
|
+
['renderServiceUnavailable', 503],
|
|
640
|
+
['renderGatewayTimeout', 504],
|
|
614
641
|
]
|
|
615
642
|
|
|
616
643
|
describe.each(errorHandlers)('#%s', (handlerName, statusCode) => {
|
|
@@ -631,6 +658,17 @@ describe('VidaServerController', () => {
|
|
|
631
658
|
expect(controller.render).toHaveBeenCalledTimes(1);
|
|
632
659
|
expect(controller.render).toHaveBeenCalledWith({errors: {message: msg}});
|
|
633
660
|
});
|
|
661
|
+
|
|
662
|
+
it ('includes a provided body and message', async () => {
|
|
663
|
+
const key = TestHelpers.Faker.Text.randomString();
|
|
664
|
+
const val = TestHelpers.Faker.Text.randomString();
|
|
665
|
+
const body = {[key]: val};
|
|
666
|
+
const msg = TestHelpers.Faker.Text.randomString();
|
|
667
|
+
const expected = {...body, errors: {message: msg}};
|
|
668
|
+
await controller[handlerName](msg, body);
|
|
669
|
+
expect(controller.render).toHaveBeenCalledTimes(1);
|
|
670
|
+
expect(controller.render).toHaveBeenCalledWith(expected);
|
|
671
|
+
});
|
|
634
672
|
});
|
|
635
673
|
});
|
|
636
674
|
|