@elysiumjs/core 0.3.2 → 0.4.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.
package/index.ts CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  import './macros';
16
16
 
17
- export { type Elysia, t } from 'elysia';
17
+ export { type Elysia, type TSchema, type Static, t } from 'elysia';
18
18
  export { type AppContext, type ElysiumPlugin, Application } from './src/app';
19
19
  export { Cache } from './src/cache';
20
20
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elysiumjs/core",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "license": "Apache-2.0",
5
5
  "description": "A super-charged web framework for Bun, built on top of ElysiaJS.",
6
6
  "private": false,
@@ -26,10 +26,10 @@
26
26
  "module": "index.ts",
27
27
  "type": "module",
28
28
  "dependencies": {
29
- "@elysiajs/swagger": "^1.2.2",
30
- "cache-manager": "^6.4.2",
31
- "cacheable": "^1.8.10",
32
- "drizzle-orm": "^0.43.1",
29
+ "@elysiajs/swagger": "^1.3.0",
30
+ "cache-manager": "^6.4.3",
31
+ "cacheable": "^1.9.0",
32
+ "drizzle-orm": "0.43.1-4aa795a",
33
33
  "elysia": "latest",
34
34
  "radash": "^12.1.0",
35
35
  "reflect-metadata": "^0.2.2",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@ianvs/prettier-plugin-sort-imports": "^4.4.1",
40
- "type-fest": "^4.40.1",
40
+ "type-fest": "^4.41.0",
41
41
  "@types/bun": "latest"
42
42
  },
43
43
  "peerDependencies": {
package/src/app.ts CHANGED
@@ -19,6 +19,7 @@ import type { DatabaseConnectionProps } from './database';
19
19
  import type { Route } from './http';
20
20
  import type { ModuleClass } from './module';
21
21
  import type { RedisConnectionProps } from './redis';
22
+ import type { WampClientProps } from './wamp';
22
23
 
23
24
  import { AsyncLocalStorage } from 'node:async_hooks';
24
25
  import { parseArgs } from 'node:util';
@@ -35,6 +36,7 @@ import { applyMiddlewares } from './middleware';
35
36
  import { Redis } from './redis';
36
37
  import { Service } from './service';
37
38
  import { deepMerge, Symbols } from './utils';
39
+ import { Wamp } from './wamp';
38
40
 
39
41
  /**
40
42
  * An application plugin, consisting of a function that returns an Elysia instance.
@@ -47,6 +49,9 @@ export type ElysiumPlugin = (app: Application) => Promise<AnyElysia>;
47
49
  * @author Axel Nana <axel.nana@workbud.com>
48
50
  */
49
51
  export type AppProps = {
52
+ // Allows to pass custom options to the application properties.
53
+ [key: string]: any;
54
+
50
55
  /**
51
56
  * Enables or disables debug mode.
52
57
  */
@@ -62,6 +67,21 @@ export type AppProps = {
62
67
  port?: number;
63
68
  };
64
69
 
70
+ /**
71
+ * The wamp client configuration for the app.
72
+ */
73
+ wamp?: {
74
+ /**
75
+ * The default connection name.
76
+ */
77
+ default: string;
78
+
79
+ /**
80
+ * The list of connections.
81
+ */
82
+ connections: Record<string, WampClientProps>;
83
+ };
84
+
65
85
  /**
66
86
  * The list of modules provided the app.
67
87
  */
@@ -214,7 +234,10 @@ export abstract class Application extends InteractsWithConsole {
214
234
 
215
235
  Service.instance('elysium.app', this);
216
236
 
217
- const { database, redis } = Reflect.getMetadata(Symbols.app, this.constructor) as AppProps;
237
+ const { database, redis, wamp } = Reflect.getMetadata(
238
+ Symbols.app,
239
+ this.constructor
240
+ ) as AppProps;
218
241
 
219
242
  if (redis) {
220
243
  for (const connectionName in redis.connections) {
@@ -236,6 +259,16 @@ export abstract class Application extends InteractsWithConsole {
236
259
  }
237
260
  }
238
261
 
262
+ if (wamp) {
263
+ for (const connectionName in wamp.connections) {
264
+ Wamp.registerConnection(connectionName, wamp.connections[connectionName]);
265
+ }
266
+
267
+ if (Wamp.connectionExists(wamp.default)) {
268
+ Wamp.setDefaultConnection(wamp.default);
269
+ }
270
+ }
271
+
239
272
  // Run the application
240
273
  this.run()
241
274
  .then(() => Event.emit('elysium:app:launched', this, this))
@@ -349,6 +382,15 @@ export abstract class Application extends InteractsWithConsole {
349
382
  return process.exit(0);
350
383
  }
351
384
 
385
+ /**
386
+ * Retrieves the configuration value for the given key.
387
+ * @param key The key to retrieve the configuration value for.
388
+ * @returns The configuration value for the given key, or `null` if not found.
389
+ */
390
+ public getConfig<T>(key: keyof AppProps): T | null {
391
+ return Reflect.getMetadata(Symbols.app, this.constructor)?.[key] ?? null;
392
+ }
393
+
352
394
  private async commandWork(argv: string[]) {
353
395
  this.info('Starting worker process...');
354
396
 
@@ -480,13 +522,18 @@ export abstract class Application extends InteractsWithConsole {
480
522
  .onError({ as: 'global' }, async (e) => {
481
523
  if (await this.onError(e)) {
482
524
  Event.emit('elysium:error', e);
525
+ this.trace({
526
+ name: 'HttpError',
527
+ message: 'Unexpected error while handling HTTP request',
528
+ // @ts-expect-error The stack property may not exist in the error.
529
+ stack: e.error.stack,
530
+ ...e.error
531
+ });
483
532
 
484
533
  let data = {};
485
534
 
486
535
  if (typeof e.code === 'number') {
487
- data = {
488
- message: e.error.response
489
- };
536
+ return e.error.response;
490
537
  } else {
491
538
  switch (e.code) {
492
539
  case 'VALIDATION': {
@@ -505,54 +552,8 @@ export abstract class Application extends InteractsWithConsole {
505
552
  }
506
553
  }
507
554
 
508
- const type = (() => {
509
- if (typeof e.code === 'string') return e.code;
510
- else {
511
- switch (e.code) {
512
- case 500:
513
- return 'INTERNAL_SERVER_ERROR';
514
- case 400:
515
- return 'BAD_REQUEST';
516
- case 401:
517
- return 'UNAUTHORIZED';
518
- case 403:
519
- return 'FORBIDDEN';
520
- case 404:
521
- return 'NOT_FOUND';
522
- case 405:
523
- return 'METHOD_NOT_ALLOWED';
524
- case 409:
525
- return 'CONFLICT';
526
- case 429:
527
- return 'TOO_MANY_REQUESTS';
528
- case 422:
529
- return 'UNPROCESSABLE_ENTITY';
530
- case 426:
531
- return 'UPGRADE_REQUIRED';
532
- case 428:
533
- return 'PRECONDITION_REQUIRED';
534
- case 431:
535
- return 'REQUEST_HEADER_FIELDS_TOO_LARGE';
536
- case 451:
537
- return 'UNAVAILABLE_FOR_LEGAL_REASONS';
538
- case 501:
539
- return 'NOT_IMPLEMENTED';
540
- case 502:
541
- return 'BAD_GATEWAY';
542
- case 503:
543
- return 'SERVICE_UNAVAILABLE';
544
- case 504:
545
- return 'GATEWAY_TIMEOUT';
546
- case 505:
547
- return 'HTTP_VERSION_NOT_SUPPORTED';
548
- }
549
- }
550
-
551
- return 'UNKNOWN';
552
- })();
553
-
554
555
  return {
555
- type,
556
+ type: e.code,
556
557
  data
557
558
  };
558
559
  }
package/src/cache.ts CHANGED
@@ -25,7 +25,7 @@ import { KeyvRedis } from './redis';
25
25
  * Interface for a cache storage.
26
26
  * @author Axel Nana <axel.nana@workbud.com>
27
27
  */
28
- interface CacheInterface {
28
+ export interface CacheInterface {
29
29
  /**
30
30
  * Retrieves the cached data for a given key.
31
31
  * @typeParam T The type of the cached data.
package/src/console.ts CHANGED
@@ -142,22 +142,26 @@ export class InteractsWithConsole {
142
142
  // Display the title if provided
143
143
  if (title) {
144
144
  this.title(title);
145
+ } else {
146
+ this.newLine();
145
147
  }
146
148
 
147
149
  // Display the error name and message in red
148
150
  this.write(this.format(`${errorName}: ${errorMessage}`, ConsoleFormat.RED));
149
151
 
150
- // Display the stack trace in gray, each line prefixed by a tab for readability
151
- this.write(this.format('Stack Trace:', ConsoleFormat.BOLD));
152
- this.write(
153
- this.format(
154
- errorStack
155
- .split('\n')
156
- .map((line) => `\t${line}`)
157
- .join('\n'),
158
- ConsoleFormat.GRAY
159
- )
160
- );
152
+ if (error.stack) {
153
+ // Display the stack trace in gray, each line prefixed by a tab for readability
154
+ this.write(this.format('Stack Trace:', ConsoleFormat.BOLD));
155
+ this.write(
156
+ this.format(
157
+ errorStack
158
+ .split('\n')
159
+ .map((line) => `\t${line}`)
160
+ .join('\n'),
161
+ ConsoleFormat.GRAY
162
+ )
163
+ );
164
+ }
161
165
 
162
166
  // Display any other custom properties
163
167
  const customProps = omit(error, ['name', 'message', 'stack']);
@@ -169,6 +173,8 @@ export class InteractsWithConsole {
169
173
  this.write(`\t${prop}: ${JSON.stringify(value)}`);
170
174
  });
171
175
  }
176
+
177
+ this.newLine();
172
178
  }
173
179
 
174
180
  /**
package/src/database.ts CHANGED
@@ -14,9 +14,14 @@
14
14
 
15
15
  import type { DrizzleConfig } from 'drizzle-orm';
16
16
  import type { BunSQLDatabase } from 'drizzle-orm/bun-sql';
17
+ import type { CacheConfig } from 'drizzle-orm/cache/core/types';
18
+ import type { CacheInterface } from './cache';
17
19
 
20
+ import { getTableName, is, Table } from 'drizzle-orm';
18
21
  import { drizzle } from 'drizzle-orm/bun-sql';
22
+ import { Cache as DrizzleCache } from 'drizzle-orm/cache/core';
19
23
 
24
+ import { Cache } from './cache';
20
25
  import { Service } from './service';
21
26
 
22
27
  /**
@@ -25,7 +30,7 @@ import { Service } from './service';
25
30
  */
26
31
  export type DatabaseConnectionProps<
27
32
  TSchema extends Record<string, unknown> = Record<string, never>
28
- > = DrizzleConfig<TSchema> &
33
+ > = Omit<DrizzleConfig<TSchema>, 'cache'> &
29
34
  (
30
35
  | {
31
36
  connection:
@@ -37,7 +42,40 @@ export type DatabaseConnectionProps<
37
42
  | {
38
43
  client: Bun.SQL;
39
44
  }
40
- );
45
+ ) & {
46
+ /**
47
+ * Configures caching behavior for the database connection.
48
+ * Set it to `true` to enable caching with default configurations.
49
+ */
50
+ cache?:
51
+ | true
52
+ | {
53
+ /**
54
+ * Whether to enable caching.
55
+ */
56
+ enabled?: boolean;
57
+
58
+ /**
59
+ * The default TTL for cached data, in seconds.
60
+ * @default 1
61
+ */
62
+ ttl?: number;
63
+
64
+ /**
65
+ * The caching strategy.
66
+ * Set it to `all` to cache all queries automatically.
67
+ * Set it to `explicit` to cache queries manually.
68
+ * @default 'explicit'
69
+ */
70
+ strategy?: 'explicit' | 'all';
71
+
72
+ /**
73
+ * The storage engine to use.
74
+ * @default 'redis'
75
+ */
76
+ storage?: 'redis' | 'memory';
77
+ };
78
+ };
41
79
 
42
80
  /**
43
81
  * A database connection.
@@ -47,6 +85,82 @@ export type DatabaseConnection = BunSQLDatabase<Record<string, never>> & {
47
85
  $client: Bun.SQL;
48
86
  };
49
87
 
88
+ /**
89
+ * Database cache implementation for Drizzle.
90
+ * @author Axel Nana <axel.nana@workbud.com>
91
+ */
92
+ export class DatabaseCache extends DrizzleCache {
93
+ #cache: Omit<CacheInterface, 'tags'>;
94
+
95
+ private usedTablesPerKey: Record<string, string[]> = {};
96
+
97
+ constructor(
98
+ private readonly cacheStrategy: 'explicit' | 'all' = 'explicit',
99
+ private readonly globalTtl: number = 1,
100
+ storageEngine: 'redis' | 'memory' = 'redis'
101
+ ) {
102
+ super();
103
+ this.#cache = Cache[storageEngine].tags('database');
104
+ }
105
+
106
+ public override strategy(): 'explicit' | 'all' {
107
+ return this.cacheStrategy;
108
+ }
109
+
110
+ public override async get(key: string): Promise<any[] | undefined> {
111
+ const res = (await this.#cache.get<any[]>(key)) ?? undefined;
112
+ return res;
113
+ }
114
+
115
+ public override async put(
116
+ key: string,
117
+ response: any,
118
+ tables: string[],
119
+ isTag: boolean,
120
+ config?: CacheConfig
121
+ ): Promise<void> {
122
+ await this.#cache.set(key, response, (config?.ex ?? this.globalTtl) * 1000);
123
+ for (const table of tables) {
124
+ const keys = this.usedTablesPerKey[table];
125
+ if (keys === undefined) {
126
+ this.usedTablesPerKey[table] = [key];
127
+ } else {
128
+ keys.push(key);
129
+ }
130
+ }
131
+ }
132
+
133
+ public override async onMutate(params: {
134
+ tags: string | string[];
135
+ tables: string | string[] | Table<any> | Table<any>[];
136
+ }): Promise<void> {
137
+ const tagsArray = params.tags ? (Array.isArray(params.tags) ? params.tags : [params.tags]) : [];
138
+ const tablesArray = params.tables
139
+ ? Array.isArray(params.tables)
140
+ ? params.tables
141
+ : [params.tables]
142
+ : [];
143
+
144
+ const keysToDelete = new Set<string>();
145
+
146
+ for (const table of tablesArray) {
147
+ const tableName = is(table, Table) ? getTableName(table) : (table as string);
148
+ const keys = this.usedTablesPerKey[tableName] ?? [];
149
+ for (const key of keys) keysToDelete.add(key);
150
+ }
151
+
152
+ if (keysToDelete.size > 0 || tagsArray.length > 0) {
153
+ await this.#cache.mdel(tagsArray);
154
+ await this.#cache.mdel([...keysToDelete]);
155
+
156
+ for (const table of tablesArray) {
157
+ const tableName = is(table, Table) ? getTableName(table) : (table as string);
158
+ this.usedTablesPerKey[tableName] = [];
159
+ }
160
+ }
161
+ }
162
+ }
163
+
50
164
  export namespace Database {
51
165
  /**
52
166
  * Creates a service name for a database connection.
@@ -94,7 +208,20 @@ export namespace Database {
94
208
  process.exit(1);
95
209
  }
96
210
 
97
- return Service.instance(getConnectionName(name), drizzle(config));
211
+ return Service.instance(
212
+ getConnectionName(name),
213
+ drizzle({
214
+ ...config,
215
+ cache:
216
+ config.cache === undefined
217
+ ? undefined
218
+ : config.cache === true
219
+ ? new DatabaseCache()
220
+ : config.cache.enabled
221
+ ? new DatabaseCache(config.cache.strategy, config.cache.ttl, config.cache.storage)
222
+ : undefined
223
+ })
224
+ );
98
225
  };
99
226
 
100
227
  /**
package/src/event.ts CHANGED
@@ -25,6 +25,8 @@ class EventBus extends EventEmitter {
25
25
 
26
26
  private constructor() {
27
27
  super();
28
+
29
+ this.on('error', (error) => this.emitError(error));
28
30
  }
29
31
 
30
32
  public emitError(error: unknown) {
package/src/http.ts CHANGED
@@ -33,6 +33,32 @@ import { nextTick, Symbols } from './utils';
33
33
  */
34
34
  export type Route = `/${string}`;
35
35
 
36
+ /**
37
+ * A class for validating data against a schema.
38
+ * @author Axel Nana <axel.nana@workbud.com>
39
+ */
40
+ export type ValidatorClass<T extends TSchema = TSchema> = {
41
+ /**
42
+ * The validation schema. This schema is automatically checked against the input data during
43
+ * the validation stage of the HTTP request.
44
+ */
45
+ readonly schema: T;
46
+
47
+ /**
48
+ * Whether the request should be authorized before validation. If the request is not authorized,
49
+ * the validation will be skipped.
50
+ */
51
+ authorize(ctx: Context<T['static']>): Promise<boolean>;
52
+
53
+ /**
54
+ * Processes custom validation logic. This method is called after the validation schema is checked
55
+ * against the input data. So it's safe to assume that the schema is valid.
56
+ * @param data The input data to validate.
57
+ * @returns Whether the input data is valid.
58
+ */
59
+ validate(ctx: Context<T['static']>): Promise<boolean>;
60
+ };
61
+
36
62
  /**
37
63
  * A function that handles an HTTP request.
38
64
  * @author Axel Nana <axel.nana@workbud.com>
@@ -62,12 +88,12 @@ type HttpRequestHandlerMetadata = {
62
88
  /**
63
89
  * The response schema.
64
90
  */
65
- response?: TSchema;
91
+ response?: TSchema | Record<number, TSchema>;
66
92
 
67
93
  /**
68
94
  * The request body schema.
69
95
  */
70
- body?: { schema?: TSchema; index: number };
96
+ body?: { schema?: TSchema; validator?: ValidatorClass; index: number };
71
97
 
72
98
  /**
73
99
  * The request query schema.
@@ -164,8 +190,14 @@ export type Singleton<TController = unknown, TModule extends Module = Module> =
164
190
  * The Elysia context with the controller injected.
165
191
  * @author Axel Nana <axel.nana@workbud.com>
166
192
  */
167
- export type Context<TController = unknown, TModule extends Module = Module> = ElysiaContext<
168
- {},
193
+ export type Context<
194
+ TBody = unknown,
195
+ TController = unknown,
196
+ TModule extends Module = Module
197
+ > = ElysiaContext<
198
+ {
199
+ body: TBody;
200
+ },
169
201
  Singleton<TController, TModule>
170
202
  >;
171
203
 
@@ -271,7 +303,7 @@ export type RequestHandlerDecoratorProps = {
271
303
  /**
272
304
  * The schema of the response body.
273
305
  */
274
- response?: TSchema;
306
+ response?: TSchema | Record<number, TSchema>;
275
307
 
276
308
  /**
277
309
  * The operation ID for the handler.
@@ -302,7 +334,7 @@ export namespace Http {
302
334
  return function (target: Class<any>) {
303
335
  async function handleHttp(): Promise<ElysiaApp> {
304
336
  // TODO: Use the logger service here
305
- console.log(`Registering HTTP route for ${props.path} using ${target.name}`);
337
+ console.log(`Registering HTTP controller for ${props.path} using ${target.name}`);
306
338
  await nextTick();
307
339
 
308
340
  props = assign({ path: '/', scope: HttpControllerScope.SERVER, tags: [] }, props);
@@ -373,6 +405,8 @@ export namespace Http {
373
405
  const isGenerator = route.handler.constructor.name.includes('GeneratorFunction');
374
406
  const isSSE = route.method === 'elysium:SSE';
375
407
  const isTransactional = route.transactional ?? props.transactional ?? false;
408
+ const isValidator =
409
+ route.body && route.body.schema === undefined && route.body.validator !== undefined;
376
410
 
377
411
  route.method = isSSE ? 'GET' : route.method;
378
412
 
@@ -439,6 +473,17 @@ export namespace Http {
439
473
  };
440
474
  }
441
475
 
476
+ if (isValidator) {
477
+ return async function (c: Context) {
478
+ if (await route.body!.validator!.validate(c)) {
479
+ const handler = initTransactedHandler(c);
480
+ return await handler(...(await getParameters(c)));
481
+ }
482
+
483
+ throw c.status(400, 'Invalid request body');
484
+ };
485
+ }
486
+
442
487
  return async function (c: Context) {
443
488
  const handler = initTransactedHandler(c);
444
489
  return await handler(...(await getParameters(c)));
@@ -456,6 +501,13 @@ export namespace Http {
456
501
  app.route(route.method, route.path, getHandler(), {
457
502
  // @ts-ignore
458
503
  config: {},
504
+ async transform(c) {
505
+ if (isValidator && !(await route.body!.validator!.authorize(c as Context))) {
506
+ throw c.status(401, 'Unauthorized');
507
+ }
508
+
509
+ return executeMiddlewareChain(mi, c, 'onTransform');
510
+ },
459
511
  beforeHandle(c) {
460
512
  return executeMiddlewareChain(mi, c, 'onBeforeHandle');
461
513
  },
@@ -470,7 +522,7 @@ export namespace Http {
470
522
  description: route.description,
471
523
  operationId: route.operationId
472
524
  },
473
- body: route.body?.schema,
525
+ body: isValidator ? route.body?.validator?.schema : route.body?.schema,
474
526
  response: route.response,
475
527
  params: isEmpty(params) ? undefined : t.Object(params)
476
528
  });
@@ -647,9 +699,23 @@ export namespace Http {
647
699
  * @author Axel Nana <axel.nana@workbud.com>
648
700
  * @param schema The schema of the request body.
649
701
  */
650
- export const body = (schema?: TSchema): ParameterDecorator => {
702
+ export const body = (schema?: TSchema | ValidatorClass): ParameterDecorator => {
651
703
  return function (target, propertyKey, parameterIndex) {
652
- Reflect.defineMetadata('http:body', { schema, index: parameterIndex }, target, propertyKey!);
704
+ if (typeof schema === 'function') {
705
+ Reflect.defineMetadata(
706
+ 'http:body',
707
+ { validator: schema, index: parameterIndex },
708
+ target,
709
+ propertyKey!
710
+ );
711
+ } else {
712
+ Reflect.defineMetadata(
713
+ 'http:body',
714
+ { schema, index: parameterIndex },
715
+ target,
716
+ propertyKey!
717
+ );
718
+ }
653
719
  };
654
720
  };
655
721
 
package/src/middleware.ts CHANGED
@@ -61,6 +61,14 @@ export const executeMiddlewareChain = (
61
61
  export const applyMiddlewares = (middlewares: Class<Middleware>[], plugin: AnyElysia) => {
62
62
  const mi = middlewares.map((middleware) => Service.make<Middleware>(middleware));
63
63
 
64
+ plugin.onRequest((c) => {
65
+ return executeMiddlewareChain(mi, c, 'onRequest');
66
+ });
67
+
68
+ plugin.onTransform((c) => {
69
+ return executeMiddlewareChain(mi, c, 'onTransform');
70
+ });
71
+
64
72
  plugin.onBeforeHandle((c) => {
65
73
  return executeMiddlewareChain(mi, c, 'onBeforeHandle');
66
74
  });
@@ -77,8 +85,9 @@ export const applyMiddlewares = (middlewares: Class<Middleware>[], plugin: AnyEl
77
85
  /**
78
86
  * Base class for middleware.
79
87
  * @author Axel Nana <axel.nana@workbud.com>
88
+ * @template TGuards The list of guards for the middleware.
80
89
  */
81
- export abstract class Middleware {
90
+ export abstract class Middleware<TGuards extends string[] = []> {
82
91
  /**
83
92
  * Registers a list of middlewares to be applied on a controller or endpoint.
84
93
  * @author Axel Nana <axel.nana@workbud.com>
@@ -117,10 +126,24 @@ export abstract class Middleware {
117
126
  /**
118
127
  * Gets the list of guards for the middleware.
119
128
  */
120
- public get guards(): string[] {
129
+ public get guards(): TGuards {
121
130
  return Reflect.getMetadata(Symbols.middlewareGuards, this.constructor) ?? [];
122
131
  }
123
132
 
133
+ /**
134
+ * Called first before everything else. This middleware hook works only when used on
135
+ * a controller, a module, or the application.
136
+ * @param ctx The request context.
137
+ */
138
+ public onRequest(ctx: Context): any {}
139
+
140
+ /**
141
+ * Called before validation, but after the request has been parsed.
142
+ * Use this method to modify the request inputs before validation.
143
+ * @param ctx The request context.
144
+ */
145
+ public onTransform(ctx: Context): any {}
146
+
124
147
  /**
125
148
  * Called after the request has been handled, but before the response is sent.
126
149
  * @param ctx The request context.