@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.
- package/.github/workflows/npm-test.yml +24 -0
- package/index.js +1 -1
- package/lib/{active_record → activeRecord}/README.md +3 -3
- package/lib/{active_record → activeRecord}/baseRecord.js +11 -2
- package/lib/http/README.md +2 -2
- package/lib/server/README.md +181 -20
- package/lib/server/errors.js +28 -0
- package/lib/server/index.js +3 -3
- package/lib/server/server.js +7 -53
- package/lib/server/serverController.js +147 -20
- package/package.json +1 -1
- package/scripts/{active_record → activeRecord}/migrate.js +1 -1
- package/test/{active_record → activeRecord}/baseRecord.test.js +46 -90
- package/test/activeRecord/db/connection.test.js +149 -0
- package/test/activeRecord/db/connectionConfiguration.test.js +128 -0
- package/test/activeRecord/db/migrator.test.js +144 -0
- package/test/activeRecord/db/queryInterface.test.js +48 -0
- package/test/activeRecord/helpers/baseRecord.js +32 -0
- package/test/activeRecord/helpers/baseRecordMocks.js +59 -0
- package/test/activeRecord/helpers/connection.js +28 -0
- package/test/activeRecord/helpers/connectionConfiguration.js +32 -0
- package/test/activeRecord/helpers/fixtures.js +39 -0
- package/test/activeRecord/helpers/migrator.js +78 -0
- package/test/activeRecord/helpers/queryInterface.js +29 -0
- package/test/http/client.test.js +61 -239
- package/test/http/error.test.js +23 -47
- package/test/http/helpers/client.js +80 -0
- package/test/http/helpers/error.js +31 -0
- package/test/server/helpers/autoload/TmpWithHelpersController.js +17 -0
- package/test/server/helpers/serverController.js +13 -0
- package/test/server/serverController.test.js +319 -6
- package/test/active_record/db/connection.test.js +0 -221
- package/test/active_record/db/connectionConfiguration.test.js +0 -184
- package/test/active_record/db/migrator.test.js +0 -266
- package/test/active_record/db/queryInterface.test.js +0 -66
- /package/lib/{active_record → activeRecord}/db/connection.js +0 -0
- /package/lib/{active_record → activeRecord}/db/connectionConfiguration.js +0 -0
- /package/lib/{active_record → activeRecord}/db/importSchema.js +0 -0
- /package/lib/{active_record → activeRecord}/db/migration.js +0 -0
- /package/lib/{active_record → activeRecord}/db/migrationTemplate.js +0 -0
- /package/lib/{active_record → activeRecord}/db/migrationVersion.js +0 -0
- /package/lib/{active_record → activeRecord}/db/migrator.js +0 -0
- /package/lib/{active_record → activeRecord}/db/queryInterface.js +0 -0
- /package/lib/{active_record → activeRecord}/db/schema.js +0 -0
- /package/lib/{active_record → activeRecord}/index.js +0 -0
- /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/
|
|
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/
|
|
86
|
-
"db:migrate": "node node_modules/@vida-global/core/scripts/
|
|
87
|
-
"db:rollback": "node node_modules/@vida-global/core/scripts/
|
|
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',
|
|
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
|
-
|
|
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
|
***********************************************************************************************/
|
package/lib/http/README.md
CHANGED
|
@@ -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
|
|
30
|
-
const response = await client.post("/foo/bar", {
|
|
29
|
+
const body = {baz: 1, ban: 2};
|
|
30
|
+
const response = await client.post("/foo/bar", { body });
|
|
31
31
|
```
|
|
32
32
|
|
package/lib/server/README.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
+
}
|
package/lib/server/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const { VidaServer } = require('./server');
|
|
2
|
-
const {
|
|
3
|
-
|
|
2
|
+
const { VidaServerController } = require('./serverController');
|
|
3
|
+
const ERRORS = require('./errors');
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
|
-
|
|
6
|
+
...ERRORS,
|
|
7
7
|
VidaServer,
|
|
8
8
|
VidaServerController,
|
|
9
9
|
}
|
package/lib/server/server.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
146
|
-
this.
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
};
|