@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 +1 -1
- package/package.json +6 -6
- package/src/app.ts +52 -51
- package/src/cache.ts +1 -1
- package/src/console.ts +17 -11
- package/src/database.ts +130 -3
- package/src/event.ts +2 -0
- package/src/http.ts +75 -9
- package/src/middleware.ts +25 -2
- package/src/model.ts +33 -7
- package/src/module.ts +10 -4
- package/src/redis.ts +4 -3
- package/src/repository.ts +52 -9
- package/src/service.ts +7 -5
- package/src/wamp.ts +307 -128
- package/tsconfig.json +47 -0
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
|
+
"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.
|
|
30
|
-
"cache-manager": "^6.4.
|
|
31
|
-
"cacheable": "^1.
|
|
32
|
-
"drizzle-orm": "
|
|
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
|
+
"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(
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
this.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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(
|
|
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
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<
|
|
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
|
|
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
|
-
|
|
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():
|
|
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.
|