@vida-global/core 1.2.5 → 1.3.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.
Files changed (46) hide show
  1. package/.github/workflows/npm-test.yml +24 -0
  2. package/index.js +1 -1
  3. package/lib/{active_record → activeRecord}/README.md +3 -3
  4. package/lib/{active_record → activeRecord}/baseRecord.js +11 -2
  5. package/lib/http/README.md +2 -2
  6. package/lib/server/README.md +181 -20
  7. package/lib/server/errors.js +28 -0
  8. package/lib/server/index.js +3 -3
  9. package/lib/server/server.js +7 -53
  10. package/lib/server/serverController.js +147 -20
  11. package/package.json +1 -1
  12. package/scripts/{active_record → activeRecord}/migrate.js +1 -1
  13. package/test/{active_record → activeRecord}/baseRecord.test.js +46 -90
  14. package/test/activeRecord/db/connection.test.js +149 -0
  15. package/test/activeRecord/db/connectionConfiguration.test.js +128 -0
  16. package/test/activeRecord/db/migrator.test.js +144 -0
  17. package/test/activeRecord/db/queryInterface.test.js +48 -0
  18. package/test/activeRecord/helpers/baseRecord.js +32 -0
  19. package/test/activeRecord/helpers/baseRecordMocks.js +59 -0
  20. package/test/activeRecord/helpers/connection.js +28 -0
  21. package/test/activeRecord/helpers/connectionConfiguration.js +32 -0
  22. package/test/activeRecord/helpers/fixtures.js +39 -0
  23. package/test/activeRecord/helpers/migrator.js +78 -0
  24. package/test/activeRecord/helpers/queryInterface.js +29 -0
  25. package/test/http/client.test.js +61 -239
  26. package/test/http/error.test.js +23 -47
  27. package/test/http/helpers/client.js +80 -0
  28. package/test/http/helpers/error.js +31 -0
  29. package/test/server/helpers/autoload/TmpWithHelpersController.js +17 -0
  30. package/test/server/helpers/serverController.js +13 -0
  31. package/test/server/serverController.test.js +319 -6
  32. package/test/active_record/db/connection.test.js +0 -221
  33. package/test/active_record/db/connectionConfiguration.test.js +0 -184
  34. package/test/active_record/db/migrator.test.js +0 -266
  35. package/test/active_record/db/queryInterface.test.js +0 -66
  36. /package/lib/{active_record → activeRecord}/db/connection.js +0 -0
  37. /package/lib/{active_record → activeRecord}/db/connectionConfiguration.js +0 -0
  38. /package/lib/{active_record → activeRecord}/db/importSchema.js +0 -0
  39. /package/lib/{active_record → activeRecord}/db/migration.js +0 -0
  40. /package/lib/{active_record → activeRecord}/db/migrationTemplate.js +0 -0
  41. /package/lib/{active_record → activeRecord}/db/migrationVersion.js +0 -0
  42. /package/lib/{active_record → activeRecord}/db/migrator.js +0 -0
  43. /package/lib/{active_record → activeRecord}/db/queryInterface.js +0 -0
  44. /package/lib/{active_record → activeRecord}/db/schema.js +0 -0
  45. /package/lib/{active_record → activeRecord}/index.js +0 -0
  46. /package/lib/{active_record → activeRecord}/utils.js +0 -0
@@ -0,0 +1,24 @@
1
+ name: npm test
2
+
3
+ on:
4
+ pull_request:
5
+
6
+ jobs:
7
+ test:
8
+ runs-on: ubuntu-latest
9
+
10
+ steps:
11
+ - name: Checkout code
12
+ uses: actions/checkout@v4
13
+
14
+ - name: Setup Node
15
+ uses: actions/setup-node@v4
16
+ with:
17
+ node-version: 20
18
+ cache: 'npm'
19
+
20
+ - name: Install dependencies
21
+ run: npm ci
22
+
23
+ - name: Run tests
24
+ run: npm test
package/index.js CHANGED
@@ -3,7 +3,7 @@ const { AuthorizationError,
3
3
  VidaServer,
4
4
  VidaServerController } = require('./lib/server');
5
5
  const { logger } = require('./lib/logger');
6
- const ActiveRecord = require('./lib/active_record');
6
+ const ActiveRecord = require('./lib/activeRecord');
7
7
  const { redisClientFactory } = require('./lib/redis');
8
8
 
9
9
 
@@ -82,9 +82,9 @@ development: {
82
82
  # Migrations
83
83
  To prepare your project to run database migrations, add the following lines to the `scripts` section of your `package.json`:
84
84
  ```
85
- "db:create_migration": "node node_modules/@vida-global/core/scripts/active_record/migrate.js create_migration",
86
- "db:migrate": "node node_modules/@vida-global/core/scripts/active_record/migrate.js migrate",
87
- "db:rollback": "node node_modules/@vida-global/core/scripts/active_record/migrate.js rollback"
85
+ "db:create_migration": "node node_modules/@vida-global/core/scripts/activeRecord/migrate.js create_migration",
86
+ "db:migrate": "node node_modules/@vida-global/core/scripts/activeRecord/migrate.js migrate",
87
+ "db:rollback": "node node_modules/@vida-global/core/scripts/activeRecord/migrate.js rollback"
88
88
  ```
89
89
 
90
90
  To generate a new migration file, run: `npm run db:create_migration createUsers`. This will automatically generate a migration file. Your migration can create tables, create indexes, and add, remove, or update columns.
@@ -98,7 +98,8 @@ class BaseRecord extends Model {
98
98
 
99
99
  static initializeHooks() {
100
100
  if (this.isCacheable) {
101
- this.addHook('afterSave', this._afterSaveCacheHook);
101
+ this.addHook('afterSave', this._afterSaveCacheHook);
102
+ this.addHook('afterDestroy', this._afterDestroyCacheHook);
102
103
  }
103
104
  }
104
105
 
@@ -202,7 +203,9 @@ class BaseRecord extends Model {
202
203
  }
203
204
 
204
205
  const client = await this._getRedisClient();
205
- await client.mSet(toCache);
206
+ if (Object.keys(toCache).length) {
207
+ await client.mSet(toCache);
208
+ }
206
209
  this.debugLog('Cache Set', Object.keys(fetchedData).join(', '));
207
210
  }
208
211
 
@@ -273,6 +276,12 @@ class BaseRecord extends Model {
273
276
  }
274
277
 
275
278
 
279
+ static async _afterDestroyCacheHook(record, options) {
280
+ await record.clearSelfCache();
281
+ await record.updateCache(options);
282
+ }
283
+
284
+
276
285
  /***********************************************************************************************
277
286
  * MISC
278
287
  ***********************************************************************************************/
@@ -26,7 +26,7 @@ const client = new MyApiClient(true, '123abctoken');
26
26
  const requestParameters = {baz: 1, ban: 2};
27
27
  const response = await client.get("/foo/bar", {requestParameters});
28
28
 
29
- const requestBody = {baz: 1, ban: 2};
30
- const response = await client.post("/foo/bar", {requestBody});
29
+ const body = {baz: 1, ban: 2};
30
+ const response = await client.post("/foo/bar", { body });
31
31
  ```
32
32
 
@@ -1,37 +1,198 @@
1
1
  # VidaServer
2
2
  The `VidaServer` is a general purpose wrapper around an `express` server. It initializes standard middleware that we want running on all Vida application servers and leaves hooks for adding additional middleware on subclasses for specific use cases.
3
- The `VidaServer` works in conjunction with the `VidaServerController`. When a `VidaServer` begins listening, it searches its directory structure (as defined in the `controllerDirectories` getter) for subclasses of `VidaServerController`. It then creates routes based on all methods of the controller that are prefixed with `get`, `post`, `put`, `patch`, or `delete`.
3
+
4
+ ## Controllers
5
+ The `VidaServer` works in conjunction with the `VidaServerController`. When a `VidaServer` begins listening, it searches its directory structure (as defined in the `controllerDirectories` getter) for subclasses of `VidaServerController` and autoloads their paths.
6
+
7
+
8
+ ## Routing
9
+ When a controller is auto loaded, it generates routes based on all methods of the controller that are prefixed with `get`, `post`, `put`, `patch`, or `delete`.
4
10
  The paths for these routes are defined by the directory structure of the controller, the name of the controller, and the name of the method. For example, a method `getFoo` defined on a `BarController` in the directory `/controllers/baz`, will create the route `GET /baz/bar/foo`. “Index” will yield an empty path (e.g. `getIndex` in the previously described controller generates `GET /baz/bar`). However, the default paths can be overridden by defining the `routes` getter.
5
11
  Each controller method will have direct access to request and response variables and a logger. Calling `render` will set the response body. It accepts either a string or a JSON object.
6
12
  ```
13
+ // defined in the /api/v2 directory
7
14
  class UsersController extends ServerController {
8
- // GET /users
9
- getIndex() {
10
- const users = getUsers(this.query.pageNumber, this.query.pageSize);
11
- this.render(users.toJson());
15
+
16
+ // GET /api/v2/users
17
+ getIndex() {}
18
+
19
+ // POST /api/v2/user/:id
20
+ postRecord() {
21
+ const user = User.find(this.params.id);
12
22
  }
13
23
 
14
- // PUT /users/foo
15
- putFoo() {
16
- this.logger.info("Putting Foo");
17
- this.render("hello world");
24
+ // PUT /api/v2/users/foo
25
+ putFoo() {}
26
+
27
+ // Delete /foo/bar/baz
28
+ deleteSomething() {}
29
+
30
+ static get routes() {
31
+ return {deleteSomething: '/foo/bar/baz'};
18
32
  }
33
+ }
34
+ ```
35
+
36
+
37
+ ## Helpers 🚨IMPORTANT🚨
38
+ Controller actions should be kept slim with most of the logic in helper files. Helper files that follow the same directory structure as the controller, but in the `/lib` directory will be auto loaded and can add instance methods and custom accessors.
39
+ ```
40
+ // ./controllers/api/v2/fooController.js
41
+ class FooController extends ServerController {
42
+ async getBar() {
43
+ await this.validateParameters({playerNumber: {isInteger: true}});
44
+ return this.doTheThing();
45
+ }
46
+ }
19
47
 
20
- // POST /user/:id
21
- postUpdateUser() {
22
- const user = getUser(this.params.id);
23
- if (!user) {
24
- this.status = 404;
25
- this.render({error: "No user found"});
26
- return;
48
+ // ./lib/controllers/api/v2/fooController.js
49
+ const InstanceMethods = {
50
+ doTheThing() {
51
+ const user = this.getTheThing();
52
+ user.name = this.playerName;
53
+ return user;
54
+ },
55
+
56
+ getTheThing() {
57
+ return new User();
58
+ }
59
+ }
60
+
61
+ const Accessors = {
62
+ playerName: {
63
+ get() {
64
+ return `Player ${this.params.playerNumber}`;
27
65
  }
28
- user.update(this.body.user);
29
- this.render({success: true});
30
66
  }
67
+ }
31
68
 
32
- static get routes() {
33
- return {postUpdateUser: '/user/:id'};
69
+
70
+ module.exports = {
71
+ InstanceMethods,
72
+ Accessors
73
+ }
74
+ ```
75
+
76
+
77
+ ## Request Properties
78
+ Within an action, the controller has access to:
79
+ - `this.params` includes:
80
+ - any parameters from the route (e.g. `/foo/:bar/:baz` would provide `this.params.bar` and `this.params.baz`)
81
+ - any values from a JSON formatted request body
82
+ - any URL query parameters
83
+ - `this.requestHeaders` the headers sent with the request
84
+ - `this.responseHeaders` the headers to be sent with the response (can be updated)
85
+ - `this.contentType`
86
+
87
+
88
+ ## Rendering a response
89
+ By default, the controller will render the value returned from the action. It recursively searches the result for any objects with a `toApiResponse` and renders that. For example...
90
+ ```
91
+ class User {
92
+ toApiResponse(opts) {
93
+ const data = {name: this.name, email: this.email}
94
+ if (opts.includeTitle) data.title = this.title;
95
+ return data;
34
96
  }
35
97
  }
98
+
99
+ async getFoo() {
100
+ const user1 = new User("Bruce", "bwayne@wayne-enterprises.inc");
101
+ const user2 = new User("Babs", "bgordon@wayne-enterprises.inc");
102
+ const response = {bar: 1, baz: [user1, user2]};
103
+ return response;
104
+ }
105
+ ```
106
+ ...will generate the response
107
+ ```
108
+ {data: {bar: 1,
109
+ baz: [
110
+ {name: "Bruce", email: "bwayne@wayne-enterprises.inc"},
111
+ {name: "Babs", email: "bgordon@wayne-enterprises.inc"],
112
+ ]
113
+ }, status: "ok"}
114
+ ```
115
+
116
+ Additional options can be passed to `toApiResponse` by explicitly calling `render`
117
+ ```
118
+ await this.render(response, {includeTitle: true});
119
+ ```
120
+
121
+
122
+ ## Error handling
123
+ There is no need to set `statusCode` directly as there are helper methods for all statuses.
124
+ HTTP 400
125
+ ```
126
+ await this.renderErrors("Something bad happened", {password: ['must be > 8 characters', 'must include numbers']})
127
+ // renders {data: {errors: {message: "Soemthing bad happened", fields: {password: [...]}}}, status: "bad request"}
128
+
129
+ await this.renderErrors("Something bad happened"})
130
+ // renders {data: {errors: {message: "Soemthing bad happened"}}, status: "bad request"}
131
+
132
+ await this.renderErrors({password: ['must be > 8 characters', 'must include numbers']}
133
+ // renders {data: {errors: {fields: {password: [...]}}}, status: "bad request"}
134
+ ```
135
+ HTTP 401
136
+ ```
137
+ await this.renderUnauthorizedResponse("Nope")
138
+ // renders {data: {message: "Nope"}, status: "unauthorized"}
139
+ ```
140
+ HTTP 403
141
+ ```
142
+ await this.renderForbiddenResponse("You shall not pass")
143
+ // renders {data: {message: "You shall not pass"}, status: "forbidden"}
144
+ ```
145
+ HTTP 404
146
+ ```
147
+ await this.renderNotFoundResponse("Whoops")
148
+ // renders {data: {message: "Whoops"}, status: "not found"}
149
+ ```
150
+ 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
+
152
+
153
+ ## Callbacks
154
+ Callbacks are methods that will run before or after an action has performed. They can be used for authentication, action setup, etc. If a callback returns false, execution is stopped and no other callbacks or the action are run.
155
+ ```
156
+ // This will run on all actions except for getIndex and will halt execution if the environment is production (e.g. development routes)
157
+ this.beforeCallback(() => process.env.NODE_ENV != 'production', {except: 'getIndex'});
158
+
159
+ // This will run an instance method, `setUpSomeStuff` before every action
160
+ this.beforeCallback('setUpSomeStuff');
161
+
162
+ // This will run an instance method, 'cleanupRequest', after `getFoo` and `getBar`
163
+ this.afterCallback('cleanupRequest', {only: ['getFoo', 'getBar']})
164
+ ```
165
+
166
+ ## Validations
167
+ The `validateParameters` method can be called to automatically validate parameters based on given criteria. If validation fails, execution is stopped and a response is sent with a 400 code and body `{data: {errors: {field1: [errorMsg]}}, status: "bad request"}`
168
+ ```
169
+ async getFoo() {
170
+ // validates that the pageSize parameter is an integer greater than or equal to one
171
+ await this.validateParameters({pageSize: {isInteger: true, gte: 1}});
172
+ }
173
+
174
+ async postBar() {
175
+ await this.validateParameters(this.barValidations);
176
+ }
177
+
178
+ get barValidations() {
179
+ return {
180
+ name: {presence: true},
181
+ role: {isEnum: {enums: ['CEO', 'COO', 'other']}},
182
+ email: {regex: /\w@\w\.com/},
183
+ title: {function: this.titleUniqueness}
184
+ }
185
+ }
186
+
187
+ titleUniqueness(title) {
188
+ if (User.findByTitle(title)) return 'must be unique';
189
+ }
36
190
  ```
37
191
 
192
+ ### Supported Validations
193
+ - `{presence: true}`
194
+ - `{isInteger: true}`, `{isInteger: {gte: 0, lte: 100}}`
195
+ - `{isDateTime}`
196
+ - `{isEnum: {enums: [a, b, c]}}`
197
+ - `{regex: /foo/}`
198
+ - `{function: someFunction}` If the function returns a string, that is considered an error and returned as the validation message
@@ -0,0 +1,28 @@
1
+ class ServerError extends Error {
2
+ #message;
3
+ constructor(message) {
4
+ super();
5
+ this.#message = message;
6
+ }
7
+ get message() { return this.#message; };
8
+ }
9
+ class AuthorizationError extends ServerError {}
10
+ class ForbiddenError extends ServerError {}
11
+ class NotFoundError extends ServerError {}
12
+ class ValidationError extends ServerError {
13
+ #fields;
14
+ constructor(message, fields) {
15
+ super(message);
16
+ this.#fields = fields;
17
+ }
18
+ get fields() { return structuredClone(this.#fields) };
19
+ }
20
+
21
+
22
+ module.exports = {
23
+ AuthorizationError,
24
+ ForbiddenError,
25
+ NotFoundError,
26
+ ServerError,
27
+ ValidationError
28
+ }
@@ -1,9 +1,9 @@
1
1
  const { VidaServer } = require('./server');
2
- const { AuthorizationError,
3
- VidaServerController } = require('./serverController');
2
+ const { VidaServerController } = require('./serverController');
3
+ const ERRORS = require('./errors');
4
4
 
5
5
  module.exports = {
6
- AuthorizationError,
6
+ ...ERRORS,
7
7
  VidaServer,
8
8
  VidaServerController,
9
9
  }
@@ -9,6 +9,8 @@ const IoServer = require("socket.io")
9
9
  const { Server } = require('http');
10
10
  const { SystemController } = require('./systemController');
11
11
  const { AuthorizationError,
12
+ ForbiddenError,
13
+ NotFoundError,
12
14
  ValidationError,
13
15
  VidaServerController } = require('./serverController');
14
16
 
@@ -191,6 +193,7 @@ class VidaServer {
191
193
 
192
194
 
193
195
  #registerController(controllerCls) {
196
+ controllerCls.autoLoadHelpers();
194
197
  controllerCls.actions.forEach((action => {
195
198
  this.#registerAction(action, controllerCls)
196
199
  }).bind(this));
@@ -200,7 +203,9 @@ class VidaServer {
200
203
  #registerAction(action, controllerCls) {
201
204
  const method = action.method.toLowerCase();
202
205
  const requestHandler = this.requestHandler(action.action, controllerCls)
203
- logger.info(`ROUTE: ${method.toUpperCase().padEnd(6)} ${action.path}`);
206
+ if (process.env.NODE_ENV != 'test') {
207
+ logger.info(`ROUTE: ${method.toUpperCase().padEnd(6)} ${action.path}`);
208
+ }
204
209
  this['_'+method](action.path, requestHandler);
205
210
  }
206
211
 
@@ -216,62 +221,11 @@ class VidaServer {
216
221
  return async function(request, response) {
217
222
  if (process.env.NODE_ENV != 'test') this.logger.info(`${controllerCls.name}#${action}`);
218
223
  const controllerInstance = this.buildController(controllerCls, request, response);
219
- await controllerInstance.setupRequestState();
220
-
221
- if (controllerInstance.rendered) {
222
- response.end();
223
- return;
224
- }
225
-
226
- controllerInstance.setupCallbacks();
227
-
228
- const responseBody = await this.performRequest(controllerInstance, action);
229
-
230
- if (!controllerInstance.rendered) {
231
- await controllerInstance.render(responseBody || {});
232
- }
233
-
234
- response.end();
224
+ await controllerInstance.performRequest(action);
235
225
  }.bind(this);
236
226
  }
237
227
 
238
228
 
239
- async performRequest(controllerInstance, action) {
240
- try {
241
- return await this._performRequest(controllerInstance, action);
242
- } catch(err) {
243
- await this.handleError(err, controllerInstance);
244
- }
245
- }
246
-
247
-
248
- async handleError(err, controllerInstance) {
249
- if (err.constructor == AuthorizationError) {
250
- await controllerInstance.renderUnauthorizedResponse();
251
- return;
252
-
253
- } else if (err.constructor == ValidationError) {
254
- await controllerInstance.renderErrors(err.errors);
255
- return;
256
-
257
- } else {
258
- controllerInstance.statusCode = 500;
259
- await controllerInstance.render({error: err.message});
260
- }
261
-
262
- if (process.env.NODE_ENV == 'development') console.log(err);
263
- }
264
-
265
-
266
- async _performRequest(controllerInstance, action) {
267
- if (await controllerInstance.runBeforeCallbacks(action) === false) return false;
268
- const responseBody = await controllerInstance[action]();
269
- if (await controllerInstance.runAfterCallbacks(action) === false) return false;
270
-
271
- return responseBody;
272
- }
273
-
274
-
275
229
  buildController(controllerCls, request, response) {
276
230
  return new controllerCls(request, response);
277
231
  }
@@ -1,5 +1,10 @@
1
1
  const { logger } = require('../logger');
2
2
  const { camelize, singularize } = require('inflection');
3
+ const{ AuthorizationError,
4
+ ForbiddenError,
5
+ NotFoundError,
6
+ ServerError,
7
+ ValidationError } = require('./errors');
3
8
 
4
9
 
5
10
  class VidaServerController {
@@ -70,6 +75,71 @@ class VidaServerController {
70
75
  }
71
76
 
72
77
 
78
+ /***********************************************************************************************
79
+ * PERFORM ACTION
80
+ ***********************************************************************************************/
81
+ async performRequest(action) {
82
+ try {
83
+ await this.#performRequest(action)
84
+ } catch(err) {
85
+ await this.#handleError(err);
86
+ }
87
+ }
88
+
89
+
90
+ async #performRequest(action) {
91
+ await this.setupRequestState();
92
+
93
+ if (this.rendered) {
94
+ this.#response.end();
95
+ return;
96
+ }
97
+
98
+ this.setupCallbacks();
99
+
100
+ const responseBody = await this.#performAction(action);
101
+
102
+ if (!this.rendered) {
103
+ await this.render(responseBody || {});
104
+ }
105
+
106
+ this.#response.end();
107
+ }
108
+
109
+
110
+ async #performAction(action) {
111
+ if (await this.runBeforeCallbacks(action) === false) return false;
112
+ const responseBody = await this[action]();
113
+ if (await this.runAfterCallbacks(action) === false) return false;
114
+
115
+ return responseBody;
116
+ }
117
+
118
+
119
+ async #handleError(err) {
120
+ if (err instanceof AuthorizationError) {
121
+ await this.renderUnauthorizedResponse(err.message);
122
+
123
+ } else if (err instanceof ForbiddenError) {
124
+ await this.renderForbiddenResponse(err.message);
125
+
126
+ } else if (err instanceof NotFoundError) {
127
+ await this.renderNotFoundResponse(err.message);
128
+
129
+ } else if (err instanceof ValidationError) {
130
+ await this.renderErrors(err.message, err.errors);
131
+
132
+ } else {
133
+ this.statusCode = 500;
134
+ await this.render({error: err.message});
135
+ if (process.env.NODE_ENV == 'development') console.log(err);
136
+ }
137
+ }
138
+
139
+
140
+ /***********************************************************************************************
141
+ * RESPONSE RENDERING
142
+ ***********************************************************************************************/
73
143
  async render(body, options={}) {
74
144
  if (typeof body == 'string') {
75
145
  this._response.send(body);
@@ -81,6 +151,46 @@ class VidaServerController {
81
151
  }
82
152
 
83
153
 
154
+ async renderErrors(message, fields) {
155
+ this.statusCode = 400;
156
+
157
+ const errors = {message: null, fields: {}};
158
+
159
+ if (message && !fields && typeof message == 'object') {
160
+ fields = message;
161
+ message = null;
162
+ }
163
+
164
+ if (message) errors.message = message;
165
+ if (fields) {
166
+ for (const [field, values] of Object.entries(fields)) {
167
+ if (!Array.isArray(values)) fields[field] = [values];
168
+ }
169
+ errors.fields = fields;
170
+ }
171
+
172
+ await this.render({ errors });
173
+ }
174
+
175
+
176
+ async renderNotFoundResponse(message=null) {
177
+ this.statusCode = 404;
178
+ await this.render({ message });
179
+ }
180
+
181
+
182
+ async renderUnauthorizedResponse(message=null) {
183
+ this.statusCode = 401;
184
+ await this.render({ message });
185
+ }
186
+
187
+
188
+ async renderForbiddenResponse(message=null) {
189
+ this.statusCode = 403;
190
+ await this.render({ message });
191
+ }
192
+
193
+
84
194
  async formatJSONBody(body, options) {
85
195
  body = await this._formatJSONBody(body, options);
86
196
  if (options.standardize === false) return body;
@@ -122,6 +232,8 @@ class VidaServerController {
122
232
  return 'bad request'
123
233
  case 401:
124
234
  return 'unauthorized';
235
+ case 403:
236
+ return 'forbidden';
125
237
  case 404:
126
238
  return 'not found';
127
239
  case 500:
@@ -130,21 +242,34 @@ class VidaServerController {
130
242
  }
131
243
 
132
244
 
133
- async renderErrors(errors) {
134
- this.statusCode = 400;
135
- await this.render({ errors });
245
+ /***********************************************************************************************
246
+ * HELPERS
247
+ ***********************************************************************************************/
248
+ 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
+ }
136
255
  }
137
256
 
138
257
 
139
- async renderNotFoundResponse() {
140
- this.statusCode = 404;
141
- await this.render();
258
+ static _autoLoadHelpers() {
259
+ const { InstanceMethods, Accessors } = require(this.autoLoadHelperPath);
260
+
261
+ if (InstanceMethods) Object.assign(this.prototype, InstanceMethods);
262
+
263
+ if (Accessors) {
264
+ for (const [attrName, attrAccessors] of Object.entries(Accessors)) {
265
+ Object.defineProperty(this.prototype, attrName, attrAccessors);
266
+ }
267
+ }
142
268
  }
143
269
 
144
270
 
145
- async renderUnauthorizedResponse() {
146
- this.statusCode = 401;
147
- await this.render();
271
+ static get autoLoadHelperPath() {
272
+ return `${process.cwd()}/lib/controllers${this.routePrefix}Controller.js`;
148
273
  }
149
274
 
150
275
 
@@ -222,7 +347,7 @@ class VidaServerController {
222
347
  if (parameterErrors.length) errors[parameterName] = parameterErrors;
223
348
  }
224
349
 
225
- if (Object.keys(errors).length) throw new ValidationError(errors);
350
+ if (Object.keys(errors).length) throw new ValidationError('', errors);
226
351
  }
227
352
 
228
353
 
@@ -264,6 +389,11 @@ class VidaServerController {
264
389
  }
265
390
 
266
391
 
392
+ validateRegex(value, regex) {
393
+ if (!regex.test(value)) return `must match the pattern ${regex}`;
394
+ }
395
+
396
+
267
397
  validateFunction(value, fnc) {
268
398
  const error = fnc(value);
269
399
  if (error) return error;
@@ -272,6 +402,7 @@ class VidaServerController {
272
402
 
273
403
  /***********************************************************************************************
274
404
  * ACTION SETUP
405
+ ************************************************************************************************
275
406
  * The controller looks for any methods that fit the pattern of `{httpMethod}ActionName`
276
407
  * (e.g. getFooBar) and prepares server routes for them. By default, a method `getFooBar` on a
277
408
  * `BazController` would have an endpoint of `GET /baz/fooBar`. The server will also prepend
@@ -356,19 +487,15 @@ class VidaServerController {
356
487
  }
357
488
 
358
489
 
359
- class AuthorizationError extends Error {}
360
- class ValidationError extends Error {
361
- #errors;
362
- constructor(errors) {
363
- super();
364
- this.#errors = errors;
365
- }
366
- get errors() { return structuredClone(this.#errors) };
490
+ VidaServerController.prototype.Errors = {
491
+ Authorization: AuthorizationError,
492
+ Forbidden: ForbiddenError,
493
+ NotFound: NotFoundError,
494
+ Server: ServerError,
495
+ Validation: ValidationError
367
496
  }
368
497
 
369
498
 
370
499
  module.exports = {
371
- AuthorizationError,
372
- ValidationError,
373
500
  VidaServerController
374
501
  };