bxo 0.0.5-dev.80 → 0.0.5-dev.82
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 +26 -0
- package/example/multipart-example.ts +119 -0
- package/package.json +1 -1
- package/src/index.ts +59 -29
- package/test-bun-file.ts +0 -0
- package/test-url-encoding.ts +1 -0
package/README.md
CHANGED
|
@@ -320,9 +320,35 @@ app.post("/items", async (ctx) => {
|
|
|
320
320
|
});
|
|
321
321
|
```
|
|
322
322
|
|
|
323
|
+
### Deep Nested Array Objects
|
|
324
|
+
Form fields like `workspace_items[0][id]`, `workspace_items[0][type]` are automatically converted to arrays of objects:
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
const WorkspaceSchema = z.object({
|
|
328
|
+
id: z.string(),
|
|
329
|
+
workspace_items: z.array(z.object({
|
|
330
|
+
id: z.string(),
|
|
331
|
+
type: z.string(),
|
|
332
|
+
value: z.string(),
|
|
333
|
+
options: z.string(),
|
|
334
|
+
label: z.string()
|
|
335
|
+
}))
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
app.post("/workspace", async (ctx) => {
|
|
339
|
+
// Form data: workspace_items[0][id]="item1", workspace_items[0][type]="Link"
|
|
340
|
+
// becomes { workspace_items: [{ id: "item1", type: "Link", ... }] }
|
|
341
|
+
console.log(ctx.body); // { id: "...", workspace_items: [{ id: "item1", type: "Link", ... }] }
|
|
342
|
+
return ctx.json({ success: true, data: ctx.body });
|
|
343
|
+
}, {
|
|
344
|
+
body: WorkspaceSchema
|
|
345
|
+
});
|
|
346
|
+
```
|
|
347
|
+
|
|
323
348
|
### Supported Patterns
|
|
324
349
|
- **Nested objects**: `profile[name]`, `settings[theme]` → `{ profile: { name: "..." }, settings: { theme: "..." } }`
|
|
325
350
|
- **Arrays**: `items[0]`, `items[1]` → `{ items: ["...", "..."] }`
|
|
351
|
+
- **Deep nested array objects**: `workspace_items[0][id]`, `workspace_items[0][type]` → `{ workspace_items: [{ id: "...", type: "..." }] }`
|
|
326
352
|
- **Duplicate keys**: Multiple values with same key → `{ tags: ["tag1", "tag2"] }`
|
|
327
353
|
|
|
328
354
|
## Running
|
|
@@ -63,6 +63,46 @@ app.post("/items", async (ctx) => {
|
|
|
63
63
|
}
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
+
// Example with deep nested array objects (like workspace_items[0][id])
|
|
67
|
+
const WorkspaceFormSchema = z.object({
|
|
68
|
+
id: z.string(),
|
|
69
|
+
created_at: z.string(),
|
|
70
|
+
updated_at: z.string(),
|
|
71
|
+
updated_by: z.string(),
|
|
72
|
+
doc_status: z.string().transform(val => parseInt(val, 10)),
|
|
73
|
+
idx: z.string().transform(val => parseInt(val, 10)),
|
|
74
|
+
workspace_items: z.array(z.object({
|
|
75
|
+
id: z.string(),
|
|
76
|
+
type: z.string(),
|
|
77
|
+
value: z.string(),
|
|
78
|
+
options: z.string(),
|
|
79
|
+
workspaceId: z.string(),
|
|
80
|
+
owner: z.string(),
|
|
81
|
+
created_at: z.string(),
|
|
82
|
+
updated_at: z.string(),
|
|
83
|
+
created_by: z.string(),
|
|
84
|
+
updated_by: z.string(),
|
|
85
|
+
doc_status: z.string().transform(val => parseInt(val, 10)),
|
|
86
|
+
label: z.string()
|
|
87
|
+
}))
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
app.post("/workspace", async (ctx) => {
|
|
91
|
+
console.log("Parsed workspace form data:", ctx.body);
|
|
92
|
+
|
|
93
|
+
return ctx.json({
|
|
94
|
+
message: "Workspace processed successfully",
|
|
95
|
+
data: ctx.body
|
|
96
|
+
});
|
|
97
|
+
}, {
|
|
98
|
+
body: WorkspaceFormSchema,
|
|
99
|
+
detail: {
|
|
100
|
+
summary: "Process workspace with deep nested array objects",
|
|
101
|
+
description: "Handles form data like workspace_items[0][id], workspace_items[0][type]",
|
|
102
|
+
tags: ["Workspace"]
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
66
106
|
// Test route to show how the parsing works
|
|
67
107
|
app.get("/test-parsing", async (ctx) => {
|
|
68
108
|
const html = `
|
|
@@ -156,6 +196,80 @@ app.get("/test-parsing", async (ctx) => {
|
|
|
156
196
|
<button type="submit">Submit Items Form</button>
|
|
157
197
|
</form>
|
|
158
198
|
|
|
199
|
+
<h2>Test 3: Deep Nested Array Objects (workspace_items[0][id])</h2>
|
|
200
|
+
<form id="workspaceForm" enctype="multipart/form-data">
|
|
201
|
+
<div class="form-group">
|
|
202
|
+
<label>ID:</label>
|
|
203
|
+
<input type="text" name="id" value="01993f54-758e-7000-9f98-4533a7cf8ce9">
|
|
204
|
+
</div>
|
|
205
|
+
<div class="form-group">
|
|
206
|
+
<label>Created At:</label>
|
|
207
|
+
<input type="text" name="created_at" value="2025-09-13 02:08:43">
|
|
208
|
+
</div>
|
|
209
|
+
<div class="form-group">
|
|
210
|
+
<label>Updated At:</label>
|
|
211
|
+
<input type="text" name="updated_at" value="2025-09-13 02:08:43">
|
|
212
|
+
</div>
|
|
213
|
+
<div class="form-group">
|
|
214
|
+
<label>Updated By:</label>
|
|
215
|
+
<input type="text" name="updated_by" value="01995120-fae9-7000-8d46-8f3502e901b6">
|
|
216
|
+
</div>
|
|
217
|
+
<div class="form-group">
|
|
218
|
+
<label>Doc Status:</label>
|
|
219
|
+
<input type="text" name="doc_status" value="0">
|
|
220
|
+
</div>
|
|
221
|
+
<div class="form-group">
|
|
222
|
+
<label>Index:</label>
|
|
223
|
+
<input type="text" name="idx" value="0">
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<h3>Workspace Item 1:</h3>
|
|
227
|
+
<div class="form-group">
|
|
228
|
+
<label>Item ID:</label>
|
|
229
|
+
<input type="text" name="workspace_items[0][id]" value="temp_1758097169599">
|
|
230
|
+
</div>
|
|
231
|
+
<div class="form-group">
|
|
232
|
+
<label>Item Type:</label>
|
|
233
|
+
<input type="text" name="workspace_items[0][type]" value="Link - URL">
|
|
234
|
+
</div>
|
|
235
|
+
<div class="form-group">
|
|
236
|
+
<label>Item Value:</label>
|
|
237
|
+
<input type="text" name="workspace_items[0][value]" value="asd">
|
|
238
|
+
</div>
|
|
239
|
+
<div class="form-group">
|
|
240
|
+
<label>Item Options:</label>
|
|
241
|
+
<input type="text" name="workspace_items[0][options]" value="asdasd">
|
|
242
|
+
</div>
|
|
243
|
+
<div class="form-group">
|
|
244
|
+
<label>Item Label:</label>
|
|
245
|
+
<input type="text" name="workspace_items[0][label]" value="asdasd">
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<h3>Workspace Item 2:</h3>
|
|
249
|
+
<div class="form-group">
|
|
250
|
+
<label>Item ID:</label>
|
|
251
|
+
<input type="text" name="workspace_items[1][id]" value="temp_1758097169600">
|
|
252
|
+
</div>
|
|
253
|
+
<div class="form-group">
|
|
254
|
+
<label>Item Type:</label>
|
|
255
|
+
<input type="text" name="workspace_items[1][type]" value="Text">
|
|
256
|
+
</div>
|
|
257
|
+
<div class="form-group">
|
|
258
|
+
<label>Item Value:</label>
|
|
259
|
+
<input type="text" name="workspace_items[1][value]" value="Another item">
|
|
260
|
+
</div>
|
|
261
|
+
<div class="form-group">
|
|
262
|
+
<label>Item Options:</label>
|
|
263
|
+
<input type="text" name="workspace_items[1][options]" value="More options">
|
|
264
|
+
</div>
|
|
265
|
+
<div class="form-group">
|
|
266
|
+
<label>Item Label:</label>
|
|
267
|
+
<input type="text" name="workspace_items[1][label]" value="Second item">
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<button type="submit">Submit Workspace Form</button>
|
|
271
|
+
</form>
|
|
272
|
+
|
|
159
273
|
<div id="result" class="result" style="display: none;">
|
|
160
274
|
<h3>Result:</h3>
|
|
161
275
|
<pre id="resultContent"></pre>
|
|
@@ -187,6 +301,11 @@ app.get("/test-parsing", async (ctx) => {
|
|
|
187
301
|
e.preventDefault();
|
|
188
302
|
submitForm(e.target, '/items');
|
|
189
303
|
});
|
|
304
|
+
|
|
305
|
+
document.getElementById('workspaceForm').addEventListener('submit', (e) => {
|
|
306
|
+
e.preventDefault();
|
|
307
|
+
submitForm(e.target, '/workspace');
|
|
308
|
+
});
|
|
190
309
|
</script>
|
|
191
310
|
</body>
|
|
192
311
|
</html>
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BunFile } from "bun";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
|
|
3
4
|
type Method =
|
|
@@ -75,7 +76,7 @@ export type Context<P extends string = string, S extends RouteSchema | undefined
|
|
|
75
76
|
headers: S extends RouteSchema ? InferOr<S["headers"], HeaderObject> : HeaderObject;
|
|
76
77
|
cookies: S extends RouteSchema ? InferOr<S["cookies"], CookieObject> : CookieObject;
|
|
77
78
|
body: S extends RouteSchema ? InferOr<S["body"], unknown> : unknown;
|
|
78
|
-
set: {
|
|
79
|
+
set: {
|
|
79
80
|
headers: Record<string, string | string[]>;
|
|
80
81
|
cookie: (name: string, value: string, options?: CookieOptions) => void;
|
|
81
82
|
};
|
|
@@ -88,7 +89,7 @@ type AnyHandler = (ctx: Context<any, any>, app: BXO) => Response | string | Prom
|
|
|
88
89
|
type Handler<P extends string, S extends RouteSchema | undefined = undefined> = (
|
|
89
90
|
ctx: Context<P, S>,
|
|
90
91
|
app: BXO
|
|
91
|
-
) => Response | string | Promise<Response | string
|
|
92
|
+
) => Response | string | Promise<Response | string> | BunFile | Promise<BunFile>
|
|
92
93
|
|
|
93
94
|
// WebSocket handler types
|
|
94
95
|
export type WebSocketHandler<T = any> = {
|
|
@@ -134,37 +135,37 @@ function parseCookies(cookieHeader: string | null): CookieObject {
|
|
|
134
135
|
|
|
135
136
|
function serializeCookie(name: string, value: string, options: CookieOptions = {}): string {
|
|
136
137
|
let cookie = `${name}=${encodeURIComponent(value)}`;
|
|
137
|
-
|
|
138
|
+
|
|
138
139
|
if (options.domain) {
|
|
139
140
|
cookie += `; Domain=${options.domain}`;
|
|
140
141
|
}
|
|
141
|
-
|
|
142
|
+
|
|
142
143
|
if (options.path) {
|
|
143
144
|
cookie += `; Path=${options.path}`;
|
|
144
145
|
} else {
|
|
145
146
|
cookie += `; Path=/`;
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
+
|
|
148
149
|
if (options.expires) {
|
|
149
150
|
cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
150
151
|
}
|
|
151
|
-
|
|
152
|
+
|
|
152
153
|
if (options.maxAge !== undefined) {
|
|
153
154
|
cookie += `; Max-Age=${options.maxAge}`;
|
|
154
155
|
}
|
|
155
|
-
|
|
156
|
+
|
|
156
157
|
if (options.httpOnly) {
|
|
157
158
|
cookie += `; HttpOnly`;
|
|
158
159
|
}
|
|
159
|
-
|
|
160
|
+
|
|
160
161
|
if (options.secure) {
|
|
161
162
|
cookie += `; Secure`;
|
|
162
163
|
}
|
|
163
|
-
|
|
164
|
+
|
|
164
165
|
if (options.sameSite) {
|
|
165
166
|
cookie += `; SameSite=${options.sameSite}`;
|
|
166
167
|
}
|
|
167
|
-
|
|
168
|
+
|
|
168
169
|
return cookie;
|
|
169
170
|
}
|
|
170
171
|
|
|
@@ -187,53 +188,82 @@ function parseQuery(searchParams: URLSearchParams, schema?: z.ZodTypeAny): any {
|
|
|
187
188
|
|
|
188
189
|
function formDataToObject(fd: FormData): Record<string, any> {
|
|
189
190
|
const obj: Record<string, any> = {};
|
|
190
|
-
|
|
191
|
+
|
|
191
192
|
for (const [key, value] of fd.entries()) {
|
|
192
193
|
setNestedValue(obj, key, value);
|
|
193
194
|
}
|
|
194
|
-
|
|
195
|
+
|
|
195
196
|
return obj;
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
function setNestedValue(obj: Record<string, any>, key: string, value: any): void {
|
|
200
|
+
// Handle deeply nested array objects like workspace_items[0][id], workspace_items[0][type]
|
|
201
|
+
const deepArrayMatch = key.match(/^(.+)\[(\d+)\]\[([^\]]+)\]$/);
|
|
202
|
+
if (deepArrayMatch) {
|
|
203
|
+
const [, arrayKey, index, propertyKey] = deepArrayMatch;
|
|
204
|
+
const arrayIndex = parseInt(index, 10);
|
|
205
|
+
|
|
206
|
+
if (!obj[arrayKey]) {
|
|
207
|
+
obj[arrayKey] = [];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Ensure it's an array
|
|
211
|
+
if (!Array.isArray(obj[arrayKey])) {
|
|
212
|
+
obj[arrayKey] = [];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Ensure the object at the index exists
|
|
216
|
+
if (!obj[arrayKey][arrayIndex]) {
|
|
217
|
+
obj[arrayKey][arrayIndex] = {};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Ensure it's an object
|
|
221
|
+
if (typeof obj[arrayKey][arrayIndex] !== 'object' || Array.isArray(obj[arrayKey][arrayIndex])) {
|
|
222
|
+
obj[arrayKey][arrayIndex] = {};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
obj[arrayKey][arrayIndex][propertyKey] = value;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
199
229
|
// Handle array notation like items[0], items[1]
|
|
200
230
|
const arrayMatch = key.match(/^(.+)\[(\d+)\]$/);
|
|
201
231
|
if (arrayMatch) {
|
|
202
232
|
const [, arrayKey, index] = arrayMatch;
|
|
203
233
|
const arrayIndex = parseInt(index, 10);
|
|
204
|
-
|
|
234
|
+
|
|
205
235
|
if (!obj[arrayKey]) {
|
|
206
236
|
obj[arrayKey] = [];
|
|
207
237
|
}
|
|
208
|
-
|
|
238
|
+
|
|
209
239
|
// Ensure it's an array
|
|
210
240
|
if (!Array.isArray(obj[arrayKey])) {
|
|
211
241
|
obj[arrayKey] = [];
|
|
212
242
|
}
|
|
213
|
-
|
|
243
|
+
|
|
214
244
|
// Set the value at the specific index
|
|
215
245
|
obj[arrayKey][arrayIndex] = value;
|
|
216
246
|
return;
|
|
217
247
|
}
|
|
218
|
-
|
|
248
|
+
|
|
219
249
|
// Handle nested object notation like profile[name], profile[age]
|
|
220
250
|
const nestedMatch = key.match(/^(.+)\[([^\]]+)\]$/);
|
|
221
251
|
if (nestedMatch) {
|
|
222
252
|
const [, parentKey, nestedKey] = nestedMatch;
|
|
223
|
-
|
|
253
|
+
|
|
224
254
|
if (!obj[parentKey]) {
|
|
225
255
|
obj[parentKey] = {};
|
|
226
256
|
}
|
|
227
|
-
|
|
257
|
+
|
|
228
258
|
// Ensure it's an object
|
|
229
259
|
if (typeof obj[parentKey] !== 'object' || Array.isArray(obj[parentKey])) {
|
|
230
260
|
obj[parentKey] = {};
|
|
231
261
|
}
|
|
232
|
-
|
|
262
|
+
|
|
233
263
|
obj[parentKey][nestedKey] = value;
|
|
234
264
|
return;
|
|
235
265
|
}
|
|
236
|
-
|
|
266
|
+
|
|
237
267
|
// Handle simple keys - check for duplicates to convert to arrays
|
|
238
268
|
if (key in obj) {
|
|
239
269
|
const existing = obj[key];
|
|
@@ -382,7 +412,7 @@ export default class BXO {
|
|
|
382
412
|
start(): void {
|
|
383
413
|
// Check if we have any WebSocket routes
|
|
384
414
|
const hasWebSocketRoutes = this.routes.some(r => r.method === "WS");
|
|
385
|
-
|
|
415
|
+
|
|
386
416
|
// Build a basic routes map for Bun's native routes (exact paths only)
|
|
387
417
|
const nativeRoutes: Record<string, Record<string, (req: Request) => Promise<Response> | Response>> = {};
|
|
388
418
|
|
|
@@ -444,7 +474,7 @@ export default class BXO {
|
|
|
444
474
|
}
|
|
445
475
|
}
|
|
446
476
|
}
|
|
447
|
-
|
|
477
|
+
|
|
448
478
|
// Handle regular HTTP requests
|
|
449
479
|
return this.dispatchAny(req, nativeRoutes);
|
|
450
480
|
}
|
|
@@ -673,7 +703,7 @@ export default class BXO {
|
|
|
673
703
|
headers: headerObj,
|
|
674
704
|
cookies: cookieObj,
|
|
675
705
|
body: bodyObj,
|
|
676
|
-
set: {
|
|
706
|
+
set: {
|
|
677
707
|
headers: {},
|
|
678
708
|
cookie: (name: string, value: string, options: CookieOptions = {}) => {
|
|
679
709
|
const cookieString = serializeCookie(name, value, options);
|
|
@@ -698,9 +728,9 @@ export default class BXO {
|
|
|
698
728
|
return new Response(JSON.stringify({ error: "Invalid response", issues: res.error?.issues ?? [] }), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
699
729
|
}
|
|
700
730
|
}
|
|
701
|
-
return new Response(JSON.stringify(data), {
|
|
702
|
-
status,
|
|
703
|
-
headers: { "Content-Type": "application/json" }
|
|
731
|
+
return new Response(JSON.stringify(data), {
|
|
732
|
+
status,
|
|
733
|
+
headers: { "Content-Type": "application/json" }
|
|
704
734
|
});
|
|
705
735
|
},
|
|
706
736
|
text: (data, status = 200) => {
|
|
@@ -711,9 +741,9 @@ export default class BXO {
|
|
|
711
741
|
return new Response(JSON.stringify({ error: "Invalid response", issues: res.error?.issues ?? [] }), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
712
742
|
}
|
|
713
743
|
}
|
|
714
|
-
return new Response(String(data), {
|
|
715
|
-
status,
|
|
716
|
-
headers: { "Content-Type": "text/plain" }
|
|
744
|
+
return new Response(String(data), {
|
|
745
|
+
status,
|
|
746
|
+
headers: { "Content-Type": "text/plain" }
|
|
717
747
|
});
|
|
718
748
|
},
|
|
719
749
|
status: (status, data) => {
|
package/test-bun-file.ts
ADDED
|
File without changes
|
package/test-url-encoding.ts
CHANGED