@elysiumjs/core 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +1 -1
- package/package.json +2 -2
- package/src/app.ts +21 -50
- package/src/cache.ts +1 -1
- package/src/console.ts +17 -11
- package/src/database.ts +131 -3
- package/src/env.ts +6 -2
- package/src/event.ts +2 -0
- package/src/http.ts +74 -8
- package/src/middleware.ts +44 -2
- package/src/model.ts +67 -17
- package/src/repository.ts +52 -9
- package/src/service.ts +7 -5
- package/src/utils.ts +12 -10
- package/src/wamp.ts +40 -8
- 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.0",
|
|
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,
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@elysiajs/swagger": "^1.2.2",
|
|
30
30
|
"cache-manager": "^6.4.2",
|
|
31
31
|
"cacheable": "^1.8.10",
|
|
32
|
-
"drizzle-orm": "
|
|
32
|
+
"drizzle-orm": "0.43.1-4aa795a",
|
|
33
33
|
"elysia": "latest",
|
|
34
34
|
"radash": "^12.1.0",
|
|
35
35
|
"reflect-metadata": "^0.2.2",
|
package/src/app.ts
CHANGED
|
@@ -47,6 +47,9 @@ export type ElysiumPlugin = (app: Application) => Promise<AnyElysia>;
|
|
|
47
47
|
* @author Axel Nana <axel.nana@workbud.com>
|
|
48
48
|
*/
|
|
49
49
|
export type AppProps = {
|
|
50
|
+
// Allows to pass custom options to the application properties.
|
|
51
|
+
[key: string]: any;
|
|
52
|
+
|
|
50
53
|
/**
|
|
51
54
|
* Enables or disables debug mode.
|
|
52
55
|
*/
|
|
@@ -349,6 +352,15 @@ export abstract class Application extends InteractsWithConsole {
|
|
|
349
352
|
return process.exit(0);
|
|
350
353
|
}
|
|
351
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Retrieves the configuration value for the given key.
|
|
357
|
+
* @param key The key to retrieve the configuration value for.
|
|
358
|
+
* @returns The configuration value for the given key, or `null` if not found.
|
|
359
|
+
*/
|
|
360
|
+
public getConfig<T>(key: keyof AppProps): T | null {
|
|
361
|
+
return Reflect.getMetadata(Symbols.app, this.constructor)?.[key] ?? null;
|
|
362
|
+
}
|
|
363
|
+
|
|
352
364
|
private async commandWork(argv: string[]) {
|
|
353
365
|
this.info('Starting worker process...');
|
|
354
366
|
|
|
@@ -480,13 +492,18 @@ export abstract class Application extends InteractsWithConsole {
|
|
|
480
492
|
.onError({ as: 'global' }, async (e) => {
|
|
481
493
|
if (await this.onError(e)) {
|
|
482
494
|
Event.emit('elysium:error', e);
|
|
495
|
+
this.trace({
|
|
496
|
+
name: 'HttpError',
|
|
497
|
+
message: 'Unexpected error while handling HTTP request',
|
|
498
|
+
// @ts-expect-error The stack property may not exist in the error.
|
|
499
|
+
stack: e.error.stack,
|
|
500
|
+
...e.error
|
|
501
|
+
});
|
|
483
502
|
|
|
484
503
|
let data = {};
|
|
485
504
|
|
|
486
505
|
if (typeof e.code === 'number') {
|
|
487
|
-
|
|
488
|
-
message: e.error.response
|
|
489
|
-
};
|
|
506
|
+
return e.error.response;
|
|
490
507
|
} else {
|
|
491
508
|
switch (e.code) {
|
|
492
509
|
case 'VALIDATION': {
|
|
@@ -505,54 +522,8 @@ export abstract class Application extends InteractsWithConsole {
|
|
|
505
522
|
}
|
|
506
523
|
}
|
|
507
524
|
|
|
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
525
|
return {
|
|
555
|
-
type,
|
|
526
|
+
type: e.code,
|
|
556
527
|
data
|
|
557
528
|
};
|
|
558
529
|
}
|
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,83 @@ 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
|
+
console.log({ key, response, tables, isTag, config });
|
|
123
|
+
await this.#cache.set(key, response, (config?.ex ?? this.globalTtl) * 1000);
|
|
124
|
+
for (const table of tables) {
|
|
125
|
+
const keys = this.usedTablesPerKey[table];
|
|
126
|
+
if (keys === undefined) {
|
|
127
|
+
this.usedTablesPerKey[table] = [key];
|
|
128
|
+
} else {
|
|
129
|
+
keys.push(key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public override async onMutate(params: {
|
|
135
|
+
tags: string | string[];
|
|
136
|
+
tables: string | string[] | Table<any> | Table<any>[];
|
|
137
|
+
}): Promise<void> {
|
|
138
|
+
const tagsArray = params.tags ? (Array.isArray(params.tags) ? params.tags : [params.tags]) : [];
|
|
139
|
+
const tablesArray = params.tables
|
|
140
|
+
? Array.isArray(params.tables)
|
|
141
|
+
? params.tables
|
|
142
|
+
: [params.tables]
|
|
143
|
+
: [];
|
|
144
|
+
|
|
145
|
+
const keysToDelete = new Set<string>();
|
|
146
|
+
|
|
147
|
+
for (const table of tablesArray) {
|
|
148
|
+
const tableName = is(table, Table) ? getTableName(table) : (table as string);
|
|
149
|
+
const keys = this.usedTablesPerKey[tableName] ?? [];
|
|
150
|
+
for (const key of keys) keysToDelete.add(key);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (keysToDelete.size > 0 || tagsArray.length > 0) {
|
|
154
|
+
await this.#cache.mdel(tagsArray);
|
|
155
|
+
await this.#cache.mdel([...keysToDelete]);
|
|
156
|
+
|
|
157
|
+
for (const table of tablesArray) {
|
|
158
|
+
const tableName = is(table, Table) ? getTableName(table) : (table as string);
|
|
159
|
+
this.usedTablesPerKey[tableName] = [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
50
165
|
export namespace Database {
|
|
51
166
|
/**
|
|
52
167
|
* Creates a service name for a database connection.
|
|
@@ -94,7 +209,20 @@ export namespace Database {
|
|
|
94
209
|
process.exit(1);
|
|
95
210
|
}
|
|
96
211
|
|
|
97
|
-
return Service.instance(
|
|
212
|
+
return Service.instance(
|
|
213
|
+
getConnectionName(name),
|
|
214
|
+
drizzle({
|
|
215
|
+
...config,
|
|
216
|
+
cache:
|
|
217
|
+
config.cache === undefined
|
|
218
|
+
? undefined
|
|
219
|
+
: config.cache === true
|
|
220
|
+
? new DatabaseCache()
|
|
221
|
+
: config.cache.enabled
|
|
222
|
+
? new DatabaseCache(config.cache.strategy, config.cache.ttl, config.cache.storage)
|
|
223
|
+
: undefined
|
|
224
|
+
})
|
|
225
|
+
);
|
|
98
226
|
};
|
|
99
227
|
|
|
100
228
|
/**
|
package/src/env.ts
CHANGED
|
@@ -32,10 +32,14 @@ export namespace Env {
|
|
|
32
32
|
* Gets the value of an environment variable.
|
|
33
33
|
* @author Axel Nana <axel.nana@workbud.com>
|
|
34
34
|
* @param name The name of the environment variable.
|
|
35
|
+
* @param defaultValue The default value to return if the environment variable is not set.
|
|
35
36
|
* @returns The value of the environment variable.
|
|
36
37
|
*/
|
|
37
|
-
export const get = <TKey extends keyof AppEnv>(
|
|
38
|
-
|
|
38
|
+
export const get = <TKey extends keyof AppEnv>(
|
|
39
|
+
name: TKey,
|
|
40
|
+
defaultValue?: AppEnv[TKey]
|
|
41
|
+
): AppEnv[TKey] => {
|
|
42
|
+
return (env.get(name) ?? defaultValue) as AppEnv[TKey];
|
|
39
43
|
};
|
|
40
44
|
|
|
41
45
|
/**
|
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.
|
|
@@ -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.error(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.error(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
|
@@ -30,7 +30,7 @@ import { Symbols } from './utils';
|
|
|
30
30
|
export const executeMiddlewareChain = (
|
|
31
31
|
middlewares: Middleware[],
|
|
32
32
|
context: any,
|
|
33
|
-
method: keyof Middleware
|
|
33
|
+
method: Exclude<keyof Middleware, 'guards'>
|
|
34
34
|
): Promise<any> => {
|
|
35
35
|
return Application.context.run(
|
|
36
36
|
new Map([
|
|
@@ -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>
|
|
@@ -102,6 +111,39 @@ export abstract class Middleware {
|
|
|
102
111
|
};
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Creates a guarded version of the middleware.
|
|
116
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
117
|
+
* @param guards The list of guards for the middleware.
|
|
118
|
+
* @returns A middleware class decorated with guards.
|
|
119
|
+
*/
|
|
120
|
+
public static guards(guards: string[]): Class<Middleware> {
|
|
121
|
+
class GuardedMiddleware extends this {}
|
|
122
|
+
Reflect.defineMetadata(Symbols.middlewareGuards, guards, GuardedMiddleware);
|
|
123
|
+
return GuardedMiddleware;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Gets the list of guards for the middleware.
|
|
128
|
+
*/
|
|
129
|
+
public get guards(): TGuards {
|
|
130
|
+
return Reflect.getMetadata(Symbols.middlewareGuards, this.constructor) ?? [];
|
|
131
|
+
}
|
|
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
|
+
|
|
105
147
|
/**
|
|
106
148
|
* Called after the request has been handled, but before the response is sent.
|
|
107
149
|
* @param ctx The request context.
|
package/src/model.ts
CHANGED
|
@@ -12,7 +12,14 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
|
-
import type {
|
|
15
|
+
import type { BuildExtraConfigColumns } from 'drizzle-orm';
|
|
16
|
+
import type {
|
|
17
|
+
PgColumn,
|
|
18
|
+
PgColumnBuilderBase,
|
|
19
|
+
PgSchema,
|
|
20
|
+
PgTable,
|
|
21
|
+
PgTableExtraConfigValue
|
|
22
|
+
} from 'drizzle-orm/pg-core';
|
|
16
23
|
import type { TSchema } from 'elysia';
|
|
17
24
|
|
|
18
25
|
import { getTableConfig, pgSchema, pgTable } from 'drizzle-orm/pg-core';
|
|
@@ -65,7 +72,9 @@ const parseTypes = (element: PgColumn) => {
|
|
|
65
72
|
let type = (() => {
|
|
66
73
|
switch (element.dataType) {
|
|
67
74
|
case 'string':
|
|
68
|
-
return t.String(
|
|
75
|
+
return t.String({
|
|
76
|
+
format: element.columnType === 'PgUUID' ? 'uuid' : undefined
|
|
77
|
+
});
|
|
69
78
|
case 'number':
|
|
70
79
|
return t.Number();
|
|
71
80
|
case 'boolean':
|
|
@@ -89,6 +98,8 @@ const parseTypes = (element: PgColumn) => {
|
|
|
89
98
|
|
|
90
99
|
if (!element.notNull) {
|
|
91
100
|
type = t.Optional(type);
|
|
101
|
+
} else {
|
|
102
|
+
type = t.Required(type);
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
return type;
|
|
@@ -101,8 +112,9 @@ const parseTypes = (element: PgColumn) => {
|
|
|
101
112
|
* @template TTable The drizzle's table schema wrapped by the repository.
|
|
102
113
|
*/
|
|
103
114
|
export type ModelClass<
|
|
115
|
+
TTableName extends string,
|
|
104
116
|
TColumnsMap extends Record<string, PgColumnBuilderBase>,
|
|
105
|
-
TTable extends PgTable = ReturnType<typeof pgTable<
|
|
117
|
+
TTable extends PgTable = ReturnType<typeof pgTable<TTableName, TColumnsMap>>
|
|
106
118
|
> = {
|
|
107
119
|
/**
|
|
108
120
|
* The data type returned by the select queries.
|
|
@@ -128,17 +140,24 @@ export type ModelClass<
|
|
|
128
140
|
/**
|
|
129
141
|
* The name of the table wrapped by the model.
|
|
130
142
|
*/
|
|
131
|
-
readonly tableName:
|
|
143
|
+
readonly tableName: TTableName;
|
|
132
144
|
|
|
133
145
|
/**
|
|
134
146
|
* The table columns configuration.
|
|
135
147
|
*/
|
|
136
148
|
readonly columns: TColumnsMap;
|
|
137
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The extra configuration for the table.
|
|
152
|
+
*/
|
|
153
|
+
readonly extraConfig?: (
|
|
154
|
+
self: BuildExtraConfigColumns<TTableName, TColumnsMap, 'pg'>
|
|
155
|
+
) => PgTableExtraConfigValue[];
|
|
156
|
+
|
|
138
157
|
/**
|
|
139
158
|
* The validation schema for creating records.
|
|
140
159
|
*/
|
|
141
|
-
readonly
|
|
160
|
+
readonly insertSchema: TSchema;
|
|
142
161
|
|
|
143
162
|
/**
|
|
144
163
|
* The validation schema for updating records.
|
|
@@ -173,11 +192,17 @@ export type ModelClass<
|
|
|
173
192
|
* @param tableName The name of the table.
|
|
174
193
|
* @param columns The table columns configuration.
|
|
175
194
|
*/
|
|
176
|
-
export const Model = <
|
|
177
|
-
|
|
178
|
-
|
|
195
|
+
export const Model = <
|
|
196
|
+
TTableName extends string,
|
|
197
|
+
TColumnsMap extends Record<string, PgColumnBuilderBase>
|
|
198
|
+
>(
|
|
199
|
+
tableName: TTableName,
|
|
200
|
+
columns: TColumnsMap,
|
|
201
|
+
extraConfig?: (
|
|
202
|
+
self: BuildExtraConfigColumns<TTableName, TColumnsMap, 'pg'>
|
|
203
|
+
) => PgTableExtraConfigValue[]
|
|
179
204
|
) => {
|
|
180
|
-
const table = pgTable(tableName, columns);
|
|
205
|
+
const table = pgTable(tableName, columns, extraConfig);
|
|
181
206
|
|
|
182
207
|
class M {
|
|
183
208
|
/**
|
|
@@ -198,7 +223,7 @@ export const Model = <TColumnsMap extends Record<string, PgColumnBuilderBase>>(
|
|
|
198
223
|
/**
|
|
199
224
|
* The drizzle's table schema wrapped by this model.
|
|
200
225
|
*/
|
|
201
|
-
public static get table(): ReturnType<typeof pgTable<
|
|
226
|
+
public static get table(): ReturnType<typeof pgTable<TTableName, TColumnsMap>> {
|
|
202
227
|
// If we are not inside an Application context, we can't use the tenancy system
|
|
203
228
|
if (!Application.instance || !Application.context.getStore) {
|
|
204
229
|
return table;
|
|
@@ -207,7 +232,7 @@ export const Model = <TColumnsMap extends Record<string, PgColumnBuilderBase>>(
|
|
|
207
232
|
if (this.supportTenancy) {
|
|
208
233
|
const tenant = Tenancy.getCurrentTenant() ?? 'public';
|
|
209
234
|
// @ts-expect-error typeof this strangely doesn't match the ModelClass type
|
|
210
|
-
return Tenancy.
|
|
235
|
+
return Tenancy.wrapTenant(tenant, this);
|
|
211
236
|
}
|
|
212
237
|
|
|
213
238
|
return table;
|
|
@@ -223,10 +248,15 @@ export const Model = <TColumnsMap extends Record<string, PgColumnBuilderBase>>(
|
|
|
223
248
|
*/
|
|
224
249
|
public static readonly columns: TColumnsMap = columns;
|
|
225
250
|
|
|
251
|
+
/**
|
|
252
|
+
* The extra configuration for the table.
|
|
253
|
+
*/
|
|
254
|
+
public static readonly extraConfig = extraConfig;
|
|
255
|
+
|
|
226
256
|
/**
|
|
227
257
|
* The validation schema for creating records.
|
|
228
258
|
*/
|
|
229
|
-
public static readonly
|
|
259
|
+
public static readonly insertSchema = createSchemaFromDrizzle(table, { mode: 'create' });
|
|
230
260
|
|
|
231
261
|
/**
|
|
232
262
|
* The validation schema for updating records.
|
|
@@ -292,9 +322,13 @@ export namespace Tenancy {
|
|
|
292
322
|
* @param model The model class that wraps the `PgTable`.
|
|
293
323
|
* @returns The drizzle's table schema wrapped by the tenant.
|
|
294
324
|
*/
|
|
295
|
-
export const
|
|
296
|
-
T extends ModelClass<TColumnsMap>,
|
|
325
|
+
export const wrapTenant = <
|
|
326
|
+
T extends ModelClass<TTableName, TColumnsMap>,
|
|
327
|
+
TTableName extends string = T extends ModelClass<infer TTableName, infer TColumnsMap>
|
|
328
|
+
? TTableName
|
|
329
|
+
: string,
|
|
297
330
|
TColumnsMap extends Record<string, PgColumnBuilderBase> = T extends ModelClass<
|
|
331
|
+
TTableName,
|
|
298
332
|
infer TColumnsMap
|
|
299
333
|
>
|
|
300
334
|
? TColumnsMap
|
|
@@ -302,21 +336,21 @@ export namespace Tenancy {
|
|
|
302
336
|
>(
|
|
303
337
|
tenant: string,
|
|
304
338
|
model: T
|
|
305
|
-
): ReturnType<typeof pgTable<
|
|
339
|
+
): ReturnType<typeof pgTable<TTableName, TColumnsMap>> => {
|
|
306
340
|
if (tenant === 'public') {
|
|
307
341
|
return pgTable(model.tableName, model.columns);
|
|
308
342
|
}
|
|
309
343
|
|
|
310
344
|
const tableName = `${tenant}.${model.tableName}`;
|
|
311
345
|
if (tenantSchemas.has(tableName)) {
|
|
312
|
-
return tenantSchemas.get(tableName)! as ReturnType<typeof pgTable<
|
|
346
|
+
return tenantSchemas.get(tableName)! as ReturnType<typeof pgTable<TTableName, TColumnsMap>>;
|
|
313
347
|
}
|
|
314
348
|
|
|
315
349
|
const tenantSchema = getTenantSchema(tenant);
|
|
316
350
|
const schemaTable = tenantSchema.table(model.tableName, model.columns);
|
|
317
351
|
tenantSchemas.set(tableName, schemaTable);
|
|
318
352
|
|
|
319
|
-
return schemaTable as unknown as ReturnType<typeof pgTable<
|
|
353
|
+
return schemaTable as unknown as ReturnType<typeof pgTable<TTableName, TColumnsMap>>;
|
|
320
354
|
};
|
|
321
355
|
|
|
322
356
|
/**
|
|
@@ -333,4 +367,20 @@ export namespace Tenancy {
|
|
|
333
367
|
|
|
334
368
|
return tenantSchema;
|
|
335
369
|
};
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Runs the given callback in a context with the given tenant.
|
|
373
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
374
|
+
* @param tenant The name of the tenant.
|
|
375
|
+
* @param callback The callback to run.
|
|
376
|
+
* @returns The result of the callback.
|
|
377
|
+
*/
|
|
378
|
+
export const withTenant = <TReturn>(tenant: string, callback: () => TReturn): TReturn => {
|
|
379
|
+
const currentStore = Application.context.getStore();
|
|
380
|
+
|
|
381
|
+
const newStore = new Map(currentStore);
|
|
382
|
+
newStore.set('tenant', tenant);
|
|
383
|
+
|
|
384
|
+
return Application.context.run(newStore, callback);
|
|
385
|
+
};
|
|
336
386
|
}
|
package/src/repository.ts
CHANGED
|
@@ -20,7 +20,6 @@ import type { DatabaseConnection } from './database';
|
|
|
20
20
|
import type { ModelClass } from './model';
|
|
21
21
|
|
|
22
22
|
import { eq } from 'drizzle-orm';
|
|
23
|
-
import { pgTable } from 'drizzle-orm/pg-core';
|
|
24
23
|
|
|
25
24
|
import { Application } from './app';
|
|
26
25
|
import { Database } from './database';
|
|
@@ -39,9 +38,13 @@ export type IdType = number | string;
|
|
|
39
38
|
* @template TColumnsMap The table columns config.
|
|
40
39
|
*/
|
|
41
40
|
export interface RepositoryInterface<
|
|
42
|
-
TModel extends ModelClass<TColumnsMap>,
|
|
41
|
+
TModel extends ModelClass<TTableName, TColumnsMap>,
|
|
43
42
|
TId extends IdType = string,
|
|
43
|
+
TTableName extends string = TModel extends ModelClass<infer TTableName, infer _>
|
|
44
|
+
? TTableName
|
|
45
|
+
: string,
|
|
44
46
|
TColumnsMap extends Record<string, PgColumnBuilderBase> = TModel extends ModelClass<
|
|
47
|
+
TTableName,
|
|
45
48
|
infer TColumnsMap
|
|
46
49
|
>
|
|
47
50
|
? TColumnsMap
|
|
@@ -104,14 +107,18 @@ export interface RepositoryInterface<
|
|
|
104
107
|
* @template TColumnsMap The table columns config.
|
|
105
108
|
*/
|
|
106
109
|
export type RepositoryClass<
|
|
107
|
-
TModel extends ModelClass<TColumnsMap>,
|
|
110
|
+
TModel extends ModelClass<TTableName, TColumnsMap>,
|
|
108
111
|
TId extends IdType = string,
|
|
112
|
+
TTableName extends string = TModel extends ModelClass<infer TTableName, infer _>
|
|
113
|
+
? TTableName
|
|
114
|
+
: string,
|
|
109
115
|
TColumnsMap extends Record<string, PgColumnBuilderBase> = TModel extends ModelClass<
|
|
116
|
+
TTableName,
|
|
110
117
|
infer TColumnsMap
|
|
111
118
|
>
|
|
112
119
|
? TColumnsMap
|
|
113
120
|
: Record<string, PgColumnBuilderBase>
|
|
114
|
-
> = Class<RepositoryInterface<TModel, TId, TColumnsMap>> & {
|
|
121
|
+
> = Class<RepositoryInterface<TModel, TId, TTableName, TColumnsMap>> & {
|
|
115
122
|
/**
|
|
116
123
|
* The drizzle's table schema wrapped by the repository.
|
|
117
124
|
*/
|
|
@@ -134,9 +141,13 @@ export type RepositoryClass<
|
|
|
134
141
|
* @template model The model class wrapped by the repository.
|
|
135
142
|
*/
|
|
136
143
|
export const Repository = <
|
|
137
|
-
TModel extends ModelClass<TColumnsMap>,
|
|
144
|
+
TModel extends ModelClass<TTableName, TColumnsMap>,
|
|
138
145
|
TId extends IdType = string,
|
|
146
|
+
TTableName extends string = TModel extends ModelClass<infer TTableName, infer _>
|
|
147
|
+
? TTableName
|
|
148
|
+
: string,
|
|
139
149
|
TColumnsMap extends Record<string, PgColumnBuilderBase> = TModel extends ModelClass<
|
|
150
|
+
TTableName,
|
|
140
151
|
infer TColumnsMap
|
|
141
152
|
>
|
|
142
153
|
? TColumnsMap
|
|
@@ -148,9 +159,7 @@ export const Repository = <
|
|
|
148
159
|
type TInsert = TModel['$inferInsert'];
|
|
149
160
|
type TUpdate = TModel['$inferUpdate'];
|
|
150
161
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
class R implements RepositoryInterface<TModel, TId, TColumnsMap> {
|
|
162
|
+
class R implements RepositoryInterface<TModel, TId, TTableName, TColumnsMap> {
|
|
154
163
|
/**
|
|
155
164
|
* The drizzle's table schema wrapped by this repository.
|
|
156
165
|
*/
|
|
@@ -167,7 +176,8 @@ export const Repository = <
|
|
|
167
176
|
* The database connection used by this repository.
|
|
168
177
|
*/
|
|
169
178
|
public get db(): DatabaseConnection {
|
|
170
|
-
const connection = (this.constructor as RepositoryClass<TModel, TId, TColumnsMap>)
|
|
179
|
+
const connection = (this.constructor as RepositoryClass<TModel, TId, TTableName, TColumnsMap>)
|
|
180
|
+
.connection;
|
|
171
181
|
let db: DatabaseConnection | null = null;
|
|
172
182
|
|
|
173
183
|
if (connection === 'default') {
|
|
@@ -185,6 +195,25 @@ export const Repository = <
|
|
|
185
195
|
return await this.db.select().from(model.table);
|
|
186
196
|
}
|
|
187
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Get a paginated subset of the records in the database.
|
|
200
|
+
* @param page The page number to retrieve.
|
|
201
|
+
* @param count The number of records to retrieve per page.
|
|
202
|
+
* @returns A paginated response containing the records and the total number of records.
|
|
203
|
+
*/
|
|
204
|
+
public async paginate(
|
|
205
|
+
count: number,
|
|
206
|
+
page: number = 1
|
|
207
|
+
): Promise<{ page: number; data: TSelect[]; total: number }> {
|
|
208
|
+
const total = await this.db.$count(model.table);
|
|
209
|
+
const offset = Math.max(page - 1, 0) * count;
|
|
210
|
+
return {
|
|
211
|
+
page,
|
|
212
|
+
total,
|
|
213
|
+
data: await this.db.select().from(model.table).offset(offset).limit(count)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
188
217
|
/**
|
|
189
218
|
* Retrieves a record by its id.
|
|
190
219
|
* @param id The id of the record to retrieve.
|
|
@@ -195,6 +224,20 @@ export const Repository = <
|
|
|
195
224
|
return (row ?? null) as TSelect | null;
|
|
196
225
|
}
|
|
197
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Finds the first record with a column matching the given value.
|
|
229
|
+
* @param column The column to filter by.
|
|
230
|
+
* @param value The value the column should match.
|
|
231
|
+
* @returns The first record with a column matching the given value.
|
|
232
|
+
*/
|
|
233
|
+
public async findBy<TKey extends keyof TSelect>(
|
|
234
|
+
column: TKey,
|
|
235
|
+
value: TSelect[TKey]
|
|
236
|
+
): Promise<TSelect | null> {
|
|
237
|
+
const [row] = await this.db.select().from(model.table).where(eq(model.table[column], value));
|
|
238
|
+
return (row ?? null) as TSelect | null;
|
|
239
|
+
}
|
|
240
|
+
|
|
198
241
|
/**
|
|
199
242
|
* Inserts a new record in the database.
|
|
200
243
|
* @param data The data to insert.
|
package/src/service.ts
CHANGED
|
@@ -169,15 +169,17 @@ export namespace Service {
|
|
|
169
169
|
/**
|
|
170
170
|
* Resolves a registered service and set it as a parameter value.
|
|
171
171
|
* @author Axel Nana <axel.nana@workbud.com>
|
|
172
|
-
* @param
|
|
172
|
+
* @param service An optional name for the service. If not set, the name of the parameter's type is used instead.
|
|
173
173
|
*/
|
|
174
|
-
export const inject = (
|
|
174
|
+
export const inject = (service?: string | ServiceClass): ParameterDecorator => {
|
|
175
175
|
return function (target, propertyKey, parameterIndex) {
|
|
176
176
|
const services = Reflect.getMetadata(Symbols.services, target, propertyKey!) ?? [];
|
|
177
|
-
const types
|
|
177
|
+
const types: Function[] =
|
|
178
|
+
Reflect.getMetadata('design:paramtypes', target, propertyKey!) ?? [];
|
|
179
|
+
const name = isString(service) ? service : (service?.name ?? types[parameterIndex].name);
|
|
178
180
|
|
|
179
181
|
services.push({
|
|
180
|
-
name
|
|
182
|
+
name,
|
|
181
183
|
index: parameterIndex
|
|
182
184
|
});
|
|
183
185
|
|
|
@@ -223,7 +225,7 @@ export namespace Service {
|
|
|
223
225
|
if (s === null) {
|
|
224
226
|
// TODO: Use the logger service here
|
|
225
227
|
console.error(
|
|
226
|
-
`Cannot inject
|
|
228
|
+
`Cannot inject service ${dependency.name}. No service was registered with the name: ${dependency.name}`
|
|
227
229
|
);
|
|
228
230
|
process.exit(1);
|
|
229
231
|
}
|
package/src/utils.ts
CHANGED
|
@@ -17,25 +17,27 @@ import type { Elysia } from 'elysia';
|
|
|
17
17
|
import { isObject, isPrimitive } from 'radash';
|
|
18
18
|
|
|
19
19
|
export namespace Symbols {
|
|
20
|
-
export const app = Symbol('
|
|
20
|
+
export const app = Symbol('elysium:Application');
|
|
21
21
|
|
|
22
|
-
export const controller = Symbol('
|
|
22
|
+
export const controller = Symbol('elysium:Controller');
|
|
23
23
|
|
|
24
|
-
export const middlewares = Symbol('
|
|
24
|
+
export const middlewares = Symbol('elysium:MiddlewaresList');
|
|
25
25
|
|
|
26
|
-
export const services = Symbol('
|
|
26
|
+
export const services = Symbol('elysium:ServicesList');
|
|
27
27
|
|
|
28
|
-
export const websocket = Symbol('
|
|
28
|
+
export const websocket = Symbol('elysium:WebSocket');
|
|
29
29
|
|
|
30
|
-
export const wamp = Symbol('
|
|
30
|
+
export const wamp = Symbol('elysium:Wamp');
|
|
31
31
|
|
|
32
|
-
export const http = Symbol('
|
|
32
|
+
export const http = Symbol('elysium:Http');
|
|
33
33
|
|
|
34
|
-
export const job = Symbol('
|
|
34
|
+
export const job = Symbol('elysium:Job');
|
|
35
35
|
|
|
36
|
-
export const arg = Symbol('
|
|
36
|
+
export const arg = Symbol('elysium:CommandArguments');
|
|
37
37
|
|
|
38
|
-
export const elysiaPlugin = Symbol('
|
|
38
|
+
export const elysiaPlugin = Symbol('elysium:ElysiaPlugin');
|
|
39
|
+
|
|
40
|
+
export const middlewareGuards = Symbol('elysium:MiddlewareGuards');
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export const nextTick = () => new Promise((resolve) => process.nextTick(resolve));
|
package/src/wamp.ts
CHANGED
|
@@ -90,22 +90,32 @@ export type WampProps = {
|
|
|
90
90
|
* The interval to wait between retries when connecting to the WAMP server.
|
|
91
91
|
*/
|
|
92
92
|
retryInterval?: number;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The authentication ID to use in challenges.
|
|
96
|
+
*/
|
|
97
|
+
authid?: string;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* An array of supported authentication methods.
|
|
101
|
+
*/
|
|
102
|
+
authmethods?: string[];
|
|
93
103
|
};
|
|
94
104
|
|
|
95
105
|
/**
|
|
96
106
|
* Data sent to a WAMP RPC.
|
|
97
107
|
* @author Axel Nana <axel.nana@workbud.com>
|
|
98
108
|
*/
|
|
99
|
-
export type WampRegistrationHandlerArgs = {
|
|
109
|
+
export type WampRegistrationHandlerArgs<TList = any[], TDict = any> = {
|
|
100
110
|
/**
|
|
101
111
|
* The arguments passed to the RPC or subscription.
|
|
102
112
|
*/
|
|
103
|
-
argsList:
|
|
113
|
+
argsList: TList;
|
|
104
114
|
|
|
105
115
|
/**
|
|
106
116
|
* The keyword arguments passed to the RPC or subscription.
|
|
107
117
|
*/
|
|
108
|
-
argsDict:
|
|
118
|
+
argsDict: TDict;
|
|
109
119
|
|
|
110
120
|
/**
|
|
111
121
|
* The WAMP details object.
|
|
@@ -213,6 +223,7 @@ export namespace Wamp {
|
|
|
213
223
|
const w = new Wampy(options.url, {
|
|
214
224
|
ws: WampWebsocket,
|
|
215
225
|
...omit(options, ['url']),
|
|
226
|
+
onChallenge: metadata.challenge?.bind(controller),
|
|
216
227
|
onClose: metadata.close?.bind(controller),
|
|
217
228
|
onError() {
|
|
218
229
|
metadata.error?.call(controller);
|
|
@@ -316,11 +327,28 @@ export namespace Wamp {
|
|
|
316
327
|
};
|
|
317
328
|
|
|
318
329
|
/**
|
|
319
|
-
* Marks a method as the WAMP "
|
|
330
|
+
* Marks a method as the WAMP "challenge" event handler.
|
|
331
|
+
*
|
|
332
|
+
* This decorator should be used on a WAMP controller method. Only one "challenge" event handler
|
|
333
|
+
* can be defined per WAMP controller.
|
|
334
|
+
*
|
|
320
335
|
* @author Axel Nana <axel.nana@workbud.com>
|
|
336
|
+
*/
|
|
337
|
+
export const onChallenge = (): MethodDecorator => {
|
|
338
|
+
return function (target, _propertyKey, descriptor) {
|
|
339
|
+
const metadata = Reflect.getMetadata(Symbols.wamp, target.constructor) ?? {};
|
|
340
|
+
metadata.challenge = descriptor.value;
|
|
341
|
+
Reflect.defineMetadata(Symbols.wamp, metadata, target.constructor);
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Marks a method as the WAMP "open" event handler.
|
|
321
347
|
*
|
|
322
348
|
* This decorator should be used on a WAMP controller method. Only one "open" event handler
|
|
323
349
|
* can be defined per WAMP controller.
|
|
350
|
+
*
|
|
351
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
324
352
|
*/
|
|
325
353
|
export const onOpen = (): MethodDecorator => {
|
|
326
354
|
return function (target, _propertyKey, descriptor) {
|
|
@@ -332,10 +360,11 @@ export namespace Wamp {
|
|
|
332
360
|
|
|
333
361
|
/**
|
|
334
362
|
* Marks a method as the WAMP "close" event handler.
|
|
335
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
336
363
|
*
|
|
337
364
|
* This decorator should be used on a WAMP controller method. Only one "close" event handler
|
|
338
365
|
* can be defined per WAMP controller.
|
|
366
|
+
*
|
|
367
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
339
368
|
*/
|
|
340
369
|
export const onClose = (): MethodDecorator => {
|
|
341
370
|
return function (target, _propertyKey, descriptor) {
|
|
@@ -347,10 +376,11 @@ export namespace Wamp {
|
|
|
347
376
|
|
|
348
377
|
/**
|
|
349
378
|
* Marks a method as the WAMP "error" event handler.
|
|
350
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
351
379
|
*
|
|
352
380
|
* This decorator should be used on a WAMP controller method. Only one "error" event handler
|
|
353
381
|
* can be defined per WAMP controller.
|
|
382
|
+
*
|
|
383
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
354
384
|
*/
|
|
355
385
|
export const onError = (): MethodDecorator => {
|
|
356
386
|
return function (target, _propertyKey, descriptor) {
|
|
@@ -362,10 +392,11 @@ export namespace Wamp {
|
|
|
362
392
|
|
|
363
393
|
/**
|
|
364
394
|
* Marks a method as the WAMP "reconnect" event handler.
|
|
365
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
366
395
|
*
|
|
367
396
|
* This decorator should be used on a WAMP controller method. Only one "reconnect" event handler
|
|
368
397
|
* can be defined per WAMP controller.
|
|
398
|
+
*
|
|
399
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
369
400
|
*/
|
|
370
401
|
export const onReconnect = (): MethodDecorator => {
|
|
371
402
|
return function (target, _propertyKey, descriptor) {
|
|
@@ -377,10 +408,11 @@ export namespace Wamp {
|
|
|
377
408
|
|
|
378
409
|
/**
|
|
379
410
|
* Marks a method as the WAMP "reconnectSuccess" event handler.
|
|
380
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
381
411
|
*
|
|
382
412
|
* This decorator should be used on a WAMP controller method. Only one "reconnectSuccess" event handler
|
|
383
413
|
* can be defined per WAMP controller.
|
|
414
|
+
*
|
|
415
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
384
416
|
*/
|
|
385
417
|
export const onReconnectSuccess = (): MethodDecorator => {
|
|
386
418
|
return function (target, _propertyKey, descriptor) {
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
{
|
|
15
|
+
"compilerOptions": {
|
|
16
|
+
// Environment setup & latest features
|
|
17
|
+
"target": "ESNext",
|
|
18
|
+
"module": "ESNext",
|
|
19
|
+
"moduleDetection": "force",
|
|
20
|
+
"allowJs": true,
|
|
21
|
+
"types": [
|
|
22
|
+
"bun-types",
|
|
23
|
+
"type-fest"
|
|
24
|
+
],
|
|
25
|
+
"experimentalDecorators": true,
|
|
26
|
+
"emitDecoratorMetadata": true,
|
|
27
|
+
// Bundler mode
|
|
28
|
+
"moduleResolution": "bundler",
|
|
29
|
+
"allowImportingTsExtensions": true,
|
|
30
|
+
"verbatimModuleSyntax": true,
|
|
31
|
+
"noEmit": true,
|
|
32
|
+
// Best practices
|
|
33
|
+
"strict": true,
|
|
34
|
+
"skipLibCheck": true,
|
|
35
|
+
"noFallthroughCasesInSwitch": true,
|
|
36
|
+
"noUncheckedIndexedAccess": false,
|
|
37
|
+
"forceConsistentCasingInFileNames": true,
|
|
38
|
+
// Some stricter flags (disabled by default)
|
|
39
|
+
"noUnusedLocals": true,
|
|
40
|
+
"noUnusedParameters": true,
|
|
41
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
42
|
+
},
|
|
43
|
+
"exclude": [
|
|
44
|
+
"node_modules",
|
|
45
|
+
"dist",
|
|
46
|
+
],
|
|
47
|
+
}
|