@vida-global/core 2.0.11 → 2.1.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/lib/activeRecord/README.md +1 -1
- package/lib/activeRecord/baseRecord.js +70 -4
- package/lib/server/README.md +8 -219
- package/lib/server/controllerImporter.js +0 -1
- package/lib/server/controllerMixins/callbacks.js +16 -61
- package/lib/server/controllerMixins/classScopedRegistry.js +48 -0
- package/lib/server/controllerMixins/documentation.js +41 -9
- package/lib/server/controllerMixins/renderer.js +140 -99
- package/lib/server/controllerMixins/validations.js +57 -0
- package/lib/server/doc/callbacks.md +23 -0
- package/lib/server/doc/documentation.md +135 -0
- package/lib/server/doc/renderer.md +115 -0
- package/lib/server/doc/requestDetails.md +52 -0
- package/lib/server/doc/validations.md +40 -0
- package/lib/server/index.js +1 -1
- package/lib/server/openApi/apiDocGenerator.js +426 -0
- package/lib/server/openApi/apiDocsGenerator.js +147 -0
- package/lib/server/openApi/schemaImporter.js +41 -0
- package/lib/server/openApi/schemaRegistry.js +43 -0
- package/lib/server/openApi/schemas.js +97 -0
- package/lib/server/openApi/tagRegistry.js +27 -0
- package/lib/server/server.js +14 -0
- package/lib/server/serverController.js +4 -9
- package/lib/server/statusTexts.js +38 -0
- package/lib/utils/yamlLoader.js +17 -0
- package/package.json +4 -2
- package/test/activeRecord/baseRecord.test.js +101 -0
- package/test/server/apiDocGenerator.test.js +743 -0
- package/test/server/controllerMixins/callbacks.test.js +326 -0
- package/test/server/controllerMixins/classScopedRegistry.test.js +95 -0
- package/test/server/controllerMixins/documentation.test.js +168 -0
- package/test/server/controllerMixins/renderer.test.js +734 -0
- package/test/server/controllerMixins/requestDetails.test.js +225 -0
- package/test/server/controllerMixins/routing.test.js +82 -0
- package/test/server/controllerMixins/validations.test.js +509 -0
- package/test/server/openApi/apiDocsGenerator.test.js +306 -0
- package/test/server/openApi/helpers/apiDocsPackageFixture/package.json +5 -0
- package/test/server/openApi/schemaImporter.test.js +54 -0
- package/test/server/openApi/schemaRegistry.test.js +93 -0
- package/test/server/openApi/schemas.test.js +109 -0
- package/test/server/serverController.test.js +8 -867
- package/test/server/statusTexts.test.js +30 -0
- package/test/utils/yamlLoader.test.js +43 -0
- package/lib/server/apiDocsGenerator.js +0 -86
- package/test/server/apiDocsGenerator.test.js +0 -38
|
@@ -173,7 +173,7 @@ const rows = await User.findAll({
|
|
|
173
173
|
|
|
174
174
|
### API serialization
|
|
175
175
|
|
|
176
|
-
`record.toApiResponse()` returns a plain object containing
|
|
176
|
+
`record.toApiResponse()` returns a plain object containing the columns exposed by the model's static `documentationSchema()` (which excludes `created_at`, `updated_at`, and `deleted_at`), with overridden getters applied. The server's response renderer calls this automatically when a record is included in an action's return value.
|
|
177
177
|
|
|
178
178
|
### Closing connections
|
|
179
179
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { Connection, DEFAULT_DATABASE_ID } = require('./db/connection');
|
|
2
2
|
const { getActiveRecordSchema } = require('./db/schema');
|
|
3
|
+
const { Schemas } = require('../server/openApi/schemas');
|
|
3
4
|
const { logger } = require('../logger');
|
|
4
5
|
const { Model, Op, Sequelize } = require('sequelize');
|
|
5
6
|
const nodeUtil = require('util');
|
|
@@ -373,11 +374,76 @@ class BaseRecord extends Model {
|
|
|
373
374
|
|
|
374
375
|
|
|
375
376
|
toApiResponse() {
|
|
376
|
-
const
|
|
377
|
-
for (const
|
|
378
|
-
|
|
377
|
+
const response = {};
|
|
378
|
+
for (const column of this.apiResponseColumns) {
|
|
379
|
+
response[column] = this[column]; // Use overridden getters when available
|
|
379
380
|
}
|
|
380
|
-
return
|
|
381
|
+
return response;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
get apiResponseColumns() {
|
|
386
|
+
return Object.keys(this.constructor.documentationSchema());
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
/***********************************************************************************************
|
|
391
|
+
* DOCUMENTATION
|
|
392
|
+
***********************************************************************************************/
|
|
393
|
+
static documentationSchema() {
|
|
394
|
+
this.initialize();
|
|
395
|
+
const schemas = {};
|
|
396
|
+
for (const [name, attribute] of Object.entries(this.getAttributes())) {
|
|
397
|
+
if (this.documentationExcludedColumns.includes(name)) continue;
|
|
398
|
+
schemas[name] = this.documentationColumnSchema(attribute);
|
|
399
|
+
}
|
|
400
|
+
return schemas;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
static get documentationExcludedColumns() {
|
|
405
|
+
return ['created_at', 'updated_at', 'deleted_at'];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
static documentationColumnSchema(attribute) {
|
|
410
|
+
const builder = this.documentationTypeBuilders[attribute.type?.key];
|
|
411
|
+
if (!builder) return Schemas.stringSchema();
|
|
412
|
+
return builder(attribute);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
static get documentationTypeBuilders() {
|
|
417
|
+
return {
|
|
418
|
+
INTEGER: () => Schemas.integerSchema(),
|
|
419
|
+
BIGINT: () => Schemas.integerSchema(),
|
|
420
|
+
SMALLINT: () => Schemas.integerSchema(),
|
|
421
|
+
TINYINT: () => Schemas.integerSchema(),
|
|
422
|
+
MEDIUMINT: () => Schemas.integerSchema(),
|
|
423
|
+
FLOAT: () => Schemas.numberSchema(),
|
|
424
|
+
DOUBLE: () => Schemas.numberSchema(),
|
|
425
|
+
REAL: () => Schemas.numberSchema(),
|
|
426
|
+
DECIMAL: () => Schemas.numberSchema(),
|
|
427
|
+
NUMBER: () => Schemas.numberSchema(),
|
|
428
|
+
BOOLEAN: () => Schemas.booleanSchema(),
|
|
429
|
+
DATE: () => Schemas.dateTimeSchema(),
|
|
430
|
+
DATEONLY: () => Schemas.dateTimeSchema(),
|
|
431
|
+
TIME: () => Schemas.dateTimeSchema(),
|
|
432
|
+
ENUM: (attribute) => Schemas.enumSchema({ enums: attribute.type.values }),
|
|
433
|
+
JSON: () => Schemas.objectSchema(),
|
|
434
|
+
JSONB: () => Schemas.objectSchema(),
|
|
435
|
+
ARRAY: () => Schemas.arraySchema(),
|
|
436
|
+
STRING: () => Schemas.stringSchema(),
|
|
437
|
+
CHAR: () => Schemas.stringSchema(),
|
|
438
|
+
TEXT: () => Schemas.stringSchema(),
|
|
439
|
+
CITEXT: () => Schemas.stringSchema(),
|
|
440
|
+
UUID: () => Schemas.stringSchema(),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
get apiXMLTagName() {
|
|
446
|
+
return this.constructor.name;
|
|
381
447
|
}
|
|
382
448
|
}
|
|
383
449
|
|
package/lib/server/README.md
CHANGED
|
@@ -113,240 +113,29 @@ module.exports = { InstanceMethods, Accessors };
|
|
|
113
113
|
```
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
## Request
|
|
116
|
+
## Request details
|
|
117
117
|
|
|
118
|
-
|
|
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. |
|
|
135
|
-
|
|
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 });
|
|
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
|
-
```
|
|
118
|
+
See [Request details](./doc/requestDetails.md) — request properties, cookies, and parameter coercion.
|
|
163
119
|
|
|
164
120
|
|
|
165
121
|
## Validations
|
|
166
122
|
|
|
167
|
-
|
|
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
|
|
209
|
-
class User {
|
|
210
|
-
toApiResponse(opts) {
|
|
211
|
-
const data = { name: this.name, email: this.email };
|
|
212
|
-
if (opts.includeTitle) data.title = this.title;
|
|
213
|
-
return data;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async getFoo() {
|
|
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] };
|
|
221
|
-
}
|
|
222
|
-
```
|
|
223
|
-
|
|
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
|
-
}
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
Pass options through to `toApiResponse` by calling `render` explicitly:
|
|
240
|
-
|
|
241
|
-
```js
|
|
242
|
-
await this.render(response, { includeTitle: true });
|
|
243
|
-
```
|
|
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 });
|
|
249
|
-
```
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
## Error handling
|
|
283
|
-
|
|
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"}
|
|
288
|
-
|
|
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"}
|
|
300
|
-
```
|
|
301
|
-
|
|
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.
|
|
123
|
+
See [Validations](./doc/validations.md) — `parametersFor<ActionName>` and the supported validators.
|
|
303
124
|
|
|
304
125
|
|
|
305
126
|
## Callbacks
|
|
306
127
|
|
|
307
|
-
Callbacks
|
|
128
|
+
See [Callbacks](./doc/callbacks.md) — before/after action callbacks and how subclasses skip them.
|
|
308
129
|
|
|
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');
|
|
316
|
-
|
|
317
|
-
// Runs `cleanupRequest` after getFoo and getBar
|
|
318
|
-
this.afterCallback('cleanupRequest', { only: ['getFoo', 'getBar'] });
|
|
319
|
-
}
|
|
320
|
-
```
|
|
321
|
-
|
|
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'] });
|
|
327
|
-
```
|
|
328
130
|
|
|
131
|
+
## Rendering responses
|
|
329
132
|
|
|
330
|
-
|
|
133
|
+
See [Rendering responses](./doc/renderer.md) — the render envelope, status-code helpers, error handling, and Server-Sent Events / streaming.
|
|
331
134
|
|
|
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
135
|
|
|
334
|
-
|
|
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
|
-
);
|
|
346
|
-
}
|
|
347
|
-
```
|
|
136
|
+
## Documentation
|
|
348
137
|
|
|
349
|
-
|
|
138
|
+
See [API Documentation (OpenAPI)](./doc/documentation.md) — generating an OpenAPI spec: per-action YAML, tags, schema refs, and auth.
|
|
350
139
|
|
|
351
140
|
|
|
352
141
|
## Advanced
|
|
@@ -1,55 +1,27 @@
|
|
|
1
1
|
const APM = require('../../apm');
|
|
2
|
+
const { ClassScopedRegistry, appliesToAction } = require('./classScopedRegistry');
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
const AUTH_CALLBACK_NAME = 'authenticateRequest';
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
-
const afterCallbacksToSkip = new Map();
|
|
9
|
-
const beforeCallbacks = new Map();
|
|
10
|
-
const beforeCallbacksToSkip = new Map();
|
|
11
|
-
const callbacksSetup = new Map();
|
|
8
|
+
const registry = new ClassScopedRegistry();
|
|
12
9
|
|
|
13
10
|
|
|
14
|
-
function
|
|
15
|
-
|
|
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;
|
|
11
|
+
function addCallback(controller, callback, options, listName) {
|
|
12
|
+
registry.list(listName, controller).push({ callback, options });
|
|
41
13
|
}
|
|
42
14
|
|
|
43
15
|
|
|
44
16
|
async function runCallbacks(callbacksList, callbacksToSkipList, actionName, controller) {
|
|
45
17
|
let toSkip = callbacksToSkipList.filter(({ callback, options }) => {
|
|
46
|
-
return
|
|
18
|
+
return appliesToAction(actionName, options);
|
|
47
19
|
});
|
|
48
20
|
toSkip = toSkip.map(({ callback }) => callback);
|
|
49
21
|
|
|
50
22
|
for (const { callback, options } of callbacksList) {
|
|
51
|
-
if (!
|
|
52
|
-
if (toSkip.includes(callback))
|
|
23
|
+
if (!appliesToAction(actionName, options)) continue;
|
|
24
|
+
if (toSkip.includes(callback)) continue;
|
|
53
25
|
const result = await runCallback(callback, controller)
|
|
54
26
|
if (result === false) return false;
|
|
55
27
|
}
|
|
@@ -69,50 +41,33 @@ const InstanceMethods = {
|
|
|
69
41
|
|
|
70
42
|
|
|
71
43
|
_applyCallbacks() {
|
|
72
|
-
|
|
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;
|
|
44
|
+
registry.applySetupOnce(this, 'setupCallbacks');
|
|
80
45
|
},
|
|
81
46
|
|
|
82
47
|
|
|
83
48
|
beforeCallback(callback, options={}) {
|
|
84
|
-
addCallback(this, callback, options,
|
|
49
|
+
addCallback(this, callback, options, 'before');
|
|
85
50
|
},
|
|
86
51
|
|
|
87
52
|
|
|
88
53
|
afterCallback(callback, options={}) {
|
|
89
|
-
addCallback(this, callback, options,
|
|
54
|
+
addCallback(this, callback, options, 'after');
|
|
90
55
|
},
|
|
91
56
|
|
|
92
57
|
|
|
93
58
|
skipBeforeCallback(callback, options={}) {
|
|
94
|
-
addCallback(this, callback, options,
|
|
59
|
+
addCallback(this, callback, options, 'beforeSkip');
|
|
95
60
|
},
|
|
96
61
|
|
|
97
62
|
|
|
98
63
|
skipAfterCallback(callback, options={}) {
|
|
99
|
-
addCallback(this, callback, options,
|
|
64
|
+
addCallback(this, callback, options, 'afterSkip');
|
|
100
65
|
},
|
|
101
66
|
|
|
102
67
|
|
|
103
68
|
async _runCallbacks(type, actionName) {
|
|
104
|
-
|
|
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
|
-
}
|
|
69
|
+
const callbacksList = registry.list(type, this);
|
|
70
|
+
const callbacksToSkipList = registry.list(`${type}Skip`, this);
|
|
116
71
|
|
|
117
72
|
return await APM.startSegment(`${type}Callbacks`, true, () => {
|
|
118
73
|
return runCallbacks(callbacksList, callbacksToSkipList, actionName, this);
|
|
@@ -121,10 +76,10 @@ const InstanceMethods = {
|
|
|
121
76
|
|
|
122
77
|
|
|
123
78
|
isAuthenticatedAction(actionName) {
|
|
124
|
-
const list =
|
|
79
|
+
const list = registry.list('before', this);
|
|
125
80
|
const entry = list.find(({ callback }) => callback === AUTH_CALLBACK_NAME);
|
|
126
81
|
if (!entry) return false;
|
|
127
|
-
return
|
|
82
|
+
return appliesToAction(actionName, entry.options);
|
|
128
83
|
}
|
|
129
84
|
}
|
|
130
85
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function appliesToAction(actionName, options) {
|
|
2
|
+
if (options.only) {
|
|
3
|
+
if (Array.isArray(options.only)) return options.only.includes(actionName);
|
|
4
|
+
return options.only == actionName;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (options.except) {
|
|
8
|
+
if (Array.isArray(options.except)) return !options.except.includes(actionName);
|
|
9
|
+
return options.except != actionName;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClassScopedRegistry {
|
|
17
|
+
#lists = new Map();
|
|
18
|
+
#setup = new Map();
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
list(name, controller) {
|
|
22
|
+
const byClass = this.#listsFor(name);
|
|
23
|
+
const cls = controller.constructor;
|
|
24
|
+
if (!byClass.has(cls)) byClass.set(cls, []);
|
|
25
|
+
return byClass.get(cls);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
#listsFor(name) {
|
|
30
|
+
if (!this.#lists.has(name)) this.#lists.set(name, new Map());
|
|
31
|
+
return this.#lists.get(name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
applySetupOnce(controller, hookName) {
|
|
36
|
+
const cls = controller.constructor;
|
|
37
|
+
if (!this.#setup.has(cls)) {
|
|
38
|
+
controller[hookName]();
|
|
39
|
+
this.#setup.set(cls, true);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// run the hook once when the instance is constructed and prevent it from being called again
|
|
43
|
+
controller[hookName] = undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
module.exports = { ClassScopedRegistry, appliesToAction };
|
|
@@ -1,36 +1,68 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { ClassScopedRegistry, appliesToAction } = require('./classScopedRegistry');
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
const registry = new ClassScopedRegistry();
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
const InstanceMethods = {
|
|
8
|
+
setupDocumentation() {},
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_applyDocumentation() {
|
|
12
|
+
registry.applySetupOnce(this, 'setupDocumentation');
|
|
9
13
|
},
|
|
10
14
|
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
documentationTag(name, options = {}) {
|
|
17
|
+
registry.list('tags', this).push({ name, options });
|
|
14
18
|
},
|
|
15
19
|
|
|
16
20
|
|
|
21
|
+
tagsForAction(actionName) {
|
|
22
|
+
return registry.list('tags', this)
|
|
23
|
+
.filter(({ options }) => appliesToAction(actionName, options))
|
|
24
|
+
.map(({ name }) => name);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
const StaticMethods = {
|
|
17
30
|
isAuthenticatedAction(actionName) {
|
|
18
|
-
const controller = new this();
|
|
31
|
+
const controller = new this({}, {});
|
|
19
32
|
return controller.isAuthenticatedAction(actionName);
|
|
20
33
|
},
|
|
21
34
|
|
|
22
35
|
|
|
23
36
|
_authenticationDocumentation(actionName) {
|
|
24
37
|
if (!this.isAuthenticatedAction(actionName)) return undefined;
|
|
38
|
+
return [this.authenticationDocumentation(actionName)];
|
|
25
39
|
},
|
|
26
40
|
|
|
27
41
|
|
|
28
42
|
authenticationDocumentation(actionName) {
|
|
29
43
|
return {apiKeyAuth: []};
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_tagsDocumentation(actionName) {
|
|
48
|
+
const controller = new this({}, {});
|
|
49
|
+
return controller.tagsForAction(actionName);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
documentationTagDefinitions() {
|
|
54
|
+
const controller = new this({}, {});
|
|
55
|
+
const list = registry.list('tags', controller);
|
|
56
|
+
const definitions = new Map();
|
|
57
|
+
for (const { name, options } of list) {
|
|
58
|
+
if (!definitions.has(name)) definitions.set(name, options.description || '');
|
|
59
|
+
}
|
|
60
|
+
return [...definitions].map(([name, description]) => ({ name, description }));
|
|
30
61
|
}
|
|
31
62
|
}
|
|
32
63
|
|
|
33
64
|
|
|
34
65
|
module.exports = {
|
|
66
|
+
InstanceMethods,
|
|
35
67
|
StaticMethods
|
|
36
68
|
}
|