@vida-global/core 2.0.0 → 2.0.3

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.
@@ -1,50 +1,93 @@
1
1
  # VidaServer
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.
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
- ## 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`.
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.
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.
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
- // POST /api/v2/user/:id
20
- postRecord() {
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
- // Delete /foo/bar/baz
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
- ## 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
- ```
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 ServerController {
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
- ## 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...
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("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;
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
- Additional options can be passed to `toApiResponse` by explicitly calling `render`
117
- ```
118
- await this.render(response, {includeTitle: true});
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
- ### Other Status Codes
122
- There is no need to set `statusCode` directly as there are helper methods for all statuses.
239
+ Pass options through to `toApiResponse` by calling `render` explicitly:
240
+
241
+ ```js
242
+ await this.render(response, { includeTitle: true });
123
243
  ```
124
- 201 - this.renderCreationSuccessful
125
- 202 - this.renderAccepted
126
- 204 - this.renderNoConent
127
- 301 - this.renderMovedPermanently
128
- 302 - this.renderFound
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
- await this.renderErrors("Something bad happened"})
155
- // renders {data: {errors: {message: "Soemthing bad happened"}}, status: "bad request"}
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({password: ['must be > 8 characters', 'must include numbers']}
158
- // renders {data: {errors: {fields: {password: [...]}}}, status: "bad request"}
159
- ```
160
- HTTP 401
161
- ```
162
- await this.renderUnauthorized("Nope")
163
- // renders {data: {message: "Nope"}, status: "unauthorized"}
164
- ```
165
- HTTP 403
166
- ```
167
- await this.renderForbidden("You shall not pass")
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, errors)` from anywhere in the code will trigger the corresponding error handler. All other errors will generate a 500 error.
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
- // This will run an instance method, `setUpSomeStuff` before every action
182
- this.beforeCallback('setUpSomeStuff');
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
- // This will run an instance method, 'cleanupRequest', after `getFoo` and `getBar`
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
- ## Validations
189
- 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"}`
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
- get barValidations() {
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
- titleUniqueness(title) {
210
- if (User.findByTitle(title)) return 'must be unique';
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
- ### Supported Validations
215
- - `{presence: true}`
216
- - `{isInteger: true}`, `{isInteger: {gte: 0, lte: 100}}`
217
- - `{isDateTime}`
218
- - `{isEnum: {enums: [a, b, c]}}`
219
- - `{isString: true}`, `{isString: {length: {gte: 10, lte: 100}, regex: /foo/}}
220
- - `{function: someFunction}` If the function returns a string, that is considered an error and returned as the validation message
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
+ ```
@@ -15,7 +15,7 @@ class ControllerImporter extends AbstractAutoImporter {
15
15
 
16
16
  processImport(obj, directoryPrefix) {
17
17
  obj.directoryPrefix = directoryPrefix;
18
- obj.autoLoadHelpers();
18
+ obj.autoLoadMixins();
19
19
  obj.autoLoadDocumentation();
20
20
  }
21
21
  }
@@ -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
+ }