wynkjs 1.0.0
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/LICENSE +21 -0
- package/README.md +522 -0
- package/dist/database.d.ts +36 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +162 -0
- package/dist/decorators/database.decorators.d.ts +55 -0
- package/dist/decorators/database.decorators.d.ts.map +1 -0
- package/dist/decorators/database.decorators.js +131 -0
- package/dist/decorators/exception.advanced.d.ts +160 -0
- package/dist/decorators/exception.advanced.d.ts.map +1 -0
- package/dist/decorators/exception.advanced.js +232 -0
- package/dist/decorators/exception.decorators.d.ts +121 -0
- package/dist/decorators/exception.decorators.d.ts.map +1 -0
- package/dist/decorators/exception.decorators.js +242 -0
- package/dist/decorators/guard.decorators.d.ts +43 -0
- package/dist/decorators/guard.decorators.d.ts.map +1 -0
- package/dist/decorators/guard.decorators.js +67 -0
- package/dist/decorators/http.decorators.d.ts +130 -0
- package/dist/decorators/http.decorators.d.ts.map +1 -0
- package/dist/decorators/http.decorators.js +209 -0
- package/dist/decorators/interceptor.advanced.d.ts +93 -0
- package/dist/decorators/interceptor.advanced.d.ts.map +1 -0
- package/dist/decorators/interceptor.advanced.js +228 -0
- package/dist/decorators/interceptor.decorators.d.ts +91 -0
- package/dist/decorators/interceptor.decorators.d.ts.map +1 -0
- package/dist/decorators/interceptor.decorators.js +163 -0
- package/dist/decorators/param.decorators.d.ts +144 -0
- package/dist/decorators/param.decorators.d.ts.map +1 -0
- package/dist/decorators/param.decorators.js +205 -0
- package/dist/decorators/pipe.advanced.d.ts +125 -0
- package/dist/decorators/pipe.advanced.d.ts.map +1 -0
- package/dist/decorators/pipe.advanced.js +263 -0
- package/dist/decorators/pipe.decorators.d.ts +226 -0
- package/dist/decorators/pipe.decorators.d.ts.map +1 -0
- package/dist/decorators/pipe.decorators.js +420 -0
- package/dist/dto.d.ts +83 -0
- package/dist/dto.d.ts.map +1 -0
- package/dist/dto.js +88 -0
- package/dist/factory.d.ts +76 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +410 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/pipes/validation.pipe.d.ts +91 -0
- package/dist/pipes/validation.pipe.d.ts.map +1 -0
- package/dist/pipes/validation.pipe.js +163 -0
- package/package.json +68 -0
package/dist/dto.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DTO Utilities for WynkJS Framework
|
|
3
|
+
* Re-exports Elysia's TypeBox for DTO validation
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Export Elysia's TypeBox as DTO builder
|
|
7
|
+
* This provides runtime validation for request bodies, queries, params, etc.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { DTO } from '@wynkjs/framework';
|
|
11
|
+
*
|
|
12
|
+
* export const CreateUserDTO = DTO.Object({
|
|
13
|
+
* name: DTO.String({ minLength: 2, maxLength: 50 }),
|
|
14
|
+
* email: DTO.String({ format: 'email' }),
|
|
15
|
+
* age: DTO.Optional(DTO.Number({ minimum: 18 })),
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* @Controller('/users')
|
|
19
|
+
* export class UserController {
|
|
20
|
+
* @Post({ body: CreateUserDTO })
|
|
21
|
+
* async create(@Body() data: any) {
|
|
22
|
+
* // data is automatically validated against CreateUserDTO
|
|
23
|
+
* return { created: data };
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
*/
|
|
27
|
+
export declare const DTO: any;
|
|
28
|
+
/**
|
|
29
|
+
* Common DTO patterns for quick use
|
|
30
|
+
*/
|
|
31
|
+
export declare const CommonDTO: {
|
|
32
|
+
/**
|
|
33
|
+
* Name validation (2-50 characters)
|
|
34
|
+
*/
|
|
35
|
+
Name: (options?: {}) => import("@sinclair/typebox").TString;
|
|
36
|
+
/**
|
|
37
|
+
* Email validation
|
|
38
|
+
*/
|
|
39
|
+
Email: (options?: {}) => import("@sinclair/typebox").TString;
|
|
40
|
+
/**
|
|
41
|
+
* Password validation (min 6 characters)
|
|
42
|
+
*/
|
|
43
|
+
Password: (options?: {}) => import("@sinclair/typebox").TString;
|
|
44
|
+
/**
|
|
45
|
+
* UUID validation
|
|
46
|
+
*/
|
|
47
|
+
UUID: (options?: {}) => import("@sinclair/typebox").TString;
|
|
48
|
+
/**
|
|
49
|
+
* URL validation
|
|
50
|
+
*/
|
|
51
|
+
URL: (options?: {}) => import("@sinclair/typebox").TString;
|
|
52
|
+
/**
|
|
53
|
+
* Date string validation (ISO 8601)
|
|
54
|
+
*/
|
|
55
|
+
DateString: (options?: {}) => import("@sinclair/typebox").TString;
|
|
56
|
+
/**
|
|
57
|
+
* Phone number (Indian format)
|
|
58
|
+
*/
|
|
59
|
+
PhoneIN: (options?: {}) => import("@sinclair/typebox").TString;
|
|
60
|
+
/**
|
|
61
|
+
* Integer ID
|
|
62
|
+
*/
|
|
63
|
+
ID: (options?: {}) => import("@sinclair/typebox").TNumber;
|
|
64
|
+
/**
|
|
65
|
+
* Pagination query
|
|
66
|
+
*/
|
|
67
|
+
Pagination: () => import("@sinclair/typebox").TObject<{
|
|
68
|
+
page: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
69
|
+
limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
70
|
+
}>;
|
|
71
|
+
/**
|
|
72
|
+
* Sort query
|
|
73
|
+
*/
|
|
74
|
+
Sort: (fields: string[]) => import("@sinclair/typebox").TObject<{
|
|
75
|
+
sortBy: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<string>[]>>;
|
|
76
|
+
sortOrder: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"asc">, import("@sinclair/typebox").TLiteral<"desc">]>>;
|
|
77
|
+
}>;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Export TypeBox types for TypeScript inference
|
|
81
|
+
*/
|
|
82
|
+
export type { TSchema, Static } from "elysia";
|
|
83
|
+
//# sourceMappingURL=dto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dto.d.ts","sourceRoot":"","sources":["../core/dto.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH,eAAO,MAAM,GAAG,EAAE,GAAO,CAAC;AAE1B;;GAEG;AACH,eAAO,MAAM,SAAS;IACpB;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAQH;;OAEG;;IAGH;;OAEG;;;;;IAOH;;OAEG;mBACY,MAAM,EAAE;;;;CAKxB,CAAC;AAEF;;GAEG;AACH,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC"}
|
package/dist/dto.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DTO Utilities for WynkJS Framework
|
|
3
|
+
* Re-exports Elysia's TypeBox for DTO validation
|
|
4
|
+
*/
|
|
5
|
+
import { t } from "elysia";
|
|
6
|
+
/**
|
|
7
|
+
* Export Elysia's TypeBox as DTO builder
|
|
8
|
+
* This provides runtime validation for request bodies, queries, params, etc.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import { DTO } from '@wynkjs/framework';
|
|
12
|
+
*
|
|
13
|
+
* export const CreateUserDTO = DTO.Object({
|
|
14
|
+
* name: DTO.String({ minLength: 2, maxLength: 50 }),
|
|
15
|
+
* email: DTO.String({ format: 'email' }),
|
|
16
|
+
* age: DTO.Optional(DTO.Number({ minimum: 18 })),
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* @Controller('/users')
|
|
20
|
+
* export class UserController {
|
|
21
|
+
* @Post({ body: CreateUserDTO })
|
|
22
|
+
* async create(@Body() data: any) {
|
|
23
|
+
* // data is automatically validated against CreateUserDTO
|
|
24
|
+
* return { created: data };
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
*/
|
|
28
|
+
// Note: Elysia's type builder exposes internal option types that TypeScript
|
|
29
|
+
// cannot emit in d.ts files (TS4023). To keep declaration generation clean
|
|
30
|
+
// while preserving runtime behavior, we export DTO with an `any` type.
|
|
31
|
+
// Consumers still get runtime validation; for static typing, prefer using
|
|
32
|
+
// `Static<TSchema>` from Elysia which we re-export below.
|
|
33
|
+
export const DTO = t;
|
|
34
|
+
/**
|
|
35
|
+
* Common DTO patterns for quick use
|
|
36
|
+
*/
|
|
37
|
+
export const CommonDTO = {
|
|
38
|
+
/**
|
|
39
|
+
* Name validation (2-50 characters)
|
|
40
|
+
*/
|
|
41
|
+
Name: (options = {}) => t.String({ minLength: 2, maxLength: 50, ...options }),
|
|
42
|
+
/**
|
|
43
|
+
* Email validation
|
|
44
|
+
*/
|
|
45
|
+
Email: (options = {}) => t.String({ format: "email", ...options }),
|
|
46
|
+
/**
|
|
47
|
+
* Password validation (min 6 characters)
|
|
48
|
+
*/
|
|
49
|
+
Password: (options = {}) => t.String({ minLength: 6, ...options }),
|
|
50
|
+
/**
|
|
51
|
+
* UUID validation
|
|
52
|
+
*/
|
|
53
|
+
UUID: (options = {}) => t.String({ format: "uuid", ...options }),
|
|
54
|
+
/**
|
|
55
|
+
* URL validation
|
|
56
|
+
*/
|
|
57
|
+
URL: (options = {}) => t.String({ format: "uri", ...options }),
|
|
58
|
+
/**
|
|
59
|
+
* Date string validation (ISO 8601)
|
|
60
|
+
*/
|
|
61
|
+
DateString: (options = {}) => t.String({ format: "date-time", ...options }),
|
|
62
|
+
/**
|
|
63
|
+
* Phone number (Indian format)
|
|
64
|
+
*/
|
|
65
|
+
PhoneIN: (options = {}) => t.String({
|
|
66
|
+
pattern: "^[6-9]{1}[0-9]{9}$",
|
|
67
|
+
errorMessage: "Invalid mobile number",
|
|
68
|
+
...options,
|
|
69
|
+
}),
|
|
70
|
+
/**
|
|
71
|
+
* Integer ID
|
|
72
|
+
*/
|
|
73
|
+
ID: (options = {}) => t.Number({ minimum: 1, ...options }),
|
|
74
|
+
/**
|
|
75
|
+
* Pagination query
|
|
76
|
+
*/
|
|
77
|
+
Pagination: () => t.Object({
|
|
78
|
+
page: t.Optional(t.Number({ minimum: 1, default: 1 })),
|
|
79
|
+
limit: t.Optional(t.Number({ minimum: 1, maximum: 100, default: 10 })),
|
|
80
|
+
}),
|
|
81
|
+
/**
|
|
82
|
+
* Sort query
|
|
83
|
+
*/
|
|
84
|
+
Sort: (fields) => t.Object({
|
|
85
|
+
sortBy: t.Optional(t.Union(fields.map((f) => t.Literal(f)))),
|
|
86
|
+
sortOrder: t.Optional(t.Union([t.Literal("asc"), t.Literal("desc")])),
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Elysia } from "elysia";
|
|
2
|
+
import "reflect-metadata";
|
|
3
|
+
/**
|
|
4
|
+
* Application Factory for WynkJS Framework
|
|
5
|
+
* Creates and configures Elysia app with all decorators support
|
|
6
|
+
*/
|
|
7
|
+
export interface ApplicationOptions {
|
|
8
|
+
cors?: boolean | any;
|
|
9
|
+
globalPrefix?: string;
|
|
10
|
+
logger?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare class WynkFramework {
|
|
13
|
+
private app;
|
|
14
|
+
private controllers;
|
|
15
|
+
private globalGuards;
|
|
16
|
+
private globalInterceptors;
|
|
17
|
+
private globalPipes;
|
|
18
|
+
private globalFilters;
|
|
19
|
+
constructor(options?: ApplicationOptions);
|
|
20
|
+
/**
|
|
21
|
+
* Static convenience creator to align with documentation examples
|
|
22
|
+
*/
|
|
23
|
+
static create(options?: ApplicationOptions & {
|
|
24
|
+
controllers?: any[];
|
|
25
|
+
}): WynkFramework;
|
|
26
|
+
/**
|
|
27
|
+
* Register controllers with the application
|
|
28
|
+
*/
|
|
29
|
+
registerControllers(...controllers: any[]): this;
|
|
30
|
+
/**
|
|
31
|
+
* Register global guards
|
|
32
|
+
*/
|
|
33
|
+
useGlobalGuards(...guards: any[]): this;
|
|
34
|
+
/**
|
|
35
|
+
* Register global interceptors
|
|
36
|
+
*/
|
|
37
|
+
useGlobalInterceptors(...interceptors: any[]): this;
|
|
38
|
+
/**
|
|
39
|
+
* Register global pipes
|
|
40
|
+
*/
|
|
41
|
+
useGlobalPipes(...pipes: any[]): this;
|
|
42
|
+
/**
|
|
43
|
+
* Register global exception filters
|
|
44
|
+
*/
|
|
45
|
+
useGlobalFilters(...filters: any[]): this;
|
|
46
|
+
/**
|
|
47
|
+
* Build the application - register all routes
|
|
48
|
+
*/
|
|
49
|
+
build(): Promise<Elysia>;
|
|
50
|
+
/**
|
|
51
|
+
* Start listening on a port
|
|
52
|
+
*/
|
|
53
|
+
listen(port: number): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Get the underlying Elysia instance
|
|
56
|
+
*/
|
|
57
|
+
getApp(): Elysia;
|
|
58
|
+
/**
|
|
59
|
+
* Register a single controller
|
|
60
|
+
*/
|
|
61
|
+
private registerController;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Factory function to create a new application
|
|
65
|
+
*/
|
|
66
|
+
export declare function createApp(options?: ApplicationOptions): WynkFramework;
|
|
67
|
+
/**
|
|
68
|
+
* Alias for WynkFramework with static create method
|
|
69
|
+
*/
|
|
70
|
+
export declare class WynkFactory {
|
|
71
|
+
static create(options?: ApplicationOptions & {
|
|
72
|
+
controllers?: any[];
|
|
73
|
+
}): WynkFramework;
|
|
74
|
+
}
|
|
75
|
+
export { WynkFramework as ElysiaFramework };
|
|
76
|
+
//# sourceMappingURL=factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../core/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,kBAAkB,CAAC;AAc1B;;;GAGG;AAEH,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,OAAO,GAAG,GAAG,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,aAAa,CAAa;gBAEtB,OAAO,GAAE,kBAAuB;IAgB5C;;OAEG;IACH,MAAM,CAAC,MAAM,CACX,OAAO,GAAE,kBAAkB,GAAG;QAAE,WAAW,CAAC,EAAE,GAAG,EAAE,CAAA;KAAO,GACzD,aAAa;IAQhB;;OAEG;IACH,mBAAmB,CAAC,GAAG,WAAW,EAAE,GAAG,EAAE,GAAG,IAAI;IAKhD;;OAEG;IACH,eAAe,CAAC,GAAG,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI;IAKvC;;OAEG;IACH,qBAAqB,CAAC,GAAG,YAAY,EAAE,GAAG,EAAE,GAAG,IAAI;IAKnD;;OAEG;IACH,cAAc,CAAC,GAAG,KAAK,EAAE,GAAG,EAAE,GAAG,IAAI;IAKrC;;OAEG;IACH,gBAAgB,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI;IAKzC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC;IAkD9B;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMzC;;OAEG;IACH,MAAM,IAAI,MAAM;IAIhB;;OAEG;YACW,kBAAkB;CAuVjC;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,OAAO,GAAE,kBAAuB,GAAG,aAAa,CAEzE;AAED;;GAEG;AACH,qBAAa,WAAW;IACtB,MAAM,CAAC,MAAM,CACX,OAAO,GAAE,kBAAkB,GAAG;QAAE,WAAW,CAAC,EAAE,GAAG,EAAE,CAAA;KAAO,GACzD,aAAa;CASjB;AAGD,OAAO,EAAE,aAAa,IAAI,eAAe,EAAE,CAAC"}
|
package/dist/factory.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { Elysia } from "elysia";
|
|
2
|
+
import "reflect-metadata";
|
|
3
|
+
import { container } from "tsyringe";
|
|
4
|
+
import { createExecutionContext, executeGuards, } from "./decorators/guard.decorators";
|
|
5
|
+
import { executeInterceptors } from "./decorators/interceptor.decorators";
|
|
6
|
+
import { executePipes } from "./decorators/pipe.decorators";
|
|
7
|
+
import { executeExceptionFilters, HttpException, } from "./decorators/exception.decorators";
|
|
8
|
+
export class WynkFramework {
|
|
9
|
+
app;
|
|
10
|
+
controllers = [];
|
|
11
|
+
globalGuards = [];
|
|
12
|
+
globalInterceptors = [];
|
|
13
|
+
globalPipes = [];
|
|
14
|
+
globalFilters = [];
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.app = new Elysia();
|
|
17
|
+
// Apply CORS if enabled
|
|
18
|
+
if (options.cors) {
|
|
19
|
+
// CORS configuration would go here
|
|
20
|
+
}
|
|
21
|
+
// Apply global prefix if specified
|
|
22
|
+
if (options.globalPrefix) {
|
|
23
|
+
// Global prefix handling
|
|
24
|
+
}
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Static convenience creator to align with documentation examples
|
|
29
|
+
*/
|
|
30
|
+
static create(options = {}) {
|
|
31
|
+
const app = new WynkFramework(options);
|
|
32
|
+
if (options.controllers && options.controllers.length) {
|
|
33
|
+
app.registerControllers(...options.controllers);
|
|
34
|
+
}
|
|
35
|
+
return app;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Register controllers with the application
|
|
39
|
+
*/
|
|
40
|
+
registerControllers(...controllers) {
|
|
41
|
+
this.controllers.push(...controllers);
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Register global guards
|
|
46
|
+
*/
|
|
47
|
+
useGlobalGuards(...guards) {
|
|
48
|
+
this.globalGuards.push(...guards);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Register global interceptors
|
|
53
|
+
*/
|
|
54
|
+
useGlobalInterceptors(...interceptors) {
|
|
55
|
+
this.globalInterceptors.push(...interceptors);
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Register global pipes
|
|
60
|
+
*/
|
|
61
|
+
useGlobalPipes(...pipes) {
|
|
62
|
+
this.globalPipes.push(...pipes);
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Register global exception filters
|
|
67
|
+
*/
|
|
68
|
+
useGlobalFilters(...filters) {
|
|
69
|
+
this.globalFilters.push(...filters);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build the application - register all routes
|
|
74
|
+
*/
|
|
75
|
+
async build() {
|
|
76
|
+
// Register global error handler if filters exist
|
|
77
|
+
if (this.globalFilters.length > 0) {
|
|
78
|
+
this.app.onError(async ({ error, set, request }) => {
|
|
79
|
+
console.log("🔴 ELYSIA ON_ERROR HOOK TRIGGERED");
|
|
80
|
+
console.log("Error:", error?.message || String(error));
|
|
81
|
+
console.log("Global filters:", this.globalFilters.length);
|
|
82
|
+
// Create a simple execution context
|
|
83
|
+
const executionContext = {
|
|
84
|
+
getRequest: () => request,
|
|
85
|
+
getResponse: () => set,
|
|
86
|
+
getContext: () => ({ request, set }),
|
|
87
|
+
getHandler: () => null,
|
|
88
|
+
getClass: () => null,
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
const result = await executeExceptionFilters(this.globalFilters, error, executionContext);
|
|
92
|
+
if (result) {
|
|
93
|
+
if (result.statusCode) {
|
|
94
|
+
set.status = result.statusCode;
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (filterError) {
|
|
100
|
+
console.error("Filter error:", filterError);
|
|
101
|
+
}
|
|
102
|
+
// Fallback error handling
|
|
103
|
+
set.status = 500;
|
|
104
|
+
return {
|
|
105
|
+
statusCode: 500,
|
|
106
|
+
message: error?.message || "Internal server error",
|
|
107
|
+
error: "Internal Server Error",
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
for (const ControllerClass of this.controllers) {
|
|
112
|
+
await this.registerController(ControllerClass);
|
|
113
|
+
}
|
|
114
|
+
return this.app;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Start listening on a port
|
|
118
|
+
*/
|
|
119
|
+
async listen(port) {
|
|
120
|
+
await this.build();
|
|
121
|
+
this.app.listen(port);
|
|
122
|
+
console.log(`🚀 Application is running on http://localhost:${port}`);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get the underlying Elysia instance
|
|
126
|
+
*/
|
|
127
|
+
getApp() {
|
|
128
|
+
return this.app;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Register a single controller
|
|
132
|
+
*/
|
|
133
|
+
async registerController(ControllerClass) {
|
|
134
|
+
// Use tsyringe container to resolve controller with all dependencies
|
|
135
|
+
const instance = container.resolve(ControllerClass);
|
|
136
|
+
const basePath = Reflect.getMetadata("basePath", ControllerClass) || "";
|
|
137
|
+
// Try reading from multiple locations
|
|
138
|
+
const routes1 = Reflect.getMetadata("routes", ControllerClass) || [];
|
|
139
|
+
const routes2 = Reflect.getMetadata("routes", ControllerClass.prototype) || [];
|
|
140
|
+
const routes3 = Reflect.getMetadata("controller:routes", instance) || [];
|
|
141
|
+
console.log(`🔍 Routes on ControllerClass: ${routes1.length}`);
|
|
142
|
+
console.log(`🔍 Routes on prototype: ${routes2.length}`);
|
|
143
|
+
console.log(`🔍 Routes on instance: ${routes3.length}`);
|
|
144
|
+
const routes = routes1.length > 0 ? routes1 : routes2.length > 0 ? routes2 : routes3;
|
|
145
|
+
console.log(`📦 Registering controller ${ControllerClass.name} with ${routes.length} routes`);
|
|
146
|
+
const controllerGuards = Reflect.getMetadata("guards", ControllerClass) || [];
|
|
147
|
+
const controllerInterceptors = Reflect.getMetadata("interceptors", ControllerClass) || [];
|
|
148
|
+
const controllerPipes = Reflect.getMetadata("pipes", ControllerClass) || [];
|
|
149
|
+
const controllerFilters = Reflect.getMetadata("filters", ControllerClass) || [];
|
|
150
|
+
// Get @Use() middleware (simple pattern like user's working code)
|
|
151
|
+
const controllerUses = Reflect.getMetadata("uses", ControllerClass) || [];
|
|
152
|
+
for (const route of routes) {
|
|
153
|
+
const fullPath = basePath + route.path;
|
|
154
|
+
const method = route.method.toLowerCase();
|
|
155
|
+
const methodName = route.methodName;
|
|
156
|
+
// Get method-specific metadata
|
|
157
|
+
const methodGuards = Reflect.getMetadata("guards", instance, methodName) || [];
|
|
158
|
+
const methodInterceptors = Reflect.getMetadata("interceptors", instance, methodName) || [];
|
|
159
|
+
const methodPipes = Reflect.getMetadata("pipes", instance, methodName) || [];
|
|
160
|
+
const methodFilters = Reflect.getMetadata("filters", instance, methodName) || [];
|
|
161
|
+
// Get @Use() middleware for this method
|
|
162
|
+
const methodUses = Reflect.getMetadata("uses", instance, methodName) || [];
|
|
163
|
+
const params = Reflect.getMetadata("params", instance, methodName) || [];
|
|
164
|
+
const httpCode = Reflect.getMetadata("route:httpCode", instance, methodName);
|
|
165
|
+
const headers = Reflect.getMetadata("route:headers", instance, methodName);
|
|
166
|
+
const redirect = Reflect.getMetadata("route:redirect", instance, methodName);
|
|
167
|
+
// Combine guards, interceptors, pipes, filters (global -> controller -> method)
|
|
168
|
+
const allGuards = [
|
|
169
|
+
...this.globalGuards,
|
|
170
|
+
...controllerGuards,
|
|
171
|
+
...methodGuards,
|
|
172
|
+
];
|
|
173
|
+
const allInterceptors = [
|
|
174
|
+
...this.globalInterceptors,
|
|
175
|
+
...controllerInterceptors,
|
|
176
|
+
...methodInterceptors,
|
|
177
|
+
];
|
|
178
|
+
const allPipes = [
|
|
179
|
+
...this.globalPipes,
|
|
180
|
+
...controllerPipes,
|
|
181
|
+
...methodPipes,
|
|
182
|
+
];
|
|
183
|
+
const allFilters = [
|
|
184
|
+
...this.globalFilters,
|
|
185
|
+
...controllerFilters,
|
|
186
|
+
...methodFilters,
|
|
187
|
+
];
|
|
188
|
+
// Get route options (for body validation schema)
|
|
189
|
+
const routeOptions = route.options || {};
|
|
190
|
+
const bodySchema = Reflect.getMetadata("route:bodySchema", instance, methodName);
|
|
191
|
+
if (bodySchema && !routeOptions.body) {
|
|
192
|
+
routeOptions.body = bodySchema;
|
|
193
|
+
}
|
|
194
|
+
// Create route handler
|
|
195
|
+
const handler = async (ctx) => {
|
|
196
|
+
try {
|
|
197
|
+
// Create execution context
|
|
198
|
+
const executionContext = createExecutionContext(ctx, instance[methodName], ControllerClass);
|
|
199
|
+
// Execute guards
|
|
200
|
+
if (allGuards.length > 0) {
|
|
201
|
+
const canActivate = await executeGuards(allGuards, executionContext);
|
|
202
|
+
if (!canActivate) {
|
|
203
|
+
throw new HttpException("Forbidden", 403, "Access denied");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Prepare handler with parameters and pipes
|
|
207
|
+
const executeHandler = async () => {
|
|
208
|
+
// Build arguments for the controller method
|
|
209
|
+
const args = [];
|
|
210
|
+
if (params.length === 0) {
|
|
211
|
+
// No parameter decorators, pass full context
|
|
212
|
+
args.push(ctx);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
// Sort params by index
|
|
216
|
+
params.sort((a, b) => a.index - b.index);
|
|
217
|
+
for (const param of params) {
|
|
218
|
+
let value;
|
|
219
|
+
// Extract value based on type
|
|
220
|
+
switch (param.type) {
|
|
221
|
+
case "body":
|
|
222
|
+
value = param.data ? ctx.body?.[param.data] : ctx.body;
|
|
223
|
+
break;
|
|
224
|
+
case "param":
|
|
225
|
+
value = param.data ? ctx.params?.[param.data] : ctx.params;
|
|
226
|
+
break;
|
|
227
|
+
case "query":
|
|
228
|
+
value = param.data ? ctx.query?.[param.data] : ctx.query;
|
|
229
|
+
break;
|
|
230
|
+
case "headers":
|
|
231
|
+
value = param.data
|
|
232
|
+
? ctx.headers?.get?.(param.data) ||
|
|
233
|
+
ctx.request?.headers?.get?.(param.data)
|
|
234
|
+
: ctx.headers || ctx.request?.headers;
|
|
235
|
+
break;
|
|
236
|
+
case "request":
|
|
237
|
+
value = ctx.request || ctx;
|
|
238
|
+
break;
|
|
239
|
+
case "response":
|
|
240
|
+
value = ctx.set || ctx.response;
|
|
241
|
+
break;
|
|
242
|
+
case "context":
|
|
243
|
+
if (param.data) {
|
|
244
|
+
// Access nested property like "session.userId"
|
|
245
|
+
const keys = param.data.split(".");
|
|
246
|
+
value = keys.reduce((obj, key) => obj?.[key], ctx);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
value = ctx;
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
case "user":
|
|
253
|
+
value = param.data ? ctx.user?.[param.data] : ctx.user;
|
|
254
|
+
break;
|
|
255
|
+
case "file":
|
|
256
|
+
value = ctx.body?.file || ctx.file;
|
|
257
|
+
break;
|
|
258
|
+
case "files":
|
|
259
|
+
value = ctx.body?.files || ctx.files;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
// Apply pipes if any
|
|
263
|
+
if (param.pipes && param.pipes.length > 0) {
|
|
264
|
+
const metadata = {
|
|
265
|
+
type: param.type,
|
|
266
|
+
data: param.data,
|
|
267
|
+
};
|
|
268
|
+
value = await executePipes(param.pipes, value, metadata);
|
|
269
|
+
}
|
|
270
|
+
// Apply global/controller/method pipes
|
|
271
|
+
if (allPipes.length > 0) {
|
|
272
|
+
const metadata = {
|
|
273
|
+
type: param.type,
|
|
274
|
+
data: param.data,
|
|
275
|
+
};
|
|
276
|
+
value = await executePipes(allPipes, value, metadata);
|
|
277
|
+
}
|
|
278
|
+
args[param.index] = value;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Call controller method
|
|
282
|
+
return await instance[methodName].apply(instance, args);
|
|
283
|
+
};
|
|
284
|
+
// Execute interceptors
|
|
285
|
+
let result;
|
|
286
|
+
if (allInterceptors.length > 0) {
|
|
287
|
+
result = await executeInterceptors(allInterceptors, executionContext, executeHandler);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
result = await executeHandler();
|
|
291
|
+
}
|
|
292
|
+
// Handle redirect
|
|
293
|
+
if (redirect) {
|
|
294
|
+
ctx.set.redirect = redirect.url;
|
|
295
|
+
ctx.set.status = redirect.statusCode;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// Set custom HTTP code
|
|
299
|
+
if (httpCode) {
|
|
300
|
+
ctx.set.status = httpCode;
|
|
301
|
+
}
|
|
302
|
+
// Set custom headers
|
|
303
|
+
if (headers) {
|
|
304
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
305
|
+
ctx.set.headers[key] = value;
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
console.log("🔴 ERROR CAUGHT IN FACTORY");
|
|
312
|
+
console.log("allFilters.length:", allFilters.length);
|
|
313
|
+
console.log("Error:", error?.message);
|
|
314
|
+
// Execute exception filters
|
|
315
|
+
if (allFilters.length > 0) {
|
|
316
|
+
console.log("✅ Executing exception filters...");
|
|
317
|
+
const executionContext = createExecutionContext(ctx, instance[methodName], ControllerClass);
|
|
318
|
+
try {
|
|
319
|
+
const result = await executeExceptionFilters(allFilters, error, executionContext);
|
|
320
|
+
if (result) {
|
|
321
|
+
if (result.statusCode) {
|
|
322
|
+
ctx.set.status = result.statusCode;
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch (filterError) {
|
|
328
|
+
// If filter doesn't handle it, continue to default error handling
|
|
329
|
+
error = filterError;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
console.log("❌ No filters registered for this route");
|
|
334
|
+
}
|
|
335
|
+
// Default error handling
|
|
336
|
+
if (error instanceof HttpException) {
|
|
337
|
+
ctx.set.status = error.getStatus();
|
|
338
|
+
return error.getResponse();
|
|
339
|
+
}
|
|
340
|
+
// Unknown error
|
|
341
|
+
ctx.set.status = 500;
|
|
342
|
+
return {
|
|
343
|
+
statusCode: 500,
|
|
344
|
+
message: error.message || "Internal server error",
|
|
345
|
+
error: "Internal Server Error",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
// Wrap handler with @Use() middleware if present
|
|
350
|
+
// Combine controller and method middleware
|
|
351
|
+
const allUses = [...controllerUses, ...methodUses];
|
|
352
|
+
let finalHandler = handler;
|
|
353
|
+
if (allUses.length > 0) {
|
|
354
|
+
console.log(`🔗 Building middleware chain for ${method} ${fullPath}:`);
|
|
355
|
+
console.log(` Middleware count: ${allUses.length}`);
|
|
356
|
+
allUses.forEach((m, i) => console.log(` [${i}] ${m.name || "anonymous"}`));
|
|
357
|
+
// Build middleware chain using reduce (O(n) complexity)
|
|
358
|
+
// Builds from right to left: handler <- middleware[n-1] <- ... <- middleware[0]
|
|
359
|
+
// Executes left to right: middleware[0] -> ... -> middleware[n-1] -> handler
|
|
360
|
+
finalHandler = allUses.reduceRight((next, middleware, index) => {
|
|
361
|
+
return async (ctx) => {
|
|
362
|
+
console.log(`▶️ Executing middleware [${index}]: ${middleware.name || "anonymous"}`);
|
|
363
|
+
return await middleware(ctx, () => next(ctx));
|
|
364
|
+
};
|
|
365
|
+
}, handler);
|
|
366
|
+
}
|
|
367
|
+
// Register route with Elysia
|
|
368
|
+
const elysiaOptions = {};
|
|
369
|
+
if (routeOptions.body || bodySchema) {
|
|
370
|
+
elysiaOptions.body = routeOptions.body || bodySchema;
|
|
371
|
+
}
|
|
372
|
+
if (routeOptions.query) {
|
|
373
|
+
elysiaOptions.query = routeOptions.query;
|
|
374
|
+
}
|
|
375
|
+
if (routeOptions.params) {
|
|
376
|
+
elysiaOptions.params = routeOptions.params;
|
|
377
|
+
}
|
|
378
|
+
if (routeOptions.headers) {
|
|
379
|
+
elysiaOptions.headers = routeOptions.headers;
|
|
380
|
+
}
|
|
381
|
+
// Register with options if any validation schemas are present
|
|
382
|
+
if (Object.keys(elysiaOptions).length > 0) {
|
|
383
|
+
this.app[method](fullPath, finalHandler, elysiaOptions);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
this.app[method](fullPath, finalHandler);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Factory function to create a new application
|
|
393
|
+
*/
|
|
394
|
+
export function createApp(options = {}) {
|
|
395
|
+
return new WynkFramework(options);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Alias for WynkFramework with static create method
|
|
399
|
+
*/
|
|
400
|
+
export class WynkFactory {
|
|
401
|
+
static create(options = {}) {
|
|
402
|
+
const app = new WynkFramework(options);
|
|
403
|
+
if (options.controllers) {
|
|
404
|
+
app.registerControllers(...options.controllers);
|
|
405
|
+
}
|
|
406
|
+
return app;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// Export ElysiaFramework as alias for backwards compatibility
|
|
410
|
+
export { WynkFramework as ElysiaFramework };
|