@hazeljs/core 0.2.0-alpha.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 (195) hide show
  1. package/LICENSE +192 -0
  2. package/README.md +560 -0
  3. package/dist/__tests__/container.test.d.ts +2 -0
  4. package/dist/__tests__/container.test.d.ts.map +1 -0
  5. package/dist/__tests__/container.test.js +454 -0
  6. package/dist/__tests__/decorators.test.d.ts +2 -0
  7. package/dist/__tests__/decorators.test.d.ts.map +1 -0
  8. package/dist/__tests__/decorators.test.js +1237 -0
  9. package/dist/__tests__/errors/http.error.test.d.ts +2 -0
  10. package/dist/__tests__/errors/http.error.test.d.ts.map +1 -0
  11. package/dist/__tests__/errors/http.error.test.js +117 -0
  12. package/dist/__tests__/filters/exception-filter.test.d.ts +2 -0
  13. package/dist/__tests__/filters/exception-filter.test.d.ts.map +1 -0
  14. package/dist/__tests__/filters/exception-filter.test.js +135 -0
  15. package/dist/__tests__/filters/http-exception.filter.test.d.ts +2 -0
  16. package/dist/__tests__/filters/http-exception.filter.test.d.ts.map +1 -0
  17. package/dist/__tests__/filters/http-exception.filter.test.js +119 -0
  18. package/dist/__tests__/hazel-app.test.d.ts +2 -0
  19. package/dist/__tests__/hazel-app.test.d.ts.map +1 -0
  20. package/dist/__tests__/hazel-app.test.js +810 -0
  21. package/dist/__tests__/hazel-module.test.d.ts +2 -0
  22. package/dist/__tests__/hazel-module.test.d.ts.map +1 -0
  23. package/dist/__tests__/hazel-module.test.js +408 -0
  24. package/dist/__tests__/hazel-response.test.d.ts +2 -0
  25. package/dist/__tests__/hazel-response.test.d.ts.map +1 -0
  26. package/dist/__tests__/hazel-response.test.js +138 -0
  27. package/dist/__tests__/health.test.d.ts +2 -0
  28. package/dist/__tests__/health.test.d.ts.map +1 -0
  29. package/dist/__tests__/health.test.js +147 -0
  30. package/dist/__tests__/index.test.d.ts +2 -0
  31. package/dist/__tests__/index.test.d.ts.map +1 -0
  32. package/dist/__tests__/index.test.js +239 -0
  33. package/dist/__tests__/interceptors/interceptor.test.d.ts +2 -0
  34. package/dist/__tests__/interceptors/interceptor.test.d.ts.map +1 -0
  35. package/dist/__tests__/interceptors/interceptor.test.js +166 -0
  36. package/dist/__tests__/logger.test.d.ts +2 -0
  37. package/dist/__tests__/logger.test.d.ts.map +1 -0
  38. package/dist/__tests__/logger.test.js +141 -0
  39. package/dist/__tests__/middleware/cors.test.d.ts +2 -0
  40. package/dist/__tests__/middleware/cors.test.d.ts.map +1 -0
  41. package/dist/__tests__/middleware/cors.test.js +129 -0
  42. package/dist/__tests__/middleware/csrf.test.d.ts +2 -0
  43. package/dist/__tests__/middleware/csrf.test.d.ts.map +1 -0
  44. package/dist/__tests__/middleware/csrf.test.js +247 -0
  45. package/dist/__tests__/middleware/global-middleware.test.d.ts +2 -0
  46. package/dist/__tests__/middleware/global-middleware.test.d.ts.map +1 -0
  47. package/dist/__tests__/middleware/global-middleware.test.js +259 -0
  48. package/dist/__tests__/middleware/rate-limit.test.d.ts +2 -0
  49. package/dist/__tests__/middleware/rate-limit.test.d.ts.map +1 -0
  50. package/dist/__tests__/middleware/rate-limit.test.js +264 -0
  51. package/dist/__tests__/middleware/security-headers.test.d.ts +2 -0
  52. package/dist/__tests__/middleware/security-headers.test.d.ts.map +1 -0
  53. package/dist/__tests__/middleware/security-headers.test.js +229 -0
  54. package/dist/__tests__/middleware/timeout.test.d.ts +2 -0
  55. package/dist/__tests__/middleware/timeout.test.d.ts.map +1 -0
  56. package/dist/__tests__/middleware/timeout.test.js +132 -0
  57. package/dist/__tests__/middleware.test.d.ts +2 -0
  58. package/dist/__tests__/middleware.test.d.ts.map +1 -0
  59. package/dist/__tests__/middleware.test.js +180 -0
  60. package/dist/__tests__/pipes/pipe.test.d.ts +2 -0
  61. package/dist/__tests__/pipes/pipe.test.d.ts.map +1 -0
  62. package/dist/__tests__/pipes/pipe.test.js +245 -0
  63. package/dist/__tests__/pipes/validation.pipe.test.d.ts +2 -0
  64. package/dist/__tests__/pipes/validation.pipe.test.d.ts.map +1 -0
  65. package/dist/__tests__/pipes/validation.pipe.test.js +297 -0
  66. package/dist/__tests__/request-parser.test.d.ts +2 -0
  67. package/dist/__tests__/request-parser.test.d.ts.map +1 -0
  68. package/dist/__tests__/request-parser.test.js +182 -0
  69. package/dist/__tests__/router.test.d.ts +2 -0
  70. package/dist/__tests__/router.test.d.ts.map +1 -0
  71. package/dist/__tests__/router.test.js +1183 -0
  72. package/dist/__tests__/routing/route-matcher.test.d.ts +2 -0
  73. package/dist/__tests__/routing/route-matcher.test.d.ts.map +1 -0
  74. package/dist/__tests__/routing/route-matcher.test.js +219 -0
  75. package/dist/__tests__/routing/version.decorator.test.d.ts +2 -0
  76. package/dist/__tests__/routing/version.decorator.test.d.ts.map +1 -0
  77. package/dist/__tests__/routing/version.decorator.test.js +298 -0
  78. package/dist/__tests__/service.test.d.ts +2 -0
  79. package/dist/__tests__/service.test.d.ts.map +1 -0
  80. package/dist/__tests__/service.test.js +121 -0
  81. package/dist/__tests__/shutdown.test.d.ts +2 -0
  82. package/dist/__tests__/shutdown.test.d.ts.map +1 -0
  83. package/dist/__tests__/shutdown.test.js +250 -0
  84. package/dist/__tests__/testing/testing.module.test.d.ts +2 -0
  85. package/dist/__tests__/testing/testing.module.test.d.ts.map +1 -0
  86. package/dist/__tests__/testing/testing.module.test.js +370 -0
  87. package/dist/__tests__/upload/file-upload.test.d.ts +2 -0
  88. package/dist/__tests__/upload/file-upload.test.d.ts.map +1 -0
  89. package/dist/__tests__/upload/file-upload.test.js +498 -0
  90. package/dist/__tests__/utils/sanitize.test.d.ts +2 -0
  91. package/dist/__tests__/utils/sanitize.test.d.ts.map +1 -0
  92. package/dist/__tests__/utils/sanitize.test.js +291 -0
  93. package/dist/__tests__/validator.test.d.ts +2 -0
  94. package/dist/__tests__/validator.test.d.ts.map +1 -0
  95. package/dist/__tests__/validator.test.js +300 -0
  96. package/dist/container.d.ts +80 -0
  97. package/dist/container.d.ts.map +1 -0
  98. package/dist/container.js +271 -0
  99. package/dist/decorators.d.ts +166 -0
  100. package/dist/decorators.d.ts.map +1 -0
  101. package/dist/decorators.js +538 -0
  102. package/dist/errors/http.error.d.ts +34 -0
  103. package/dist/errors/http.error.d.ts.map +1 -0
  104. package/dist/errors/http.error.js +69 -0
  105. package/dist/filters/exception-filter.d.ts +39 -0
  106. package/dist/filters/exception-filter.d.ts.map +1 -0
  107. package/dist/filters/exception-filter.js +38 -0
  108. package/dist/filters/http-exception.filter.d.ts +9 -0
  109. package/dist/filters/http-exception.filter.d.ts.map +1 -0
  110. package/dist/filters/http-exception.filter.js +42 -0
  111. package/dist/hazel-app.d.ts +94 -0
  112. package/dist/hazel-app.d.ts.map +1 -0
  113. package/dist/hazel-app.js +516 -0
  114. package/dist/hazel-module.d.ts +29 -0
  115. package/dist/hazel-module.d.ts.map +1 -0
  116. package/dist/hazel-module.js +137 -0
  117. package/dist/hazel-response.d.ts +25 -0
  118. package/dist/hazel-response.d.ts.map +1 -0
  119. package/dist/hazel-response.js +89 -0
  120. package/dist/health.d.ts +73 -0
  121. package/dist/health.d.ts.map +1 -0
  122. package/dist/health.js +174 -0
  123. package/dist/index.d.ts +41 -0
  124. package/dist/index.d.ts.map +1 -0
  125. package/dist/index.js +159 -0
  126. package/dist/interceptors/interceptor.d.ts +30 -0
  127. package/dist/interceptors/interceptor.d.ts.map +1 -0
  128. package/dist/interceptors/interceptor.js +71 -0
  129. package/dist/logger.d.ts +8 -0
  130. package/dist/logger.d.ts.map +1 -0
  131. package/dist/logger.js +261 -0
  132. package/dist/middleware/cors.middleware.d.ts +44 -0
  133. package/dist/middleware/cors.middleware.d.ts.map +1 -0
  134. package/dist/middleware/cors.middleware.js +118 -0
  135. package/dist/middleware/csrf.middleware.d.ts +82 -0
  136. package/dist/middleware/csrf.middleware.d.ts.map +1 -0
  137. package/dist/middleware/csrf.middleware.js +183 -0
  138. package/dist/middleware/global-middleware.d.ts +111 -0
  139. package/dist/middleware/global-middleware.d.ts.map +1 -0
  140. package/dist/middleware/global-middleware.js +179 -0
  141. package/dist/middleware/rate-limit.middleware.d.ts +73 -0
  142. package/dist/middleware/rate-limit.middleware.d.ts.map +1 -0
  143. package/dist/middleware/rate-limit.middleware.js +124 -0
  144. package/dist/middleware/security-headers.middleware.d.ts +76 -0
  145. package/dist/middleware/security-headers.middleware.d.ts.map +1 -0
  146. package/dist/middleware/security-headers.middleware.js +123 -0
  147. package/dist/middleware/timeout.middleware.d.ts +25 -0
  148. package/dist/middleware/timeout.middleware.d.ts.map +1 -0
  149. package/dist/middleware/timeout.middleware.js +74 -0
  150. package/dist/middleware.d.ts +13 -0
  151. package/dist/middleware.d.ts.map +1 -0
  152. package/dist/middleware.js +47 -0
  153. package/dist/pipes/pipe.d.ts +50 -0
  154. package/dist/pipes/pipe.d.ts.map +1 -0
  155. package/dist/pipes/pipe.js +96 -0
  156. package/dist/pipes/validation.pipe.d.ts +6 -0
  157. package/dist/pipes/validation.pipe.d.ts.map +1 -0
  158. package/dist/pipes/validation.pipe.js +61 -0
  159. package/dist/request-context.d.ts +22 -0
  160. package/dist/request-context.d.ts.map +1 -0
  161. package/dist/request-context.js +2 -0
  162. package/dist/request-parser.d.ts +7 -0
  163. package/dist/request-parser.d.ts.map +1 -0
  164. package/dist/request-parser.js +60 -0
  165. package/dist/router.d.ts +33 -0
  166. package/dist/router.d.ts.map +1 -0
  167. package/dist/router.js +506 -0
  168. package/dist/routing/route-matcher.d.ts +39 -0
  169. package/dist/routing/route-matcher.d.ts.map +1 -0
  170. package/dist/routing/route-matcher.js +93 -0
  171. package/dist/routing/version.decorator.d.ts +36 -0
  172. package/dist/routing/version.decorator.d.ts.map +1 -0
  173. package/dist/routing/version.decorator.js +89 -0
  174. package/dist/service.d.ts +9 -0
  175. package/dist/service.d.ts.map +1 -0
  176. package/dist/service.js +39 -0
  177. package/dist/shutdown.d.ts +32 -0
  178. package/dist/shutdown.d.ts.map +1 -0
  179. package/dist/shutdown.js +109 -0
  180. package/dist/testing/testing.module.d.ts +83 -0
  181. package/dist/testing/testing.module.d.ts.map +1 -0
  182. package/dist/testing/testing.module.js +164 -0
  183. package/dist/types.d.ts +82 -0
  184. package/dist/types.d.ts.map +1 -0
  185. package/dist/types.js +2 -0
  186. package/dist/upload/file-upload.d.ts +75 -0
  187. package/dist/upload/file-upload.d.ts.map +1 -0
  188. package/dist/upload/file-upload.js +261 -0
  189. package/dist/utils/sanitize.d.ts +45 -0
  190. package/dist/utils/sanitize.d.ts.map +1 -0
  191. package/dist/utils/sanitize.js +165 -0
  192. package/dist/validator.d.ts +7 -0
  193. package/dist/validator.d.ts.map +1 -0
  194. package/dist/validator.js +119 -0
  195. package/package.json +67 -0
package/dist/router.js ADDED
@@ -0,0 +1,506 @@
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
+ const TIMEOUT_METADATA_KEY = 'hazel:timeout';
21
+ const OPTIONAL_INDICES_METADATA_KEY = 'hazel:optional-indices';
22
+ class Router {
23
+ constructor(container) {
24
+ this.container = container;
25
+ this.routes = new Map();
26
+ this.routesByMethod = new Map();
27
+ this.middlewareHandler = new middleware_1.MiddlewareHandler(container);
28
+ // Initialize route cache for common HTTP methods
29
+ this.routesByMethod.set('GET', new Map());
30
+ this.routesByMethod.set('POST', new Map());
31
+ this.routesByMethod.set('PUT', new Map());
32
+ this.routesByMethod.set('DELETE', new Map());
33
+ this.routesByMethod.set('PATCH', new Map());
34
+ }
35
+ registerController(controller) {
36
+ logger_1.default.debug(`Registering controller: ${controller.name}`);
37
+ const controllerMetadata = Reflect.getMetadata(CONTROLLER_METADATA_KEY, controller) || {};
38
+ const routes = Reflect.getMetadata(ROUTE_METADATA_KEY, controller) || [];
39
+ logger_1.default.debug('Controller metadata:', controllerMetadata);
40
+ logger_1.default.debug('Found routes:', routes);
41
+ routes.forEach((route) => {
42
+ const { method, path, propertyKey } = route;
43
+ const basePath = controllerMetadata.path || '';
44
+ const routePath = path || '';
45
+ const fullPath = this.normalizePath(`${basePath}${routePath}`);
46
+ logger_1.default.debug(`Registering route: ${method} ${fullPath} (handler: ${String(propertyKey)})`);
47
+ // Get parameter types from TypeScript metadata
48
+ const paramTypes = Reflect.getMetadata('design:paramtypes', controller.prototype, propertyKey) || [];
49
+ logger_1.default.debug('Parameter types:', paramTypes.map((t) => t?.name || 'undefined'));
50
+ // Get parameter injections
51
+ const injections = Reflect.getMetadata('hazel:inject', controller, propertyKey) || [];
52
+ logger_1.default.debug('Parameter injections:', injections);
53
+ // Create a route handler for this specific route
54
+ // Pass the controller class, not instance, so it can be resolved per-request
55
+ const handler = this.createRouteHandler(controller, propertyKey);
56
+ const methodUpper = method.toUpperCase();
57
+ const routeKey = `${methodUpper} ${fullPath}`;
58
+ // Store in both maps for compatibility
59
+ this.routes.set(routeKey, [handler]);
60
+ // Store in method-specific cache for faster lookup
61
+ let methodRoutes = this.routesByMethod.get(methodUpper);
62
+ if (!methodRoutes) {
63
+ methodRoutes = new Map();
64
+ this.routesByMethod.set(methodUpper, methodRoutes);
65
+ }
66
+ methodRoutes.set(fullPath, [handler]);
67
+ // Log the route pattern for debugging
68
+ const pattern = this.createRoutePattern(fullPath);
69
+ logger_1.default.debug('Route pattern:', {
70
+ path: fullPath,
71
+ pattern: pattern.toString(),
72
+ pipes: route.pipes?.map((p) => p.type.name) || [],
73
+ paramTypes: paramTypes.map((t) => t?.name || 'undefined'),
74
+ });
75
+ });
76
+ logger_1.default.debug('All registered routes:');
77
+ this.routes.forEach((handlers, route) => {
78
+ logger_1.default.debug(`${route}`);
79
+ });
80
+ }
81
+ async applyPipes(value, pipes, context) {
82
+ let result = value;
83
+ // Get route-specific pipes
84
+ const allPipes = pipes.map((p) => this.container.resolve(p.type));
85
+ for (const pipe of allPipes) {
86
+ try {
87
+ result = await pipe.transform(result, context);
88
+ }
89
+ catch (error) {
90
+ logger_1.default.error('Validation error:', error);
91
+ // Re-throw the error to be caught by the route handler
92
+ throw error;
93
+ }
94
+ }
95
+ return result;
96
+ }
97
+ async applyInterceptors(interceptors, context, next) {
98
+ if (interceptors.length === 0) {
99
+ return next();
100
+ }
101
+ const [current, ...remaining] = interceptors;
102
+ const interceptor = this.container.resolve(current.type);
103
+ return interceptor.intercept(context, () => this.applyInterceptors(remaining, context, next));
104
+ }
105
+ createRouteHandler(controllerClass, methodName) {
106
+ return async (req, res, matchedContext) => {
107
+ // Generate a unique request ID for request-scoped providers
108
+ const requestId = `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
109
+ try {
110
+ logger_1.default.debug('=== Request Handler Start ===');
111
+ logger_1.default.debug(`Method: ${req.method}, URL: ${req.url}`);
112
+ logger_1.default.debug('Matched context:', matchedContext);
113
+ // Optimize: Only serialize for debug logging
114
+ if (logger_1.default.isDebugEnabled && logger_1.default.isDebugEnabled()) {
115
+ const safeBody = JSON.parse(JSON.stringify(req.body || {}));
116
+ logger_1.default.debug(`Request body: ${JSON.stringify(safeBody, null, 2)}`);
117
+ }
118
+ // Resolve controller instance per-request with requestId
119
+ const controller = this.container.resolve(controllerClass, requestId);
120
+ const routes = Reflect.getMetadata(ROUTE_METADATA_KEY, controllerClass) || [];
121
+ const route = routes.find((r) => r.propertyKey === methodName);
122
+ if (!route) {
123
+ throw new Error(`Route not found for method ${String(methodName)}`);
124
+ }
125
+ // Get parameter injections
126
+ const injections = Reflect.getMetadata('hazel:inject', controllerClass, methodName) || [];
127
+ // Use the matched context if available, otherwise create a new one
128
+ const context = matchedContext || {
129
+ params: req.params || {},
130
+ query: req.query || {},
131
+ body: req.body || {},
132
+ headers: Object.fromEntries(Object.entries(req.headers || {})
133
+ .filter(([key]) => !key.toLowerCase().includes('authorization'))
134
+ .map(([key, value]) => [key, String(value)])),
135
+ method: req.method || 'GET',
136
+ url: req.url || '/',
137
+ };
138
+ context.retryOptions = Reflect.getMetadata('hazel:retry', controllerClass.prototype, methodName);
139
+ // Execute guards (class-level + method-level)
140
+ const classGuards = Reflect.getMetadata('hazel:guards', controllerClass) || [];
141
+ const methodGuards = Reflect.getMetadata('hazel:guards', controllerClass.prototype, methodName) || [];
142
+ const allGuards = [...classGuards, ...methodGuards];
143
+ if (allGuards.length > 0) {
144
+ const executionContext = {
145
+ switchToHttp: () => ({
146
+ getRequest: () => req,
147
+ getResponse: () => res,
148
+ getContext: () => context,
149
+ }),
150
+ };
151
+ for (const guardType of allGuards) {
152
+ const guard = this.container.resolve(guardType);
153
+ const result = await guard.canActivate(executionContext);
154
+ if (!result) {
155
+ throw new http_error_1.UnauthorizedError('Unauthorized');
156
+ }
157
+ }
158
+ // Propagate user set by guard to context
159
+ if (req.user) {
160
+ context.user = req.user;
161
+ }
162
+ }
163
+ // Set DTO type from the first parameter that has a DTO type
164
+ for (const injection of injections) {
165
+ if (injection?.dtoType) {
166
+ context.dtoType = injection.dtoType;
167
+ break;
168
+ }
169
+ }
170
+ // Get pipes for this route
171
+ const routePipes = route.pipes || [];
172
+ // Prepare arguments for the controller method
173
+ const args = [];
174
+ for (let i = 0; i < injections.length; i++) {
175
+ const injection = injections[i];
176
+ if (typeof injection === 'string') {
177
+ // Handle @Body, @Param, @Query decorators
178
+ if (injection === 'body') {
179
+ args[i] = context.body;
180
+ }
181
+ else if (injection === 'param') {
182
+ args[i] = context.params;
183
+ }
184
+ else if (injection === 'query') {
185
+ args[i] = context.query;
186
+ }
187
+ else if (injection === 'headers') {
188
+ args[i] = context.headers;
189
+ }
190
+ }
191
+ else if (typeof injection === 'object' && injection !== null) {
192
+ if (injection.type === 'headers') {
193
+ // Handle @Headers decorator
194
+ const headerName = injection.name;
195
+ if (headerName) {
196
+ args[i] = context.headers[headerName.toLowerCase()];
197
+ }
198
+ else {
199
+ args[i] = context.headers;
200
+ }
201
+ }
202
+ else if (injection.type === 'request') {
203
+ // Handle @Req() / @Request() decorator - raw request for multipart, etc.
204
+ args[i] = req;
205
+ }
206
+ else if (injection.type === 'response') {
207
+ // Handle @Res decorator
208
+ args[i] = new hazel_response_1.HazelExpressResponse(res);
209
+ }
210
+ else if (injection.type === 'body') {
211
+ // Handle @Body decorator with DTO type
212
+ if (injection.dtoType) {
213
+ context.dtoType = injection.dtoType;
214
+ logger_1.default.debug('Setting DTO type in context:', injection.dtoType.name);
215
+ }
216
+ args[i] = context.body;
217
+ }
218
+ else if (injection.type === 'param') {
219
+ // Handle @Param decorator with pipe
220
+ const paramName = injection.name;
221
+ const paramValue = matchedContext?.params[paramName] || context.params[paramName];
222
+ if (injection.pipe) {
223
+ const pipe = this.container.resolve(injection.pipe);
224
+ args[i] = await pipe.transform(paramValue, context);
225
+ }
226
+ else {
227
+ args[i] = paramValue;
228
+ }
229
+ }
230
+ else if (injection.type === 'query') {
231
+ // Handle @Query decorator with pipe
232
+ const paramName = injection.name;
233
+ const queryValue = paramName ? context.query[paramName] : context.query;
234
+ if (injection.pipe) {
235
+ const pipe = this.container.resolve(injection.pipe);
236
+ args[i] = await pipe.transform(queryValue, context);
237
+ }
238
+ else {
239
+ args[i] = queryValue;
240
+ }
241
+ }
242
+ else if (injection.type === 'user') {
243
+ // Handle @CurrentUser() decorator — reads from context.user (set by a guard)
244
+ const user = context.user ?? req.user;
245
+ args[i] = injection.field ? user?.[injection.field] : user;
246
+ }
247
+ else if (injection.type === 'ip') {
248
+ const r = req;
249
+ const forwarded = r.headers?.['x-forwarded-for'];
250
+ const ip = typeof forwarded === 'string'
251
+ ? forwarded.split(',')[0].trim()
252
+ : Array.isArray(forwarded)
253
+ ? forwarded[0]?.trim()
254
+ : r.socket?.remoteAddress;
255
+ args[i] = ip ?? undefined;
256
+ }
257
+ else if (injection.type === 'host') {
258
+ const host = req.headers?.['host'];
259
+ args[i] = typeof host === 'string' ? host : Array.isArray(host) ? host[0] : undefined;
260
+ }
261
+ else if (injection.type === 'session') {
262
+ args[i] = req.session;
263
+ }
264
+ else if (injection.type === 'custom' && typeof injection.resolve === 'function') {
265
+ // Handle custom parameter decorators (e.g. @Ability() from @hazeljs/casl).
266
+ // The decorator stores a resolver function; call it with request, context, container.
267
+ args[i] = await injection.resolve(req, context, this.container);
268
+ }
269
+ }
270
+ }
271
+ const optionalIndices = Reflect.getMetadata(OPTIONAL_INDICES_METADATA_KEY, controllerClass, methodName) || [];
272
+ for (const i of optionalIndices) {
273
+ if (i < args.length && (args[i] === undefined || args[i] === null)) {
274
+ args[i] = undefined;
275
+ }
276
+ }
277
+ // Auto-inject RequestContext for undecorated parameters
278
+ const paramTypes = Reflect.getMetadata('design:paramtypes', controllerClass.prototype, methodName) || [];
279
+ for (let i = 0; i < paramTypes.length; i++) {
280
+ if (args[i] === undefined && !injections[i]) {
281
+ args[i] = context;
282
+ }
283
+ }
284
+ // Apply ValidationPipe to the body if a DTO type is present
285
+ if (context.body && context.dtoType) {
286
+ logger_1.default.debug('Applying ValidationPipe with DTO type:', context.dtoType.name);
287
+ const validationPipe = this.container.resolve(validation_pipe_1.ValidationPipe);
288
+ context.body = await validationPipe.transform(context.body, context);
289
+ args[injections.findIndex((i) => typeof i === 'object' && i !== null && i.type === 'body')] = context.body;
290
+ }
291
+ // Apply other pipes to the body if it exists
292
+ if (context.body) {
293
+ context.body = await this.applyPipes(context.body, routePipes, context);
294
+ }
295
+ // Get the controller method
296
+ const method = controller[methodName];
297
+ const timeoutMs = Reflect.getMetadata(TIMEOUT_METADATA_KEY, controllerClass.prototype, methodName);
298
+ let handlerPromise = this.applyInterceptors(Reflect.getMetadata('hazel:interceptors', controllerClass, methodName) || [], context, async () => {
299
+ return method.apply(controller, args);
300
+ });
301
+ if (timeoutMs != null && timeoutMs > 0) {
302
+ handlerPromise = Promise.race([
303
+ handlerPromise,
304
+ new Promise((_, reject) => {
305
+ setTimeout(() => reject(new http_error_1.RequestTimeoutError(`Request timed out after ${timeoutMs}ms`)), timeoutMs);
306
+ }),
307
+ ]);
308
+ }
309
+ const result = await handlerPromise;
310
+ // Apply @Redirect metadata
311
+ const redirectMeta = Reflect.getMetadata(REDIRECT_METADATA_KEY, controllerClass.prototype, methodName);
312
+ if (redirectMeta) {
313
+ res.status(redirectMeta.statusCode).setHeader('Location', redirectMeta.url);
314
+ res.end();
315
+ return;
316
+ }
317
+ // Apply @Header metadata (response headers)
318
+ const headersMeta = Reflect.getMetadata(HEADER_METADATA_KEY, controllerClass.prototype, methodName);
319
+ if (headersMeta) {
320
+ for (const h of headersMeta) {
321
+ res.setHeader(h.name, h.value);
322
+ }
323
+ }
324
+ // Apply @HttpCode metadata
325
+ const httpCode = Reflect.getMetadata(HTTP_CODE_METADATA_KEY, controllerClass.prototype, methodName);
326
+ // Handle the response
327
+ if (result !== undefined) {
328
+ if (httpCode) {
329
+ res.status(httpCode);
330
+ }
331
+ if (typeof result === 'string' && result.trim().startsWith('<!DOCTYPE html>')) {
332
+ // Handle HTML response
333
+ res.setHeader('Content-Type', 'text/html');
334
+ res.send(result);
335
+ }
336
+ else {
337
+ // Handle JSON response
338
+ res.json(result);
339
+ }
340
+ }
341
+ else if (httpCode) {
342
+ res.status(httpCode).end();
343
+ }
344
+ }
345
+ catch (error) {
346
+ logger_1.default.error('Request handler error:', error);
347
+ if (error instanceof pipe_1.ValidationError) {
348
+ const errorResponse = error.toJSON();
349
+ logger_1.default.error('Validation error response:', errorResponse);
350
+ res.status(400).json({
351
+ statusCode: 400,
352
+ message: errorResponse.message,
353
+ errors: errorResponse.errors,
354
+ });
355
+ }
356
+ else if (error instanceof http_error_1.HttpError) {
357
+ res.status(error.statusCode).json({
358
+ statusCode: error.statusCode,
359
+ message: error.message,
360
+ });
361
+ }
362
+ else {
363
+ // Log unhandled errors with full context
364
+ logger_1.default.error('Unhandled error:', {
365
+ error: error instanceof Error ? error.message : String(error),
366
+ stack: error instanceof Error ? error.stack : undefined,
367
+ requestId,
368
+ method: req.method,
369
+ url: req.url,
370
+ });
371
+ res.status(500).json({
372
+ statusCode: 500,
373
+ message: process.env.NODE_ENV === 'production'
374
+ ? 'Internal server error'
375
+ : error instanceof Error ? error.message : 'Unknown error',
376
+ });
377
+ }
378
+ }
379
+ finally {
380
+ // CRITICAL: Clean up request-scoped providers to prevent memory leaks
381
+ this.container.clearRequestScope(requestId);
382
+ }
383
+ };
384
+ }
385
+ extractParams(path, routePath) {
386
+ const params = {};
387
+ const pathParts = path.split('/');
388
+ const routeParts = routePath.split('/');
389
+ for (let i = 0; i < routeParts.length; i++) {
390
+ if (routeParts[i].startsWith(':')) {
391
+ const paramName = routeParts[i].slice(1);
392
+ params[paramName] = pathParts[i];
393
+ logger_1.default.debug(`Extracted param ${paramName}:`, pathParts[i]);
394
+ }
395
+ }
396
+ logger_1.default.debug('Extracted params:', params);
397
+ return params;
398
+ }
399
+ matchPath(path, routePath) {
400
+ const pathParts = path.split('/');
401
+ const routeParts = routePath.split('/');
402
+ if (pathParts.length !== routeParts.length) {
403
+ return false;
404
+ }
405
+ for (let i = 0; i < routeParts.length; i++) {
406
+ if (!routeParts[i].startsWith(':') && routeParts[i] !== pathParts[i]) {
407
+ return false;
408
+ }
409
+ }
410
+ return true;
411
+ }
412
+ createRoutePattern(path) {
413
+ if (path === '/') {
414
+ return /^\/?$/;
415
+ }
416
+ // Escape forward slashes and convert :param to ([^/]+)
417
+ const pattern = path
418
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape special regex characters
419
+ .replace(/:[^/]+/g, '([^/]+)');
420
+ return new RegExp(`^${pattern}/?$`);
421
+ }
422
+ normalizePath(path) {
423
+ if (path.length > 1 && path.endsWith('/')) {
424
+ path = path.slice(0, -1);
425
+ }
426
+ return path.startsWith('/') ? path : `/${path}`;
427
+ }
428
+ async match(method, url, context) {
429
+ const path = this.normalizePath(url.split('?')[0] || '/');
430
+ logger_1.default.debug(`Matching route: ${method} ${path}`);
431
+ // Use method-specific cache for O(1) method lookup instead of O(n)
432
+ const methodRoutes = this.routesByMethod.get(method);
433
+ if (!methodRoutes) {
434
+ logger_1.default.debug(`No routes registered for method: ${method}`);
435
+ return null;
436
+ }
437
+ // Now only iterate through routes for this specific HTTP method
438
+ for (const [routePath, handlers] of methodRoutes.entries()) {
439
+ if (this.matchPath(path, routePath)) {
440
+ const params = this.extractParams(path, routePath);
441
+ logger_1.default.debug('Matched route:', { method, url, params });
442
+ // Create a new context with the matched parameters
443
+ const matchedContext = {
444
+ ...context,
445
+ params: params, // Don't merge, just use the extracted params
446
+ };
447
+ logger_1.default.debug('Created matched context:', matchedContext);
448
+ // Create a new handler that ensures the context is passed
449
+ const handler = handlers[0];
450
+ const wrappedHandler = async (req, res, ctx) => {
451
+ logger_1.default.debug('Wrapped handler called with context:', ctx);
452
+ await handler(req, res, matchedContext);
453
+ };
454
+ return { handler: wrappedHandler, context: matchedContext };
455
+ }
456
+ }
457
+ return null;
458
+ }
459
+ get(path, handlers) {
460
+ this.addRoute('GET', path, handlers);
461
+ }
462
+ post(path, handlers) {
463
+ this.addRoute('POST', path, handlers);
464
+ }
465
+ put(path, handlers) {
466
+ this.addRoute('PUT', path, handlers);
467
+ }
468
+ delete(path, handlers) {
469
+ this.addRoute('DELETE', path, handlers);
470
+ }
471
+ addRoute(method, path, handlers) {
472
+ const normalizedPath = this.normalizePath(path);
473
+ this.routes.set(`${method.toUpperCase()} ${normalizedPath}`, handlers);
474
+ }
475
+ async handleRequest(req, res) {
476
+ try {
477
+ logger_1.default.debug('=== Request Handler Start ===');
478
+ logger_1.default.debug(`Method: ${req.method}, URL: ${req.url}`);
479
+ // Parse the request to get the initial context
480
+ const context = await request_parser_1.RequestParser.parseRequest(req);
481
+ logger_1.default.debug('Initial context:', context);
482
+ // Match the request method and URL
483
+ const match = await this.match(req.method || 'GET', req.url || '/', context);
484
+ if (!match) {
485
+ logger_1.default.debug(`No route found for ${req.method} ${req.url}`);
486
+ res.status(404).json({ error: 'Not Found' });
487
+ return;
488
+ }
489
+ // Log the matched context before passing it
490
+ logger_1.default.debug('Matched context before handler:', match.context);
491
+ // Update request params with matched parameters
492
+ req.params = match.context.params;
493
+ logger_1.default.debug('Updated request params:', req.params);
494
+ // Call the matched handler with the matched context
495
+ const handler = match.handler;
496
+ await handler(req, res, match.context);
497
+ }
498
+ catch (error) {
499
+ logger_1.default.error('Error handling request:', error);
500
+ res
501
+ .status(500)
502
+ .json({ error: error instanceof Error ? error.message : 'Internal Server Error' });
503
+ }
504
+ }
505
+ }
506
+ 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