@vida-global/core 1.3.0 → 1.3.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.
@@ -16,13 +16,23 @@ jobs:
16
16
  publish:
17
17
  runs-on: ubuntu-latest
18
18
  steps:
19
- - uses: actions/checkout@v4
20
- - uses: actions/setup-node@v4
19
+ - name: Checkout code
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Setup Node
23
+ uses: actions/setup-node@v4
21
24
  with:
22
- node-version: '24'
25
+ node-version: '20'
23
26
  registry-url: 'https://registry.npmjs.org'
24
- - run: npm ci
25
- env:
26
- NODE_AUTH_TOKEN: ${{ secrets.NPM_READ_TOKEN }}
27
- - run: npm test
28
- - run: npm publish
27
+
28
+ - name: Update npm
29
+ run: npm install -g npm@latest
30
+
31
+ - name: Install dependencies
32
+ run: npm ci
33
+
34
+ - name: Run tests
35
+ run: npm test
36
+
37
+ - name: Publish
38
+ run: npm publish
@@ -1,3 +1,6 @@
1
+ # This workflow will run tests using node
2
+
3
+
1
4
  name: npm test
2
5
 
3
6
  on:
@@ -6,7 +9,6 @@ on:
6
9
  jobs:
7
10
  test:
8
11
  runs-on: ubuntu-latest
9
-
10
12
  steps:
11
13
  - name: Checkout code
12
14
  uses: actions/checkout@v4
@@ -14,8 +16,8 @@ jobs:
14
16
  - name: Setup Node
15
17
  uses: actions/setup-node@v4
16
18
  with:
17
- node-version: 20
18
- cache: 'npm'
19
+ node-version: '20'
20
+ registry-url: 'https://registry.npmjs.org'
19
21
 
20
22
  - name: Install dependencies
21
23
  run: npm ci
package/index.js CHANGED
@@ -1,19 +1,14 @@
1
- const { HttpClient, HttpError } = require('./lib/http/client');
2
- const { AuthorizationError,
3
- VidaServer,
4
- VidaServerController } = require('./lib/server');
5
- const { logger } = require('./lib/logger');
6
- const ActiveRecord = require('./lib/activeRecord');
7
- const { redisClientFactory } = require('./lib/redis');
1
+ const httpLibs = require('./lib/http/client');
2
+ const serverLibs = require('./lib/server');
3
+ const { logger } = require('./lib/logger');
4
+ const ActiveRecord = require('./lib/activeRecord');
5
+ const redisLibs = require('./lib/redis');
8
6
 
9
7
 
10
8
  module.exports = {
11
9
  ActiveRecord,
12
- AuthorizationError,
13
- HttpClient,
14
- HttpError,
10
+ ...httpLibs,
15
11
  logger,
16
- redisClientFactory,
17
- VidaServer,
18
- VidaServerController,
12
+ ...redisLibs,
13
+ ...serverLibs
19
14
  };
@@ -59,7 +59,7 @@ function resolveAutoIncrementColumns(details) {
59
59
  if (!details.defaultValue) return;
60
60
 
61
61
  const regExp = /^nextval\(\w+_seq::regclass\)$/;
62
- if (details.defaultValue.match(regExp)) {
62
+ if (regExp.test(details.defaultValue)) {
63
63
  delete details.defaultValue;
64
64
  delete details.allowNull;
65
65
  details.autoIncrement = true;
@@ -1,5 +1,5 @@
1
1
  const { redisClientFactory } = require('./redisClient');
2
2
 
3
3
  module.exports = {
4
- redisClientFactory
4
+ redisClientFactory
5
5
  }
@@ -194,5 +194,5 @@ titleUniqueness(title) {
194
194
  - `{isInteger: true}`, `{isInteger: {gte: 0, lte: 100}}`
195
195
  - `{isDateTime}`
196
196
  - `{isEnum: {enums: [a, b, c]}}`
197
- - `{regex: /foo/}`
197
+ - `{isString: true}`, `{isString: {length: {gte: 10, lte: 100}, regex: /foo/}}
198
198
  - `{function: someFunction}` If the function returns a string, that is considered an error and returned as the validation message
@@ -0,0 +1,86 @@
1
+ const { ControllerImporter } = require('./controllerImporter');
2
+
3
+
4
+ class ApiDocsGenerator {
5
+ #controllerDirectories
6
+ #controllerClasses;
7
+ #docs = {};
8
+
9
+ constructor(server) {
10
+ this.#controllerDirectories = server.controllerDirectories;
11
+ }
12
+
13
+
14
+ generateDocs() {
15
+ console.log("\n\n");
16
+ this.controllerClasses.forEach(controllerClass => {
17
+ controllerClass.actions.forEach(action => {
18
+ this.generateDocsForAction(action, controllerClass);
19
+ });
20
+ });
21
+ console.log("\n");
22
+ }
23
+
24
+
25
+ generateDocsForAction(action, controllerClass) {
26
+ const documentationDetails = controllerClass.documentationForAction(action.action);
27
+ if (!documentationDetails?.description) return;
28
+
29
+ const components = this.collectComponents(controllerClass, documentationDetails, action);
30
+
31
+ const endpoint = components.endpoint;
32
+ this.#docs[endpoint] = this.#docs[endpoint] || {};
33
+ const actionDocs = this.#docs[endpoint][action.method.toLowerCase()] = {};
34
+
35
+ }
36
+
37
+
38
+ collectComponents(controllerClass, documentationDetails, action) {
39
+ const components = {...documentationDetails};
40
+ components.endpoint = this.formatEndpoint(action);
41
+ components.method = action.method;
42
+
43
+ const pathParamNames = components.endpoint.match(/{[^}]+}/g).map(p => {
44
+ return p.replace(/[{}]/g, '')
45
+ });
46
+
47
+ const parameters = Object.entries(controllerClass.parametersForAction(action.action));
48
+ const pathParams = parameters.filter(([param, _]) => pathParamNames.includes(param));
49
+ const otherParams = parameters.filter(([param, _]) => !pathParamNames.includes(param));
50
+ console.log(components.endpoint);
51
+ console.log(pathParams);
52
+ console.log(components.method);
53
+ console.log(parameters);
54
+ console.log(components.description);
55
+ console.log(components.summary);
56
+ console.log("\n");
57
+ //const auth = controllerClass.authenticationDocumentation(action.action);
58
+ //const method = action.method;
59
+
60
+ return components;
61
+ }
62
+
63
+
64
+ get controllerClasses() {
65
+ if (!this.#controllerClasses) {
66
+ const controllerImporter = new ControllerImporter(this.#controllerDirectories);
67
+ this.#controllerClasses = controllerImporter.controllerClasses;
68
+ }
69
+ return [...this.#controllerClasses];
70
+ }
71
+
72
+
73
+ /***********************************************************************************************
74
+ * OPENAPI COMPONENTS
75
+ ***********************************************************************************************/
76
+ formatEndpoint(action) {
77
+ const endpoint = action.path.replace(/:([^/]+)/g, '{$1}');
78
+ return endpoint;
79
+ }
80
+
81
+ }
82
+
83
+
84
+ module.exports = {
85
+ ApiDocsGenerator
86
+ }
@@ -0,0 +1,64 @@
1
+ const fs = require('fs');
2
+ const { VidaServerController } = require('./serverController');
3
+
4
+
5
+ class ControllerImporter {
6
+ #controllerDirectories;
7
+ #controllerClasses;
8
+
9
+
10
+ constructor(controllerDirectories) {
11
+ if (!Array.isArray(controllerDirectories)) {
12
+ controllerDirectories = [controllerDirectories];
13
+ }
14
+ this.#controllerDirectories = controllerDirectories;
15
+ }
16
+
17
+
18
+ get controllerClasses() {
19
+ if (!this.#controllerClasses) {
20
+ this.#importControllerClasses();
21
+ }
22
+ return this.#controllerClasses;
23
+ }
24
+
25
+
26
+ #importControllerClasses() {
27
+ this.#controllerClasses = [];
28
+ this.#controllerDirectories.forEach(dir => {
29
+ this.#importDirectory(dir, dir);
30
+ });
31
+ }
32
+
33
+
34
+ #importDirectory(dir, topLevelDirectory) {
35
+ fs.readdirSync(dir).forEach((f => {
36
+ const filePath = `${dir}/${f}`;
37
+ if (fs.lstatSync(filePath).isDirectory()) {
38
+ this.#importDirectory(filePath, topLevelDirectory);
39
+ } else if (f.endsWith('.js')) {
40
+ this.#importFromFile(filePath, dir, topLevelDirectory);
41
+ }
42
+ }).bind(this));
43
+ }
44
+
45
+
46
+ #importFromFile(filePath, dir, topLevelDirectory) {
47
+ const exports = Object.values(require(filePath));
48
+ exports.forEach(_export => {
49
+ if (_export.prototype instanceof VidaServerController) {
50
+ const directoryPrefix = dir.replace(topLevelDirectory, '');
51
+ _export.directoryPrefix = directoryPrefix;
52
+ _export.autoLoadHelpers();
53
+ _export.autoLoadDocumentation();
54
+ this.#controllerClasses.push(_export);
55
+ }
56
+ });
57
+ }
58
+
59
+ }
60
+
61
+
62
+ module.exports = {
63
+ ControllerImporter
64
+ }
@@ -1,8 +1,10 @@
1
1
  const { VidaServer } = require('./server');
2
2
  const { VidaServerController } = require('./serverController');
3
3
  const ERRORS = require('./errors');
4
+ const { ApiDocsGenerator } = require('./apiDocsGenerator');
4
5
 
5
6
  module.exports = {
7
+ ApiDocsGenerator,
6
8
  ...ERRORS,
7
9
  VidaServer,
8
10
  VidaServerController,
@@ -1,5 +1,5 @@
1
+ const { ControllerImporter } = require('./controllerImporter');
1
2
  const express = require('express');
2
- const fs = require('fs');
3
3
  const httpLogger = require('pino-http')
4
4
  const { logger } = require('../logger');
5
5
  const mustacheExpress = require('mustache-express');
@@ -8,11 +8,6 @@ const responseTime = require('response-time');
8
8
  const IoServer = require("socket.io")
9
9
  const { Server } = require('http');
10
10
  const { SystemController } = require('./systemController');
11
- const { AuthorizationError,
12
- ForbiddenError,
13
- NotFoundError,
14
- ValidationError,
15
- VidaServerController } = require('./serverController');
16
11
 
17
12
 
18
13
  class VidaServer {
@@ -32,11 +27,12 @@ class VidaServer {
32
27
  }
33
28
 
34
29
 
35
- async listen() {
30
+ async listen(callback) {
36
31
  await this.registerControllers();
37
32
 
38
33
  this.#httpServer.listen(this.#port, this.#host, () => {
39
34
  this.logger.info(`Server is running on port ${this.#port}`);
35
+ if (callback) callback();
40
36
  });
41
37
  }
42
38
 
@@ -75,6 +71,7 @@ class VidaServer {
75
71
  ***********************************************************************************************/
76
72
  setupMiddleware() {
77
73
  this.use(this.jsonParsingMiddleware);
74
+ this.use(this.octetStreamParsingMiddleware);
78
75
  this.use(responseTime())
79
76
  this.use(express.static('public'))
80
77
  this.use(this.loggingMiddleware);
@@ -97,6 +94,16 @@ class VidaServer {
97
94
  }
98
95
 
99
96
 
97
+ get octetStreamParsingMiddleware() {
98
+ return express.raw({ type: 'application/octet-stream', limit: this.octetStreamLimit });
99
+ }
100
+
101
+
102
+ get octetStreamLimit() {
103
+ return '128mb';
104
+ }
105
+
106
+
100
107
  get loggingMiddleware() {
101
108
  return httpLogger({
102
109
  logger: this.middlewareLogger,
@@ -193,7 +200,6 @@ class VidaServer {
193
200
 
194
201
 
195
202
  #registerController(controllerCls) {
196
- controllerCls.autoLoadHelpers();
197
203
  controllerCls.actions.forEach((action => {
198
204
  this.#registerAction(action, controllerCls)
199
205
  }).bind(this));
@@ -215,6 +221,7 @@ class VidaServer {
215
221
  _put() { this.#expressServer.put(...arguments); }
216
222
  _delete() { this.#expressServer.delete(...arguments); }
217
223
  _patch() { this.#expressServer.patch(...arguments); }
224
+ _head() { this.#expressServer.head(...arguments); }
218
225
 
219
226
 
220
227
  requestHandler(action, controllerCls) {
@@ -232,41 +239,15 @@ class VidaServer {
232
239
 
233
240
 
234
241
  get controllerClasses() {
235
- if (this.#controllerClasses) return this.#controllerClasses;
236
- this.#controllerClasses = [];
237
-
238
- this.controllerDirectories.forEach(dir => {
239
- this.#importControllers(dir, dir);
240
- });
242
+ if (!this.#controllerClasses) {
243
+ const controllerImporter = new ControllerImporter(this.controllerDirectories);
244
+ this.#controllerClasses = controllerImporter.controllerClasses;
245
+ }
241
246
 
242
247
  return this.#controllerClasses;
243
248
  }
244
249
 
245
250
 
246
- #importControllers(dir, topLevelDirectory) {
247
- fs.readdirSync(dir).forEach((f => {
248
- const filePath = `${dir}/${f}`;
249
- if (fs.lstatSync(filePath).isDirectory()) {
250
- this.#importControllers(filePath, topLevelDirectory);
251
- } else if (f.endsWith('.js')) {
252
- this.#importControllersFromFile(filePath, dir, topLevelDirectory);
253
- }
254
- }).bind(this));
255
- }
256
-
257
-
258
- #importControllersFromFile(filePath, dir, topLevelDirectory) {
259
- const exports = Object.values(require(filePath));
260
- exports.forEach(_export => {
261
- if (_export.prototype instanceof VidaServerController) {
262
- const directoryPrefix = dir.replace(topLevelDirectory, '');
263
- _export.directoryPrefix = directoryPrefix;
264
- this.#controllerClasses.push(_export);
265
- }
266
- });
267
- }
268
-
269
-
270
251
  get controllerDirectories() {
271
252
  return [`${process.cwd()}/controllers`];
272
253
  }
@@ -291,6 +272,7 @@ class VidaServer {
291
272
  post() { this.handleDeprecatedRoutingCall('post', arguments); }
292
273
  put() { this.handleDeprecatedRoutingCall('put', arguments); }
293
274
  patch() { this.handleDeprecatedRoutingCall('patch', arguments); }
275
+ head() { this.handleDeprecatedRoutingCall('head', arguments); }
294
276
  delete() { this.handleDeprecatedRoutingCall('delete', arguments); }
295
277
 
296
278
 
@@ -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,7 +130,7 @@ 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
136
  this.statusCode = 500;
@@ -144,7 +147,8 @@ class VidaServerController {
144
147
  if (typeof body == 'string') {
145
148
  this._response.send(body);
146
149
  } else {
147
- body = await this.formatJSONBody(body, options);
150
+ body = await this.processJSONBody(body, options);
151
+ body = this.formatJSONBody(body, options);
148
152
  this._response.json(body);
149
153
  }
150
154
  this.#rendered = true;
@@ -191,24 +195,23 @@ class VidaServerController {
191
195
  }
192
196
 
193
197
 
194
- async formatJSONBody(body, options) {
195
- body = await this._formatJSONBody(body, options);
198
+ formatJSONBody(body, options) {
196
199
  if (options.standardize === false) return body;
197
200
  return {data: body, status: this.statusText};
198
201
  }
199
202
 
200
203
 
201
- async _formatJSONBody(body, options) {
204
+ async processJSONBody(body, options) {
202
205
  if (body === undefined) return null;
203
206
  if (!body || typeof body != 'object') return body;
204
207
 
205
208
  if (body.toApiResponse) {
206
209
  const formattedBody = await body.toApiResponse(options);
207
- return await this._formatJSONBody(formattedBody, options);
210
+ return await this.processJSONBody(formattedBody, options);
208
211
 
209
212
  } else if (Array.isArray(body)) {
210
213
  for (const idx in body) {
211
- body[idx] = await this._formatJSONBody(body[idx], options);
214
+ body[idx] = await this.processJSONBody(body[idx], options);
212
215
  }
213
216
 
214
217
  } else if (body.constructor == Date) {
@@ -216,7 +219,7 @@ class VidaServerController {
216
219
 
217
220
  } else {
218
221
  for (let [key, val] of Object.entries(body)) {
219
- body[key] = await this._formatJSONBody(val, options);
222
+ body[key] = await this.processJSONBody(val, options);
220
223
  }
221
224
  }
222
225
 
@@ -246,17 +249,7 @@ class VidaServerController {
246
249
  * HELPERS
247
250
  ***********************************************************************************************/
248
251
  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);
252
+ const { InstanceMethods, Accessors } = this._autoLoadModule(this.autoLoadHelperPath);
260
253
 
261
254
  if (InstanceMethods) Object.assign(this.prototype, InstanceMethods);
262
255
 
@@ -268,11 +261,34 @@ class VidaServerController {
268
261
  }
269
262
 
270
263
 
264
+ static autoLoadDocumentation() {
265
+ const docs = this._autoLoadModule(this.autoLoadDocumentationPath);
266
+ Object.assign(this, docs);
267
+ }
268
+
269
+
270
+ static _autoLoadModule(path) {
271
+ try {
272
+ return require(path);
273
+ } catch(err) {
274
+ const errorText = `Cannot find module '${path}'`;
275
+ if (!err.message.includes(errorText)) throw err;
276
+ }
277
+
278
+ return {};
279
+ }
280
+
281
+
271
282
  static get autoLoadHelperPath() {
272
283
  return `${process.cwd()}/lib/controllers${this.routePrefix}Controller.js`;
273
284
  }
274
285
 
275
286
 
287
+ static get autoLoadDocumentationPath() {
288
+ return `${process.cwd()}/lib/controllers/_docs${this.routePrefix}Controller.js`;
289
+ }
290
+
291
+
276
292
  /***********************************************************************************************
277
293
  * CALLBACKS
278
294
  ***********************************************************************************************/
@@ -280,6 +296,13 @@ class VidaServerController {
280
296
  setupCallbacks() {}
281
297
 
282
298
 
299
+ #applyCallbacks() {
300
+ // set up the callbacks when the instance is constructed and prevent it from being called again
301
+ this.setupCallbacks();
302
+ this.setupCallbacks = undefined;
303
+ }
304
+
305
+
283
306
  beforeCallback(callback, options={}) {
284
307
  this.#addCallback(callback, options, this.#beforeCallbacks);
285
308
  }
@@ -340,7 +363,7 @@ class VidaServerController {
340
363
  /***********************************************************************************************
341
364
  * VALIDATIONS
342
365
  ***********************************************************************************************/
343
- async validateParameters(validations) {
366
+ validateParameters(validations) {
344
367
  const errors = {};
345
368
  for (const [parameterName, parameterValidations] of Object.entries(validations)) {
346
369
  const parameterErrors = this.validateParameter(this.params[parameterName], parameterValidations);
@@ -353,8 +376,12 @@ class VidaServerController {
353
376
 
354
377
  validateParameter(value, parameterValidations) {
355
378
  const errors = [];
379
+
380
+ if (parameterValidations?.optional && value === undefined) return errors;
381
+
356
382
  for (const [validationType, validationOptions] of Object.entries(parameterValidations)) {
357
383
  const validationMethod = `validate${camelize(validationType)}`;
384
+ if (!this[validationMethod]) continue;
358
385
  const error = this[validationMethod](value, validationOptions);
359
386
  if (error) errors.push(error);
360
387
  }
@@ -370,10 +397,29 @@ class VidaServerController {
370
397
 
371
398
  validateIsInteger(value, options) {
372
399
  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) {
400
+ if (!/^-?\d+$/.test(value) || isNaN(num) || !Number.isInteger(num)) return 'must be an integer';
401
+
402
+ if (options?.gte !== undefined) {
375
403
  if (num < options.gte) return `must be greater than or equal to ${options.gte}`;
376
404
  }
405
+ if (options?.lte !== undefined) {
406
+ if (num > options.lte) return `must be less than or equal to ${options.lte}`;
407
+ }
408
+ }
409
+
410
+
411
+ validateIsString(value, options) {
412
+ if (typeof value !== 'string') return 'must be a string';
413
+
414
+ if (options?.length?.gte !== undefined) {
415
+ if (value.length < options.length.gte) return `must be greater than or equal to ${options.length.gte} characters`;
416
+ }
417
+ if (options?.length?.lte !== undefined) {
418
+ if (value.length > options.length.lte) return `must be fewer than or equal to ${options.length.lte} characters`;
419
+ }
420
+ if (options?.regex !== undefined) {
421
+ if (!options.regex.test(value)) return `must match ${options.regex}`;
422
+ }
377
423
  }
378
424
 
379
425
 
@@ -389,11 +435,6 @@ class VidaServerController {
389
435
  }
390
436
 
391
437
 
392
- validateRegex(value, regex) {
393
- if (!regex.test(value)) return `must match the pattern ${regex}`;
394
- }
395
-
396
-
397
438
  validateFunction(value, fnc) {
398
439
  const error = fnc(value);
399
440
  if (error) return error;
@@ -482,7 +523,56 @@ class VidaServerController {
482
523
 
483
524
 
484
525
  static get actionRegExp() {
485
- return new RegExp(/^(?<method>get|post|put|delete|patch)(?<path>[A-Z][a-zA-Z]+)$/);
526
+ return new RegExp(/^(?<method>get|post|put|delete|patch|head)(?<path>[A-Z][a-zA-Z]+)$/);
527
+ }
528
+
529
+
530
+ /***********************************************************************************************
531
+ * DOCUMENTATION
532
+ ***********************************************************************************************/
533
+ static parametersForAction(actionName) {
534
+ const methodName = this.parametersMethodForAction(actionName);
535
+ if (this[methodName]) return this[methodName]();
536
+ return null;
537
+ }
538
+
539
+
540
+ static parametersMethodForAction(actionName) {
541
+ return `parametersFor${camelize(actionName)}`;
542
+ }
543
+
544
+
545
+ static documentationForAction(actionName) {
546
+ const methodName = this.documentationMethodForAction(actionName);
547
+ if (this[methodName]) return this[methodName]();
548
+ return null;
549
+ }
550
+
551
+
552
+ static documentationMethodForAction(actionName) {
553
+ return `document${camelize(actionName)}`;
554
+ }
555
+
556
+
557
+ isAuthenticatedAction(actionName) {
558
+ const { options } = this.#beforeCallbacks.find(({ callback, options }) => callback == AUTH_CALLBACK_NAME)
559
+ return Boolean(options) && this.#shouldRunCallback(actionName, options);
560
+ }
561
+
562
+
563
+ static isAuthenticatedAction(actionName) {
564
+ const controller = new this();
565
+ return controller.isAuthenticatedAction(actionName);
566
+ }
567
+
568
+
569
+ static _authenticationDocumentation(actionName) {
570
+ if (!this.isAuthenticatedAction(actionName)) return undefined;
571
+ }
572
+
573
+
574
+ static authenticationDocumentation(actionName) {
575
+ return {apiKeyAuth: []};
486
576
  }
487
577
  }
488
578
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vida-global/core",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Core libraries for supporting Vida development",
5
5
  "author": "",
6
6
  "license": "ISC",
@@ -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
 
@@ -165,20 +165,20 @@ describe('VidaServerController', () => {
165
165
  expect(validator2).toHaveBeenCalledWith(value1, options2);
166
166
  });
167
167
 
168
- it ('throws a `ValidationError` when there\'s an error', async () => {
168
+ it ('throws a `ValidationError` when there\'s an error', () => {
169
169
  validator1.mockImplementation(() => error1);
170
- await expect(async () => {
171
- await controller.validateParameters(validations);
172
- }).rejects.toThrow(Errors.ValidationError);
170
+ expect(() => {
171
+ controller.validateParameters(validations);
172
+ }).toThrow(Errors.ValidationError);
173
173
  });
174
174
 
175
- it ('throws a `ValidationError` when there are multiple errors', async () => {
175
+ it ('throws a `ValidationError` when there are multiple errors', () => {
176
176
  validator1.mockImplementation(() => error1);
177
177
  validator2.mockImplementation(() => error2);
178
178
 
179
- await expect(async () => {
180
- await controller.validateParameters(validations);
181
- }).rejects.toThrow(Errors.ValidationError);
179
+ expect(() => {
180
+ controller.validateParameters(validations);
181
+ }).toThrow(Errors.ValidationError);
182
182
  });
183
183
 
184
184
  it ('sets nothing when there is no error', () => {
@@ -186,6 +186,34 @@ describe('VidaServerController', () => {
186
186
  expect(controller.render).toHaveBeenCalledTimes(0);
187
187
  expect(response.statusCode).toEqual(200);
188
188
  });
189
+
190
+ it ('ignores details where there is no validator', () => {
191
+ const validations = {
192
+ [paramName2]: {[validationType1]: options2, 'foo': true}
193
+ };
194
+ controller.validateParameters(validations);
195
+
196
+ expect(validator1).toHaveBeenCalledTimes(1);
197
+ expect(validator1).toHaveBeenCalledWith(value2, options2);
198
+ });
199
+
200
+ it ('does not error when optional parameters are not included', () => {
201
+ const validations = {foo: {isString: true, optional: true}};
202
+ expect(() => {
203
+ controller.validateParameters(validations);
204
+ }).not.toThrow();
205
+ });
206
+
207
+ it ('does error when optional parameters are invalid', () => {
208
+ const validations = {foo: {isString: {length: {gte: 2}}, optional: true}};
209
+ params.foo = '';
210
+
211
+ expect(() => {
212
+ controller.validateParameters(validations);
213
+ }).toThrow(Errors.ValidationError);
214
+
215
+ delete params.foo;
216
+ });
189
217
  });
190
218
 
191
219
 
@@ -230,6 +258,21 @@ describe('VidaServerController', () => {
230
258
  expect(result).toEqual('must be an integer');
231
259
  });
232
260
 
261
+ it ('returns an error when the value is null', () => {
262
+ const result = controller.validateIsInteger(null)
263
+ expect(result).toEqual('must be an integer');
264
+ });
265
+
266
+ it ('returns an error when the value is undefined', () => {
267
+ const result = controller.validateIsInteger(undefined)
268
+ expect(result).toEqual('must be an integer');
269
+ });
270
+
271
+ it ('returns an error when the value is an object', () => {
272
+ const result = controller.validateIsInteger({})
273
+ expect(result).toEqual('must be an integer');
274
+ });
275
+
233
276
  it ('returns an error when the number is less than the gte option', () => {
234
277
  const result = controller.validateIsInteger(1, {gte: 2});
235
278
  expect(result).toEqual('must be greater than or equal to 2');
@@ -244,6 +287,112 @@ describe('VidaServerController', () => {
244
287
  const result = controller.validateIsInteger(2, {gte: 2});
245
288
  expect(result).toEqual(undefined);
246
289
  });
290
+
291
+ it ('returns an error when the number is greater than the lte option', () => {
292
+ const result = controller.validateIsInteger(3, {lte: 2});
293
+ expect(result).toEqual('must be less than or equal to 2');
294
+ });
295
+
296
+ it ('returns undefined when the number is less than the lte option', () => {
297
+ const result = controller.validateIsInteger(1, {lte: 2});
298
+ expect(result).toEqual(undefined);
299
+ });
300
+
301
+ it ('returns undefined when the number is equal to the lte option', () => {
302
+ const result = controller.validateIsInteger(2, {lte: 2});
303
+ expect(result).toEqual(undefined);
304
+ });
305
+
306
+ it ('supports both the lte and gte options', () => {
307
+ let result = controller.validateIsInteger(1, {lte: 2, gte: 0});
308
+ expect(result).toEqual(undefined);
309
+
310
+ result = controller.validateIsInteger(-1, {lte: 2, gte: 0});
311
+ expect(result).toEqual('must be greater than or equal to 0');
312
+
313
+ result = controller.validateIsInteger(3, {lte: 2, gte: 0});
314
+ expect(result).toEqual('must be less than or equal to 2');
315
+ });
316
+ });
317
+
318
+
319
+ describe('VidaServerController.validateIsString', () => {
320
+ const controller = new FooController({}, {});
321
+ it ('returns undefined when there is an string', () => {
322
+ const result = controller.validateIsString('')
323
+ expect(result).toEqual(undefined);
324
+ });
325
+
326
+ it ('returns an error when there is a number', () => {
327
+ const result = controller.validateIsString(Math.random());
328
+ expect(result).toEqual('must be a string');
329
+ });
330
+
331
+ it ('returns an error when the value is null', () => {
332
+ const result = controller.validateIsString(null)
333
+ expect(result).toEqual('must be a string');
334
+ });
335
+
336
+ it ('returns an error when the value is undefined', () => {
337
+ const result = controller.validateIsString(undefined)
338
+ expect(result).toEqual('must be a string');
339
+ });
340
+
341
+ it ('returns an error when the value is an object', () => {
342
+ const result = controller.validateIsString({})
343
+ expect(result).toEqual('must be a string');
344
+ });
345
+
346
+ it ('returns an error when the length is fewer than the gte option', () => {
347
+ const result = controller.validateIsString('f', {length: {gte: 2}});
348
+ expect(result).toEqual('must be greater than or equal to 2 characters');
349
+ });
350
+
351
+ it ('returns undefined when the length is greater than the gte option', () => {
352
+ const result = controller.validateIsString('foo', {length: {gte: 2}});
353
+ expect(result).toEqual(undefined);
354
+ });
355
+
356
+ it ('returns undefined when the length is equal to the gte option', () => {
357
+ const result = controller.validateIsString('fo', {length: {gte: 2}});
358
+ expect(result).toEqual(undefined);
359
+ });
360
+
361
+ it ('returns an error when the length is greater than the lte option', () => {
362
+ const result = controller.validateIsString('foo', {length: {lte: 2}});
363
+ expect(result).toEqual('must be fewer than or equal to 2 characters');
364
+ });
365
+
366
+ it ('returns undefined when the length is fewer than the lte option', () => {
367
+ const result = controller.validateIsString('f', {length: {lte: 2}});
368
+ expect(result).toEqual(undefined);
369
+ });
370
+
371
+ it ('returns undefined when the length is equal to the lte option', () => {
372
+ const result = controller.validateIsString('fo', {length: {lte: 2}});
373
+ expect(result).toEqual(undefined);
374
+ });
375
+
376
+ it ('supports both the lte and gte options', () => {
377
+ let result = controller.validateIsString('fo', {length: {lte: 3, gte: 1}});
378
+ expect(result).toEqual(undefined);
379
+
380
+ result = controller.validateIsString('', {length: {lte: 3, gte: 1}});
381
+ expect(result).toEqual('must be greater than or equal to 1 characters');
382
+
383
+ result = controller.validateIsString('foobar', {length: {lte: 3, gte: 1}});
384
+ expect(result).toEqual('must be fewer than or equal to 3 characters');
385
+ });
386
+
387
+ it ('returns undefined when the value matches the pattern', () => {
388
+ const result = controller.validateIsString('foo', {regex: /oo/})
389
+ expect(result).toBe(undefined);
390
+ });
391
+
392
+ it ('returns an error when the value does not match the pattern', () => {
393
+ const result = controller.validateIsString('bar', {regex: /oo/})
394
+ expect(result).toBe(`must match /oo/`);
395
+ });
247
396
  });
248
397
 
249
398
 
@@ -321,36 +470,6 @@ describe('VidaServerController', () => {
321
470
  });
322
471
 
323
472
 
324
- describe('VidaServerController.validateRegex', () => {
325
- const controller = new FooController({}, {});
326
-
327
- it ('returns undefined when the value matches the pattern', () => {
328
- const result = controller.validateRegex('foo', /oo/)
329
- expect(result).toBe(undefined);
330
- });
331
-
332
- it ('returns an error when the value does not match the pattern', () => {
333
- const result = controller.validateRegex('bar', /oo/)
334
- expect(result).toBe(`must match the pattern /oo/`);
335
- });
336
-
337
- it ('returns an error when the value is null', () => {
338
- const result = controller.validateRegex(null, /oo/)
339
- expect(result).toBe(`must match the pattern /oo/`);
340
- });
341
-
342
- it ('returns an error when the value is undefined', () => {
343
- const result = controller.validateRegex(undefined, /oo/)
344
- expect(result).toBe(`must match the pattern /oo/`);
345
- });
346
-
347
- it ('returns an error when the value is an object', () => {
348
- const result = controller.validateRegex({}, /oo/)
349
- expect(result).toBe(`must match the pattern /oo/`);
350
- });
351
- });
352
-
353
-
354
473
  describe('VidaServerController#performRequest', () => {
355
474
  const errorHandlers = [
356
475
  ['renderUnauthorizedResponse', 'AuthorizationError'],
@@ -371,7 +490,7 @@ describe('VidaServerController', () => {
371
490
 
372
491
  expect(controller[handlerName]).toHaveBeenCalledTimes(1);
373
492
  if (handlerName == 'renderErrors') {
374
- expect(controller[handlerName]).toHaveBeenCalledWith(message, error.errors);
493
+ expect(controller[handlerName]).toHaveBeenCalledWith(message, error.fields);
375
494
  } else {
376
495
  expect(controller[handlerName]).toHaveBeenCalledWith(message);
377
496
  }
@@ -594,4 +713,21 @@ describe('VidaServerController', () => {
594
713
  });
595
714
  });
596
715
  });
716
+
717
+
718
+ describe('ServerController#setupCallbacks', () => {
719
+ it ('is only called once', () => {
720
+ class CallbacksTestController extends VidaServerController {}
721
+ CallbacksTestController.prototype.setupCallbacks = jest.fn();
722
+ const controller = new CallbacksTestController({}, {});
723
+
724
+ expect(CallbacksTestController.prototype.setupCallbacks).toHaveBeenCalledTimes(1);
725
+ expect(controller.setupCallbacks).toBe(undefined);
726
+
727
+ });
728
+ });
729
+
730
+
731
+ describe('Request Properties', () => {
732
+ });
597
733
  });