@vida-global/core 1.4.0 → 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 -10
- package/lib/server/index.js +3 -2
- package/lib/server/serverController.js +39 -60
- package/package.json +1 -1
- package/test/server/errors.test.js +54 -0
- package/test/server/serverController.test.js +46 -7
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,15 +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
|
-
|
|
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 {
|
|
13
56
|
#fields;
|
|
14
57
|
constructor(message, fields) {
|
|
15
58
|
super(message);
|
|
@@ -19,10 +62,22 @@ class ValidationError extends ServerError {
|
|
|
19
62
|
}
|
|
20
63
|
|
|
21
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
|
+
|
|
22
76
|
module.exports = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
77
|
+
...ErrorClasses,
|
|
78
|
+
AuthorizationError: ErrorClasses.UnauthorizedError, // Deprecated
|
|
79
|
+
errorForStatusCode,
|
|
80
|
+
AbstractServerError,
|
|
81
|
+
Statuses,
|
|
27
82
|
ValidationError
|
|
28
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,10 +1,6 @@
|
|
|
1
1
|
const { logger } = require('../logger');
|
|
2
2
|
const { camelize, singularize } = require('inflection');
|
|
3
|
-
const
|
|
4
|
-
ForbiddenError,
|
|
5
|
-
NotFoundError,
|
|
6
|
-
ServerError,
|
|
7
|
-
ValidationError } = require('./errors');
|
|
3
|
+
const Errors = require('./errors');
|
|
8
4
|
|
|
9
5
|
|
|
10
6
|
const AUTH_CALLBACK_NAME = 'authenticateRequest';
|
|
@@ -121,22 +117,22 @@ class VidaServerController {
|
|
|
121
117
|
|
|
122
118
|
|
|
123
119
|
async #handleError(err) {
|
|
124
|
-
if (err instanceof AuthorizationError) {
|
|
125
|
-
await this.renderUnauthorized(err.message);
|
|
120
|
+
if (err instanceof Errors.AuthorizationError) {
|
|
121
|
+
await this.renderUnauthorized(err.message, err.body);
|
|
126
122
|
|
|
127
|
-
} else if (err instanceof
|
|
128
|
-
await this.renderForbidden(err.message);
|
|
129
|
-
|
|
130
|
-
} else if (err instanceof NotFoundError) {
|
|
131
|
-
await this.renderNotFound(err.message);
|
|
132
|
-
|
|
133
|
-
} else if (err instanceof ValidationError) {
|
|
123
|
+
} else if (err instanceof Errors.ValidationError) {
|
|
134
124
|
await this.renderErrors(err.message, err.fields);
|
|
135
125
|
|
|
126
|
+
} else if (err instanceof Errors.AbstractServerError) {
|
|
127
|
+
const renderer = `render${err.type}`;
|
|
128
|
+
await this[renderer](err.message, err.body);
|
|
129
|
+
|
|
136
130
|
} else {
|
|
137
131
|
this._response.error = err;
|
|
138
|
-
let message;
|
|
139
|
-
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') {
|
|
140
136
|
message = err;
|
|
141
137
|
} else {
|
|
142
138
|
message = err.message;
|
|
@@ -166,8 +162,6 @@ class VidaServerController {
|
|
|
166
162
|
|
|
167
163
|
|
|
168
164
|
async renderErrors(message, fields) {
|
|
169
|
-
this.statusCode = 400;
|
|
170
|
-
|
|
171
165
|
const errors = {message: null, fields: {}};
|
|
172
166
|
|
|
173
167
|
if (message && !fields && typeof message == 'object') {
|
|
@@ -175,7 +169,6 @@ class VidaServerController {
|
|
|
175
169
|
message = null;
|
|
176
170
|
}
|
|
177
171
|
|
|
178
|
-
if (message) errors.message = message;
|
|
179
172
|
if (fields) {
|
|
180
173
|
for (const [field, values] of Object.entries(fields)) {
|
|
181
174
|
if (!Array.isArray(values)) fields[field] = [values];
|
|
@@ -183,15 +176,7 @@ class VidaServerController {
|
|
|
183
176
|
errors.fields = fields;
|
|
184
177
|
}
|
|
185
178
|
|
|
186
|
-
await this.
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
async renderServerError(message=null) {
|
|
191
|
-
if (process.env.NODE_ENV != 'development') {
|
|
192
|
-
message = null;
|
|
193
|
-
}
|
|
194
|
-
return await this.#renderError(500, message);
|
|
179
|
+
await this.renderBadRequest(message, { errors });
|
|
195
180
|
}
|
|
196
181
|
|
|
197
182
|
|
|
@@ -209,6 +194,8 @@ class VidaServerController {
|
|
|
209
194
|
if (body === undefined) return null;
|
|
210
195
|
if (!body || typeof body != 'object') return body;
|
|
211
196
|
|
|
197
|
+
body = structuredClone(body);
|
|
198
|
+
|
|
212
199
|
if (body.toApiResponse) {
|
|
213
200
|
const formattedBody = await body.toApiResponse(options);
|
|
214
201
|
return await this.processJSONBody(formattedBody, options);
|
|
@@ -281,27 +268,19 @@ class VidaServerController {
|
|
|
281
268
|
return 'too many requests';
|
|
282
269
|
case 500:
|
|
283
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';
|
|
284
279
|
}
|
|
285
280
|
}
|
|
286
281
|
|
|
287
282
|
|
|
288
283
|
#setupStatusRenderers() {
|
|
289
|
-
const errorStatuses = {
|
|
290
|
-
Unauthorized: 401,
|
|
291
|
-
PaymentRequired: 402,
|
|
292
|
-
Forbidden: 403,
|
|
293
|
-
NotFound: 404,
|
|
294
|
-
MethodNotAllowed: 405,
|
|
295
|
-
RequestTimeout: 408,
|
|
296
|
-
Conflict: 409,
|
|
297
|
-
Gone: 410,
|
|
298
|
-
ContentTooLarge: 413,
|
|
299
|
-
UnsupportedMediaType: 415,
|
|
300
|
-
UnprocessableContent: 422,
|
|
301
|
-
Locked: 423,
|
|
302
|
-
TooManyRequests: 429,
|
|
303
|
-
}
|
|
304
|
-
|
|
305
284
|
const successStatuses = {
|
|
306
285
|
CreationSuccessful: 201,
|
|
307
286
|
Accepted: 202,
|
|
@@ -313,23 +292,26 @@ class VidaServerController {
|
|
|
313
292
|
PermanentRedirect: 308,
|
|
314
293
|
}
|
|
315
294
|
|
|
316
|
-
for (
|
|
317
|
-
|
|
318
|
-
|
|
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);
|
|
319
299
|
}
|
|
320
300
|
}
|
|
321
301
|
|
|
322
|
-
for (
|
|
323
|
-
this[`render${
|
|
324
|
-
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);
|
|
325
305
|
}
|
|
326
306
|
}
|
|
327
307
|
}
|
|
328
308
|
|
|
329
309
|
|
|
330
|
-
async #renderError(statusCode, message) {
|
|
310
|
+
async #renderError(statusCode, message, body) {
|
|
331
311
|
this.statusCode = statusCode;
|
|
332
|
-
|
|
312
|
+
body = structuredClone(body || {});
|
|
313
|
+
body.errors = body.errors || {};
|
|
314
|
+
body.errors.message = message || null;
|
|
333
315
|
return await this.render(body);
|
|
334
316
|
}
|
|
335
317
|
|
|
@@ -466,7 +448,7 @@ class VidaServerController {
|
|
|
466
448
|
if (parameterErrors.length) errors[parameterName] = parameterErrors;
|
|
467
449
|
}
|
|
468
450
|
|
|
469
|
-
if (Object.keys(errors).length) throw new ValidationError('', errors);
|
|
451
|
+
if (Object.keys(errors).length) throw new Errors.ValidationError('', errors);
|
|
470
452
|
}
|
|
471
453
|
|
|
472
454
|
|
|
@@ -673,13 +655,10 @@ class VidaServerController {
|
|
|
673
655
|
}
|
|
674
656
|
|
|
675
657
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
Server: ServerError,
|
|
681
|
-
Validation: ValidationError
|
|
682
|
-
}
|
|
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
|
+
}));
|
|
683
662
|
|
|
684
663
|
|
|
685
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,16 +464,39 @@ describe('VidaServerController', () => {
|
|
|
464
464
|
|
|
465
465
|
describe('#performRequest', () => {
|
|
466
466
|
const errorHandlers = [
|
|
467
|
-
['renderUnauthorized',
|
|
468
|
-
['
|
|
469
|
-
['
|
|
470
|
-
['
|
|
471
|
-
|
|
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
|
+
];
|
|
472
489
|
|
|
473
490
|
it.each(errorHandlers)('calls %s when %s is thrown', async (handlerName, errorClsName) => {
|
|
474
491
|
const controller = new FooController({}, {});
|
|
492
|
+
const key = TestHelpers.Faker.Text.randomString();
|
|
493
|
+
const value = TestHelpers.Faker.Text.randomString();
|
|
494
|
+
const body = {[key]: value};
|
|
475
495
|
const message = TestHelpers.Faker.Text.randomString();
|
|
476
|
-
|
|
496
|
+
|
|
497
|
+
if (!Errors[errorClsName]) console.log(errorClsName);
|
|
498
|
+
|
|
499
|
+
const error = new Errors[errorClsName](message, body);
|
|
477
500
|
if (handlerName == 'renderErrors') error.errors = randomPayload();
|
|
478
501
|
|
|
479
502
|
controller[handlerName] = jest.fn();
|
|
@@ -484,7 +507,7 @@ describe('VidaServerController', () => {
|
|
|
484
507
|
if (handlerName == 'renderErrors') {
|
|
485
508
|
expect(controller[handlerName]).toHaveBeenCalledWith(message, error.fields);
|
|
486
509
|
} else {
|
|
487
|
-
expect(controller[handlerName]).toHaveBeenCalledWith(message);
|
|
510
|
+
expect(controller[handlerName]).toHaveBeenCalledWith(message, body);
|
|
488
511
|
}
|
|
489
512
|
});
|
|
490
513
|
|
|
@@ -610,6 +633,11 @@ describe('VidaServerController', () => {
|
|
|
610
633
|
['renderUnprocessableContent', 422],
|
|
611
634
|
['renderLocked', 423],
|
|
612
635
|
['renderTooManyRequests', 429],
|
|
636
|
+
['renderServerError', 500],
|
|
637
|
+
['renderNotImplemented', 501],
|
|
638
|
+
['renderBadGateway', 502],
|
|
639
|
+
['renderServiceUnavailable', 503],
|
|
640
|
+
['renderGatewayTimeout', 504],
|
|
613
641
|
]
|
|
614
642
|
|
|
615
643
|
describe.each(errorHandlers)('#%s', (handlerName, statusCode) => {
|
|
@@ -630,6 +658,17 @@ describe('VidaServerController', () => {
|
|
|
630
658
|
expect(controller.render).toHaveBeenCalledTimes(1);
|
|
631
659
|
expect(controller.render).toHaveBeenCalledWith({errors: {message: msg}});
|
|
632
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
|
+
});
|
|
633
672
|
});
|
|
634
673
|
});
|
|
635
674
|
|