wynkjs 1.0.0 → 1.0.2
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 +776 -129
- package/dist/decorators/exception.advanced.d.ts +43 -14
- package/dist/decorators/exception.advanced.d.ts.map +1 -1
- package/dist/decorators/exception.advanced.js +132 -15
- package/dist/decorators/http.decorators.d.ts +14 -12
- package/dist/decorators/http.decorators.d.ts.map +1 -1
- package/dist/decorators/http.decorators.js +34 -16
- package/dist/dto.d.ts +15 -5
- package/dist/dto.d.ts.map +1 -1
- package/dist/dto.js +37 -9
- package/dist/factory.d.ts +3 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +87 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/package.json +5 -4
|
@@ -1,27 +1,56 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
import { WynkExceptionFilter } from "./exception.decorators";
|
|
3
3
|
import { ExecutionContext } from "./guard.decorators";
|
|
4
|
-
import { NotFoundException,
|
|
4
|
+
import { NotFoundException, UnauthorizedException, ForbiddenException } from "./exception.decorators";
|
|
5
5
|
/**
|
|
6
6
|
* Advanced Exception Filters for WynkJS Framework
|
|
7
7
|
* Specialized filters for different error scenarios
|
|
8
8
|
*/
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* Error Formatter Interface
|
|
11
|
+
* Used by ValidationExceptionFilter to format validation errors
|
|
12
|
+
*/
|
|
13
|
+
export interface ErrorFormatter {
|
|
14
|
+
format(validationError: any): any;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* FormatErrorFormatter - Formats as { field: [messages] } like NestJS
|
|
18
|
+
*/
|
|
19
|
+
export declare class FormatErrorFormatter implements ErrorFormatter {
|
|
20
|
+
format(error: any): any;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* SimpleErrorFormatter - Formats as simple array of messages
|
|
24
|
+
*/
|
|
25
|
+
export declare class SimpleErrorFormatter implements ErrorFormatter {
|
|
26
|
+
format(error: any): any;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* DetailedErrorFormatter - Formats with detailed field info
|
|
30
|
+
*/
|
|
31
|
+
export declare class DetailedErrorFormatter implements ErrorFormatter {
|
|
32
|
+
format(error: any): any;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validation Exception Filter - Handles validation errors with customizable formatting
|
|
11
36
|
* @example
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
37
|
+
* // With FormatErrorFormatter (NestJS-style)
|
|
38
|
+
* app.useGlobalFilters(new ValidationExceptionFilter(new FormatErrorFormatter()));
|
|
39
|
+
*
|
|
40
|
+
* // With SimpleErrorFormatter
|
|
41
|
+
* app.useGlobalFilters(new ValidationExceptionFilter(new SimpleErrorFormatter()));
|
|
42
|
+
*
|
|
43
|
+
* // With DetailedErrorFormatter
|
|
44
|
+
* app.useGlobalFilters(new ValidationExceptionFilter(new DetailedErrorFormatter()));
|
|
45
|
+
*
|
|
46
|
+
* // Without formatter (default detailed format)
|
|
47
|
+
* app.useGlobalFilters(new ValidationExceptionFilter());
|
|
15
48
|
*/
|
|
16
|
-
export declare class ValidationExceptionFilter implements WynkExceptionFilter
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
errors: any;
|
|
22
|
-
timestamp: string;
|
|
23
|
-
path: any;
|
|
24
|
-
};
|
|
49
|
+
export declare class ValidationExceptionFilter implements WynkExceptionFilter {
|
|
50
|
+
private formatter;
|
|
51
|
+
constructor(formatter?: ErrorFormatter);
|
|
52
|
+
catch(exception: any, context: ExecutionContext): any;
|
|
53
|
+
private isValidationError;
|
|
25
54
|
}
|
|
26
55
|
/**
|
|
27
56
|
* Database Exception Filter - Handles database errors
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exception.advanced.d.ts","sourceRoot":"","sources":["../../core/decorators/exception.advanced.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAEL,iBAAiB,
|
|
1
|
+
{"version":3,"file":"exception.advanced.d.ts","sourceRoot":"","sources":["../../core/decorators/exception.advanced.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAEL,iBAAiB,EAEjB,qBAAqB,EACrB,kBAAkB,EAEnB,MAAM,wBAAwB,CAAC;AAEhC;;;GAGG;AAEH;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,eAAe,EAAE,GAAG,GAAG,GAAG,CAAC;CACnC;AAED;;GAEG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,MAAM,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG;CAsBxB;AAED;;GAEG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,MAAM,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG;CAiBxB;AAED;;GAEG;AACH,qBAAa,sBAAuB,YAAW,cAAc;IAC3D,MAAM,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG;CAgCxB;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,yBAA0B,YAAW,mBAAmB;IACnE,OAAO,CAAC,SAAS,CAA+B;gBAEpC,SAAS,CAAC,EAAE,cAAc;IAItC,KAAK,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB;IAgC/C,OAAO,CAAC,iBAAiB;CAiB1B;AAED;;;;;;GAMG;AACH,qBAAa,uBAAwB,YAAW,mBAAmB;IACjE,KAAK,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB;;;;;;;CAiChD;AAED;;;;;;GAMG;AACH,qBAAa,6BACX,YAAW,mBAAmB,CAAC,qBAAqB,CAAC;IAErD,KAAK,CAAC,SAAS,EAAE,qBAAqB,EAAE,OAAO,EAAE,gBAAgB;;;;;;;;CAalE;AAED;;;;;;GAMG;AACH,qBAAa,4BACX,YAAW,mBAAmB,CAAC,kBAAkB,CAAC;IAElD,KAAK,CAAC,SAAS,EAAE,kBAAkB,EAAE,OAAO,EAAE,gBAAgB;;;;;;;;CAe/D;AAED;;;;;;GAMG;AACH,qBAAa,uBACX,YAAW,mBAAmB,CAAC,iBAAiB,CAAC;IAEjD,KAAK,CAAC,SAAS,EAAE,iBAAiB,EAAE,OAAO,EAAE,gBAAgB;;;;;;;;CAa9D;AAED;;;;;;GAMG;AACH,qBAAa,wBAAyB,YAAW,mBAAmB;IAClE,KAAK,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB;;;;;;;;;CAchD;AAED;;;;;;GAMG;AACH,qBAAa,4BAA6B,YAAW,mBAAmB;IACtE,KAAK,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB;;;;;;;;;CAchD;AAED;;;;;;GAMG;AACH,qBAAa,yBAA0B,YAAW,mBAAmB;IACnE,KAAK,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB;;;;;;;CAsBhD;AAED;;;;GAIG;AACH,qBAAa,qBAAsB,YAAW,mBAAmB;IAC/D,KAAK,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,gBAAgB;;;;;;;;CA2BhD"}
|
|
@@ -1,27 +1,144 @@
|
|
|
1
1
|
import "reflect-metadata";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* Specialized filters for different error scenarios
|
|
3
|
+
* FormatErrorFormatter - Formats as { field: [messages] } like NestJS
|
|
5
4
|
*/
|
|
5
|
+
export class FormatErrorFormatter {
|
|
6
|
+
format(error) {
|
|
7
|
+
const formattedErrors = {};
|
|
8
|
+
if (error.errors && error.errors.length > 0) {
|
|
9
|
+
error.errors.forEach((err) => {
|
|
10
|
+
const field = err.path?.replace(/^\//, "") || "unknown";
|
|
11
|
+
if (!formattedErrors[field]) {
|
|
12
|
+
formattedErrors[field] = [];
|
|
13
|
+
}
|
|
14
|
+
formattedErrors[field].push(err.summary || err.message);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
const field = error.property?.replace(/^\//, "") || "unknown";
|
|
19
|
+
formattedErrors[field] = [error.summary || error.message];
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
statusCode: 400,
|
|
23
|
+
message: "Validation failed",
|
|
24
|
+
errors: formattedErrors,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* SimpleErrorFormatter - Formats as simple array of messages
|
|
30
|
+
*/
|
|
31
|
+
export class SimpleErrorFormatter {
|
|
32
|
+
format(error) {
|
|
33
|
+
const messages = [];
|
|
34
|
+
if (error.errors && error.errors.length > 0) {
|
|
35
|
+
error.errors.forEach((err) => {
|
|
36
|
+
messages.push(err.summary || err.message);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
messages.push(error.summary || error.message);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
statusCode: 400,
|
|
44
|
+
message: "Validation failed",
|
|
45
|
+
errors: messages,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
6
49
|
/**
|
|
7
|
-
*
|
|
50
|
+
* DetailedErrorFormatter - Formats with detailed field info
|
|
51
|
+
*/
|
|
52
|
+
export class DetailedErrorFormatter {
|
|
53
|
+
format(error) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
if (error.errors && error.errors.length > 0) {
|
|
56
|
+
error.errors.forEach((err) => {
|
|
57
|
+
errors.push({
|
|
58
|
+
field: err.path?.replace(/^\//, "") || "unknown",
|
|
59
|
+
message: err.summary || err.message,
|
|
60
|
+
value: err.value,
|
|
61
|
+
expected: err.schema,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
errors.push({
|
|
67
|
+
field: error.property?.replace(/^\//, "") || "unknown",
|
|
68
|
+
message: error.summary || error.message,
|
|
69
|
+
value: error.found,
|
|
70
|
+
expected: error.expected,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
statusCode: 400,
|
|
75
|
+
message: "Validation failed",
|
|
76
|
+
errors,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Validation Exception Filter - Handles validation errors with customizable formatting
|
|
8
82
|
* @example
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
83
|
+
* // With FormatErrorFormatter (NestJS-style)
|
|
84
|
+
* app.useGlobalFilters(new ValidationExceptionFilter(new FormatErrorFormatter()));
|
|
85
|
+
*
|
|
86
|
+
* // With SimpleErrorFormatter
|
|
87
|
+
* app.useGlobalFilters(new ValidationExceptionFilter(new SimpleErrorFormatter()));
|
|
88
|
+
*
|
|
89
|
+
* // With DetailedErrorFormatter
|
|
90
|
+
* app.useGlobalFilters(new ValidationExceptionFilter(new DetailedErrorFormatter()));
|
|
91
|
+
*
|
|
92
|
+
* // Without formatter (default detailed format)
|
|
93
|
+
* app.useGlobalFilters(new ValidationExceptionFilter());
|
|
12
94
|
*/
|
|
13
95
|
export class ValidationExceptionFilter {
|
|
96
|
+
formatter = null;
|
|
97
|
+
constructor(formatter) {
|
|
98
|
+
this.formatter = formatter || null;
|
|
99
|
+
}
|
|
14
100
|
catch(exception, context) {
|
|
15
|
-
const response = context.getResponse();
|
|
16
101
|
const request = context.getRequest();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
102
|
+
// Check if this is a validation error from Elysia
|
|
103
|
+
const isValidationError = this.isValidationError(exception);
|
|
104
|
+
if (!isValidationError) {
|
|
105
|
+
// Not a validation error, re-throw to let other filters handle it
|
|
106
|
+
throw exception;
|
|
107
|
+
}
|
|
108
|
+
// Parse the validation error
|
|
109
|
+
let validationError;
|
|
110
|
+
if (typeof exception.message === "string") {
|
|
111
|
+
try {
|
|
112
|
+
validationError = JSON.parse(exception.message);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
validationError = { message: exception.message };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
validationError = exception;
|
|
120
|
+
}
|
|
121
|
+
// Format the error using the provided formatter
|
|
122
|
+
if (this.formatter) {
|
|
123
|
+
return this.formatter.format(validationError);
|
|
124
|
+
}
|
|
125
|
+
// Default format (detailed)
|
|
126
|
+
return new DetailedErrorFormatter().format(validationError);
|
|
127
|
+
}
|
|
128
|
+
isValidationError(exception) {
|
|
129
|
+
if (!exception)
|
|
130
|
+
return false;
|
|
131
|
+
// Check if it's a validation error by looking at the error structure
|
|
132
|
+
if (exception.message && typeof exception.message === "string") {
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(exception.message);
|
|
135
|
+
return parsed.type === "validation";
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return (exception.type === "validation" || exception.code === "VALIDATION_ERROR");
|
|
25
142
|
}
|
|
26
143
|
}
|
|
27
144
|
/**
|
|
@@ -21,13 +21,15 @@ export interface RouteOptions {
|
|
|
21
21
|
export declare function Controller(path?: string): ClassDecorator;
|
|
22
22
|
/**
|
|
23
23
|
* HTTP GET decorator
|
|
24
|
-
* @param
|
|
25
|
-
* @param options Route configuration options
|
|
24
|
+
* @param pathOrOptions Route path or options with DTO
|
|
26
25
|
* @example
|
|
27
26
|
* @Get('/profile')
|
|
28
27
|
* async getProfile() {}
|
|
28
|
+
*
|
|
29
|
+
* @Get({ path: '/:id', params: UserIdDTO, query: QueryDTO })
|
|
30
|
+
* async findOne(@Param('id') id: string, @Query() query: any) {}
|
|
29
31
|
*/
|
|
30
|
-
export declare function Get(
|
|
32
|
+
export declare function Get(pathOrOptions?: string | RouteOptions): MethodDecorator;
|
|
31
33
|
/**
|
|
32
34
|
* @Post decorator - Define a POST route
|
|
33
35
|
* @param pathOrOptions Optional route path or options with DTO
|
|
@@ -66,25 +68,25 @@ export declare function Put(pathOrOptions?: string | RouteOptions): MethodDecora
|
|
|
66
68
|
export declare function Patch(pathOrOptions?: string | RouteOptions): MethodDecorator;
|
|
67
69
|
/**
|
|
68
70
|
* HTTP DELETE decorator
|
|
69
|
-
* @param
|
|
70
|
-
* @param options Route configuration options
|
|
71
|
+
* @param pathOrOptions Route path or options with DTO
|
|
71
72
|
* @example
|
|
72
73
|
* @Delete('/:id')
|
|
73
74
|
* async remove(@Param('id') id: string) {}
|
|
75
|
+
*
|
|
76
|
+
* @Delete({ path: '/:id', params: UserIdDTO })
|
|
77
|
+
* async remove(@Param('id') id: string) {}
|
|
74
78
|
*/
|
|
75
|
-
export declare function Delete(
|
|
79
|
+
export declare function Delete(pathOrOptions?: string | RouteOptions): MethodDecorator;
|
|
76
80
|
/**
|
|
77
81
|
* HTTP OPTIONS decorator
|
|
78
|
-
* @param
|
|
79
|
-
* @param options Route configuration options
|
|
82
|
+
* @param pathOrOptions Route path or options with DTO
|
|
80
83
|
*/
|
|
81
|
-
export declare function Options(
|
|
84
|
+
export declare function Options(pathOrOptions?: string | RouteOptions): MethodDecorator;
|
|
82
85
|
/**
|
|
83
86
|
* HTTP HEAD decorator
|
|
84
|
-
* @param
|
|
85
|
-
* @param options Route configuration options
|
|
87
|
+
* @param pathOrOptions Route path or options with DTO
|
|
86
88
|
*/
|
|
87
|
-
export declare function Head(
|
|
89
|
+
export declare function Head(pathOrOptions?: string | RouteOptions): MethodDecorator;
|
|
88
90
|
/**
|
|
89
91
|
* Set custom HTTP status code for a route
|
|
90
92
|
* @param code HTTP status code
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.decorators.d.ts","sourceRoot":"","sources":["../../core/decorators/http.decorators.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B;;;GAGG;AAEH,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAE,MAAW,GAAG,cAAc,CAK5D;AAED
|
|
1
|
+
{"version":3,"file":"http.decorators.d.ts","sourceRoot":"","sources":["../../core/decorators/http.decorators.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAE1B;;;GAGG;AAEH,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,GAAE,MAAW,GAAG,cAAc,CAK5D;AAED;;;;;;;;;GASG;AACH,wBAAgB,GAAG,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,eAAe,CAM1E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,IAAI,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,eAAe,CAM3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,GAAG,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,eAAe,CAM1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,eAAe,CAM5E;AAED;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,eAAe,CAM7E;AAED;;;GAGG;AACH,wBAAgB,OAAO,CACrB,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,GACpC,eAAe,CAMjB;AAED;;;GAGG;AACH,wBAAgB,IAAI,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,eAAe,CAM3E;AAmDD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAStD;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,eAAe,CAYnE;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,MAAM,EACX,UAAU,GAAE,MAAY,GACvB,eAAe,CAcjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,GAAG,CAAC,GAAG,WAAW,EAAE,GAAG,EAAE,GAAG,cAAc,GAAG,eAAe,CAuB3E"}
|
|
@@ -14,14 +14,20 @@ export function Controller(path = "") {
|
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* HTTP GET decorator
|
|
17
|
-
* @param
|
|
18
|
-
* @param options Route configuration options
|
|
17
|
+
* @param pathOrOptions Route path or options with DTO
|
|
19
18
|
* @example
|
|
20
19
|
* @Get('/profile')
|
|
21
20
|
* async getProfile() {}
|
|
21
|
+
*
|
|
22
|
+
* @Get({ path: '/:id', params: UserIdDTO, query: QueryDTO })
|
|
23
|
+
* async findOne(@Param('id') id: string, @Query() query: any) {}
|
|
22
24
|
*/
|
|
23
|
-
export function Get(
|
|
24
|
-
|
|
25
|
+
export function Get(pathOrOptions) {
|
|
26
|
+
if (typeof pathOrOptions === "string") {
|
|
27
|
+
return createRouteDecorator("GET", pathOrOptions);
|
|
28
|
+
}
|
|
29
|
+
const options = pathOrOptions || {};
|
|
30
|
+
return createRouteDecorator("GET", options.path || "", options);
|
|
25
31
|
}
|
|
26
32
|
/**
|
|
27
33
|
* @Post decorator - Define a POST route
|
|
@@ -79,30 +85,42 @@ export function Patch(pathOrOptions) {
|
|
|
79
85
|
}
|
|
80
86
|
/**
|
|
81
87
|
* HTTP DELETE decorator
|
|
82
|
-
* @param
|
|
83
|
-
* @param options Route configuration options
|
|
88
|
+
* @param pathOrOptions Route path or options with DTO
|
|
84
89
|
* @example
|
|
85
90
|
* @Delete('/:id')
|
|
86
91
|
* async remove(@Param('id') id: string) {}
|
|
92
|
+
*
|
|
93
|
+
* @Delete({ path: '/:id', params: UserIdDTO })
|
|
94
|
+
* async remove(@Param('id') id: string) {}
|
|
87
95
|
*/
|
|
88
|
-
export function Delete(
|
|
89
|
-
|
|
96
|
+
export function Delete(pathOrOptions) {
|
|
97
|
+
if (typeof pathOrOptions === "string") {
|
|
98
|
+
return createRouteDecorator("DELETE", pathOrOptions);
|
|
99
|
+
}
|
|
100
|
+
const options = pathOrOptions || {};
|
|
101
|
+
return createRouteDecorator("DELETE", options.path || "", options);
|
|
90
102
|
}
|
|
91
103
|
/**
|
|
92
104
|
* HTTP OPTIONS decorator
|
|
93
|
-
* @param
|
|
94
|
-
* @param options Route configuration options
|
|
105
|
+
* @param pathOrOptions Route path or options with DTO
|
|
95
106
|
*/
|
|
96
|
-
export function Options(
|
|
97
|
-
|
|
107
|
+
export function Options(pathOrOptions) {
|
|
108
|
+
if (typeof pathOrOptions === "string") {
|
|
109
|
+
return createRouteDecorator("OPTIONS", pathOrOptions);
|
|
110
|
+
}
|
|
111
|
+
const options = pathOrOptions || {};
|
|
112
|
+
return createRouteDecorator("OPTIONS", options.path || "", options);
|
|
98
113
|
}
|
|
99
114
|
/**
|
|
100
115
|
* HTTP HEAD decorator
|
|
101
|
-
* @param
|
|
102
|
-
* @param options Route configuration options
|
|
116
|
+
* @param pathOrOptions Route path or options with DTO
|
|
103
117
|
*/
|
|
104
|
-
export function Head(
|
|
105
|
-
|
|
118
|
+
export function Head(pathOrOptions) {
|
|
119
|
+
if (typeof pathOrOptions === "string") {
|
|
120
|
+
return createRouteDecorator("HEAD", pathOrOptions);
|
|
121
|
+
}
|
|
122
|
+
const options = pathOrOptions || {};
|
|
123
|
+
return createRouteDecorator("HEAD", options.path || "", options);
|
|
106
124
|
}
|
|
107
125
|
/**
|
|
108
126
|
* Helper function to create route decorators
|
package/dist/dto.d.ts
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* DTO Utilities for WynkJS Framework
|
|
3
3
|
* Re-exports Elysia's TypeBox for DTO validation
|
|
4
4
|
*/
|
|
5
|
+
import { t, type TSchema } from "elysia";
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* DTO Builder - Full TypeBox support with IntelliSense
|
|
8
|
+
*
|
|
9
|
+
* Provides runtime validation for request bodies, queries, params, etc.
|
|
8
10
|
*
|
|
9
11
|
* @example
|
|
10
|
-
* import { DTO } from '
|
|
12
|
+
* import { DTO } from 'wynkjs';
|
|
11
13
|
*
|
|
12
14
|
* export const CreateUserDTO = DTO.Object({
|
|
13
15
|
* name: DTO.String({ minLength: 2, maxLength: 50 }),
|
|
@@ -24,7 +26,14 @@
|
|
|
24
26
|
* }
|
|
25
27
|
* }
|
|
26
28
|
*/
|
|
27
|
-
export declare const DTO:
|
|
29
|
+
export declare const DTO: typeof t & {
|
|
30
|
+
/**
|
|
31
|
+
* Create a strict object schema that strips additional properties
|
|
32
|
+
* @param properties - Object properties schema
|
|
33
|
+
* @param options - Additional schema options
|
|
34
|
+
*/
|
|
35
|
+
Strict: (properties: Record<string, TSchema>, options?: Record<string, any>) => TSchema;
|
|
36
|
+
};
|
|
28
37
|
/**
|
|
29
38
|
* Common DTO patterns for quick use
|
|
30
39
|
*/
|
|
@@ -35,8 +44,9 @@ export declare const CommonDTO: {
|
|
|
35
44
|
Name: (options?: {}) => import("@sinclair/typebox").TString;
|
|
36
45
|
/**
|
|
37
46
|
* Email validation
|
|
47
|
+
* Uses format: email which validates basic email structure
|
|
38
48
|
*/
|
|
39
|
-
Email: (options?:
|
|
49
|
+
Email: (options?: any) => import("@sinclair/typebox").TString;
|
|
40
50
|
/**
|
|
41
51
|
* Password validation (min 6 characters)
|
|
42
52
|
*/
|
package/dist/dto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dto.d.ts","sourceRoot":"","sources":["../core/dto.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"dto.d.ts","sourceRoot":"","sources":["../core/dto.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,GAAG,EAAQ,OAAO,CAAC,GAAG;IACjC;;;;OAIG;IACH,MAAM,EAAE,CACN,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAC1B,OAAO,CAAC;CACd,CAAC;AAiCF;;GAEG;AACH,eAAO,MAAM,SAAS;IACpB;;OAEG;;IAGH;;;OAGG;sBACc,GAAG;IAOpB;;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
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { t } from "elysia";
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* DTO Builder - Full TypeBox support with IntelliSense
|
|
8
|
+
*
|
|
9
|
+
* Provides runtime validation for request bodies, queries, params, etc.
|
|
9
10
|
*
|
|
10
11
|
* @example
|
|
11
|
-
* import { DTO } from '
|
|
12
|
+
* import { DTO } from 'wynkjs';
|
|
12
13
|
*
|
|
13
14
|
* export const CreateUserDTO = DTO.Object({
|
|
14
15
|
* name: DTO.String({ minLength: 2, maxLength: 50 }),
|
|
@@ -25,12 +26,34 @@ import { t } from "elysia";
|
|
|
25
26
|
* }
|
|
26
27
|
* }
|
|
27
28
|
*/
|
|
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
29
|
export const DTO = t;
|
|
30
|
+
/**
|
|
31
|
+
* Helper to attach strict validation to a DTO Object schema
|
|
32
|
+
* Sets additionalProperties to false which makes Elysia strip unknown fields
|
|
33
|
+
*
|
|
34
|
+
* NOTE: Elysia's default behavior is to STRIP additional properties, not throw errors.
|
|
35
|
+
* This is secure by default. If you need to throw errors on unknown properties,
|
|
36
|
+
* use the approach documented in STRICT_VALIDATION.md
|
|
37
|
+
*
|
|
38
|
+
* @param properties - Object properties schema
|
|
39
|
+
* @param options - Additional schema options
|
|
40
|
+
* @returns TypeBox schema with additionalProperties: false
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* export const UserDTO = DTO.Strict({
|
|
44
|
+
* email: DTO.String(),
|
|
45
|
+
* age: DTO.Number()
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* // Request: { email: "test@test.com", age: 25, extra: "field" }
|
|
49
|
+
* // Result: { email: "test@test.com", age: 25 } ← extra stripped (secure)
|
|
50
|
+
*/
|
|
51
|
+
DTO.Strict = (properties, options = {}) => {
|
|
52
|
+
return t.Object(properties, {
|
|
53
|
+
...options,
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
34
57
|
/**
|
|
35
58
|
* Common DTO patterns for quick use
|
|
36
59
|
*/
|
|
@@ -41,8 +64,13 @@ export const CommonDTO = {
|
|
|
41
64
|
Name: (options = {}) => t.String({ minLength: 2, maxLength: 50, ...options }),
|
|
42
65
|
/**
|
|
43
66
|
* Email validation
|
|
67
|
+
* Uses format: email which validates basic email structure
|
|
44
68
|
*/
|
|
45
|
-
Email: (options = {}) => t.String({
|
|
69
|
+
Email: (options = {}) => t.String({
|
|
70
|
+
format: "email",
|
|
71
|
+
error: "Invalid email address",
|
|
72
|
+
...options,
|
|
73
|
+
}),
|
|
46
74
|
/**
|
|
47
75
|
* Password validation (min 6 characters)
|
|
48
76
|
*/
|
package/dist/factory.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Elysia } from "elysia";
|
|
2
2
|
import "reflect-metadata";
|
|
3
|
+
import { ErrorFormatter } from "./decorators/exception.advanced";
|
|
3
4
|
/**
|
|
4
5
|
* Application Factory for WynkJS Framework
|
|
5
6
|
* Creates and configures Elysia app with all decorators support
|
|
@@ -8,6 +9,7 @@ export interface ApplicationOptions {
|
|
|
8
9
|
cors?: boolean | any;
|
|
9
10
|
globalPrefix?: string;
|
|
10
11
|
logger?: boolean;
|
|
12
|
+
validationErrorFormatter?: ErrorFormatter;
|
|
11
13
|
}
|
|
12
14
|
export declare class WynkFramework {
|
|
13
15
|
private app;
|
|
@@ -16,6 +18,7 @@ export declare class WynkFramework {
|
|
|
16
18
|
private globalInterceptors;
|
|
17
19
|
private globalPipes;
|
|
18
20
|
private globalFilters;
|
|
21
|
+
private validationFormatter?;
|
|
19
22
|
constructor(options?: ApplicationOptions);
|
|
20
23
|
/**
|
|
21
24
|
* Static convenience creator to align with documentation examples
|
package/dist/factory.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
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,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjE;;;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;IACjB,wBAAwB,CAAC,EAAE,cAAc,CAAC;CAC3C;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;IAClC,OAAO,CAAC,mBAAmB,CAAC,CAAiB;gBAEjC,OAAO,GAAE,kBAAuB;IAmH5C;;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
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Elysia } from "elysia";
|
|
2
2
|
import "reflect-metadata";
|
|
3
3
|
import { container } from "tsyringe";
|
|
4
|
+
import { Value } from "@sinclair/typebox/value";
|
|
4
5
|
import { createExecutionContext, executeGuards, } from "./decorators/guard.decorators";
|
|
5
6
|
import { executeInterceptors } from "./decorators/interceptor.decorators";
|
|
6
7
|
import { executePipes } from "./decorators/pipe.decorators";
|
|
@@ -12,8 +13,94 @@ export class WynkFramework {
|
|
|
12
13
|
globalInterceptors = [];
|
|
13
14
|
globalPipes = [];
|
|
14
15
|
globalFilters = [];
|
|
16
|
+
validationFormatter;
|
|
15
17
|
constructor(options = {}) {
|
|
16
18
|
this.app = new Elysia();
|
|
19
|
+
this.validationFormatter = options.validationErrorFormatter;
|
|
20
|
+
// Configure Elysia's error handling for validation errors
|
|
21
|
+
this.app.onError(({ code, error, set }) => {
|
|
22
|
+
// Handle ValidationError from Elysia
|
|
23
|
+
if (code === "VALIDATION" ||
|
|
24
|
+
error?.constructor?.name === "ValidationError") {
|
|
25
|
+
const validationError = error;
|
|
26
|
+
set.status = 400;
|
|
27
|
+
// Try to collect all validation errors using TypeBox
|
|
28
|
+
const allErrors = {};
|
|
29
|
+
// Check if we have the validator and value to collect all errors
|
|
30
|
+
if (validationError.validator && validationError.value) {
|
|
31
|
+
const schema = validationError.validator.schema || validationError.validator;
|
|
32
|
+
// Use TypeBox's Errors iterator to collect ALL validation errors
|
|
33
|
+
try {
|
|
34
|
+
const errors = [...Value.Errors(schema, validationError.value)];
|
|
35
|
+
if (errors.length > 0) {
|
|
36
|
+
errors.forEach((err) => {
|
|
37
|
+
const field = err.path?.replace(/^\//, "") || "unknown";
|
|
38
|
+
if (!allErrors[field]) {
|
|
39
|
+
allErrors[field] = [];
|
|
40
|
+
}
|
|
41
|
+
allErrors[field].push(err.message || "Validation failed");
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// Fallback to single error
|
|
46
|
+
const field = validationError.valueError?.path?.replace(/^\//, "") ||
|
|
47
|
+
validationError.on ||
|
|
48
|
+
"body";
|
|
49
|
+
const message = validationError.customError ||
|
|
50
|
+
validationError.valueError?.message ||
|
|
51
|
+
"Validation failed";
|
|
52
|
+
allErrors[field] = [message];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
// Fallback to single error if TypeBox iteration fails
|
|
57
|
+
const field = validationError.valueError?.path?.replace(/^\//, "") ||
|
|
58
|
+
validationError.on ||
|
|
59
|
+
"body";
|
|
60
|
+
const message = validationError.customError ||
|
|
61
|
+
validationError.valueError?.message ||
|
|
62
|
+
"Validation failed";
|
|
63
|
+
allErrors[field] = [message];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Fallback to single error
|
|
68
|
+
const field = validationError.valueError?.path?.replace(/^\//, "") ||
|
|
69
|
+
validationError.on ||
|
|
70
|
+
"body";
|
|
71
|
+
const message = validationError.customError ||
|
|
72
|
+
validationError.valueError?.message ||
|
|
73
|
+
"Validation failed";
|
|
74
|
+
allErrors[field] = [message];
|
|
75
|
+
}
|
|
76
|
+
// If a custom formatter is provided, use it
|
|
77
|
+
if (this.validationFormatter) {
|
|
78
|
+
// Convert allErrors to the format expected by formatters
|
|
79
|
+
const formattedError = {
|
|
80
|
+
errors: Object.entries(allErrors).map(([field, messages]) => ({
|
|
81
|
+
path: `/${field}`,
|
|
82
|
+
summary: messages[0],
|
|
83
|
+
message: messages.join(", "),
|
|
84
|
+
})),
|
|
85
|
+
};
|
|
86
|
+
return this.validationFormatter.format(formattedError);
|
|
87
|
+
}
|
|
88
|
+
// Default format: { statusCode, message, errors: { field: [messages] } }
|
|
89
|
+
return {
|
|
90
|
+
statusCode: 400,
|
|
91
|
+
message: "Validation failed",
|
|
92
|
+
errors: allErrors,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Default error handling
|
|
96
|
+
const err = error;
|
|
97
|
+
set.status = err.status || 500;
|
|
98
|
+
return {
|
|
99
|
+
statusCode: err.status || 500,
|
|
100
|
+
message: err.message || "Internal server error",
|
|
101
|
+
error: err.name || "Error",
|
|
102
|
+
};
|
|
103
|
+
});
|
|
17
104
|
// Apply CORS if enabled
|
|
18
105
|
if (options.cors) {
|
|
19
106
|
// CORS configuration would go here
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* @author WynkJS Team
|
|
6
6
|
* @license MIT
|
|
7
7
|
*/
|
|
8
|
+
import "reflect-metadata";
|
|
9
|
+
export { injectable, inject, singleton, autoInjectable, registry, container, } from "tsyringe";
|
|
10
|
+
export type { DependencyContainer } from "tsyringe";
|
|
11
|
+
export { injectable as Injectable, inject as Inject, singleton as Singleton, autoInjectable as AutoInjectable, registry as Registry, container as Container, } from "tsyringe";
|
|
8
12
|
export * from "./decorators/http.decorators";
|
|
9
13
|
export * from "./decorators/param.decorators";
|
|
10
14
|
export * from "./decorators/guard.decorators";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../core/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../core/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,kBAAkB,CAAC;AAI1B,OAAO,EACL,UAAU,EACV,MAAM,EACN,SAAS,EACT,cAAc,EACd,QAAQ,EACR,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAGpD,OAAO,EACL,UAAU,IAAI,UAAU,EACxB,MAAM,IAAI,MAAM,EAChB,SAAS,IAAI,SAAS,EACtB,cAAc,IAAI,cAAc,EAChC,QAAQ,IAAI,QAAQ,EACpB,SAAS,IAAI,SAAS,GACvB,MAAM,UAAU,CAAC;AAGlB,cAAc,8BAA8B,CAAC;AAG7C,cAAc,+BAA+B,CAAC;AAG9C,cAAc,+BAA+B,CAAC;AAG9C,cAAc,qCAAqC,CAAC;AACpD,cAAc,mCAAmC,CAAC;AAGlD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,4BAA4B,CAAC;AAI3C,cAAc,mCAAmC,CAAC;AAClD,cAAc,iCAAiC,CAAC;AAIhD,cAAc,kCAAkC,CAAC;AASjD,cAAc,OAAO,CAAC;AAGtB,cAAc,WAAW,CAAC;AAE1B;;GAEG;AACH,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B;;GAEG;AACH,eAAO,MAAM,cAAc,qBAAqB,CAAC"}
|