fragment-ts 1.0.30 → 1.0.32

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 (52) hide show
  1. package/API.md +752 -0
  2. package/DOCS.md +555 -0
  3. package/README.md +76 -339
  4. package/USAGE.md +309 -1306
  5. package/dist/cli/commands/init.command.js +1 -1
  6. package/dist/core/container/di-container.d.ts.map +1 -1
  7. package/dist/core/container/di-container.js +62 -106
  8. package/dist/core/container/di-container.js.map +1 -1
  9. package/dist/core/decorators/exception-filter.decorator.d.ts +5 -0
  10. package/dist/core/decorators/exception-filter.decorator.d.ts.map +1 -0
  11. package/dist/core/decorators/exception-filter.decorator.js +23 -0
  12. package/dist/core/decorators/exception-filter.decorator.js.map +1 -0
  13. package/dist/core/decorators/guard.decorator.d.ts +5 -0
  14. package/dist/core/decorators/guard.decorator.d.ts.map +1 -0
  15. package/dist/core/decorators/guard.decorator.js +23 -0
  16. package/dist/core/decorators/guard.decorator.js.map +1 -0
  17. package/dist/core/decorators/injection.decorators.d.ts.map +1 -1
  18. package/dist/core/decorators/injection.decorators.js +5 -0
  19. package/dist/core/decorators/injection.decorators.js.map +1 -1
  20. package/dist/core/decorators/interceptor.decorator.d.ts +5 -0
  21. package/dist/core/decorators/interceptor.decorator.d.ts.map +1 -0
  22. package/dist/core/decorators/interceptor.decorator.js +23 -0
  23. package/dist/core/decorators/interceptor.decorator.js.map +1 -0
  24. package/dist/core/decorators/middleware.decorator.d.ts +8 -0
  25. package/dist/core/decorators/middleware.decorator.d.ts.map +1 -0
  26. package/dist/core/decorators/middleware.decorator.js +28 -0
  27. package/dist/core/decorators/middleware.decorator.js.map +1 -0
  28. package/dist/core/metadata/metadata-keys.d.ts +1 -0
  29. package/dist/core/metadata/metadata-keys.d.ts.map +1 -1
  30. package/dist/core/metadata/metadata-keys.js +1 -0
  31. package/dist/core/metadata/metadata-keys.js.map +1 -1
  32. package/dist/core/metadata/metadata-storage.d.ts +20 -4
  33. package/dist/core/metadata/metadata-storage.d.ts.map +1 -1
  34. package/dist/core/metadata/metadata-storage.js +94 -14
  35. package/dist/core/metadata/metadata-storage.js.map +1 -1
  36. package/dist/web/application.d.ts.map +1 -1
  37. package/dist/web/application.js +79 -10
  38. package/dist/web/application.js.map +1 -1
  39. package/dist/web/interfaces.d.ts +5 -1
  40. package/dist/web/interfaces.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/src/cli/commands/init.command.ts +1 -1
  43. package/src/core/container/di-container.ts +95 -177
  44. package/src/core/decorators/exception-filter.decorator.ts +20 -0
  45. package/src/core/decorators/guard.decorator.ts +20 -0
  46. package/src/core/decorators/injection.decorators.ts +5 -0
  47. package/src/core/decorators/interceptor.decorator.ts +20 -0
  48. package/src/core/decorators/middleware.decorator.ts +25 -0
  49. package/src/core/metadata/metadata-keys.ts +1 -0
  50. package/src/core/metadata/metadata-storage.ts +128 -24
  51. package/src/web/application.ts +207 -53
  52. package/src/web/interfaces.ts +11 -2
@@ -29,11 +29,26 @@ export interface ParamMetadata {
29
29
  }
30
30
 
31
31
  export interface PropertyInjectionMetadata {
32
+ propertyKey: string;
32
33
  type: "autowired" | "inject" | "repository" | "value" | "lazy";
33
34
  key: string;
34
35
  metadata?: any;
35
36
  }
36
37
 
38
+ export interface ExtendedClassMetadata extends ClassMetadata {
39
+ middlewares: any[];
40
+ guards: any[];
41
+ interceptors: any[];
42
+ exceptionFilters: any[];
43
+ }
44
+
45
+ export interface ExtendedMethodMetadata extends MethodMetadata {
46
+ middlewares?: any[];
47
+ guards?: any[];
48
+ interceptors?: any[];
49
+ exceptionFilters?: any[];
50
+ }
51
+
37
52
  export class MetadataStorage {
38
53
  private static instance: MetadataStorage;
39
54
  private classes: Map<any, ClassMetadata> = new Map();
@@ -41,6 +56,8 @@ export class MetadataStorage {
41
56
  private params: Map<string, ParamMetadata[]> = new Map();
42
57
  private propertyInjections: Map<string, PropertyInjectionMetadata[]> =
43
58
  new Map();
59
+ private extendedClasses: Map<any, ExtendedClassMetadata> = new Map();
60
+ private extendedMethods: Map<string, ExtendedMethodMetadata> = new Map();
44
61
 
45
62
  static getInstance(): MetadataStorage {
46
63
  if (!MetadataStorage.instance) {
@@ -102,37 +119,124 @@ export class MetadataStorage {
102
119
  propertyKey: string,
103
120
  injection: PropertyInjectionMetadata,
104
121
  ): void {
105
- const className = target.name || target.constructor?.name;
106
- const key = `${className}.${propertyKey}`;
107
-
108
- if (!this.propertyInjections.has(key)) {
109
- this.propertyInjections.set(key, []);
122
+ if (!this.propertyInjections.has(target)) {
123
+ this.propertyInjections.set(target, []);
110
124
  }
111
125
 
112
- const injections = this.propertyInjections.get(key)!;
126
+ const injections = this.propertyInjections.get(target)!;
113
127
  injections.push(injection);
128
+ }
114
129
 
115
- console.log(
116
- ` šŸŽÆ Registered property injection: ${className}.${propertyKey} [${injection.type}]`,
117
- );
130
+ getPropertyInjections(target: any): PropertyInjectionMetadata[] {
131
+ // Get injections for this specific class
132
+ let injections = this.propertyInjections.get(target) || [];
133
+
134
+ // Also get injections from parent classes
135
+ let parent = Object.getPrototypeOf(target);
136
+ while (parent && parent !== Object && parent !== Function.prototype) {
137
+ const parentInjections = this.propertyInjections.get(parent) || [];
138
+ injections = [...injections, ...parentInjections];
139
+ parent = Object.getPrototypeOf(parent);
140
+ }
141
+
142
+ return injections;
118
143
  }
119
144
 
120
- getPropertyInjections(
121
- target: any,
122
- ): { propertyKey: string; injections: PropertyInjectionMetadata[] }[] {
123
- const className = target.name || target.constructor?.name;
124
- const result: {
125
- propertyKey: string;
126
- injections: PropertyInjectionMetadata[];
127
- }[] = [];
128
-
129
- this.propertyInjections.forEach((injections, key) => {
130
- if (key.startsWith(`${className}.`)) {
131
- const propertyKey = key.split(".")[1];
132
- result.push({ propertyKey, injections });
145
+ addClassMetadata(target: any, type: string, value: any): void {
146
+ let meta = this.extendedClasses.get(target);
147
+
148
+ if (!meta) {
149
+ const base = this.classes.get(target);
150
+ if (!base) {
151
+ // If no base ClassMetadata exists (e.g., decorator used on non-@Controller/@Service class),
152
+ // we cannot safely create ExtendedClassMetadata — skip or warn.
153
+ console.warn(
154
+ `āš ļø @${type} applied to ${target.name}, but it's not registered as a known component (missing @Controller, @Service, etc.). Ignoring.`,
155
+ );
156
+ return;
133
157
  }
134
- });
135
158
 
136
- return result;
159
+ // Clone base and add enhancement arrays
160
+ meta = {
161
+ ...base,
162
+ middlewares: [],
163
+ guards: [],
164
+ interceptors: [],
165
+ exceptionFilters: [],
166
+ };
167
+ }
168
+
169
+ switch (type) {
170
+ case "middleware":
171
+ meta.middlewares.push(value);
172
+ break;
173
+ case "guard":
174
+ meta.guards.push(value);
175
+ break;
176
+ case "interceptor":
177
+ meta.interceptors.push(value);
178
+ break;
179
+ case "exceptionFilter":
180
+ meta.exceptionFilters.push(value);
181
+ break;
182
+ default:
183
+ console.warn(`Unknown metadata type: ${type}`);
184
+ return;
185
+ }
186
+
187
+ this.extendedClasses.set(target, meta);
188
+ }
189
+
190
+ addMethodMetadata(
191
+ target: any,
192
+ propertyKey: string,
193
+ type: string,
194
+ value: any,
195
+ ): void {
196
+ const key = `${target.name}.${propertyKey}`;
197
+ let meta = this.extendedMethods.get(key);
198
+ if (!meta) {
199
+ const base = this.methods.get(key);
200
+ meta = {
201
+ ...(base || {
202
+ target,
203
+ propertyKey,
204
+ method: "",
205
+ path: "",
206
+ paramMetadata: [],
207
+ }),
208
+ middlewares: [],
209
+ guards: [],
210
+ interceptors: [],
211
+ exceptionFilters: [],
212
+ };
213
+ }
214
+ switch (type) {
215
+ case "middleware":
216
+ meta.middlewares!.push(value);
217
+ break;
218
+ case "guard":
219
+ meta.guards!.push(value);
220
+ break;
221
+ case "interceptor":
222
+ meta.interceptors!.push(value);
223
+ break;
224
+ case "exceptionFilter":
225
+ meta.exceptionFilters!.push(value);
226
+ break;
227
+ }
228
+ this.extendedMethods.set(key, meta);
229
+ }
230
+
231
+ getExtendedClass(target: any): ExtendedClassMetadata | undefined {
232
+ return this.extendedClasses.get(target);
233
+ }
234
+
235
+ getExtendedMethod(
236
+ target: any,
237
+ propertyKey: string,
238
+ ): ExtendedMethodMetadata | undefined {
239
+ const key = `${target.name}.${propertyKey}`;
240
+ return this.extendedMethods.get(key);
137
241
  }
138
242
  }
@@ -9,6 +9,16 @@ import { MetadataStorage } from "../core/metadata/metadata-storage";
9
9
  import { METADATA_KEYS } from "../core/metadata/metadata-keys";
10
10
  import { ComponentScanner } from "../core/scanner/component-scanner";
11
11
  import { TypeORMModule } from "../typeorm/typeorm-module";
12
+ import {
13
+ MiddlewareClass,
14
+ GuardClass,
15
+ InterceptorClass,
16
+ ExceptionFilterClass,
17
+ Middleware,
18
+ Guard,
19
+ Interceptor,
20
+ ExceptionFilter,
21
+ } from "./interfaces";
12
22
 
13
23
  export class FragmentWebApplication {
14
24
  private app: Express;
@@ -34,7 +44,7 @@ export class FragmentWebApplication {
34
44
 
35
45
  async bootstrap(appClass: any): Promise<void> {
36
46
  console.log("\nšŸš€ Bootstrapping application");
37
-
47
+
38
48
  try {
39
49
  await TypeORMModule.initialize();
40
50
  console.log("āœ… TypeORM initialized successfully");
@@ -47,7 +57,7 @@ export class FragmentWebApplication {
47
57
  METADATA_KEYS.APPLICATION,
48
58
  appClass,
49
59
  );
50
-
60
+
51
61
  console.log(`šŸŽÆ Application metadata:`, appMetadata);
52
62
 
53
63
  // CRITICAL: Scan and load all component files first
@@ -63,11 +73,13 @@ export class FragmentWebApplication {
63
73
 
64
74
  const port = appMetadata?.port || 3000;
65
75
  const host = appMetadata?.host || "0.0.0.0";
66
-
76
+
67
77
  this.app.use(this.errorHandler.bind(this));
68
-
78
+
69
79
  this.app.listen(port, host, () => {
70
- console.log(`\n✨ Fragment application running on http://${host}:${port}`);
80
+ console.log(
81
+ `\n✨ Fragment application running on http://${host}:${port}`,
82
+ );
71
83
  console.log("========================================\n");
72
84
  });
73
85
  }
@@ -76,15 +88,17 @@ export class FragmentWebApplication {
76
88
  const cwd = process.cwd();
77
89
  const distExists = fs.existsSync(path.join(cwd, "dist"));
78
90
  const srcExists = fs.existsSync(path.join(cwd, "src"));
79
-
91
+
80
92
  console.log(`šŸ“ Current working directory: ${cwd}`);
81
93
  console.log(`šŸ“ dist/ exists: ${distExists}`);
82
94
  console.log(`šŸ“ src/ exists: ${srcExists}`);
83
95
 
84
96
  // Determine if we're running TypeScript directly (dev) or compiled JS (prod)
85
97
  const isDevMode = this.isRunningTypeScript();
86
- console.log(`šŸ’» Running in ${isDevMode ? 'development' : 'production'} mode`);
87
-
98
+ console.log(
99
+ `šŸ’» Running in ${isDevMode ? "development" : "production"} mode`,
100
+ );
101
+
88
102
  if (isDevMode && srcExists) {
89
103
  // Development mode: scan TypeScript source files
90
104
  console.log(" šŸ“ Development mode: scanning TypeScript files");
@@ -103,11 +117,11 @@ export class FragmentWebApplication {
103
117
  if (require.extensions[".ts"]) {
104
118
  return true;
105
119
  }
106
-
120
+
107
121
  // Check if process is running with ts-node or tsx
108
122
  const execPath = process.argv[0];
109
123
  const scriptPath = process.argv[1] || "";
110
-
124
+
111
125
  if (
112
126
  execPath.includes("ts-node") ||
113
127
  execPath.includes("tsx") ||
@@ -116,24 +130,24 @@ export class FragmentWebApplication {
116
130
  ) {
117
131
  return true;
118
132
  }
119
-
133
+
120
134
  // Check if main module has .ts extension
121
135
  if (require.main?.filename.endsWith(".ts")) {
122
136
  return true;
123
137
  }
124
-
138
+
125
139
  // Check NODE_ENV or explicit flag
126
140
  if (process.env.FRAGMENT_DEV_MODE === "true") {
127
141
  return true;
128
142
  }
129
-
143
+
130
144
  return false;
131
145
  }
132
146
 
133
147
  private discoverAndRegisterComponents(): void {
134
148
  const classes = this.metadataStorage.getAllClasses();
135
149
  console.log(`\nšŸ“¦ Discovered ${classes.length} component(s)`);
136
-
150
+
137
151
  // Group by type for display
138
152
  const grouped = classes.reduce(
139
153
  (acc, cls) => {
@@ -143,19 +157,21 @@ export class FragmentWebApplication {
143
157
  },
144
158
  {} as Record<string, any[]>,
145
159
  );
146
-
160
+
147
161
  Object.entries(grouped).forEach(([type, items]) => {
148
162
  const icon = this.getTypeIcon(type);
149
163
  console.log(` ${icon} ${items.length} ${type}(s)`);
150
-
151
- items.forEach(item => {
152
- console.log(` • ${item.target.name}${item.path ? ` (${item.path})` : ''}`);
164
+
165
+ items.forEach((item) => {
166
+ console.log(
167
+ ` • ${item.target.name}${item.path ? ` (${item.path})` : ""}`,
168
+ );
153
169
  });
154
170
  });
155
171
 
156
172
  let registered = 0;
157
173
  let skipped = 0;
158
-
174
+
159
175
  classes.forEach((metadata) => {
160
176
  if (this.shouldRegister(metadata.target)) {
161
177
  // CRITICAL: Register with container
@@ -178,10 +194,10 @@ export class FragmentWebApplication {
178
194
  if (skipped > 0) {
179
195
  console.log(`⊘ Skipped ${skipped} component(s) (conditions not met)`);
180
196
  }
181
-
197
+
182
198
  // Pre-resolve all singleton components to ensure dependencies are injected
183
199
  console.log("\nšŸ”§ Initializing components");
184
- classes.forEach(metadata => {
200
+ classes.forEach((metadata) => {
185
201
  if (this.shouldRegister(metadata.target)) {
186
202
  const instance = this.container.resolve(metadata.target);
187
203
  console.log(` āœ“ Initialized: ${metadata.target.name}`);
@@ -205,9 +221,11 @@ export class FragmentWebApplication {
205
221
  METADATA_KEYS.CONDITIONAL_ON_CLASS,
206
222
  target,
207
223
  );
208
-
224
+
209
225
  if (conditionalClass && !this.isClassAvailable(conditionalClass)) {
210
- console.log(` 🚫 Conditional check failed for ${target.name}: Class not available`);
226
+ console.log(
227
+ ` 🚫 Conditional check failed for ${target.name}: Class not available`,
228
+ );
211
229
  return false;
212
230
  }
213
231
 
@@ -215,9 +233,11 @@ export class FragmentWebApplication {
215
233
  METADATA_KEYS.CONDITIONAL_ON_MISSING_BEAN,
216
234
  target,
217
235
  );
218
-
236
+
219
237
  if (conditionalMissingBean && this.container.has(conditionalMissingBean)) {
220
- console.log(` 🚫 Conditional check failed for ${target.name}: Bean already exists`);
238
+ console.log(
239
+ ` 🚫 Conditional check failed for ${target.name}: Bean already exists`,
240
+ );
221
241
  return false;
222
242
  }
223
243
 
@@ -225,17 +245,21 @@ export class FragmentWebApplication {
225
245
  METADATA_KEYS.CONDITIONAL_ON_PROPERTY,
226
246
  target,
227
247
  );
228
-
248
+
229
249
  if (conditionalProperty) {
230
250
  const value = process.env[conditionalProperty.key];
231
-
251
+
232
252
  if (conditionalProperty.expectedValue !== undefined) {
233
253
  if (value !== conditionalProperty.expectedValue) {
234
- console.log(` 🚫 Conditional check failed for ${target.name}: Expected ${conditionalProperty.expectedValue}, got ${value}`);
254
+ console.log(
255
+ ` 🚫 Conditional check failed for ${target.name}: Expected ${conditionalProperty.expectedValue}, got ${value}`,
256
+ );
235
257
  return false;
236
258
  }
237
259
  } else if (!value) {
238
- console.log(` 🚫 Conditional check failed for ${target.name}: Property not set`);
260
+ console.log(
261
+ ` 🚫 Conditional check failed for ${target.name}: Property not set`,
262
+ );
239
263
  return false;
240
264
  }
241
265
  }
@@ -255,73 +279,203 @@ export class FragmentWebApplication {
255
279
  const controllers = this.metadataStorage
256
280
  .getAllClasses()
257
281
  .filter((c) => c.type === "controller");
258
-
282
+
259
283
  if (controllers.length === 0) {
260
284
  console.log("\nšŸ›£ļø No routes to register");
261
285
  return;
262
286
  }
263
-
287
+
264
288
  let totalRoutes = 0;
265
289
  console.log(`\nšŸ›£ļø Registering routes...`);
266
-
290
+
267
291
  controllers.forEach((controllerMetadata) => {
268
292
  try {
269
293
  console.log(`\nšŸ“ Controller: ${controllerMetadata.target.name}`);
270
- console.log(` Base path: ${controllerMetadata.path || '/'}`);
271
-
294
+ console.log(` Base path: ${controllerMetadata.path || "/"}`);
295
+
272
296
  const controller = this.container.resolve(controllerMetadata.target);
273
297
  const basePath = controllerMetadata.path || "";
274
-
298
+
299
+ // Get controller-level enhancements
300
+ const controllerMiddlewares = (Reflect.getMetadata(
301
+ METADATA_KEYS.USE_MIDDLEWARE,
302
+ controllerMetadata.target,
303
+ ) || []) as MiddlewareClass[];
304
+
305
+ const controllerGuards = (Reflect.getMetadata(
306
+ METADATA_KEYS.USE_GUARDS,
307
+ controllerMetadata.target,
308
+ ) || []) as GuardClass[];
309
+
310
+ const controllerInterceptors = (Reflect.getMetadata(
311
+ METADATA_KEYS.USE_INTERCEPTORS,
312
+ controllerMetadata.target,
313
+ ) || []) as InterceptorClass[];
314
+
315
+ const controllerFilters = (Reflect.getMetadata(
316
+ METADATA_KEYS.USE_FILTERS,
317
+ controllerMetadata.target,
318
+ ) || []) as ExceptionFilterClass[];
319
+
275
320
  const methods = this.metadataStorage
276
321
  .getAllMethods()
277
322
  .filter((m) => m.target === controllerMetadata.target);
278
-
323
+
279
324
  if (methods.length === 0) {
280
325
  console.log(` āš ļø No routes defined for this controller`);
281
326
  return;
282
327
  }
283
-
328
+
284
329
  methods.forEach((methodMetadata) => {
285
330
  const fullPath = this.normalizePath(basePath + methodMetadata.path);
286
331
  const httpMethod = methodMetadata.method.toLowerCase();
287
332
  const methodColor = this.getMethodColor(httpMethod);
288
333
  const methodIcon = this.getMethodIcon(httpMethod);
289
-
334
+
290
335
  console.log(
291
336
  ` ${methodIcon} ${methodColor}${httpMethod.toUpperCase().padEnd(7)}\x1b[0m ${fullPath}`,
292
337
  );
293
-
338
+
294
339
  totalRoutes++;
295
-
340
+
341
+ // Collect method-level enhancements
342
+ const methodMiddlewares = (Reflect.getMetadata(
343
+ METADATA_KEYS.USE_MIDDLEWARE,
344
+ controllerMetadata.target.prototype,
345
+ methodMetadata.propertyKey,
346
+ ) || []) as MiddlewareClass[];
347
+
348
+ const methodGuards = (Reflect.getMetadata(
349
+ METADATA_KEYS.USE_GUARDS,
350
+ controllerMetadata.target.prototype,
351
+ methodMetadata.propertyKey,
352
+ ) || []) as GuardClass[];
353
+
354
+ const methodInterceptors = (Reflect.getMetadata(
355
+ METADATA_KEYS.USE_INTERCEPTORS,
356
+ controllerMetadata.target.prototype,
357
+ methodMetadata.propertyKey,
358
+ ) || []) as InterceptorClass[];
359
+
360
+ const methodFilters = (Reflect.getMetadata(
361
+ METADATA_KEYS.USE_FILTERS,
362
+ controllerMetadata.target.prototype,
363
+ methodMetadata.propertyKey,
364
+ ) || []) as ExceptionFilterClass[];
365
+
366
+ // Combine: controller + method
367
+ const allMiddlewares = [
368
+ ...controllerMiddlewares,
369
+ ...methodMiddlewares,
370
+ ];
371
+ const allGuards = [...controllerGuards, ...methodGuards];
372
+ const allInterceptors = [
373
+ ...controllerInterceptors,
374
+ ...methodInterceptors,
375
+ ];
376
+ const allFilters = [...controllerFilters, ...methodFilters];
377
+
378
+ // Resolve via DI container
379
+ const resolvedMiddlewares = allMiddlewares.map(
380
+ (M) => this.container.resolve(M) as Middleware,
381
+ );
382
+ const resolvedGuards = allGuards.map(
383
+ (G) => this.container.resolve(G) as Guard,
384
+ );
385
+ const resolvedInterceptors = allInterceptors.map(
386
+ (I) => this.container.resolve(I) as Interceptor,
387
+ );
388
+
389
+ // Build Express route handler with pipeline
296
390
  (this.app as any)[httpMethod](
297
391
  fullPath,
392
+ // Middleware stack (Express-compatible)
393
+ ...resolvedMiddlewares.map(
394
+ (mw) => (req: Request, res: Response, next: NextFunction) =>
395
+ Promise.resolve(mw.use(req, res, next)).catch(next),
396
+ ),
397
+ // Final unified handler
298
398
  async (req: Request, res: Response, next: NextFunction) => {
299
399
  try {
400
+ // āž¤ Step 1: Guards
401
+ for (const guard of resolvedGuards) {
402
+ const canActivate = await Promise.resolve(
403
+ guard.canActivate(req),
404
+ );
405
+ if (!canActivate) {
406
+ const error = new Error("Forbidden");
407
+ (error as any).status = 403;
408
+ throw error;
409
+ }
410
+ }
411
+
412
+ // āž¤ Step 2: Resolve parameters
300
413
  const args = this.resolveMethodParameters(
301
414
  methodMetadata,
302
415
  req,
303
416
  res,
304
417
  );
305
-
306
- console.log(`\nšŸ” Handling ${httpMethod.toUpperCase()} ${fullPath}`);
307
- // console.log(` Parameters:`, args);
308
-
309
- const result = await (controller as any)[
310
- methodMetadata.propertyKey
311
- ](...args);
312
-
418
+
419
+ console.log(
420
+ `\nšŸ” Handling ${httpMethod.toUpperCase()} ${fullPath}`,
421
+ );
422
+
423
+ // āž¤ Step 3: Interceptors (wrap handler)
424
+ let result: any;
425
+ if (resolvedInterceptors.length > 0) {
426
+ // First, call the real handler
427
+ const rawResult = await (controller as any)[
428
+ methodMetadata.propertyKey
429
+ ](...args);
430
+
431
+ // Then pass result through interceptors in reverse order (or forward — your choice)
432
+ result = rawResult;
433
+ for (const interceptor of resolvedInterceptors) {
434
+ // Allow interceptor to transform the result
435
+ result = await Promise.resolve(
436
+ interceptor.intercept(req, res, result),
437
+ );
438
+ }
439
+ } else {
440
+ result = await (controller as any)[
441
+ methodMetadata.propertyKey
442
+ ](...args);
443
+ }
444
+
445
+ // āž¤ Step 4: Send response
313
446
  if (!res.headersSent) {
314
447
  res.json(result);
315
448
  }
316
449
  } catch (error) {
317
- console.error(`āŒ Error handling route ${fullPath}:`, error);
318
- next(error);
450
+ // āž¤ Step 5: Exception Filters (method + controller level)
451
+ let handled = false;
452
+ for (const FilterClass of allFilters) {
453
+ const filter = this.container.resolve(
454
+ FilterClass,
455
+ ) as ExceptionFilter;
456
+ try {
457
+ const err =
458
+ error instanceof Error ? error : new Error(String(error));
459
+ filter.catch(err, req, res);
460
+ handled = true;
461
+ break;
462
+ } catch (e) {
463
+ console.error(`āŒ Exception filter failed:`, e);
464
+ }
465
+ }
466
+
467
+ if (!handled) {
468
+ next(error);
469
+ }
319
470
  }
320
471
  },
321
472
  );
322
473
  });
323
474
  } catch (error) {
324
- console.error(`āŒ Failed to register controller ${controllerMetadata.target.name}:`, error);
475
+ console.error(
476
+ `āŒ Failed to register controller ${controllerMetadata.target.name}:`,
477
+ error,
478
+ );
325
479
  }
326
480
  });
327
481
 
@@ -358,7 +512,7 @@ export class FragmentWebApplication {
358
512
  const params = [...methodMetadata.paramMetadata].sort(
359
513
  (a: any, b: any) => a.index - b.index,
360
514
  );
361
-
515
+
362
516
  return params.map((param: any) => {
363
517
  switch (param.type) {
364
518
  case "body":
@@ -402,4 +556,4 @@ export class FragmentWebApplication {
402
556
  getExpressApp(): Express {
403
557
  return this.app;
404
558
  }
405
- }
559
+ }
@@ -1,4 +1,4 @@
1
- import { Request, Response, NextFunction } from 'express';
1
+ import { Request, Response, NextFunction } from "express";
2
2
 
3
3
  export interface Middleware {
4
4
  use(req: Request, res: Response, next: NextFunction): void | Promise<void>;
@@ -9,9 +9,18 @@ export interface Guard {
9
9
  }
10
10
 
11
11
  export interface Interceptor {
12
- intercept(req: Request, res: Response, next: NextFunction): void | Promise<void>;
12
+ intercept(
13
+ req: Request,
14
+ res: Response,
15
+ next: NextFunction,
16
+ ): void | Promise<void>;
13
17
  }
14
18
 
15
19
  export interface ExceptionFilter {
16
20
  catch(exception: Error, req: Request, res: Response): void;
17
21
  }
22
+
23
+ export type MiddlewareClass = new () => Middleware;
24
+ export type GuardClass = new () => Guard;
25
+ export type InterceptorClass = new () => Interceptor;
26
+ export type ExceptionFilterClass = new () => ExceptionFilter;