@vida-global/core 1.3.1 → 1.3.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.
@@ -7,14 +7,16 @@ const{ AuthorizationError,
7
7
  ValidationError } = require('./errors');
8
8
 
9
9
 
10
+ const AUTH_CALLBACK_NAME = 'authenticateRequest';
11
+
12
+
10
13
  class VidaServerController {
11
14
  #afterCallbacks = [];
12
15
  #beforeCallbacks = [];
16
+ #callbacksSetUp = false;
13
17
  #params;
14
18
  #rendered = false;
15
19
  #request;
16
- #requestBody;
17
- #requestQuery;
18
20
  #response;
19
21
 
20
22
 
@@ -24,6 +26,8 @@ class VidaServerController {
24
26
  }
25
27
  this.#request = request;
26
28
  this.#response = response;
29
+
30
+ this.#applyCallbacks();
27
31
  }
28
32
 
29
33
 
@@ -32,15 +36,15 @@ class VidaServerController {
32
36
 
33
37
  get params() {
34
38
  if (!this.#params) {
35
- const body = this.#processRequestData('body');
36
- const query = this.#processRequestData('query');
37
- this.#params = {...this._request.params, ...body, ...query};
39
+ const body = Buffer.isBuffer(this._request.body) ? {} : this.#processRequestData('body');
40
+ this.#params = {...this._request.params, ...this._request.query, ...body};
38
41
  }
39
42
  return structuredClone(this.#params);
40
43
  }
41
44
 
42
45
  get requestHeaders() { return structuredClone(this._request.headers || {}); }
43
46
  get responseHeaders() { return this._response.headers; };
47
+ get requestBody() { return this._request.body; }
44
48
  get contentType() { return this.requestHeaders['content-type']; }
45
49
  get logger() { return logger; }
46
50
  get rendered() { return this.#rendered }
@@ -51,23 +55,20 @@ class VidaServerController {
51
55
 
52
56
 
53
57
  #processRequestData(key) {
54
- const instanceVarKey = key.charAt(0).toUpperCase() + key.slice(1);
55
- const instanceVarName = `#request${instanceVarKey}`;
56
- const data = this._request[key] || {};
57
- if (this[instanceVarName] === undefined) {
58
- const processedData = {};
59
- this[instanceVarName] = processedData;
60
- for (let [paramName, paramValue] of Object.entries(data)) {
61
- processedData[paramName] = this.#processedRequestDataValue(paramValue)
62
- }
58
+ const data = this._request[key] || {};
59
+ const processedData = {};
60
+ for (let [paramName, paramValue] of Object.entries(data)) {
61
+ processedData[paramName] = this.#processedRequestDataValue(paramValue)
63
62
  }
64
63
 
65
- return structuredClone(this[instanceVarName]);
64
+ return processedData;
66
65
  }
67
66
 
68
67
 
69
68
  #processedRequestDataValue(value) {
69
+ return value;
70
70
  try {
71
+ if (!/^\[|{/.test(value)) return value;
71
72
  return JSON.parse(value);
72
73
  } catch(err) {
73
74
  return value;
@@ -95,8 +96,6 @@ class VidaServerController {
95
96
  return;
96
97
  }
97
98
 
98
- this.setupCallbacks();
99
-
100
99
  const responseBody = await this.#performAction(action);
101
100
 
102
101
  if (!this.rendered) {
@@ -109,6 +108,10 @@ class VidaServerController {
109
108
 
110
109
  async #performAction(action) {
111
110
  if (await this.runBeforeCallbacks(action) === false) return false;
111
+
112
+ const parameters = this.constructor.parametersForAction(action);
113
+ if (parameters) await this.validateParameters(parameters);
114
+
112
115
  const responseBody = await this[action]();
113
116
  if (await this.runAfterCallbacks(action) === false) return false;
114
117
 
@@ -127,12 +130,17 @@ class VidaServerController {
127
130
  await this.renderNotFoundResponse(err.message);
128
131
 
129
132
  } else if (err instanceof ValidationError) {
130
- await this.renderErrors(err.message, err.errors);
133
+ await this.renderErrors(err.message, err.fields);
131
134
 
132
135
  } else {
133
- this.statusCode = 500;
134
- await this.render({error: err.message});
135
- if (process.env.NODE_ENV == 'development') console.log(err);
136
+ this._response.error = err;
137
+ let message;
138
+ if (typeof err == 'string') {
139
+ message = err;
140
+ } else {
141
+ message = err.message;
142
+ }
143
+ await this.renderServerErrorResponse(message);
136
144
  }
137
145
  }
138
146
 
@@ -144,7 +152,12 @@ class VidaServerController {
144
152
  if (typeof body == 'string') {
145
153
  this._response.send(body);
146
154
  } else {
147
- body = await this.formatJSONBody(body, options);
155
+ body = await this.processJSONBody(body, options);
156
+
157
+ const errors = body.errors || null;
158
+ delete body.errors;
159
+
160
+ body = this.formatJSONBody(body, errors, options);
148
161
  this._response.json(body);
149
162
  }
150
163
  this.#rendered = true;
@@ -173,42 +186,54 @@ class VidaServerController {
173
186
  }
174
187
 
175
188
 
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
+
176
199
  async renderNotFoundResponse(message=null) {
177
- this.statusCode = 404;
178
- await this.render({ message });
200
+ return await this.#renderError(404, message);
179
201
  }
180
202
 
181
203
 
182
- async renderUnauthorizedResponse(message=null) {
183
- this.statusCode = 401;
184
- await this.render({ message });
204
+ async renderServerErrorResponse(message=null) {
205
+ return await this.#renderError(500, message);
185
206
  }
186
207
 
187
208
 
188
- async renderForbiddenResponse(message=null) {
189
- this.statusCode = 403;
190
- await this.render({ message });
209
+ async #renderError(statusCode, message) {
210
+ this.statusCode = statusCode;
211
+ const body = {errors: {message: message || null}};
212
+ return await this.render(body);
191
213
  }
192
214
 
193
215
 
194
- async formatJSONBody(body, options) {
195
- body = await this._formatJSONBody(body, options);
216
+ formatJSONBody(body, errors, options) {
196
217
  if (options.standardize === false) return body;
197
- return {data: body, status: this.statusText};
218
+
219
+ const response = {data: body, status: this.statusText};
220
+ if (errors) response.errors = errors;
221
+
222
+ return response;
198
223
  }
199
224
 
200
225
 
201
- async _formatJSONBody(body, options) {
226
+ async processJSONBody(body, options) {
202
227
  if (body === undefined) return null;
203
228
  if (!body || typeof body != 'object') return body;
204
229
 
205
230
  if (body.toApiResponse) {
206
231
  const formattedBody = await body.toApiResponse(options);
207
- return await this._formatJSONBody(formattedBody, options);
232
+ return await this.processJSONBody(formattedBody, options);
208
233
 
209
234
  } else if (Array.isArray(body)) {
210
235
  for (const idx in body) {
211
- body[idx] = await this._formatJSONBody(body[idx], options);
236
+ body[idx] = await this.processJSONBody(body[idx], options);
212
237
  }
213
238
 
214
239
  } else if (body.constructor == Date) {
@@ -216,7 +241,7 @@ class VidaServerController {
216
241
 
217
242
  } else {
218
243
  for (let [key, val] of Object.entries(body)) {
219
- body[key] = await this._formatJSONBody(val, options);
244
+ body[key] = await this.processJSONBody(val, options);
220
245
  }
221
246
  }
222
247
 
@@ -246,17 +271,7 @@ class VidaServerController {
246
271
  * HELPERS
247
272
  ***********************************************************************************************/
248
273
  static autoLoadHelpers() {
249
- try {
250
- this._autoLoadHelpers()
251
- } catch(err) {
252
- if (err.message.includes(`Cannot find module '${this.autoLoadHelperPath}'`)) return;
253
- throw err;
254
- }
255
- }
256
-
257
-
258
- static _autoLoadHelpers() {
259
- const { InstanceMethods, Accessors } = require(this.autoLoadHelperPath);
274
+ const { InstanceMethods, Accessors } = this._autoLoadModule(this.autoLoadHelperPath);
260
275
 
261
276
  if (InstanceMethods) Object.assign(this.prototype, InstanceMethods);
262
277
 
@@ -268,11 +283,34 @@ class VidaServerController {
268
283
  }
269
284
 
270
285
 
286
+ static autoLoadDocumentation() {
287
+ const docs = this._autoLoadModule(this.autoLoadDocumentationPath);
288
+ Object.assign(this, docs);
289
+ }
290
+
291
+
292
+ static _autoLoadModule(path) {
293
+ try {
294
+ return require(path);
295
+ } catch(err) {
296
+ const errorText = `Cannot find module '${path}'`;
297
+ if (!err.message.includes(errorText)) throw err;
298
+ }
299
+
300
+ return {};
301
+ }
302
+
303
+
271
304
  static get autoLoadHelperPath() {
272
305
  return `${process.cwd()}/lib/controllers${this.routePrefix}Controller.js`;
273
306
  }
274
307
 
275
308
 
309
+ static get autoLoadDocumentationPath() {
310
+ return `${process.cwd()}/lib/controllers/_docs${this.routePrefix}Controller.js`;
311
+ }
312
+
313
+
276
314
  /***********************************************************************************************
277
315
  * CALLBACKS
278
316
  ***********************************************************************************************/
@@ -280,6 +318,13 @@ class VidaServerController {
280
318
  setupCallbacks() {}
281
319
 
282
320
 
321
+ #applyCallbacks() {
322
+ // set up the callbacks when the instance is constructed and prevent it from being called again
323
+ this.setupCallbacks();
324
+ this.setupCallbacks = undefined;
325
+ }
326
+
327
+
283
328
  beforeCallback(callback, options={}) {
284
329
  this.#addCallback(callback, options, this.#beforeCallbacks);
285
330
  }
@@ -340,7 +385,7 @@ class VidaServerController {
340
385
  /***********************************************************************************************
341
386
  * VALIDATIONS
342
387
  ***********************************************************************************************/
343
- async validateParameters(validations) {
388
+ validateParameters(validations) {
344
389
  const errors = {};
345
390
  for (const [parameterName, parameterValidations] of Object.entries(validations)) {
346
391
  const parameterErrors = this.validateParameter(this.params[parameterName], parameterValidations);
@@ -353,8 +398,12 @@ class VidaServerController {
353
398
 
354
399
  validateParameter(value, parameterValidations) {
355
400
  const errors = [];
401
+
402
+ if (parameterValidations?.optional && value === undefined) return errors;
403
+
356
404
  for (const [validationType, validationOptions] of Object.entries(parameterValidations)) {
357
405
  const validationMethod = `validate${camelize(validationType)}`;
406
+ if (!this[validationMethod]) continue;
358
407
  const error = this[validationMethod](value, validationOptions);
359
408
  if (error) errors.push(error);
360
409
  }
@@ -370,10 +419,29 @@ class VidaServerController {
370
419
 
371
420
  validateIsInteger(value, options) {
372
421
  const num = parseFloat(value);
373
- if (/\D/.test(value) || isNaN(num) || !Number.isInteger(num)) return 'must be an integer';
374
- if (options && options.gte !== undefined) {
422
+ if (!/^-?\d+$/.test(value) || isNaN(num) || !Number.isInteger(num)) return 'must be an integer';
423
+
424
+ if (options?.gte !== undefined) {
375
425
  if (num < options.gte) return `must be greater than or equal to ${options.gte}`;
376
426
  }
427
+ if (options?.lte !== undefined) {
428
+ if (num > options.lte) return `must be less than or equal to ${options.lte}`;
429
+ }
430
+ }
431
+
432
+
433
+ validateIsString(value, options) {
434
+ if (typeof value !== 'string') return 'must be a string';
435
+
436
+ if (options?.length?.gte !== undefined) {
437
+ if (value.length < options.length.gte) return `must be greater than or equal to ${options.length.gte} characters`;
438
+ }
439
+ if (options?.length?.lte !== undefined) {
440
+ if (value.length > options.length.lte) return `must be fewer than or equal to ${options.length.lte} characters`;
441
+ }
442
+ if (options?.regex !== undefined) {
443
+ if (!options.regex.test(value)) return `must match ${options.regex}`;
444
+ }
377
445
  }
378
446
 
379
447
 
@@ -389,11 +457,6 @@ class VidaServerController {
389
457
  }
390
458
 
391
459
 
392
- validateRegex(value, regex) {
393
- if (!regex.test(value)) return `must match the pattern ${regex}`;
394
- }
395
-
396
-
397
460
  validateFunction(value, fnc) {
398
461
  const error = fnc(value);
399
462
  if (error) return error;
@@ -482,7 +545,56 @@ class VidaServerController {
482
545
 
483
546
 
484
547
  static get actionRegExp() {
485
- return new RegExp(/^(?<method>get|post|put|delete|patch)(?<path>[A-Z][a-zA-Z]+)$/);
548
+ return new RegExp(/^(?<method>get|post|put|delete|patch|head)(?<path>[A-Z][a-zA-Z]+)$/);
549
+ }
550
+
551
+
552
+ /***********************************************************************************************
553
+ * DOCUMENTATION
554
+ ***********************************************************************************************/
555
+ static parametersForAction(actionName) {
556
+ const methodName = this.parametersMethodForAction(actionName);
557
+ if (this[methodName]) return this[methodName]();
558
+ return null;
559
+ }
560
+
561
+
562
+ static parametersMethodForAction(actionName) {
563
+ return `parametersFor${camelize(actionName)}`;
564
+ }
565
+
566
+
567
+ static documentationForAction(actionName) {
568
+ const methodName = this.documentationMethodForAction(actionName);
569
+ if (this[methodName]) return this[methodName]();
570
+ return null;
571
+ }
572
+
573
+
574
+ static documentationMethodForAction(actionName) {
575
+ return `document${camelize(actionName)}`;
576
+ }
577
+
578
+
579
+ isAuthenticatedAction(actionName) {
580
+ const { options } = this.#beforeCallbacks.find(({ callback, options }) => callback == AUTH_CALLBACK_NAME)
581
+ return Boolean(options) && this.#shouldRunCallback(actionName, options);
582
+ }
583
+
584
+
585
+ static isAuthenticatedAction(actionName) {
586
+ const controller = new this();
587
+ return controller.isAuthenticatedAction(actionName);
588
+ }
589
+
590
+
591
+ static _authenticationDocumentation(actionName) {
592
+ if (!this.isAuthenticatedAction(actionName)) return undefined;
593
+ }
594
+
595
+
596
+ static authenticationDocumentation(actionName) {
597
+ return {apiKeyAuth: []};
486
598
  }
487
599
  }
488
600
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vida-global/core",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Core libraries for supporting Vida development",
5
5
  "author": "",
6
6
  "license": "ISC",
@@ -33,7 +33,7 @@
33
33
  "@vida-global/release": "^1.0.0"
34
34
  },
35
35
  "devDependencies": {
36
- "jest": "^30.0.0",
36
+ "jest": "^29.0.0",
37
37
  "jest-express": "^1.12.0",
38
38
  "jest-extended": "^7.0.0",
39
39
  "@vida-global/test-helpers": "^1.0.1"
@@ -8,12 +8,14 @@ const { VidaServer } = require('../../lib/server');
8
8
 
9
9
 
10
10
  jest.mock('express', () => {
11
+ const express = require('jest-express');
11
12
  const { Response } = require('jest-express/lib/response');
12
13
 
13
- // missing functions in mock Response
14
- Response.prototype.on = jest.fn();
14
+ // missing functions in mock express
15
+ Response.prototype.on = jest.fn();
16
+ express.raw = jest.fn();
15
17
 
16
- return require('jest-express');
18
+ return express;
17
19
  });
18
20
 
19
21