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
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
/**
|
|
3
|
+
* Pipe Decorators and Interfaces for WynkJS Framework
|
|
4
|
+
* Pipes for data validation and transformation
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* WynkPipeTransform interface - All pipes must implement this
|
|
8
|
+
*/
|
|
9
|
+
export interface WynkPipeTransform<T = any, R = any> {
|
|
10
|
+
transform(value: T, metadata?: ArgumentMetadata): R | Promise<R>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Argument metadata interface
|
|
14
|
+
*/
|
|
15
|
+
export interface ArgumentMetadata {
|
|
16
|
+
type: "body" | "query" | "param" | "custom";
|
|
17
|
+
metatype?: any;
|
|
18
|
+
data?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* @UsePipes decorator - Apply pipes to routes, controllers, or parameters
|
|
22
|
+
* @param pipes Pipe classes to apply
|
|
23
|
+
* @example
|
|
24
|
+
* @UsePipes(ValidationPipe)
|
|
25
|
+
* @Controller('/users')
|
|
26
|
+
* export class UserController {}
|
|
27
|
+
*
|
|
28
|
+
* @Post()
|
|
29
|
+
* @UsePipes(ValidationPipe, TransformPipe)
|
|
30
|
+
* create(@Body() dto: CreateDto) {}
|
|
31
|
+
*/
|
|
32
|
+
export declare function UsePipes(...pipes: (Function | WynkPipeTransform)[]): MethodDecorator & ClassDecorator;
|
|
33
|
+
/**
|
|
34
|
+
* Helper function to execute pipes
|
|
35
|
+
*/
|
|
36
|
+
export declare function executePipes(pipes: (Function | WynkPipeTransform)[], value: any, metadata: ArgumentMetadata): Promise<any>;
|
|
37
|
+
/**
|
|
38
|
+
* Built-in Pipes
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* ValidationPipe - Validates and transforms input data
|
|
42
|
+
* Supports custom error formatting via exceptionFactory
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Default formatting
|
|
46
|
+
* @UsePipes(new ValidationPipe())
|
|
47
|
+
* create(@Body() dto: CreateDto) {}
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // Custom formatting (like NestJS)
|
|
51
|
+
* const customPipe = new ValidationPipe({
|
|
52
|
+
* exceptionFactory: (errors) => ({
|
|
53
|
+
* statusCode: 400,
|
|
54
|
+
* message: 'Validation failed',
|
|
55
|
+
* errors: errors
|
|
56
|
+
* })
|
|
57
|
+
* });
|
|
58
|
+
*/
|
|
59
|
+
export declare class ValidationPipe implements WynkPipeTransform {
|
|
60
|
+
private options;
|
|
61
|
+
constructor(options?: {
|
|
62
|
+
whitelist?: boolean;
|
|
63
|
+
forbidNonWhitelisted?: boolean;
|
|
64
|
+
transform?: boolean;
|
|
65
|
+
exceptionFactory?: (errors: any) => any;
|
|
66
|
+
});
|
|
67
|
+
transform(value: any, metadata: ArgumentMetadata): Promise<any>;
|
|
68
|
+
/**
|
|
69
|
+
* Format Elysia validation error
|
|
70
|
+
* Called by ValidationExceptionFilter
|
|
71
|
+
*/
|
|
72
|
+
formatError(exception: any): any;
|
|
73
|
+
/**
|
|
74
|
+
* Parse validation error from Elysia exception
|
|
75
|
+
*/
|
|
76
|
+
private parseValidationError;
|
|
77
|
+
/**
|
|
78
|
+
* Default error formatting
|
|
79
|
+
*/
|
|
80
|
+
private defaultFormatError;
|
|
81
|
+
private isPrimitive;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* ParseIntPipe - Transforms string to integer
|
|
85
|
+
* @example
|
|
86
|
+
* @Get('/:id')
|
|
87
|
+
* findOne(@Param('id', ParseIntPipe) id: number) {}
|
|
88
|
+
*/
|
|
89
|
+
export declare class ParseIntPipe implements WynkPipeTransform<string, number> {
|
|
90
|
+
transform(value: string, metadata: ArgumentMetadata): Promise<number>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* ParseFloatPipe - Transforms string to float
|
|
94
|
+
* @example
|
|
95
|
+
* @Get()
|
|
96
|
+
* search(@Query('price', ParseFloatPipe) price: number) {}
|
|
97
|
+
*/
|
|
98
|
+
export declare class ParseFloatPipe implements WynkPipeTransform<string, number> {
|
|
99
|
+
transform(value: string, metadata: ArgumentMetadata): Promise<number>;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* ParseBoolPipe - Transforms string to boolean
|
|
103
|
+
* @example
|
|
104
|
+
* @Get()
|
|
105
|
+
* search(@Query('active', ParseBoolPipe) active: boolean) {}
|
|
106
|
+
*/
|
|
107
|
+
export declare class ParseBoolPipe implements WynkPipeTransform<string, boolean> {
|
|
108
|
+
transform(value: string, metadata: ArgumentMetadata): Promise<boolean>;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* ParseArrayPipe - Transforms string to array
|
|
112
|
+
* @example
|
|
113
|
+
* @Get()
|
|
114
|
+
* search(@Query('ids', ParseArrayPipe) ids: string[]) {}
|
|
115
|
+
*/
|
|
116
|
+
export declare class ParseArrayPipe implements WynkPipeTransform<string, string[]> {
|
|
117
|
+
private separator;
|
|
118
|
+
constructor(separator?: string);
|
|
119
|
+
transform(value: string, metadata: ArgumentMetadata): Promise<string[]>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* ParseUUIDPipe - Validates UUID format
|
|
123
|
+
* @example
|
|
124
|
+
* @Get('/:id')
|
|
125
|
+
* findOne(@Param('id', ParseUUIDPipe) id: string) {}
|
|
126
|
+
*/
|
|
127
|
+
export declare class ParseUUIDPipe implements WynkPipeTransform<string, string> {
|
|
128
|
+
transform(value: string, metadata: ArgumentMetadata): Promise<string>;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* ParseEnumPipe - Validates enum values
|
|
132
|
+
* @example
|
|
133
|
+
* enum Status { ACTIVE, INACTIVE }
|
|
134
|
+
* @Get()
|
|
135
|
+
* search(@Query('status', new ParseEnumPipe(Status)) status: Status) {}
|
|
136
|
+
*/
|
|
137
|
+
export declare class ParseEnumPipe<T = any> implements WynkPipeTransform<string, T> {
|
|
138
|
+
private enumType;
|
|
139
|
+
constructor(enumType: any);
|
|
140
|
+
transform(value: string, metadata: ArgumentMetadata): Promise<T>;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* DefaultValuePipe - Provides default value if undefined
|
|
144
|
+
* @example
|
|
145
|
+
* @Get()
|
|
146
|
+
* search(@Query('page', new DefaultValuePipe(1)) page: number) {}
|
|
147
|
+
*/
|
|
148
|
+
export declare class DefaultValuePipe<T = any> implements WynkPipeTransform<T, T> {
|
|
149
|
+
private defaultValue;
|
|
150
|
+
constructor(defaultValue: T);
|
|
151
|
+
transform(value: T, metadata: ArgumentMetadata): Promise<T>;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* TrimPipe - Trims whitespace from strings
|
|
155
|
+
* @example
|
|
156
|
+
* @Post()
|
|
157
|
+
* create(@Body('name', TrimPipe) name: string) {}
|
|
158
|
+
*/
|
|
159
|
+
export declare class TrimPipe implements WynkPipeTransform<string, string> {
|
|
160
|
+
transform(value: string, metadata: ArgumentMetadata): Promise<string>;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* FormatErrorPipe - Formats validation errors as { [field]: [messages] }
|
|
164
|
+
* Like NestJS format with field names as keys
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* // In index.ts or controller
|
|
168
|
+
* app.useGlobalPipes(new FormatErrorPipe());
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* // Response format:
|
|
172
|
+
* {
|
|
173
|
+
* statusCode: 400,
|
|
174
|
+
* message: "Validation failed",
|
|
175
|
+
* errors: {
|
|
176
|
+
* email: ["Property 'email' should be email"],
|
|
177
|
+
* mobile: ["Expected string to match '^[6-9]{1}[0-9]{9}$'"]
|
|
178
|
+
* }
|
|
179
|
+
* }
|
|
180
|
+
*/
|
|
181
|
+
export declare class FormatErrorPipe extends ValidationPipe {
|
|
182
|
+
constructor();
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* SimpleErrorPipe - Returns flat array of error messages
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* app.useGlobalPipes(new SimpleErrorPipe());
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* // Response format:
|
|
192
|
+
* {
|
|
193
|
+
* statusCode: 400,
|
|
194
|
+
* message: "Property 'email' should be email, Invalid mobile number",
|
|
195
|
+
* errors: [
|
|
196
|
+
* "Property 'email' should be email",
|
|
197
|
+
* "Invalid mobile number"
|
|
198
|
+
* ]
|
|
199
|
+
* }
|
|
200
|
+
*/
|
|
201
|
+
export declare class SimpleErrorPipe extends ValidationPipe {
|
|
202
|
+
constructor();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* DetailedErrorPipe - Returns detailed information for each field
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* app.useGlobalPipes(new DetailedErrorPipe());
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* // Response format:
|
|
212
|
+
* {
|
|
213
|
+
* statusCode: 400,
|
|
214
|
+
* message: "Validation failed",
|
|
215
|
+
* errors: [{
|
|
216
|
+
* field: "email",
|
|
217
|
+
* message: "Property 'email' should be email",
|
|
218
|
+
* value: "demo@demo.",
|
|
219
|
+
* expected: "string (format: email)"
|
|
220
|
+
* }]
|
|
221
|
+
* }
|
|
222
|
+
*/
|
|
223
|
+
export declare class DetailedErrorPipe extends ValidationPipe {
|
|
224
|
+
constructor();
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=pipe.decorators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipe.decorators.d.ts","sourceRoot":"","sources":["../../core/decorators/pipe.decorators.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
/**
|
|
3
|
+
* @UsePipes decorator - Apply pipes to routes, controllers, or parameters
|
|
4
|
+
* @param pipes Pipe classes to apply
|
|
5
|
+
* @example
|
|
6
|
+
* @UsePipes(ValidationPipe)
|
|
7
|
+
* @Controller('/users')
|
|
8
|
+
* export class UserController {}
|
|
9
|
+
*
|
|
10
|
+
* @Post()
|
|
11
|
+
* @UsePipes(ValidationPipe, TransformPipe)
|
|
12
|
+
* create(@Body() dto: CreateDto) {}
|
|
13
|
+
*/
|
|
14
|
+
export function UsePipes(...pipes) {
|
|
15
|
+
return (target, propertyKey, descriptor) => {
|
|
16
|
+
if (propertyKey && descriptor) {
|
|
17
|
+
// Method decorator
|
|
18
|
+
const existing = Reflect.getMetadata("pipes", target, propertyKey) || [];
|
|
19
|
+
Reflect.defineMetadata("pipes", [...existing, ...pipes], target, propertyKey);
|
|
20
|
+
return descriptor;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
// Class decorator
|
|
24
|
+
const existing = Reflect.getMetadata("pipes", target) || [];
|
|
25
|
+
Reflect.defineMetadata("pipes", [...existing, ...pipes], target);
|
|
26
|
+
return target;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Helper function to execute pipes
|
|
32
|
+
*/
|
|
33
|
+
export async function executePipes(pipes, value, metadata) {
|
|
34
|
+
let transformedValue = value;
|
|
35
|
+
for (const pipe of pipes) {
|
|
36
|
+
let pipeInstance;
|
|
37
|
+
if (typeof pipe === "function") {
|
|
38
|
+
pipeInstance = new pipe();
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
pipeInstance = pipe;
|
|
42
|
+
}
|
|
43
|
+
if (!pipeInstance.transform) {
|
|
44
|
+
throw new Error(`Pipe must implement WynkPipeTransform interface`);
|
|
45
|
+
}
|
|
46
|
+
transformedValue = await pipeInstance.transform(transformedValue, metadata);
|
|
47
|
+
}
|
|
48
|
+
return transformedValue;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Built-in Pipes
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* ValidationPipe - Validates and transforms input data
|
|
55
|
+
* Supports custom error formatting via exceptionFactory
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // Default formatting
|
|
59
|
+
* @UsePipes(new ValidationPipe())
|
|
60
|
+
* create(@Body() dto: CreateDto) {}
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // Custom formatting (like NestJS)
|
|
64
|
+
* const customPipe = new ValidationPipe({
|
|
65
|
+
* exceptionFactory: (errors) => ({
|
|
66
|
+
* statusCode: 400,
|
|
67
|
+
* message: 'Validation failed',
|
|
68
|
+
* errors: errors
|
|
69
|
+
* })
|
|
70
|
+
* });
|
|
71
|
+
*/
|
|
72
|
+
export class ValidationPipe {
|
|
73
|
+
options;
|
|
74
|
+
constructor(options = {}) {
|
|
75
|
+
this.options = {
|
|
76
|
+
whitelist: true,
|
|
77
|
+
forbidNonWhitelisted: false,
|
|
78
|
+
transform: true,
|
|
79
|
+
...options,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async transform(value, metadata) {
|
|
83
|
+
// If no metatype or primitive type, return as is
|
|
84
|
+
if (!metadata.metatype || this.isPrimitive(metadata.metatype)) {
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
// Here you would integrate with class-validator
|
|
88
|
+
// For now, basic validation
|
|
89
|
+
if (value === undefined || value === null) {
|
|
90
|
+
throw new Error(`Validation failed: Value is required`);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Format Elysia validation error
|
|
96
|
+
* Called by ValidationExceptionFilter
|
|
97
|
+
*/
|
|
98
|
+
formatError(exception) {
|
|
99
|
+
const validationError = this.parseValidationError(exception);
|
|
100
|
+
// If user provided custom exception factory, use it
|
|
101
|
+
if (this.options.exceptionFactory) {
|
|
102
|
+
return this.options.exceptionFactory(validationError);
|
|
103
|
+
}
|
|
104
|
+
// Default formatting
|
|
105
|
+
return this.defaultFormatError(validationError);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Parse validation error from Elysia exception
|
|
109
|
+
*/
|
|
110
|
+
parseValidationError(exception) {
|
|
111
|
+
let validationData;
|
|
112
|
+
if (typeof exception.message === "string") {
|
|
113
|
+
try {
|
|
114
|
+
validationData = JSON.parse(exception.message);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
validationData = { type: "validation", message: exception.message };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
validationData = exception;
|
|
122
|
+
}
|
|
123
|
+
return validationData;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Default error formatting
|
|
127
|
+
*/
|
|
128
|
+
defaultFormatError(error) {
|
|
129
|
+
const errors = [];
|
|
130
|
+
if (error.errors && error.errors.length > 0) {
|
|
131
|
+
error.errors.forEach((err) => {
|
|
132
|
+
errors.push({
|
|
133
|
+
field: err.path?.replace(/^\//, "") || "unknown",
|
|
134
|
+
message: err.summary || err.message,
|
|
135
|
+
value: err.value,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
errors.push({
|
|
141
|
+
field: error.property?.replace(/^\//, "") || "unknown",
|
|
142
|
+
message: error.summary || error.message,
|
|
143
|
+
value: error.value,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
statusCode: 400,
|
|
148
|
+
message: "Validation failed",
|
|
149
|
+
errors,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
isPrimitive(metatype) {
|
|
153
|
+
const types = [String, Boolean, Number, Array, Object];
|
|
154
|
+
return types.includes(metatype);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* ParseIntPipe - Transforms string to integer
|
|
159
|
+
* @example
|
|
160
|
+
* @Get('/:id')
|
|
161
|
+
* findOne(@Param('id', ParseIntPipe) id: number) {}
|
|
162
|
+
*/
|
|
163
|
+
export class ParseIntPipe {
|
|
164
|
+
async transform(value, metadata) {
|
|
165
|
+
const val = parseInt(value, 10);
|
|
166
|
+
if (isNaN(val)) {
|
|
167
|
+
throw new Error(`Validation failed: "${value}" is not an integer`);
|
|
168
|
+
}
|
|
169
|
+
return val;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* ParseFloatPipe - Transforms string to float
|
|
174
|
+
* @example
|
|
175
|
+
* @Get()
|
|
176
|
+
* search(@Query('price', ParseFloatPipe) price: number) {}
|
|
177
|
+
*/
|
|
178
|
+
export class ParseFloatPipe {
|
|
179
|
+
async transform(value, metadata) {
|
|
180
|
+
const val = parseFloat(value);
|
|
181
|
+
if (isNaN(val)) {
|
|
182
|
+
throw new Error(`Validation failed: "${value}" is not a number`);
|
|
183
|
+
}
|
|
184
|
+
return val;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* ParseBoolPipe - Transforms string to boolean
|
|
189
|
+
* @example
|
|
190
|
+
* @Get()
|
|
191
|
+
* search(@Query('active', ParseBoolPipe) active: boolean) {}
|
|
192
|
+
*/
|
|
193
|
+
export class ParseBoolPipe {
|
|
194
|
+
async transform(value, metadata) {
|
|
195
|
+
if (value === "true" || value === "1")
|
|
196
|
+
return true;
|
|
197
|
+
if (value === "false" || value === "0")
|
|
198
|
+
return false;
|
|
199
|
+
throw new Error(`Validation failed: "${value}" is not a boolean`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* ParseArrayPipe - Transforms string to array
|
|
204
|
+
* @example
|
|
205
|
+
* @Get()
|
|
206
|
+
* search(@Query('ids', ParseArrayPipe) ids: string[]) {}
|
|
207
|
+
*/
|
|
208
|
+
export class ParseArrayPipe {
|
|
209
|
+
separator;
|
|
210
|
+
constructor(separator = ",") {
|
|
211
|
+
this.separator = separator;
|
|
212
|
+
}
|
|
213
|
+
async transform(value, metadata) {
|
|
214
|
+
if (Array.isArray(value))
|
|
215
|
+
return value;
|
|
216
|
+
if (typeof value === "string") {
|
|
217
|
+
return value.split(this.separator).map((item) => item.trim());
|
|
218
|
+
}
|
|
219
|
+
throw new Error(`Validation failed: "${value}" cannot be parsed to array`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* ParseUUIDPipe - Validates UUID format
|
|
224
|
+
* @example
|
|
225
|
+
* @Get('/:id')
|
|
226
|
+
* findOne(@Param('id', ParseUUIDPipe) id: string) {}
|
|
227
|
+
*/
|
|
228
|
+
export class ParseUUIDPipe {
|
|
229
|
+
async transform(value, metadata) {
|
|
230
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
231
|
+
if (!uuidRegex.test(value)) {
|
|
232
|
+
throw new Error(`Validation failed: "${value}" is not a valid UUID`);
|
|
233
|
+
}
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* ParseEnumPipe - Validates enum values
|
|
239
|
+
* @example
|
|
240
|
+
* enum Status { ACTIVE, INACTIVE }
|
|
241
|
+
* @Get()
|
|
242
|
+
* search(@Query('status', new ParseEnumPipe(Status)) status: Status) {}
|
|
243
|
+
*/
|
|
244
|
+
export class ParseEnumPipe {
|
|
245
|
+
enumType;
|
|
246
|
+
constructor(enumType) {
|
|
247
|
+
this.enumType = enumType;
|
|
248
|
+
}
|
|
249
|
+
async transform(value, metadata) {
|
|
250
|
+
const enumValues = Object.values(this.enumType);
|
|
251
|
+
if (!enumValues.includes(value)) {
|
|
252
|
+
throw new Error(`Validation failed: "${value}" is not a valid enum value. Valid values: ${enumValues.join(", ")}`);
|
|
253
|
+
}
|
|
254
|
+
return value;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* DefaultValuePipe - Provides default value if undefined
|
|
259
|
+
* @example
|
|
260
|
+
* @Get()
|
|
261
|
+
* search(@Query('page', new DefaultValuePipe(1)) page: number) {}
|
|
262
|
+
*/
|
|
263
|
+
export class DefaultValuePipe {
|
|
264
|
+
defaultValue;
|
|
265
|
+
constructor(defaultValue) {
|
|
266
|
+
this.defaultValue = defaultValue;
|
|
267
|
+
}
|
|
268
|
+
async transform(value, metadata) {
|
|
269
|
+
if (value === undefined || value === null) {
|
|
270
|
+
return this.defaultValue;
|
|
271
|
+
}
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* TrimPipe - Trims whitespace from strings
|
|
277
|
+
* @example
|
|
278
|
+
* @Post()
|
|
279
|
+
* create(@Body('name', TrimPipe) name: string) {}
|
|
280
|
+
*/
|
|
281
|
+
export class TrimPipe {
|
|
282
|
+
async transform(value, metadata) {
|
|
283
|
+
if (typeof value !== "string") {
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
return value.trim();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* FormatErrorPipe - Formats validation errors as { [field]: [messages] }
|
|
291
|
+
* Like NestJS format with field names as keys
|
|
292
|
+
*
|
|
293
|
+
* @example
|
|
294
|
+
* // In index.ts or controller
|
|
295
|
+
* app.useGlobalPipes(new FormatErrorPipe());
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* // Response format:
|
|
299
|
+
* {
|
|
300
|
+
* statusCode: 400,
|
|
301
|
+
* message: "Validation failed",
|
|
302
|
+
* errors: {
|
|
303
|
+
* email: ["Property 'email' should be email"],
|
|
304
|
+
* mobile: ["Expected string to match '^[6-9]{1}[0-9]{9}$'"]
|
|
305
|
+
* }
|
|
306
|
+
* }
|
|
307
|
+
*/
|
|
308
|
+
export class FormatErrorPipe extends ValidationPipe {
|
|
309
|
+
constructor() {
|
|
310
|
+
super({
|
|
311
|
+
whitelist: true,
|
|
312
|
+
forbidNonWhitelisted: true,
|
|
313
|
+
exceptionFactory: (error) => {
|
|
314
|
+
const formattedErrors = {};
|
|
315
|
+
if (error.errors && error.errors.length > 0) {
|
|
316
|
+
error.errors.forEach((err) => {
|
|
317
|
+
const field = err.path?.replace(/^\//, "") || "unknown";
|
|
318
|
+
if (!formattedErrors[field]) {
|
|
319
|
+
formattedErrors[field] = [];
|
|
320
|
+
}
|
|
321
|
+
formattedErrors[field].push(err.summary || err.message);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
const field = error.property?.replace(/^\//, "") || "unknown";
|
|
326
|
+
formattedErrors[field] = [error.summary || error.message];
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
statusCode: 400,
|
|
330
|
+
message: "Validation failed",
|
|
331
|
+
errors: formattedErrors,
|
|
332
|
+
};
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* SimpleErrorPipe - Returns flat array of error messages
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* app.useGlobalPipes(new SimpleErrorPipe());
|
|
342
|
+
*
|
|
343
|
+
* @example
|
|
344
|
+
* // Response format:
|
|
345
|
+
* {
|
|
346
|
+
* statusCode: 400,
|
|
347
|
+
* message: "Property 'email' should be email, Invalid mobile number",
|
|
348
|
+
* errors: [
|
|
349
|
+
* "Property 'email' should be email",
|
|
350
|
+
* "Invalid mobile number"
|
|
351
|
+
* ]
|
|
352
|
+
* }
|
|
353
|
+
*/
|
|
354
|
+
export class SimpleErrorPipe extends ValidationPipe {
|
|
355
|
+
constructor() {
|
|
356
|
+
super({
|
|
357
|
+
exceptionFactory: (error) => {
|
|
358
|
+
const messages = [];
|
|
359
|
+
if (error.errors && error.errors.length > 0) {
|
|
360
|
+
error.errors.forEach((err) => {
|
|
361
|
+
messages.push(err.summary || err.message);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
messages.push(error.summary || error.message);
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
statusCode: 400,
|
|
369
|
+
message: messages.join(", "),
|
|
370
|
+
errors: messages,
|
|
371
|
+
};
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* DetailedErrorPipe - Returns detailed information for each field
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* app.useGlobalPipes(new DetailedErrorPipe());
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* // Response format:
|
|
384
|
+
* {
|
|
385
|
+
* statusCode: 400,
|
|
386
|
+
* message: "Validation failed",
|
|
387
|
+
* errors: [{
|
|
388
|
+
* field: "email",
|
|
389
|
+
* message: "Property 'email' should be email",
|
|
390
|
+
* value: "demo@demo.",
|
|
391
|
+
* expected: "string (format: email)"
|
|
392
|
+
* }]
|
|
393
|
+
* }
|
|
394
|
+
*/
|
|
395
|
+
export class DetailedErrorPipe extends ValidationPipe {
|
|
396
|
+
constructor() {
|
|
397
|
+
super({
|
|
398
|
+
exceptionFactory: (error) => {
|
|
399
|
+
const errors = [];
|
|
400
|
+
if (error.errors && error.errors.length > 0) {
|
|
401
|
+
error.errors.forEach((err) => {
|
|
402
|
+
errors.push({
|
|
403
|
+
field: err.path?.replace(/^\//, "") || "unknown",
|
|
404
|
+
message: err.summary || err.message,
|
|
405
|
+
value: err.value,
|
|
406
|
+
expected: err.schema?.format
|
|
407
|
+
? `${err.schema.type} (format: ${err.schema.format})`
|
|
408
|
+
: err.schema?.type,
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
statusCode: 400,
|
|
414
|
+
message: "Validation failed",
|
|
415
|
+
errors,
|
|
416
|
+
};
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|