@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
@@ -1,7 +1,9 @@
1
1
  import { closeAllDbConnections } from '@rvoh/dream/db';
2
- import * as cookieParser from 'cookie-parser';
3
- import * as cors from 'cors';
4
- import * as express from 'express';
2
+ import cors from '@koa/cors';
3
+ import Koa from 'koa';
4
+ import koaBodyparser from 'koa-bodyparser';
5
+ import conditional from 'koa-conditional-get';
6
+ import etag from 'koa-etag';
5
7
  import logIfDevelopment from '../controller/helpers/logIfDevelopment.js';
6
8
  import EnvInternal from '../helpers/EnvInternal.js';
7
9
  import PsychicApp from '../psychic-app/index.js';
@@ -15,14 +17,14 @@ export default class PsychicServer {
15
17
  static createPsychicHttpInstance(app, sslCredentials) {
16
18
  return createPsychicHttpInstance(app, sslCredentials);
17
19
  }
18
- expressApp;
20
+ koaApp;
19
21
  httpServer;
20
22
  booted = false;
21
23
  constructor() {
22
24
  this.buildApp();
23
25
  }
24
26
  async routes() {
25
- const r = new PsychicRouter(this.expressApp);
27
+ const r = new PsychicRouter(this.koaApp);
26
28
  await PsychicApp.getOrFail().routesCb(r);
27
29
  return r.routes;
28
30
  }
@@ -31,16 +33,19 @@ export default class PsychicServer {
31
33
  return;
32
34
  const psychicApp = PsychicApp.getOrFail();
33
35
  this.setSecureDefaultHeaders();
34
- this.expressApp.use((_, res, next) => {
36
+ this.koaApp.use(async (ctx, next) => {
35
37
  Object.keys(psychicApp.defaultResponseHeaders).forEach(key => {
36
- res.setHeader(key, psychicApp.defaultResponseHeaders[key]);
38
+ ctx.set(key, psychicApp.defaultResponseHeaders[key]);
37
39
  });
38
- next();
40
+ await next();
39
41
  });
40
42
  for (const serverInitBeforeMiddlewareHook of PsychicApp.getOrFail().specialHooks
41
43
  .serverInitBeforeMiddleware) {
42
44
  await serverInitBeforeMiddlewareHook(this);
43
45
  }
46
+ // ETag support (Express has this built-in, Koa needs middleware)
47
+ this.koaApp.use(conditional());
48
+ this.koaApp.use(etag());
44
49
  this.initializeCors();
45
50
  this.initializeJSON();
46
51
  try {
@@ -69,29 +74,23 @@ export default class PsychicServer {
69
74
  applyNotFoundMiddleware() {
70
75
  if (!EnvInternal.isDevelopment)
71
76
  return;
72
- this.expressApp.use((req, res, next) => {
73
- // express by default will set the 200 status code. If a user explicitly
74
- // provides anything other than 200, we should assume that a prior middleware
75
- // would have have sent headers, which would prevent any of this from happening.
76
- // this means that if we are here, we should not be sending a 200. Future middleware
77
- // by express will automatically pick this up and turn it into a 404, so we are
78
- // going to automatically set the status to 404 now, so that our logger can
79
- // pick up the correct status code.
80
- if (res.statusCode === 200)
81
- res.status(404);
82
- logIfDevelopment({ req, res, startTime: Date.now(), fallbackStatusCode: 404 });
83
- // call next to let express handle sending the 404
84
- next();
77
+ this.koaApp.use(async (ctx, next) => {
78
+ await next();
79
+ // Koa defaults to 404 for unmatched routes. If nothing set the body,
80
+ // log the 404 in development.
81
+ if (ctx.status === 404 && !ctx.body) {
82
+ logIfDevelopment({ ctx, startTime: Date.now(), fallbackStatusCode: 404 });
83
+ }
85
84
  });
86
85
  }
87
86
  setSecureDefaultHeaders() {
88
- this.expressApp.disable('x-powered-by');
89
- this.expressApp.use((_, res, next) => {
90
- res.setHeader('X-Content-Type-Options', 'nosniff');
87
+ // Koa doesn't send x-powered-by by default, no need to disable it.
88
+ this.koaApp.use(async (ctx, next) => {
89
+ ctx.set('X-Content-Type-Options', 'nosniff');
91
90
  if (EnvInternal.isProduction) {
92
- res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
91
+ ctx.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
93
92
  }
94
- next();
93
+ await next();
95
94
  });
96
95
  }
97
96
  // TODO: use config helper for fetching default port
@@ -104,7 +103,7 @@ export default class PsychicServer {
104
103
  }
105
104
  else {
106
105
  const httpServer = await startPsychicServer({
107
- app: this.expressApp,
106
+ app: this.koaApp,
108
107
  port: port || psychicApp.port,
109
108
  sslCredentials: PsychicApp.getOrFail().sslCredentials,
110
109
  });
@@ -146,26 +145,24 @@ export default class PsychicServer {
146
145
  await this.boot();
147
146
  let server;
148
147
  await new Promise(accept => {
149
- server = this.expressApp.listen(port, () => accept({}));
148
+ server = this.koaApp.listen(port, () => accept({}));
150
149
  });
151
150
  await block();
152
151
  server.close();
153
152
  return true;
154
153
  }
155
154
  buildApp() {
156
- this.expressApp = express.default();
157
- this.expressApp.use(cookieParser.default());
155
+ this.koaApp = new Koa();
158
156
  }
159
157
  initializeCors() {
160
- this.expressApp.use(
161
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
162
- cors.default(PsychicApp.getOrFail().corsOptions));
158
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
159
+ this.koaApp.use(cors(PsychicApp.getOrFail().corsOptions));
163
160
  }
164
161
  initializeJSON() {
165
- this.expressApp.use(express.json(PsychicApp.getOrFail().jsonOptions));
162
+ this.koaApp.use(koaBodyparser(PsychicApp.getOrFail().jsonOptions));
166
163
  }
167
164
  async buildRoutes() {
168
- const r = new PsychicRouter(this.expressApp);
165
+ const r = new PsychicRouter(this.koaApp);
169
166
  await PsychicApp.getOrFail().routesCb(r);
170
167
  r.commit();
171
168
  }
@@ -1,4 +1,4 @@
1
- import { CalendarDate, DateTime } from '@rvoh/dream';
1
+ import { CalendarDate, ClockTime, ClockTimeTz, DateTime } from '@rvoh/dream';
2
2
  import { camelize, compact, snakeify } from '@rvoh/dream/utils';
3
3
  import ParamValidationError from '../error/controller/ParamValidationError.js';
4
4
  import ParamValidationErrors from '../error/controller/ParamValidationErrors.js';
@@ -85,6 +85,22 @@ export default class Params {
85
85
  case 'timestamp without time zone[]':
86
86
  returnObj[columnName] = this.cast(params, columnName.toString(), 'datetime[]', { allowNull: columnMetadata.allowNull });
87
87
  break;
88
+ case 'time':
89
+ case 'time without time zone':
90
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'time', { allowNull: columnMetadata.allowNull });
91
+ break;
92
+ case 'time[]':
93
+ case 'time without time zone[]':
94
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'time[]', { allowNull: columnMetadata.allowNull });
95
+ break;
96
+ case 'timetz':
97
+ case 'time with time zone':
98
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'timetz', { allowNull: columnMetadata.allowNull });
99
+ break;
100
+ case 'timetz[]':
101
+ case 'time with time zone[]':
102
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'timetz[]', { allowNull: columnMetadata.allowNull });
103
+ break;
88
104
  case 'jsonb':
89
105
  returnObj[columnName] = this.cast(params, columnName.toString(), 'json', { allowNull: columnMetadata.allowNull });
90
106
  break;
@@ -203,7 +219,6 @@ export default class Params {
203
219
  }
204
220
  return paramValue;
205
221
  }
206
- let dateClass;
207
222
  const integerRegexp = /^-?\d+$/;
208
223
  switch (expectedType) {
209
224
  case 'string':
@@ -227,7 +242,8 @@ export default class Params {
227
242
  return false;
228
243
  throw new ParamValidationError(paramName, [typeToError(expectedType)]);
229
244
  case 'datetime':
230
- case 'date':
245
+ case 'date': {
246
+ let dateClass;
231
247
  switch (expectedType) {
232
248
  case 'datetime':
233
249
  dateClass = DateTime;
@@ -252,6 +268,35 @@ export default class Params {
252
268
  }
253
269
  }
254
270
  throw new ParamValidationError(paramName, [typeToError(expectedType)]);
271
+ }
272
+ case 'time':
273
+ case 'timetz': {
274
+ let timeClass;
275
+ switch (expectedType) {
276
+ case 'time':
277
+ timeClass = ClockTime;
278
+ break;
279
+ case 'timetz':
280
+ timeClass = ClockTimeTz;
281
+ break;
282
+ default:
283
+ if (typeof expectedType === 'string')
284
+ throw Error(`${expectedType} must be "time" or "timetz"`);
285
+ else
286
+ throw Error(`expectedType is not a string`);
287
+ }
288
+ if (paramValue instanceof ClockTime || paramValue instanceof ClockTimeTz)
289
+ return paramValue;
290
+ if (typeof paramValue === 'string') {
291
+ try {
292
+ return timeClass.fromISO(paramValue);
293
+ }
294
+ catch {
295
+ throw new ParamValidationError(paramName, [typeToError(expectedType)]);
296
+ }
297
+ }
298
+ throw new ParamValidationError(paramName, [typeToError(expectedType)]);
299
+ }
255
300
  case 'integer':
256
301
  if (typeof paramValue !== 'string' && typeof paramValue !== 'number')
257
302
  throw new ParamValidationError(paramName, [typeToError(expectedType)]);
@@ -287,6 +332,8 @@ export default class Params {
287
332
  case 'json[]':
288
333
  case 'number[]':
289
334
  case 'string[]':
335
+ case 'time[]':
336
+ case 'timetz[]':
290
337
  case 'uuid[]':
291
338
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
292
339
  if (!Array.isArray(paramValue))
@@ -365,6 +412,8 @@ const typeToErrorMap = {
365
412
  null: 'expecting null',
366
413
  number: 'expected number or string number',
367
414
  string: 'expected string',
415
+ time: 'expected ISO time string',
416
+ timetz: 'expected ISO timetz string',
368
417
  uuid: 'expected uuid',
369
418
  'bigint[]': 'expected bigint array',
370
419
  'boolean[]': 'expected boolean array',
@@ -375,6 +424,8 @@ const typeToErrorMap = {
375
424
  'null[]': 'expecting null array',
376
425
  'number[]': 'expected number or string number array',
377
426
  'string[]': 'expected string array',
427
+ 'time[]': 'expected ISO time string array',
428
+ 'timetz[]': 'expected ISO timetz string array',
378
429
  'uuid[]': 'expected uuid array',
379
430
  };
380
431
  function typeToError(param) {
@@ -393,6 +444,8 @@ const arrayTypeToNonArrayTypeMap = {
393
444
  'null[]': 'null',
394
445
  'number[]': 'number',
395
446
  'string[]': 'string',
447
+ 'time[]': 'time',
448
+ 'timetz[]': 'timetz',
396
449
  'uuid[]': 'uuid',
397
450
  };
398
451
  function arrayTypeToNonArrayType(param) {
@@ -3,31 +3,28 @@ import cookieMaxAgeFromCookieOpts from '../helpers/cookieMaxAgeFromCookieOpts.js
3
3
  import EnvInternal from '../helpers/EnvInternal.js';
4
4
  import PsychicApp from '../psychic-app/index.js';
5
5
  export default class Session {
6
- req;
7
- res;
8
- constructor(req, res) {
9
- this.req = req;
10
- this.res = res;
6
+ ctx;
7
+ constructor(ctx) {
8
+ this.ctx = ctx;
11
9
  }
12
10
  getCookie(name) {
13
- const cookies = this.req.cookies;
14
- const value = cookies[name];
11
+ const value = this.ctx.cookies.get(name);
15
12
  if (value)
16
13
  return InternalEncrypt.decryptCookie(value);
17
14
  return null;
18
15
  }
19
16
  setCookie(name, data, opts = {}) {
20
- this.res.cookie(name, InternalEncrypt.encryptCookie(data), {
21
- secure: EnvInternal.isProduction,
22
- httpOnly: true,
17
+ this.ctx.cookies.set(name, InternalEncrypt.encryptCookie(data), {
23
18
  ...opts,
19
+ secure: opts.secure ?? EnvInternal.isProduction,
20
+ httpOnly: opts.httpOnly ?? true,
24
21
  maxAge: opts.maxAge
25
22
  ? cookieMaxAgeFromCookieOpts(opts.maxAge)
26
- : PsychicApp.getOrFail().cookieOptions?.maxAge,
23
+ : (PsychicApp.getOrFail().cookieOptions?.maxAge ?? cookieMaxAgeFromCookieOpts()),
27
24
  });
28
25
  }
29
26
  clearCookie(name) {
30
- this.res.clearCookie(name);
27
+ this.ctx.cookies.set(name, '', { maxAge: 0 });
31
28
  }
32
29
  daysToMilliseconds(numDays) {
33
30
  return numDays * 60 * 60 * 24 * 1000;
@@ -1,7 +1,6 @@
1
- import { Request, Response } from 'express';
2
- export default function logIfDevelopment({ req, res, startTime, fallbackStatusCode, }: {
3
- req: Request;
4
- res: Response;
1
+ import Koa from 'koa';
2
+ export default function logIfDevelopment({ ctx, startTime, fallbackStatusCode, }: {
3
+ ctx: Koa.Context;
5
4
  startTime: number;
6
5
  fallbackStatusCode?: number;
7
6
  }): void;
@@ -1,7 +1,7 @@
1
1
  import { Dream } from '@rvoh/dream';
2
2
  import { OpenapiSchemaBody } from '@rvoh/dream/openapi';
3
3
  import { DreamParamSafeAttributes, DreamParamSafeColumnNames, SerializerRendererOpts, StrictInterface } from '@rvoh/dream/types';
4
- import { Request, Response } from 'express';
4
+ import Koa from 'koa';
5
5
  import { ControllerHook } from '../controller/hooks.js';
6
6
  import { HttpStatusCodeInt, HttpStatusSymbol } from '../error/http/status-codes.js';
7
7
  import OpenapiEndpointRenderer from '../openapi-renderer/endpoint.js';
@@ -14,7 +14,7 @@ export type ControllerActionMetadata = Record<string, {
14
14
  serializerKey?: string;
15
15
  }>;
16
16
  export type PsychicParamsPrimitive = string | number | boolean | null | undefined | PsychicParamsPrimitive[];
17
- export declare const PsychicParamsPrimitiveLiterals: readonly ["bigint", "bigint[]", "boolean", "boolean[]", "date", "date[]", "datetime", "datetime[]", "integer", "integer[]", "json", "json[]", "null", "null[]", "number", "number[]", "string", "string[]", "uuid", "uuid[]"];
17
+ export declare const PsychicParamsPrimitiveLiterals: readonly ["bigint", "bigint[]", "boolean", "boolean[]", "date", "date[]", "datetime", "datetime[]", "integer", "integer[]", "json", "json[]", "null", "null[]", "number", "number[]", "string", "string[]", "time", "time[]", "timetz", "timetz[]", "uuid", "uuid[]"];
18
18
  export type PsychicParamsPrimitiveLiteral = (typeof PsychicParamsPrimitiveLiterals)[number];
19
19
  export interface PsychicParamsDictionary {
20
20
  [key: string]: PsychicParamsPrimitive | PsychicParamsDictionary | PsychicParamsDictionary[];
@@ -101,13 +101,13 @@ export default class PsychicController {
101
101
  * and non-controllers
102
102
  */
103
103
  get isPsychicControllerInstance(): boolean;
104
- req: Request;
105
- res: Response;
104
+ ctx: Koa.Context;
106
105
  session: Session;
107
106
  action: string;
108
107
  renderOpts: SerializerRendererOpts;
109
108
  private startTime;
110
- constructor(req: Request, res: Response, { action, }: {
109
+ private _responseSent;
110
+ constructor(ctx: Koa.Context, { action, }: {
111
111
  action: string;
112
112
  });
113
113
  /**
@@ -384,9 +384,20 @@ export default class PsychicController {
384
384
  * @param data - the data to validate and render
385
385
  */
386
386
  private validateAndRenderJsonResponse;
387
- private expressSendJson;
388
- private expressSendStatus;
389
- private expressRedirect;
387
+ private koaSendJson;
388
+ /**
389
+ * @internal
390
+ *
391
+ * Attempts to retrieve or create a cached fast-json-stringify function
392
+ * for the current endpoint and status code. Returns undefined if no
393
+ * OpenAPI schema exists or if the controller's globalName is not set.
394
+ *
395
+ * @param statusCode - the HTTP status code
396
+ * @returns A stringify function, or undefined if no schema exists
397
+ */
398
+ private getFastJsonStringifyFunction;
399
+ private koaSendStatus;
400
+ private koaRedirect;
390
401
  private logIfDevelopment;
391
402
  protected defaultSerializerPassthrough: SerializerResult;
392
403
  /**
@@ -1,8 +1,9 @@
1
- export declare function launchDevServer(key: string, { port, cmd, timeout }?: LaunchDevServerOpts): Promise<void>;
1
+ export declare function launchDevServer(key: string, { port, cmd, timeout, onStdOut }?: LaunchDevServerOpts): Promise<void>;
2
2
  export declare function stopDevServer(key: string): void;
3
3
  export declare function stopDevServers(): void;
4
4
  export interface LaunchDevServerOpts {
5
5
  port?: number;
6
6
  cmd?: string;
7
7
  timeout?: number;
8
+ onStdOut?: (message: string) => void;
8
9
  }
@@ -0,0 +1,7 @@
1
+ export default class UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute extends Error {
2
+ private source;
3
+ private attributeName;
4
+ private dbType;
5
+ constructor(source: string, attributeName: string, dbType: string);
6
+ get message(): string;
7
+ }
@@ -0,0 +1,3 @@
1
+ export default class CannotCommitRoutesWithoutKoaApp extends Error {
2
+ get message(): string;
3
+ }
@@ -1,2 +1,2 @@
1
1
  import { CustomCookieMaxAgeOptions } from '../psychic-app/index.js';
2
- export default function cookieMaxAgeFromCookieOpts(cookieOpts: CustomCookieMaxAgeOptions | undefined): number;
2
+ export default function cookieMaxAgeFromCookieOpts(cookieOpts?: CustomCookieMaxAgeOptions): number;
@@ -1 +1 @@
1
- export default function toJson<T>(data: T, sanitize: boolean): string;
1
+ export default function toJson<T>(data: T): string;
@@ -1,4 +1,4 @@
1
- import { Ajv, type JSONSchemaType, type ValidateFunction } from 'ajv';
1
+ import { Ajv, type ErrorObject, type JSONSchemaType, type ValidateFunction } from 'ajv';
2
2
  /**
3
3
  * @internal
4
4
  *
@@ -56,6 +56,10 @@ export declare function validateObject<T = unknown>(data: unknown, schema: JSONS
56
56
  * @returns A validator function that can be reused for multiple validations
57
57
  */
58
58
  export declare function createValidator<T = unknown>(schema: JSONSchemaType<T> | object, options?: ValidateOpenapiSchemaOptions): ValidateFunction<T>;
59
+ /**
60
+ * Formats AJV errors into a more readable format
61
+ */
62
+ export declare function formatAjvErrors(ajvErrors: ErrorObject[]): ValidationError[];
59
63
  export interface ValidationResult<T = unknown> {
60
64
  isValid: boolean;
61
65
  data?: T;
@@ -98,6 +98,27 @@ export default class OpenapiPayloadValidator {
98
98
  * @param statusCode - the status code used to render
99
99
  */
100
100
  validateOpenapiResponseBody(data: any, statusCode: number): void;
101
+ /**
102
+ * @internal
103
+ *
104
+ * Retrieves the OpenAPI response schema for a given status code.
105
+ * Returns undefined if no schema is defined for this endpoint/status code.
106
+ *
107
+ * @param statusCode - the HTTP status code
108
+ * @returns The response schema, or undefined if not found
109
+ */
110
+ getResponseSchema(statusCode: number): object | undefined;
111
+ /**
112
+ * @internal
113
+ *
114
+ * Retrieves the OpenAPI response schema for a given status code with
115
+ * all components merged in. This is the schema format needed by
116
+ * fast-json-stringify and AJV validators.
117
+ *
118
+ * @param statusCode - the HTTP status code
119
+ * @returns The response schema with components, or undefined if not found
120
+ */
121
+ getResponseSchemaWithComponents(statusCode: number): object | undefined;
101
122
  /**
102
123
  * @internal
103
124
  *
@@ -137,6 +158,26 @@ export default class OpenapiPayloadValidator {
137
158
  * @param headers - the request headers
138
159
  */
139
160
  private validateOrFail;
161
+ /**
162
+ * @internal
163
+ *
164
+ * Generates a cache key for the validator based on controller, action, openapiName, and target.
165
+ *
166
+ * @param target - the validation target (one of: 'requestBody', 'query', 'headers', 'responseBody')
167
+ * @returns cache key string
168
+ */
169
+ private getCacheKey;
170
+ /**
171
+ * @internal
172
+ *
173
+ * Compiles a validator using AJV and caches it for future use.
174
+ *
175
+ * @param cacheKey - the cache key for this validator
176
+ * @param schema - the schema to compile
177
+ * @param options - AJV options
178
+ * @returns compiled validator function
179
+ */
180
+ private compileAndCacheValidator;
140
181
  /**
141
182
  * @internal
142
183
  *
@@ -5,7 +5,7 @@ export interface VirtualAttributeStatement {
5
5
  type: OpenapiShorthandPrimitiveTypes | OpenapiSchemaBodyShorthand | undefined;
6
6
  }
7
7
  type DreamClassColumnNames<DreamClass extends typeof Dream, DreamInstance extends InstanceType<DreamClass> = InstanceType<DreamClass>, DB = DreamInstance['DB'], TableName extends keyof DB = DreamInstance['table'] & keyof DB, Table extends DB[keyof DB] = DB[TableName]> = keyof Table & string;
8
- export declare function dreamColumnOpenapiShape<DreamClass extends typeof Dream>(dreamClass: DreamClass, column: DreamClassColumnNames<DreamClass>, openapi?: OpenapiDescription | OpenapiSchemaBodyShorthand | OpenapiShorthandPrimitiveTypes | undefined, { suppressResponseEnums, allowGenericJson, }?: {
8
+ export declare function dreamColumnOpenapiShape<DreamClass extends typeof Dream>(source: string, dreamClass: DreamClass, column: DreamClassColumnNames<DreamClass>, openapi?: OpenapiDescription | OpenapiSchemaBodyShorthand | OpenapiShorthandPrimitiveTypes | undefined, { suppressResponseEnums, allowGenericJson, }?: {
9
9
  suppressResponseEnums?: boolean;
10
10
  allowGenericJson?: boolean;
11
11
  }): OpenapiSchemaBody;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @internal
3
+ *
4
+ * Retrieves a cached stringify function if it exists.
5
+ *
6
+ * @param cacheKey - The cache key identifying the stringify function
7
+ * @returns The cached stringify function, or undefined if not found
8
+ */
9
+ export declare function getCachedStringify(cacheKey: string): ((data: any) => string) | undefined;
10
+ /**
11
+ * @internal
12
+ *
13
+ * Stores a compiled stringify function in the cache.
14
+ *
15
+ * @param cacheKey - The cache key identifying the stringify function
16
+ * @param stringifyFn - The compiled fast-json-stringify function to cache
17
+ */
18
+ export declare function cacheStringify(cacheKey: string, stringifyFn: (data: any) => string): void;
19
+ /**
20
+ * @internal
21
+ *
22
+ * Clears a specific stringify function from the cache.
23
+ * Used in test environments to ensure test isolation.
24
+ *
25
+ * @param cacheKey - The cache key identifying the stringify function to clear
26
+ */
27
+ export declare function _testOnlyClearStringify(cacheKey: string): void;
28
+ /**
29
+ * @internal
30
+ *
31
+ * Clears all stringify functions from the cache.
32
+ * Used in test environments to ensure test isolation.
33
+ */
34
+ export declare function _testOnlyClearStringifyCache(): void;
@@ -0,0 +1,35 @@
1
+ import { ValidateFunction } from 'ajv';
2
+ /**
3
+ * @internal
4
+ *
5
+ * Retrieves a cached validator function if it exists.
6
+ *
7
+ * @param cacheKey - The cache key identifying the validator
8
+ * @returns The cached validator function, or undefined if not found
9
+ */
10
+ export declare function getCachedValidator(cacheKey: string): ValidateFunction | undefined;
11
+ /**
12
+ * @internal
13
+ *
14
+ * Stores a compiled validator function in the cache.
15
+ *
16
+ * @param cacheKey - The cache key identifying the validator
17
+ * @param validator - The compiled AJV validator function to cache
18
+ */
19
+ export declare function cacheValidator(cacheKey: string, validator: ValidateFunction): void;
20
+ /**
21
+ * @internal
22
+ *
23
+ * Clears a specific validator from the cache.
24
+ * Used in test environments to ensure test isolation.
25
+ *
26
+ * @param cacheKey - The cache key identifying the validator to clear
27
+ */
28
+ export declare function _testOnlyClearValidator(cacheKey: string): void;
29
+ /**
30
+ * @internal
31
+ *
32
+ * Clears all validators from the cache.
33
+ * Used in test environments to ensure test isolation.
34
+ */
35
+ export declare function _testOnlyClearValidatorCache(): void;
@@ -1,11 +1,11 @@
1
+ import cors from '@koa/cors';
1
2
  import { DreamApp } from '@rvoh/dream';
2
3
  import { OpenapiSchemaBody } from '@rvoh/dream/openapi';
3
4
  import { DreamAppAllowedPackageManagersEnum } from '@rvoh/dream/system';
4
5
  import { DreamAppInitOptions, DreamLogLevel, DreamLogger, EncryptOptions } from '@rvoh/dream/types';
5
- import * as bodyParser from 'body-parser';
6
6
  import { Command } from 'commander';
7
- import { CorsOptions } from 'cors';
8
- import { Express, Request, RequestHandler, Response } from 'express';
7
+ import Koa from 'koa';
8
+ import bodyParser from 'koa-bodyparser';
9
9
  import * as http from 'node:http';
10
10
  import * as https from 'node:https';
11
11
  import { OpenapiValidateTarget } from '../openapi-renderer/defaults.js';
@@ -35,7 +35,7 @@ export default class PsychicApp {
35
35
  /**
36
36
  * @internal
37
37
  */
38
- static getPsychicHttpInstance(app: Express, sslCredentials: PsychicSslCredentials | undefined): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse> | https.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
38
+ static getPsychicHttpInstance(app: Koa, sslCredentials: PsychicSslCredentials | undefined): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse> | https.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
39
39
  /**
40
40
  * Builds the routes cache if it does not already
41
41
  * exist. This is called during PsychicApp.init,
@@ -143,7 +143,7 @@ export default class PsychicApp {
143
143
  private _httpServerOptions;
144
144
  get httpServerOptions(): http.ServerOptions<typeof http.IncomingMessage, typeof http.ServerResponse> | https.ServerOptions<typeof http.IncomingMessage, typeof http.ServerResponse>;
145
145
  private _corsOptions;
146
- get corsOptions(): CorsOptions;
146
+ get corsOptions(): cors.Options;
147
147
  private _jsonOptions;
148
148
  get jsonOptions(): bodyParser.Options;
149
149
  private _cookieOptions;
@@ -156,8 +156,6 @@ export default class PsychicApp {
156
156
  get sslCredentials(): PsychicSslCredentials | undefined;
157
157
  private _saltRounds;
158
158
  get saltRounds(): number | undefined;
159
- private _sanitizeResponseJson;
160
- get sanitizeResponseJson(): boolean;
161
159
  private _packageManager;
162
160
  get packageManager(): "pnpm" | "yarn" | "npm";
163
161
  private _importExtension;
@@ -219,16 +217,15 @@ export default class PsychicApp {
219
217
  load<RT extends 'controllers' | 'services' | 'initializers'>(resourceType: RT, resourcePath: string, importCb: (path: string) => Promise<any>): Promise<void>;
220
218
  private booted;
221
219
  boot(force?: boolean): Promise<void>;
222
- use(on: PsychicUseEventType, handler: RequestHandler): void;
223
- use(handler: RequestHandler): void;
224
- use(handler: () => void): void;
220
+ use(on: PsychicUseEventType, handler: Koa.Middleware): void;
221
+ use(handler: Koa.Middleware): void;
225
222
  plugin(cb: (app: PsychicApp) => void | Promise<void>): void;
226
- on<T extends PsychicHookEventType>(hookEventType: T, cb: T extends 'server:error' ? (err: Error, req: Request, res: Response) => void | Promise<void> : T extends 'server:init:before-middleware' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-middleware' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:start' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:shutdown' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-routes' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'cli:start' ? (program: Command) => void | Promise<void> : T extends 'cli:sync' ? () => any : (conf: PsychicApp) => void | Promise<void>): void;
223
+ on<T extends PsychicHookEventType>(hookEventType: T, cb: T extends 'server:error' ? (err: Error, ctx: Koa.Context) => void | Promise<void> : T extends 'server:init:before-middleware' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-middleware' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:start' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:shutdown' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-routes' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'cli:start' ? (program: Command) => void | Promise<void> : T extends 'cli:sync' ? () => any : (conf: PsychicApp) => void | Promise<void>): void;
227
224
  set(option: 'openapi', name: string, value: NamedPsychicOpenapiOptions): void;
228
- set<Opt extends PsychicAppOption>(option: Opt, value: Opt extends 'appName' ? string : Opt extends 'apiOnly' ? boolean : Opt extends 'defaultResponseHeaders' ? Record<string, string | null> : Opt extends 'httpServerOptions' ? http.ServerOptions | https.ServerOptions : Opt extends 'encryption' ? PsychicAppEncryptionOptions : Opt extends 'cors' ? CorsOptions : Opt extends 'cookie' ? CustomCookieOptions : Opt extends 'apiRoot' ? string : Opt extends 'importExtension' ? GeneratorImportStyle : Opt extends 'sessionCookieName' ? string : Opt extends 'json' ? bodyParser.Options : Opt extends 'logger' ? PsychicLogger : Opt extends 'ssl' ? PsychicSslCredentials : Opt extends 'openapi' ? DefaultPsychicOpenapiOptions : Opt extends 'paths' ? PsychicPathOptions : Opt extends 'port' ? number : Opt extends 'saltRounds' ? number : Opt extends 'sanitizeResponseJson' ? boolean : Opt extends 'packageManager' ? DreamAppAllowedPackageManagersEnum : Opt extends 'inflections' ? () => void | Promise<void> : Opt extends 'routes' ? (r: PsychicRouter) => void | Promise<void> : never): void;
225
+ set<Opt extends PsychicAppOption>(option: Opt, value: Opt extends 'appName' ? string : Opt extends 'apiOnly' ? boolean : Opt extends 'defaultResponseHeaders' ? Record<string, string | null> : Opt extends 'httpServerOptions' ? http.ServerOptions | https.ServerOptions : Opt extends 'encryption' ? PsychicAppEncryptionOptions : Opt extends 'cors' ? cors.Options : Opt extends 'cookie' ? CustomCookieOptions : Opt extends 'apiRoot' ? string : Opt extends 'importExtension' ? GeneratorImportStyle : Opt extends 'sessionCookieName' ? string : Opt extends 'json' ? bodyParser.Options : Opt extends 'logger' ? PsychicLogger : Opt extends 'ssl' ? PsychicSslCredentials : Opt extends 'openapi' ? DefaultPsychicOpenapiOptions : Opt extends 'paths' ? PsychicPathOptions : Opt extends 'port' ? number : Opt extends 'saltRounds' ? number : Opt extends 'packageManager' ? DreamAppAllowedPackageManagersEnum : Opt extends 'inflections' ? () => void | Promise<void> : Opt extends 'routes' ? (r: PsychicRouter) => void | Promise<void> : never): void;
229
226
  override<Override extends keyof PsychicAppOverrides>(override: Override, value: PsychicAppOverrides[Override]): void;
230
227
  }
231
- export type PsychicAppOption = 'appName' | 'apiOnly' | 'apiRoot' | 'httpServerOptions' | 'importExtension' | 'encryption' | 'sessionCookieName' | 'cookie' | 'cors' | 'defaultResponseHeaders' | 'inflections' | 'json' | 'logger' | 'openapi' | 'packageManager' | 'paths' | 'port' | 'routes' | 'saltRounds' | 'sanitizeResponseJson' | 'ssl';
228
+ export type PsychicAppOption = 'appName' | 'apiOnly' | 'apiRoot' | 'httpServerOptions' | 'importExtension' | 'encryption' | 'sessionCookieName' | 'cookie' | 'cors' | 'defaultResponseHeaders' | 'inflections' | 'json' | 'logger' | 'openapi' | 'packageManager' | 'paths' | 'port' | 'routes' | 'saltRounds' | 'ssl';
232
229
  export interface PsychicAppSpecialHooks {
233
230
  cliSync: (() => any)[];
234
231
  serverInitBeforeMiddleware: ((server: PsychicServer) => void | Promise<void>)[];
@@ -236,7 +233,7 @@ export interface PsychicAppSpecialHooks {
236
233
  serverInitAfterRoutes: ((server: PsychicServer) => void | Promise<void>)[];
237
234
  serverStart: ((server: PsychicServer) => void | Promise<void>)[];
238
235
  serverShutdown: ((server: PsychicServer) => void | Promise<void>)[];
239
- serverError: ((err: Error, req: Request, res: Response) => void | Promise<void>)[];
236
+ serverError: ((err: Error, ctx: Koa.Context) => void | Promise<void>)[];
240
237
  }
241
238
  export interface PsychicAppOverrides {
242
239
  ['server:start']: ((psychicServer: PsychicServer, opts: PsychicServerStartProviderOptions) => http.Server | Promise<http.Server>) | null;