@rvoh/psychic 2.3.8 → 3.0.0-alpha.1

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 (65) hide show
  1. package/dist/cjs/src/cli/index.js +4 -0
  2. package/dist/cjs/src/controller/helpers/logIfDevelopment.js +5 -5
  3. package/dist/cjs/src/controller/index.js +119 -40
  4. package/dist/cjs/src/devtools/helpers/launchDevServer.js +15 -1
  5. package/dist/cjs/src/error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.js +44 -0
  6. package/dist/cjs/src/error/router/cannot-commit-routes-without-koa-app.js +12 -0
  7. package/dist/cjs/src/helpers/toJson.js +2 -8
  8. package/dist/cjs/src/helpers/validateOpenApiSchema.js +1 -1
  9. package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +2 -2
  10. package/dist/cjs/src/openapi-renderer/endpoint.js +2 -2
  11. package/dist/cjs/src/openapi-renderer/helpers/OpenapiPayloadValidator.js +75 -9
  12. package/dist/cjs/src/openapi-renderer/helpers/{dreamAttributeOpenapiShape.js → dreamColumnOpenapiShape.js} +19 -6
  13. package/dist/cjs/src/openapi-renderer/helpers/stringify-cache.js +55 -0
  14. package/dist/cjs/src/openapi-renderer/helpers/validator-cache.js +52 -0
  15. package/dist/cjs/src/psychic-app/helpers/import/importControllers.js +1 -1
  16. package/dist/cjs/src/psychic-app/index.js +3 -10
  17. package/dist/cjs/src/router/index.js +31 -25
  18. package/dist/cjs/src/server/helpers/startPsychicServer.js +6 -2
  19. package/dist/cjs/src/server/index.js +32 -35
  20. package/dist/cjs/src/server/params.js +56 -3
  21. package/dist/cjs/src/session/index.js +9 -12
  22. package/dist/esm/src/cli/index.js +4 -0
  23. package/dist/esm/src/controller/helpers/logIfDevelopment.js +5 -5
  24. package/dist/esm/src/controller/index.js +119 -40
  25. package/dist/esm/src/devtools/helpers/launchDevServer.js +15 -1
  26. package/dist/esm/src/error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.js +44 -0
  27. package/dist/esm/src/error/router/cannot-commit-routes-without-koa-app.js +12 -0
  28. package/dist/esm/src/helpers/toJson.js +2 -8
  29. package/dist/esm/src/helpers/validateOpenApiSchema.js +1 -1
  30. package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +2 -2
  31. package/dist/esm/src/openapi-renderer/endpoint.js +2 -2
  32. package/dist/esm/src/openapi-renderer/helpers/OpenapiPayloadValidator.js +75 -9
  33. package/dist/esm/src/openapi-renderer/helpers/{dreamAttributeOpenapiShape.js → dreamColumnOpenapiShape.js} +19 -6
  34. package/dist/esm/src/openapi-renderer/helpers/stringify-cache.js +55 -0
  35. package/dist/esm/src/openapi-renderer/helpers/validator-cache.js +52 -0
  36. package/dist/esm/src/psychic-app/helpers/import/importControllers.js +1 -1
  37. package/dist/esm/src/psychic-app/index.js +3 -10
  38. package/dist/esm/src/router/index.js +31 -25
  39. package/dist/esm/src/server/helpers/startPsychicServer.js +6 -2
  40. package/dist/esm/src/server/index.js +32 -35
  41. package/dist/esm/src/server/params.js +56 -3
  42. package/dist/esm/src/session/index.js +9 -12
  43. package/dist/types/src/controller/helpers/logIfDevelopment.d.ts +3 -4
  44. package/dist/types/src/controller/index.d.ts +19 -8
  45. package/dist/types/src/devtools/helpers/launchDevServer.d.ts +2 -1
  46. package/dist/types/src/error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.d.ts +7 -0
  47. package/dist/types/src/error/router/cannot-commit-routes-without-koa-app.d.ts +3 -0
  48. package/dist/types/src/helpers/cookieMaxAgeFromCookieOpts.d.ts +1 -1
  49. package/dist/types/src/helpers/toJson.d.ts +1 -1
  50. package/dist/types/src/helpers/validateOpenApiSchema.d.ts +5 -1
  51. package/dist/types/src/openapi-renderer/helpers/OpenapiPayloadValidator.d.ts +41 -0
  52. package/dist/types/src/openapi-renderer/helpers/{dreamAttributeOpenapiShape.d.ts → dreamColumnOpenapiShape.d.ts} +1 -1
  53. package/dist/types/src/openapi-renderer/helpers/stringify-cache.d.ts +34 -0
  54. package/dist/types/src/openapi-renderer/helpers/validator-cache.d.ts +35 -0
  55. package/dist/types/src/psychic-app/index.d.ts +11 -14
  56. package/dist/types/src/router/index.d.ts +17 -17
  57. package/dist/types/src/router/route-manager.d.ts +4 -3
  58. package/dist/types/src/server/helpers/startPsychicServer.d.ts +3 -3
  59. package/dist/types/src/server/index.d.ts +3 -3
  60. package/dist/types/src/server/params.d.ts +2 -2
  61. package/dist/types/src/session/index.d.ts +13 -5
  62. package/package.json +30 -19
  63. package/dist/cjs/src/error/router/cannot-commit-routes-without-express-app.js +0 -12
  64. package/dist/esm/src/error/router/cannot-commit-routes-without-express-app.js +0 -12
  65. package/dist/types/src/error/router/cannot-commit-routes-without-express-app.d.ts +0 -3
@@ -14,7 +14,7 @@ import NoSerializerFoundForRendersOneAndMany from '../error/openapi/NoSerializer
14
14
  import ObjectSerializerRendersOneAndManyRequireClassType from '../error/openapi/ObjectSerializerRendersOneAndManyRequireClassType.js';
15
15
  import allSerializersFromHandWrittenOpenapi from './helpers/allSerializersFromHandWrittenOpenapi.js';
16
16
  import allSerializersToRefsInOpenapi from './helpers/allSerializersToRefsInOpenapi.js';
17
- import { dreamColumnOpenapiShape } from './helpers/dreamAttributeOpenapiShape.js';
17
+ import { dreamColumnOpenapiShape } from './helpers/dreamColumnOpenapiShape.js';
18
18
  import openapiShorthandToOpenapi from './helpers/openapiShorthandToOpenapi.js';
19
19
  const NULL_OBJECT_OPENAPI = { type: 'null' };
20
20
  export default class SerializerOpenapiRenderer {
@@ -153,7 +153,7 @@ export default class SerializerOpenapiRenderer {
153
153
  target = DataTypeForOpenapi;
154
154
  }
155
155
  accumulator[outputAttributeName] = allSerializersToRefsInOpenapi(target?.isDream
156
- ? dreamColumnOpenapiShape(target, attribute.name, openapi, {
156
+ ? dreamColumnOpenapiShape(this.serializer.globalName, target, attribute.name, openapi, {
157
157
  suppressResponseEnums: this.suppressResponseEnums,
158
158
  })
159
159
  : openapiShorthandToOpenapi(openapi));
@@ -10,7 +10,7 @@ import openapiParamNamesForDreamClass from '../server/helpers/openapiParamNamesF
10
10
  import OpenapiSegmentExpander from './body-segment.js';
11
11
  import { DEFAULT_OPENAPI_RESPONSES } from './defaults.js';
12
12
  import cursorPaginationParamOpenapiProperty from './helpers/cursorPaginationParamOpenapiProperty.js';
13
- import { dreamColumnOpenapiShape } from './helpers/dreamAttributeOpenapiShape.js';
13
+ import { dreamColumnOpenapiShape } from './helpers/dreamColumnOpenapiShape.js';
14
14
  import openapiOpts from './helpers/openapiOpts.js';
15
15
  import openapiRoute from './helpers/openapiRoute.js';
16
16
  import paginationPageParamOpenapiProperty from './helpers/paginationPageParamOpenapiProperty.js';
@@ -545,7 +545,7 @@ export default class OpenapiEndpointRenderer {
545
545
  paramsShape.required = required;
546
546
  }
547
547
  paramsShape.properties = paramSafeColumns.reduce((acc, columnName) => {
548
- acc[columnName] = dreamColumnOpenapiShape(dreamClass, columnName, undefined, {
548
+ acc[columnName] = dreamColumnOpenapiShape(this.controllerClass.controllerActionPath(this.action), dreamClass, columnName, undefined, {
549
549
  allowGenericJson: true,
550
550
  });
551
551
  return acc;
@@ -1,8 +1,9 @@
1
1
  import OpenapiRequestValidationFailure from '../../error/openapi/OpenapiRequestValidationFailure.js';
2
2
  import OpenapiResponseValidationFailure from '../../error/openapi/OpenapResponseValidationFailure.js';
3
- import validateOpenApiSchema from '../../helpers/validateOpenApiSchema.js';
3
+ import { createValidator, formatAjvErrors, } from '../../helpers/validateOpenApiSchema.js';
4
4
  import PsychicApp from '../../psychic-app/index.js';
5
5
  import suppressResponseEnumsConfig from './suppressResponseEnumsConfig.js';
6
+ import { cacheValidator, getCachedValidator } from './validator-cache.js';
6
7
  /**
7
8
  * @internal
8
9
  *
@@ -157,16 +158,46 @@ export default class OpenapiPayloadValidator {
157
158
  const openapiEndpointRenderer = this.openapiEndpointRenderer;
158
159
  const openapiName = this.openapiName;
159
160
  if (openapiEndpointRenderer.shouldValidateResponseBody(openapiName)) {
160
- const openapiResponseBody = openapiEndpointRenderer['parseResponses']({
161
- openapiName,
162
- renderOpts: this.renderOpts,
163
- }).openapi?.[statusCode.toString()];
164
- const schema = openapiResponseBody?.['content']?.['application/json']?.['schema'];
161
+ const schema = this.getResponseSchema(statusCode);
165
162
  if (schema) {
166
163
  this.validateOrFail(data, schema, 'responseBody');
167
164
  }
168
165
  }
169
166
  }
167
+ /**
168
+ * @internal
169
+ *
170
+ * Retrieves the OpenAPI response schema for a given status code.
171
+ * Returns undefined if no schema is defined for this endpoint/status code.
172
+ *
173
+ * @param statusCode - the HTTP status code
174
+ * @returns The response schema, or undefined if not found
175
+ */
176
+ getResponseSchema(statusCode) {
177
+ const openapiEndpointRenderer = this.openapiEndpointRenderer;
178
+ const openapiName = this.openapiName;
179
+ const openapiResponseBody = openapiEndpointRenderer['parseResponses']({
180
+ openapiName,
181
+ renderOpts: this.renderOpts,
182
+ }).openapi?.[statusCode.toString()];
183
+ return openapiResponseBody?.['content']?.['application/json']?.['schema'];
184
+ }
185
+ /**
186
+ * @internal
187
+ *
188
+ * Retrieves the OpenAPI response schema for a given status code with
189
+ * all components merged in. This is the schema format needed by
190
+ * fast-json-stringify and AJV validators.
191
+ *
192
+ * @param statusCode - the HTTP status code
193
+ * @returns The response schema with components, or undefined if not found
194
+ */
195
+ getResponseSchemaWithComponents(statusCode) {
196
+ const schema = this.getResponseSchema(statusCode);
197
+ if (!schema)
198
+ return undefined;
199
+ return this.addComponentsToSchema(schema);
200
+ }
170
201
  /**
171
202
  * @internal
172
203
  *
@@ -241,12 +272,47 @@ export default class OpenapiPayloadValidator {
241
272
  validateOrFail(
242
273
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
243
274
  data, openapiSchema, target) {
244
- const validationResults = validateOpenApiSchema(data || {}, this.addComponentsToSchema(openapiSchema), this.getAjvOptions(target));
245
- if (!validationResults.isValid) {
275
+ const cacheKey = this.getCacheKey(target);
276
+ const schemaWithComponents = this.addComponentsToSchema(openapiSchema);
277
+ const ajvOptions = this.getAjvOptions(target);
278
+ const validator = getCachedValidator(cacheKey) ||
279
+ this.compileAndCacheValidator(cacheKey, schemaWithComponents, ajvOptions);
280
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
281
+ const clonedData = structuredClone(data || {});
282
+ const isValid = validator(clonedData);
283
+ if (!isValid) {
246
284
  const errorClass = target === 'responseBody' ? OpenapiResponseValidationFailure : OpenapiRequestValidationFailure;
247
- throw new errorClass(validationResults.errors || [], target);
285
+ throw new errorClass(formatAjvErrors(validator.errors || []), target);
248
286
  }
249
287
  }
288
+ /**
289
+ * @internal
290
+ *
291
+ * Generates a cache key for the validator based on controller, action, openapiName, and target.
292
+ *
293
+ * @param target - the validation target (one of: 'requestBody', 'query', 'headers', 'responseBody')
294
+ * @returns cache key string
295
+ */
296
+ getCacheKey(target) {
297
+ const controllerClass = this.openapiEndpointRenderer['controllerClass'];
298
+ const actionName = this.openapiEndpointRenderer['action'];
299
+ return `${controllerClass.globalName}#${actionName}|${this.openapiName}|${target}`;
300
+ }
301
+ /**
302
+ * @internal
303
+ *
304
+ * Compiles a validator using AJV and caches it for future use.
305
+ *
306
+ * @param cacheKey - the cache key for this validator
307
+ * @param schema - the schema to compile
308
+ * @param options - AJV options
309
+ * @returns compiled validator function
310
+ */
311
+ compileAndCacheValidator(cacheKey, schema, options) {
312
+ const validator = createValidator(schema, options);
313
+ cacheValidator(cacheKey, validator);
314
+ return validator;
315
+ }
250
316
  /**
251
317
  * @internal
252
318
  *
@@ -1,7 +1,13 @@
1
1
  import { SerializingPlainPropertyWithoutOpenapiShape } from '../../error/openapi/SerializingPlainPropertyWithoutOpenapiShape.js';
2
+ import UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute from '../../error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.js';
2
3
  import OpenapiSegmentExpander from '../body-segment.js';
3
4
  import openapiShorthandToOpenapi from './openapiShorthandToOpenapi.js';
4
- export function dreamColumnOpenapiShape(dreamClass, column, openapi = undefined, { suppressResponseEnums = false, allowGenericJson = false, } = {}) {
5
+ export function dreamColumnOpenapiShape(
6
+ // this is the global name of the serializer or controller calling down
7
+ // to get this information. If an unrecognized db type is provided, the
8
+ // source will be rendered in the exception that is returned, enabling
9
+ // the dev to identify the source of the issue and fix it
10
+ source, dreamClass, column, openapi = undefined, { suppressResponseEnums = false, allowGenericJson = false, } = {}) {
5
11
  if (dreamClass.isVirtualColumn(column)) {
6
12
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
7
13
  const openapiObject = openapiShorthandToOpenapi((openapi ?? {}));
@@ -46,7 +52,7 @@ export function dreamColumnOpenapiShape(dreamClass, column, openapi = undefined,
46
52
  }
47
53
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
48
54
  const openapiObject = openapiShorthandToOpenapi((openapi ?? {}));
49
- const singleType = singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, openapiObject);
55
+ const singleType = singularAttributeOpenapiShape(source, column, dreamColumnInfo, suppressResponseEnums, openapiObject);
50
56
  if (dreamColumnInfo.isArray) {
51
57
  return {
52
58
  type: dreamColumnInfo.allowNull ? ['array', 'null'] : 'array',
@@ -71,7 +77,12 @@ export function dreamColumnOpenapiShape(dreamClass, column, openapi = undefined,
71
77
  function baseDbType(dreamColumnInfo) {
72
78
  return dreamColumnInfo.dbType.replace('[]', '');
73
79
  }
74
- function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, openapiSchema) {
80
+ function singularAttributeOpenapiShape(
81
+ // this is the global name of the serializer or controller calling down
82
+ // to get this information. If an unrecognized db type is provided, the
83
+ // source will be rendered in the exception that is returned, enabling
84
+ // the dev to identify the source of the issue and fix it
85
+ source, column, dreamColumnInfo, suppressResponseEnums, openapiSchema) {
75
86
  if (dreamColumnInfo.enumValues) {
76
87
  const enumOverrides = openapiSchema.enum || dreamColumnInfo.enumValues;
77
88
  if (suppressResponseEnums) {
@@ -102,6 +113,10 @@ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, o
102
113
  case 'money':
103
114
  case 'path':
104
115
  case 'text':
116
+ case 'time':
117
+ case 'time without time zone':
118
+ case 'timetz':
119
+ case 'time with time zone':
105
120
  case 'uuid':
106
121
  case 'varbit':
107
122
  case 'varchar':
@@ -121,8 +136,6 @@ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, o
121
136
  case 'real':
122
137
  return { type: 'number' };
123
138
  case 'datetime':
124
- case 'time':
125
- case 'time with time zone':
126
139
  case 'timestamp':
127
140
  case 'timestamp with time zone':
128
141
  case 'timestamp without time zone':
@@ -133,7 +146,7 @@ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, o
133
146
  case 'jsonb':
134
147
  return { type: 'object' };
135
148
  default:
136
- throw new Error(`Unrecognized dbType used in serializer OpenAPI type declaration: ${dreamColumnInfo.dbType}`);
149
+ throw new UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute(source, column, dreamColumnInfo.dbType);
137
150
  }
138
151
  }
139
152
  export class UseCustomOpenapiForJson extends Error {
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @internal
3
+ *
4
+ * Cache for compiled fast-json-stringify functions.
5
+ * Eliminates redundant schema compilation on every request by storing
6
+ * stringify functions keyed by controller, action, openapiName, and status code.
7
+ */
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ const _stringifyCache = {};
10
+ /**
11
+ * @internal
12
+ *
13
+ * Retrieves a cached stringify function if it exists.
14
+ *
15
+ * @param cacheKey - The cache key identifying the stringify function
16
+ * @returns The cached stringify function, or undefined if not found
17
+ */
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ export function getCachedStringify(cacheKey) {
20
+ return _stringifyCache[cacheKey];
21
+ }
22
+ /**
23
+ * @internal
24
+ *
25
+ * Stores a compiled stringify function in the cache.
26
+ *
27
+ * @param cacheKey - The cache key identifying the stringify function
28
+ * @param stringifyFn - The compiled fast-json-stringify function to cache
29
+ */
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ export function cacheStringify(cacheKey, stringifyFn) {
32
+ _stringifyCache[cacheKey] = stringifyFn;
33
+ }
34
+ /**
35
+ * @internal
36
+ *
37
+ * Clears a specific stringify function from the cache.
38
+ * Used in test environments to ensure test isolation.
39
+ *
40
+ * @param cacheKey - The cache key identifying the stringify function to clear
41
+ */
42
+ export function _testOnlyClearStringify(cacheKey) {
43
+ delete _stringifyCache[cacheKey];
44
+ }
45
+ /**
46
+ * @internal
47
+ *
48
+ * Clears all stringify functions from the cache.
49
+ * Used in test environments to ensure test isolation.
50
+ */
51
+ export function _testOnlyClearStringifyCache() {
52
+ Object.keys(_stringifyCache).forEach(key => {
53
+ delete _stringifyCache[key];
54
+ });
55
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @internal
3
+ *
4
+ * Cache for compiled AJV validator functions.
5
+ * Eliminates redundant schema compilation on every request by storing
6
+ * validators keyed by controller, action, openapiName, and validation target.
7
+ */
8
+ const _validatorCache = {};
9
+ /**
10
+ * @internal
11
+ *
12
+ * Retrieves a cached validator function if it exists.
13
+ *
14
+ * @param cacheKey - The cache key identifying the validator
15
+ * @returns The cached validator function, or undefined if not found
16
+ */
17
+ export function getCachedValidator(cacheKey) {
18
+ return _validatorCache[cacheKey];
19
+ }
20
+ /**
21
+ * @internal
22
+ *
23
+ * Stores a compiled validator function in the cache.
24
+ *
25
+ * @param cacheKey - The cache key identifying the validator
26
+ * @param validator - The compiled AJV validator function to cache
27
+ */
28
+ export function cacheValidator(cacheKey, validator) {
29
+ _validatorCache[cacheKey] = validator;
30
+ }
31
+ /**
32
+ * @internal
33
+ *
34
+ * Clears a specific validator from the cache.
35
+ * Used in test environments to ensure test isolation.
36
+ *
37
+ * @param cacheKey - The cache key identifying the validator to clear
38
+ */
39
+ export function _testOnlyClearValidator(cacheKey) {
40
+ delete _validatorCache[cacheKey];
41
+ }
42
+ /**
43
+ * @internal
44
+ *
45
+ * Clears all validators from the cache.
46
+ * Used in test environments to ensure test isolation.
47
+ */
48
+ export function _testOnlyClearValidatorCache() {
49
+ Object.keys(_validatorCache).forEach(key => {
50
+ delete _validatorCache[key];
51
+ });
52
+ }
@@ -32,7 +32,7 @@ importCb) {
32
32
  * at decoration time such that the class of a property being decorated is only avilable during instance instantiation. In order
33
33
  * to only apply static values once, on boot, `globallyInitializingDecorators` is set to true on Dream, and all Dream models are instantiated.
34
34
  */
35
- new controllerClass({}, {}, { action: 'a' });
35
+ new controllerClass({}, { action: 'a' });
36
36
  }
37
37
  }
38
38
  PsychicController['globallyInitializingDecorators'] = false;
@@ -250,7 +250,7 @@ Try setting it to something valid, like:
250
250
  get httpServerOptions() {
251
251
  return this._httpServerOptions;
252
252
  }
253
- _corsOptions = {};
253
+ _corsOptions;
254
254
  get corsOptions() {
255
255
  return this._corsOptions;
256
256
  }
@@ -274,10 +274,6 @@ Try setting it to something valid, like:
274
274
  get saltRounds() {
275
275
  return this._saltRounds;
276
276
  }
277
- _sanitizeResponseJson = false;
278
- get sanitizeResponseJson() {
279
- return this._sanitizeResponseJson;
280
- }
281
277
  _packageManager;
282
278
  get packageManager() {
283
279
  return this._packageManager;
@@ -427,7 +423,7 @@ Try setting it to something valid, like:
427
423
  const eventType = pathOrOnOrHandler;
428
424
  const handler = maybeHandler;
429
425
  const wrappedHandler = (server) => {
430
- server.expressApp.use(handler);
426
+ server.koaApp.use(handler);
431
427
  };
432
428
  switch (eventType) {
433
429
  case 'before-middleware':
@@ -446,7 +442,7 @@ Try setting it to something valid, like:
446
442
  }
447
443
  else {
448
444
  const wrappedHandler = (server) => {
449
- server.expressApp.use(pathOrOnOrHandler);
445
+ server.koaApp.use(pathOrOnOrHandler);
450
446
  };
451
447
  this.on('server:init:after-middleware', wrappedHandler);
452
448
  }
@@ -541,9 +537,6 @@ Try setting it to something valid, like:
541
537
  case 'saltRounds':
542
538
  this._saltRounds = value;
543
539
  break;
544
- case 'sanitizeResponseJson':
545
- this._sanitizeResponseJson = value;
546
- break;
547
540
  case 'openapi':
548
541
  this._openapi = {
549
542
  ...this.openapi,
@@ -1,12 +1,12 @@
1
1
  import { DataIncompatibleWithDatabaseField, RecordNotFound, ValidationError } from '@rvoh/dream/errors';
2
2
  import { camelize } from '@rvoh/dream/utils';
3
- import { Router } from 'express';
3
+ import KoaRouter from '@koa/router';
4
4
  import util, { debuglog } from 'node:util';
5
5
  import pluralize from 'pluralize-esm';
6
6
  import ParamValidationError from '../error/controller/ParamValidationError.js';
7
7
  import ParamValidationErrors from '../error/controller/ParamValidationErrors.js';
8
8
  import OpenapiRequestValidationFailure from '../error/openapi/OpenapiRequestValidationFailure.js';
9
- import CannotCommitRoutesWithoutExpressApp from '../error/router/cannot-commit-routes-without-express-app.js';
9
+ import CannotCommitRoutesWithoutKoaApp from '../error/router/cannot-commit-routes-without-koa-app.js';
10
10
  import EnvInternal from '../helpers/EnvInternal.js';
11
11
  import errorIsRescuableHttpError from '../helpers/error/errorIsRescuableHttpError.js';
12
12
  import PsychicApp from '../psychic-app/index.js';
@@ -31,19 +31,25 @@ export default class PsychicRouter {
31
31
  commit() {
32
32
  const app = this.app;
33
33
  if (!app)
34
- throw new CannotCommitRoutesWithoutExpressApp();
34
+ throw new CannotCommitRoutesWithoutKoaApp();
35
+ const router = new KoaRouter();
35
36
  this.routes.forEach(route => {
36
37
  if (route.middleware) {
37
38
  const routeConf = route;
38
- app[routeConf.httpMethod](routePath(routeConf.path), ...(Array.isArray(routeConf.middleware) ? routeConf.middleware : [routeConf.middleware]));
39
+ const middlewares = Array.isArray(routeConf.middleware)
40
+ ? routeConf.middleware
41
+ : [routeConf.middleware];
42
+ router[routeConf.httpMethod](routePath(routeConf.path), ...middlewares);
39
43
  }
40
44
  else {
41
45
  const routeConf = route;
42
- app[routeConf.httpMethod](routePath(routeConf.path), (req, res) => {
43
- this.handle(routeConf.controller, routeConf.action, { req, res }).catch(() => { });
46
+ router[routeConf.httpMethod](routePath(routeConf.path), async (ctx) => {
47
+ await this.handle(routeConf.controller, routeConf.action, { ctx });
44
48
  });
45
49
  }
46
50
  });
51
+ app.use(router.routes());
52
+ app.use(router.allowedMethods());
47
53
  }
48
54
  get(path, controller, action) {
49
55
  this.crud('get', path, controller, action);
@@ -72,7 +78,7 @@ export default class PsychicRouter {
72
78
  this.checkPathForInvalidChars(path);
73
79
  const isMiddleware = (typeof controllerOrMiddleware === 'function' || Array.isArray(controllerOrMiddleware)) &&
74
80
  !controllerOrMiddleware?.isPsychicController;
75
- // devs can provide custom express middleware which bypasses
81
+ // devs can provide custom Koa middleware which bypasses
76
82
  // the normal Controller#action paradigm.
77
83
  if (isMiddleware) {
78
84
  this.routeManager.addMiddleware({
@@ -98,7 +104,7 @@ export default class PsychicRouter {
98
104
  if (path.includes('{'))
99
105
  throw new Error(`
100
106
  The provided route "${path}" contains characters that are not supported.
101
- If you are trying to write a uri param, you will need to use expressjs
107
+ If you are trying to write a uri param, you will need to use the
102
108
  param syntax, which is a prefixing colon, rather than using brackets
103
109
  to surround the param.
104
110
 
@@ -236,10 +242,10 @@ suggested fix: "${convertRouteParams(path)}"
236
242
  * By default, do not provide an attacker with any visibility into which layer
237
243
  * of the application rejected their request.
238
244
  */
239
- async handle(controller, action, { req, res, }) {
240
- const controllerInstance = this._initializeController(controller, req, res, action);
245
+ async handle(controller, action, { ctx, }) {
246
+ const controllerInstance = this._initializeController(controller, ctx, action);
241
247
  if (typeof controllerInstance[action] !== 'function') {
242
- controllerInstance['expressSendStatus'](404);
248
+ controllerInstance['koaSendStatus'](404);
243
249
  return;
244
250
  }
245
251
  try {
@@ -250,20 +256,20 @@ suggested fix: "${convertRouteParams(path)}"
250
256
  if (errorIsRescuableHttpError(err)) {
251
257
  const httpErr = err;
252
258
  if (httpErr.data) {
253
- controllerInstance['expressSendJson'](httpErr.data, httpErr.status);
259
+ controllerInstance['koaSendJson'](httpErr.data, httpErr.status);
254
260
  }
255
261
  else {
256
- controllerInstance['expressSendStatus'](httpErr.status);
262
+ controllerInstance['koaSendStatus'](httpErr.status);
257
263
  }
258
264
  }
259
265
  else if (err instanceof RecordNotFound) {
260
- controllerInstance['expressSendStatus'](404);
266
+ controllerInstance['koaSendStatus'](404);
261
267
  }
262
268
  else if (err instanceof DataIncompatibleWithDatabaseField) {
263
269
  /**
264
270
  * See comment at top of this method for philosophy of 400
265
271
  */
266
- controllerInstance['expressSendStatus'](400);
272
+ controllerInstance['koaSendStatus'](400);
267
273
  }
268
274
  else if (err instanceof ValidationError) {
269
275
  if (this.validationErrorLoggingEnabled) {
@@ -275,7 +281,7 @@ suggested fix: "${convertRouteParams(path)}"
275
281
  /**
276
282
  * See comment at top of this method for philosophy of 400
277
283
  */
278
- controllerInstance['expressSendStatus'](400);
284
+ controllerInstance['koaSendStatus'](400);
279
285
  }
280
286
  else if (err instanceof OpenapiRequestValidationFailure) {
281
287
  if (this.validationErrorLoggingEnabled) {
@@ -288,7 +294,7 @@ suggested fix: "${convertRouteParams(path)}"
288
294
  /**
289
295
  * See comment at top of this method for philosophy of 400
290
296
  */
291
- controllerInstance['expressSendStatus'](400);
297
+ controllerInstance['koaSendStatus'](400);
292
298
  }
293
299
  else if (err instanceof ParamValidationError) {
294
300
  if (this.validationErrorLoggingEnabled) {
@@ -302,7 +308,7 @@ suggested fix: "${convertRouteParams(path)}"
302
308
  /**
303
309
  * See comment at top of this method for philosophy of 400
304
310
  */
305
- controllerInstance['expressSendStatus'](400);
311
+ controllerInstance['koaSendStatus'](400);
306
312
  }
307
313
  else if (err instanceof ParamValidationErrors) {
308
314
  if (this.validationErrorLoggingEnabled) {
@@ -314,14 +320,14 @@ suggested fix: "${convertRouteParams(path)}"
314
320
  /**
315
321
  * See comment at top of this method for philosophy of 400
316
322
  */
317
- controllerInstance['expressSendStatus'](400);
323
+ controllerInstance['koaSendStatus'](400);
318
324
  }
319
325
  else {
320
326
  PsychicApp.logWithLevel('error', util.inspect(err, { depth: ERROR_LOGGING_DEPTH }));
321
327
  if (PsychicApp.getOrFail().specialHooks.serverError.length) {
322
328
  try {
323
329
  for (const hook of PsychicApp.getOrFail().specialHooks.serverError) {
324
- await hook(err, req, res);
330
+ await hook(err, ctx);
325
331
  }
326
332
  }
327
333
  catch (error) {
@@ -348,17 +354,17 @@ suggested fix: "${convertRouteParams(path)}"
348
354
  }
349
355
  }
350
356
  }
351
- _initializeController(ControllerClass, req, res, action) {
352
- return new ControllerClass(req, res, {
357
+ _initializeController(ControllerClass, ctx, action) {
358
+ return new ControllerClass(ctx, {
353
359
  action,
354
360
  });
355
361
  }
356
362
  }
357
363
  export class PsychicNestedRouter extends PsychicRouter {
358
364
  router;
359
- constructor(expressApp, routeManager, { namespaces = [], } = {}) {
360
- super(expressApp);
361
- this.router = Router();
365
+ constructor(koaApp, routeManager, { namespaces = [], } = {}) {
366
+ super(koaApp);
367
+ this.router = new KoaRouter();
362
368
  this.currentNamespaces = namespaces;
363
369
  this.routeManager = routeManager;
364
370
  }
@@ -17,6 +17,7 @@ export default async function startPsychicServer({ app, port, sslCredentials, })
17
17
  }
18
18
  export function createPsychicHttpInstance(app, sslCredentials) {
19
19
  const psychicApp = PsychicApp.getOrFail();
20
+ const callback = app.callback();
20
21
  if (sslCredentials?.key && sslCredentials?.cert) {
21
22
  return https.createServer({
22
23
  key: fs.readFileSync(sslCredentials.key),
@@ -24,10 +25,13 @@ export function createPsychicHttpInstance(app, sslCredentials) {
24
25
  ca: sslCredentials.ca?.map(filePath => fs.readFileSync(filePath)),
25
26
  rejectUnauthorized: sslCredentials?.rejectUnauthorized,
26
27
  ...psychicApp.httpServerOptions,
27
- }, app);
28
+ },
29
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
30
+ callback);
28
31
  }
29
32
  else {
30
- return http.createServer(psychicApp.httpServerOptions, app);
33
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
34
+ return http.createServer(psychicApp.httpServerOptions, callback);
31
35
  }
32
36
  }
33
37
  function welcomeMessage({ port }) {