@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 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
- export { type AppContext, Application } from './src/app';
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.1.1",
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.42.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.0",
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, PreContext } from 'elysia';
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, Singleton } from './http';
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
- props = assign<AppProps>({ modules: [], database: undefined, swagger: false }, props);
144
- props = assign<AppProps>(Reflect.getMetadata(Symbols.app, target) ?? {}, props);
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
- Service.instance('elysium.app', this);
199
+ const { env, debug }: AppProps = Reflect.getMetadata(Symbols.app, this.constructor) ?? {};
200
+ this.#debug = debug ?? false;
156
201
 
157
- const { debug, database, redis } = Reflect.getMetadata(
158
- Symbols.app,
159
- this.constructor
160
- ) as AppProps;
202
+ if (env) {
203
+ const rawEnv = Value.Convert(env, Value.Clone(Bun.env));
161
204
 
162
- this.#debug = debug ?? false;
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().then(() => Event.emit('elysium:app:launched', this, this));
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 = new Elysia(server);
464
+ this.#elysia.resolve({ as: 'global' }, ({ request }) => ({
465
+ tenant: request.headers.get('x-tenant-id') ?? 'public'
466
+ }));
397
467
 
398
- this.#elysia
399
- .onRequest((c: PreContext<Singleton>) => {
400
- c.tenant = c.request.headers.get('x-tenant-id') ?? 'public';
468
+ if (swagger) {
469
+ this.#elysia.use(await swaggerPlugin(swagger));
470
+ }
401
471
 
402
- if (this.isDebug) {
403
- // TODO: Use the logger service here
404
- console.log(c.request.method, c.request.url);
405
- }
406
- })
407
- .onError(async (e) => {
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)}`, false);
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
- Context as ElysiaContext,
17
- Handler,
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> = SingletonBase & {
144
- /**
145
- * The controller instance.
146
- */
147
- controller: TController;
148
-
149
- /**
150
- * The module instance.
151
- */
152
- module: TModule;
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
- * The tenant ID.
156
- */
157
- tenant: string;
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<Elysia<Route>> {
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
- app.decorate('controller', Service.make(target));
316
+ const controller = Service.make(target);
317
+ app.decorate('controller', () => controller);
312
318
  } else if (props.scope === HttpControllerScope.REQUEST) {
313
- app.resolve({ as: 'scoped' }, () => ({
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<SingletonBase & { controller: any }>) => {
321
- const controller = c.controller;
322
- const handler = onRequest.handler.bind(controller);
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: Context) {
459
+ beforeHandle(c) {
459
460
  return executeMiddlewareChain(mi, c, 'onBeforeHandle');
460
461
  },
461
- afterHandle(c: Context) {
462
+ afterHandle(c) {
462
463
  return executeMiddlewareChain(mi, c, 'onAfterHandle');
463
464
  },
464
- afterResponse(c: Context) {
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 { Elysia } from 'elysia';
15
+ import type { AnyElysia } from 'elysia';
16
16
  import type { Class } from 'type-fest';
17
- import type { Context, Route } from './http';
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 = <T extends Context>(
30
+ export const executeMiddlewareChain = (
31
31
  middlewares: Middleware[],
32
- context: T,
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: Elysia<Route>) => {
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
  /**