@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipes System
|
|
3
|
+
*
|
|
4
|
+
* Pipes run after guards in the request pipeline and can transform/validate
|
|
5
|
+
* data before it reaches the handler.
|
|
6
|
+
*
|
|
7
|
+
* Execution Order:
|
|
8
|
+
* Incoming Request → Guards → Pipes → Handler
|
|
9
|
+
*
|
|
10
|
+
* Pipes can:
|
|
11
|
+
* - Transform data (e.g., string to number)
|
|
12
|
+
* - Validate data (e.g., using Standard Schema)
|
|
13
|
+
* - Provide default values
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Context } from "../context";
|
|
17
|
+
import type { Token } from "../container";
|
|
18
|
+
import type { StandardSchema } from "../types";
|
|
19
|
+
import { validate, isStandardSchema, type ValidationResult } from "../validation";
|
|
20
|
+
|
|
21
|
+
// ============= Types =============
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parameter metadata available to pipes
|
|
25
|
+
*/
|
|
26
|
+
export interface ParameterMetadata {
|
|
27
|
+
/** Parameter index */
|
|
28
|
+
index: number;
|
|
29
|
+
/** Parameter name if available */
|
|
30
|
+
name?: string;
|
|
31
|
+
/** Decorator type (body, query, param, etc.) */
|
|
32
|
+
decorator?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Context available to pipes during transformation
|
|
37
|
+
*/
|
|
38
|
+
export interface PipeContext {
|
|
39
|
+
/** The request context */
|
|
40
|
+
context: Context;
|
|
41
|
+
/** Parameter metadata */
|
|
42
|
+
metadata?: ParameterMetadata;
|
|
43
|
+
/** Target type information */
|
|
44
|
+
type?: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Pipe interface for data transformation
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* class ParseIntPipe implements PipeTransform<string, number> {
|
|
53
|
+
* transform(value: string, context: PipeContext): number {
|
|
54
|
+
* const parsed = parseInt(value, 10);
|
|
55
|
+
* if (isNaN(parsed)) {
|
|
56
|
+
* throw new Error('Validation failed');
|
|
57
|
+
* }
|
|
58
|
+
* return parsed;
|
|
59
|
+
* }
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export interface PipeTransform<T = unknown, R = unknown> {
|
|
64
|
+
transform(value: T, context: PipeContext): R | Promise<R>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Pipe function type (for functional pipes)
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const trimPipe: PipeFn<string, string> = (value) => value.trim();
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export type PipeFn<T = unknown, R = unknown> = (
|
|
76
|
+
value: T,
|
|
77
|
+
context: PipeContext
|
|
78
|
+
) => R | Promise<R>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Pipe type - can be:
|
|
82
|
+
* - A token for a pipe class registered in the container
|
|
83
|
+
* - A pipe class instance
|
|
84
|
+
* - A pipe function
|
|
85
|
+
*/
|
|
86
|
+
export type Pipe<T = unknown, R = unknown> =
|
|
87
|
+
| Token<PipeTransform<T, R>>
|
|
88
|
+
| PipeTransform<T, R>
|
|
89
|
+
| PipeFn<T, R>;
|
|
90
|
+
|
|
91
|
+
// ============= Metadata Storage =============
|
|
92
|
+
|
|
93
|
+
// Type alias for class constructors
|
|
94
|
+
type Constructor = new (...args: unknown[]) => unknown;
|
|
95
|
+
|
|
96
|
+
// WeakMap for storing pipes metadata on method prototypes
|
|
97
|
+
const pipesMethodMetadata = new WeakMap<object, Map<string | symbol, ParameterPipeMetadata[]>>();
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Metadata for a parameter with pipes
|
|
101
|
+
*/
|
|
102
|
+
export interface ParameterPipeMetadata {
|
|
103
|
+
/** Parameter index */
|
|
104
|
+
index: number;
|
|
105
|
+
/** Parameter decorator type */
|
|
106
|
+
decorator: 'body' | 'query' | 'param' | 'custom';
|
|
107
|
+
/** Key for query/param decorators */
|
|
108
|
+
key?: string;
|
|
109
|
+
/** Schema for validation */
|
|
110
|
+
schema?: StandardSchema;
|
|
111
|
+
/** Pipes to apply */
|
|
112
|
+
pipes: Pipe[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Set pipes metadata on a method
|
|
117
|
+
*/
|
|
118
|
+
function setMethodPipes(
|
|
119
|
+
target: object,
|
|
120
|
+
propertyKey: string | symbol,
|
|
121
|
+
metadata: ParameterPipeMetadata[]
|
|
122
|
+
): void {
|
|
123
|
+
if (!pipesMethodMetadata.has(target)) {
|
|
124
|
+
pipesMethodMetadata.set(target, new Map());
|
|
125
|
+
}
|
|
126
|
+
pipesMethodMetadata.get(target)?.set(propertyKey, metadata);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get pipes metadata from a method
|
|
131
|
+
*/
|
|
132
|
+
export function getMethodPipes(
|
|
133
|
+
target: object,
|
|
134
|
+
propertyKey: string | symbol
|
|
135
|
+
): ParameterPipeMetadata[] | undefined {
|
|
136
|
+
return pipesMethodMetadata.get(target)?.get(propertyKey);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============= Pipe Decorator =============
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Decorator to apply pipes to a parameter.
|
|
143
|
+
* Pipes are executed in the order they are provided.
|
|
144
|
+
*
|
|
145
|
+
* @param pipes - Pipes to apply
|
|
146
|
+
* @returns ParameterDecorator
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* @Get(':id')
|
|
151
|
+
* getUser(@Param('id', ParseIntPipe) id: number) {}
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export function UsePipes(...pipes: Pipe[]): ParameterDecorator {
|
|
155
|
+
return (
|
|
156
|
+
target: unknown,
|
|
157
|
+
propertyKey: string | symbol | undefined,
|
|
158
|
+
parameterIndex: number
|
|
159
|
+
) => {
|
|
160
|
+
if (propertyKey === undefined) {
|
|
161
|
+
throw new Error("UsePipes can only be used on method parameters");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const targetObj = target as object;
|
|
165
|
+
const existing = getMethodPipes(targetObj, propertyKey) ?? [];
|
|
166
|
+
|
|
167
|
+
// Find existing metadata for this parameter or create new
|
|
168
|
+
const existingParam = existing.find(p => p.index === parameterIndex);
|
|
169
|
+
if (existingParam) {
|
|
170
|
+
existingParam.pipes.push(...pipes);
|
|
171
|
+
} else {
|
|
172
|
+
existing.push({
|
|
173
|
+
index: parameterIndex,
|
|
174
|
+
decorator: 'custom',
|
|
175
|
+
pipes: [...pipes]
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
setMethodPipes(targetObj, propertyKey, existing);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============= Parameter Decorators =============
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Extract and optionally validate request body.
|
|
187
|
+
*
|
|
188
|
+
* @param schema - Optional Standard Schema for validation
|
|
189
|
+
* @returns ParameterDecorator
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* @Post()
|
|
194
|
+
* createUser(@Body(userSchema) body: User) {}
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export function Body(schema?: StandardSchema): ParameterDecorator {
|
|
198
|
+
return (
|
|
199
|
+
target: unknown,
|
|
200
|
+
propertyKey: string | symbol | undefined,
|
|
201
|
+
parameterIndex: number
|
|
202
|
+
) => {
|
|
203
|
+
if (propertyKey === undefined) {
|
|
204
|
+
throw new Error("Body can only be used on method parameters");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const targetObj = target as object;
|
|
208
|
+
const existing = getMethodPipes(targetObj, propertyKey) ?? [];
|
|
209
|
+
|
|
210
|
+
existing.push({
|
|
211
|
+
index: parameterIndex,
|
|
212
|
+
decorator: 'body',
|
|
213
|
+
schema,
|
|
214
|
+
pipes: schema ? [new ValidationPipe(schema)] : []
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
setMethodPipes(targetObj, propertyKey, existing);
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Extract and optionally validate query parameter.
|
|
223
|
+
*
|
|
224
|
+
* @param key - Query parameter key (optional, if omitted returns all query params)
|
|
225
|
+
* @param schema - Optional Standard Schema for validation
|
|
226
|
+
* @returns ParameterDecorator
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* @Get()
|
|
231
|
+
* search(@Query('q') query: string) {}
|
|
232
|
+
*
|
|
233
|
+
* @Get()
|
|
234
|
+
* search(@Query('limit', limitSchema) limit: number) {}
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export function Query(key?: string, schema?: StandardSchema): ParameterDecorator {
|
|
238
|
+
return (
|
|
239
|
+
target: unknown,
|
|
240
|
+
propertyKey: string | symbol | undefined,
|
|
241
|
+
parameterIndex: number
|
|
242
|
+
) => {
|
|
243
|
+
if (propertyKey === undefined) {
|
|
244
|
+
throw new Error("Query can only be used on method parameters");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const targetObj = target as object;
|
|
248
|
+
const existing = getMethodPipes(targetObj, propertyKey) ?? [];
|
|
249
|
+
|
|
250
|
+
existing.push({
|
|
251
|
+
index: parameterIndex,
|
|
252
|
+
decorator: 'query',
|
|
253
|
+
key,
|
|
254
|
+
schema,
|
|
255
|
+
pipes: schema ? [new ValidationPipe(schema)] : []
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
setMethodPipes(targetObj, propertyKey, existing);
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Extract and transform route parameter.
|
|
264
|
+
*
|
|
265
|
+
* @param key - Route parameter key (optional, if omitted returns all params)
|
|
266
|
+
* @param pipes - Pipes to apply for transformation
|
|
267
|
+
* @returns ParameterDecorator
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* @Get(':id')
|
|
272
|
+
* getUser(@Param('id', ParseIntPipe) id: number) {}
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
export function Param(key?: string, ...pipes: Pipe[]): ParameterDecorator {
|
|
276
|
+
return (
|
|
277
|
+
target: unknown,
|
|
278
|
+
propertyKey: string | symbol | undefined,
|
|
279
|
+
parameterIndex: number
|
|
280
|
+
) => {
|
|
281
|
+
if (propertyKey === undefined) {
|
|
282
|
+
throw new Error("Param can only be used on method parameters");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const targetObj = target as object;
|
|
286
|
+
const existing = getMethodPipes(targetObj, propertyKey) ?? [];
|
|
287
|
+
|
|
288
|
+
existing.push({
|
|
289
|
+
index: parameterIndex,
|
|
290
|
+
decorator: 'param',
|
|
291
|
+
key,
|
|
292
|
+
pipes: [...pipes]
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
setMethodPipes(targetObj, propertyKey, existing);
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ============= Built-in Pipes =============
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* ValidationPipe - Validates using Standard Schema
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* ```typescript
|
|
306
|
+
* @Body(userSchema) body: User
|
|
307
|
+
* // or explicitly
|
|
308
|
+
* @UsePipes(new ValidationPipe(userSchema))
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
export class ValidationPipe<T = unknown> implements PipeTransform<unknown, T> {
|
|
312
|
+
constructor(private schema: StandardSchema<unknown, T>) {}
|
|
313
|
+
|
|
314
|
+
async transform(value: unknown, context: PipeContext): Promise<T> {
|
|
315
|
+
const result: ValidationResult<T> = await validate(this.schema, value);
|
|
316
|
+
|
|
317
|
+
if (result.success) {
|
|
318
|
+
return result.data;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Validation failed
|
|
322
|
+
const failedResult = result as Extract<ValidationResult<T>, { success: false }>;
|
|
323
|
+
const error = new Error("Validation failed");
|
|
324
|
+
(error as Error & { issues: unknown[] }).issues = [...failedResult.issues];
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* ParseIntPipe - Transforms string to integer
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```typescript
|
|
334
|
+
* @Param('id', ParseIntPipe) id: number
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
export class ParseIntPipe implements PipeTransform<string, number> {
|
|
338
|
+
transform(value: string, context: PipeContext): number {
|
|
339
|
+
const parsed = parseInt(value, 10);
|
|
340
|
+
|
|
341
|
+
if (isNaN(parsed)) {
|
|
342
|
+
throw new Error(`Validation failed: "${value}" is not a valid integer`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return parsed;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* ParseFloatPipe - Transforms string to float
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```typescript
|
|
354
|
+
* @Param('price', ParseFloatPipe) price: number
|
|
355
|
+
* ```
|
|
356
|
+
*/
|
|
357
|
+
export class ParseFloatPipe implements PipeTransform<string, number> {
|
|
358
|
+
transform(value: string, context: PipeContext): number {
|
|
359
|
+
const parsed = parseFloat(value);
|
|
360
|
+
|
|
361
|
+
if (isNaN(parsed)) {
|
|
362
|
+
throw new Error(`Validation failed: "${value}" is not a valid number`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return parsed;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* ParseBoolPipe - Transforms string to boolean
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```typescript
|
|
374
|
+
* @Query('active', ParseBoolPipe) active: boolean
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
export class ParseBoolPipe implements PipeTransform<string, boolean> {
|
|
378
|
+
private readonly truthyValues = ['true', '1', 'yes', 'on'];
|
|
379
|
+
private readonly falsyValues = ['false', '0', 'no', 'off'];
|
|
380
|
+
|
|
381
|
+
transform(value: string, context: PipeContext): boolean {
|
|
382
|
+
const lower = value.toLowerCase();
|
|
383
|
+
|
|
384
|
+
if (this.truthyValues.includes(lower)) {
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (this.falsyValues.includes(lower)) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
throw new Error(`Validation failed: "${value}" is not a valid boolean`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* DefaultValuePipe - Provides default value when input is undefined or null
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```typescript
|
|
401
|
+
* @Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
404
|
+
export class DefaultValuePipe<T> implements PipeTransform<unknown, T> {
|
|
405
|
+
constructor(private defaultValue: T) {}
|
|
406
|
+
|
|
407
|
+
transform(value: unknown, context: PipeContext): T {
|
|
408
|
+
if (value === undefined || value === null) {
|
|
409
|
+
return this.defaultValue;
|
|
410
|
+
}
|
|
411
|
+
return value as T;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* TrimPipe - Trims string whitespace
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* ```typescript
|
|
420
|
+
* @Query('name', TrimPipe) name: string
|
|
421
|
+
* ```
|
|
422
|
+
*/
|
|
423
|
+
export class TrimPipe implements PipeTransform<string, string> {
|
|
424
|
+
transform(value: string, context: PipeContext): string {
|
|
425
|
+
if (typeof value !== 'string') {
|
|
426
|
+
throw new Error('Value must be a string');
|
|
427
|
+
}
|
|
428
|
+
return value.trim();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* ParseJsonPipe - Parses JSON string to object
|
|
434
|
+
*
|
|
435
|
+
* @example
|
|
436
|
+
* ```typescript
|
|
437
|
+
* @Query('data', ParseJsonPipe) data: MyObject
|
|
438
|
+
* ```
|
|
439
|
+
*/
|
|
440
|
+
export class ParseJsonPipe<T = unknown> implements PipeTransform<string, T> {
|
|
441
|
+
transform(value: string, context: PipeContext): T {
|
|
442
|
+
try {
|
|
443
|
+
return JSON.parse(value) as T;
|
|
444
|
+
} catch {
|
|
445
|
+
throw new Error(`Validation failed: "${value}" is not valid JSON`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* ParseArrayPipe - Transforms comma-separated string to array
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* ```typescript
|
|
455
|
+
* @Query('tags', ParseArrayPipe) tags: string[]
|
|
456
|
+
* ```
|
|
457
|
+
*/
|
|
458
|
+
export class ParseArrayPipe implements PipeTransform<string, string[]> {
|
|
459
|
+
constructor(private separator: string = ',') {}
|
|
460
|
+
|
|
461
|
+
transform(value: string, context: PipeContext): string[] {
|
|
462
|
+
if (typeof value !== 'string') {
|
|
463
|
+
throw new Error('Value must be a string');
|
|
464
|
+
}
|
|
465
|
+
return value.split(this.separator).map(s => s.trim()).filter(s => s.length > 0);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ============= Pipe Executor =============
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Pipe executor options
|
|
473
|
+
*/
|
|
474
|
+
export interface PipeExecutorOptions {
|
|
475
|
+
/** Global pipes applied to all parameters */
|
|
476
|
+
globalPipes?: Pipe[];
|
|
477
|
+
/** Pipes from parameter decorator */
|
|
478
|
+
parameterPipes?: Pipe[];
|
|
479
|
+
/** Container for resolving pipe instances */
|
|
480
|
+
resolvePipe?: (pipe: Pipe) => PipeTransform | PipeFn | null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Execute pipes in order and return the transformed value
|
|
485
|
+
*
|
|
486
|
+
* @param value - Initial value to transform
|
|
487
|
+
* @param context - Pipe context
|
|
488
|
+
* @param options - Pipe executor options
|
|
489
|
+
* @returns Transformed value
|
|
490
|
+
* @throws Error if any pipe fails
|
|
491
|
+
*/
|
|
492
|
+
export async function executePipes<T = unknown>(
|
|
493
|
+
value: unknown,
|
|
494
|
+
context: PipeContext,
|
|
495
|
+
options: PipeExecutorOptions
|
|
496
|
+
): Promise<T> {
|
|
497
|
+
const { globalPipes = [], parameterPipes = [], resolvePipe } = options;
|
|
498
|
+
|
|
499
|
+
// Combine all pipes in execution order
|
|
500
|
+
const allPipes = [...globalPipes, ...parameterPipes];
|
|
501
|
+
|
|
502
|
+
let currentValue: unknown = value;
|
|
503
|
+
|
|
504
|
+
// Execute each pipe in order
|
|
505
|
+
for (const pipe of allPipes) {
|
|
506
|
+
let pipeInstance: PipeTransform | PipeFn | null = null;
|
|
507
|
+
|
|
508
|
+
// Resolve the pipe
|
|
509
|
+
if (typeof pipe === "function") {
|
|
510
|
+
// Check if it's a pipe function or a class constructor
|
|
511
|
+
const funcPipe = pipe as { prototype?: unknown; transform?: unknown };
|
|
512
|
+
if (funcPipe.prototype && typeof funcPipe.prototype === "object" &&
|
|
513
|
+
"transform" in (funcPipe.prototype as object)) {
|
|
514
|
+
// It's a class constructor - try to resolve from container or create instance
|
|
515
|
+
pipeInstance = resolvePipe ? resolvePipe(pipe) : null;
|
|
516
|
+
if (!pipeInstance) {
|
|
517
|
+
// Create a new instance if not in container
|
|
518
|
+
const PipeClass = pipe as unknown as new () => PipeTransform;
|
|
519
|
+
pipeInstance = new PipeClass();
|
|
520
|
+
}
|
|
521
|
+
} else {
|
|
522
|
+
// It's a pipe function
|
|
523
|
+
pipeInstance = pipe as PipeFn;
|
|
524
|
+
}
|
|
525
|
+
} else if (typeof pipe === "object" && pipe !== null) {
|
|
526
|
+
// It's a token or already an instance
|
|
527
|
+
const objPipe = pipe as { transform?: unknown };
|
|
528
|
+
if ("transform" in objPipe && typeof objPipe.transform === "function") {
|
|
529
|
+
// It's already a PipeTransform instance
|
|
530
|
+
pipeInstance = pipe as PipeTransform;
|
|
531
|
+
} else {
|
|
532
|
+
// It's a token - try to resolve
|
|
533
|
+
pipeInstance = resolvePipe ? resolvePipe(pipe) : null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!pipeInstance) {
|
|
538
|
+
console.warn("Pipe could not be resolved:", pipe);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Execute the pipe
|
|
543
|
+
if (typeof pipeInstance === "function") {
|
|
544
|
+
// Pipe function
|
|
545
|
+
currentValue = await pipeInstance(currentValue, context);
|
|
546
|
+
} else {
|
|
547
|
+
// PipeTransform instance
|
|
548
|
+
currentValue = await pipeInstance.transform(currentValue, context);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return currentValue as T;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ============= Parameter Value Extractor =============
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Extract parameter value from context based on decorator type
|
|
559
|
+
*/
|
|
560
|
+
export async function extractParameterValue(
|
|
561
|
+
context: Context,
|
|
562
|
+
metadata: ParameterPipeMetadata
|
|
563
|
+
): Promise<unknown> {
|
|
564
|
+
switch (metadata.decorator) {
|
|
565
|
+
case 'body':
|
|
566
|
+
return await context.body();
|
|
567
|
+
|
|
568
|
+
case 'query':
|
|
569
|
+
if (metadata.key) {
|
|
570
|
+
return context.query[metadata.key];
|
|
571
|
+
}
|
|
572
|
+
return context.query;
|
|
573
|
+
|
|
574
|
+
case 'param':
|
|
575
|
+
if (metadata.key) {
|
|
576
|
+
return context.params[metadata.key];
|
|
577
|
+
}
|
|
578
|
+
return context.params;
|
|
579
|
+
|
|
580
|
+
case 'custom':
|
|
581
|
+
default:
|
|
582
|
+
return undefined;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ============= Error Response =============
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Create a 400 Bad Request response for pipe errors
|
|
590
|
+
*/
|
|
591
|
+
export function createBadRequestResponse(error: Error): Response {
|
|
592
|
+
const issues = (error as Error & { issues?: unknown[] }).issues;
|
|
593
|
+
|
|
594
|
+
return new Response(JSON.stringify({
|
|
595
|
+
statusCode: 400,
|
|
596
|
+
error: "Bad Request",
|
|
597
|
+
message: error.message,
|
|
598
|
+
...(issues && { issues })
|
|
599
|
+
}), {
|
|
600
|
+
status: 400,
|
|
601
|
+
headers: {
|
|
602
|
+
"Content-Type": "application/json",
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ============= Type Guards =============
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Check if a value is a PipeTransform instance
|
|
611
|
+
*/
|
|
612
|
+
export function isPipeTransform(value: unknown): value is PipeTransform {
|
|
613
|
+
return (
|
|
614
|
+
typeof value === "object" &&
|
|
615
|
+
value !== null &&
|
|
616
|
+
"transform" in value &&
|
|
617
|
+
typeof (value as PipeTransform).transform === "function"
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Check if a value is a pipe function
|
|
623
|
+
*/
|
|
624
|
+
export function isPipeFn(value: unknown): value is PipeFn {
|
|
625
|
+
return typeof value === "function" && !isPipeTransform(value);
|
|
626
|
+
}
|