@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 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
- ...serverLibs
22
+ ...serverErrorsToExport,
23
+ ApiDocsGenerator,
24
+ VidaServer,
25
+ VidaServerController,
14
26
  };
@@ -1,16 +1,58 @@
1
- class ServerError extends Error {
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
- constructor(message) {
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
- class AuthorizationError extends ServerError {}
10
- class ConflictError extends ServerError {}
11
- class ForbiddenError extends ServerError {}
12
- class NotFoundError extends ServerError {}
13
- class ValidationError extends ServerError {
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
- AuthorizationError,
25
- ConflictError,
26
- ForbiddenError,
27
- NotFoundError,
28
- ServerError,
77
+ ...ErrorClasses,
78
+ AuthorizationError: ErrorClasses.UnauthorizedError, // Deprecated
79
+ errorForStatusCode,
80
+ AbstractServerError,
81
+ Statuses,
29
82
  ValidationError
30
83
  }
@@ -1,11 +1,12 @@
1
1
  const { VidaServer } = require('./server');
2
2
  const { VidaServerController } = require('./serverController');
3
- const ERRORS = require('./errors');
3
+ const Errors = require('./errors');
4
4
  const { ApiDocsGenerator } = require('./apiDocsGenerator');
5
5
 
6
+
6
7
  module.exports = {
7
8
  ApiDocsGenerator,
8
- ...ERRORS,
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{ AuthorizationError,
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 ConflictError) {
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 (typeof err == 'string') {
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.render({ errors });
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 (const [method, statusCode] of Object.entries(errorStatuses)) {
321
- this[`render${method}`] = async function(message=null) {
322
- return await this.#renderError(statusCode, message);
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 (const [method, statusCode] of Object.entries(successStatuses)) {
327
- this[`render${method}`] = async function(message=null) {
328
- return await this.#renderSuccess(statusCode, message);
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
- const body = {errors: {message: message || null}};
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
- VidaServerController.prototype.Errors = {
681
- Authorization: AuthorizationError,
682
- Conflict: ConflictError,
683
- Forbidden: ForbiddenError,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vida-global/core",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "Core libraries for supporting Vida development",
5
5
  "author": "",
6
6
  "license": "ISC",
@@ -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', 'AuthorizationError'],
468
- ['renderConflict', 'ConflictError'],
469
- ['renderForbidden', 'ForbiddenError'],
470
- ['renderNotFound', 'NotFoundError'],
471
- ['renderErrors', 'ValidationError'],
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
- const error = new Errors[errorClsName](message);
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