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.
- package/API.md +752 -0
- package/DOCS.md +555 -0
- package/README.md +76 -339
- package/USAGE.md +309 -1306
- package/dist/cli/commands/init.command.js +1 -1
- package/dist/core/container/di-container.d.ts.map +1 -1
- package/dist/core/container/di-container.js +62 -106
- package/dist/core/container/di-container.js.map +1 -1
- package/dist/core/decorators/exception-filter.decorator.d.ts +5 -0
- package/dist/core/decorators/exception-filter.decorator.d.ts.map +1 -0
- package/dist/core/decorators/exception-filter.decorator.js +23 -0
- package/dist/core/decorators/exception-filter.decorator.js.map +1 -0
- package/dist/core/decorators/guard.decorator.d.ts +5 -0
- package/dist/core/decorators/guard.decorator.d.ts.map +1 -0
- package/dist/core/decorators/guard.decorator.js +23 -0
- package/dist/core/decorators/guard.decorator.js.map +1 -0
- package/dist/core/decorators/injection.decorators.d.ts.map +1 -1
- package/dist/core/decorators/injection.decorators.js +5 -0
- package/dist/core/decorators/injection.decorators.js.map +1 -1
- package/dist/core/decorators/interceptor.decorator.d.ts +5 -0
- package/dist/core/decorators/interceptor.decorator.d.ts.map +1 -0
- package/dist/core/decorators/interceptor.decorator.js +23 -0
- package/dist/core/decorators/interceptor.decorator.js.map +1 -0
- package/dist/core/decorators/middleware.decorator.d.ts +8 -0
- package/dist/core/decorators/middleware.decorator.d.ts.map +1 -0
- package/dist/core/decorators/middleware.decorator.js +28 -0
- package/dist/core/decorators/middleware.decorator.js.map +1 -0
- package/dist/core/metadata/metadata-keys.d.ts +1 -0
- package/dist/core/metadata/metadata-keys.d.ts.map +1 -1
- package/dist/core/metadata/metadata-keys.js +1 -0
- package/dist/core/metadata/metadata-keys.js.map +1 -1
- package/dist/core/metadata/metadata-storage.d.ts +20 -4
- package/dist/core/metadata/metadata-storage.d.ts.map +1 -1
- package/dist/core/metadata/metadata-storage.js +94 -14
- package/dist/core/metadata/metadata-storage.js.map +1 -1
- package/dist/web/application.d.ts.map +1 -1
- package/dist/web/application.js +79 -10
- package/dist/web/application.js.map +1 -1
- package/dist/web/interfaces.d.ts +5 -1
- package/dist/web/interfaces.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/init.command.ts +1 -1
- package/src/core/container/di-container.ts +95 -177
- package/src/core/decorators/exception-filter.decorator.ts +20 -0
- package/src/core/decorators/guard.decorator.ts +20 -0
- package/src/core/decorators/injection.decorators.ts +5 -0
- package/src/core/decorators/interceptor.decorator.ts +20 -0
- package/src/core/decorators/middleware.decorator.ts +25 -0
- package/src/core/metadata/metadata-keys.ts +1 -0
- package/src/core/metadata/metadata-storage.ts +128 -24
- package/src/web/application.ts +207 -53
- 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
|
-
|
|
106
|
-
|
|
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(
|
|
126
|
+
const injections = this.propertyInjections.get(target)!;
|
|
113
127
|
injections.push(injection);
|
|
128
|
+
}
|
|
114
129
|
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
target
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/web/application.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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(
|
|
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
|
+
}
|
package/src/web/interfaces.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Request, Response, NextFunction } from
|
|
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(
|
|
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;
|