@elysiumjs/core 0.4.3 → 0.5.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/src/app.ts DELETED
@@ -1,647 +0,0 @@
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 { ElysiaSwaggerConfig } from '@elysiajs/swagger';
16
- import type { AnyElysia, ElysiaConfig, ErrorContext, TSchema } from 'elysia';
17
- import type { CommandClass } from './command';
18
- import type { DatabaseConnectionProps } from './database';
19
- import type { Route } from './http';
20
- import type { ModuleClass } from './module';
21
- import type { RedisConnectionProps } from './redis';
22
- import type { WampClientProps } from './wamp';
23
-
24
- import { AsyncLocalStorage } from 'node:async_hooks';
25
- import { parseArgs } from 'node:util';
26
-
27
- import { swagger as swaggerPlugin } from '@elysiajs/swagger';
28
- import { Value } from '@sinclair/typebox/value';
29
- import { Elysia } from 'elysia';
30
-
31
- import { ConsoleFormat, InteractsWithConsole } from './console';
32
- import { Database } from './database';
33
- import { initEnv } from './env';
34
- import { Event } from './event';
35
- import { applyMiddlewares } from './middleware';
36
- import { Redis } from './redis';
37
- import { Service } from './service';
38
- import { deepMerge, Symbols } from './utils';
39
- import { Wamp } from './wamp';
40
-
41
- /**
42
- * An application plugin, consisting of a function that returns an Elysia instance.
43
- * @author Axel Nana <axel.nana@workbud.com>
44
- */
45
- export type ElysiumPlugin = (app: Application) => Promise<AnyElysia>;
46
-
47
- /**
48
- * Properties required when declaring an app using the `@app()` decorator.
49
- * @author Axel Nana <axel.nana@workbud.com>
50
- */
51
- export type AppProps = {
52
- // Allows to pass custom options to the application properties.
53
- [key: string]: any;
54
-
55
- /**
56
- * Enables or disables debug mode.
57
- */
58
- debug?: boolean;
59
-
60
- /**
61
- * The Elysia server configuration.
62
- */
63
- server?: ElysiaConfig<Route> & {
64
- /**
65
- * The port to listen on.
66
- */
67
- port?: number;
68
- };
69
-
70
- /**
71
- * The wamp client configuration for the app.
72
- */
73
- wamp?: {
74
- /**
75
- * The default connection name.
76
- */
77
- default: string;
78
-
79
- /**
80
- * The list of connections.
81
- */
82
- connections: Record<string, WampClientProps>;
83
- };
84
-
85
- /**
86
- * The list of modules provided the app.
87
- */
88
- modules?: ModuleClass[];
89
-
90
- /**
91
- * The list of CLI commands provided the app.
92
- */
93
- commands?: CommandClass[];
94
-
95
- /**
96
- * The database configuration for the app.
97
- */
98
- database?: {
99
- /**
100
- * The default connection name.
101
- */
102
- default: string;
103
-
104
- /**
105
- * The list of connections.
106
- */
107
- connections: Record<string, DatabaseConnectionProps>;
108
- };
109
-
110
- /**
111
- * The redis configuration for the app.
112
- */
113
- redis?: {
114
- /**
115
- * The default connection name.
116
- */
117
- default: string;
118
-
119
- /**
120
- * The list of connections.
121
- */
122
- connections: Record<string, RedisConnectionProps>;
123
- };
124
-
125
- /**
126
- * Swagger documentation configuration.
127
- *
128
- * Set it to `false` to disable Swagger documentation.
129
- */
130
- swagger?: ElysiaSwaggerConfig<Route> | false;
131
-
132
- /**
133
- * The validation schema for the application environment.
134
- */
135
- env?: TSchema;
136
-
137
- /**
138
- * The list of plugins to load.
139
- */
140
- plugins?: ElysiumPlugin[];
141
- };
142
-
143
- /**
144
- * Type for the application context.
145
- * @author Axel Nana <axel.nana@workbud.com>
146
- */
147
- export type AppContext = AsyncLocalStorage<Map<'tenant' | 'http:context' | 'db:tx', unknown>>;
148
-
149
- /**
150
- * Base class for the application main entry.
151
- * @author Axel Nana <axel.nana@workbud.com>
152
- */
153
- export abstract class Application extends InteractsWithConsole {
154
- // @ts-expect-error The property is not initialized in the constructor.
155
- #elysia: Elysia<Route>;
156
- #debug: boolean;
157
-
158
- private readonly _appContextStorage: AppContext = new AsyncLocalStorage();
159
-
160
- /**
161
- * Gets the running application instance.
162
- */
163
- public static get instance(): Application {
164
- return Service.get<Application>('elysium.app')!;
165
- }
166
-
167
- /**
168
- * Gets the application context shared between asynchronous operations.
169
- */
170
- public static get context(): AppContext {
171
- return Application.instance._appContextStorage;
172
- }
173
-
174
- /**
175
- * Marks a class as the application main entry.
176
- * @param props The decorator options.
177
- */
178
- public static register(props: AppProps = {}): ClassDecorator {
179
- return function (target) {
180
- // Get metadata from the prototype chain
181
- let parentMetadata: AppProps | undefined;
182
- let proto = Object.getPrototypeOf(target);
183
-
184
- // Traverse up the prototype chain to find metadata
185
- while (proto && proto !== Object.prototype) {
186
- const metadata = Reflect.getMetadata(Symbols.app, proto);
187
- if (metadata) {
188
- parentMetadata = metadata;
189
- break;
190
- }
191
- proto = Object.getPrototypeOf(proto);
192
- }
193
-
194
- // Merge with parent metadata if found, then with current metadata
195
- if (parentMetadata) {
196
- props = deepMerge<AppProps>(parentMetadata, props);
197
- }
198
-
199
- // Merge with existing metadata on the current class (if any)
200
- const currentMetadata = Reflect.getMetadata(Symbols.app, target);
201
- if (currentMetadata) {
202
- props = deepMerge<AppProps>(currentMetadata, props);
203
- }
204
-
205
- // Default props
206
- props = deepMerge<AppProps>({ modules: [], swagger: false }, props);
207
-
208
- // Store the merged metadata
209
- Reflect.defineMetadata(Symbols.app, props, target);
210
- };
211
- }
212
-
213
- /**
214
- * Creates a new instance of the application.
215
- */
216
- public constructor() {
217
- super();
218
-
219
- const { env, debug }: AppProps = Reflect.getMetadata(Symbols.app, this.constructor) ?? {};
220
- this.#debug = debug ?? false;
221
-
222
- if (env) {
223
- const rawEnv = Value.Convert(env, Value.Clone(Bun.env));
224
-
225
- if (this.#debug && !Value.Check(env, rawEnv)) {
226
- const error = Value.Errors(env, rawEnv).First();
227
- this.trace(error! as unknown as Error, 'Invalid environment configuration');
228
- }
229
-
230
- initEnv(Value.Clean(env, rawEnv) as Record<string, unknown>);
231
- } else {
232
- initEnv(Bun.env);
233
- }
234
-
235
- Service.instance('elysium.app', this);
236
-
237
- const { database, redis, wamp } = Reflect.getMetadata(
238
- Symbols.app,
239
- this.constructor
240
- ) as AppProps;
241
-
242
- if (redis) {
243
- for (const connectionName in redis.connections) {
244
- Redis.registerConnection(connectionName, redis.connections[connectionName]);
245
- }
246
-
247
- if (Redis.connectionExists(redis.default)) {
248
- Redis.setDefaultConnection(redis.default);
249
- }
250
- }
251
-
252
- if (database) {
253
- for (const connectionName in database.connections) {
254
- Database.registerConnection(connectionName, database.connections[connectionName]);
255
- }
256
-
257
- if (Database.connectionExists(database.default)) {
258
- Database.setDefaultConnection(database.default);
259
- }
260
- }
261
-
262
- if (wamp) {
263
- for (const connectionName in wamp.connections) {
264
- Wamp.registerConnection(connectionName, wamp.connections[connectionName]);
265
- }
266
-
267
- if (Wamp.connectionExists(wamp.default)) {
268
- Wamp.setDefaultConnection(wamp.default);
269
- }
270
- }
271
-
272
- // Run the application
273
- this.run()
274
- .then(() => Event.emit('elysium:app:launched', this, this))
275
- .catch((error) => {
276
- Event.emit('elysium:error', error);
277
- this.trace(error, 'An error occurred while launching the application');
278
- process.exit(1);
279
- });
280
- }
281
-
282
- /**
283
- * Gets the Elysia instance.
284
- */
285
- public get elysia(): Elysia<Route> {
286
- return this.#elysia;
287
- }
288
-
289
- /**
290
- * Gets the debug mode flag.
291
- */
292
- public get isDebug(): boolean {
293
- return this.#debug;
294
- }
295
-
296
- /**
297
- * Sets the debug mode flag.
298
- * @param debug The debug mode flag.
299
- */
300
- public set isDebug(debug: boolean) {
301
- this.#debug = debug;
302
- }
303
-
304
- /**
305
- * Hook that is executed when an error occurs.
306
- * @param e The error context.
307
- * @returns Whether to continue the error propagation.
308
- */
309
- protected async onError(e: ErrorContext): Promise<boolean> {
310
- return true;
311
- }
312
-
313
- /**
314
- * Hook that is executed when the application starts.
315
- * @param elysia The Elysia instance.
316
- */
317
- protected async onStart(elysia: Elysia<Route>): Promise<void> {}
318
-
319
- /**
320
- * Hook that is executed when the application stops.
321
- * @param elysia The Elysia instance.
322
- */
323
- protected async onStop(elysia: Elysia<Route>): Promise<void> {}
324
-
325
- protected async run(): Promise<void> {
326
- const argv = process.argv.slice(2);
327
-
328
- const action = argv[0];
329
-
330
- if (!action) {
331
- this.commandDescribe();
332
- } else {
333
- const { commands } = Reflect.getMetadata(Symbols.app, this.constructor) as AppProps;
334
-
335
- switch (action) {
336
- case 'serve': {
337
- return this.commandServe();
338
- }
339
- case 'exec': {
340
- const command = argv[1];
341
- const args = argv.slice(2);
342
-
343
- return this.commandExec(command, args);
344
- }
345
- case 'help': {
346
- const command = argv[1];
347
-
348
- if (command) {
349
- const commandClass = commands?.find((commandClass) => commandClass.command === command);
350
-
351
- if (commandClass) {
352
- const commandInstance = Service.make(commandClass);
353
- this.write(await commandInstance.help());
354
- return process.exit(0);
355
- }
356
-
357
- console.error(`Command <${command}> not found.`);
358
- } else {
359
- console.error('No command provided. Usage: styx help <command>');
360
- }
361
-
362
- this.commandList();
363
- break;
364
- }
365
- case 'list': {
366
- this.write(this.bold('Available commands:'));
367
- this.commandList();
368
- break;
369
- }
370
- case 'work': {
371
- return this.commandWork(argv.slice(1));
372
- }
373
- default: {
374
- console.error(`Invalid command: ${this.bold(action)}`);
375
-
376
- this.commandDescribe();
377
- break;
378
- }
379
- }
380
- }
381
-
382
- return process.exit(0);
383
- }
384
-
385
- /**
386
- * Retrieves the configuration value for the given key.
387
- * @param key The key to retrieve the configuration value for.
388
- * @returns The configuration value for the given key, or `null` if not found.
389
- */
390
- public getConfig<T>(key: keyof AppProps): T | null {
391
- return Reflect.getMetadata(Symbols.app, this.constructor)?.[key] ?? null;
392
- }
393
-
394
- private async commandWork(argv: string[]) {
395
- this.info('Starting worker process...');
396
-
397
- // Parse queue arguments
398
- const { values } = parseArgs({
399
- args: argv,
400
- options: {
401
- queue: {
402
- type: 'string',
403
- multiple: true,
404
- default: ['default'],
405
- short: 'q'
406
- },
407
- concurrency: {
408
- type: 'string',
409
- default: '1',
410
- short: 'c'
411
- },
412
- ['max-retries']: {
413
- type: 'string',
414
- default: '5',
415
- short: 'r'
416
- },
417
- ['retry-delay']: {
418
- type: 'string',
419
- default: '5000',
420
- short: 'd'
421
- },
422
- ['pause-on-error']: {
423
- type: 'boolean',
424
- default: false,
425
- short: 'p'
426
- }
427
- }
428
- });
429
-
430
- // Import worker-specific code
431
- const { Worker } = await import('./worker');
432
-
433
- // Start the worker with the specified queues
434
- const queues = values.queue
435
- .map((queue) => queue.split(','))
436
- .reduce((acc, queues) => acc.concat(queues), []);
437
-
438
- const worker = Worker.spawn(self as unknown as globalThis.Worker, queues, {
439
- concurrency: parseInt(values.concurrency, 10),
440
- maxRetries: parseInt(values['max-retries'], 10),
441
- retryDelay: parseInt(values['retry-delay'], 10),
442
- pauseOnError: values['pause-on-error']
443
- });
444
-
445
- this.success(
446
- `Worker process ${this.format(worker.id, ConsoleFormat.GREEN)} started with queues: ${queues.map((q) => this.format(q, ConsoleFormat.CYAN)).join(', ')}`
447
- );
448
- }
449
-
450
- /**
451
- * Executes a command.
452
- * @param command The command to execute.
453
- * @param argv The command line arguments.
454
- */
455
- private async commandExec(command: string, argv: string[]): Promise<void> {
456
- const { commands } = Reflect.getMetadata(Symbols.app, this.constructor) as AppProps;
457
-
458
- // Find the command class
459
- const commandClass = commands?.find((commandClass) => commandClass.command === command);
460
-
461
- if (!commandClass) {
462
- console.error(`Command ${command} not found`);
463
- this.commandList();
464
- return process.exit(1);
465
- }
466
-
467
- try {
468
- // Create the command instance
469
- const commandInstance = Service.make(commandClass);
470
-
471
- // Run the command
472
- const initialized = await commandInstance.init(...argv);
473
-
474
- if (initialized) {
475
- await commandInstance.run();
476
- } else {
477
- const help = await commandInstance.help();
478
- this.write(help);
479
- }
480
-
481
- return process.exit(0);
482
- } catch (error: any) {
483
- console.error(error.message);
484
- return process.exit(1);
485
- }
486
- }
487
-
488
- /**
489
- * Starts the server on the specified port.
490
- */
491
- private async commandServe(): Promise<void> {
492
- const { server, modules, swagger, plugins } = Reflect.getMetadata(
493
- Symbols.app,
494
- this.constructor
495
- ) as AppProps;
496
-
497
- this.#elysia = new Elysia(server).onRequest((c) => {
498
- if (this.isDebug) {
499
- // TODO: Use the logger service here
500
- console.log(c.request.method, c.request.url);
501
- }
502
- });
503
-
504
- Event.emit('elysium:server:before-init', this, this);
505
-
506
- this.#elysia.resolve({ as: 'global' }, ({ request }) => ({
507
- tenant: request.headers.get('x-tenant-id') ?? 'public'
508
- }));
509
-
510
- if (swagger) {
511
- this.#elysia.use(await swaggerPlugin(swagger));
512
- }
513
-
514
- if (plugins && plugins.length > 0) {
515
- this.debug('Registering plugins...');
516
- for (const plugin of plugins) {
517
- this.#elysia.use(await plugin(this));
518
- }
519
- }
520
-
521
- this.#elysia
522
- .onError({ as: 'global' }, async (e) => {
523
- if (await this.onError(e)) {
524
- Event.emit('elysium:error', e);
525
- this.trace({
526
- name: 'HttpError',
527
- message: 'Unexpected error while handling HTTP request',
528
- // @ts-expect-error The stack property may not exist in the error.
529
- stack: e.error.stack,
530
- ...e.error
531
- });
532
-
533
- let data = {};
534
-
535
- if (typeof e.code === 'number') {
536
- return e.error.response;
537
- } else {
538
- switch (e.code) {
539
- case 'VALIDATION': {
540
- data = {
541
- scope: e.error.type,
542
- code: e.error.message
543
- };
544
- break;
545
- }
546
- case 'INTERNAL_SERVER_ERROR': {
547
- data = {
548
- message: e.error.message
549
- };
550
- break;
551
- }
552
- }
553
- }
554
-
555
- return {
556
- type: e.code,
557
- data
558
- };
559
- }
560
- })
561
- .onStart(async (elysia) => {
562
- await this.onStart(elysia);
563
- Event.emit('elysium:server:start', elysia, this);
564
- })
565
- .onStop(async (elysia) => {
566
- await this.onStop(elysia);
567
- Event.emit('elysium:server:stop', elysia, this);
568
- this._appContextStorage.disable();
569
- });
570
-
571
- const middlewares = Reflect.getMetadata(Symbols.middlewares, this.constructor) ?? [];
572
- applyMiddlewares(middlewares, this.#elysia);
573
-
574
- for (const moduleClass of modules!) {
575
- const plugin = Reflect.getMetadata(Symbols.elysiaPlugin, moduleClass);
576
- if (plugin === undefined) {
577
- // TODO: Use the logger service here
578
- console.error(
579
- `Invalid module class ${moduleClass.name} registered in app ${this.constructor.name}. Ensure that you used the @module() decorator on the module.`
580
- );
581
- return process.exit(1);
582
- }
583
-
584
- const module = Service.bind(moduleClass);
585
-
586
- await module.beforeRegister();
587
- this.#elysia.use(await plugin(module));
588
- await module.afterRegister();
589
- }
590
-
591
- Event.emit('elysium:server:after-init', this, this);
592
-
593
- // Register the server
594
- this.#elysia.listen(server?.port ?? (parseInt(process.env.PORT!, 10) || 3000));
595
-
596
- process.on('SIGINT', () => {
597
- this.#elysia.stop().then(() => process.exit(0));
598
- });
599
- }
600
-
601
- /**
602
- * Describes the CLI and available commands.
603
- */
604
- private commandDescribe() {
605
- this.section(
606
- `${this.bold('Usage:')} ${this.format('styx', ConsoleFormat.MAGENTA)} <command> [options]`
607
- );
608
-
609
- this.section(this.bold('Commands:'));
610
- this.commandDescription('serve', '', 'Starts the server.');
611
- this.commandDescription('exec', '<command> [options]', 'Executes a command.');
612
- this.commandDescription('work', '[options]', 'Starts a worker process.');
613
- this.commandDescription('help', '<command>', 'Displays help for a command.');
614
- this.commandDescription('list', '', 'List all available commands.');
615
- }
616
-
617
- private commandList() {
618
- const { commands } = Reflect.getMetadata(Symbols.app, this.constructor) as AppProps;
619
-
620
- const groups = (commands ?? []).reduce((acc, commandClass) => {
621
- const groupKey = commandClass.command.split(':')[0];
622
- const group = acc.get(groupKey) ?? [];
623
- group.push(commandClass);
624
- acc.set(groupKey, group);
625
- return acc;
626
- }, new Map<string, CommandClass[]>());
627
-
628
- for (const [group, groupCommands] of groups) {
629
- this.section(this.format(group, ConsoleFormat.BLUE));
630
- for (const commandClass of groupCommands) {
631
- this.commandDescription(commandClass.command, '', commandClass.description);
632
- }
633
- }
634
- }
635
-
636
- private commandDescription(
637
- command: string,
638
- args: string,
639
- description: string,
640
- width: number = InteractsWithConsole.SPACE_WIDTH
641
- ) {
642
- args = args.length === 0 ? ' ' : ` ${args} `;
643
- this.write(
644
- ` ${this.format(command, ConsoleFormat.CYAN)}${args}${'∙'.repeat(width - command.length - args.length)} ${description}`
645
- );
646
- }
647
- }