@vida-global/core 1.4.1 → 1.4.3

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,22 +1,19 @@
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';
12
7
 
13
8
 
14
9
  class VidaServerController {
15
- #afterCallbacks = [];
16
- #beforeCallbacks = [];
17
- #callbacksSetUp = false;
10
+ #afterCallbacks = [];
11
+ #afterCallbacksToSkip = [];
12
+ #beforeCallbacks = [];
13
+ #beforeCallbacksToSkip = [];
14
+ #callbacksSetUp = false;
18
15
  #params;
19
- #rendered = false;
16
+ #rendered = false;
20
17
  #request;
21
18
  #response;
22
19
 
@@ -122,25 +119,22 @@ class VidaServerController {
122
119
 
123
120
 
124
121
  async #handleError(err) {
125
- if (err instanceof AuthorizationError) {
126
- await this.renderUnauthorized(err.message);
122
+ if (err instanceof Errors.AuthorizationError) {
123
+ await this.renderUnauthorized(err.message, err.body);
127
124
 
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) {
125
+ } else if (err instanceof Errors.ValidationError) {
138
126
  await this.renderErrors(err.message, err.fields);
139
127
 
128
+ } else if (err instanceof Errors.AbstractServerError) {
129
+ const renderer = `render${err.type}`;
130
+ await this[renderer](err.message, err.body);
131
+
140
132
  } else {
141
133
  this._response.error = err;
142
- let message;
143
- if (typeof err == 'string') {
134
+ let message = null;
135
+ if (process.env.NODE_ENV != 'development') {
136
+ // don't show error details outside of development
137
+ } else if (typeof err == 'string') {
144
138
  message = err;
145
139
  } else {
146
140
  message = err.message;
@@ -170,8 +164,6 @@ class VidaServerController {
170
164
 
171
165
 
172
166
  async renderErrors(message, fields) {
173
- this.statusCode = 400;
174
-
175
167
  const errors = {message: null, fields: {}};
176
168
 
177
169
  if (message && !fields && typeof message == 'object') {
@@ -179,7 +171,6 @@ class VidaServerController {
179
171
  message = null;
180
172
  }
181
173
 
182
- if (message) errors.message = message;
183
174
  if (fields) {
184
175
  for (const [field, values] of Object.entries(fields)) {
185
176
  if (!Array.isArray(values)) fields[field] = [values];
@@ -187,15 +178,7 @@ class VidaServerController {
187
178
  errors.fields = fields;
188
179
  }
189
180
 
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);
181
+ await this.renderBadRequest(message, { errors });
199
182
  }
200
183
 
201
184
 
@@ -213,6 +196,8 @@ class VidaServerController {
213
196
  if (body === undefined) return null;
214
197
  if (!body || typeof body != 'object') return body;
215
198
 
199
+ body = structuredClone(body);
200
+
216
201
  if (body.toApiResponse) {
217
202
  const formattedBody = await body.toApiResponse(options);
218
203
  return await this.processJSONBody(formattedBody, options);
@@ -285,27 +270,19 @@ class VidaServerController {
285
270
  return 'too many requests';
286
271
  case 500:
287
272
  return 'server error';
273
+ case 501:
274
+ return 'not implemented';
275
+ case 502:
276
+ return 'bad gateway';
277
+ case 503:
278
+ return 'service unavailable';
279
+ case 504:
280
+ return 'gateway timeout';
288
281
  }
289
282
  }
290
283
 
291
284
 
292
285
  #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
286
  const successStatuses = {
310
287
  CreationSuccessful: 201,
311
288
  Accepted: 202,
@@ -317,23 +294,26 @@ class VidaServerController {
317
294
  PermanentRedirect: 308,
318
295
  }
319
296
 
320
- for (const [method, statusCode] of Object.entries(errorStatuses)) {
321
- this[`render${method}`] = async function(message=null) {
322
- return await this.#renderError(statusCode, message);
297
+ for (let [type, statusCode] of Object.entries(Errors.Statuses)) {
298
+ if (type == 'Server') type = 'ServerError';
299
+ this[`render${type}`] = async function(message=null, body=null) {
300
+ return await this.#renderError(statusCode, message, body);
323
301
  }
324
302
  }
325
303
 
326
- for (const [method, statusCode] of Object.entries(successStatuses)) {
327
- this[`render${method}`] = async function(message=null) {
328
- return await this.#renderSuccess(statusCode, message);
304
+ for (let [type, statusCode] of Object.entries(successStatuses)) {
305
+ this[`render${type}`] = async function(body=null) {
306
+ return await this.#renderSuccess(statusCode, body);
329
307
  }
330
308
  }
331
309
  }
332
310
 
333
311
 
334
- async #renderError(statusCode, message) {
312
+ async #renderError(statusCode, message, body) {
335
313
  this.statusCode = statusCode;
336
- const body = {errors: {message: message || null}};
314
+ body = structuredClone(body || {});
315
+ body.errors = body.errors || {};
316
+ body.errors.message = message || null;
337
317
  return await this.render(body);
338
318
  }
339
319
 
@@ -413,24 +393,40 @@ class VidaServerController {
413
393
  }
414
394
 
415
395
 
396
+ skipBeforeCallback(callback, options={}) {
397
+ this.#addCallback(callback, options, this.#beforeCallbacksToSkip);
398
+ }
399
+
400
+
401
+ skipAfterCallback(callback, options={}) {
402
+ this.#addCallback(callback, options, this.#afterCallbacksToSkip);
403
+ }
404
+
405
+
416
406
  #addCallback(callback, options, callbacksList) {
417
407
  callbacksList.push({ callback, options });
418
408
  }
419
409
 
420
410
 
421
411
  async runBeforeCallbacks(actionName) {
422
- return await this.#runCallbacks(this.#beforeCallbacks, actionName);
412
+ return await this.#runCallbacks(this.#beforeCallbacks, this.#beforeCallbacksToSkip, actionName);
423
413
  }
424
414
 
425
415
 
426
416
  async runAfterCallbacks(actionName) {
427
- return await this.#runCallbacks(this.#afterCallbacks, actionName);
417
+ return await this.#runCallbacks(this.#afterCallbacks, this.#afterCallbacksToSkip, actionName);
428
418
  }
429
419
 
430
420
 
431
- async #runCallbacks(callbacksList, actionName) {
421
+ async #runCallbacks(callbacksList, callbacksToSkipList, actionName) {
422
+ let toSkip = callbacksToSkipList.filter(({ callback, options }) => {
423
+ return this.#shouldRunCallback(actionName, options);
424
+ });
425
+ toSkip = toSkip.map(({ callback }) => callback);
426
+
432
427
  for (const { callback, options } of callbacksList) {
433
428
  if (!this.#shouldRunCallback(actionName, options)) continue;
429
+ if (toSkip.includes(callback)) continue;
434
430
  const result = await this.#runCallback(callback)
435
431
  if (result === false) return false;
436
432
  }
@@ -470,7 +466,7 @@ class VidaServerController {
470
466
  if (parameterErrors.length) errors[parameterName] = parameterErrors;
471
467
  }
472
468
 
473
- if (Object.keys(errors).length) throw new ValidationError('', errors);
469
+ if (Object.keys(errors).length) throw new Errors.ValidationError('', errors);
474
470
  }
475
471
 
476
472
 
@@ -677,14 +673,10 @@ class VidaServerController {
677
673
  }
678
674
 
679
675
 
680
- VidaServerController.prototype.Errors = {
681
- Authorization: AuthorizationError,
682
- Conflict: ConflictError,
683
- Forbidden: ForbiddenError,
684
- NotFound: NotFoundError,
685
- Server: ServerError,
686
- Validation: ValidationError
687
- }
676
+ const errorClasses = Object.values(Errors).filter(cls => cls == Errors.ServerError || cls.prototype instanceof Errors.AbstractServerError);
677
+ VidaServerController.prototype.Errors = Object.fromEntries(errorClasses.map(cls => {
678
+ return [cls.name.replace('Error', ''), cls]
679
+ }));
688
680
 
689
681
 
690
682
  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.3",
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