@rvoh/psychic 1.5.4 → 1.6.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/CHANGELOG.md +82 -0
- package/dist/cjs/src/bin/helpers/printRoutes.js +3 -6
- package/dist/cjs/src/bin/index.js +13 -11
- package/dist/cjs/src/cli/index.js +1 -1
- package/dist/cjs/src/controller/index.js +199 -6
- package/dist/cjs/src/error/openapi/OpenapResponseValidationFailure.js +9 -0
- package/dist/cjs/src/error/openapi/OpenapiRequestValidationFailure.js +9 -0
- package/dist/cjs/src/error/openapi/OpenapiValidationFailure.js +12 -0
- package/dist/cjs/src/error/psychic-app/must-call-psychic-app-init-first.js +22 -0
- package/dist/cjs/src/helpers/validateOpenApiSchema.js +146 -0
- package/dist/cjs/src/openapi-renderer/app.js +8 -9
- package/dist/cjs/src/openapi-renderer/defaults.js +43 -2
- package/dist/cjs/src/openapi-renderer/endpoint.js +73 -2
- package/dist/cjs/src/openapi-renderer/helpers/OpenapiPayloadValidator.js +254 -0
- package/dist/cjs/src/psychic-app/cache.js +5 -1
- package/dist/cjs/src/psychic-app/index.js +142 -14
- package/dist/cjs/src/psychic-app/openapi-cache.js +79 -0
- package/dist/cjs/src/router/index.js +34 -164
- package/dist/cjs/src/router/route-computer.js +209 -0
- package/dist/cjs/src/server/index.js +12 -0
- package/dist/esm/src/bin/helpers/printRoutes.js +3 -6
- package/dist/esm/src/bin/index.js +12 -10
- package/dist/esm/src/cli/index.js +1 -1
- package/dist/esm/src/controller/index.js +199 -6
- package/dist/esm/src/error/openapi/OpenapResponseValidationFailure.js +3 -0
- package/dist/esm/src/error/openapi/OpenapiRequestValidationFailure.js +3 -0
- package/dist/esm/src/error/openapi/OpenapiValidationFailure.js +9 -0
- package/dist/esm/src/error/psychic-app/must-call-psychic-app-init-first.js +19 -0
- package/dist/esm/src/helpers/validateOpenApiSchema.js +138 -0
- package/dist/esm/src/openapi-renderer/app.js +8 -9
- package/dist/esm/src/openapi-renderer/defaults.js +43 -2
- package/dist/esm/src/openapi-renderer/endpoint.js +72 -1
- package/dist/esm/src/openapi-renderer/helpers/OpenapiPayloadValidator.js +248 -0
- package/dist/esm/src/psychic-app/cache.js +2 -1
- package/dist/esm/src/psychic-app/index.js +142 -14
- package/dist/esm/src/psychic-app/openapi-cache.js +49 -0
- package/dist/esm/src/router/index.js +36 -164
- package/dist/esm/src/router/route-computer.js +201 -0
- package/dist/esm/src/server/index.js +12 -0
- package/dist/types/src/bin/helpers/printRoutes.d.ts +1 -1
- package/dist/types/src/bin/index.d.ts +2 -2
- package/dist/types/src/controller/index.d.ts +150 -1
- package/dist/types/src/error/openapi/OpenapResponseValidationFailure.d.ts +3 -0
- package/dist/types/src/error/openapi/OpenapiRequestValidationFailure.d.ts +3 -0
- package/dist/types/src/error/openapi/OpenapiValidationFailure.d.ts +13 -0
- package/dist/types/src/error/psychic-app/must-call-psychic-app-init-first.d.ts +4 -0
- package/dist/types/src/helpers/validateOpenApiSchema.d.ts +75 -0
- package/dist/types/src/openapi-renderer/app.d.ts +1 -1
- package/dist/types/src/openapi-renderer/endpoint.d.ts +112 -9
- package/dist/types/src/openapi-renderer/helpers/OpenapiPayloadValidator.d.ts +138 -0
- package/dist/types/src/psychic-app/index.d.ts +156 -14
- package/dist/types/src/psychic-app/openapi-cache.d.ts +28 -0
- package/dist/types/src/router/helpers.d.ts +4 -3
- package/dist/types/src/router/index.d.ts +7 -21
- package/dist/types/src/router/route-computer.d.ts +54 -0
- package/dist/types/src/server/index.d.ts +1 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,85 @@
|
|
|
1
|
+
## 1.6.0
|
|
2
|
+
|
|
3
|
+
enables validation to be added to both openapi configurations, as well as to `OpenAPI` decorator calls, enabling the developer to granularly control validation logic for their endpoints.
|
|
4
|
+
|
|
5
|
+
To leverage global config:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// conf/app.ts
|
|
9
|
+
export default async (psy: PsychicApp) => {
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
psy.set('openapi', {
|
|
13
|
+
// ...
|
|
14
|
+
validate: {
|
|
15
|
+
headers: true,
|
|
16
|
+
requestBody: true,
|
|
17
|
+
query: true,
|
|
18
|
+
responseBody: AppEnv.isTest,
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
To leverage endpoint config:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// controllers/PetsController
|
|
28
|
+
export default class PetsController {
|
|
29
|
+
@OpenAPI(Pet, {
|
|
30
|
+
...
|
|
31
|
+
validate: {
|
|
32
|
+
headers: true,
|
|
33
|
+
requestBody: true,
|
|
34
|
+
query: true,
|
|
35
|
+
responseBody: AppEnv.isTest,
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
public async index() {
|
|
39
|
+
...
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This PR additionally formally introduces a new possible error type for 400 status codes, and to help distinguish, it also introduces a `type` field, which can be either `openapi` or `dream` to aid the developer in easily handling the various cases.
|
|
45
|
+
|
|
46
|
+
We have made a conscious decision to render openapi errors in the exact format that ajv returns, since it empowers the developer to utilize tools which can already respond to ajv errors.
|
|
47
|
+
|
|
48
|
+
For added flexibility, this PR includes the ability to provide configuration overrides for the ajv instance, as well as the ability to provide an initialization function to override ajv behavior, since much of the configuration for ajv is driven by method calls, rather than simple config.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// controllers/PetsController
|
|
52
|
+
export default class PetsController {
|
|
53
|
+
@OpenAPI(Pet, {
|
|
54
|
+
...
|
|
55
|
+
validate: {
|
|
56
|
+
ajvOptions: {
|
|
57
|
+
// this is off by default, but you will
|
|
58
|
+
// always want to keep this off in prod
|
|
59
|
+
// to avoid DoS vulnerabilities
|
|
60
|
+
allErrors: AppEnv.isTest,
|
|
61
|
+
|
|
62
|
+
// provide a custom init function to further
|
|
63
|
+
// configure your ajv instance before validating
|
|
64
|
+
init: ajv => {
|
|
65
|
+
ajv.addFormat('myFormat', {
|
|
66
|
+
type: 'string',
|
|
67
|
+
validate: data => MY_FORMAT_REGEX.test(data),
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
public async index() {
|
|
74
|
+
...
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 1.5.5
|
|
80
|
+
|
|
81
|
+
- ensure that openapi-typescript and typescript are not required dependencies when running migrations with --skip-sync flag
|
|
82
|
+
|
|
1
83
|
## 1.5.4
|
|
2
84
|
|
|
3
85
|
- fix issue when providing the `including` argument exclusively to an OpenAPI decorator's `requestBody`
|
|
@@ -5,12 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.default = printRoutes;
|
|
7
7
|
const yoctocolors_1 = __importDefault(require("yoctocolors"));
|
|
8
|
-
const index_js_1 = __importDefault(require("../../
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
await server.boot();
|
|
12
|
-
const routes = await server.routes();
|
|
13
|
-
const expressions = buildExpressions(routes);
|
|
8
|
+
const index_js_1 = __importDefault(require("../../psychic-app/index.js"));
|
|
9
|
+
function printRoutes() {
|
|
10
|
+
const expressions = buildExpressions(index_js_1.default.getOrFail().routesCache);
|
|
14
11
|
// NOTE: intentionally doing this outside of the expression loop below
|
|
15
12
|
// to avoid N+1
|
|
16
13
|
const desiredFirstGapSpaceCount = calculateNumSpacesInFirstGap(expressions);
|
|
@@ -35,11 +35,9 @@ const resource_js_1 = __importDefault(require("../generate/resource.js"));
|
|
|
35
35
|
const isObject_js_1 = __importDefault(require("../helpers/isObject.js"));
|
|
36
36
|
const app_js_1 = __importDefault(require("../openapi-renderer/app.js"));
|
|
37
37
|
const index_js_1 = __importDefault(require("../psychic-app/index.js"));
|
|
38
|
-
const index_js_2 = __importDefault(require("../server/index.js"));
|
|
39
38
|
const enumsFileStr_js_1 = __importDefault(require("./helpers/enumsFileStr.js"));
|
|
40
39
|
const generateRouteTypes_js_1 = __importDefault(require("./helpers/generateRouteTypes.js"));
|
|
41
40
|
const printRoutes_js_1 = __importDefault(require("./helpers/printRoutes.js"));
|
|
42
|
-
const syncOpenapiTypescriptFiles_js_1 = __importDefault(require("./helpers/syncOpenapiTypescriptFiles.js"));
|
|
43
41
|
class PsychicBin {
|
|
44
42
|
static async generateController(controllerName, actions) {
|
|
45
43
|
await (0, controller_js_1.default)({
|
|
@@ -51,8 +49,8 @@ class PsychicBin {
|
|
|
51
49
|
static async generateResource(route, fullyQualifiedModelName, columnsWithTypes, options) {
|
|
52
50
|
await (0, resource_js_1.default)({ route, fullyQualifiedModelName, columnsWithTypes, options });
|
|
53
51
|
}
|
|
54
|
-
static
|
|
55
|
-
|
|
52
|
+
static printRoutes() {
|
|
53
|
+
(0, printRoutes_js_1.default)();
|
|
56
54
|
}
|
|
57
55
|
static async sync({ bypassDreamSync = false, schemaOnly = false, } = {}) {
|
|
58
56
|
if (!bypassDreamSync)
|
|
@@ -77,7 +75,7 @@ class PsychicBin {
|
|
|
77
75
|
try {
|
|
78
76
|
await this.syncOpenapiJson();
|
|
79
77
|
await this.runCliHooksAndUpdatePsychicTypesFileWithOutput();
|
|
80
|
-
await this.
|
|
78
|
+
await this.syncOpenapiTypescriptFiles();
|
|
81
79
|
}
|
|
82
80
|
catch (error) {
|
|
83
81
|
console.error(error);
|
|
@@ -90,9 +88,16 @@ class PsychicBin {
|
|
|
90
88
|
await TypesBuilder_js_1.default.sync(customTypes);
|
|
91
89
|
dream_1.DreamCLI.logger.logEndProgress();
|
|
92
90
|
}
|
|
93
|
-
static async
|
|
91
|
+
static async syncOpenapiTypescriptFiles() {
|
|
94
92
|
dream_1.DreamCLI.logger.logStartProgress(`syncing openapi types...`);
|
|
95
|
-
|
|
93
|
+
// https://rvohealth.atlassian.net/browse/PDTC-8359
|
|
94
|
+
// by dynamically importing this file, we prevent both openapi-typescript
|
|
95
|
+
// and typescript from being required as dependencies, since in production
|
|
96
|
+
// environments these won't be installed. By running migrations with
|
|
97
|
+
// --skip-sync, this function will never run, preventing the file which
|
|
98
|
+
// requires the dev dependencies from ever being imported.
|
|
99
|
+
const syncOpenapiTypescriptFiles = (await Promise.resolve().then(() => __importStar(require('./helpers/syncOpenapiTypescriptFiles.js')))).default;
|
|
100
|
+
await syncOpenapiTypescriptFiles();
|
|
96
101
|
dream_1.DreamCLI.logger.logEndProgress();
|
|
97
102
|
}
|
|
98
103
|
static async syncOpenapiJson() {
|
|
@@ -102,10 +107,7 @@ class PsychicBin {
|
|
|
102
107
|
}
|
|
103
108
|
static async syncRoutes() {
|
|
104
109
|
dream_1.DreamCLI.logger.logStartProgress(`syncing routes...`);
|
|
105
|
-
|
|
106
|
-
await server.boot();
|
|
107
|
-
const routes = await server.routes();
|
|
108
|
-
await (0, generateRouteTypes_js_1.default)(routes);
|
|
110
|
+
await (0, generateRouteTypes_js_1.default)(index_js_1.default.getOrFail().routesCache);
|
|
109
111
|
dream_1.DreamCLI.logger.logEndProgress();
|
|
110
112
|
}
|
|
111
113
|
static async syncClientEnums(outfile) {
|
|
@@ -132,7 +132,7 @@ class PsychicCLI {
|
|
|
132
132
|
.description('examines your current models, building a type-map of the associations so that the ORM can understand your relational setup. This is commited to your repo, and synced to the dream repo for consumption within the underlying library.')
|
|
133
133
|
.action(async () => {
|
|
134
134
|
await initializePsychicApp();
|
|
135
|
-
|
|
135
|
+
index_js_1.default.printRoutes();
|
|
136
136
|
process.exit();
|
|
137
137
|
});
|
|
138
138
|
program
|
|
@@ -37,6 +37,7 @@ const Unauthorized_js_1 = __importDefault(require("../error/http/Unauthorized.js
|
|
|
37
37
|
const UnavailableForLegalReasons_js_1 = __importDefault(require("../error/http/UnavailableForLegalReasons.js"));
|
|
38
38
|
const UnprocessableContent_js_1 = __importDefault(require("../error/http/UnprocessableContent.js"));
|
|
39
39
|
const UnsupportedMediaType_js_1 = __importDefault(require("../error/http/UnsupportedMediaType.js"));
|
|
40
|
+
const OpenapiPayloadValidator_js_1 = __importDefault(require("../openapi-renderer/helpers/OpenapiPayloadValidator.js"));
|
|
40
41
|
const params_js_1 = __importDefault(require("../server/params.js"));
|
|
41
42
|
const index_js_1 = __importDefault(require("../session/index.js"));
|
|
42
43
|
const isPaginatedResult_js_1 = __importDefault(require("./helpers/isPaginatedResult.js"));
|
|
@@ -205,9 +206,33 @@ class PsychicController {
|
|
|
205
206
|
casing: 'camel',
|
|
206
207
|
};
|
|
207
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* @returns the request headers
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```ts
|
|
214
|
+
* class MyController extends ApplicationController {
|
|
215
|
+
* public index() {
|
|
216
|
+
* console.log(this.headers)
|
|
217
|
+
* }
|
|
218
|
+
* }
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
208
221
|
get headers() {
|
|
209
222
|
return this.req.headers;
|
|
210
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* @returns the combination of the request uri params, request body, and query
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* class MyController extends ApplicationController {
|
|
230
|
+
* public index() {
|
|
231
|
+
* console.log(this.params)
|
|
232
|
+
* }
|
|
233
|
+
* }
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
211
236
|
get params() {
|
|
212
237
|
const params = {
|
|
213
238
|
...this.req.params,
|
|
@@ -216,9 +241,49 @@ class PsychicController {
|
|
|
216
241
|
};
|
|
217
242
|
return params;
|
|
218
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* @returns the value found for a particular param
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```ts
|
|
249
|
+
* class MyController extends ApplicationController {
|
|
250
|
+
* public index() {
|
|
251
|
+
* console.log(this.param('myParam'))
|
|
252
|
+
* }
|
|
253
|
+
* }
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
219
256
|
param(key) {
|
|
220
257
|
return this.params[key];
|
|
221
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* finds the specified param, and validates it against
|
|
261
|
+
* the provided type. If the param does not match the specified
|
|
262
|
+
* validation arguments, ParamValidationError is raised, which
|
|
263
|
+
* Psychic will catch and convert into a 400 response.
|
|
264
|
+
*
|
|
265
|
+
* @returns the value found for a particular param
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```ts
|
|
269
|
+
* class MyController extends ApplicationController {
|
|
270
|
+
* public index() {
|
|
271
|
+
* const id = this.castParam('id', 'bigint')
|
|
272
|
+
* }
|
|
273
|
+
* }
|
|
274
|
+
* ```
|
|
275
|
+
*
|
|
276
|
+
* You can provide additional restrictions using the options arg:
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```ts
|
|
280
|
+
* class MyController extends ApplicationController {
|
|
281
|
+
* public index() {
|
|
282
|
+
* const type = this.castParam('type', 'string', { enum: ['Type1', 'Type2'], allowNull: true })
|
|
283
|
+
* }
|
|
284
|
+
* }
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
222
287
|
castParam(key, expectedType, opts) {
|
|
223
288
|
try {
|
|
224
289
|
return this._castParam(key.split('.'), this.params, expectedType, opts);
|
|
@@ -247,6 +312,28 @@ class PsychicController {
|
|
|
247
312
|
}
|
|
248
313
|
return this._castParam(keys, nestedParams, expectedType, opts);
|
|
249
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Captures params for the provided model. Will exclude params that are not
|
|
317
|
+
* considered "safe" by default. It will use `castParam` for each of the
|
|
318
|
+
* params, and will raise an exception if any of those params does not
|
|
319
|
+
* pass validation.
|
|
320
|
+
*
|
|
321
|
+
* @param dreamClass - the dream class you wish to retreive params for
|
|
322
|
+
* @param opts - optional configuration object
|
|
323
|
+
* @param opts.only - optional: restrict the list of allowed params
|
|
324
|
+
* @param opts.including - optional: include params that would normally be excluded.
|
|
325
|
+
*
|
|
326
|
+
* @returns a typed object, containing the casted params for this dream class
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```ts
|
|
330
|
+
* class MyController extends ApplicationController {
|
|
331
|
+
* public index() {
|
|
332
|
+
* const params = this.paramsFor(User, { only: ['email'], including: ['createdAt'] })
|
|
333
|
+
* }
|
|
334
|
+
* }
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
250
337
|
paramsFor(dreamClass, opts) {
|
|
251
338
|
return params_js_1.default.for(opts?.key ? this.params[opts.key] || {} : this.params, dreamClass,
|
|
252
339
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -313,15 +400,27 @@ class PsychicController {
|
|
|
313
400
|
}
|
|
314
401
|
json(data, opts = {}) {
|
|
315
402
|
if (Array.isArray(data))
|
|
316
|
-
return this.
|
|
403
|
+
return this.validateAndRenderJsonResponse(data.map(d =>
|
|
317
404
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
318
405
|
this.singleObjectJson(d, opts)));
|
|
319
406
|
if ((0, isPaginatedResult_js_1.default)(data))
|
|
320
|
-
return this.
|
|
407
|
+
return this.validateAndRenderJsonResponse({
|
|
321
408
|
...data,
|
|
322
409
|
results: data.results.map(result => this.singleObjectJson(result, opts)),
|
|
323
410
|
});
|
|
324
|
-
return this.
|
|
411
|
+
return this.validateAndRenderJsonResponse(this.singleObjectJson(data, opts));
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Runs the data through openapi response validation, and then renders
|
|
415
|
+
* the data if no errors were found.
|
|
416
|
+
*
|
|
417
|
+
* @param data - the data to validate and render
|
|
418
|
+
*/
|
|
419
|
+
validateAndRenderJsonResponse(
|
|
420
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
421
|
+
data) {
|
|
422
|
+
this.validateOpenapiResponseBody(data);
|
|
423
|
+
this.res.json(data);
|
|
325
424
|
}
|
|
326
425
|
defaultSerializerPassthrough = {};
|
|
327
426
|
serializerPassthrough(passthrough) {
|
|
@@ -343,7 +442,8 @@ class PsychicController {
|
|
|
343
442
|
const realStatus = (typeof status_codes_js_1.default[status] === 'string'
|
|
344
443
|
? status_codes_js_1.default[status_codes_js_1.default[status]]
|
|
345
444
|
: status_codes_js_1.default[status]);
|
|
346
|
-
this.res.status(realStatus)
|
|
445
|
+
this.res.status(realStatus);
|
|
446
|
+
this.json(body);
|
|
347
447
|
}
|
|
348
448
|
redirect(path) {
|
|
349
449
|
this.res.redirect(path);
|
|
@@ -380,12 +480,14 @@ class PsychicController {
|
|
|
380
480
|
// 205
|
|
381
481
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
382
482
|
resetContent(message = undefined) {
|
|
383
|
-
this.res.status(205)
|
|
483
|
+
this.res.status(205);
|
|
484
|
+
this.json(message);
|
|
384
485
|
}
|
|
385
486
|
// 208
|
|
386
487
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
387
488
|
alreadyReported(message = undefined) {
|
|
388
|
-
this.res.status(208)
|
|
489
|
+
this.res.status(208);
|
|
490
|
+
this.json(message);
|
|
389
491
|
}
|
|
390
492
|
// 301
|
|
391
493
|
movedPermanently(newLocation) {
|
|
@@ -562,13 +664,104 @@ class PsychicController {
|
|
|
562
664
|
throw new NotExtended_js_1.default(message);
|
|
563
665
|
}
|
|
564
666
|
// end: http status codes
|
|
667
|
+
/**
|
|
668
|
+
* @internal
|
|
669
|
+
*
|
|
670
|
+
* Called by the psychic router when the endpoint for
|
|
671
|
+
* this controller was hit.
|
|
672
|
+
*
|
|
673
|
+
* @param action - the action to use when validating the query params.
|
|
674
|
+
*/
|
|
565
675
|
async runAction(action) {
|
|
566
676
|
await this.runBeforeActionsFor(action);
|
|
567
677
|
if (this.res.headersSent)
|
|
568
678
|
return;
|
|
679
|
+
this.validateOpenapiHeadersForAction(action);
|
|
680
|
+
this.validateOpenapiQueryForAction(action);
|
|
681
|
+
this.validateOpenapiRequestBodyForAction(action);
|
|
569
682
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
|
|
570
683
|
await this[action]();
|
|
571
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* Validates the request body.
|
|
687
|
+
* If an OpenAPI decorator was attached to this endpoint,
|
|
688
|
+
* the computed headers for that decorator will
|
|
689
|
+
* be used to validate this endpoint.
|
|
690
|
+
*
|
|
691
|
+
* @param action - the action to use when validating the query params.
|
|
692
|
+
*/
|
|
693
|
+
validateOpenapiRequestBodyForAction(action) {
|
|
694
|
+
const openapiEndpointRenderer = this.constructor.openapi?.[action];
|
|
695
|
+
if (!openapiEndpointRenderer)
|
|
696
|
+
return;
|
|
697
|
+
this.computedOpenapiNames.forEach(openapiName => {
|
|
698
|
+
new OpenapiPayloadValidator_js_1.default(openapiName, openapiEndpointRenderer).validateOpenapiRequestBody(this.req.body);
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Validates the request headers.
|
|
703
|
+
* If an OpenAPI decorator was attached to this endpoint,
|
|
704
|
+
* the computed headers for that decorator will
|
|
705
|
+
* be used to validate this endpoint.
|
|
706
|
+
*
|
|
707
|
+
* @param action - the action to use when validating the query params.
|
|
708
|
+
*/
|
|
709
|
+
validateOpenapiHeadersForAction(action) {
|
|
710
|
+
const openapiEndpointRenderer = this.constructor.openapi?.[action];
|
|
711
|
+
if (!openapiEndpointRenderer)
|
|
712
|
+
return;
|
|
713
|
+
this.computedOpenapiNames.forEach(openapiName => {
|
|
714
|
+
new OpenapiPayloadValidator_js_1.default(openapiName, openapiEndpointRenderer).validateOpenapiHeaders(this.req.headers);
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Validates the request query params.
|
|
719
|
+
* If an OpenAPI decorator was attached to this endpoint,
|
|
720
|
+
* the computed query params for that decorator will
|
|
721
|
+
* be used to validate this endpoint.
|
|
722
|
+
*
|
|
723
|
+
* @param action - the action to use when validating the query params.
|
|
724
|
+
*/
|
|
725
|
+
validateOpenapiQueryForAction(action) {
|
|
726
|
+
const openapiEndpointRenderer = this.constructor.openapi?.[action];
|
|
727
|
+
if (!openapiEndpointRenderer)
|
|
728
|
+
return;
|
|
729
|
+
this.computedOpenapiNames.forEach(openapiName => {
|
|
730
|
+
new OpenapiPayloadValidator_js_1.default(openapiName, openapiEndpointRenderer).validateOpenapiQuery(this.req.query);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Validates the data in the context of a response body.
|
|
735
|
+
* If an OpenAPI decorator was attached to this endpoint,
|
|
736
|
+
* the computed response schema for that decorator will
|
|
737
|
+
* be used to validate this endpoint based on the status code
|
|
738
|
+
* set.
|
|
739
|
+
*
|
|
740
|
+
* @param data - the response body data to render
|
|
741
|
+
*/
|
|
742
|
+
validateOpenapiResponseBody(
|
|
743
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
744
|
+
data) {
|
|
745
|
+
const openapiEndpointRenderer = this.constructor.openapi?.[this.action];
|
|
746
|
+
if (!openapiEndpointRenderer)
|
|
747
|
+
return;
|
|
748
|
+
this.computedOpenapiNames.forEach(openapiName => {
|
|
749
|
+
new OpenapiPayloadValidator_js_1.default(openapiName, openapiEndpointRenderer).validateOpenapiResponseBody(data, this.res.statusCode);
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* @internal
|
|
754
|
+
*
|
|
755
|
+
* @returns the openapiNames set on the constructor, or else ['default']
|
|
756
|
+
*/
|
|
757
|
+
get computedOpenapiNames() {
|
|
758
|
+
return this.constructor.openapiNames || ['default'];
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* @internal
|
|
762
|
+
*
|
|
763
|
+
* Runs the before actions for a particular action on a controller
|
|
764
|
+
*/
|
|
572
765
|
async runBeforeActionsFor(action) {
|
|
573
766
|
const beforeActions = this.constructor.controllerHooks.filter(hook => hook.shouldFireForAction(action));
|
|
574
767
|
for (const hook of beforeActions) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const OpenapiValidationFailure_js_1 = __importDefault(require("./OpenapiValidationFailure.js"));
|
|
7
|
+
class OpenapiResponseValidationFailure extends OpenapiValidationFailure_js_1.default {
|
|
8
|
+
}
|
|
9
|
+
exports.default = OpenapiResponseValidationFailure;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const OpenapiValidationFailure_js_1 = __importDefault(require("./OpenapiValidationFailure.js"));
|
|
7
|
+
class OpenapiRequestValidationFailure extends OpenapiValidationFailure_js_1.default {
|
|
8
|
+
}
|
|
9
|
+
exports.default = OpenapiRequestValidationFailure;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class OpenapiValidationFailure extends Error {
|
|
4
|
+
errors;
|
|
5
|
+
target;
|
|
6
|
+
constructor(errors, target) {
|
|
7
|
+
super();
|
|
8
|
+
this.errors = errors;
|
|
9
|
+
this.target = target;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.default = OpenapiValidationFailure;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class MustCallPsychicAppInitFirst extends Error {
|
|
4
|
+
constructor() {
|
|
5
|
+
super();
|
|
6
|
+
}
|
|
7
|
+
get message() {
|
|
8
|
+
return `
|
|
9
|
+
For some reason, PsychicApp.init was never called. In order for the
|
|
10
|
+
execution context to proceed, you must first ensure that PsychicApp.init
|
|
11
|
+
has been called. Usually, this is done by calling
|
|
12
|
+
|
|
13
|
+
await initializePsychicApp()
|
|
14
|
+
|
|
15
|
+
which is set up in your application boilerplate for you by default for
|
|
16
|
+
the relevant execution contexts. Perhaps it was mistakenly removed, or
|
|
17
|
+
otherwise you are trying to implement a new entrypoint into psychic, at
|
|
18
|
+
which case, please make sure to call 'await initializePsychicApp()' first
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.default = MustCallPsychicAppInitFirst;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = validateOpenApiSchema;
|
|
7
|
+
exports.validateObject = validateObject;
|
|
8
|
+
exports.createValidator = createValidator;
|
|
9
|
+
const ajv_1 = require("ajv");
|
|
10
|
+
const ajv_formats_1 = __importDefault(require("ajv-formats"));
|
|
11
|
+
/**
|
|
12
|
+
* @internal
|
|
13
|
+
*
|
|
14
|
+
* Validates an object against an OpenAPI schema
|
|
15
|
+
* Convenience wrapper around validateObject with OpenAPI-friendly defaults.
|
|
16
|
+
*
|
|
17
|
+
* This function is used internally by the OpenapiPayloadValidator
|
|
18
|
+
* class to correctly validate request body, query, headers,
|
|
19
|
+
* and response body.
|
|
20
|
+
*
|
|
21
|
+
* @param data - Object to validate
|
|
22
|
+
* @param openapiSchema - OpenAPI schema object
|
|
23
|
+
* @param options - (Optional) options to provide when initializing the ajv instance
|
|
24
|
+
* @returns ValidationResult
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* const schema = {
|
|
30
|
+
* type: 'object',
|
|
31
|
+
* properties: {
|
|
32
|
+
* name: { type: 'string' },
|
|
33
|
+
* age: { type: 'integer', minimum: 0 }
|
|
34
|
+
* },
|
|
35
|
+
* required: ['name']
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* const result = validateOpenApiSchema({ name: 'John', age: 30 }, schema)
|
|
39
|
+
* if (result.isValid) {
|
|
40
|
+
* console.log('Valid data:', result.data)
|
|
41
|
+
* } else {
|
|
42
|
+
* console.log('Validation errors:', result.errors)
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
function validateOpenApiSchema(data, openapiSchema, options = {}) {
|
|
47
|
+
return validateObject(data, openapiSchema, {
|
|
48
|
+
removeAdditional: 'failing', // Remove properties that fail validation
|
|
49
|
+
useDefaults: true,
|
|
50
|
+
coerceTypes: true,
|
|
51
|
+
...options,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @internal
|
|
56
|
+
*
|
|
57
|
+
* Validates an object against a JSON schema using AJV
|
|
58
|
+
*
|
|
59
|
+
* @param data - Object to validate
|
|
60
|
+
* @param schema - JSON Schema to validate against
|
|
61
|
+
* @param options - Validation options
|
|
62
|
+
* @returns ValidationResult with success status, validated data, and any errors
|
|
63
|
+
*/
|
|
64
|
+
function validateObject(data, schema, options = {}) {
|
|
65
|
+
try {
|
|
66
|
+
const validate = createValidator(schema, options);
|
|
67
|
+
// Clone the data to prevent AJV from mutating the original object
|
|
68
|
+
const clonedData = structuredClone(data);
|
|
69
|
+
const isValid = validate(clonedData);
|
|
70
|
+
if (isValid) {
|
|
71
|
+
return {
|
|
72
|
+
isValid: true,
|
|
73
|
+
data: clonedData,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
return {
|
|
78
|
+
isValid: false,
|
|
79
|
+
errors: formatAjvErrors(validate.errors || []),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
// Handle schema compilation errors
|
|
85
|
+
return {
|
|
86
|
+
isValid: false,
|
|
87
|
+
errors: [
|
|
88
|
+
{
|
|
89
|
+
instancePath: '',
|
|
90
|
+
schemaPath: '',
|
|
91
|
+
keyword: 'schema',
|
|
92
|
+
message: error instanceof Error ? error.message : 'Schema compilation failed',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* @internal
|
|
100
|
+
*
|
|
101
|
+
* Creates an AJV validator function for validating objects against JSON schemas
|
|
102
|
+
*
|
|
103
|
+
* @param schema - JSON Schema to validate against
|
|
104
|
+
* @param options - Validation options
|
|
105
|
+
* @returns A validator function that can be reused for multiple validations
|
|
106
|
+
*/
|
|
107
|
+
function createValidator(schema, options = {}) {
|
|
108
|
+
const ajvOptions = {
|
|
109
|
+
...options,
|
|
110
|
+
init: undefined,
|
|
111
|
+
};
|
|
112
|
+
const ajv = new ajv_1.Ajv({
|
|
113
|
+
removeAdditional: false,
|
|
114
|
+
useDefaults: true,
|
|
115
|
+
coerceTypes: true,
|
|
116
|
+
strict: false, // Allow unknown keywords for OpenAPI compatibility
|
|
117
|
+
allErrors: options.allErrors || false, // Collect all errors, not just the first one
|
|
118
|
+
validateFormats: true, // Enable format validation for date-time, email, etc.
|
|
119
|
+
...ajvOptions,
|
|
120
|
+
});
|
|
121
|
+
ajv_formats_1.default(ajv);
|
|
122
|
+
ajv.addFormat('decimal', {
|
|
123
|
+
type: 'string',
|
|
124
|
+
validate: data => DECIMAL_REGEX.test(data),
|
|
125
|
+
});
|
|
126
|
+
ajv.addFormat('bigint', {
|
|
127
|
+
type: 'string',
|
|
128
|
+
validate: data => BIGINT_REGEX.test(data),
|
|
129
|
+
});
|
|
130
|
+
options?.init?.(ajv);
|
|
131
|
+
return ajv.compile(schema);
|
|
132
|
+
}
|
|
133
|
+
const DECIMAL_REGEX = /^-?(\d+\.?\d*|\.\d+)$/;
|
|
134
|
+
const BIGINT_REGEX = /^-?\d+(\.0*)?$/;
|
|
135
|
+
/**
|
|
136
|
+
* Formats AJV errors into a more readable format
|
|
137
|
+
*/
|
|
138
|
+
function formatAjvErrors(ajvErrors) {
|
|
139
|
+
return ajvErrors.map(error => ({
|
|
140
|
+
instancePath: error.instancePath,
|
|
141
|
+
schemaPath: error.schemaPath,
|
|
142
|
+
keyword: error.keyword,
|
|
143
|
+
message: error.message || 'Validation failed',
|
|
144
|
+
params: error.params,
|
|
145
|
+
}));
|
|
146
|
+
}
|