bxo 0.0.5-dev.20 โ 0.0.5-dev.22
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 +1 -1
- package/index.ts +129 -7
- package/package.json +1 -1
- package/plugins/index.ts +0 -2
- package/example.ts +0 -204
package/README.md
CHANGED
package/index.ts
CHANGED
@@ -3,6 +3,31 @@ 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
|
+
|
18
|
+
// Cookie interface
|
19
|
+
interface Cookie {
|
20
|
+
name: string;
|
21
|
+
value: string;
|
22
|
+
domain?: string;
|
23
|
+
path?: string;
|
24
|
+
expires?: Date;
|
25
|
+
maxAge?: number;
|
26
|
+
secure?: boolean;
|
27
|
+
httpOnly?: boolean;
|
28
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
29
|
+
}
|
30
|
+
|
6
31
|
// OpenAPI detail information
|
7
32
|
interface RouteDetail {
|
8
33
|
summary?: string;
|
@@ -21,7 +46,8 @@ interface RouteConfig {
|
|
21
46
|
query?: z.ZodSchema<any>;
|
22
47
|
body?: z.ZodSchema<any>;
|
23
48
|
headers?: z.ZodSchema<any>;
|
24
|
-
|
49
|
+
cookies?: z.ZodSchema<any>;
|
50
|
+
response?: ResponseConfig;
|
25
51
|
detail?: RouteDetail;
|
26
52
|
}
|
27
53
|
|
@@ -31,17 +57,20 @@ export type Context<TConfig extends RouteConfig = {}> = {
|
|
31
57
|
query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
|
32
58
|
body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
|
33
59
|
headers: TConfig['headers'] extends z.ZodSchema<any> ? InferZodType<TConfig['headers']> : Record<string, string>;
|
60
|
+
cookies: TConfig['cookies'] extends z.ZodSchema<any> ? InferZodType<TConfig['cookies']> : Record<string, string>;
|
34
61
|
path: string;
|
35
62
|
request: Request;
|
36
63
|
set: {
|
37
64
|
status?: number;
|
38
65
|
headers?: Record<string, string>;
|
66
|
+
cookies?: Cookie[];
|
39
67
|
};
|
68
|
+
status: (code: number, data?: any) => any;
|
40
69
|
[key: string]: any;
|
41
70
|
};
|
42
71
|
|
43
|
-
// Handler function type
|
44
|
-
type Handler<TConfig extends RouteConfig = {}, EC = {}> = (ctx: Context<TConfig> & EC) => Promise<any> | any;
|
72
|
+
// Handler function type with proper response typing
|
73
|
+
type Handler<TConfig extends RouteConfig = {}, EC = {}> = (ctx: Context<TConfig> & EC) => Promise<InferResponseType<TConfig['response']> | any> | InferResponseType<TConfig['response']> | any;
|
45
74
|
|
46
75
|
// Route definition
|
47
76
|
interface Route {
|
@@ -394,12 +423,61 @@ export default class BXO {
|
|
394
423
|
return headerObj;
|
395
424
|
}
|
396
425
|
|
426
|
+
// Parse cookies from Cookie header
|
427
|
+
private parseCookies(cookieHeader: string | null): Record<string, string> {
|
428
|
+
const cookies: Record<string, string> = {};
|
429
|
+
|
430
|
+
if (!cookieHeader) return cookies;
|
431
|
+
|
432
|
+
const cookiePairs = cookieHeader.split(';');
|
433
|
+
for (const pair of cookiePairs) {
|
434
|
+
const [name, value] = pair.trim().split('=');
|
435
|
+
if (name && value) {
|
436
|
+
cookies[decodeURIComponent(name)] = decodeURIComponent(value);
|
437
|
+
}
|
438
|
+
}
|
439
|
+
|
440
|
+
return cookies;
|
441
|
+
}
|
442
|
+
|
397
443
|
// Validate data against Zod schema
|
398
444
|
private validateData<T>(schema: z.ZodSchema<T> | undefined, data: any): T {
|
399
445
|
if (!schema) return data;
|
400
446
|
return schema.parse(data);
|
401
447
|
}
|
402
448
|
|
449
|
+
// Validate response against response config (supports both simple and status-based schemas)
|
450
|
+
private validateResponse(responseConfig: ResponseConfig | undefined, data: any, status: number = 200): any {
|
451
|
+
if (!responseConfig) return data;
|
452
|
+
|
453
|
+
// If it's a simple schema (not status-based)
|
454
|
+
if ('parse' in responseConfig && typeof responseConfig.parse === 'function') {
|
455
|
+
return responseConfig.parse(data);
|
456
|
+
}
|
457
|
+
|
458
|
+
// If it's a status-based schema
|
459
|
+
if (typeof responseConfig === 'object' && !('parse' in responseConfig)) {
|
460
|
+
const statusSchema = responseConfig[status];
|
461
|
+
if (statusSchema) {
|
462
|
+
return statusSchema.parse(data);
|
463
|
+
}
|
464
|
+
|
465
|
+
// If no specific status schema found, try to find a fallback
|
466
|
+
// Common fallback statuses: 200, 201, 400, 500
|
467
|
+
const fallbackStatuses = [200, 201, 400, 500];
|
468
|
+
for (const fallbackStatus of fallbackStatuses) {
|
469
|
+
if (responseConfig[fallbackStatus]) {
|
470
|
+
return responseConfig[fallbackStatus].parse(data);
|
471
|
+
}
|
472
|
+
}
|
473
|
+
|
474
|
+
// If no schema found for the status, return data as-is
|
475
|
+
return data;
|
476
|
+
}
|
477
|
+
|
478
|
+
return data;
|
479
|
+
}
|
480
|
+
|
403
481
|
// Main request handler
|
404
482
|
private async handleRequest(request: Request, server?: any): Promise<Response | undefined> {
|
405
483
|
const url = new URL(request.url);
|
@@ -433,6 +511,7 @@ export default class BXO {
|
|
433
511
|
const { route, params } = matchResult;
|
434
512
|
const query = this.parseQuery(url.searchParams);
|
435
513
|
const headers = this.parseHeaders(request.headers);
|
514
|
+
const cookies = this.parseCookies(request.headers.get('cookie'));
|
436
515
|
|
437
516
|
let body: any;
|
438
517
|
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
@@ -470,15 +549,21 @@ export default class BXO {
|
|
470
549
|
const validatedQuery = route.config?.query ? this.validateData(route.config.query, query) : query;
|
471
550
|
const validatedBody = route.config?.body ? this.validateData(route.config.body, body) : body;
|
472
551
|
const validatedHeaders = route.config?.headers ? this.validateData(route.config.headers, headers) : headers;
|
552
|
+
const validatedCookies = route.config?.cookies ? this.validateData(route.config.cookies, cookies) : cookies;
|
473
553
|
|
474
554
|
ctx = {
|
475
555
|
params: validatedParams,
|
476
556
|
query: validatedQuery,
|
477
557
|
body: validatedBody,
|
478
558
|
headers: validatedHeaders,
|
559
|
+
cookies: validatedCookies,
|
479
560
|
path: pathname,
|
480
561
|
request,
|
481
|
-
set: {}
|
562
|
+
set: {},
|
563
|
+
status: (code: number, data?: any) => {
|
564
|
+
ctx.set.status = code;
|
565
|
+
return data;
|
566
|
+
}
|
482
567
|
};
|
483
568
|
} catch (validationError) {
|
484
569
|
// Validation failed - return error response
|
@@ -539,7 +624,8 @@ export default class BXO {
|
|
539
624
|
if (route.config?.response && !(response instanceof Response)) {
|
540
625
|
try {
|
541
626
|
console.log('response', response);
|
542
|
-
|
627
|
+
const status = ctx.set.status || 200;
|
628
|
+
response = this.validateResponse(route.config.response, response, status);
|
543
629
|
} catch (validationError) {
|
544
630
|
// Response validation failed
|
545
631
|
|
@@ -602,9 +688,34 @@ export default class BXO {
|
|
602
688
|
return new Response(bunFile, responseInit);
|
603
689
|
}
|
604
690
|
|
691
|
+
// Prepare headers with cookies
|
692
|
+
let responseHeaders = ctx.set.headers ? { ...ctx.set.headers } : {};
|
693
|
+
|
694
|
+
// Handle cookies if any are set
|
695
|
+
if (ctx.set.cookies && ctx.set.cookies.length > 0) {
|
696
|
+
const cookieHeaders = ctx.set.cookies.map(cookie => {
|
697
|
+
let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
|
698
|
+
|
699
|
+
if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
|
700
|
+
if (cookie.path) cookieString += `; Path=${cookie.path}`;
|
701
|
+
if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
|
702
|
+
if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
|
703
|
+
if (cookie.secure) cookieString += `; Secure`;
|
704
|
+
if (cookie.httpOnly) cookieString += `; HttpOnly`;
|
705
|
+
if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
|
706
|
+
|
707
|
+
return cookieString;
|
708
|
+
});
|
709
|
+
|
710
|
+
// Add Set-Cookie headers
|
711
|
+
cookieHeaders.forEach((cookieHeader, index) => {
|
712
|
+
responseHeaders[index === 0 ? 'Set-Cookie' : `Set-Cookie-${index}`] = cookieHeader;
|
713
|
+
});
|
714
|
+
}
|
715
|
+
|
605
716
|
const responseInit: ResponseInit = {
|
606
717
|
status: ctx.set.status || 200,
|
607
|
-
headers:
|
718
|
+
headers: responseHeaders
|
608
719
|
};
|
609
720
|
|
610
721
|
if (typeof response === 'string') {
|
@@ -905,4 +1016,15 @@ const file = (path: string, options?: { type?: string; headers?: Record<string,
|
|
905
1016
|
export { z, error, file };
|
906
1017
|
|
907
1018
|
// Export types for external use
|
908
|
-
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute };
|
1019
|
+
export type { RouteConfig, RouteDetail, Handler, WebSocketHandler, WSRoute, Cookie };
|
1020
|
+
|
1021
|
+
// Helper function to create a cookie
|
1022
|
+
export const createCookie = (
|
1023
|
+
name: string,
|
1024
|
+
value: string,
|
1025
|
+
options: Omit<Cookie, 'name' | 'value'> = {}
|
1026
|
+
): Cookie => ({
|
1027
|
+
name,
|
1028
|
+
value,
|
1029
|
+
...options
|
1030
|
+
});
|
package/package.json
CHANGED
package/plugins/index.ts
CHANGED
package/example.ts
DELETED
@@ -1,204 +0,0 @@
|
|
1
|
-
import BXO, { z } from './index';
|
2
|
-
import { cors, logger, auth, rateLimit, createJWT } from './plugins';
|
3
|
-
|
4
|
-
// Create a simple API plugin that defines its own routes
|
5
|
-
function createApiPlugin(): BXO {
|
6
|
-
const apiPlugin = new BXO();
|
7
|
-
|
8
|
-
apiPlugin
|
9
|
-
.get('/api/info', async (ctx) => {
|
10
|
-
return {
|
11
|
-
name: 'BXO API Plugin',
|
12
|
-
version: '1.0.0',
|
13
|
-
endpoints: ['/api/info', '/api/ping', '/api/time']
|
14
|
-
};
|
15
|
-
})
|
16
|
-
.get('/api/ping', async (ctx) => {
|
17
|
-
return { ping: 'pong', timestamp: Date.now() };
|
18
|
-
})
|
19
|
-
.get('/api/time', async (ctx) => {
|
20
|
-
return { time: new Date().toISOString() };
|
21
|
-
})
|
22
|
-
.post('/api/echo', async (ctx) => {
|
23
|
-
return { echo: ctx.body };
|
24
|
-
}, {
|
25
|
-
body: z.object({
|
26
|
-
message: z.string()
|
27
|
-
})
|
28
|
-
});
|
29
|
-
|
30
|
-
return apiPlugin;
|
31
|
-
}
|
32
|
-
|
33
|
-
// Create the app instance
|
34
|
-
const app = new BXO();
|
35
|
-
|
36
|
-
// Add plugins (including our new API plugin)
|
37
|
-
app
|
38
|
-
.use(logger({ format: 'simple' }))
|
39
|
-
.use(cors({
|
40
|
-
origin: ['http://localhost:3000', 'https://example.com'],
|
41
|
-
credentials: true
|
42
|
-
}))
|
43
|
-
.use(rateLimit({
|
44
|
-
max: 100,
|
45
|
-
window: 60, // 1 minute
|
46
|
-
exclude: ['/health']
|
47
|
-
}))
|
48
|
-
.use(auth({
|
49
|
-
type: 'jwt',
|
50
|
-
secret: 'your-secret-key',
|
51
|
-
exclude: ['/', '/login', '/health', '/api/*']
|
52
|
-
}))
|
53
|
-
.use(createApiPlugin()); // Add our plugin with actual routes
|
54
|
-
|
55
|
-
// Add simplified lifecycle hooks
|
56
|
-
app
|
57
|
-
.onBeforeStart(() => {
|
58
|
-
console.log('๐ง Preparing to start server...');
|
59
|
-
})
|
60
|
-
.onAfterStart(() => {
|
61
|
-
console.log('โ
Server fully started and ready!');
|
62
|
-
})
|
63
|
-
.onBeforeStop(() => {
|
64
|
-
console.log('๐ง Preparing to stop server...');
|
65
|
-
})
|
66
|
-
.onAfterStop(() => {
|
67
|
-
console.log('โ
Server fully stopped!');
|
68
|
-
})
|
69
|
-
.onRequest((ctx) => {
|
70
|
-
console.log(`๐จ Processing ${ctx.request.method} ${ctx.request.url}`);
|
71
|
-
})
|
72
|
-
.onResponse((ctx, response) => {
|
73
|
-
console.log(`๐ค Response sent for ${ctx.request.method} ${ctx.request.url}`);
|
74
|
-
return response;
|
75
|
-
})
|
76
|
-
.onError((ctx, error) => {
|
77
|
-
console.error(`๐ฅ Error in ${ctx.request.method} ${ctx.request.url}:`, error.message);
|
78
|
-
return { error: 'Something went wrong', timestamp: new Date().toISOString() };
|
79
|
-
});
|
80
|
-
|
81
|
-
// Routes exactly like your example
|
82
|
-
app
|
83
|
-
// Two arguments: path, handler
|
84
|
-
.get('/simple', async (ctx) => {
|
85
|
-
return { message: 'Hello World' };
|
86
|
-
})
|
87
|
-
|
88
|
-
// Three arguments: path, handler, config
|
89
|
-
.get('/users/:id', async (ctx) => {
|
90
|
-
// ctx.params.id is fully typed as string (UUID)
|
91
|
-
// ctx.query.include is typed as string | undefined
|
92
|
-
return { user: { id: ctx.params.id, include: ctx.query.include } };
|
93
|
-
}, {
|
94
|
-
params: z.object({ id: z.string().uuid() }),
|
95
|
-
query: z.object({ include: z.string().optional() })
|
96
|
-
})
|
97
|
-
|
98
|
-
.post('/users', async (ctx) => {
|
99
|
-
// ctx.body is fully typed with name: string, email: string
|
100
|
-
return { created: ctx.body };
|
101
|
-
}, {
|
102
|
-
body: z.object({
|
103
|
-
name: z.string(),
|
104
|
-
email: z.string().email()
|
105
|
-
})
|
106
|
-
})
|
107
|
-
|
108
|
-
// Additional examples
|
109
|
-
.get('/health', async (ctx) => {
|
110
|
-
return {
|
111
|
-
status: 'ok',
|
112
|
-
timestamp: new Date().toISOString(),
|
113
|
-
server: app.getServerInfo()
|
114
|
-
};
|
115
|
-
})
|
116
|
-
|
117
|
-
.post('/login', async (ctx) => {
|
118
|
-
const { username, password } = ctx.body;
|
119
|
-
|
120
|
-
// Simple auth check (in production, verify against database)
|
121
|
-
if (username === 'admin' && password === 'password') {
|
122
|
-
const token = createJWT({ username, role: 'admin' }, 'your-secret-key', 3600);
|
123
|
-
return { token, user: { username, role: 'admin' } };
|
124
|
-
}
|
125
|
-
|
126
|
-
ctx.set.status = 401;
|
127
|
-
return { error: 'Invalid credentials' };
|
128
|
-
}, {
|
129
|
-
body: z.object({
|
130
|
-
username: z.string(),
|
131
|
-
password: z.string()
|
132
|
-
})
|
133
|
-
})
|
134
|
-
|
135
|
-
.get('/protected', async (ctx) => {
|
136
|
-
// ctx.user is available here because of auth plugin
|
137
|
-
return { message: 'This is protected', user: ctx.user };
|
138
|
-
})
|
139
|
-
|
140
|
-
.get('/status', async (ctx) => {
|
141
|
-
return {
|
142
|
-
...app.getServerInfo(),
|
143
|
-
uptime: process.uptime(),
|
144
|
-
memory: process.memoryUsage()
|
145
|
-
};
|
146
|
-
})
|
147
|
-
|
148
|
-
.put('/users/:id', async (ctx) => {
|
149
|
-
return {
|
150
|
-
updated: ctx.body,
|
151
|
-
id: ctx.params.id,
|
152
|
-
version: ctx.headers['if-match']
|
153
|
-
};
|
154
|
-
}, {
|
155
|
-
params: z.object({ id: z.string().uuid() }),
|
156
|
-
body: z.object({
|
157
|
-
name: z.string().optional(),
|
158
|
-
email: z.string().email().optional()
|
159
|
-
}),
|
160
|
-
headers: z.object({
|
161
|
-
'if-match': z.string()
|
162
|
-
})
|
163
|
-
})
|
164
|
-
|
165
|
-
.delete('/users/:id', async (ctx) => {
|
166
|
-
ctx.set.status = 204;
|
167
|
-
return null;
|
168
|
-
}, {
|
169
|
-
params: z.object({ id: z.string().uuid() })
|
170
|
-
});
|
171
|
-
|
172
|
-
// Start the server (with hot reload enabled)
|
173
|
-
app.start(3000, 'localhost');
|
174
|
-
|
175
|
-
console.log(`
|
176
|
-
๐ฆ BXO Framework with Hot Reload
|
177
|
-
|
178
|
-
โจ Features Enabled:
|
179
|
-
- ๐ฃ Full lifecycle hooks (before/after pattern)
|
180
|
-
- ๐ JWT authentication
|
181
|
-
- ๐ Rate limiting
|
182
|
-
- ๐ CORS support
|
183
|
-
- ๐ Request logging
|
184
|
-
- ๐ API Plugin with routes
|
185
|
-
|
186
|
-
๐งช Try these endpoints:
|
187
|
-
- GET /simple
|
188
|
-
- GET /users/123e4567-e89b-12d3-a456-426614174000?include=profile
|
189
|
-
- POST /users (with JSON body: {"name": "John", "email": "john@example.com"})
|
190
|
-
- GET /health (shows server info)
|
191
|
-
- POST /login (with JSON body: {"username": "admin", "password": "password"})
|
192
|
-
- GET /protected (requires Bearer token from /login)
|
193
|
-
- GET /status (server statistics)
|
194
|
-
|
195
|
-
๐ API Plugin endpoints:
|
196
|
-
- GET /api/info (plugin information)
|
197
|
-
- GET /api/ping (ping pong)
|
198
|
-
- GET /api/time (current time)
|
199
|
-
- POST /api/echo (echo message: {"message": "hello"})
|
200
|
-
|
201
|
-
๐ก Edit this file and save to see hot reload in action!
|
202
|
-
`);
|
203
|
-
|
204
|
-
console.log(app.routes)
|