wynkjs 1.0.3 → 1.0.6
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/README.md +528 -348
- package/dist/cors.d.ts +28 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +121 -0
- package/dist/database.d.ts +1 -1
- package/dist/database.js +1 -1
- package/dist/decorators/exception.advanced.d.ts +286 -18
- package/dist/decorators/exception.advanced.d.ts.map +1 -1
- package/dist/decorators/exception.advanced.js +410 -17
- package/dist/decorators/exception.decorators.d.ts +93 -2
- package/dist/decorators/exception.decorators.d.ts.map +1 -1
- package/dist/decorators/exception.decorators.js +140 -8
- package/dist/decorators/formatter.decorators.d.ts +93 -0
- package/dist/decorators/formatter.decorators.d.ts.map +1 -0
- package/dist/decorators/formatter.decorators.js +131 -0
- package/dist/decorators/guard.decorators.d.ts +2 -2
- package/dist/decorators/guard.decorators.d.ts.map +1 -1
- package/dist/decorators/guard.decorators.js +11 -5
- package/dist/decorators/http.decorators.d.ts +10 -4
- package/dist/decorators/http.decorators.d.ts.map +1 -1
- package/dist/decorators/http.decorators.js +9 -2
- package/dist/decorators/interceptor.advanced.d.ts +9 -9
- package/dist/decorators/interceptor.advanced.d.ts.map +1 -1
- package/dist/decorators/interceptor.advanced.js +7 -7
- package/dist/decorators/interceptor.decorators.d.ts +9 -7
- package/dist/decorators/interceptor.decorators.d.ts.map +1 -1
- package/dist/decorators/interceptor.decorators.js +29 -18
- package/dist/decorators/param.decorators.d.ts +2 -2
- package/dist/decorators/param.decorators.js +1 -1
- package/dist/decorators/pipe.decorators.d.ts +4 -4
- package/dist/decorators/pipe.decorators.d.ts.map +1 -1
- package/dist/decorators/pipe.decorators.js +4 -4
- package/dist/dto.js +1 -1
- package/dist/factory.d.ts +30 -2
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +210 -186
- package/dist/filters/exception.filters.d.ts +124 -0
- package/dist/filters/exception.filters.d.ts.map +1 -0
- package/dist/filters/exception.filters.js +208 -0
- package/dist/global-prefix.d.ts +49 -0
- package/dist/global-prefix.d.ts.map +1 -0
- package/dist/global-prefix.js +155 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/interfaces/interceptor.interface.d.ts +15 -0
- package/dist/interfaces/interceptor.interface.d.ts.map +1 -0
- package/dist/interfaces/interceptor.interface.js +1 -0
- package/dist/optimized-handler.d.ts +31 -0
- package/dist/optimized-handler.d.ts.map +1 -0
- package/dist/optimized-handler.js +180 -0
- package/dist/pipes/validation.pipe.d.ts +12 -12
- package/dist/pipes/validation.pipe.d.ts.map +1 -1
- package/dist/pipes/validation.pipe.js +43 -15
- package/dist/schema-registry.d.ts +51 -0
- package/dist/schema-registry.d.ts.map +1 -0
- package/dist/schema-registry.js +134 -0
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/ultra-optimized-handler.d.ts +51 -0
- package/dist/ultra-optimized-handler.d.ts.map +1 -0
- package/dist/ultra-optimized-handler.js +302 -0
- package/package.json +17 -10
package/dist/factory.js
CHANGED
|
@@ -2,28 +2,51 @@ import { Elysia } from "elysia";
|
|
|
2
2
|
import "reflect-metadata";
|
|
3
3
|
import { container } from "tsyringe";
|
|
4
4
|
import { Value } from "@sinclair/typebox/value";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { executeExceptionFilters, } from "./decorators/exception.decorators";
|
|
6
|
+
import { schemaRegistry } from "./schema-registry";
|
|
7
|
+
import { setupCors } from "./cors";
|
|
8
|
+
import { normalizePrefixPath } from "./global-prefix";
|
|
9
|
+
import { buildUltraOptimizedHandler, buildMiddlewareChain, } from "./ultra-optimized-handler";
|
|
9
10
|
export class WynkFramework {
|
|
10
11
|
app;
|
|
11
12
|
controllers = [];
|
|
13
|
+
providers = []; // Store registered providers
|
|
12
14
|
globalGuards = [];
|
|
13
15
|
globalInterceptors = [];
|
|
14
16
|
globalPipes = [];
|
|
15
17
|
globalFilters = [];
|
|
16
18
|
validationFormatter;
|
|
19
|
+
shutdownHandlersRegistered = false; // Prevent duplicate signal handlers
|
|
20
|
+
globalPrefix; // Store global prefix for route registration
|
|
21
|
+
isBuilt = false; // Track if build() has been called
|
|
17
22
|
constructor(options = {}) {
|
|
18
23
|
this.app = new Elysia();
|
|
19
24
|
this.validationFormatter = options.validationErrorFormatter;
|
|
25
|
+
// Store global prefix for later use
|
|
26
|
+
if (options.globalPrefix) {
|
|
27
|
+
this.globalPrefix = normalizePrefixPath(options.globalPrefix);
|
|
28
|
+
}
|
|
29
|
+
// Register providers if provided
|
|
30
|
+
if (options.providers && options.providers.length > 0) {
|
|
31
|
+
this.providers.push(...options.providers);
|
|
32
|
+
}
|
|
33
|
+
// Apply CORS configuration
|
|
34
|
+
if (options.cors) {
|
|
35
|
+
setupCors(this.app, options.cors);
|
|
36
|
+
}
|
|
20
37
|
// Configure Elysia's error handling for validation errors
|
|
21
|
-
this.app.onError(({ code, error, set }) => {
|
|
38
|
+
this.app.onError(({ code, error, set, request }) => {
|
|
22
39
|
// Handle ValidationError from Elysia
|
|
23
40
|
if (code === "VALIDATION" ||
|
|
24
41
|
error?.constructor?.name === "ValidationError") {
|
|
25
42
|
const validationError = error;
|
|
26
43
|
set.status = 400;
|
|
44
|
+
// Get the validation type (body, query, params)
|
|
45
|
+
const validationType = validationError.on || "body";
|
|
46
|
+
// Try to find the schema key for this route
|
|
47
|
+
const method = request.method;
|
|
48
|
+
const path = new URL(request.url).pathname;
|
|
49
|
+
const schemaKey = schemaRegistry.getSchemaKeyForRoute(method, path, validationType);
|
|
27
50
|
// Try to collect all validation errors using TypeBox
|
|
28
51
|
const allErrors = {};
|
|
29
52
|
// Check if we have the validator and value to collect all errors
|
|
@@ -38,7 +61,15 @@ export class WynkFramework {
|
|
|
38
61
|
if (!allErrors[field]) {
|
|
39
62
|
allErrors[field] = [];
|
|
40
63
|
}
|
|
41
|
-
|
|
64
|
+
// Try to get custom error message from schema registry
|
|
65
|
+
let message = err.message || "Validation failed";
|
|
66
|
+
if (schemaKey) {
|
|
67
|
+
const customMessage = schemaRegistry.getErrorMessage(schemaKey, field);
|
|
68
|
+
if (customMessage) {
|
|
69
|
+
message = customMessage;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
allErrors[field].push(message);
|
|
42
73
|
});
|
|
43
74
|
}
|
|
44
75
|
else {
|
|
@@ -46,9 +77,16 @@ export class WynkFramework {
|
|
|
46
77
|
const field = validationError.valueError?.path?.replace(/^\//, "") ||
|
|
47
78
|
validationError.on ||
|
|
48
79
|
"body";
|
|
49
|
-
|
|
80
|
+
let message = validationError.customError ||
|
|
50
81
|
validationError.valueError?.message ||
|
|
51
82
|
"Validation failed";
|
|
83
|
+
// Try to get custom error message
|
|
84
|
+
if (schemaKey) {
|
|
85
|
+
const customMessage = schemaRegistry.getErrorMessage(schemaKey, field);
|
|
86
|
+
if (customMessage) {
|
|
87
|
+
message = customMessage;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
52
90
|
allErrors[field] = [message];
|
|
53
91
|
}
|
|
54
92
|
}
|
|
@@ -57,9 +95,16 @@ export class WynkFramework {
|
|
|
57
95
|
const field = validationError.valueError?.path?.replace(/^\//, "") ||
|
|
58
96
|
validationError.on ||
|
|
59
97
|
"body";
|
|
60
|
-
|
|
98
|
+
let message = validationError.customError ||
|
|
61
99
|
validationError.valueError?.message ||
|
|
62
100
|
"Validation failed";
|
|
101
|
+
// Try to get custom error message
|
|
102
|
+
if (schemaKey) {
|
|
103
|
+
const customMessage = schemaRegistry.getErrorMessage(schemaKey, field);
|
|
104
|
+
if (customMessage) {
|
|
105
|
+
message = customMessage;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
63
108
|
allErrors[field] = [message];
|
|
64
109
|
}
|
|
65
110
|
}
|
|
@@ -68,9 +113,16 @@ export class WynkFramework {
|
|
|
68
113
|
const field = validationError.valueError?.path?.replace(/^\//, "") ||
|
|
69
114
|
validationError.on ||
|
|
70
115
|
"body";
|
|
71
|
-
|
|
116
|
+
let message = validationError.customError ||
|
|
72
117
|
validationError.valueError?.message ||
|
|
73
118
|
"Validation failed";
|
|
119
|
+
// Try to get custom error message
|
|
120
|
+
if (schemaKey) {
|
|
121
|
+
const customMessage = schemaRegistry.getErrorMessage(schemaKey, field);
|
|
122
|
+
if (customMessage) {
|
|
123
|
+
message = customMessage;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
74
126
|
allErrors[field] = [message];
|
|
75
127
|
}
|
|
76
128
|
// If a custom formatter is provided, use it
|
|
@@ -101,13 +153,10 @@ export class WynkFramework {
|
|
|
101
153
|
error: err.name || "Error",
|
|
102
154
|
};
|
|
103
155
|
});
|
|
104
|
-
// Apply CORS if enabled
|
|
105
|
-
if (options.cors) {
|
|
106
|
-
// CORS configuration would go here
|
|
107
|
-
}
|
|
108
156
|
// Apply global prefix if specified
|
|
109
157
|
if (options.globalPrefix) {
|
|
110
|
-
// Global prefix
|
|
158
|
+
// Global prefix is handled during route registration
|
|
159
|
+
console.log(`✅ Global prefix configured: ${this.globalPrefix}`);
|
|
111
160
|
}
|
|
112
161
|
return this;
|
|
113
162
|
}
|
|
@@ -116,11 +165,21 @@ export class WynkFramework {
|
|
|
116
165
|
*/
|
|
117
166
|
static create(options = {}) {
|
|
118
167
|
const app = new WynkFramework(options);
|
|
168
|
+
// Don't re-register providers/controllers if they were already added in constructor
|
|
169
|
+
// The constructor already handles options.providers
|
|
119
170
|
if (options.controllers && options.controllers.length) {
|
|
120
171
|
app.registerControllers(...options.controllers);
|
|
121
172
|
}
|
|
122
173
|
return app;
|
|
123
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Register providers with the application
|
|
177
|
+
* Providers are singleton services that are initialized when the app starts
|
|
178
|
+
*/
|
|
179
|
+
registerProviders(...providers) {
|
|
180
|
+
this.providers.push(...providers);
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
124
183
|
/**
|
|
125
184
|
* Register controllers with the application
|
|
126
185
|
*/
|
|
@@ -156,10 +215,42 @@ export class WynkFramework {
|
|
|
156
215
|
this.globalFilters.push(...filters);
|
|
157
216
|
return this;
|
|
158
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Initialize all registered providers
|
|
220
|
+
* Providers with onModuleInit() method will be called
|
|
221
|
+
*/
|
|
222
|
+
async initializeProviders() {
|
|
223
|
+
console.log(`🔧 Initializing ${this.providers.length} providers...`);
|
|
224
|
+
for (const ProviderClass of this.providers) {
|
|
225
|
+
try {
|
|
226
|
+
console.log(` ⚙️ Initializing provider: ${ProviderClass.name}`);
|
|
227
|
+
// Resolve provider instance from DI container
|
|
228
|
+
const instance = container.resolve(ProviderClass);
|
|
229
|
+
// Check if provider has onModuleInit lifecycle hook
|
|
230
|
+
if (typeof instance.onModuleInit === "function") {
|
|
231
|
+
await instance.onModuleInit();
|
|
232
|
+
console.log(` ✅ ${ProviderClass.name} initialized successfully`);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// Just register in container for injection
|
|
236
|
+
console.log(` ✅ ${ProviderClass.name} registered in container`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
console.error(` ❌ Failed to initialize provider ${ProviderClass.name}:`, error);
|
|
241
|
+
throw new Error(`Provider initialization failed for ${ProviderClass.name}: ${error.message}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
console.log(`✅ All providers initialized successfully\n`);
|
|
245
|
+
}
|
|
159
246
|
/**
|
|
160
247
|
* Build the application - register all routes
|
|
161
248
|
*/
|
|
162
249
|
async build() {
|
|
250
|
+
// Initialize providers first (database connections, etc.)
|
|
251
|
+
if (this.providers.length > 0) {
|
|
252
|
+
await this.initializeProviders();
|
|
253
|
+
}
|
|
163
254
|
// Register global error handler if filters exist
|
|
164
255
|
if (this.globalFilters.length > 0) {
|
|
165
256
|
this.app.onError(async ({ error, set, request }) => {
|
|
@@ -198,8 +289,33 @@ export class WynkFramework {
|
|
|
198
289
|
for (const ControllerClass of this.controllers) {
|
|
199
290
|
await this.registerController(ControllerClass);
|
|
200
291
|
}
|
|
292
|
+
this.isBuilt = true;
|
|
201
293
|
return this.app;
|
|
202
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Cleanup all providers when app shuts down
|
|
297
|
+
* Providers with onModuleDestroy() method will be called
|
|
298
|
+
*/
|
|
299
|
+
async destroyProviders() {
|
|
300
|
+
console.log(`\n🔧 Cleaning up ${this.providers.length} providers...`);
|
|
301
|
+
for (const ProviderClass of this.providers) {
|
|
302
|
+
try {
|
|
303
|
+
// Resolve provider instance from DI container
|
|
304
|
+
const instance = container.resolve(ProviderClass);
|
|
305
|
+
// Check if provider has onModuleDestroy lifecycle hook
|
|
306
|
+
if (typeof instance.onModuleDestroy === "function") {
|
|
307
|
+
console.log(` 🧹 Destroying provider: ${ProviderClass.name}`);
|
|
308
|
+
await instance.onModuleDestroy();
|
|
309
|
+
console.log(` ✅ ${ProviderClass.name} destroyed successfully`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
console.error(` ❌ Failed to destroy provider ${ProviderClass.name}:`, error);
|
|
314
|
+
// Continue cleanup even if one provider fails
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
console.log(`✅ All providers cleaned up\n`);
|
|
318
|
+
}
|
|
203
319
|
/**
|
|
204
320
|
* Start listening on a port
|
|
205
321
|
*/
|
|
@@ -207,6 +323,29 @@ export class WynkFramework {
|
|
|
207
323
|
await this.build();
|
|
208
324
|
this.app.listen(port);
|
|
209
325
|
console.log(`🚀 Application is running on http://localhost:${port}`);
|
|
326
|
+
// Register signal handlers only once to prevent memory leaks
|
|
327
|
+
if (!this.shutdownHandlersRegistered) {
|
|
328
|
+
this.shutdownHandlersRegistered = true;
|
|
329
|
+
// Setup graceful shutdown handlers
|
|
330
|
+
const gracefulShutdown = async (signal) => {
|
|
331
|
+
console.log(`\n📡 Received ${signal}, shutting down gracefully...`);
|
|
332
|
+
try {
|
|
333
|
+
// Cleanup providers (close database connections, etc.)
|
|
334
|
+
await this.destroyProviders();
|
|
335
|
+
// Stop the Elysia server
|
|
336
|
+
await this.app.stop();
|
|
337
|
+
console.log("👋 Application shut down successfully");
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
console.error("❌ Error during shutdown:", error);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
// Register signal handlers (only once)
|
|
346
|
+
process.once("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
347
|
+
process.once("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
348
|
+
}
|
|
210
349
|
}
|
|
211
350
|
/**
|
|
212
351
|
* Get the underlying Elysia instance
|
|
@@ -214,6 +353,16 @@ export class WynkFramework {
|
|
|
214
353
|
getApp() {
|
|
215
354
|
return this.app;
|
|
216
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Handle an HTTP request
|
|
358
|
+
* Automatically builds the app if not already built
|
|
359
|
+
*/
|
|
360
|
+
async handle(request) {
|
|
361
|
+
if (!this.isBuilt) {
|
|
362
|
+
await this.build();
|
|
363
|
+
}
|
|
364
|
+
return this.app.handle(request);
|
|
365
|
+
}
|
|
217
366
|
/**
|
|
218
367
|
* Register a single controller
|
|
219
368
|
*/
|
|
@@ -237,7 +386,11 @@ export class WynkFramework {
|
|
|
237
386
|
// Get @Use() middleware (simple pattern like user's working code)
|
|
238
387
|
const controllerUses = Reflect.getMetadata("uses", ControllerClass) || [];
|
|
239
388
|
for (const route of routes) {
|
|
240
|
-
|
|
389
|
+
// Apply global prefix to route path
|
|
390
|
+
let fullPath = basePath + route.path;
|
|
391
|
+
if (this.globalPrefix) {
|
|
392
|
+
fullPath = this.globalPrefix + fullPath;
|
|
393
|
+
}
|
|
241
394
|
const method = route.method.toLowerCase();
|
|
242
395
|
const methodName = route.methodName;
|
|
243
396
|
// Get method-specific metadata
|
|
@@ -248,19 +401,24 @@ export class WynkFramework {
|
|
|
248
401
|
// Get @Use() middleware for this method
|
|
249
402
|
const methodUses = Reflect.getMetadata("uses", instance, methodName) || [];
|
|
250
403
|
const params = Reflect.getMetadata("params", instance, methodName) || [];
|
|
404
|
+
// Sort params once during registration, not on every request
|
|
405
|
+
if (params.length > 0) {
|
|
406
|
+
params.sort((a, b) => a.index - b.index);
|
|
407
|
+
}
|
|
251
408
|
const httpCode = Reflect.getMetadata("route:httpCode", instance, methodName);
|
|
252
409
|
const headers = Reflect.getMetadata("route:headers", instance, methodName);
|
|
253
410
|
const redirect = Reflect.getMetadata("route:redirect", instance, methodName);
|
|
254
|
-
// Combine guards, interceptors, pipes, filters
|
|
411
|
+
// Combine guards, interceptors, pipes, filters
|
|
412
|
+
// Order: method -> controller -> global (method is innermost/closest to handler)
|
|
255
413
|
const allGuards = [
|
|
256
414
|
...this.globalGuards,
|
|
257
415
|
...controllerGuards,
|
|
258
416
|
...methodGuards,
|
|
259
417
|
];
|
|
260
418
|
const allInterceptors = [
|
|
261
|
-
...this.globalInterceptors,
|
|
262
|
-
...controllerInterceptors,
|
|
263
419
|
...methodInterceptors,
|
|
420
|
+
...controllerInterceptors,
|
|
421
|
+
...this.globalInterceptors,
|
|
264
422
|
];
|
|
265
423
|
const allPipes = [
|
|
266
424
|
...this.globalPipes,
|
|
@@ -278,189 +436,53 @@ export class WynkFramework {
|
|
|
278
436
|
if (bodySchema && !routeOptions.body) {
|
|
279
437
|
routeOptions.body = bodySchema;
|
|
280
438
|
}
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
// No parameter decorators, pass full context
|
|
299
|
-
args.push(ctx);
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
// Sort params by index
|
|
303
|
-
params.sort((a, b) => a.index - b.index);
|
|
304
|
-
for (const param of params) {
|
|
305
|
-
let value;
|
|
306
|
-
// Extract value based on type
|
|
307
|
-
switch (param.type) {
|
|
308
|
-
case "body":
|
|
309
|
-
value = param.data ? ctx.body?.[param.data] : ctx.body;
|
|
310
|
-
break;
|
|
311
|
-
case "param":
|
|
312
|
-
value = param.data ? ctx.params?.[param.data] : ctx.params;
|
|
313
|
-
break;
|
|
314
|
-
case "query":
|
|
315
|
-
value = param.data ? ctx.query?.[param.data] : ctx.query;
|
|
316
|
-
break;
|
|
317
|
-
case "headers":
|
|
318
|
-
value = param.data
|
|
319
|
-
? ctx.headers?.get?.(param.data) ||
|
|
320
|
-
ctx.request?.headers?.get?.(param.data)
|
|
321
|
-
: ctx.headers || ctx.request?.headers;
|
|
322
|
-
break;
|
|
323
|
-
case "request":
|
|
324
|
-
value = ctx.request || ctx;
|
|
325
|
-
break;
|
|
326
|
-
case "response":
|
|
327
|
-
value = ctx.set || ctx.response;
|
|
328
|
-
break;
|
|
329
|
-
case "context":
|
|
330
|
-
if (param.data) {
|
|
331
|
-
// Access nested property like "session.userId"
|
|
332
|
-
const keys = param.data.split(".");
|
|
333
|
-
value = keys.reduce((obj, key) => obj?.[key], ctx);
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
value = ctx;
|
|
337
|
-
}
|
|
338
|
-
break;
|
|
339
|
-
case "user":
|
|
340
|
-
value = param.data ? ctx.user?.[param.data] : ctx.user;
|
|
341
|
-
break;
|
|
342
|
-
case "file":
|
|
343
|
-
value = ctx.body?.file || ctx.file;
|
|
344
|
-
break;
|
|
345
|
-
case "files":
|
|
346
|
-
value = ctx.body?.files || ctx.files;
|
|
347
|
-
break;
|
|
348
|
-
}
|
|
349
|
-
// Apply pipes if any
|
|
350
|
-
if (param.pipes && param.pipes.length > 0) {
|
|
351
|
-
const metadata = {
|
|
352
|
-
type: param.type,
|
|
353
|
-
data: param.data,
|
|
354
|
-
};
|
|
355
|
-
value = await executePipes(param.pipes, value, metadata);
|
|
356
|
-
}
|
|
357
|
-
// Apply global/controller/method pipes
|
|
358
|
-
if (allPipes.length > 0) {
|
|
359
|
-
const metadata = {
|
|
360
|
-
type: param.type,
|
|
361
|
-
data: param.data,
|
|
362
|
-
};
|
|
363
|
-
value = await executePipes(allPipes, value, metadata);
|
|
364
|
-
}
|
|
365
|
-
args[param.index] = value;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
// Call controller method
|
|
369
|
-
return await instance[methodName].apply(instance, args);
|
|
370
|
-
};
|
|
371
|
-
// Execute interceptors
|
|
372
|
-
let result;
|
|
373
|
-
if (allInterceptors.length > 0) {
|
|
374
|
-
result = await executeInterceptors(allInterceptors, executionContext, executeHandler);
|
|
375
|
-
}
|
|
376
|
-
else {
|
|
377
|
-
result = await executeHandler();
|
|
378
|
-
}
|
|
379
|
-
// Handle redirect
|
|
380
|
-
if (redirect) {
|
|
381
|
-
ctx.set.redirect = redirect.url;
|
|
382
|
-
ctx.set.status = redirect.statusCode;
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
// Set custom HTTP code
|
|
386
|
-
if (httpCode) {
|
|
387
|
-
ctx.set.status = httpCode;
|
|
388
|
-
}
|
|
389
|
-
// Set custom headers
|
|
390
|
-
if (headers) {
|
|
391
|
-
Object.entries(headers).forEach(([key, value]) => {
|
|
392
|
-
ctx.set.headers[key] = value;
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
return result;
|
|
396
|
-
}
|
|
397
|
-
catch (error) {
|
|
398
|
-
console.log("🔴 ERROR CAUGHT IN FACTORY");
|
|
399
|
-
console.log("allFilters.length:", allFilters.length);
|
|
400
|
-
console.log("Error:", error?.message);
|
|
401
|
-
// Execute exception filters
|
|
402
|
-
if (allFilters.length > 0) {
|
|
403
|
-
console.log("✅ Executing exception filters...");
|
|
404
|
-
const executionContext = createExecutionContext(ctx, instance[methodName], ControllerClass);
|
|
405
|
-
try {
|
|
406
|
-
const result = await executeExceptionFilters(allFilters, error, executionContext);
|
|
407
|
-
if (result) {
|
|
408
|
-
if (result.statusCode) {
|
|
409
|
-
ctx.set.status = result.statusCode;
|
|
410
|
-
}
|
|
411
|
-
return result;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
catch (filterError) {
|
|
415
|
-
// If filter doesn't handle it, continue to default error handling
|
|
416
|
-
error = filterError;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
else {
|
|
420
|
-
console.log("❌ No filters registered for this route");
|
|
421
|
-
}
|
|
422
|
-
// Default error handling
|
|
423
|
-
if (error instanceof HttpException) {
|
|
424
|
-
ctx.set.status = error.getStatus();
|
|
425
|
-
return error.getResponse();
|
|
426
|
-
}
|
|
427
|
-
// Unknown error
|
|
428
|
-
ctx.set.status = 500;
|
|
429
|
-
return {
|
|
430
|
-
statusCode: 500,
|
|
431
|
-
message: error.message || "Internal server error",
|
|
432
|
-
error: "Internal Server Error",
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
};
|
|
439
|
+
// ⚡ ULTRA-OPTIMIZED HANDLER - Use specialized builder
|
|
440
|
+
// This eliminates nested async/await and IIFEs for maximum performance
|
|
441
|
+
const handler = buildUltraOptimizedHandler({
|
|
442
|
+
instance,
|
|
443
|
+
methodName,
|
|
444
|
+
ControllerClass,
|
|
445
|
+
params,
|
|
446
|
+
allGuards,
|
|
447
|
+
allInterceptors,
|
|
448
|
+
allPipes,
|
|
449
|
+
allFilters,
|
|
450
|
+
httpCode,
|
|
451
|
+
headers,
|
|
452
|
+
redirect,
|
|
453
|
+
routePath: route.path,
|
|
454
|
+
routeMethod: method.toUpperCase(),
|
|
455
|
+
});
|
|
436
456
|
// Wrap handler with @Use() middleware if present
|
|
437
457
|
// Combine controller and method middleware
|
|
438
458
|
const allUses = [...controllerUses, ...methodUses];
|
|
439
459
|
let finalHandler = handler;
|
|
440
460
|
if (allUses.length > 0) {
|
|
441
|
-
|
|
442
|
-
console.log(` Middleware count: ${allUses.length}`);
|
|
443
|
-
allUses.forEach((m, i) => console.log(` [${i}] ${m.name || "anonymous"}`));
|
|
444
|
-
// Build middleware chain using reduce (O(n) complexity)
|
|
445
|
-
// Builds from right to left: handler <- middleware[n-1] <- ... <- middleware[0]
|
|
446
|
-
// Executes left to right: middleware[0] -> ... -> middleware[n-1] -> handler
|
|
447
|
-
finalHandler = allUses.reduceRight((next, middleware, index) => {
|
|
448
|
-
return async (ctx) => {
|
|
449
|
-
console.log(`▶️ Executing middleware [${index}]: ${middleware.name || "anonymous"}`);
|
|
450
|
-
return await middleware(ctx, () => next(ctx));
|
|
451
|
-
};
|
|
452
|
-
}, handler);
|
|
461
|
+
finalHandler = buildMiddlewareChain(handler, allUses);
|
|
453
462
|
}
|
|
454
463
|
// Register route with Elysia
|
|
455
464
|
const elysiaOptions = {};
|
|
456
465
|
if (routeOptions.body || bodySchema) {
|
|
457
|
-
|
|
466
|
+
const schema = routeOptions.body || bodySchema;
|
|
467
|
+
elysiaOptions.body = schema;
|
|
468
|
+
// Register schema with custom error messages
|
|
469
|
+
const schemaKey = `${ControllerClass.name}.${methodName}.body`;
|
|
470
|
+
schemaRegistry.registerSchema(schemaKey, schema);
|
|
471
|
+
schemaRegistry.registerRoute(method, fullPath, schemaKey, "body");
|
|
458
472
|
}
|
|
459
473
|
if (routeOptions.query) {
|
|
460
474
|
elysiaOptions.query = routeOptions.query;
|
|
475
|
+
// Register query schema
|
|
476
|
+
const schemaKey = `${ControllerClass.name}.${methodName}.query`;
|
|
477
|
+
schemaRegistry.registerSchema(schemaKey, routeOptions.query);
|
|
478
|
+
schemaRegistry.registerRoute(method, fullPath, schemaKey, "query");
|
|
461
479
|
}
|
|
462
480
|
if (routeOptions.params) {
|
|
463
481
|
elysiaOptions.params = routeOptions.params;
|
|
482
|
+
// Register params schema
|
|
483
|
+
const schemaKey = `${ControllerClass.name}.${methodName}.params`;
|
|
484
|
+
schemaRegistry.registerSchema(schemaKey, routeOptions.params);
|
|
485
|
+
schemaRegistry.registerRoute(method, fullPath, schemaKey, "params");
|
|
464
486
|
}
|
|
465
487
|
if (routeOptions.headers) {
|
|
466
488
|
elysiaOptions.headers = routeOptions.headers;
|
|
@@ -487,6 +509,8 @@ export function createApp(options = {}) {
|
|
|
487
509
|
export class WynkFactory {
|
|
488
510
|
static create(options = {}) {
|
|
489
511
|
const app = new WynkFramework(options);
|
|
512
|
+
// Don't re-register providers if they were already added in constructor
|
|
513
|
+
// The constructor already handles options.providers
|
|
490
514
|
if (options.controllers) {
|
|
491
515
|
app.registerControllers(...options.controllers);
|
|
492
516
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { NotFoundException, WynkExceptionFilter } from "../decorators/exception.decorators";
|
|
2
|
+
import { ExecutionContext } from "../decorators/guard.decorators";
|
|
3
|
+
/**
|
|
4
|
+
* Database Exception Filter - Handles database errors ONLY (not HttpExceptions)
|
|
5
|
+
*
|
|
6
|
+
* This filter catches actual database errors (like unique constraint violations,
|
|
7
|
+
* foreign key errors, etc.) and converts them to user-friendly messages.
|
|
8
|
+
*
|
|
9
|
+
* It will NOT catch HttpException or its subclasses (ConflictException, etc.)
|
|
10
|
+
* that you throw manually - those will pass through to be handled correctly.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Use as global filter
|
|
14
|
+
* app.useGlobalFilters(new DatabaseExceptionFilter());
|
|
15
|
+
*
|
|
16
|
+
* // Use on specific controller
|
|
17
|
+
* @UseFilters(DatabaseExceptionFilter)
|
|
18
|
+
* @Controller('/users')
|
|
19
|
+
* export class UserController {
|
|
20
|
+
* @Post()
|
|
21
|
+
* async create(@Body() data: any) {
|
|
22
|
+
* // If you throw manually, it passes through:
|
|
23
|
+
* if (await this.userExists(data.email)) {
|
|
24
|
+
* throw new ConflictException('User with this email already exists'); // ✅ Works correctly
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // If database throws error, filter catches it:
|
|
28
|
+
* return await this.db.insert(users).values(data); // ❌ DB unique constraint error → caught by filter
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* Handles these database error codes:
|
|
33
|
+
* - 23505: Unique constraint violation → 409 Conflict
|
|
34
|
+
* - 23503: Foreign key constraint violation → 400 Bad Request
|
|
35
|
+
* - 23502: Not null constraint violation → 400 Bad Request
|
|
36
|
+
*/
|
|
37
|
+
export declare class DatabaseExceptionFilter implements WynkExceptionFilter {
|
|
38
|
+
catch(exception: any, context: ExecutionContext): {
|
|
39
|
+
statusCode: number;
|
|
40
|
+
error: string;
|
|
41
|
+
message: string;
|
|
42
|
+
timestamp: string;
|
|
43
|
+
path: any;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Not Found Exception Filter - Handles 404 errors with smart detection
|
|
48
|
+
*
|
|
49
|
+
* This filter is SMART - it only handles NotFoundExceptionFilter if:
|
|
50
|
+
* 1. The exception is NotFoundException, AND
|
|
51
|
+
* 2. No response data has been set (empty, null, empty array, or empty object)
|
|
52
|
+
*
|
|
53
|
+
* This allows it to be used globally without breaking routes that return
|
|
54
|
+
* legitimate empty responses or have their own error handling.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // ✅ Can be used globally - smart filtering
|
|
58
|
+
* app.useGlobalFilters(
|
|
59
|
+
* new NotFoundExceptionFilter(), // Safe to use globally now!
|
|
60
|
+
* new GlobalExceptionFilter()
|
|
61
|
+
* );
|
|
62
|
+
*
|
|
63
|
+
*/
|
|
64
|
+
export declare class NotFoundExceptionFilter implements WynkExceptionFilter<NotFoundException> {
|
|
65
|
+
catch(exception: NotFoundException, context: ExecutionContext): {
|
|
66
|
+
statusCode: number;
|
|
67
|
+
error: string;
|
|
68
|
+
message: string;
|
|
69
|
+
timestamp: string;
|
|
70
|
+
path: any;
|
|
71
|
+
suggestion: string;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Check if response has meaningful data
|
|
75
|
+
* Returns false for: null, undefined, {}, [], ""
|
|
76
|
+
* Returns true for: anything else
|
|
77
|
+
*/
|
|
78
|
+
private hasResponseData;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* File Upload Exception Filter - Handles file upload errors
|
|
82
|
+
* @example
|
|
83
|
+
* @UseFilters(FileUploadExceptionFilter)
|
|
84
|
+
* @Post('/upload')
|
|
85
|
+
* async upload(@UploadedFile() file: any) {}
|
|
86
|
+
*/
|
|
87
|
+
export declare class FileUploadExceptionFilter implements WynkExceptionFilter {
|
|
88
|
+
catch(exception: any, context: ExecutionContext): {
|
|
89
|
+
statusCode: number;
|
|
90
|
+
error: string;
|
|
91
|
+
message: string;
|
|
92
|
+
timestamp: string;
|
|
93
|
+
path: any;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Global Exception Filter - Catches all unhandled exceptions
|
|
98
|
+
* @example
|
|
99
|
+
* app.useGlobalFilters(new GlobalExceptionFilter());
|
|
100
|
+
*/
|
|
101
|
+
export declare class GlobalExceptionFilter implements WynkExceptionFilter {
|
|
102
|
+
catch(exception: any, context: ExecutionContext): {
|
|
103
|
+
stack?: any;
|
|
104
|
+
statusCode: any;
|
|
105
|
+
error: any;
|
|
106
|
+
message: any;
|
|
107
|
+
timestamp: string;
|
|
108
|
+
path: any;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// KEY TAKEAWAYS
|
|
114
|
+
// ============================================================================
|
|
115
|
+
//
|
|
116
|
+
// 1. Order matters: Specific → General
|
|
117
|
+
// 2. Filters can re-throw exceptions they don't handle
|
|
118
|
+
// 3. HttpException and subclasses should pass through specialized filters
|
|
119
|
+
// 4. Global filters catch everything not handled by controller/method filters
|
|
120
|
+
// 5. Always have a GlobalExceptionFilter as the last filter (catch-all)
|
|
121
|
+
//
|
|
122
|
+
// ============================================================================
|
|
123
|
+
*/
|
|
124
|
+
//# sourceMappingURL=exception.filters.d.ts.map
|