@elysiumjs/core 0.1.1 → 0.3.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 +5 -1
- package/macros.ts +16 -0
- package/package.json +3 -3
- package/src/app.ts +175 -32
- package/src/console.ts +1 -1
- package/src/env.ts +58 -0
- package/src/http.ts +44 -43
- package/src/middleware.ts +5 -5
- package/src/model.ts +27 -3
- package/src/repository.ts +11 -20
- package/src/utils.ts +52 -0
- package/src/wamp.ts +38 -34
- package/src/websocket.ts +11 -6
- package/src/worker.ts +16 -9
- package/types.d.ts +35 -0
- package/.prettierignore +0 -5
- package/.prettierrc +0 -25
- package/bun.lock +0 -262
package/index.ts
CHANGED
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
import './macros';
|
|
16
|
+
|
|
17
|
+
export { type Elysia, t } from 'elysia';
|
|
18
|
+
export { type AppContext, type ElysiumPlugin, Application } from './src/app';
|
|
16
19
|
export { Cache } from './src/cache';
|
|
17
20
|
export {
|
|
18
21
|
type CommandArgumentProps,
|
|
@@ -22,6 +25,7 @@ export {
|
|
|
22
25
|
} from './src/command';
|
|
23
26
|
export { ConsoleFormat, InteractsWithConsole } from './src/console';
|
|
24
27
|
export { Database } from './src/database';
|
|
28
|
+
export { Env } from './src/env';
|
|
25
29
|
export { type EventData, type EventHandler, Event } from './src/event';
|
|
26
30
|
export { type Context, type Route, HttpControllerScope, Http } from './src/http';
|
|
27
31
|
export { Job } from './src/job';
|
package/macros.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
// Disable build mode by default
|
|
16
|
+
global.ELYSIUM_BUILD = typeof ELYSIUM_BUILD !== 'undefined';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elysiumjs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": "^0.
|
|
32
|
+
"drizzle-orm": "^0.43.1",
|
|
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.
|
|
40
|
+
"type-fest": "^4.40.1",
|
|
41
41
|
"@types/bun": "latest"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
package/src/app.ts
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
15
|
import type { ElysiaSwaggerConfig } from '@elysiajs/swagger';
|
|
16
|
-
import type { ElysiaConfig, ErrorContext,
|
|
16
|
+
import type { AnyElysia, ElysiaConfig, ErrorContext, TSchema } from 'elysia';
|
|
17
17
|
import type { CommandClass } from './command';
|
|
18
18
|
import type { DatabaseConnectionProps } from './database';
|
|
19
|
-
import type { Route
|
|
19
|
+
import type { Route } from './http';
|
|
20
20
|
import type { ModuleClass } from './module';
|
|
21
21
|
import type { RedisConnectionProps } from './redis';
|
|
22
22
|
|
|
@@ -24,16 +24,23 @@ import { AsyncLocalStorage } from 'node:async_hooks';
|
|
|
24
24
|
import { parseArgs } from 'node:util';
|
|
25
25
|
|
|
26
26
|
import { swagger as swaggerPlugin } from '@elysiajs/swagger';
|
|
27
|
+
import { Value } from '@sinclair/typebox/value';
|
|
27
28
|
import { Elysia } from 'elysia';
|
|
28
|
-
import { assign } from 'radash';
|
|
29
29
|
|
|
30
30
|
import { ConsoleFormat, InteractsWithConsole } from './console';
|
|
31
31
|
import { Database } from './database';
|
|
32
|
+
import { initEnv } from './env';
|
|
32
33
|
import { Event } from './event';
|
|
33
34
|
import { applyMiddlewares } from './middleware';
|
|
34
35
|
import { Redis } from './redis';
|
|
35
36
|
import { Service } from './service';
|
|
36
|
-
import { Symbols } from './utils';
|
|
37
|
+
import { deepMerge, Symbols } from './utils';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* An application plugin, consisting of a function that returns an Elysia instance.
|
|
41
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
42
|
+
*/
|
|
43
|
+
export type ElysiumPlugin = (app: Application) => Promise<AnyElysia>;
|
|
37
44
|
|
|
38
45
|
/**
|
|
39
46
|
* Properties required when declaring an app using the `@app()` decorator.
|
|
@@ -101,6 +108,16 @@ export type AppProps = {
|
|
|
101
108
|
* Set it to `false` to disable Swagger documentation.
|
|
102
109
|
*/
|
|
103
110
|
swagger?: ElysiaSwaggerConfig<Route> | false;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The validation schema for the application environment.
|
|
114
|
+
*/
|
|
115
|
+
env?: TSchema;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The list of plugins to load.
|
|
119
|
+
*/
|
|
120
|
+
plugins?: ElysiumPlugin[];
|
|
104
121
|
};
|
|
105
122
|
|
|
106
123
|
/**
|
|
@@ -140,8 +157,35 @@ export abstract class Application extends InteractsWithConsole {
|
|
|
140
157
|
*/
|
|
141
158
|
public static register(props: AppProps = {}): ClassDecorator {
|
|
142
159
|
return function (target) {
|
|
143
|
-
|
|
144
|
-
|
|
160
|
+
// Get metadata from the prototype chain
|
|
161
|
+
let parentMetadata: AppProps | undefined;
|
|
162
|
+
let proto = Object.getPrototypeOf(target);
|
|
163
|
+
|
|
164
|
+
// Traverse up the prototype chain to find metadata
|
|
165
|
+
while (proto && proto !== Object.prototype) {
|
|
166
|
+
const metadata = Reflect.getMetadata(Symbols.app, proto);
|
|
167
|
+
if (metadata) {
|
|
168
|
+
parentMetadata = metadata;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
proto = Object.getPrototypeOf(proto);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Merge with parent metadata if found, then with current metadata
|
|
175
|
+
if (parentMetadata) {
|
|
176
|
+
props = deepMerge<AppProps>(parentMetadata, props);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Merge with existing metadata on the current class (if any)
|
|
180
|
+
const currentMetadata = Reflect.getMetadata(Symbols.app, target);
|
|
181
|
+
if (currentMetadata) {
|
|
182
|
+
props = deepMerge<AppProps>(currentMetadata, props);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Default props
|
|
186
|
+
props = deepMerge<AppProps>({ modules: [], swagger: false }, props);
|
|
187
|
+
|
|
188
|
+
// Store the merged metadata
|
|
145
189
|
Reflect.defineMetadata(Symbols.app, props, target);
|
|
146
190
|
};
|
|
147
191
|
}
|
|
@@ -152,14 +196,25 @@ export abstract class Application extends InteractsWithConsole {
|
|
|
152
196
|
public constructor() {
|
|
153
197
|
super();
|
|
154
198
|
|
|
155
|
-
|
|
199
|
+
const { env, debug }: AppProps = Reflect.getMetadata(Symbols.app, this.constructor) ?? {};
|
|
200
|
+
this.#debug = debug ?? false;
|
|
156
201
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.constructor
|
|
160
|
-
) as AppProps;
|
|
202
|
+
if (env) {
|
|
203
|
+
const rawEnv = Value.Convert(env, Value.Clone(Bun.env));
|
|
161
204
|
|
|
162
|
-
|
|
205
|
+
if (this.#debug && !Value.Check(env, rawEnv)) {
|
|
206
|
+
const error = Value.Errors(env, rawEnv).First();
|
|
207
|
+
this.trace(error! as unknown as Error, 'Invalid environment configuration');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
initEnv(Value.Clean(env, rawEnv) as Record<string, unknown>);
|
|
211
|
+
} else {
|
|
212
|
+
initEnv(Bun.env);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
Service.instance('elysium.app', this);
|
|
216
|
+
|
|
217
|
+
const { database, redis } = Reflect.getMetadata(Symbols.app, this.constructor) as AppProps;
|
|
163
218
|
|
|
164
219
|
if (redis) {
|
|
165
220
|
for (const connectionName in redis.connections) {
|
|
@@ -182,7 +237,13 @@ export abstract class Application extends InteractsWithConsole {
|
|
|
182
237
|
}
|
|
183
238
|
|
|
184
239
|
// Run the application
|
|
185
|
-
this.run()
|
|
240
|
+
this.run()
|
|
241
|
+
.then(() => Event.emit('elysium:app:launched', this, this))
|
|
242
|
+
.catch((error) => {
|
|
243
|
+
Event.emit('elysium:error', error);
|
|
244
|
+
this.trace(error, 'An error occurred while launching the application');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|
|
186
247
|
}
|
|
187
248
|
|
|
188
249
|
/**
|
|
@@ -386,27 +447,114 @@ export abstract class Application extends InteractsWithConsole {
|
|
|
386
447
|
* Starts the server on the specified port.
|
|
387
448
|
*/
|
|
388
449
|
private async commandServe(): Promise<void> {
|
|
389
|
-
const { server, modules, swagger } = Reflect.getMetadata(
|
|
450
|
+
const { server, modules, swagger, plugins } = Reflect.getMetadata(
|
|
390
451
|
Symbols.app,
|
|
391
452
|
this.constructor
|
|
392
453
|
) as AppProps;
|
|
393
454
|
|
|
455
|
+
this.#elysia = new Elysia(server).onRequest((c) => {
|
|
456
|
+
if (this.isDebug) {
|
|
457
|
+
// TODO: Use the logger service here
|
|
458
|
+
console.log(c.request.method, c.request.url);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
394
462
|
Event.emit('elysium:server:before-init', this, this);
|
|
395
463
|
|
|
396
|
-
this.#elysia
|
|
464
|
+
this.#elysia.resolve({ as: 'global' }, ({ request }) => ({
|
|
465
|
+
tenant: request.headers.get('x-tenant-id') ?? 'public'
|
|
466
|
+
}));
|
|
397
467
|
|
|
398
|
-
|
|
399
|
-
.
|
|
400
|
-
|
|
468
|
+
if (swagger) {
|
|
469
|
+
this.#elysia.use(await swaggerPlugin(swagger));
|
|
470
|
+
}
|
|
401
471
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
|
|
472
|
+
if (plugins && plugins.length > 0) {
|
|
473
|
+
this.debug('Registering plugins...');
|
|
474
|
+
for (const plugin of plugins) {
|
|
475
|
+
this.#elysia.use(await plugin(this));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.#elysia
|
|
480
|
+
.onError({ as: 'global' }, async (e) => {
|
|
408
481
|
if (await this.onError(e)) {
|
|
409
482
|
Event.emit('elysium:error', e);
|
|
483
|
+
|
|
484
|
+
let data = {};
|
|
485
|
+
|
|
486
|
+
if (typeof e.code === 'number') {
|
|
487
|
+
data = {
|
|
488
|
+
message: e.error.response
|
|
489
|
+
};
|
|
490
|
+
} else {
|
|
491
|
+
switch (e.code) {
|
|
492
|
+
case 'VALIDATION': {
|
|
493
|
+
data = {
|
|
494
|
+
scope: e.error.type,
|
|
495
|
+
code: e.error.message
|
|
496
|
+
};
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
case 'INTERNAL_SERVER_ERROR': {
|
|
500
|
+
data = {
|
|
501
|
+
message: e.error.message
|
|
502
|
+
};
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
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
|
+
return {
|
|
555
|
+
type,
|
|
556
|
+
data
|
|
557
|
+
};
|
|
410
558
|
}
|
|
411
559
|
})
|
|
412
560
|
.onStart(async (elysia) => {
|
|
@@ -419,12 +567,6 @@ export abstract class Application extends InteractsWithConsole {
|
|
|
419
567
|
this._appContextStorage.disable();
|
|
420
568
|
});
|
|
421
569
|
|
|
422
|
-
Event.emit('elysium:server:after-init', this, this);
|
|
423
|
-
|
|
424
|
-
if (swagger) {
|
|
425
|
-
this.#elysia.use(await swaggerPlugin(swagger));
|
|
426
|
-
}
|
|
427
|
-
|
|
428
570
|
const middlewares = Reflect.getMetadata(Symbols.middlewares, this.constructor) ?? [];
|
|
429
571
|
applyMiddlewares(middlewares, this.#elysia);
|
|
430
572
|
|
|
@@ -445,12 +587,13 @@ export abstract class Application extends InteractsWithConsole {
|
|
|
445
587
|
module.afterRegister();
|
|
446
588
|
}
|
|
447
589
|
|
|
590
|
+
Event.emit('elysium:server:after-init', this, this);
|
|
591
|
+
|
|
448
592
|
// Register the server
|
|
449
593
|
this.#elysia.listen(server?.port ?? (parseInt(process.env.PORT!, 10) || 3000));
|
|
450
594
|
|
|
451
595
|
process.on('SIGINT', () => {
|
|
452
|
-
this.#elysia.stop();
|
|
453
|
-
return process.exit(0);
|
|
596
|
+
this.#elysia.stop().then(() => process.exit(0));
|
|
454
597
|
});
|
|
455
598
|
}
|
|
456
599
|
|
package/src/console.ts
CHANGED
|
@@ -166,7 +166,7 @@ export class InteractsWithConsole {
|
|
|
166
166
|
this.write(this.format('Additional Information:', ConsoleFormat.BOLD));
|
|
167
167
|
Object.keys(customProps).forEach((prop) => {
|
|
168
168
|
const value = error[prop];
|
|
169
|
-
this.write(`\t${prop}: ${JSON.stringify(value)}
|
|
169
|
+
this.write(`\t${prop}: ${JSON.stringify(value)}`);
|
|
170
170
|
});
|
|
171
171
|
}
|
|
172
172
|
}
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
import type { AppEnv } from '@elysiumjs/core';
|
|
16
|
+
|
|
17
|
+
const env: Map<string, unknown> = new Map();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initializes the environment variables.
|
|
21
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
22
|
+
* @param vars The environment variables to initialize.
|
|
23
|
+
*/
|
|
24
|
+
export const initEnv = (vars: Record<string, unknown>) => {
|
|
25
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
26
|
+
env.set(key, value);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export namespace Env {
|
|
31
|
+
/**
|
|
32
|
+
* Gets the value of an environment variable.
|
|
33
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
34
|
+
* @param name The name of the environment variable.
|
|
35
|
+
* @returns The value of the environment variable.
|
|
36
|
+
*/
|
|
37
|
+
export const get = <TKey extends keyof AppEnv>(name: TKey): AppEnv[TKey] => {
|
|
38
|
+
return env.get(name) as AppEnv[TKey];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checks if an environment variable exists.
|
|
43
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
44
|
+
* @param name The name of the environment variable.
|
|
45
|
+
* @returns `true` if the environment variable exists, `false` otherwise.
|
|
46
|
+
*/
|
|
47
|
+
export const exists = <TKey extends keyof AppEnv>(name: TKey): boolean => {
|
|
48
|
+
return env.has(name);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Clears the environment variables.
|
|
53
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
54
|
+
*/
|
|
55
|
+
export const clear = () => {
|
|
56
|
+
env.clear();
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/http.ts
CHANGED
|
@@ -12,15 +12,9 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
|
-
import type {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
HTTPMethod,
|
|
19
|
-
PreContext,
|
|
20
|
-
SingletonBase,
|
|
21
|
-
TSchema
|
|
22
|
-
} from 'elysia';
|
|
23
|
-
import type { Class } from 'type-fest';
|
|
15
|
+
import type { AppHttpContext } from '@elysiumjs/core';
|
|
16
|
+
import type { Context as ElysiaContext, Handler, HTTPMethod, PreContext, TSchema } from 'elysia';
|
|
17
|
+
import type { Class, MergeDeep } from 'type-fest';
|
|
24
18
|
import type { Middleware } from './middleware';
|
|
25
19
|
import type { Module } from './module';
|
|
26
20
|
|
|
@@ -140,22 +134,31 @@ type HttpRequestHandlerRegistrationProps = Pick<
|
|
|
140
134
|
* Metadata for the HTTP context.
|
|
141
135
|
* @author Axel Nana <axel.nana@workbud.com>
|
|
142
136
|
*/
|
|
143
|
-
export type Singleton<TController = unknown, TModule extends Module = Module> =
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
137
|
+
export type Singleton<TController = unknown, TModule extends Module = Module> = MergeDeep<
|
|
138
|
+
AppHttpContext,
|
|
139
|
+
{
|
|
140
|
+
decorator: {
|
|
141
|
+
[key: string]: unknown;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The controller instance.
|
|
145
|
+
*/
|
|
146
|
+
controller: () => TController;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The module instance.
|
|
150
|
+
*/
|
|
151
|
+
module: TModule;
|
|
152
|
+
};
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
resolve: {
|
|
155
|
+
/**
|
|
156
|
+
* The tenant ID.
|
|
157
|
+
*/
|
|
158
|
+
readonly tenant: string;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
>;
|
|
159
162
|
|
|
160
163
|
/**
|
|
161
164
|
* The Elysia context with the controller injected.
|
|
@@ -166,6 +169,8 @@ export type Context<TController = unknown, TModule extends Module = Module> = El
|
|
|
166
169
|
Singleton<TController, TModule>
|
|
167
170
|
>;
|
|
168
171
|
|
|
172
|
+
export type ElysiaApp = Elysia<Route, Singleton>;
|
|
173
|
+
|
|
169
174
|
const registerHttpRequestHandler = (props: HttpRequestHandlerRegistrationProps) => {
|
|
170
175
|
const body = Reflect.getMetadata('http:body', props.target, props.propertyKey);
|
|
171
176
|
const params = Reflect.getMetadata('http:params', props.target, props.propertyKey) ?? [];
|
|
@@ -295,32 +300,30 @@ export namespace Http {
|
|
|
295
300
|
*/
|
|
296
301
|
export const controller = (props: HttpControllerProps = { path: '/' }) => {
|
|
297
302
|
return function (target: Class<any>) {
|
|
298
|
-
async function handleHttp(): Promise<
|
|
303
|
+
async function handleHttp(): Promise<ElysiaApp> {
|
|
299
304
|
// TODO: Use the logger service here
|
|
300
305
|
console.log(`Registering HTTP route for ${props.path} using ${target.name}`);
|
|
301
306
|
await nextTick();
|
|
302
307
|
|
|
303
308
|
props = assign({ path: '/', scope: HttpControllerScope.SERVER, tags: [] }, props);
|
|
304
309
|
|
|
305
|
-
const app = new Elysia({
|
|
310
|
+
const app: ElysiaApp = new Elysia({
|
|
306
311
|
prefix: props.path,
|
|
307
312
|
name: target.name
|
|
308
313
|
});
|
|
309
314
|
|
|
310
315
|
if (props.scope === HttpControllerScope.SERVER) {
|
|
311
|
-
|
|
316
|
+
const controller = Service.make(target);
|
|
317
|
+
app.decorate('controller', () => controller);
|
|
312
318
|
} else if (props.scope === HttpControllerScope.REQUEST) {
|
|
313
|
-
app.
|
|
314
|
-
controller: Service.make(target)
|
|
315
|
-
}));
|
|
319
|
+
app.decorate('controller', () => Service.make(target));
|
|
316
320
|
}
|
|
317
321
|
|
|
318
322
|
const onRequest = Reflect.getMetadata('http:onRequest', target) ?? {};
|
|
319
323
|
if (onRequest.handler) {
|
|
320
|
-
app.onRequest((c: PreContext<
|
|
321
|
-
const controller = c.controller;
|
|
322
|
-
|
|
323
|
-
return handler(c);
|
|
324
|
+
app.onRequest((c: PreContext<Singleton>) => {
|
|
325
|
+
const controller = c.controller();
|
|
326
|
+
return onRequest.handler.call(controller, c);
|
|
324
327
|
});
|
|
325
328
|
}
|
|
326
329
|
|
|
@@ -375,20 +378,18 @@ export namespace Http {
|
|
|
375
378
|
|
|
376
379
|
const getHandler = () => {
|
|
377
380
|
const initTransactedHandler = (c: Context) => {
|
|
378
|
-
const controller = c.controller;
|
|
379
|
-
|
|
380
381
|
if (isTransactional) {
|
|
381
382
|
return function (...args: unknown[]) {
|
|
382
383
|
return Database.getDefaultConnection().transaction((tx) => {
|
|
383
384
|
return Application.context.run(
|
|
384
385
|
new Map([
|
|
385
|
-
['tenant', c.tenant],
|
|
386
|
+
['tenant', c.tenant as unknown],
|
|
386
387
|
['http:context', c],
|
|
387
388
|
['db:tx', tx]
|
|
388
389
|
]),
|
|
389
390
|
() => {
|
|
390
391
|
try {
|
|
391
|
-
return route.handler.call(controller, ...args) as Promise<unknown>;
|
|
392
|
+
return route.handler.call(c.controller(), ...args) as Promise<unknown>;
|
|
392
393
|
} catch (e) {
|
|
393
394
|
tx.rollback();
|
|
394
395
|
throw e;
|
|
@@ -402,10 +403,10 @@ export namespace Http {
|
|
|
402
403
|
return function (...args: unknown[]) {
|
|
403
404
|
return Application.context.run(
|
|
404
405
|
new Map([
|
|
405
|
-
['tenant', c.tenant],
|
|
406
|
+
['tenant', c.tenant as unknown],
|
|
406
407
|
['http:context', c]
|
|
407
408
|
]),
|
|
408
|
-
() => route.handler.call(controller, ...args) as Promise<unknown>
|
|
409
|
+
() => route.handler.call(c.controller(), ...args) as Promise<unknown>
|
|
409
410
|
);
|
|
410
411
|
};
|
|
411
412
|
};
|
|
@@ -455,13 +456,13 @@ export namespace Http {
|
|
|
455
456
|
app.route(route.method, route.path, getHandler(), {
|
|
456
457
|
// @ts-ignore
|
|
457
458
|
config: {},
|
|
458
|
-
beforeHandle(c
|
|
459
|
+
beforeHandle(c) {
|
|
459
460
|
return executeMiddlewareChain(mi, c, 'onBeforeHandle');
|
|
460
461
|
},
|
|
461
|
-
afterHandle(c
|
|
462
|
+
afterHandle(c) {
|
|
462
463
|
return executeMiddlewareChain(mi, c, 'onAfterHandle');
|
|
463
464
|
},
|
|
464
|
-
afterResponse(c
|
|
465
|
+
afterResponse(c) {
|
|
465
466
|
return executeMiddlewareChain(mi, c, 'onAfterResponse');
|
|
466
467
|
},
|
|
467
468
|
detail: {
|
package/src/middleware.ts
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
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 { AnyElysia } from 'elysia';
|
|
16
16
|
import type { Class } from 'type-fest';
|
|
17
|
-
import type { Context
|
|
17
|
+
import type { Context } from './http';
|
|
18
18
|
|
|
19
19
|
import { Application } from './app';
|
|
20
20
|
import { Service } from './service';
|
|
@@ -27,9 +27,9 @@ import { Symbols } from './utils';
|
|
|
27
27
|
* @param context The context to pass to each middleware.
|
|
28
28
|
* @param method The method to call on each middleware.
|
|
29
29
|
*/
|
|
30
|
-
export const executeMiddlewareChain =
|
|
30
|
+
export const executeMiddlewareChain = (
|
|
31
31
|
middlewares: Middleware[],
|
|
32
|
-
context:
|
|
32
|
+
context: any,
|
|
33
33
|
method: keyof Middleware
|
|
34
34
|
): Promise<any> => {
|
|
35
35
|
return Application.context.run(
|
|
@@ -58,7 +58,7 @@ export const executeMiddlewareChain = <T extends Context>(
|
|
|
58
58
|
* @param middlewares The list of middlewares to apply.
|
|
59
59
|
* @param plugin The Elysia plugin instance.
|
|
60
60
|
*/
|
|
61
|
-
export const applyMiddlewares = (middlewares: Class<Middleware>[], plugin:
|
|
61
|
+
export const applyMiddlewares = (middlewares: Class<Middleware>[], plugin: AnyElysia) => {
|
|
62
62
|
const mi = middlewares.map((middleware) => Service.make<Middleware>(middleware));
|
|
63
63
|
|
|
64
64
|
plugin.onBeforeHandle((c) => {
|
package/src/model.ts
CHANGED
|
@@ -119,6 +119,12 @@ export type ModelClass<
|
|
|
119
119
|
*/
|
|
120
120
|
readonly $inferUpdate: Partial<TTable['$inferSelect']>;
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* The drizzle's table schema wrapped by this model.
|
|
124
|
+
* Will be automatically wrapped by the `Tenancy.withTenant()` function when the current tenant is not `public`.
|
|
125
|
+
*/
|
|
126
|
+
readonly table: TTable;
|
|
127
|
+
|
|
122
128
|
/**
|
|
123
129
|
* The name of the table wrapped by the model.
|
|
124
130
|
*/
|
|
@@ -189,6 +195,24 @@ export const Model = <TColumnsMap extends Record<string, PgColumnBuilderBase>>(
|
|
|
189
195
|
*/
|
|
190
196
|
public static readonly $inferUpdate: Partial<typeof table.$inferInsert> = table.$inferInsert;
|
|
191
197
|
|
|
198
|
+
/**
|
|
199
|
+
* The drizzle's table schema wrapped by this model.
|
|
200
|
+
*/
|
|
201
|
+
public static get table(): ReturnType<typeof pgTable<string, TColumnsMap>> {
|
|
202
|
+
// If we are not inside an Application context, we can't use the tenancy system
|
|
203
|
+
if (!Application.instance || !Application.context.getStore) {
|
|
204
|
+
return table;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (this.supportTenancy) {
|
|
208
|
+
const tenant = Tenancy.getCurrentTenant() ?? 'public';
|
|
209
|
+
// @ts-expect-error typeof this strangely doesn't match the ModelClass type
|
|
210
|
+
return Tenancy.withTenant(tenant, this);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return table;
|
|
214
|
+
}
|
|
215
|
+
|
|
192
216
|
/**
|
|
193
217
|
* The name of the table wrapped by the model.
|
|
194
218
|
*/
|
|
@@ -278,21 +302,21 @@ export namespace Tenancy {
|
|
|
278
302
|
>(
|
|
279
303
|
tenant: string,
|
|
280
304
|
model: T
|
|
281
|
-
) => {
|
|
305
|
+
): ReturnType<typeof pgTable<string, TColumnsMap>> => {
|
|
282
306
|
if (tenant === 'public') {
|
|
283
307
|
return pgTable(model.tableName, model.columns);
|
|
284
308
|
}
|
|
285
309
|
|
|
286
310
|
const tableName = `${tenant}.${model.tableName}`;
|
|
287
311
|
if (tenantSchemas.has(tableName)) {
|
|
288
|
-
return tenantSchemas.get(tableName)
|
|
312
|
+
return tenantSchemas.get(tableName)! as ReturnType<typeof pgTable<string, TColumnsMap>>;
|
|
289
313
|
}
|
|
290
314
|
|
|
291
315
|
const tenantSchema = getTenantSchema(tenant);
|
|
292
316
|
const schemaTable = tenantSchema.table(model.tableName, model.columns);
|
|
293
317
|
tenantSchemas.set(tableName, schemaTable);
|
|
294
318
|
|
|
295
|
-
return schemaTable
|
|
319
|
+
return schemaTable as unknown as ReturnType<typeof pgTable<string, TColumnsMap>>;
|
|
296
320
|
};
|
|
297
321
|
|
|
298
322
|
/**
|