bxo 0.0.5-dev.21 → 0.0.5-dev.23
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 +76 -1
- package/index.ts +79 -9
- package/package.json +1 -1
package/README.md
CHANGED
@@ -20,7 +20,7 @@ BXO is a fast, lightweight, and fully type-safe web framework built specifically
|
|
20
20
|
### Installation
|
21
21
|
|
22
22
|
```bash
|
23
|
-
bun add
|
23
|
+
bun add bxo
|
24
24
|
```
|
25
25
|
|
26
26
|
### Basic Usage
|
@@ -93,11 +93,86 @@ interface Context<TConfig> {
|
|
93
93
|
status?: number;
|
94
94
|
headers?: Record<string, string>;
|
95
95
|
};
|
96
|
+
status: (code: number, data?: any) => any; // Type-safe status method
|
96
97
|
user?: any; // Added by auth plugin
|
97
98
|
[key: string]: any; // Extended by plugins
|
98
99
|
}
|
99
100
|
```
|
100
101
|
|
102
|
+
### Type-Safe Status Method
|
103
|
+
|
104
|
+
BXO provides a type-safe `ctx.status()` method similar to Elysia, which allows you to set HTTP status codes and return data in one call:
|
105
|
+
|
106
|
+
```typescript
|
107
|
+
// Simple usage
|
108
|
+
app.get('/hello', (ctx) => {
|
109
|
+
return ctx.status(200, { message: 'Hello World' });
|
110
|
+
});
|
111
|
+
|
112
|
+
// With response validation
|
113
|
+
app.get('/user/:id', (ctx) => {
|
114
|
+
const userId = ctx.params.id;
|
115
|
+
|
116
|
+
if (userId === 'not-found') {
|
117
|
+
// TypeScript suggests 404 as valid status
|
118
|
+
return ctx.status(404, { error: 'User not found' });
|
119
|
+
}
|
120
|
+
|
121
|
+
// TypeScript suggests 200 as valid status
|
122
|
+
return ctx.status(200, { user: { id: userId, name: 'John Doe' } });
|
123
|
+
}, {
|
124
|
+
response: {
|
125
|
+
200: z.object({
|
126
|
+
user: z.object({
|
127
|
+
id: z.string(),
|
128
|
+
name: z.string()
|
129
|
+
})
|
130
|
+
}),
|
131
|
+
404: z.object({
|
132
|
+
error: z.string()
|
133
|
+
})
|
134
|
+
}
|
135
|
+
});
|
136
|
+
|
137
|
+
// POST with validation and status responses
|
138
|
+
app.post('/users', (ctx) => {
|
139
|
+
const { name, email } = ctx.body;
|
140
|
+
|
141
|
+
if (!name || !email) {
|
142
|
+
return ctx.status(400, { error: 'Missing required fields' });
|
143
|
+
}
|
144
|
+
|
145
|
+
return ctx.status(201, {
|
146
|
+
success: true,
|
147
|
+
user: { id: 1, name, email }
|
148
|
+
});
|
149
|
+
}, {
|
150
|
+
body: z.object({
|
151
|
+
name: z.string(),
|
152
|
+
email: z.string().email()
|
153
|
+
}),
|
154
|
+
response: {
|
155
|
+
201: z.object({
|
156
|
+
success: z.boolean(),
|
157
|
+
user: z.object({
|
158
|
+
id: z.number(),
|
159
|
+
name: z.string(),
|
160
|
+
email: z.string()
|
161
|
+
})
|
162
|
+
}),
|
163
|
+
400: z.object({
|
164
|
+
error: z.string()
|
165
|
+
})
|
166
|
+
}
|
167
|
+
});
|
168
|
+
```
|
169
|
+
|
170
|
+
**Key Features:**
|
171
|
+
- **Type Safety**: Status codes are suggested based on your response configuration
|
172
|
+
- **Data Validation**: Return data is validated against the corresponding schema
|
173
|
+
- **Autocomplete**: TypeScript provides autocomplete for valid status codes
|
174
|
+
- **Return Type Inference**: Return types are properly inferred from schemas
|
175
|
+
|
101
176
|
### Validation Configuration
|
102
177
|
|
103
178
|
Each route can specify validation schemas for different parts of the request:
|
package/index.ts
CHANGED
@@ -3,6 +3,18 @@ import { z } from 'zod';
|
|
3
3
|
// Type utilities for extracting types from Zod schemas
|
4
4
|
type InferZodType<T> = T extends z.ZodType<infer U> ? U : never;
|
5
5
|
|
6
|
+
// Response configuration types
|
7
|
+
type ResponseSchema = z.ZodSchema<any>;
|
8
|
+
type StatusResponseSchema = Record<number, ResponseSchema>;
|
9
|
+
type ResponseConfig = ResponseSchema | StatusResponseSchema;
|
10
|
+
|
11
|
+
// Type utility to extract response type from response config
|
12
|
+
type InferResponseType<T> = T extends ResponseSchema
|
13
|
+
? InferZodType<T>
|
14
|
+
: T extends StatusResponseSchema
|
15
|
+
? { [K in keyof T]: InferZodType<T[K]> }[keyof T]
|
16
|
+
: never;
|
17
|
+
|
6
18
|
// Cookie interface
|
7
19
|
interface Cookie {
|
8
20
|
name: string;
|
@@ -35,10 +47,13 @@ interface RouteConfig {
|
|
35
47
|
body?: z.ZodSchema<any>;
|
36
48
|
headers?: z.ZodSchema<any>;
|
37
49
|
cookies?: z.ZodSchema<any>;
|
38
|
-
response?:
|
50
|
+
response?: ResponseConfig;
|
39
51
|
detail?: RouteDetail;
|
40
52
|
}
|
41
53
|
|
54
|
+
// Helper type to extract status codes from response config
|
55
|
+
type StatusCodes<T> = T extends Record<number, any> ? keyof T : never;
|
56
|
+
|
42
57
|
// Context type that's fully typed based on the route configuration
|
43
58
|
export type Context<TConfig extends RouteConfig = {}> = {
|
44
59
|
params: TConfig['params'] extends z.ZodSchema<any> ? InferZodType<TConfig['params']> : Record<string, string>;
|
@@ -53,11 +68,29 @@ export type Context<TConfig extends RouteConfig = {}> = {
|
|
53
68
|
headers?: Record<string, string>;
|
54
69
|
cookies?: Cookie[];
|
55
70
|
};
|
71
|
+
status: <T extends number>(
|
72
|
+
code: TConfig['response'] extends StatusResponseSchema
|
73
|
+
? StatusCodes<TConfig['response']> | number
|
74
|
+
: T,
|
75
|
+
data?: TConfig['response'] extends StatusResponseSchema
|
76
|
+
? T extends keyof TConfig['response']
|
77
|
+
? InferZodType<TConfig['response'][T]>
|
78
|
+
: any
|
79
|
+
: TConfig['response'] extends ResponseSchema
|
80
|
+
? InferZodType<TConfig['response']>
|
81
|
+
: any
|
82
|
+
) => TConfig['response'] extends StatusResponseSchema
|
83
|
+
? T extends keyof TConfig['response']
|
84
|
+
? InferZodType<TConfig['response'][T]>
|
85
|
+
: any
|
86
|
+
: TConfig['response'] extends ResponseSchema
|
87
|
+
? InferZodType<TConfig['response']>
|
88
|
+
: any;
|
56
89
|
[key: string]: any;
|
57
90
|
};
|
58
91
|
|
59
|
-
// Handler function type
|
60
|
-
type Handler<TConfig extends RouteConfig = {}, EC = {}> = (ctx: Context<TConfig> & EC) => Promise<
|
92
|
+
// Handler function type with proper response typing
|
93
|
+
type Handler<TConfig extends RouteConfig = {}, EC = {}> = (ctx: Context<TConfig> & EC) => Promise<InferResponseType<TConfig['response']>> | InferResponseType<TConfig['response']>;
|
61
94
|
|
62
95
|
// Route definition
|
63
96
|
interface Route {
|
@@ -395,18 +428,18 @@ export default class BXO {
|
|
395
428
|
// Parse query string
|
396
429
|
private parseQuery(searchParams: URLSearchParams): Record<string, string | undefined> {
|
397
430
|
const query: Record<string, string | undefined> = {};
|
398
|
-
|
431
|
+
searchParams.forEach((value, key) => {
|
399
432
|
query[key] = value;
|
400
|
-
}
|
433
|
+
});
|
401
434
|
return query;
|
402
435
|
}
|
403
436
|
|
404
437
|
// Parse headers
|
405
438
|
private parseHeaders(headers: Headers): Record<string, string> {
|
406
439
|
const headerObj: Record<string, string> = {};
|
407
|
-
|
440
|
+
headers.forEach((value, key) => {
|
408
441
|
headerObj[key] = value;
|
409
|
-
}
|
442
|
+
});
|
410
443
|
return headerObj;
|
411
444
|
}
|
412
445
|
|
@@ -433,6 +466,38 @@ export default class BXO {
|
|
433
466
|
return schema.parse(data);
|
434
467
|
}
|
435
468
|
|
469
|
+
// Validate response against response config (supports both simple and status-based schemas)
|
470
|
+
private validateResponse(responseConfig: ResponseConfig | undefined, data: any, status: number = 200): any {
|
471
|
+
if (!responseConfig) return data;
|
472
|
+
|
473
|
+
// If it's a simple schema (not status-based)
|
474
|
+
if ('parse' in responseConfig && typeof responseConfig.parse === 'function') {
|
475
|
+
return responseConfig.parse(data);
|
476
|
+
}
|
477
|
+
|
478
|
+
// If it's a status-based schema
|
479
|
+
if (typeof responseConfig === 'object' && !('parse' in responseConfig)) {
|
480
|
+
const statusSchema = responseConfig[status];
|
481
|
+
if (statusSchema) {
|
482
|
+
return statusSchema.parse(data);
|
483
|
+
}
|
484
|
+
|
485
|
+
// If no specific status schema found, try to find a fallback
|
486
|
+
// Common fallback statuses: 200, 201, 400, 500
|
487
|
+
const fallbackStatuses = [200, 201, 400, 500];
|
488
|
+
for (const fallbackStatus of fallbackStatuses) {
|
489
|
+
if (responseConfig[fallbackStatus]) {
|
490
|
+
return responseConfig[fallbackStatus].parse(data);
|
491
|
+
}
|
492
|
+
}
|
493
|
+
|
494
|
+
// If no schema found for the status, return data as-is
|
495
|
+
return data;
|
496
|
+
}
|
497
|
+
|
498
|
+
return data;
|
499
|
+
}
|
500
|
+
|
436
501
|
// Main request handler
|
437
502
|
private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
|
438
503
|
const url = new URL(request.url);
|
@@ -514,7 +579,11 @@ export default class BXO {
|
|
514
579
|
cookies: validatedCookies,
|
515
580
|
path: pathname,
|
516
581
|
request,
|
517
|
-
set: {}
|
582
|
+
set: {},
|
583
|
+
status: ((code: number, data?: any) => {
|
584
|
+
ctx.set.status = code;
|
585
|
+
return data;
|
586
|
+
}) as any
|
518
587
|
};
|
519
588
|
} catch (validationError) {
|
520
589
|
// Validation failed - return error response
|
@@ -575,7 +644,8 @@ export default class BXO {
|
|
575
644
|
if (route.config?.response && !(response instanceof Response)) {
|
576
645
|
try {
|
577
646
|
console.log('response', response);
|
578
|
-
|
647
|
+
const status = ctx.set.status || 200;
|
648
|
+
response = this.validateResponse(route.config.response, response, status);
|
579
649
|
} catch (validationError) {
|
580
650
|
// Response validation failed
|
581
651
|
|