next-js-backend 1.0.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.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +402 -0
  3. package/dist/docs/build.d.ts +2 -0
  4. package/dist/docs/dist/chunk-8whee738.d.ts +1 -0
  5. package/dist/docs/src/App.d.ts +2 -0
  6. package/dist/docs/src/components/MarkdownRenderer.d.ts +5 -0
  7. package/dist/docs/src/components/layout/DocsLayout.d.ts +1 -0
  8. package/dist/docs/src/components/ui/button.d.ts +10 -0
  9. package/dist/docs/src/components/ui/card.d.ts +9 -0
  10. package/dist/docs/src/components/ui/input.d.ts +3 -0
  11. package/dist/docs/src/components/ui/label.d.ts +4 -0
  12. package/dist/docs/src/components/ui/select.d.ts +15 -0
  13. package/dist/docs/src/components/ui/textarea.d.ts +3 -0
  14. package/dist/docs/src/frontend.d.ts +7 -0
  15. package/dist/docs/src/index.d.ts +1 -0
  16. package/dist/docs/src/lib/utils.d.ts +2 -0
  17. package/dist/docs/src/pages/DocsPages.d.ts +14 -0
  18. package/dist/docs/src/pages/index.d.ts +1 -0
  19. package/dist/docs/src/test/test-build.d.ts +1 -0
  20. package/dist/docs/src/test/test-mdx.d.ts +1 -0
  21. package/dist/docs/test-build.d.ts +1 -0
  22. package/dist/index.d.ts +25 -0
  23. package/dist/index.js +61316 -0
  24. package/dist/samples/basic-crud/app.module.d.ts +2 -0
  25. package/dist/samples/basic-crud/index.d.ts +1 -0
  26. package/dist/samples/basic-crud/users.controller.d.ts +23 -0
  27. package/dist/samples/basic-crud/users.service.d.ts +18 -0
  28. package/dist/samples/class-validator-pipe/admins.controller.d.ts +12 -0
  29. package/dist/samples/class-validator-pipe/app.module.d.ts +2 -0
  30. package/dist/samples/class-validator-pipe/dto/create-admin.dto.d.ts +5 -0
  31. package/dist/samples/class-validator-pipe/index.d.ts +1 -0
  32. package/dist/samples/guards-interceptors/app.module.d.ts +2 -0
  33. package/dist/samples/guards-interceptors/auth.guard.d.ts +5 -0
  34. package/dist/samples/guards-interceptors/dashboard.controller.d.ts +11 -0
  35. package/dist/samples/guards-interceptors/index.d.ts +1 -0
  36. package/dist/samples/guards-interceptors/logging.interceptor.d.ts +5 -0
  37. package/dist/samples/swagger-openapi/app.module.d.ts +2 -0
  38. package/dist/samples/swagger-openapi/books.controller.d.ts +23 -0
  39. package/dist/samples/swagger-openapi/books.service.d.ts +18 -0
  40. package/dist/samples/swagger-openapi/index.d.ts +1 -0
  41. package/dist/src/auth/__tests__/password.service.test.d.ts +1 -0
  42. package/dist/src/auth/auth.guard.d.ts +8 -0
  43. package/dist/src/auth/index.d.ts +4 -0
  44. package/dist/src/auth/jwt.module.d.ts +10 -0
  45. package/dist/src/auth/jwt.service.d.ts +25 -0
  46. package/dist/src/auth/password.service.d.ts +32 -0
  47. package/dist/src/config/config.module.d.ts +10 -0
  48. package/dist/src/config/config.service.d.ts +25 -0
  49. package/dist/src/config/index.d.ts +2 -0
  50. package/dist/src/constants.d.ts +16 -0
  51. package/dist/src/decorators/catch.decorator.d.ts +10 -0
  52. package/dist/src/decorators/controller.decorator.d.ts +2 -0
  53. package/dist/src/decorators/filter.decorator.d.ts +7 -0
  54. package/dist/src/decorators/guard.decorator.d.ts +3 -0
  55. package/dist/src/decorators/interceptor.decorator.d.ts +3 -0
  56. package/dist/src/decorators/method.decorator.d.ts +19 -0
  57. package/dist/src/decorators/module.decorator.d.ts +16 -0
  58. package/dist/src/decorators/param.decorator.d.ts +37 -0
  59. package/dist/src/decorators/pipe.decorator.d.ts +3 -0
  60. package/dist/src/decorators/schema.decorator.d.ts +10 -0
  61. package/dist/src/di/__tests__/container.test.d.ts +1 -0
  62. package/dist/src/di/container.d.ts +23 -0
  63. package/dist/src/di/inject.decorator.d.ts +7 -0
  64. package/dist/src/di/injectable.decorator.d.ts +2 -0
  65. package/dist/src/di/provider.d.ts +20 -0
  66. package/dist/src/exceptions/http.exception.d.ts +13 -0
  67. package/dist/src/exceptions/index.d.ts +17 -0
  68. package/dist/src/exceptions/validation.pipe.d.ts +15 -0
  69. package/dist/src/factory/__tests__/exception-filters.test.d.ts +1 -0
  70. package/dist/src/factory/__tests__/file-upload.test.d.ts +1 -0
  71. package/dist/src/factory/elysia-factory.d.ts +26 -0
  72. package/dist/src/interfaces.d.ts +26 -0
  73. package/dist/src/services/logger.service.d.ts +39 -0
  74. package/dist/src/session/__tests__/session.module.test.d.ts +1 -0
  75. package/dist/src/session/session.module.d.ts +7 -0
  76. package/dist/src/session/session.options.d.ts +35 -0
  77. package/dist/src/session/session.service.d.ts +20 -0
  78. package/dist/src/session/session.store.d.ts +39 -0
  79. package/dist/test.d.ts +1 -0
  80. package/dist/testing/e2e/auth.test.d.ts +1 -0
  81. package/dist/testing/e2e/config.test.d.ts +1 -0
  82. package/dist/testing/e2e/di.test.d.ts +1 -0
  83. package/dist/testing/e2e/guards-interceptors.test.d.ts +1 -0
  84. package/dist/testing/e2e/routing.test.d.ts +1 -0
  85. package/dist/testing/e2e/validation.test.d.ts +1 -0
  86. package/index.ts +24 -0
  87. package/package.json +61 -0
  88. package/src/auth/__tests__/password.service.test.ts +38 -0
  89. package/src/auth/auth.guard.ts +34 -0
  90. package/src/auth/index.ts +4 -0
  91. package/src/auth/jwt.module.ts +24 -0
  92. package/src/auth/jwt.service.ts +65 -0
  93. package/src/auth/password.service.ts +48 -0
  94. package/src/config/config.module.ts +24 -0
  95. package/src/config/config.service.ts +78 -0
  96. package/src/config/index.ts +2 -0
  97. package/src/constants.ts +16 -0
  98. package/src/decorators/catch.decorator.ts +16 -0
  99. package/src/decorators/controller.decorator.ts +14 -0
  100. package/src/decorators/filter.decorator.ts +22 -0
  101. package/src/decorators/guard.decorator.ts +19 -0
  102. package/src/decorators/interceptor.decorator.ts +19 -0
  103. package/src/decorators/method.decorator.ts +37 -0
  104. package/src/decorators/module.decorator.ts +28 -0
  105. package/src/decorators/param.decorator.ts +80 -0
  106. package/src/decorators/pipe.decorator.ts +16 -0
  107. package/src/decorators/schema.decorator.ts +19 -0
  108. package/src/di/__tests__/container.test.ts +106 -0
  109. package/src/di/container.ts +98 -0
  110. package/src/di/inject.decorator.ts +14 -0
  111. package/src/di/injectable.decorator.ts +9 -0
  112. package/src/di/provider.ts +31 -0
  113. package/src/exceptions/http.exception.ts +29 -0
  114. package/src/exceptions/index.ts +32 -0
  115. package/src/exceptions/validation.pipe.ts +68 -0
  116. package/src/factory/__tests__/exception-filters.test.ts +102 -0
  117. package/src/factory/__tests__/file-upload.test.ts +70 -0
  118. package/src/factory/elysia-factory.ts +445 -0
  119. package/src/globals.d.ts +7 -0
  120. package/src/interfaces.ts +33 -0
  121. package/src/services/logger.service.ts +135 -0
  122. package/src/session/__tests__/session.module.test.ts +55 -0
  123. package/src/session/session.module.ts +43 -0
  124. package/src/session/session.options.ts +40 -0
  125. package/src/session/session.service.ts +47 -0
  126. package/src/session/session.store.ts +73 -0
@@ -0,0 +1,102 @@
1
+ import { expect, test, describe } from 'bun:test';
2
+ import { Controller } from '../../decorators/controller.decorator';
3
+ import { Get } from '../../decorators/method.decorator';
4
+ import { Catch } from '../../decorators/catch.decorator';
5
+ import { UseFilters } from '../../decorators/filter.decorator';
6
+ import { Module } from '../../decorators/module.decorator';
7
+ import { Injectable } from '../../di/injectable.decorator';
8
+ import { ElysiaFactory } from '../elysia-factory';
9
+ import { ExceptionFilter } from '../../interfaces';
10
+
11
+ class CustomError extends Error {
12
+ constructor(message: string) {
13
+ super(message);
14
+ this.name = 'CustomError';
15
+ }
16
+ }
17
+
18
+ @Injectable()
19
+ @Catch(CustomError)
20
+ class CustomErrorFilter implements ExceptionFilter {
21
+ catch(exception: CustomError, context: any) {
22
+ context.set.status = 418;
23
+ return {
24
+ caught: true,
25
+ message: exception.message,
26
+ };
27
+ }
28
+ }
29
+
30
+ @Injectable()
31
+ @Catch() // empty catch all
32
+ class GlobalFallbackFilter implements ExceptionFilter {
33
+ catch(exception: Error, context: any) {
34
+ context.set.status = 500;
35
+ return {
36
+ fromGlobal: true,
37
+ msg: 'Something went wrong',
38
+ };
39
+ }
40
+ }
41
+
42
+ @Controller('/errors')
43
+ class ErrorController {
44
+ @Get('/custom')
45
+ @UseFilters(CustomErrorFilter)
46
+ throwCustom() {
47
+ throw new CustomError('This is a custom error');
48
+ }
49
+
50
+ @Get('/unhandled')
51
+ throwUnhandled() {
52
+ throw new Error('This should hit global');
53
+ }
54
+
55
+ @Get('/success')
56
+ success() {
57
+ return { ok: true };
58
+ }
59
+ }
60
+
61
+ @Module({
62
+ controllers: [ErrorController],
63
+ providers: [CustomErrorFilter, GlobalFallbackFilter]
64
+ })
65
+ class ErrorModule {}
66
+
67
+ describe('Exception Filters', () => {
68
+ test('should catch specific error via local @UseFilters', async () => {
69
+ const app = await ElysiaFactory.create(ErrorModule);
70
+ const req = new Request('http://localhost/errors/custom');
71
+ const res = await app.handle(req);
72
+ const json = await res.json();
73
+
74
+ expect(res.status).toBe(418);
75
+ expect(json.caught).toBe(true);
76
+ expect(json.message).toBe('This is a custom error');
77
+ });
78
+
79
+ test('should catch unhandled error via global filter', async () => {
80
+ const app = await ElysiaFactory.create(ErrorModule, {
81
+ globalFilters: [GlobalFallbackFilter]
82
+ });
83
+ const req = new Request('http://localhost/errors/unhandled');
84
+ const res = await app.handle(req);
85
+ const json = await res.json();
86
+
87
+ expect(res.status).toBe(500);
88
+ expect(json.fromGlobal).toBe(true);
89
+ });
90
+
91
+ test('should not interfere with successful requests', async () => {
92
+ const app = await ElysiaFactory.create(ErrorModule, {
93
+ globalFilters: [GlobalFallbackFilter]
94
+ });
95
+ const req = new Request('http://localhost/errors/success');
96
+ const res = await app.handle(req);
97
+ const json = await res.json();
98
+
99
+ expect(res.status).toBe(200);
100
+ expect(json.ok).toBe(true);
101
+ });
102
+ });
@@ -0,0 +1,70 @@
1
+ import { expect, test, describe } from 'bun:test';
2
+ import { Controller } from '../../decorators/controller.decorator';
3
+ import { Post } from '../../decorators/method.decorator';
4
+ import { File, Files } from '../../decorators/param.decorator';
5
+ import { Module } from '../../decorators/module.decorator';
6
+ import { ElysiaFactory } from '../elysia-factory';
7
+
8
+ @Controller('/upload')
9
+ class UploadController {
10
+ @Post('/single')
11
+ uploadSingle(@File('avatar') avatar: globalThis.File) {
12
+ if (!avatar) return { error: 'No file' };
13
+ return { name: avatar.name, size: avatar.size };
14
+ }
15
+
16
+ @Post('/multiple')
17
+ uploadMultiple(@Files('documents') docs: globalThis.File[]) {
18
+ if (!docs) return { error: 'No files' };
19
+ // Elysia sometimes parses single items as just the item, and multiple as an array.
20
+ // If it's a single file, we wrap it in an array for consistency
21
+ const filesArray = Array.isArray(docs) ? docs : [docs];
22
+ return { count: filesArray.length, first: filesArray[0]?.name };
23
+ }
24
+ }
25
+
26
+ @Module({
27
+ controllers: [UploadController]
28
+ })
29
+ class UploadModule {}
30
+
31
+ describe('File Upload Decorators', () => {
32
+ test('should handle single file upload', async () => {
33
+ const app = await ElysiaFactory.create(UploadModule);
34
+
35
+ const formData = new FormData();
36
+ const file = new globalThis.File(['hello world'], 'avatar.jpg', { type: 'image/jpeg' });
37
+ formData.append('avatar', file);
38
+
39
+ const req = new Request('http://localhost/upload/single', {
40
+ method: 'POST',
41
+ body: formData,
42
+ });
43
+
44
+ const res = await app.handle(req);
45
+ const json = await res.json();
46
+
47
+ expect(res.status).toBe(200);
48
+ expect(json).toEqual({ name: 'avatar.jpg', size: 11 });
49
+ });
50
+
51
+ test('should handle multiple files upload', async () => {
52
+ const app = await ElysiaFactory.create(UploadModule);
53
+
54
+ const formData = new FormData();
55
+ formData.append('documents', new globalThis.File(['doc1'], 'doc1.pdf'));
56
+ formData.append('documents', new globalThis.File(['doc22'], 'doc2.pdf'));
57
+
58
+ const req = new Request('http://localhost/upload/multiple', {
59
+ method: 'POST',
60
+ body: formData,
61
+ });
62
+
63
+ const res = await app.handle(req);
64
+ const json = await res.json();
65
+
66
+ expect(res.status).toBe(200);
67
+ expect(json.count).toBe(2);
68
+ expect(json.first).toBe('doc1.pdf');
69
+ });
70
+ });
@@ -0,0 +1,445 @@
1
+ import { Elysia, type Context } from 'elysia';
2
+ import { globalContainer } from '../di/container';
3
+ import {
4
+ CONTROLLER_WATERMARK,
5
+ PATH_METADATA,
6
+ METHOD_METADATA,
7
+ ROUTE_ARGS_METADATA,
8
+ GUARDS_METADATA,
9
+ INTERCEPTORS_METADATA,
10
+ SCHEMA_METADATA,
11
+ PIPES_METADATA,
12
+ FILTERS_METADATA
13
+ } from '../constants';
14
+ import { RequestMethod } from '../decorators/method.decorator';
15
+ import { RouteParamtypes, type RouteParamMetadata } from '../decorators/param.decorator';
16
+ import { type CanActivate, type NestInterceptor, type PipeTransform, type ExceptionFilter } from '../interfaces';
17
+ import { FILTER_CATCH_EXCEPTIONS } from '../decorators/catch.decorator';
18
+ import { HttpException, ForbiddenException } from '../exceptions';
19
+ import { MODULE_METADATA } from '../constants';
20
+ import { type Type, type Provider, type InjectionToken } from '../di/provider';
21
+ import { Logger } from '../services/logger.service';
22
+ import { SessionService } from '../session/session.service';
23
+ import { SESSION_OPTIONS } from '../session/session.module';
24
+
25
+ export interface FactoryOptions {
26
+ globalPrefix?: string;
27
+ globalFilters?: (Type<ExceptionFilter> | ExceptionFilter)[];
28
+ }
29
+
30
+ export class ElysiaFactory {
31
+ private static readonly logger = new Logger('ElysiaFactory');
32
+
33
+ static async create(module: Type<unknown>, options?: FactoryOptions): Promise<Elysia> {
34
+ const banner = `
35
+ _ __ __ _ ____ __ __
36
+ / | / /___ _ _/ /_ (_)____ / __ )____ ______/ /_____ ____ ____/ /
37
+ / |/ / __ \\| |/_/ __/ / / ___/ ______ / __ / __ \`/ ___/ //_/ _ \\/ __ \\/ __ /
38
+ / /| / __/_> </ /_ / (__ ) /_____/ / /_/ / /_/ / /__/ ,< / __/ / / / /_/ /
39
+ /_/ |_/\\___/_/|_|\\__/ _/ /____/ /_____/\\__,_/\\___/_/|_|\\___/_/ /_/\\__,_/
40
+ /___/
41
+ (v1.0.0)
42
+ `;
43
+ console.log(`\x1b[36m${banner}\x1b[0m`);
44
+ this.logger.log('Starting Next.js-Backend application...');
45
+
46
+ const app = new Elysia({ prefix: options?.globalPrefix });
47
+
48
+ // Global Error Handler
49
+ app.onError(({ error, set }) => {
50
+ if (error instanceof HttpException) {
51
+ set.status = error.getStatus();
52
+ return error.getResponse();
53
+ }
54
+
55
+ // Fallback
56
+ set.status = 500;
57
+ return {
58
+ message:
59
+ error instanceof Error
60
+ ? error.message
61
+ : (error as Record<string, unknown>)?.message || 'Internal server error',
62
+ statusCode: 500,
63
+ };
64
+ });
65
+
66
+ // Extract module metadata recursively
67
+ const controllers: Type<unknown>[] = [];
68
+ const providers: Provider[] = [];
69
+
70
+ // Store already resolved modules to prevent circular dependencies
71
+ const resolvedModules = new Set<unknown>();
72
+
73
+ const resolveModule = (mod: any) => {
74
+ // Handle Dynamic Modules
75
+ if (mod && typeof mod === 'object' && 'module' in mod) {
76
+ if (resolvedModules.has(mod.module)) return;
77
+ resolvedModules.add(mod.module);
78
+
79
+ if (mod.controllers) controllers.push(...mod.controllers);
80
+ if (mod.providers) providers.push(...mod.providers);
81
+
82
+ // Dynamic modules can also have imports
83
+ const dynImports = mod.imports || [];
84
+ for (const imp of dynImports) resolveModule(imp);
85
+
86
+ // Process the static parts of the dynamic module class
87
+ resolveModule(mod.module);
88
+ return;
89
+ }
90
+
91
+ // Handle Static Modules
92
+ if (resolvedModules.has(mod)) return;
93
+ resolvedModules.add(mod);
94
+
95
+ const modImports = Reflect.getMetadata(MODULE_METADATA.IMPORTS, mod) || [];
96
+ const modControllers = Reflect.getMetadata(MODULE_METADATA.CONTROLLERS, mod) || [];
97
+ const modProviders = Reflect.getMetadata(MODULE_METADATA.PROVIDERS, mod) || [];
98
+
99
+ controllers.push(...modControllers);
100
+ providers.push(...modProviders);
101
+
102
+ for (const imp of modImports) resolveModule(imp);
103
+ };
104
+
105
+ resolveModule(module);
106
+
107
+ // Register all collected providers into the container
108
+ globalContainer.addProviders(providers);
109
+
110
+ // Ensure all providers are resolved before taking requests
111
+ for (const provider of providers) {
112
+ let token: InjectionToken;
113
+ if (typeof provider === 'function') {
114
+ token = provider;
115
+ } else if (provider && typeof provider === 'object' && 'provide' in provider) {
116
+ token = provider.provide;
117
+ } else {
118
+ continue;
119
+ }
120
+ await globalContainer.resolve(token);
121
+ }
122
+
123
+ for (const controllerClass of controllers) {
124
+ // We expect controllerClass to be a constructor function
125
+ const constructorTarget = controllerClass as new (...args: unknown[]) => unknown;
126
+
127
+ const isController = Reflect.getMetadata(CONTROLLER_WATERMARK, constructorTarget);
128
+ if (!isController) {
129
+ throw new Error(`${constructorTarget.name} is not a valid controller`);
130
+ }
131
+
132
+ // Resolve controller instance via DI container
133
+ const instance = await globalContainer.resolve(constructorTarget) as Record<string, unknown>;
134
+
135
+ const prefix = Reflect.getMetadata(PATH_METADATA, constructorTarget) || '';
136
+
137
+ const prototype = Object.getPrototypeOf(instance);
138
+ const methodsNames = Object.getOwnPropertyNames(prototype).filter(
139
+ (method) => method !== 'constructor' && typeof prototype[method] === 'function',
140
+ );
141
+
142
+ for (const methodName of methodsNames) {
143
+ const methodFn = prototype[methodName] as Function;
144
+
145
+ const httpMethod: RequestMethod = Reflect.getMetadata(METHOD_METADATA, methodFn);
146
+ if (!httpMethod) continue; // Not an endpoint
147
+
148
+ const pathMetadata = Reflect.getMetadata(PATH_METADATA, methodFn) || '/';
149
+ // Handle paths correctly. If root array we just use empty string to avoid trailing slashes
150
+ const normalizedPrefix = prefix === '/' ? '' : prefix;
151
+ // Keep standard routing rules
152
+ let fullPath = `${normalizedPrefix}${pathMetadata === '/' ? '' : pathMetadata}`;
153
+ if (!fullPath) fullPath = '/';
154
+
155
+ const routeArgsMetadata = (Reflect.getMetadata(ROUTE_ARGS_METADATA, constructorTarget, methodName) || {}) as Record<string, RouteParamMetadata>;
156
+ const schemaMetadata = Reflect.getMetadata(SCHEMA_METADATA, methodFn) || {};
157
+
158
+ // Dynamically build Elysia Schemas from inline parameter decorators
159
+ const builtSchema: Record<string, unknown> = { ...schemaMetadata };
160
+ const bodySchemaProperties: Record<string, unknown> = {};
161
+ const querySchemaProperties: Record<string, unknown> = {};
162
+ const paramSchemaProperties: Record<string, unknown> = {};
163
+ const headersSchemaProperties: Record<string, unknown> = {};
164
+ let hasBodySchema = false;
165
+
166
+ Object.keys(routeArgsMetadata).forEach((key) => {
167
+ const metadata = routeArgsMetadata[key];
168
+ const paramType = Number(key.split(':')[0]) as RouteParamtypes;
169
+
170
+ if (metadata && metadata.schema) {
171
+ if (paramType === RouteParamtypes.BODY) {
172
+ if (metadata.data) {
173
+ bodySchemaProperties[metadata.data] = metadata.schema;
174
+ hasBodySchema = true;
175
+ } else {
176
+ // If the entire body is replaced by a schema (e.g @Body(t.Object(...)))
177
+ builtSchema.body = metadata.schema;
178
+ }
179
+ } else if (paramType === RouteParamtypes.QUERY && metadata.data) {
180
+ querySchemaProperties[metadata.data] = metadata.schema;
181
+ } else if (paramType === RouteParamtypes.PARAM && metadata.data) {
182
+ paramSchemaProperties[metadata.data] = metadata.schema;
183
+ } else if (paramType === RouteParamtypes.HEADERS && metadata.data) {
184
+ headersSchemaProperties[metadata.data.toLowerCase()] = metadata.schema;
185
+ } else if (paramType === RouteParamtypes.FILE || paramType === RouteParamtypes.FILES) {
186
+ // Files are part of the body but require special multi-part handling
187
+ if (metadata.data) {
188
+ bodySchemaProperties[metadata.data] = metadata.schema;
189
+ hasBodySchema = true;
190
+ }
191
+ }
192
+ }
193
+ });
194
+
195
+ // We only use Object schema wrapper if there wasn't a catch-all body schema already defined
196
+ if (hasBodySchema && !builtSchema.body) {
197
+ const { t } = require('elysia');
198
+ builtSchema.body = t.Object(bodySchemaProperties);
199
+ }
200
+
201
+ if (Object.keys(querySchemaProperties).length > 0 && !builtSchema.query) {
202
+ const { t } = require('elysia');
203
+ builtSchema.query = t.Object(querySchemaProperties);
204
+ }
205
+ if (Object.keys(paramSchemaProperties).length > 0 && !builtSchema.params) {
206
+ const { t } = require('elysia');
207
+ builtSchema.params = t.Object(paramSchemaProperties);
208
+ }
209
+ if (Object.keys(headersSchemaProperties).length > 0 && !builtSchema.headers) {
210
+ const { t } = require('elysia');
211
+ builtSchema.headers = t.Object(headersSchemaProperties);
212
+ }
213
+
214
+ const controllerGuards = Reflect.getMetadata(GUARDS_METADATA, constructorTarget) || [];
215
+ const methodGuards = Reflect.getMetadata(GUARDS_METADATA, methodFn) || [];
216
+ const allGuards = [...controllerGuards, ...methodGuards];
217
+
218
+ const controllerInterceptors = Reflect.getMetadata(INTERCEPTORS_METADATA, constructorTarget) || [];
219
+ const methodInterceptors = Reflect.getMetadata(INTERCEPTORS_METADATA, methodFn) || [];
220
+ const allInterceptors = [...controllerInterceptors, ...methodInterceptors];
221
+
222
+ const controllerPipes = Reflect.getMetadata(PIPES_METADATA, constructorTarget) || [];
223
+ const methodPipes = Reflect.getMetadata(PIPES_METADATA, methodFn) || [];
224
+ const allPipes = [...controllerPipes, ...methodPipes];
225
+
226
+ const controllerFilters = Reflect.getMetadata(FILTERS_METADATA, constructorTarget) || [];
227
+ const methodFilters = Reflect.getMetadata(FILTERS_METADATA, methodFn) || [];
228
+ const allFilters = [...(options?.globalFilters || []), ...controllerFilters, ...methodFilters];
229
+
230
+ // Map the method descriptor back to Elysia handlers
231
+ const elysiaMethod = httpMethod === RequestMethod.DELETE ? 'delete' : httpMethod;
232
+
233
+ // Using type assertion for dynamic method calling on Elysia instance
234
+ const elysiaApp = app as unknown as Record<string, Function>;
235
+
236
+ if (typeof elysiaApp[elysiaMethod] === 'function') {
237
+ this.logger.log(`Mapped {${fullPath}, ${httpMethod}} route`, 'RouterExplorer');
238
+ elysiaApp[elysiaMethod](
239
+ fullPath,
240
+ async (context: Context) => {
241
+ // 1. Extract base args
242
+ const extractedArgs = await ElysiaFactory.extractContextArgs(routeArgsMetadata, context);
243
+
244
+ // 2. Get Typescript types of arguments
245
+ const paramTypes = Reflect.getMetadata('design:paramtypes', prototype, methodName) || [];
246
+
247
+ // 3. Apply Pipes (Transform/Validation)
248
+ for (let i = 0; i < extractedArgs.length; i++) {
249
+ const metadataPair = Object.entries(routeArgsMetadata).find(([_, meta]) => meta.index === i);
250
+ if (!metadataPair) continue;
251
+
252
+ const [key, meta] = metadataPair;
253
+ const paramTypeNumber = Number(key.split(':')[0]) as RouteParamtypes;
254
+
255
+ let argType: 'body' | 'query' | 'param' | 'custom' = 'custom';
256
+ if (paramTypeNumber === RouteParamtypes.BODY) argType = 'body';
257
+ if (paramTypeNumber === RouteParamtypes.QUERY) argType = 'query';
258
+ if (paramTypeNumber === RouteParamtypes.PARAM) argType = 'param';
259
+
260
+ const argMeta = {
261
+ type: argType,
262
+ metatype: paramTypes[i],
263
+ data: meta.data
264
+ };
265
+
266
+ // Run through all pipes
267
+ for (const pipeClass of allPipes) {
268
+ const pipeInstance = (typeof pipeClass === 'function' && !pipeClass.transform)
269
+ ? await globalContainer.resolve(pipeClass as Type<unknown>) as PipeTransform
270
+ : pipeClass; // Handle both Class reference and pre-instantiated pipe (e.g new ValidationPipe())
271
+
272
+ extractedArgs[i] = await pipeInstance.transform(extractedArgs[i], argMeta, context);
273
+ }
274
+ }
275
+
276
+ // execution wrapper for interceptors
277
+ const executeMethod = async () => await methodFn.apply(instance, extractedArgs);
278
+ let runner: () => Promise<unknown> = executeMethod;
279
+
280
+ // Interceptors wrap the actual controller execution
281
+ for (let i = allInterceptors.length - 1; i >= 0; i--) {
282
+ const interceptorClass = allInterceptors[i];
283
+ // Instantiate interceptor via DI
284
+ const interceptorInstance = await globalContainer.resolve(interceptorClass as Type<unknown>) as NestInterceptor;
285
+ const nextRunner = runner;
286
+ runner = async () => await interceptorInstance.intercept(context, nextRunner);
287
+ }
288
+
289
+ try {
290
+ return await runner();
291
+ } catch (error: any) {
292
+ // Process Filters in Reverse Order (closest to method first)
293
+ for (let i = allFilters.length - 1; i >= 0; i--) {
294
+ const filterClassOrInstance = allFilters[i];
295
+
296
+ const filterInstance = (typeof filterClassOrInstance === 'function' && !filterClassOrInstance.catch)
297
+ ? await globalContainer.resolve(filterClassOrInstance as Type<unknown>) as ExceptionFilter
298
+ : filterClassOrInstance as ExceptionFilter;
299
+
300
+ // Which exceptions does this filter catch?
301
+ const catchTypes = Reflect.getMetadata(FILTER_CATCH_EXCEPTIONS, filterInstance.constructor) || [];
302
+
303
+ // If @Catch() is empty, it catches everything. Otherwise, it must match the error type.
304
+ const shouldCatch = catchTypes.length === 0 || catchTypes.some((type: any) => error instanceof type);
305
+
306
+ if (shouldCatch) {
307
+ return await filterInstance.catch(error, context);
308
+ }
309
+ }
310
+
311
+ // Re-throw to Elysia's global error handler if no local filter caught it
312
+ throw error;
313
+ }
314
+ },
315
+ {
316
+ ...builtSchema,
317
+ // Force JSON parsing if body exists but no strict schema is declared natively
318
+ type: hasBodySchema ? 'json' : undefined,
319
+ beforeHandle: async (context: Context) => {
320
+ for (const guard of allGuards) {
321
+ const guardInstance = await globalContainer.resolve(guard as Type<unknown>) as CanActivate;
322
+ const canActivate = await guardInstance.canActivate(context);
323
+ if (!canActivate) {
324
+ throw new ForbiddenException('Forbidden resource');
325
+ }
326
+ }
327
+ }
328
+ }
329
+ );
330
+ }
331
+ }
332
+ }
333
+
334
+ return app as unknown as Elysia;
335
+ }
336
+
337
+ private static async extractContextArgs(routeArgsMetadata: Record<string, unknown>, context: Context): Promise<unknown[]> {
338
+ const args: unknown[] = [];
339
+
340
+ if (Object.keys(routeArgsMetadata).length === 0) {
341
+ return [];
342
+ }
343
+
344
+ for (const key of Object.keys(routeArgsMetadata)) {
345
+ const metadata = routeArgsMetadata[key] as RouteParamMetadata;
346
+ const { index, data } = metadata;
347
+
348
+ const paramType = Number(key.split(':')[0]) as RouteParamtypes;
349
+
350
+ switch (paramType) {
351
+ case RouteParamtypes.BODY:
352
+ args[index] = data && typeof context.body === 'object' && context.body !== null
353
+ ? (context.body as Record<string, unknown>)[data as string]
354
+ : context.body;
355
+ break;
356
+ case RouteParamtypes.QUERY:
357
+ args[index] = data
358
+ ? (context.query as Record<string, unknown>)?.[data as string]
359
+ : context.query;
360
+ break;
361
+ case RouteParamtypes.PARAM:
362
+ args[index] = data
363
+ ? (context.params as Record<string, unknown>)?.[data as string]
364
+ : context.params;
365
+ break;
366
+ case RouteParamtypes.HEADERS:
367
+ args[index] = data
368
+ ? (context.headers as Record<string, unknown>)?.[(data as string || '').toLowerCase()]
369
+ : context.headers;
370
+ break;
371
+ case RouteParamtypes.REQUEST:
372
+ args[index] = context.request;
373
+ break;
374
+ case RouteParamtypes.RESPONSE:
375
+ // Elysia context.set is used to modify response like status
376
+ args[index] = context.set;
377
+ break;
378
+ case RouteParamtypes.SESSION: {
379
+ try {
380
+ const sessionService = await globalContainer.resolve(SessionService) as any;
381
+ const options = await globalContainer.resolve(SESSION_OPTIONS) as any;
382
+ const cookieName = options.cookieName || 'sid';
383
+
384
+ const cookieObj = (context.cookie as Record<string, any>)?.[cookieName];
385
+ const cookieVal = cookieObj?.value;
386
+
387
+ if (cookieVal) {
388
+ const sessionData = await sessionService.getSession(cookieVal);
389
+ args[index] = data && sessionData ? sessionData[data as string] : sessionData;
390
+ } else {
391
+ args[index] = null;
392
+ }
393
+ } catch (e) {
394
+ // Unregistered SessionModule fallback
395
+ args[index] = null;
396
+ }
397
+ break;
398
+ }
399
+ case RouteParamtypes.FILE:
400
+ case RouteParamtypes.FILES:
401
+ args[index] = data && typeof context.body === 'object' && context.body !== null
402
+ ? (context.body as Record<string, unknown>)[data as string]
403
+ : context.body;
404
+ break;
405
+ default:
406
+ args[index] = null;
407
+ }
408
+ }
409
+
410
+ return args;
411
+ }
412
+
413
+ /**
414
+ * Helper function to directly mount the application into Next.js App Router.
415
+ * Simply destruct the HTTP method verbs and export them in your `route.ts`.
416
+ * e.g., export const { GET, POST, PUT, PATCH, DELETE } = ElysiaFactory.createNextJsHandlers(AppModule, { globalPrefix: '/api' });
417
+ */
418
+ static createNextJsHandlers(module: Type<unknown>, options?: FactoryOptions) {
419
+ let appInstance: Elysia | null = null;
420
+
421
+ // We instantiate the application only once (Singleton pattern)
422
+ const getApp = async () => {
423
+ if (!appInstance) {
424
+ appInstance = await ElysiaFactory.create(module, options);
425
+ }
426
+ return appInstance;
427
+ };
428
+
429
+ const handler = async (req: Request) => {
430
+ const app = await getApp();
431
+ return app.handle(req);
432
+ };
433
+
434
+ return {
435
+ GET: handler,
436
+ POST: handler,
437
+ PUT: handler,
438
+ PATCH: handler,
439
+ DELETE: handler,
440
+ OPTIONS: handler,
441
+ HEAD: handler,
442
+ };
443
+ }
444
+ }
445
+
@@ -0,0 +1,7 @@
1
+ export interface SessionData {
2
+ [key: string]: unknown;
3
+ }
4
+
5
+ export interface User {
6
+ [key: string]: unknown;
7
+ }
@@ -0,0 +1,33 @@
1
+ import { type Context } from 'elysia';
2
+ import { type Provider, type Type } from './di/provider';
3
+
4
+ export interface CanActivate {
5
+ canActivate(context: Context): boolean | Promise<boolean>;
6
+ }
7
+
8
+ export interface NestInterceptor<T = any, R = any> {
9
+ intercept(context: any, next: () => Promise<R>): Promise<R>;
10
+ }
11
+
12
+ export interface ExceptionFilter<T = any> {
13
+ catch(exception: T, context: any): any | Promise<any>;
14
+ }
15
+
16
+ export interface PipeTransform<T = unknown, R = unknown> {
17
+ transform(value: T, metadata: ArgumentMetadata, context: Context): R | Promise<R>;
18
+ }
19
+
20
+ export interface ArgumentMetadata {
21
+ type: 'body' | 'query' | 'param' | 'custom' | 'headers';
22
+ metatype?: Type<unknown> | Function;
23
+ data?: string;
24
+ }
25
+
26
+ export interface DynamicModule {
27
+ module: Type<unknown>;
28
+ providers?: Provider[];
29
+ controllers?: Type<unknown>[];
30
+ imports?: Array<Type<unknown> | DynamicModule | Promise<DynamicModule>>;
31
+ exports?: Array<Type<unknown> | Provider | string | symbol>;
32
+ }
33
+