@vida-global/core 1.3.3 → 1.4.1

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.
@@ -1,7 +1,10 @@
1
1
  const pino = require('pino');
2
2
 
3
3
  const config = {
4
- level: process.env.LOG_LEVEL || 'info'
4
+ level: process.env.LOG_LEVEL || 'info',
5
+ customLevels: {
6
+ test: 1000
7
+ }
5
8
  };
6
9
 
7
10
  if (!process.env.ENV_VERCEL) {
@@ -118,9 +118,34 @@ Additional options can be passed to `toApiResponse` by explicitly calling `rende
118
118
  await this.render(response, {includeTitle: true});
119
119
  ```
120
120
 
121
+ ### Other Status Codes
122
+ There is no need to set `statusCode` directly as there are helper methods for all statuses.
123
+ ```
124
+ 201 - this.renderCreationSuccessful
125
+ 202 - this.renderAccepted
126
+ 204 - this.renderNoConent
127
+ 301 - this.renderMovedPermanently
128
+ 302 - this.renderFound
129
+ 304 - this.renderNotModified
130
+ 307 - this.renderTemporaryRedirect
131
+ 308 - this.renderPermanentRedirect
132
+ 401 - this.renderUnauthorizedResponse
133
+ 402 - this.renderPaymentRequired
134
+ 403 - this.renderForbiddenResponse
135
+ 404 - this.renderNotFoundResponse
136
+ 405 - this.renderMethodNotAllowed
137
+ 408 - this.renderRequestTimeout
138
+ 409 - this.renderConflictResponse
139
+ 410 - this.renderGone
140
+ 413 - this.renderContentTooLarge
141
+ 415 - this.renderUnsupportedMediaType
142
+ 422 - this.renderUnprocessableContent
143
+ 423 - this.renderLocked
144
+ 429 - this.renderTooManyRequests
145
+ ```
146
+
121
147
 
122
148
  ## Error handling
123
- There is no need to set `statusCode` directly as there are helper methods for all statuses.
124
149
  HTTP 400
125
150
  ```
126
151
  await this.renderErrors("Something bad happened", {password: ['must be > 8 characters', 'must include numbers']})
@@ -134,19 +159,16 @@ await this.renderErrors({password: ['must be > 8 characters', 'must include numb
134
159
  ```
135
160
  HTTP 401
136
161
  ```
137
- await this.renderUnauthorizedResponse("Nope")
162
+ await this.renderUnauthorized("Nope")
138
163
  // renders {data: {message: "Nope"}, status: "unauthorized"}
139
164
  ```
140
165
  HTTP 403
141
166
  ```
142
- await this.renderForbiddenResponse("You shall not pass")
167
+ await this.renderForbidden("You shall not pass")
143
168
  // renders {data: {message: "You shall not pass"}, status: "forbidden"}
144
169
  ```
145
- HTTP 404
146
- ```
147
- await this.renderNotFoundResponse("Whoops")
148
- // renders {data: {message: "Whoops"}, status: "not found"}
149
- ```
170
+ * _Other statuses detailed above._
171
+
150
172
  Throwing any of `this.Errors.Authorization(msg)`, `this.Errors.Forbidden(msg)`, `this.Errors.NotFound(msg)`, `this.Errors.Validation(msg, errors)` from anywhere in the code will trigger the corresponding error handler. All other errors will generate a 500 error.
151
173
 
152
174
 
@@ -7,6 +7,7 @@ class ServerError extends Error {
7
7
  get message() { return this.#message; };
8
8
  }
9
9
  class AuthorizationError extends ServerError {}
10
+ class ConflictError extends ServerError {}
10
11
  class ForbiddenError extends ServerError {}
11
12
  class NotFoundError extends ServerError {}
12
13
  class ValidationError extends ServerError {
@@ -21,6 +22,7 @@ class ValidationError extends ServerError {
21
22
 
22
23
  module.exports = {
23
24
  AuthorizationError,
25
+ ConflictError,
24
26
  ForbiddenError,
25
27
  NotFoundError,
26
28
  ServerError,
@@ -1,6 +1,7 @@
1
1
  const { logger } = require('../logger');
2
2
  const { camelize, singularize } = require('inflection');
3
3
  const{ AuthorizationError,
4
+ ConflictError,
4
5
  ForbiddenError,
5
6
  NotFoundError,
6
7
  ServerError,
@@ -28,6 +29,7 @@ class VidaServerController {
28
29
  this.#response = response;
29
30
 
30
31
  this.#applyCallbacks();
32
+ this.#setupStatusRenderers();
31
33
  }
32
34
 
33
35
 
@@ -121,13 +123,16 @@ class VidaServerController {
121
123
 
122
124
  async #handleError(err) {
123
125
  if (err instanceof AuthorizationError) {
124
- await this.renderUnauthorizedResponse(err.message);
126
+ await this.renderUnauthorized(err.message);
127
+
128
+ } else if (err instanceof ConflictError) {
129
+ await this.renderConflict(err.message);
125
130
 
126
131
  } else if (err instanceof ForbiddenError) {
127
- await this.renderForbiddenResponse(err.message);
132
+ await this.renderForbidden(err.message);
128
133
 
129
134
  } else if (err instanceof NotFoundError) {
130
- await this.renderNotFoundResponse(err.message);
135
+ await this.renderNotFound(err.message);
131
136
 
132
137
  } else if (err instanceof ValidationError) {
133
138
  await this.renderErrors(err.message, err.fields);
@@ -140,7 +145,7 @@ class VidaServerController {
140
145
  } else {
141
146
  message = err.message;
142
147
  }
143
- await this.renderServerErrorResponse(message);
148
+ await this.renderServerError(message);
144
149
  }
145
150
  }
146
151
 
@@ -186,33 +191,14 @@ class VidaServerController {
186
191
  }
187
192
 
188
193
 
189
- async renderUnauthorizedResponse(message=null) {
190
- return await this.#renderError(401, message);
191
- }
192
-
193
-
194
- async renderForbiddenResponse(message=null) {
195
- return await this.#renderError(403, message);
196
- }
197
-
198
-
199
- async renderNotFoundResponse(message=null) {
200
- return await this.#renderError(404, message);
201
- }
202
-
203
-
204
- async renderServerErrorResponse(message=null) {
194
+ async renderServerError(message=null) {
195
+ if (process.env.NODE_ENV != 'development') {
196
+ message = null;
197
+ }
205
198
  return await this.#renderError(500, message);
206
199
  }
207
200
 
208
201
 
209
- async #renderError(statusCode, message) {
210
- this.statusCode = statusCode;
211
- const body = {errors: {message: message || null}};
212
- return await this.render(body);
213
- }
214
-
215
-
216
202
  formatJSONBody(body, errors, options) {
217
203
  if (options.standardize === false) return body;
218
204
 
@@ -253,26 +239,118 @@ class VidaServerController {
253
239
  switch(this.statusCode) {
254
240
  case 200:
255
241
  return 'ok';
242
+ case 201:
243
+ return 'created';
244
+ case 202:
245
+ return 'accepted';
246
+ case 204:
247
+ return 'no content';
248
+ case 301:
249
+ return 'moved permanently';
250
+ case 302:
251
+ return 'found';
252
+ case 304:
253
+ return 'not modified';
254
+ case 307:
255
+ return 'temporary redirect';
256
+ case 308:
257
+ return 'permanent redirect';
256
258
  case 400:
257
259
  return 'bad request'
258
260
  case 401:
259
261
  return 'unauthorized';
262
+ case 402:
263
+ return 'payment required';
260
264
  case 403:
261
265
  return 'forbidden';
262
266
  case 404:
263
267
  return 'not found';
268
+ case 405:
269
+ return 'method not allowed';
270
+ case 408:
271
+ return 'request timeout';
272
+ case 409:
273
+ return 'conflict';
274
+ case 410:
275
+ return 'gone';
276
+ case 413:
277
+ return 'content too large';
278
+ case 415:
279
+ return 'unsupported media type';
280
+ case 422:
281
+ return 'unprocessable content';
282
+ case 423:
283
+ return 'locked';
284
+ case 429:
285
+ return 'too many requests';
264
286
  case 500:
265
287
  return 'server error';
266
288
  }
267
289
  }
268
290
 
269
291
 
292
+ #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
+ const successStatuses = {
310
+ CreationSuccessful: 201,
311
+ Accepted: 202,
312
+ NoConent: 204,
313
+ MovedPermanently: 301,
314
+ Found: 302,
315
+ NotModified: 304,
316
+ TemporaryRedirect: 307,
317
+ PermanentRedirect: 308,
318
+ }
319
+
320
+ for (const [method, statusCode] of Object.entries(errorStatuses)) {
321
+ this[`render${method}`] = async function(message=null) {
322
+ return await this.#renderError(statusCode, message);
323
+ }
324
+ }
325
+
326
+ for (const [method, statusCode] of Object.entries(successStatuses)) {
327
+ this[`render${method}`] = async function(message=null) {
328
+ return await this.#renderSuccess(statusCode, message);
329
+ }
330
+ }
331
+ }
332
+
333
+
334
+ async #renderError(statusCode, message) {
335
+ this.statusCode = statusCode;
336
+ const body = {errors: {message: message || null}};
337
+ return await this.render(body);
338
+ }
339
+
340
+
341
+ async #renderSuccess(statusCode, body) {
342
+ this.statusCode = statusCode;
343
+ return await this.render(body);
344
+ }
345
+
346
+
270
347
  /***********************************************************************************************
271
348
  * HELPERS
272
349
  ***********************************************************************************************/
273
350
  static autoLoadHelpers() {
274
- const { InstanceMethods, Accessors } = this._autoLoadModule(this.autoLoadHelperPath);
351
+ const { StaticMethods, InstanceMethods, Accessors } = this._autoLoadModule(this.autoLoadHelperPath);
275
352
 
353
+ if (StaticMethods) Object.assign(this, StaticMethods);
276
354
  if (InstanceMethods) Object.assign(this.prototype, InstanceMethods);
277
355
 
278
356
  if (Accessors) {
@@ -385,10 +463,10 @@ class VidaServerController {
385
463
  /***********************************************************************************************
386
464
  * VALIDATIONS
387
465
  ***********************************************************************************************/
388
- validateParameters(validations) {
466
+ async validateParameters(validations) {
389
467
  const errors = {};
390
468
  for (const [parameterName, parameterValidations] of Object.entries(validations)) {
391
- const parameterErrors = this.validateParameter(this.params[parameterName], parameterValidations);
469
+ const parameterErrors = await this.validateParameter(this.params[parameterName], parameterValidations);
392
470
  if (parameterErrors.length) errors[parameterName] = parameterErrors;
393
471
  }
394
472
 
@@ -396,7 +474,7 @@ class VidaServerController {
396
474
  }
397
475
 
398
476
 
399
- validateParameter(value, parameterValidations) {
477
+ async validateParameter(value, parameterValidations) {
400
478
  const errors = [];
401
479
 
402
480
  if (parameterValidations?.optional && value === undefined) return errors;
@@ -404,7 +482,7 @@ class VidaServerController {
404
482
  for (const [validationType, validationOptions] of Object.entries(parameterValidations)) {
405
483
  const validationMethod = `validate${camelize(validationType)}`;
406
484
  if (!this[validationMethod]) continue;
407
- const error = this[validationMethod](value, validationOptions);
485
+ const error = await this[validationMethod](value, validationOptions);
408
486
  if (error) errors.push(error);
409
487
  }
410
488
 
@@ -457,8 +535,8 @@ class VidaServerController {
457
535
  }
458
536
 
459
537
 
460
- validateFunction(value, fnc) {
461
- const error = fnc(value);
538
+ async validateFunction(value, fnc) {
539
+ const error = await fnc.call(this, value);
462
540
  if (error) return error;
463
541
  }
464
542
 
@@ -601,6 +679,7 @@ class VidaServerController {
601
679
 
602
680
  VidaServerController.prototype.Errors = {
603
681
  Authorization: AuthorizationError,
682
+ Conflict: ConflictError,
604
683
  Forbidden: ForbiddenError,
605
684
  NotFound: NotFoundError,
606
685
  Server: ServerError,
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@vida-global/core",
3
- "version": "1.3.3",
3
+ "version": "1.4.1",
4
4
  "description": "Core libraries for supporting Vida development",
5
5
  "author": "",
6
6
  "license": "ISC",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
- "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules node_modules/jest/bin/jest.js",
9
+ "test": "LOG_LEVEL=test NODE_NO_WARNINGS=1 node --experimental-vm-modules node_modules/jest/bin/jest.js",
10
10
  "release": "node node_modules/@vida-global/release/scripts/release.js",
11
11
  "develop": "node node_modules/@vida-global/release/scripts/release.js develop"
12
12
  },
@@ -155,8 +155,8 @@ describe('VidaServerController', () => {
155
155
  [paramName2]: {[validationType1]: options2}
156
156
  };
157
157
 
158
- it ('runs validation checkers based on inputs', () => {
159
- controller.validateParameters(validations);
158
+ it ('runs validation checkers based on inputs', async () => {
159
+ await controller.validateParameters(validations);
160
160
  expect(validator1).toHaveBeenCalledTimes(2);
161
161
  expect(validator1).toHaveBeenCalledWith(value1, options1);
162
162
  expect(validator1).toHaveBeenCalledWith(value2, options2);
@@ -165,52 +165,44 @@ describe('VidaServerController', () => {
165
165
  expect(validator2).toHaveBeenCalledWith(value1, options2);
166
166
  });
167
167
 
168
- it ('throws a `ValidationError` when there\'s an error', () => {
168
+ it ('throws a `ValidationError` when there\'s an error', async () => {
169
169
  validator1.mockImplementation(() => error1);
170
- expect(() => {
171
- controller.validateParameters(validations);
172
- }).toThrow(Errors.ValidationError);
170
+ await expect(controller.validateParameters(validations)).rejects.toThrow();
173
171
  });
174
172
 
175
- it ('throws a `ValidationError` when there are multiple errors', () => {
173
+ it ('throws a `ValidationError` when there are multiple errors', async () => {
176
174
  validator1.mockImplementation(() => error1);
177
175
  validator2.mockImplementation(() => error2);
178
176
 
179
- expect(() => {
180
- controller.validateParameters(validations);
181
- }).toThrow(Errors.ValidationError);
177
+ await expect(controller.validateParameters(validations)).rejects.toThrow(Errors.ValidationError);
182
178
  });
183
179
 
184
- it ('sets nothing when there is no error', () => {
185
- controller.validateParameters(validations);
180
+ it ('sets nothing when there is no error', async () => {
181
+ await controller.validateParameters(validations);
186
182
  expect(controller.render).toHaveBeenCalledTimes(0);
187
183
  expect(response.statusCode).toEqual(200);
188
184
  });
189
185
 
190
- it ('ignores details where there is no validator', () => {
186
+ it ('ignores details where there is no validator', async () => {
191
187
  const validations = {
192
188
  [paramName2]: {[validationType1]: options2, 'foo': true}
193
189
  };
194
- controller.validateParameters(validations);
190
+ await controller.validateParameters(validations);
195
191
 
196
192
  expect(validator1).toHaveBeenCalledTimes(1);
197
193
  expect(validator1).toHaveBeenCalledWith(value2, options2);
198
194
  });
199
195
 
200
- it ('does not error when optional parameters are not included', () => {
196
+ it ('does not error when optional parameters are not included', async () => {
201
197
  const validations = {foo: {isString: true, optional: true}};
202
- expect(() => {
203
- controller.validateParameters(validations);
204
- }).not.toThrow();
198
+ expect(controller.validateParameters(validations)).resolves.not.toThrow();
205
199
  });
206
200
 
207
- it ('does error when optional parameters are invalid', () => {
201
+ it ('does error when optional parameters are invalid', async () => {
208
202
  const validations = {foo: {isString: {length: {gte: 2}}, optional: true}};
209
203
  params.foo = '';
210
204
 
211
- expect(() => {
212
- controller.validateParameters(validations);
213
- }).toThrow(Errors.ValidationError);
205
+ await expect(controller.validateParameters(validations)).rejects.toThrow(Errors.ValidationError);
214
206
 
215
207
  delete params.foo;
216
208
  });
@@ -458,13 +450,13 @@ describe('VidaServerController', () => {
458
450
  const error = TestHelpers.Faker.Text.randomString();
459
451
  const validator = (value) => value == correctValue ? undefined : error;
460
452
 
461
- it ('returns undefiend when passed a valid value', () => {
462
- const result = controller.validateFunction(correctValue, validator);
453
+ it ('returns undefiend when passed a valid value', async () => {
454
+ const result = await controller.validateFunction(correctValue, validator);
463
455
  expect(result).toBe(undefined);
464
456
  });
465
457
 
466
- it ('returns an error when the function returns an error', () => {
467
- const result = controller.validateFunction(Math.random(), validator);
458
+ it ('returns an error when the function returns an error', async () => {
459
+ const result = await controller.validateFunction(Math.random(), validator);
468
460
  expect(result).toBe(error);
469
461
  });
470
462
  });
@@ -472,10 +464,11 @@ describe('VidaServerController', () => {
472
464
 
473
465
  describe('#performRequest', () => {
474
466
  const errorHandlers = [
475
- ['renderUnauthorizedResponse', 'AuthorizationError'],
476
- ['renderForbiddenResponse', 'ForbiddenError'],
477
- ['renderNotFoundResponse', 'NotFoundError'],
478
- ['renderErrors', 'ValidationError'],
467
+ ['renderUnauthorized', 'AuthorizationError'],
468
+ ['renderConflict', 'ConflictError'],
469
+ ['renderForbidden', 'ForbiddenError'],
470
+ ['renderNotFound', 'NotFoundError'],
471
+ ['renderErrors', 'ValidationError'],
479
472
  ]
480
473
 
481
474
  it.each(errorHandlers)('calls %s when %s is thrown', async (handlerName, errorClsName) => {
@@ -505,7 +498,7 @@ describe('VidaServerController', () => {
505
498
  await controller.performRequest('testAction');
506
499
 
507
500
  expect(controller.statusCode).toEqual(500);
508
- expect(controller.render).toHaveBeenCalledWith({errors: { message }});
501
+ expect(controller.render).toHaveBeenCalledWith({errors: {message: null}});
509
502
  });
510
503
  });
511
504
 
@@ -519,7 +512,6 @@ describe('VidaServerController', () => {
519
512
  controller.render = jest.fn();
520
513
  });
521
514
 
522
-
523
515
  describe('#renderErrors', () => {
524
516
  it ('returns a 400 response', async () => {
525
517
  await controller.renderErrors();
@@ -576,64 +568,66 @@ describe('VidaServerController', () => {
576
568
  });
577
569
  });
578
570
 
579
-
580
- describe('#renderUnauthorizedResponse', () => {
581
- it ('returns a 401 response', async () => {
582
- await controller.renderUnauthorizedResponse();
583
- expect(response.statusCode).toBe(401);
584
- })
585
-
586
- it ('includes a blank message by default', async () => {
587
- await controller.renderUnauthorizedResponse();
588
- expect(controller.render).toHaveBeenCalledTimes(1);
589
- expect(controller.render).toHaveBeenCalledWith({errors: {message: null}});
590
- })
591
571
 
592
- it ('includes a provided message', async () => {
593
- const msg = TestHelpers.Faker.Text.randomString();
594
- await controller.renderUnauthorizedResponse(msg);
595
- expect(controller.render).toHaveBeenCalledTimes(1);
596
- expect(controller.render).toHaveBeenCalledWith({errors: {message: msg}});
597
- });
598
- });
599
-
600
-
601
- describe('#renderForbiddenResponse', () => {
602
- it ('returns a 403 response', async () => {
603
- await controller.renderForbiddenResponse();
604
- expect(response.statusCode).toBe(403);
572
+ const successHandlers = [
573
+ ['renderCreationSuccessful', 201],
574
+ ['renderAccepted', 202],
575
+ ['renderNoConent', 204],
576
+ ['renderMovedPermanently', 301],
577
+ ['renderFound', 302],
578
+ ['renderNotModified', 304],
579
+ ['renderTemporaryRedirect', 307],
580
+ ['renderPermanentRedirect', 308],
581
+ ]
582
+ describe.each(successHandlers)('#%s', (handlerName, statusCode) => {
583
+ it (`returns a ${statusCode} response`, async () => {
584
+ await (controller[handlerName]());
585
+ expect(response.statusCode).toBe(statusCode);
605
586
  })
606
587
 
607
- it ('includes a blank message by default', async () => {
608
- await controller.renderForbiddenResponse();
609
- expect(controller.render).toHaveBeenCalledTimes(1);
610
- expect(controller.render).toHaveBeenCalledWith({errors: {message: null}});
611
- })
588
+ it ('includes the provided body', async () => {
589
+ const key = TestHelpers.Faker.Text.randomString();
590
+ const value = TestHelpers.Faker.Text.randomString();
591
+ const body = {[key]: value};
592
+ await (controller[handlerName](body));
612
593
 
613
- it ('includes a provided message', async () => {
614
- const msg = TestHelpers.Faker.Text.randomString();
615
- await controller.renderForbiddenResponse(msg);
616
594
  expect(controller.render).toHaveBeenCalledTimes(1);
617
- expect(controller.render).toHaveBeenCalledWith({errors: {message: msg}});
595
+ expect(controller.render).toHaveBeenCalledWith(body);
618
596
  });
619
597
  });
620
598
 
599
+
600
+ const errorHandlers = [
601
+ ['renderUnauthorized', 401],
602
+ ['renderPaymentRequired', 402],
603
+ ['renderForbidden', 403],
604
+ ['renderNotFound', 404],
605
+ ['renderMethodNotAllowed', 405],
606
+ ['renderRequestTimeout', 408],
607
+ ['renderConflict', 409],
608
+ ['renderGone', 410],
609
+ ['renderContentTooLarge', 413],
610
+ ['renderUnsupportedMediaType', 415],
611
+ ['renderUnprocessableContent', 422],
612
+ ['renderLocked', 423],
613
+ ['renderTooManyRequests', 429],
614
+ ]
621
615
 
622
- describe('#renderNotFoundResponse', () => {
623
- it ('returns a 404 response', async () => {
624
- await controller.renderNotFoundResponse();
625
- expect(response.statusCode).toBe(404);
616
+ describe.each(errorHandlers)('#%s', (handlerName, statusCode) => {
617
+ it (`returns a ${statusCode} response`, async () => {
618
+ await (controller[handlerName]());
619
+ expect(response.statusCode).toBe(statusCode);
626
620
  })
627
621
 
628
622
  it ('includes a blank message by default', async () => {
629
- await controller.renderNotFoundResponse();
623
+ await controller[handlerName]();
630
624
  expect(controller.render).toHaveBeenCalledTimes(1);
631
625
  expect(controller.render).toHaveBeenCalledWith({errors: {message: null}});
632
626
  })
633
627
 
634
628
  it ('includes a provided message', async () => {
635
629
  const msg = TestHelpers.Faker.Text.randomString();
636
- await controller.renderNotFoundResponse(msg);
630
+ await controller[handlerName](msg);
637
631
  expect(controller.render).toHaveBeenCalledTimes(1);
638
632
  expect(controller.render).toHaveBeenCalledWith({errors: {message: msg}});
639
633
  });