@vida-global/core 2.0.1 → 2.0.4
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/README.md +8 -7
- package/config/newrelic-config.js +2 -0
- package/lib/activeRecord/README.md +220 -111
- package/lib/http/README.md +84 -19
- package/lib/http/error.js +1 -1
- package/lib/jobQueue/README.md +139 -46
- package/lib/logger/README.md +48 -5
- package/lib/server/README.md +282 -129
- package/lib/server/controllerImporter.js +1 -1
- package/lib/server/controllerMixins/callbacks.js +132 -0
- package/lib/server/controllerMixins/documentation.js +36 -0
- package/lib/server/controllerMixins/renderer.js +315 -0
- package/lib/server/controllerMixins/requestDetails.js +82 -0
- package/lib/server/controllerMixins/routing.js +108 -0
- package/lib/server/controllerMixins/validations.js +114 -0
- package/lib/server/server.js +2 -0
- package/lib/server/serverController.js +68 -672
- package/package.json +8 -5
- package/test/http/error.test.js +1 -1
- package/test/http/helpers/error.js +5 -2
- package/test/server/serverController.test.js +822 -22
- package/test/apm/agent.test.js +0 -56
- package/test/apm/utils.test.js +0 -121
package/lib/server/README.md
CHANGED
|
@@ -1,50 +1,93 @@
|
|
|
1
1
|
# VidaServer
|
|
2
|
-
|
|
2
|
+
`VidaServer` is a general-purpose wrapper around an `express` server. It initializes the middleware that should run on every Vida application server and exposes hooks that subclasses can override to customize behavior for a specific use case. Routes are not registered by hand — they come from subclasses of `VidaServerController` that the server auto-loads from disk.
|
|
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
4
|
|
|
5
|
+
## Setup
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
```js
|
|
8
|
+
const { VidaServer } = require('@vida-global/core');
|
|
9
|
+
|
|
10
|
+
const server = new VidaServer({ port: 3000, host: 'localhost' });
|
|
11
|
+
await server.listen();
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
On `listen()`, the server walks every path in its `controllerDirectories` getter (default: `./controllers`) and registers every `VidaServerController` subclass it finds. To register controllers from additional directories, override the getter on a `VidaServer` subclass.
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
class MyServer extends VidaServer {
|
|
18
|
+
get controllerDirectories() {
|
|
19
|
+
return [
|
|
20
|
+
...super.controllerDirectories,
|
|
21
|
+
`${process.cwd()}/admin/controllers`,
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
12
25
|
```
|
|
13
|
-
// defined in the /api/v2 directory
|
|
14
|
-
class UsersController extends ServerController {
|
|
15
26
|
|
|
27
|
+
|
|
28
|
+
## Core Concepts
|
|
29
|
+
|
|
30
|
+
A request is handled by an instance of a `VidaServerController` subclass that the server constructs per request. The controller exposes:
|
|
31
|
+
|
|
32
|
+
- **Action methods** prefixed with `get`, `post`, `put`, `patch`, `delete`, or `head` — these become routes.
|
|
33
|
+
- **Request state** as getters (`params`, `requestHeaders`, `cookies`, etc.).
|
|
34
|
+
- **Render helpers** for every HTTP status code that needs one.
|
|
35
|
+
- **Callbacks** that run before or after actions for auth, validation setup, cleanup, etc.
|
|
36
|
+
|
|
37
|
+
Heavy logic lives in a paired helper module under `lib/controllers/...` that mirrors the controller's path; the framework auto-loads it and mixes `InstanceMethods`, `Accessors`, and `StaticMethods` into the controller class.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Routing
|
|
41
|
+
|
|
42
|
+
When a controller is auto-loaded, every method whose name matches `get|post|put|patch|delete|head` followed by a CamelCase suffix is turned into a route. The path comes from the controller's directory, the controller name, and the method name.
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// defined in ./controllers/api/v2/usersController.js
|
|
46
|
+
class UsersController extends VidaServerController {
|
|
16
47
|
// GET /api/v2/users
|
|
17
48
|
getIndex() {}
|
|
18
49
|
|
|
19
|
-
//
|
|
20
|
-
|
|
50
|
+
// GET /api/v2/user/:id -- "Record" is singularized and gets an :id param
|
|
51
|
+
getRecord() {
|
|
21
52
|
const user = User.find(this.params.id);
|
|
22
53
|
}
|
|
23
54
|
|
|
55
|
+
// GET /api/v2/user/:id/status
|
|
56
|
+
getRecordStatus() {}
|
|
57
|
+
|
|
24
58
|
// PUT /api/v2/users/foo
|
|
25
59
|
putFoo() {}
|
|
26
60
|
|
|
27
|
-
//
|
|
61
|
+
// DELETE /foo/bar/baz -- explicit override
|
|
28
62
|
deleteSomething() {}
|
|
29
63
|
|
|
30
64
|
static get routes() {
|
|
31
|
-
return {deleteSomething: '/foo/bar/baz'};
|
|
65
|
+
return { deleteSomething: '/foo/bar/baz' };
|
|
32
66
|
}
|
|
33
67
|
}
|
|
34
68
|
```
|
|
35
69
|
|
|
70
|
+
`Index` produces the empty suffix (so the route is just the controller's prefix). `Record` produces a singularized prefix with an `:id` path parameter.
|
|
36
71
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
72
|
+
|
|
73
|
+
## Helpers 🚨 IMPORTANT 🚨
|
|
74
|
+
|
|
75
|
+
Keep controller actions slim. Put detailed logic in a helper file under `lib/controllers/` whose path mirrors the controller's path. The framework auto-loads it and applies `InstanceMethods`, `Accessors`, and `StaticMethods`.
|
|
76
|
+
|
|
77
|
+
```js
|
|
40
78
|
// ./controllers/api/v2/fooController.js
|
|
41
|
-
class FooController extends
|
|
79
|
+
class FooController extends VidaServerController {
|
|
42
80
|
async getBar() {
|
|
43
|
-
await this.validateParameters({playerNumber: {isInteger: true}});
|
|
44
81
|
return this.doTheThing();
|
|
45
82
|
}
|
|
83
|
+
|
|
84
|
+
static parametersForGetBar() {
|
|
85
|
+
return { playerNumber: { isInteger: true } };
|
|
86
|
+
}
|
|
46
87
|
}
|
|
88
|
+
```
|
|
47
89
|
|
|
90
|
+
```js
|
|
48
91
|
// ./lib/controllers/api/v2/fooController.js
|
|
49
92
|
const InstanceMethods = {
|
|
50
93
|
doTheThing() {
|
|
@@ -56,7 +99,7 @@ const InstanceMethods = {
|
|
|
56
99
|
getTheThing() {
|
|
57
100
|
return new User();
|
|
58
101
|
}
|
|
59
|
-
}
|
|
102
|
+
};
|
|
60
103
|
|
|
61
104
|
const Accessors = {
|
|
62
105
|
playerName: {
|
|
@@ -64,157 +107,267 @@ const Accessors = {
|
|
|
64
107
|
return `Player ${this.params.playerNumber}`;
|
|
65
108
|
}
|
|
66
109
|
}
|
|
67
|
-
}
|
|
110
|
+
};
|
|
68
111
|
|
|
69
|
-
|
|
70
|
-
module.exports = {
|
|
71
|
-
InstanceMethods,
|
|
72
|
-
Accessors
|
|
73
|
-
}
|
|
112
|
+
module.exports = { InstanceMethods, Accessors };
|
|
74
113
|
```
|
|
75
114
|
|
|
76
115
|
|
|
77
116
|
## 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
117
|
|
|
118
|
+
Within an action, the controller exposes:
|
|
119
|
+
|
|
120
|
+
| Property | Description |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `this.params` | Merged route, query, and JSON-body parameters. Returns a structured clone — mutate freely. |
|
|
123
|
+
| `this.requestHeaders` | Cloned request headers. |
|
|
124
|
+
| `this.responseHeaders` | Outgoing response headers (mutable). |
|
|
125
|
+
| `this.contentType` | Shortcut for `requestHeaders['content-type']`. |
|
|
126
|
+
| `this.userAgent` | Shortcut for `requestHeaders['user-agent']`. |
|
|
127
|
+
| `this.requestId` | Unique request id (added by `express-request-id`). |
|
|
128
|
+
| `this.requestBody` | Raw request body. |
|
|
129
|
+
| `this.requestMethod` | HTTP method. |
|
|
130
|
+
| `this.requestIp` | Client IP address. |
|
|
131
|
+
| `this.url` | Original URL. |
|
|
132
|
+
| `this.bearerToken` | Parsed `Authorization: Bearer <token>` value, or `null`. |
|
|
133
|
+
| `this.statusCode` | Current outgoing status (settable, but prefer the render helpers below). |
|
|
134
|
+
| `this.logger` | Per-request child logger. |
|
|
87
135
|
|
|
88
|
-
|
|
89
|
-
|
|
136
|
+
|
|
137
|
+
## Cookies
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
// All cookies (object)
|
|
141
|
+
const all = this.cookies;
|
|
142
|
+
|
|
143
|
+
// A single cookie, or null if missing
|
|
144
|
+
const session = this.getCookie('session');
|
|
145
|
+
|
|
146
|
+
// Set a cookie. Options are passed through to express `res.cookie`.
|
|
147
|
+
this.setCookie('session', token, { httpOnly: true, secure: true, maxAge: 86400_000 });
|
|
90
148
|
```
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Parameter coercion
|
|
152
|
+
|
|
153
|
+
Use these on the controller instance to normalize inbound params without throwing. Each accepts a `{ fallback }` option (and other shape-specific options) and returns the fallback for unusable input.
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
this.hasParam('agentId'); // true/false
|
|
157
|
+
this.coerceString(this.params.name, { fallback: 'anonymous' }); // trims; empty -> fallback
|
|
158
|
+
this.coerceBoolean(this.params.enabled, { fallback: false }); // accepts true/false, "true"/"false"/"1"/"0"/"yes"/"no"/"on"/"off"
|
|
159
|
+
this.coerceInteger(this.params.limit,
|
|
160
|
+
{ min: 1, max: 100, fallback: 25 }); // floors, clamps, nonNegative supported
|
|
161
|
+
this.coerceObject(this.params.filters, { fallback: {} }); // arrays return fallback
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
## Validations
|
|
166
|
+
|
|
167
|
+
Prefer the static `parametersFor<ActionName>` method on the controller class — the framework runs it before the action and renders a 400 response with structured field errors if anything fails.
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
class UsersController extends VidaServerController {
|
|
171
|
+
static parametersForGetIndex() {
|
|
172
|
+
return {
|
|
173
|
+
pageSize: { isInteger: { gte: 1, lte: 100 }, optional: true },
|
|
174
|
+
email: { presence: true, isString: { regex: /\S+@\S+\.\S+/ } },
|
|
175
|
+
role: { isEnum: { enums: ['admin', 'member', 'guest'] } },
|
|
176
|
+
title: { function: this.prototype.validateUniqueTitle },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async getIndex() {
|
|
181
|
+
// pageSize/email/role/title are already validated when this runs
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async validateUniqueTitle(title) {
|
|
185
|
+
if (await User.findByTitle(title)) return 'must be unique';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
If you need to validate values that are constructed mid-action, call `this.validateParameters(...)` directly with the same shape.
|
|
191
|
+
|
|
192
|
+
### Supported validators
|
|
193
|
+
|
|
194
|
+
- `{ optional: true }` — skip validation when the value is `undefined`.
|
|
195
|
+
- `{ presence: true }`
|
|
196
|
+
- `{ isInteger: true }` or `{ isInteger: { gte: 0, lte: 100 } }`
|
|
197
|
+
- `{ isString: true }` or `{ isString: { length: { gte: 10, lte: 100 }, regex: /foo/ } }`
|
|
198
|
+
- `{ isBoolean: true }` or `{ isBoolean: false }` (asserts the exact value)
|
|
199
|
+
- `{ isDateTime: true }`
|
|
200
|
+
- `{ isEnum: { enums: ['a', 'b', 'c'], error: 'optional message' } }`
|
|
201
|
+
- `{ function: someFn }` — return a string to flag an error.
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
## Rendering responses
|
|
205
|
+
|
|
206
|
+
By default the controller renders the value returned from the action. Render walks the result recursively and calls `toApiResponse(opts)` on anything that defines it, then wraps the result in a standard envelope.
|
|
207
|
+
|
|
208
|
+
```js
|
|
91
209
|
class User {
|
|
92
210
|
toApiResponse(opts) {
|
|
93
|
-
const data = {name: this.name, email: this.email}
|
|
211
|
+
const data = { name: this.name, email: this.email };
|
|
94
212
|
if (opts.includeTitle) data.title = this.title;
|
|
95
213
|
return data;
|
|
96
214
|
}
|
|
97
215
|
}
|
|
98
216
|
|
|
99
217
|
async getFoo() {
|
|
100
|
-
const user1 = new User(
|
|
101
|
-
const user2 = new User(
|
|
102
|
-
|
|
103
|
-
return response;
|
|
218
|
+
const user1 = new User('Bruce', 'bwayne@wayne-enterprises.inc');
|
|
219
|
+
const user2 = new User('Babs', 'bgordon@wayne-enterprises.inc');
|
|
220
|
+
return { bar: 1, baz: [user1, user2] };
|
|
104
221
|
}
|
|
105
222
|
```
|
|
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
223
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
224
|
+
…produces:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"data": {
|
|
229
|
+
"bar": 1,
|
|
230
|
+
"baz": [
|
|
231
|
+
{ "name": "Bruce", "email": "bwayne@wayne-enterprises.inc" },
|
|
232
|
+
{ "name": "Babs", "email": "bgordon@wayne-enterprises.inc" }
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
"status": "ok"
|
|
236
|
+
}
|
|
119
237
|
```
|
|
120
238
|
|
|
121
|
-
|
|
122
|
-
|
|
239
|
+
Pass options through to `toApiResponse` by calling `render` explicitly:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
await this.render(response, { includeTitle: true });
|
|
123
243
|
```
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
304 - this.renderNotModified
|
|
130
|
-
307 - this.renderTemporaryRedirect
|
|
131
|
-
308 - this.renderPermanentRedirect
|
|
132
|
-
401 - this.renderUnauthorizedResponse
|
|
133
|
-
402 - this.renderPaymentRequired
|
|
134
|
-
403 - this.renderForbiddenResponse
|
|
135
|
-
404 - this.renderNotFoundResponse
|
|
136
|
-
405 - this.renderMethodNotAllowed
|
|
137
|
-
408 - this.renderRequestTimeout
|
|
138
|
-
409 - this.renderConflictResponse
|
|
139
|
-
410 - this.renderGone
|
|
140
|
-
413 - this.renderContentTooLarge
|
|
141
|
-
415 - this.renderUnsupportedMediaType
|
|
142
|
-
422 - this.renderUnprocessableContent
|
|
143
|
-
423 - this.renderLocked
|
|
144
|
-
429 - this.renderTooManyRequests
|
|
244
|
+
|
|
245
|
+
To skip the standard envelope and emit the body verbatim (legacy clients, file-style payloads):
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
return await this.render(response, { standardize: false });
|
|
145
249
|
```
|
|
146
250
|
|
|
251
|
+
### Status code helpers
|
|
252
|
+
|
|
253
|
+
Don't set `this.statusCode` directly — use the helper that pairs the status with `render`:
|
|
254
|
+
|
|
255
|
+
| Status | Helper |
|
|
256
|
+
|---|---|
|
|
257
|
+
| 201 Created | `this.renderCreationSuccessful(body)` |
|
|
258
|
+
| 202 Accepted | `this.renderAccepted(body)` |
|
|
259
|
+
| 204 No Content | `this.renderNoConent(body)` |
|
|
260
|
+
| 301 Moved Permanently | `this.renderMovedPermanently(body)` |
|
|
261
|
+
| 302 Found | `this.renderFound(body)` |
|
|
262
|
+
| 304 Not Modified | `this.renderNotModified(body)` |
|
|
263
|
+
| 307 Temporary Redirect | `this.renderTemporaryRedirect(body)` |
|
|
264
|
+
| 308 Permanent Redirect | `this.renderPermanentRedirect(body)` |
|
|
265
|
+
| 401 Unauthorized | `this.renderUnauthorized(message, body)` |
|
|
266
|
+
| 402 Payment Required | `this.renderPaymentRequired(message, body)` |
|
|
267
|
+
| 403 Forbidden | `this.renderForbidden(message, body)` |
|
|
268
|
+
| 404 Not Found | `this.renderNotFound(message, body)` |
|
|
269
|
+
| 405 Method Not Allowed | `this.renderMethodNotAllowed(message, body)` |
|
|
270
|
+
| 408 Request Timeout | `this.renderRequestTimeout(message, body)` |
|
|
271
|
+
| 409 Conflict | `this.renderConflict(message, body)` |
|
|
272
|
+
| 410 Gone | `this.renderGone(message, body)` |
|
|
273
|
+
| 413 Content Too Large | `this.renderContentTooLarge(message, body)` |
|
|
274
|
+
| 415 Unsupported Media Type | `this.renderUnsupportedMediaType(message, body)` |
|
|
275
|
+
| 422 Unprocessable Content | `this.renderUnprocessableContent(message, body)` |
|
|
276
|
+
| 423 Locked | `this.renderLocked(message, body)` |
|
|
277
|
+
| 429 Too Many Requests | `this.renderTooManyRequests(message, body)` |
|
|
278
|
+
|
|
279
|
+
After a render helper runs, the action does not need to return — the response has already been sent.
|
|
280
|
+
|
|
147
281
|
|
|
148
282
|
## Error handling
|
|
149
|
-
HTTP 400
|
|
150
|
-
```
|
|
151
|
-
await this.renderErrors("Something bad happened", {password: ['must be > 8 characters', 'must include numbers']})
|
|
152
|
-
// renders {data: {errors: {message: "Soemthing bad happened", fields: {password: [...]}}}, status: "bad request"}
|
|
153
283
|
|
|
154
|
-
|
|
155
|
-
|
|
284
|
+
```js
|
|
285
|
+
await this.renderErrors('Something bad happened',
|
|
286
|
+
{ password: ['must be > 8 characters', 'must include numbers'] });
|
|
287
|
+
// {data: {errors: {message: "Something bad happened", fields: {password: [...]}}}, status: "bad request"}
|
|
156
288
|
|
|
157
|
-
await this.renderErrors(
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// renders {data: {message: "You shall not pass"}, status: "forbidden"}
|
|
289
|
+
await this.renderErrors('Something bad happened');
|
|
290
|
+
// {data: {errors: {message: "Something bad happened"}}, status: "bad request"}
|
|
291
|
+
|
|
292
|
+
await this.renderErrors({ password: ['must be > 8 characters', 'must include numbers'] });
|
|
293
|
+
// {data: {errors: {fields: {password: [...]}}}, status: "bad request"}
|
|
294
|
+
|
|
295
|
+
await this.renderUnauthorized('Nope');
|
|
296
|
+
// {data: {message: "Nope"}, status: "unauthorized"}
|
|
297
|
+
|
|
298
|
+
await this.renderForbidden('You shall not pass');
|
|
299
|
+
// {data: {message: "You shall not pass"}, status: "forbidden"}
|
|
169
300
|
```
|
|
170
|
-
* _Other statuses detailed above._
|
|
171
301
|
|
|
172
|
-
Throwing any of `this.Errors.Authorization(msg)`, `this.Errors.Forbidden(msg)`, `this.Errors.NotFound(msg)`, `this.Errors.Validation(msg,
|
|
302
|
+
Throwing any of `this.Errors.Authorization(msg)`, `this.Errors.Forbidden(msg)`, `this.Errors.NotFound(msg)`, or `this.Errors.Validation(msg, fields)` from anywhere in the call chain triggers the matching render helper. Any other thrown error becomes a 500.
|
|
173
303
|
|
|
174
304
|
|
|
175
305
|
## Callbacks
|
|
176
|
-
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.
|
|
177
|
-
```
|
|
178
|
-
// This will run on all actions except for getIndex and will halt execution if the environment is production (e.g. development routes)
|
|
179
|
-
this.beforeCallback(() => process.env.NODE_ENV != 'production', {except: 'getIndex'});
|
|
180
306
|
|
|
181
|
-
|
|
182
|
-
|
|
307
|
+
Callbacks are methods that run before or after an action. They are used for authentication, action setup, cleanup, etc. If a callback returns `false`, execution stops and no further callbacks or the action are run. Define callbacks in `setupCallbacks()` on the controller.
|
|
308
|
+
|
|
309
|
+
```js
|
|
310
|
+
setupCallbacks() {
|
|
311
|
+
// Runs before every action except getIndex; halts in production
|
|
312
|
+
this.beforeCallback(() => process.env.NODE_ENV != 'production', { except: 'getIndex' });
|
|
313
|
+
|
|
314
|
+
// Runs the `setUpSomeStuff` instance method before every action
|
|
315
|
+
this.beforeCallback('setUpSomeStuff');
|
|
183
316
|
|
|
184
|
-
//
|
|
185
|
-
this.afterCallback('cleanupRequest', {only: ['getFoo', 'getBar']})
|
|
317
|
+
// Runs `cleanupRequest` after getFoo and getBar
|
|
318
|
+
this.afterCallback('cleanupRequest', { only: ['getFoo', 'getBar'] });
|
|
319
|
+
}
|
|
186
320
|
```
|
|
187
321
|
|
|
188
|
-
|
|
189
|
-
|
|
322
|
+
A subclass can suppress a callback inherited from a parent without removing it for everyone:
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
this.skipBeforeCallback('authenticateRequest', { only: 'getHealthCheck' });
|
|
326
|
+
this.skipAfterCallback('logActivity', { except: ['getRecord'] });
|
|
190
327
|
```
|
|
191
|
-
async getFoo() {
|
|
192
|
-
// validates that the pageSize parameter is an integer greater than or equal to one
|
|
193
|
-
await this.validateParameters({pageSize: {isInteger: true, gte: 1}});
|
|
194
|
-
}
|
|
195
328
|
|
|
196
|
-
async postBar() {
|
|
197
|
-
await this.validateParameters(this.barValidations);
|
|
198
|
-
}
|
|
199
329
|
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
name: {presence: true},
|
|
203
|
-
role: {isEnum: {enums: ['CEO', 'COO', 'other']}},
|
|
204
|
-
email: {regex: /\w@\w\.com/},
|
|
205
|
-
title: {function: this.titleUniqueness}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
330
|
+
## Server-Sent Events / streaming
|
|
208
331
|
|
|
209
|
-
|
|
210
|
-
|
|
332
|
+
Use `streamSseResponse` to write a long-lived stream of events instead of a one-shot JSON response. The handler is run with `this` bound to the controller, and `writeStreamEvent(name, data)` emits SSE frames.
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
async getEvents() {
|
|
336
|
+
const ctrl = new AbortController();
|
|
337
|
+
await this.streamSseResponse(
|
|
338
|
+
async () => {
|
|
339
|
+
for await (const evt of this.subscribeToEvents(ctrl.signal)) {
|
|
340
|
+
this.writeStreamEvent('update', JSON.stringify(evt));
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
async (err) => this.logger.error(err),
|
|
344
|
+
ctrl,
|
|
345
|
+
);
|
|
211
346
|
}
|
|
212
347
|
```
|
|
213
348
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
349
|
+
A trailing `data: [DONE]\n\n` frame is emitted automatically when the handler returns. If the client disconnects, the abort controller (when provided) is aborted.
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
## Advanced
|
|
353
|
+
|
|
354
|
+
These hooks let a `VidaServer` subclass tune the middleware stack and integrations. Override the getter on the subclass.
|
|
355
|
+
|
|
356
|
+
| Hook | Default | Purpose |
|
|
357
|
+
|---|---|---|
|
|
358
|
+
| `corsOrigins` | `[]` | List of allowed origins for the Socket.IO server. |
|
|
359
|
+
| `staticFilesDirectory` | `${cwd}/static` | Served under `/static`. |
|
|
360
|
+
| `viewsDirectory` | `${cwd}/views` | Mustache views root. |
|
|
361
|
+
| `jsonParsingMiddleware` | `express.json({...})` | Customize JSON body parsing limits. |
|
|
362
|
+
| `octetStreamLimit` | `'128mb'` | Limit applied to `application/octet-stream` bodies. |
|
|
363
|
+
| `octetStreamParsingMiddleware` | `express.raw({...})` | Full replacement for the octet-stream parser. |
|
|
364
|
+
| `loggingMiddleware` | Vida logging middleware | Replace the request logger. |
|
|
365
|
+
| `connectionAbortedMiddleware` | Silent ECONNABORTED handler | Replace the abort handler. |
|
|
366
|
+
| `setupMiddleware()` | Wires everything above | Override to fully control the middleware order. |
|
|
367
|
+
|
|
368
|
+
WebSocket helpers expose Socket.IO:
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
server.emitWebSocketEvent(channel, eventName, eventData);
|
|
372
|
+
server.addWebSocketEventHandler(eventName, handler);
|
|
373
|
+
```
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const APM = require('../../apm');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const AUTH_CALLBACK_NAME = 'authenticateRequest';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const afterCallbacks = new Map();
|
|
8
|
+
const afterCallbacksToSkip = new Map();
|
|
9
|
+
const beforeCallbacks = new Map();
|
|
10
|
+
const beforeCallbacksToSkip = new Map();
|
|
11
|
+
const callbacksSetup = new Map();
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
function callbacksListForController(list, controller) {
|
|
15
|
+
const cls = controller.constructor;
|
|
16
|
+
if (!list.has(cls)) {
|
|
17
|
+
list.set(cls, []);
|
|
18
|
+
}
|
|
19
|
+
return list.get(cls);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
function addCallback(controller, callback, options, callbacksList) {
|
|
24
|
+
const list = callbacksListForController(callbacksList, controller)
|
|
25
|
+
list.push({ callback, options });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
function shouldRunCallback(actionName, options) {
|
|
30
|
+
if (options.only) {
|
|
31
|
+
if (Array.isArray(options.only)) return options.only.includes(actionName);
|
|
32
|
+
return options.only == actionName;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (options.except) {
|
|
36
|
+
if (Array.isArray(options.except)) return !options.except.includes(actionName);
|
|
37
|
+
return options.except != actionName;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async function runCallbacks(callbacksList, callbacksToSkipList, actionName, controller) {
|
|
45
|
+
let toSkip = callbacksToSkipList.filter(({ callback, options }) => {
|
|
46
|
+
return shouldRunCallback(actionName, options);
|
|
47
|
+
});
|
|
48
|
+
toSkip = toSkip.map(({ callback }) => callback);
|
|
49
|
+
|
|
50
|
+
for (const { callback, options } of callbacksList) {
|
|
51
|
+
if (!shouldRunCallback(actionName, options)) continue;
|
|
52
|
+
if (toSkip.includes(callback)) continue;
|
|
53
|
+
const result = await runCallback(callback, controller)
|
|
54
|
+
if (result === false) return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async function runCallback(callback, controller) {
|
|
62
|
+
if (typeof callback == 'function') return await callback.call(controller);
|
|
63
|
+
return await controller[callback]();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
const InstanceMethods = {
|
|
68
|
+
setupCallbacks() {},
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_applyCallbacks() {
|
|
72
|
+
const cls = this.constructor;
|
|
73
|
+
if (!callbacksSetup.has(cls)) {
|
|
74
|
+
this.setupCallbacks();
|
|
75
|
+
callbacksSetup.set(cls, true)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// set up the callbacks when the instance is constructed and prevent it from being called again
|
|
79
|
+
this.setupCallbacks = undefined;
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
beforeCallback(callback, options={}) {
|
|
84
|
+
addCallback(this, callback, options, beforeCallbacks);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
afterCallback(callback, options={}) {
|
|
89
|
+
addCallback(this, callback, options, afterCallbacks);
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
skipBeforeCallback(callback, options={}) {
|
|
94
|
+
addCallback(this, callback, options, beforeCallbacksToSkip);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
skipAfterCallback(callback, options={}) {
|
|
99
|
+
addCallback(this, callback, options, afterCallbacksToSkip);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async _runCallbacks(type, actionName) {
|
|
104
|
+
let callbacksList, callbacksToSkipList;
|
|
105
|
+
|
|
106
|
+
switch(type) {
|
|
107
|
+
case 'after':
|
|
108
|
+
callbacksList = callbacksListForController(afterCallbacks, this);
|
|
109
|
+
callbacksToSkipList = callbacksListForController(afterCallbacksToSkip, this);
|
|
110
|
+
break;
|
|
111
|
+
case 'before':
|
|
112
|
+
callbacksList = callbacksListForController(beforeCallbacks, this);
|
|
113
|
+
callbacksToSkipList = callbacksListForController(beforeCallbacksToSkip, this);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return await APM.startSegment(`${type}Callbacks`, true, () => {
|
|
118
|
+
return runCallbacks(callbacksList, callbacksToSkipList, actionName, this);
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
isAuthenticatedAction(actionName) {
|
|
124
|
+
const list = callbacksListForController(beforeCallbacks, this);
|
|
125
|
+
const entry = list.find(({ callback }) => callback === AUTH_CALLBACK_NAME);
|
|
126
|
+
if (!entry) return false;
|
|
127
|
+
return shouldRunCallback(actionName, entry.options);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
module.exports = { InstanceMethods };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { camelize } = require('inflection');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const StaticMethods = {
|
|
5
|
+
documentationForAction(actionName) {
|
|
6
|
+
const methodName = this.documentationMethodForAction(actionName);
|
|
7
|
+
if (this[methodName]) return this[methodName]();
|
|
8
|
+
return null;
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
documentationMethodForAction(actionName) {
|
|
13
|
+
return `document${camelize(actionName)}`;
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
isAuthenticatedAction(actionName) {
|
|
18
|
+
const controller = new this();
|
|
19
|
+
return controller.isAuthenticatedAction(actionName);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_authenticationDocumentation(actionName) {
|
|
24
|
+
if (!this.isAuthenticatedAction(actionName)) return undefined;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
authenticationDocumentation(actionName) {
|
|
29
|
+
return {apiKeyAuth: []};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
StaticMethods
|
|
36
|
+
}
|