@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 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,15 +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 ForbiddenError extends ServerError {}
11
- class NotFoundError extends ServerError {}
12
- 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 {
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
- AuthorizationError,
24
- ForbiddenError,
25
- NotFoundError,
26
- ServerError,
77
+ ...ErrorClasses,
78
+ AuthorizationError: ErrorClasses.UnauthorizedError, // Deprecated
79
+ errorForStatusCode,
80
+ AbstractServerError,
81
+ Statuses,
27
82
  ValidationError
28
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,10 +1,6 @@
1
1
  const { logger } = require('../logger');
2
2
  const { camelize, singularize } = require('inflection');
3
- const{ AuthorizationError,
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 ForbiddenError) {
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 (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') {
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.render({ errors });
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 (const [method, statusCode] of Object.entries(errorStatuses)) {
317
- this[`render${method}`] = async function(message=null) {
318
- 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);
319
299
  }
320
300
  }
321
301
 
322
- for (const [method, statusCode] of Object.entries(successStatuses)) {
323
- this[`render${method}`] = async function(message=null) {
324
- 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);
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
- const body = {errors: {message: message || null}};
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
- VidaServerController.prototype.Errors = {
677
- Authorization: AuthorizationError,
678
- Forbidden: ForbiddenError,
679
- NotFound: NotFoundError,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vida-global/core",
3
- "version": "1.4.0",
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,16 +464,39 @@ describe('VidaServerController', () => {
464
464
 
465
465
  describe('#performRequest', () => {
466
466
  const errorHandlers = [
467
- ['renderUnauthorized', 'AuthorizationError'],
468
- ['renderForbidden', 'ForbiddenError'],
469
- ['renderNotFound', 'NotFoundError'],
470
- ['renderErrors', 'ValidationError'],
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
- const error = new Errors[errorClsName](message);
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