@vida-global/core 1.2.5 → 1.3.1
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/{active_record → activeRecord}/db/schema.js +1 -1
- package/lib/http/README.md +2 -2
- package/lib/server/README.md +181 -20
- package/lib/server/apiDocsGenerator.js +55 -0
- package/lib/server/controllerImporter.js +62 -0
- 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}/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
|
***********************************************************************************************/
|
|
@@ -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
|
|
62
|
+
if (regExp.test(details.defaultValue)) {
|
|
63
63
|
delete details.defaultValue;
|
|
64
64
|
delete details.allowNull;
|
|
65
65
|
details.autoIncrement = true;
|
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,55 @@
|
|
|
1
|
+
const { ControllerImporter } = require('./controllerImporter');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ApiDocsGenerator {
|
|
5
|
+
#controllerDirectories
|
|
6
|
+
#controllers;
|
|
7
|
+
#docs = {};
|
|
8
|
+
|
|
9
|
+
constructor(server) {
|
|
10
|
+
this.#controllerDirectories = server.controllerDirectories;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
generateDocs() {
|
|
15
|
+
console.log("\n\n");
|
|
16
|
+
this.controllers.forEach(controller => {
|
|
17
|
+
controller.actions.forEach(action => {
|
|
18
|
+
this.generateDocsForAction(action);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
console.log("\n\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
generateDocsForAction(action) {
|
|
26
|
+
const endpoint = this.formatEndpoint(action);
|
|
27
|
+
this.#docs[endpoint] = this.#docs[endpoint] || {};
|
|
28
|
+
const actionDocs = this.#docs[endpoint][action.method.toLowerCase()] = {foo: 1};
|
|
29
|
+
console.log(actionDocs);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
get controllers() {
|
|
34
|
+
if (!this.#controllers) {
|
|
35
|
+
const controllerImporter = new ControllerImporter(this.#controllerDirectories);
|
|
36
|
+
this.#controllers = controllerImporter.controllers;
|
|
37
|
+
}
|
|
38
|
+
return this.#controllers;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
/***********************************************************************************************
|
|
43
|
+
* OPENAPI COMPONENTS
|
|
44
|
+
***********************************************************************************************/
|
|
45
|
+
formatEndpoint(action) {
|
|
46
|
+
const endpoint = action.path.replace(/:([^/]+)/g, '{$1}');
|
|
47
|
+
return endpoint;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
ApiDocsGenerator
|
|
55
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { VidaServerController } = require('./serverController');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ControllerImporter {
|
|
6
|
+
#controllerDirectories;
|
|
7
|
+
#controllers;
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
constructor(controllerDirectories) {
|
|
11
|
+
if (!Array.isArray(controllerDirectories)) {
|
|
12
|
+
controllerDirectories = [controllerDirectories];
|
|
13
|
+
}
|
|
14
|
+
this.#controllerDirectories = controllerDirectories;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
get controllers() {
|
|
19
|
+
if (!this.#controllers) {
|
|
20
|
+
this.#importControllers();
|
|
21
|
+
}
|
|
22
|
+
return this.#controllers;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
#importControllers() {
|
|
27
|
+
this.#controllers = [];
|
|
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
|
+
this.#controllers.push(_export);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
ControllerImporter
|
|
62
|
+
}
|
|
@@ -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
|
}
|