@hazeljs/core 0.2.0-beta.1

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 (194) hide show
  1. package/README.md +522 -0
  2. package/dist/__tests__/container.test.d.ts +2 -0
  3. package/dist/__tests__/container.test.d.ts.map +1 -0
  4. package/dist/__tests__/container.test.js +454 -0
  5. package/dist/__tests__/decorators.test.d.ts +2 -0
  6. package/dist/__tests__/decorators.test.d.ts.map +1 -0
  7. package/dist/__tests__/decorators.test.js +693 -0
  8. package/dist/__tests__/errors/http.error.test.d.ts +2 -0
  9. package/dist/__tests__/errors/http.error.test.d.ts.map +1 -0
  10. package/dist/__tests__/errors/http.error.test.js +117 -0
  11. package/dist/__tests__/filters/exception-filter.test.d.ts +2 -0
  12. package/dist/__tests__/filters/exception-filter.test.d.ts.map +1 -0
  13. package/dist/__tests__/filters/exception-filter.test.js +135 -0
  14. package/dist/__tests__/filters/http-exception.filter.test.d.ts +2 -0
  15. package/dist/__tests__/filters/http-exception.filter.test.d.ts.map +1 -0
  16. package/dist/__tests__/filters/http-exception.filter.test.js +119 -0
  17. package/dist/__tests__/hazel-app.test.d.ts +2 -0
  18. package/dist/__tests__/hazel-app.test.d.ts.map +1 -0
  19. package/dist/__tests__/hazel-app.test.js +682 -0
  20. package/dist/__tests__/hazel-module.test.d.ts +2 -0
  21. package/dist/__tests__/hazel-module.test.d.ts.map +1 -0
  22. package/dist/__tests__/hazel-module.test.js +408 -0
  23. package/dist/__tests__/hazel-response.test.d.ts +2 -0
  24. package/dist/__tests__/hazel-response.test.d.ts.map +1 -0
  25. package/dist/__tests__/hazel-response.test.js +138 -0
  26. package/dist/__tests__/health.test.d.ts +2 -0
  27. package/dist/__tests__/health.test.d.ts.map +1 -0
  28. package/dist/__tests__/health.test.js +147 -0
  29. package/dist/__tests__/index.test.d.ts +2 -0
  30. package/dist/__tests__/index.test.d.ts.map +1 -0
  31. package/dist/__tests__/index.test.js +239 -0
  32. package/dist/__tests__/interceptors/interceptor.test.d.ts +2 -0
  33. package/dist/__tests__/interceptors/interceptor.test.d.ts.map +1 -0
  34. package/dist/__tests__/interceptors/interceptor.test.js +166 -0
  35. package/dist/__tests__/logger.test.d.ts +2 -0
  36. package/dist/__tests__/logger.test.d.ts.map +1 -0
  37. package/dist/__tests__/logger.test.js +141 -0
  38. package/dist/__tests__/middleware/cors.test.d.ts +2 -0
  39. package/dist/__tests__/middleware/cors.test.d.ts.map +1 -0
  40. package/dist/__tests__/middleware/cors.test.js +129 -0
  41. package/dist/__tests__/middleware/csrf.test.d.ts +2 -0
  42. package/dist/__tests__/middleware/csrf.test.d.ts.map +1 -0
  43. package/dist/__tests__/middleware/csrf.test.js +247 -0
  44. package/dist/__tests__/middleware/global-middleware.test.d.ts +2 -0
  45. package/dist/__tests__/middleware/global-middleware.test.d.ts.map +1 -0
  46. package/dist/__tests__/middleware/global-middleware.test.js +259 -0
  47. package/dist/__tests__/middleware/rate-limit.test.d.ts +2 -0
  48. package/dist/__tests__/middleware/rate-limit.test.d.ts.map +1 -0
  49. package/dist/__tests__/middleware/rate-limit.test.js +264 -0
  50. package/dist/__tests__/middleware/security-headers.test.d.ts +2 -0
  51. package/dist/__tests__/middleware/security-headers.test.d.ts.map +1 -0
  52. package/dist/__tests__/middleware/security-headers.test.js +229 -0
  53. package/dist/__tests__/middleware/timeout.test.d.ts +2 -0
  54. package/dist/__tests__/middleware/timeout.test.d.ts.map +1 -0
  55. package/dist/__tests__/middleware/timeout.test.js +132 -0
  56. package/dist/__tests__/middleware.test.d.ts +2 -0
  57. package/dist/__tests__/middleware.test.d.ts.map +1 -0
  58. package/dist/__tests__/middleware.test.js +180 -0
  59. package/dist/__tests__/pipes/pipe.test.d.ts +2 -0
  60. package/dist/__tests__/pipes/pipe.test.d.ts.map +1 -0
  61. package/dist/__tests__/pipes/pipe.test.js +245 -0
  62. package/dist/__tests__/pipes/validation.pipe.test.d.ts +2 -0
  63. package/dist/__tests__/pipes/validation.pipe.test.d.ts.map +1 -0
  64. package/dist/__tests__/pipes/validation.pipe.test.js +297 -0
  65. package/dist/__tests__/request-parser.test.d.ts +2 -0
  66. package/dist/__tests__/request-parser.test.d.ts.map +1 -0
  67. package/dist/__tests__/request-parser.test.js +182 -0
  68. package/dist/__tests__/router.test.d.ts +2 -0
  69. package/dist/__tests__/router.test.d.ts.map +1 -0
  70. package/dist/__tests__/router.test.js +680 -0
  71. package/dist/__tests__/routing/route-matcher.test.d.ts +2 -0
  72. package/dist/__tests__/routing/route-matcher.test.d.ts.map +1 -0
  73. package/dist/__tests__/routing/route-matcher.test.js +219 -0
  74. package/dist/__tests__/routing/version.decorator.test.d.ts +2 -0
  75. package/dist/__tests__/routing/version.decorator.test.d.ts.map +1 -0
  76. package/dist/__tests__/routing/version.decorator.test.js +298 -0
  77. package/dist/__tests__/service.test.d.ts +2 -0
  78. package/dist/__tests__/service.test.d.ts.map +1 -0
  79. package/dist/__tests__/service.test.js +121 -0
  80. package/dist/__tests__/shutdown.test.d.ts +2 -0
  81. package/dist/__tests__/shutdown.test.d.ts.map +1 -0
  82. package/dist/__tests__/shutdown.test.js +250 -0
  83. package/dist/__tests__/testing/testing.module.test.d.ts +2 -0
  84. package/dist/__tests__/testing/testing.module.test.d.ts.map +1 -0
  85. package/dist/__tests__/testing/testing.module.test.js +370 -0
  86. package/dist/__tests__/upload/file-upload.test.d.ts +2 -0
  87. package/dist/__tests__/upload/file-upload.test.d.ts.map +1 -0
  88. package/dist/__tests__/upload/file-upload.test.js +498 -0
  89. package/dist/__tests__/utils/sanitize.test.d.ts +2 -0
  90. package/dist/__tests__/utils/sanitize.test.d.ts.map +1 -0
  91. package/dist/__tests__/utils/sanitize.test.js +291 -0
  92. package/dist/__tests__/validator.test.d.ts +2 -0
  93. package/dist/__tests__/validator.test.d.ts.map +1 -0
  94. package/dist/__tests__/validator.test.js +300 -0
  95. package/dist/container.d.ts +80 -0
  96. package/dist/container.d.ts.map +1 -0
  97. package/dist/container.js +271 -0
  98. package/dist/decorators.d.ts +92 -0
  99. package/dist/decorators.d.ts.map +1 -0
  100. package/dist/decorators.js +343 -0
  101. package/dist/errors/http.error.d.ts +31 -0
  102. package/dist/errors/http.error.d.ts.map +1 -0
  103. package/dist/errors/http.error.js +62 -0
  104. package/dist/filters/exception-filter.d.ts +39 -0
  105. package/dist/filters/exception-filter.d.ts.map +1 -0
  106. package/dist/filters/exception-filter.js +38 -0
  107. package/dist/filters/http-exception.filter.d.ts +9 -0
  108. package/dist/filters/http-exception.filter.d.ts.map +1 -0
  109. package/dist/filters/http-exception.filter.js +42 -0
  110. package/dist/hazel-app.d.ts +78 -0
  111. package/dist/hazel-app.d.ts.map +1 -0
  112. package/dist/hazel-app.js +453 -0
  113. package/dist/hazel-module.d.ts +20 -0
  114. package/dist/hazel-module.d.ts.map +1 -0
  115. package/dist/hazel-module.js +109 -0
  116. package/dist/hazel-response.d.ts +20 -0
  117. package/dist/hazel-response.d.ts.map +1 -0
  118. package/dist/hazel-response.js +68 -0
  119. package/dist/health.d.ts +73 -0
  120. package/dist/health.d.ts.map +1 -0
  121. package/dist/health.js +174 -0
  122. package/dist/index.d.ts +41 -0
  123. package/dist/index.d.ts.map +1 -0
  124. package/dist/index.js +140 -0
  125. package/dist/interceptors/interceptor.d.ts +22 -0
  126. package/dist/interceptors/interceptor.d.ts.map +1 -0
  127. package/dist/interceptors/interceptor.js +46 -0
  128. package/dist/logger.d.ts +8 -0
  129. package/dist/logger.d.ts.map +1 -0
  130. package/dist/logger.js +238 -0
  131. package/dist/middleware/cors.middleware.d.ts +44 -0
  132. package/dist/middleware/cors.middleware.d.ts.map +1 -0
  133. package/dist/middleware/cors.middleware.js +118 -0
  134. package/dist/middleware/csrf.middleware.d.ts +82 -0
  135. package/dist/middleware/csrf.middleware.d.ts.map +1 -0
  136. package/dist/middleware/csrf.middleware.js +183 -0
  137. package/dist/middleware/global-middleware.d.ts +111 -0
  138. package/dist/middleware/global-middleware.d.ts.map +1 -0
  139. package/dist/middleware/global-middleware.js +179 -0
  140. package/dist/middleware/rate-limit.middleware.d.ts +73 -0
  141. package/dist/middleware/rate-limit.middleware.d.ts.map +1 -0
  142. package/dist/middleware/rate-limit.middleware.js +124 -0
  143. package/dist/middleware/security-headers.middleware.d.ts +76 -0
  144. package/dist/middleware/security-headers.middleware.d.ts.map +1 -0
  145. package/dist/middleware/security-headers.middleware.js +123 -0
  146. package/dist/middleware/timeout.middleware.d.ts +25 -0
  147. package/dist/middleware/timeout.middleware.d.ts.map +1 -0
  148. package/dist/middleware/timeout.middleware.js +74 -0
  149. package/dist/middleware.d.ts +13 -0
  150. package/dist/middleware.d.ts.map +1 -0
  151. package/dist/middleware.js +47 -0
  152. package/dist/pipes/pipe.d.ts +50 -0
  153. package/dist/pipes/pipe.d.ts.map +1 -0
  154. package/dist/pipes/pipe.js +96 -0
  155. package/dist/pipes/validation.pipe.d.ts +6 -0
  156. package/dist/pipes/validation.pipe.d.ts.map +1 -0
  157. package/dist/pipes/validation.pipe.js +61 -0
  158. package/dist/request-context.d.ts +17 -0
  159. package/dist/request-context.d.ts.map +1 -0
  160. package/dist/request-context.js +2 -0
  161. package/dist/request-parser.d.ts +7 -0
  162. package/dist/request-parser.d.ts.map +1 -0
  163. package/dist/request-parser.js +60 -0
  164. package/dist/router.d.ts +33 -0
  165. package/dist/router.d.ts.map +1 -0
  166. package/dist/router.js +426 -0
  167. package/dist/routing/route-matcher.d.ts +39 -0
  168. package/dist/routing/route-matcher.d.ts.map +1 -0
  169. package/dist/routing/route-matcher.js +93 -0
  170. package/dist/routing/version.decorator.d.ts +36 -0
  171. package/dist/routing/version.decorator.d.ts.map +1 -0
  172. package/dist/routing/version.decorator.js +89 -0
  173. package/dist/service.d.ts +9 -0
  174. package/dist/service.d.ts.map +1 -0
  175. package/dist/service.js +39 -0
  176. package/dist/shutdown.d.ts +32 -0
  177. package/dist/shutdown.d.ts.map +1 -0
  178. package/dist/shutdown.js +109 -0
  179. package/dist/testing/testing.module.d.ts +83 -0
  180. package/dist/testing/testing.module.d.ts.map +1 -0
  181. package/dist/testing/testing.module.js +164 -0
  182. package/dist/types.d.ts +76 -0
  183. package/dist/types.d.ts.map +1 -0
  184. package/dist/types.js +2 -0
  185. package/dist/upload/file-upload.d.ts +75 -0
  186. package/dist/upload/file-upload.d.ts.map +1 -0
  187. package/dist/upload/file-upload.js +261 -0
  188. package/dist/utils/sanitize.d.ts +45 -0
  189. package/dist/utils/sanitize.d.ts.map +1 -0
  190. package/dist/utils/sanitize.js +165 -0
  191. package/dist/validator.d.ts +7 -0
  192. package/dist/validator.d.ts.map +1 -0
  193. package/dist/validator.js +119 -0
  194. package/package.json +65 -0
package/dist/router.js ADDED
@@ -0,0 +1,426 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Router = void 0;
7
+ const middleware_1 = require("./middleware");
8
+ const pipe_1 = require("./pipes/pipe");
9
+ const http_error_1 = require("./errors/http.error");
10
+ require("reflect-metadata");
11
+ const logger_1 = __importDefault(require("./logger"));
12
+ const request_parser_1 = require("./request-parser");
13
+ const hazel_response_1 = require("./hazel-response");
14
+ const validation_pipe_1 = require("./pipes/validation.pipe");
15
+ const ROUTE_METADATA_KEY = 'hazel:routes';
16
+ const CONTROLLER_METADATA_KEY = 'hazel:controller';
17
+ const HTTP_CODE_METADATA_KEY = 'hazel:http-code';
18
+ const HEADER_METADATA_KEY = 'hazel:headers';
19
+ const REDIRECT_METADATA_KEY = 'hazel:redirect';
20
+ class Router {
21
+ constructor(container) {
22
+ this.container = container;
23
+ this.routes = new Map();
24
+ this.routesByMethod = new Map();
25
+ this.middlewareHandler = new middleware_1.MiddlewareHandler(container);
26
+ // Initialize route cache for common HTTP methods
27
+ this.routesByMethod.set('GET', new Map());
28
+ this.routesByMethod.set('POST', new Map());
29
+ this.routesByMethod.set('PUT', new Map());
30
+ this.routesByMethod.set('DELETE', new Map());
31
+ this.routesByMethod.set('PATCH', new Map());
32
+ }
33
+ registerController(controller) {
34
+ logger_1.default.info(`Registering controller: ${controller.name}`);
35
+ const controllerMetadata = Reflect.getMetadata(CONTROLLER_METADATA_KEY, controller) || {};
36
+ const routes = Reflect.getMetadata(ROUTE_METADATA_KEY, controller) || [];
37
+ logger_1.default.debug('Controller metadata:', controllerMetadata);
38
+ logger_1.default.debug('Found routes:', routes);
39
+ routes.forEach((route) => {
40
+ const { method, path, propertyKey } = route;
41
+ const basePath = controllerMetadata.path || '';
42
+ const routePath = path || '';
43
+ const fullPath = this.normalizePath(`${basePath}${routePath}`);
44
+ logger_1.default.info(`Registering route: ${method} ${fullPath} (handler: ${String(propertyKey)})`);
45
+ // Get parameter types from TypeScript metadata
46
+ const paramTypes = Reflect.getMetadata('design:paramtypes', controller.prototype, propertyKey) || [];
47
+ logger_1.default.debug('Parameter types:', paramTypes.map((t) => t?.name || 'undefined'));
48
+ // Get parameter injections
49
+ const injections = Reflect.getMetadata('hazel:inject', controller, propertyKey) || [];
50
+ logger_1.default.debug('Parameter injections:', injections);
51
+ // Create a route handler for this specific route
52
+ // Pass the controller class, not instance, so it can be resolved per-request
53
+ const handler = this.createRouteHandler(controller, propertyKey);
54
+ const methodUpper = method.toUpperCase();
55
+ const routeKey = `${methodUpper} ${fullPath}`;
56
+ // Store in both maps for compatibility
57
+ this.routes.set(routeKey, [handler]);
58
+ // Store in method-specific cache for faster lookup
59
+ let methodRoutes = this.routesByMethod.get(methodUpper);
60
+ if (!methodRoutes) {
61
+ methodRoutes = new Map();
62
+ this.routesByMethod.set(methodUpper, methodRoutes);
63
+ }
64
+ methodRoutes.set(fullPath, [handler]);
65
+ // Log the route pattern for debugging
66
+ const pattern = this.createRoutePattern(fullPath);
67
+ logger_1.default.debug('Route pattern:', {
68
+ path: fullPath,
69
+ pattern: pattern.toString(),
70
+ pipes: route.pipes?.map((p) => p.type.name) || [],
71
+ paramTypes: paramTypes.map((t) => t?.name || 'undefined'),
72
+ });
73
+ });
74
+ logger_1.default.debug('All registered routes:');
75
+ this.routes.forEach((handlers, route) => {
76
+ logger_1.default.debug(`${route}`);
77
+ });
78
+ }
79
+ async applyPipes(value, pipes, context) {
80
+ let result = value;
81
+ // Get route-specific pipes
82
+ const allPipes = pipes.map((p) => this.container.resolve(p.type));
83
+ for (const pipe of allPipes) {
84
+ try {
85
+ result = await pipe.transform(result, context);
86
+ }
87
+ catch (error) {
88
+ logger_1.default.error('Validation error:', error);
89
+ // Re-throw the error to be caught by the route handler
90
+ throw error;
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+ async applyInterceptors(interceptors, context, next) {
96
+ if (interceptors.length === 0) {
97
+ return next();
98
+ }
99
+ const [current, ...remaining] = interceptors;
100
+ const interceptor = this.container.resolve(current.type);
101
+ return interceptor.intercept(context, () => this.applyInterceptors(remaining, context, next));
102
+ }
103
+ createRouteHandler(controllerClass, methodName) {
104
+ return async (req, res, matchedContext) => {
105
+ // Generate a unique request ID for request-scoped providers
106
+ const requestId = `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
107
+ try {
108
+ logger_1.default.debug('=== Request Handler Start ===');
109
+ logger_1.default.debug(`Method: ${req.method}, URL: ${req.url}`);
110
+ logger_1.default.debug('Matched context:', matchedContext);
111
+ // Optimize: Only serialize for debug logging
112
+ if (logger_1.default.isDebugEnabled && logger_1.default.isDebugEnabled()) {
113
+ const safeBody = JSON.parse(JSON.stringify(req.body || {}));
114
+ logger_1.default.debug(`Request body: ${JSON.stringify(safeBody, null, 2)}`);
115
+ }
116
+ // Resolve controller instance per-request with requestId
117
+ const controller = this.container.resolve(controllerClass, requestId);
118
+ const routes = Reflect.getMetadata(ROUTE_METADATA_KEY, controllerClass) || [];
119
+ const route = routes.find((r) => r.propertyKey === methodName);
120
+ if (!route) {
121
+ throw new Error(`Route not found for method ${String(methodName)}`);
122
+ }
123
+ // Get parameter injections
124
+ const injections = Reflect.getMetadata('hazel:inject', controllerClass, methodName) || [];
125
+ // Use the matched context if available, otherwise create a new one
126
+ const context = matchedContext || {
127
+ params: req.params || {},
128
+ query: req.query || {},
129
+ body: req.body || {},
130
+ headers: Object.fromEntries(Object.entries(req.headers || {})
131
+ .filter(([key]) => !key.toLowerCase().includes('authorization'))
132
+ .map(([key, value]) => [key, String(value)])),
133
+ method: req.method || 'GET',
134
+ url: req.url || '/',
135
+ };
136
+ // Set DTO type from the first parameter that has a DTO type
137
+ for (const injection of injections) {
138
+ if (injection?.dtoType) {
139
+ context.dtoType = injection.dtoType;
140
+ break;
141
+ }
142
+ }
143
+ // Get pipes for this route
144
+ const routePipes = route.pipes || [];
145
+ // Prepare arguments for the controller method
146
+ const args = [];
147
+ for (let i = 0; i < injections.length; i++) {
148
+ const injection = injections[i];
149
+ if (typeof injection === 'string') {
150
+ // Handle @Body, @Param, @Query decorators
151
+ if (injection === 'body') {
152
+ args[i] = context.body;
153
+ }
154
+ else if (injection === 'param') {
155
+ args[i] = context.params;
156
+ }
157
+ else if (injection === 'query') {
158
+ args[i] = context.query;
159
+ }
160
+ else if (injection === 'headers') {
161
+ args[i] = context.headers;
162
+ }
163
+ }
164
+ else if (typeof injection === 'object' && injection !== null) {
165
+ if (injection.type === 'headers') {
166
+ // Handle @Headers decorator
167
+ const headerName = injection.name;
168
+ if (headerName) {
169
+ args[i] = context.headers[headerName.toLowerCase()];
170
+ }
171
+ else {
172
+ args[i] = context.headers;
173
+ }
174
+ }
175
+ else if (injection.type === 'response') {
176
+ // Handle @Res decorator
177
+ args[i] = new hazel_response_1.HazelExpressResponse(res);
178
+ }
179
+ else if (injection.type === 'body') {
180
+ // Handle @Body decorator with DTO type
181
+ if (injection.dtoType) {
182
+ context.dtoType = injection.dtoType;
183
+ logger_1.default.debug('Setting DTO type in context:', injection.dtoType.name);
184
+ }
185
+ args[i] = context.body;
186
+ }
187
+ else if (injection.type === 'param') {
188
+ // Handle @Param decorator with pipe
189
+ const paramName = injection.name;
190
+ const paramValue = matchedContext?.params[paramName] || context.params[paramName];
191
+ if (injection.pipe) {
192
+ const pipe = this.container.resolve(injection.pipe);
193
+ args[i] = await pipe.transform(paramValue, context);
194
+ }
195
+ else {
196
+ args[i] = paramValue;
197
+ }
198
+ }
199
+ else if (injection.type === 'query') {
200
+ // Handle @Query decorator with pipe
201
+ const paramName = injection.name;
202
+ const queryValue = paramName ? context.query[paramName] : context.query;
203
+ if (injection.pipe) {
204
+ const pipe = this.container.resolve(injection.pipe);
205
+ args[i] = await pipe.transform(queryValue, context);
206
+ }
207
+ else {
208
+ args[i] = queryValue;
209
+ }
210
+ }
211
+ }
212
+ }
213
+ // Apply ValidationPipe to the body if a DTO type is present
214
+ if (context.body && context.dtoType) {
215
+ logger_1.default.debug('Applying ValidationPipe with DTO type:', context.dtoType.name);
216
+ const validationPipe = this.container.resolve(validation_pipe_1.ValidationPipe);
217
+ context.body = await validationPipe.transform(context.body, context);
218
+ args[injections.findIndex((i) => typeof i === 'object' && i !== null && i.type === 'body')] = context.body;
219
+ }
220
+ // Apply other pipes to the body if it exists
221
+ if (context.body) {
222
+ context.body = await this.applyPipes(context.body, routePipes, context);
223
+ }
224
+ // Get the controller method
225
+ const method = controller[methodName];
226
+ // Execute the controller method with interceptors
227
+ const result = await this.applyInterceptors(Reflect.getMetadata('hazel:interceptors', controllerClass, methodName) || [], context, async () => {
228
+ return method.apply(controller, args);
229
+ });
230
+ // Apply @Redirect metadata
231
+ const redirectMeta = Reflect.getMetadata(REDIRECT_METADATA_KEY, controllerClass.prototype, methodName);
232
+ if (redirectMeta) {
233
+ res.status(redirectMeta.statusCode).setHeader('Location', redirectMeta.url);
234
+ res.end();
235
+ return;
236
+ }
237
+ // Apply @Header metadata (response headers)
238
+ const headersMeta = Reflect.getMetadata(HEADER_METADATA_KEY, controllerClass.prototype, methodName);
239
+ if (headersMeta) {
240
+ for (const h of headersMeta) {
241
+ res.setHeader(h.name, h.value);
242
+ }
243
+ }
244
+ // Apply @HttpCode metadata
245
+ const httpCode = Reflect.getMetadata(HTTP_CODE_METADATA_KEY, controllerClass.prototype, methodName);
246
+ // Handle the response
247
+ if (result !== undefined) {
248
+ if (httpCode) {
249
+ res.status(httpCode);
250
+ }
251
+ if (typeof result === 'string' && result.trim().startsWith('<!DOCTYPE html>')) {
252
+ // Handle HTML response
253
+ res.setHeader('Content-Type', 'text/html');
254
+ res.send(result);
255
+ }
256
+ else {
257
+ // Handle JSON response
258
+ res.json(result);
259
+ }
260
+ }
261
+ else if (httpCode) {
262
+ res.status(httpCode).end();
263
+ }
264
+ }
265
+ catch (error) {
266
+ logger_1.default.error('Request handler error:', error);
267
+ if (error instanceof pipe_1.ValidationError) {
268
+ const errorResponse = error.toJSON();
269
+ logger_1.default.error('Validation error response:', errorResponse);
270
+ res.status(400).json({
271
+ statusCode: 400,
272
+ message: errorResponse.message,
273
+ errors: errorResponse.errors,
274
+ });
275
+ }
276
+ else if (error instanceof http_error_1.HttpError) {
277
+ res.status(error.statusCode).json({
278
+ statusCode: error.statusCode,
279
+ message: error.message,
280
+ });
281
+ }
282
+ else {
283
+ // Log unhandled errors with full context
284
+ logger_1.default.error('Unhandled error:', {
285
+ error: error instanceof Error ? error.message : String(error),
286
+ stack: error instanceof Error ? error.stack : undefined,
287
+ requestId,
288
+ method: req.method,
289
+ url: req.url,
290
+ });
291
+ res.status(500).json({
292
+ statusCode: 500,
293
+ message: process.env.NODE_ENV === 'production'
294
+ ? 'Internal server error'
295
+ : error instanceof Error ? error.message : 'Unknown error',
296
+ });
297
+ }
298
+ }
299
+ finally {
300
+ // CRITICAL: Clean up request-scoped providers to prevent memory leaks
301
+ this.container.clearRequestScope(requestId);
302
+ }
303
+ };
304
+ }
305
+ extractParams(path, routePath) {
306
+ const params = {};
307
+ const pathParts = path.split('/');
308
+ const routeParts = routePath.split('/');
309
+ for (let i = 0; i < routeParts.length; i++) {
310
+ if (routeParts[i].startsWith(':')) {
311
+ const paramName = routeParts[i].slice(1);
312
+ params[paramName] = pathParts[i];
313
+ logger_1.default.debug(`Extracted param ${paramName}:`, pathParts[i]);
314
+ }
315
+ }
316
+ logger_1.default.debug('Extracted params:', params);
317
+ return params;
318
+ }
319
+ matchPath(path, routePath) {
320
+ const pathParts = path.split('/');
321
+ const routeParts = routePath.split('/');
322
+ if (pathParts.length !== routeParts.length) {
323
+ return false;
324
+ }
325
+ for (let i = 0; i < routeParts.length; i++) {
326
+ if (!routeParts[i].startsWith(':') && routeParts[i] !== pathParts[i]) {
327
+ return false;
328
+ }
329
+ }
330
+ return true;
331
+ }
332
+ createRoutePattern(path) {
333
+ if (path === '/') {
334
+ return /^\/?$/;
335
+ }
336
+ // Escape forward slashes and convert :param to ([^/]+)
337
+ const pattern = path
338
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape special regex characters
339
+ .replace(/:[^/]+/g, '([^/]+)');
340
+ return new RegExp(`^${pattern}/?$`);
341
+ }
342
+ normalizePath(path) {
343
+ if (path.length > 1 && path.endsWith('/')) {
344
+ path = path.slice(0, -1);
345
+ }
346
+ return path.startsWith('/') ? path : `/${path}`;
347
+ }
348
+ async match(method, url, context) {
349
+ const path = url.split('?')[0];
350
+ logger_1.default.debug(`Matching route: ${method} ${path}`);
351
+ // Use method-specific cache for O(1) method lookup instead of O(n)
352
+ const methodRoutes = this.routesByMethod.get(method);
353
+ if (!methodRoutes) {
354
+ logger_1.default.debug(`No routes registered for method: ${method}`);
355
+ return null;
356
+ }
357
+ // Now only iterate through routes for this specific HTTP method
358
+ for (const [routePath, handlers] of methodRoutes.entries()) {
359
+ if (this.matchPath(path, routePath)) {
360
+ const params = this.extractParams(path, routePath);
361
+ logger_1.default.debug('Matched route:', { method, url, params });
362
+ // Create a new context with the matched parameters
363
+ const matchedContext = {
364
+ ...context,
365
+ params: params, // Don't merge, just use the extracted params
366
+ };
367
+ logger_1.default.debug('Created matched context:', matchedContext);
368
+ // Create a new handler that ensures the context is passed
369
+ const handler = handlers[0];
370
+ const wrappedHandler = async (req, res, ctx) => {
371
+ logger_1.default.debug('Wrapped handler called with context:', ctx);
372
+ await handler(req, res, matchedContext);
373
+ };
374
+ return { handler: wrappedHandler, context: matchedContext };
375
+ }
376
+ }
377
+ return null;
378
+ }
379
+ get(path, handlers) {
380
+ this.addRoute('GET', path, handlers);
381
+ }
382
+ post(path, handlers) {
383
+ this.addRoute('POST', path, handlers);
384
+ }
385
+ put(path, handlers) {
386
+ this.addRoute('PUT', path, handlers);
387
+ }
388
+ delete(path, handlers) {
389
+ this.addRoute('DELETE', path, handlers);
390
+ }
391
+ addRoute(method, path, handlers) {
392
+ const normalizedPath = this.normalizePath(path);
393
+ this.routes.set(`${method.toUpperCase()} ${normalizedPath}`, handlers);
394
+ }
395
+ async handleRequest(req, res) {
396
+ try {
397
+ logger_1.default.debug('=== Request Handler Start ===');
398
+ logger_1.default.debug(`Method: ${req.method}, URL: ${req.url}`);
399
+ // Parse the request to get the initial context
400
+ const context = await request_parser_1.RequestParser.parseRequest(req);
401
+ logger_1.default.debug('Initial context:', context);
402
+ // Match the request method and URL
403
+ const match = await this.match(req.method || 'GET', req.url || '/', context);
404
+ if (!match) {
405
+ logger_1.default.warn(`No route found for ${req.method} ${req.url}`);
406
+ res.status(404).json({ error: 'Not Found' });
407
+ return;
408
+ }
409
+ // Log the matched context before passing it
410
+ logger_1.default.debug('Matched context before handler:', match.context);
411
+ // Update request params with matched parameters
412
+ req.params = match.context.params;
413
+ logger_1.default.debug('Updated request params:', req.params);
414
+ // Call the matched handler with the matched context
415
+ const handler = match.handler;
416
+ await handler(req, res, match.context);
417
+ }
418
+ catch (error) {
419
+ logger_1.default.error('Error handling request:', error);
420
+ res
421
+ .status(500)
422
+ .json({ error: error instanceof Error ? error.message : 'Internal Server Error' });
423
+ }
424
+ }
425
+ }
426
+ exports.Router = Router;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Route pattern matcher with support for:
3
+ * - Named parameters (:id)
4
+ * - Optional parameters (:id?)
5
+ * - Wildcard routes (*)
6
+ * - Regex patterns
7
+ */
8
+ export declare class RouteMatcher {
9
+ private readonly path;
10
+ private pattern;
11
+ private paramNames;
12
+ private isWildcard;
13
+ constructor(path: string);
14
+ /**
15
+ * Compile the route path into a regex pattern
16
+ */
17
+ private compile;
18
+ /**
19
+ * Match a URL path against this route
20
+ */
21
+ match(path: string): RouteMatch | null;
22
+ /**
23
+ * Get the route path
24
+ */
25
+ getPath(): string;
26
+ /**
27
+ * Check if route has parameters
28
+ */
29
+ hasParams(): boolean;
30
+ /**
31
+ * Get parameter names
32
+ */
33
+ getParamNames(): string[];
34
+ }
35
+ export interface RouteMatch {
36
+ params: Record<string, string>;
37
+ path: string;
38
+ }
39
+ //# sourceMappingURL=route-matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/routing/route-matcher.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,qBAAa,YAAY;IAKX,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,OAAO,CAAC,OAAO,CAAU;IACzB,OAAO,CAAC,UAAU,CAAgB;IAClC,OAAO,CAAC,UAAU,CAAS;gBAEE,IAAI,EAAE,MAAM;IAIzC;;OAEG;IACH,OAAO,CAAC,OAAO;IAgCf;;OAEG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IA4BtC;;OAEG;IACH,OAAO,IAAI,MAAM;IAIjB;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,aAAa,IAAI,MAAM,EAAE;CAG1B;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,IAAI,EAAE,MAAM,CAAC;CACd"}
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.RouteMatcher = void 0;
7
+ const logger_1 = __importDefault(require("../logger"));
8
+ /**
9
+ * Route pattern matcher with support for:
10
+ * - Named parameters (:id)
11
+ * - Optional parameters (:id?)
12
+ * - Wildcard routes (*)
13
+ * - Regex patterns
14
+ */
15
+ class RouteMatcher {
16
+ constructor(path) {
17
+ this.path = path;
18
+ this.paramNames = [];
19
+ this.isWildcard = false;
20
+ this.compile();
21
+ }
22
+ /**
23
+ * Compile the route path into a regex pattern
24
+ */
25
+ compile() {
26
+ // Check for wildcard
27
+ if (this.path.includes('*')) {
28
+ this.isWildcard = true;
29
+ }
30
+ let pattern = this.path;
31
+ // Escape special regex characters except : and *
32
+ pattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
33
+ // Handle optional parameters (:id?)
34
+ pattern = pattern.replace(/:(\w+)\?/g, (_, paramName) => {
35
+ this.paramNames.push(paramName);
36
+ return '(?:/([^/]+))?';
37
+ });
38
+ // Handle required parameters (:id)
39
+ pattern = pattern.replace(/:(\w+)/g, (_, paramName) => {
40
+ this.paramNames.push(paramName);
41
+ return '([^/]+)';
42
+ });
43
+ // Handle wildcards (*)
44
+ pattern = pattern.replace(/\*/g, '(.*)');
45
+ // Ensure exact match with optional trailing slash
46
+ this.pattern = new RegExp(`^${pattern}/?$`);
47
+ logger_1.default.debug(`Compiled route pattern: ${this.path} -> ${this.pattern}`);
48
+ }
49
+ /**
50
+ * Match a URL path against this route
51
+ */
52
+ match(path) {
53
+ const match = this.pattern.exec(path);
54
+ if (!match) {
55
+ return null;
56
+ }
57
+ const params = {};
58
+ // Extract parameters
59
+ for (let i = 0; i < this.paramNames.length; i++) {
60
+ const value = match[i + 1];
61
+ if (value !== undefined) {
62
+ params[this.paramNames[i]] = decodeURIComponent(value);
63
+ }
64
+ }
65
+ // Handle wildcard
66
+ if (this.isWildcard && match[match.length - 1]) {
67
+ params['*'] = match[match.length - 1];
68
+ }
69
+ return {
70
+ params,
71
+ path: this.path,
72
+ };
73
+ }
74
+ /**
75
+ * Get the route path
76
+ */
77
+ getPath() {
78
+ return this.path;
79
+ }
80
+ /**
81
+ * Check if route has parameters
82
+ */
83
+ hasParams() {
84
+ return this.paramNames.length > 0;
85
+ }
86
+ /**
87
+ * Get parameter names
88
+ */
89
+ getParamNames() {
90
+ return [...this.paramNames];
91
+ }
92
+ }
93
+ exports.RouteMatcher = RouteMatcher;
@@ -0,0 +1,36 @@
1
+ import 'reflect-metadata';
2
+ /**
3
+ * Version types
4
+ */
5
+ export declare enum VersioningType {
6
+ URI = "uri",
7
+ HEADER = "header",
8
+ MEDIA_TYPE = "media-type",
9
+ CUSTOM = "custom"
10
+ }
11
+ /**
12
+ * Versioning options
13
+ */
14
+ export interface VersioningOptions {
15
+ type: VersioningType;
16
+ header?: string;
17
+ key?: string;
18
+ extractor?: (request: unknown) => string | undefined;
19
+ }
20
+ /**
21
+ * Version decorator for controllers and routes
22
+ */
23
+ export declare function Version(version: string | string[]): MethodDecorator & ClassDecorator;
24
+ /**
25
+ * Get version metadata from a class or method
26
+ */
27
+ export declare function getVersionMetadata(target: object | (new (...args: unknown[]) => object), propertyKey?: string | symbol): string[] | undefined;
28
+ /**
29
+ * Check if a version matches the requested version
30
+ */
31
+ export declare function matchVersion(routeVersions: string[] | undefined, requestedVersion: string | undefined, _options?: VersioningOptions): boolean;
32
+ /**
33
+ * Extract version from request based on versioning type
34
+ */
35
+ export declare function extractVersion(request: unknown, options: VersioningOptions): string | undefined;
36
+ //# sourceMappingURL=version.decorator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.decorator.d.ts","sourceRoot":"","sources":["../../src/routing/version.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B;;GAEG;AACH,oBAAY,cAAc;IACxB,GAAG,QAAQ;IACX,MAAM,WAAW;IACjB,UAAU,eAAe;IACzB,MAAM,WAAW;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAC;CACtD;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,eAAe,GAAG,cAAc,CAmBpF;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,GAAG,CAAC,KAAK,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,MAAM,CAAC,EACrD,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,GAC5B,MAAM,EAAE,GAAG,SAAS,CAKtB;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,aAAa,EAAE,MAAM,EAAE,GAAG,SAAS,EACnC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,QAAQ,CAAC,EAAE,iBAAiB,GAC3B,OAAO,CAaT;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CA6B/F"}