@vida-global/core 1.3.3 → 1.4.0

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
 
@@ -28,6 +28,7 @@ class VidaServerController {
28
28
  this.#response = response;
29
29
 
30
30
  this.#applyCallbacks();
31
+ this.#setupStatusRenderers();
31
32
  }
32
33
 
33
34
 
@@ -121,13 +122,13 @@ class VidaServerController {
121
122
 
122
123
  async #handleError(err) {
123
124
  if (err instanceof AuthorizationError) {
124
- await this.renderUnauthorizedResponse(err.message);
125
+ await this.renderUnauthorized(err.message);
125
126
 
126
127
  } else if (err instanceof ForbiddenError) {
127
- await this.renderForbiddenResponse(err.message);
128
+ await this.renderForbidden(err.message);
128
129
 
129
130
  } else if (err instanceof NotFoundError) {
130
- await this.renderNotFoundResponse(err.message);
131
+ await this.renderNotFound(err.message);
131
132
 
132
133
  } else if (err instanceof ValidationError) {
133
134
  await this.renderErrors(err.message, err.fields);
@@ -140,7 +141,7 @@ class VidaServerController {
140
141
  } else {
141
142
  message = err.message;
142
143
  }
143
- await this.renderServerErrorResponse(message);
144
+ await this.renderServerError(message);
144
145
  }
145
146
  }
146
147
 
@@ -186,33 +187,14 @@ class VidaServerController {
186
187
  }
187
188
 
188
189
 
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) {
190
+ async renderServerError(message=null) {
191
+ if (process.env.NODE_ENV != 'development') {
192
+ message = null;
193
+ }
205
194
  return await this.#renderError(500, message);
206
195
  }
207
196
 
208
197
 
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
198
  formatJSONBody(body, errors, options) {
217
199
  if (options.standardize === false) return body;
218
200
 
@@ -253,26 +235,118 @@ class VidaServerController {
253
235
  switch(this.statusCode) {
254
236
  case 200:
255
237
  return 'ok';
238
+ case 201:
239
+ return 'created';
240
+ case 202:
241
+ return 'accepted';
242
+ case 204:
243
+ return 'no content';
244
+ case 301:
245
+ return 'moved permanently';
246
+ case 302:
247
+ return 'found';
248
+ case 304:
249
+ return 'not modified';
250
+ case 307:
251
+ return 'temporary redirect';
252
+ case 308:
253
+ return 'permanent redirect';
256
254
  case 400:
257
255
  return 'bad request'
258
256
  case 401:
259
257
  return 'unauthorized';
258
+ case 402:
259
+ return 'payment required';
260
260
  case 403:
261
261
  return 'forbidden';
262
262
  case 404:
263
263
  return 'not found';
264
+ case 405:
265
+ return 'method not allowed';
266
+ case 408:
267
+ return 'request timeout';
268
+ case 409:
269
+ return 'conflict';
270
+ case 410:
271
+ return 'gone';
272
+ case 413:
273
+ return 'content too large';
274
+ case 415:
275
+ return 'unsupported media type';
276
+ case 422:
277
+ return 'unprocessable content';
278
+ case 423:
279
+ return 'locked';
280
+ case 429:
281
+ return 'too many requests';
264
282
  case 500:
265
283
  return 'server error';
266
284
  }
267
285
  }
268
286
 
269
287
 
288
+ #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
+ const successStatuses = {
306
+ CreationSuccessful: 201,
307
+ Accepted: 202,
308
+ NoConent: 204,
309
+ MovedPermanently: 301,
310
+ Found: 302,
311
+ NotModified: 304,
312
+ TemporaryRedirect: 307,
313
+ PermanentRedirect: 308,
314
+ }
315
+
316
+ for (const [method, statusCode] of Object.entries(errorStatuses)) {
317
+ this[`render${method}`] = async function(message=null) {
318
+ return await this.#renderError(statusCode, message);
319
+ }
320
+ }
321
+
322
+ for (const [method, statusCode] of Object.entries(successStatuses)) {
323
+ this[`render${method}`] = async function(message=null) {
324
+ return await this.#renderSuccess(statusCode, message);
325
+ }
326
+ }
327
+ }
328
+
329
+
330
+ async #renderError(statusCode, message) {
331
+ this.statusCode = statusCode;
332
+ const body = {errors: {message: message || null}};
333
+ return await this.render(body);
334
+ }
335
+
336
+
337
+ async #renderSuccess(statusCode, body) {
338
+ this.statusCode = statusCode;
339
+ return await this.render(body);
340
+ }
341
+
342
+
270
343
  /***********************************************************************************************
271
344
  * HELPERS
272
345
  ***********************************************************************************************/
273
346
  static autoLoadHelpers() {
274
- const { InstanceMethods, Accessors } = this._autoLoadModule(this.autoLoadHelperPath);
347
+ const { StaticMethods, InstanceMethods, Accessors } = this._autoLoadModule(this.autoLoadHelperPath);
275
348
 
349
+ if (StaticMethods) Object.assign(this, StaticMethods);
276
350
  if (InstanceMethods) Object.assign(this.prototype, InstanceMethods);
277
351
 
278
352
  if (Accessors) {
@@ -385,10 +459,10 @@ class VidaServerController {
385
459
  /***********************************************************************************************
386
460
  * VALIDATIONS
387
461
  ***********************************************************************************************/
388
- validateParameters(validations) {
462
+ async validateParameters(validations) {
389
463
  const errors = {};
390
464
  for (const [parameterName, parameterValidations] of Object.entries(validations)) {
391
- const parameterErrors = this.validateParameter(this.params[parameterName], parameterValidations);
465
+ const parameterErrors = await this.validateParameter(this.params[parameterName], parameterValidations);
392
466
  if (parameterErrors.length) errors[parameterName] = parameterErrors;
393
467
  }
394
468
 
@@ -396,7 +470,7 @@ class VidaServerController {
396
470
  }
397
471
 
398
472
 
399
- validateParameter(value, parameterValidations) {
473
+ async validateParameter(value, parameterValidations) {
400
474
  const errors = [];
401
475
 
402
476
  if (parameterValidations?.optional && value === undefined) return errors;
@@ -404,7 +478,7 @@ class VidaServerController {
404
478
  for (const [validationType, validationOptions] of Object.entries(parameterValidations)) {
405
479
  const validationMethod = `validate${camelize(validationType)}`;
406
480
  if (!this[validationMethod]) continue;
407
- const error = this[validationMethod](value, validationOptions);
481
+ const error = await this[validationMethod](value, validationOptions);
408
482
  if (error) errors.push(error);
409
483
  }
410
484
 
@@ -457,8 +531,8 @@ class VidaServerController {
457
531
  }
458
532
 
459
533
 
460
- validateFunction(value, fnc) {
461
- const error = fnc(value);
534
+ async validateFunction(value, fnc) {
535
+ const error = await fnc.call(this, value);
462
536
  if (error) return error;
463
537
  }
464
538
 
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.0",
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,10 @@ 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
+ ['renderForbidden', 'ForbiddenError'],
469
+ ['renderNotFound', 'NotFoundError'],
470
+ ['renderErrors', 'ValidationError'],
479
471
  ]
480
472
 
481
473
  it.each(errorHandlers)('calls %s when %s is thrown', async (handlerName, errorClsName) => {
@@ -505,7 +497,7 @@ describe('VidaServerController', () => {
505
497
  await controller.performRequest('testAction');
506
498
 
507
499
  expect(controller.statusCode).toEqual(500);
508
- expect(controller.render).toHaveBeenCalledWith({errors: { message }});
500
+ expect(controller.render).toHaveBeenCalledWith({errors: {message: null}});
509
501
  });
510
502
  });
511
503
 
@@ -519,7 +511,6 @@ describe('VidaServerController', () => {
519
511
  controller.render = jest.fn();
520
512
  });
521
513
 
522
-
523
514
  describe('#renderErrors', () => {
524
515
  it ('returns a 400 response', async () => {
525
516
  await controller.renderErrors();
@@ -576,64 +567,66 @@ describe('VidaServerController', () => {
576
567
  });
577
568
  });
578
569
 
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
570
 
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);
571
+ const successHandlers = [
572
+ ['renderCreationSuccessful', 201],
573
+ ['renderAccepted', 202],
574
+ ['renderNoConent', 204],
575
+ ['renderMovedPermanently', 301],
576
+ ['renderFound', 302],
577
+ ['renderNotModified', 304],
578
+ ['renderTemporaryRedirect', 307],
579
+ ['renderPermanentRedirect', 308],
580
+ ]
581
+ describe.each(successHandlers)('#%s', (handlerName, statusCode) => {
582
+ it (`returns a ${statusCode} response`, async () => {
583
+ await (controller[handlerName]());
584
+ expect(response.statusCode).toBe(statusCode);
605
585
  })
606
586
 
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
- })
587
+ it ('includes the provided body', async () => {
588
+ const key = TestHelpers.Faker.Text.randomString();
589
+ const value = TestHelpers.Faker.Text.randomString();
590
+ const body = {[key]: value};
591
+ await (controller[handlerName](body));
612
592
 
613
- it ('includes a provided message', async () => {
614
- const msg = TestHelpers.Faker.Text.randomString();
615
- await controller.renderForbiddenResponse(msg);
616
593
  expect(controller.render).toHaveBeenCalledTimes(1);
617
- expect(controller.render).toHaveBeenCalledWith({errors: {message: msg}});
594
+ expect(controller.render).toHaveBeenCalledWith(body);
618
595
  });
619
596
  });
620
597
 
598
+
599
+ const errorHandlers = [
600
+ ['renderUnauthorized', 401],
601
+ ['renderPaymentRequired', 402],
602
+ ['renderForbidden', 403],
603
+ ['renderNotFound', 404],
604
+ ['renderMethodNotAllowed', 405],
605
+ ['renderRequestTimeout', 408],
606
+ ['renderConflict', 409],
607
+ ['renderGone', 410],
608
+ ['renderContentTooLarge', 413],
609
+ ['renderUnsupportedMediaType', 415],
610
+ ['renderUnprocessableContent', 422],
611
+ ['renderLocked', 423],
612
+ ['renderTooManyRequests', 429],
613
+ ]
621
614
 
622
- describe('#renderNotFoundResponse', () => {
623
- it ('returns a 404 response', async () => {
624
- await controller.renderNotFoundResponse();
625
- expect(response.statusCode).toBe(404);
615
+ describe.each(errorHandlers)('#%s', (handlerName, statusCode) => {
616
+ it (`returns a ${statusCode} response`, async () => {
617
+ await (controller[handlerName]());
618
+ expect(response.statusCode).toBe(statusCode);
626
619
  })
627
620
 
628
621
  it ('includes a blank message by default', async () => {
629
- await controller.renderNotFoundResponse();
622
+ await controller[handlerName]();
630
623
  expect(controller.render).toHaveBeenCalledTimes(1);
631
624
  expect(controller.render).toHaveBeenCalledWith({errors: {message: null}});
632
625
  })
633
626
 
634
627
  it ('includes a provided message', async () => {
635
628
  const msg = TestHelpers.Faker.Text.randomString();
636
- await controller.renderNotFoundResponse(msg);
629
+ await controller[handlerName](msg);
637
630
  expect(controller.render).toHaveBeenCalledTimes(1);
638
631
  expect(controller.render).toHaveBeenCalledWith({errors: {message: msg}});
639
632
  });