@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/cjs/src/bin/helpers/printRoutes.js +3 -6
  3. package/dist/cjs/src/bin/index.js +13 -11
  4. package/dist/cjs/src/cli/index.js +1 -1
  5. package/dist/cjs/src/controller/index.js +199 -6
  6. package/dist/cjs/src/error/openapi/OpenapResponseValidationFailure.js +9 -0
  7. package/dist/cjs/src/error/openapi/OpenapiRequestValidationFailure.js +9 -0
  8. package/dist/cjs/src/error/openapi/OpenapiValidationFailure.js +12 -0
  9. package/dist/cjs/src/error/psychic-app/must-call-psychic-app-init-first.js +22 -0
  10. package/dist/cjs/src/helpers/validateOpenApiSchema.js +146 -0
  11. package/dist/cjs/src/openapi-renderer/app.js +8 -9
  12. package/dist/cjs/src/openapi-renderer/defaults.js +43 -2
  13. package/dist/cjs/src/openapi-renderer/endpoint.js +73 -2
  14. package/dist/cjs/src/openapi-renderer/helpers/OpenapiPayloadValidator.js +254 -0
  15. package/dist/cjs/src/psychic-app/cache.js +5 -1
  16. package/dist/cjs/src/psychic-app/index.js +142 -14
  17. package/dist/cjs/src/psychic-app/openapi-cache.js +79 -0
  18. package/dist/cjs/src/router/index.js +34 -164
  19. package/dist/cjs/src/router/route-computer.js +209 -0
  20. package/dist/cjs/src/server/index.js +12 -0
  21. package/dist/esm/src/bin/helpers/printRoutes.js +3 -6
  22. package/dist/esm/src/bin/index.js +12 -10
  23. package/dist/esm/src/cli/index.js +1 -1
  24. package/dist/esm/src/controller/index.js +199 -6
  25. package/dist/esm/src/error/openapi/OpenapResponseValidationFailure.js +3 -0
  26. package/dist/esm/src/error/openapi/OpenapiRequestValidationFailure.js +3 -0
  27. package/dist/esm/src/error/openapi/OpenapiValidationFailure.js +9 -0
  28. package/dist/esm/src/error/psychic-app/must-call-psychic-app-init-first.js +19 -0
  29. package/dist/esm/src/helpers/validateOpenApiSchema.js +138 -0
  30. package/dist/esm/src/openapi-renderer/app.js +8 -9
  31. package/dist/esm/src/openapi-renderer/defaults.js +43 -2
  32. package/dist/esm/src/openapi-renderer/endpoint.js +72 -1
  33. package/dist/esm/src/openapi-renderer/helpers/OpenapiPayloadValidator.js +248 -0
  34. package/dist/esm/src/psychic-app/cache.js +2 -1
  35. package/dist/esm/src/psychic-app/index.js +142 -14
  36. package/dist/esm/src/psychic-app/openapi-cache.js +49 -0
  37. package/dist/esm/src/router/index.js +36 -164
  38. package/dist/esm/src/router/route-computer.js +201 -0
  39. package/dist/esm/src/server/index.js +12 -0
  40. package/dist/types/src/bin/helpers/printRoutes.d.ts +1 -1
  41. package/dist/types/src/bin/index.d.ts +2 -2
  42. package/dist/types/src/controller/index.d.ts +150 -1
  43. package/dist/types/src/error/openapi/OpenapResponseValidationFailure.d.ts +3 -0
  44. package/dist/types/src/error/openapi/OpenapiRequestValidationFailure.d.ts +3 -0
  45. package/dist/types/src/error/openapi/OpenapiValidationFailure.d.ts +13 -0
  46. package/dist/types/src/error/psychic-app/must-call-psychic-app-init-first.d.ts +4 -0
  47. package/dist/types/src/helpers/validateOpenApiSchema.d.ts +75 -0
  48. package/dist/types/src/openapi-renderer/app.d.ts +1 -1
  49. package/dist/types/src/openapi-renderer/endpoint.d.ts +112 -9
  50. package/dist/types/src/openapi-renderer/helpers/OpenapiPayloadValidator.d.ts +138 -0
  51. package/dist/types/src/psychic-app/index.d.ts +156 -14
  52. package/dist/types/src/psychic-app/openapi-cache.d.ts +28 -0
  53. package/dist/types/src/router/helpers.d.ts +4 -3
  54. package/dist/types/src/router/index.d.ts +7 -21
  55. package/dist/types/src/router/route-computer.d.ts +54 -0
  56. package/dist/types/src/server/index.d.ts +1 -0
  57. 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("../../server/index.js"));
9
- async function printRoutes() {
10
- const server = new index_js_1.default();
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 async routes() {
55
- await (0, printRoutes_js_1.default)();
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.syncTypescriptOpenapiFiles();
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 syncTypescriptOpenapiFiles() {
91
+ static async syncOpenapiTypescriptFiles() {
94
92
  dream_1.DreamCLI.logger.logStartProgress(`syncing openapi types...`);
95
- await (0, syncOpenapiTypescriptFiles_js_1.default)();
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
- const server = new index_js_2.default();
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
- await index_js_1.default.routes();
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.res.json(data.map(d =>
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.res.json({
407
+ return this.validateAndRenderJsonResponse({
321
408
  ...data,
322
409
  results: data.results.map(result => this.singleObjectJson(result, opts)),
323
410
  });
324
- return this.res.json(this.singleObjectJson(data, opts));
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).json(body);
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).send(message);
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).send(message);
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
+ }