bxo 0.0.5-dev.79 → 0.0.5-dev.80
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 +58 -1
- package/example/multipart-example.ts +203 -0
- package/package.json +1 -1
- package/src/index.ts +55 -5
- package/test-url-encoding.ts +1 -0
package/README.md
CHANGED
|
@@ -273,16 +273,72 @@ app.post("/users", async (ctx) => {
|
|
|
273
273
|
});
|
|
274
274
|
```
|
|
275
275
|
|
|
276
|
+
## Multipart/Form-Data Parsing
|
|
277
|
+
|
|
278
|
+
BXO automatically parses multipart/form-data into nested objects and arrays before Zod validation:
|
|
279
|
+
|
|
280
|
+
### Nested Objects
|
|
281
|
+
Form fields like `profile[name]` are automatically converted to nested objects:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
const UserFormSchema = z.object({
|
|
285
|
+
name: z.string(),
|
|
286
|
+
email: z.string().email(),
|
|
287
|
+
profile: z.object({
|
|
288
|
+
name: z.string()
|
|
289
|
+
})
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
app.post("/users", async (ctx) => {
|
|
293
|
+
// Form data: profile[name]="John" becomes { profile: { name: "John" } }
|
|
294
|
+
console.log(ctx.body); // { name: "...", email: "...", profile: { name: "John" } }
|
|
295
|
+
return ctx.json({ success: true, data: ctx.body });
|
|
296
|
+
}, {
|
|
297
|
+
body: UserFormSchema
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Arrays
|
|
302
|
+
Form fields like `items[0]`, `items[1]` are automatically converted to arrays:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
const ItemsSchema = z.object({
|
|
306
|
+
items: z.array(z.string()),
|
|
307
|
+
tags: z.array(z.string()),
|
|
308
|
+
profile: z.object({
|
|
309
|
+
name: z.string(),
|
|
310
|
+
age: z.string().transform(val => parseInt(val, 10))
|
|
311
|
+
})
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
app.post("/items", async (ctx) => {
|
|
315
|
+
// Form data: items[0]="Apple", items[1]="Banana" becomes { items: ["Apple", "Banana"] }
|
|
316
|
+
console.log(ctx.body); // { items: ["Apple", "Banana"], tags: [...], profile: {...} }
|
|
317
|
+
return ctx.json({ success: true, data: ctx.body });
|
|
318
|
+
}, {
|
|
319
|
+
body: ItemsSchema
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Supported Patterns
|
|
324
|
+
- **Nested objects**: `profile[name]`, `settings[theme]` → `{ profile: { name: "..." }, settings: { theme: "..." } }`
|
|
325
|
+
- **Arrays**: `items[0]`, `items[1]` → `{ items: ["...", "..."] }`
|
|
326
|
+
- **Duplicate keys**: Multiple values with same key → `{ tags: ["tag1", "tag2"] }`
|
|
327
|
+
|
|
276
328
|
## Running
|
|
277
329
|
|
|
278
330
|
```bash
|
|
279
331
|
bun run ./src/index.ts
|
|
280
332
|
```
|
|
281
333
|
|
|
282
|
-
Or run the
|
|
334
|
+
Or run the examples:
|
|
283
335
|
|
|
284
336
|
```bash
|
|
337
|
+
# CORS example
|
|
285
338
|
bun run ./example/cors-example.ts
|
|
339
|
+
|
|
340
|
+
# Multipart form data parsing example
|
|
341
|
+
bun run ./example/multipart-example.ts
|
|
286
342
|
```
|
|
287
343
|
|
|
288
344
|
## Examples
|
|
@@ -292,6 +348,7 @@ Check out the `example/` directory for more usage examples:
|
|
|
292
348
|
- `cors-example.ts` - Demonstrates CORS plugin and lifecycle hooks
|
|
293
349
|
- `openapi-example.ts` - Demonstrates OpenAPI plugin with tags and security
|
|
294
350
|
- `websocket-example.ts` - Demonstrates WebSocket functionality with interactive HTML client
|
|
351
|
+
- `multipart-example.ts` - Demonstrates multipart/form-data parsing with nested objects and arrays
|
|
295
352
|
- `index.ts` - Basic routing example
|
|
296
353
|
|
|
297
354
|
This project was created using `bun init` in bun v1.2.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import BXO from "../src/index";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const app = new BXO();
|
|
5
|
+
|
|
6
|
+
// Define Zod schema for the form data structure
|
|
7
|
+
const UserFormSchema = z.object({
|
|
8
|
+
name: z.string(),
|
|
9
|
+
email: z.string().email(),
|
|
10
|
+
password: z.string().min(6),
|
|
11
|
+
is_active: z.string().transform(val => val === "1"),
|
|
12
|
+
profile: z.object({
|
|
13
|
+
name: z.string()
|
|
14
|
+
}),
|
|
15
|
+
id: z.string().optional(),
|
|
16
|
+
created_at: z.string().optional(),
|
|
17
|
+
updated_at: z.string().optional(),
|
|
18
|
+
idx: z.string().transform(val => parseInt(val, 10))
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Route to handle multipart/form-data with nested objects
|
|
22
|
+
app.post("/users", async (ctx) => {
|
|
23
|
+
// The form data is automatically parsed into nested objects
|
|
24
|
+
// ctx.body will contain the structured data based on the schema
|
|
25
|
+
console.log("Parsed form data:", ctx.body);
|
|
26
|
+
|
|
27
|
+
return ctx.json({
|
|
28
|
+
message: "User created successfully",
|
|
29
|
+
data: ctx.body
|
|
30
|
+
});
|
|
31
|
+
}, {
|
|
32
|
+
body: UserFormSchema,
|
|
33
|
+
detail: {
|
|
34
|
+
summary: "Create user with multipart/form-data",
|
|
35
|
+
description: "Handles form data with nested objects and arrays",
|
|
36
|
+
tags: ["Users"]
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Example with arrays
|
|
41
|
+
const ArrayFormSchema = z.object({
|
|
42
|
+
items: z.array(z.string()),
|
|
43
|
+
tags: z.array(z.string()),
|
|
44
|
+
profile: z.object({
|
|
45
|
+
name: z.string(),
|
|
46
|
+
age: z.string().transform(val => parseInt(val, 10))
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
app.post("/items", async (ctx) => {
|
|
51
|
+
console.log("Parsed array form data:", ctx.body);
|
|
52
|
+
|
|
53
|
+
return ctx.json({
|
|
54
|
+
message: "Items processed successfully",
|
|
55
|
+
data: ctx.body
|
|
56
|
+
});
|
|
57
|
+
}, {
|
|
58
|
+
body: ArrayFormSchema,
|
|
59
|
+
detail: {
|
|
60
|
+
summary: "Process items with arrays",
|
|
61
|
+
description: "Handles form data with arrays like items[0], items[1]",
|
|
62
|
+
tags: ["Items"]
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Test route to show how the parsing works
|
|
67
|
+
app.get("/test-parsing", async (ctx) => {
|
|
68
|
+
const html = `
|
|
69
|
+
<!DOCTYPE html>
|
|
70
|
+
<html>
|
|
71
|
+
<head>
|
|
72
|
+
<title>Multipart Form Data Test</title>
|
|
73
|
+
<style>
|
|
74
|
+
body { font-family: Arial, sans-serif; margin: 40px; }
|
|
75
|
+
.form-group { margin: 20px 0; }
|
|
76
|
+
label { display: block; margin-bottom: 5px; font-weight: bold; }
|
|
77
|
+
input, textarea { width: 300px; padding: 8px; margin-bottom: 10px; }
|
|
78
|
+
button { background: #007bff; color: white; padding: 10px 20px; border: none; cursor: pointer; }
|
|
79
|
+
.result { margin-top: 20px; padding: 20px; background: #f8f9fa; border-radius: 5px; }
|
|
80
|
+
</style>
|
|
81
|
+
</head>
|
|
82
|
+
<body>
|
|
83
|
+
<h1>Multipart Form Data Parsing Test</h1>
|
|
84
|
+
|
|
85
|
+
<h2>Test 1: Nested Objects (like your image example)</h2>
|
|
86
|
+
<form id="userForm" enctype="multipart/form-data">
|
|
87
|
+
<div class="form-group">
|
|
88
|
+
<label>Name:</label>
|
|
89
|
+
<input type="text" name="name" value="John Doe" required>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="form-group">
|
|
92
|
+
<label>Email:</label>
|
|
93
|
+
<input type="email" name="email" value="john@example.com" required>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="form-group">
|
|
96
|
+
<label>Password:</label>
|
|
97
|
+
<input type="password" name="password" value="password123" required>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="form-group">
|
|
100
|
+
<label>Is Active:</label>
|
|
101
|
+
<input type="text" name="is_active" value="1">
|
|
102
|
+
</div>
|
|
103
|
+
<div class="form-group">
|
|
104
|
+
<label>Profile Name:</label>
|
|
105
|
+
<input type="text" name="profile[name]" value="John Profile" required>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="form-group">
|
|
108
|
+
<label>ID:</label>
|
|
109
|
+
<input type="text" name="id" value="UUID()">
|
|
110
|
+
</div>
|
|
111
|
+
<div class="form-group">
|
|
112
|
+
<label>Created At:</label>
|
|
113
|
+
<input type="text" name="created_at" value="NOW()">
|
|
114
|
+
</div>
|
|
115
|
+
<div class="form-group">
|
|
116
|
+
<label>Updated At:</label>
|
|
117
|
+
<input type="text" name="updated_at" value="NOW()">
|
|
118
|
+
</div>
|
|
119
|
+
<div class="form-group">
|
|
120
|
+
<label>Index:</label>
|
|
121
|
+
<input type="text" name="idx" value="0">
|
|
122
|
+
</div>
|
|
123
|
+
<button type="submit">Submit User Form</button>
|
|
124
|
+
</form>
|
|
125
|
+
|
|
126
|
+
<h2>Test 2: Arrays</h2>
|
|
127
|
+
<form id="itemsForm" enctype="multipart/form-data">
|
|
128
|
+
<div class="form-group">
|
|
129
|
+
<label>Item 1:</label>
|
|
130
|
+
<input type="text" name="items[0]" value="Apple">
|
|
131
|
+
</div>
|
|
132
|
+
<div class="form-group">
|
|
133
|
+
<label>Item 2:</label>
|
|
134
|
+
<input type="text" name="items[1]" value="Banana">
|
|
135
|
+
</div>
|
|
136
|
+
<div class="form-group">
|
|
137
|
+
<label>Item 3:</label>
|
|
138
|
+
<input type="text" name="items[2]" value="Cherry">
|
|
139
|
+
</div>
|
|
140
|
+
<div class="form-group">
|
|
141
|
+
<label>Tag 1:</label>
|
|
142
|
+
<input type="text" name="tags[0]" value="fruit">
|
|
143
|
+
</div>
|
|
144
|
+
<div class="form-group">
|
|
145
|
+
<label>Tag 2:</label>
|
|
146
|
+
<input type="text" name="tags[1]" value="healthy">
|
|
147
|
+
</div>
|
|
148
|
+
<div class="form-group">
|
|
149
|
+
<label>Profile Name:</label>
|
|
150
|
+
<input type="text" name="profile[name]" value="Test Profile">
|
|
151
|
+
</div>
|
|
152
|
+
<div class="form-group">
|
|
153
|
+
<label>Profile Age:</label>
|
|
154
|
+
<input type="text" name="profile[age]" value="25">
|
|
155
|
+
</div>
|
|
156
|
+
<button type="submit">Submit Items Form</button>
|
|
157
|
+
</form>
|
|
158
|
+
|
|
159
|
+
<div id="result" class="result" style="display: none;">
|
|
160
|
+
<h3>Result:</h3>
|
|
161
|
+
<pre id="resultContent"></pre>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<script>
|
|
165
|
+
async function submitForm(form, endpoint) {
|
|
166
|
+
const formData = new FormData(form);
|
|
167
|
+
try {
|
|
168
|
+
const response = await fetch(endpoint, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
body: formData
|
|
171
|
+
});
|
|
172
|
+
const result = await response.json();
|
|
173
|
+
document.getElementById('result').style.display = 'block';
|
|
174
|
+
document.getElementById('resultContent').textContent = JSON.stringify(result, null, 2);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
document.getElementById('result').style.display = 'block';
|
|
177
|
+
document.getElementById('resultContent').textContent = 'Error: ' + error.message;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
document.getElementById('userForm').addEventListener('submit', (e) => {
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
submitForm(e.target, '/users');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
document.getElementById('itemsForm').addEventListener('submit', (e) => {
|
|
187
|
+
e.preventDefault();
|
|
188
|
+
submitForm(e.target, '/items');
|
|
189
|
+
});
|
|
190
|
+
</script>
|
|
191
|
+
</body>
|
|
192
|
+
</html>
|
|
193
|
+
`;
|
|
194
|
+
|
|
195
|
+
return new Response(html, {
|
|
196
|
+
headers: { "Content-Type": "text/html" }
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
app.start();
|
|
201
|
+
|
|
202
|
+
console.log("🚀 Server running at http://localhost:3000");
|
|
203
|
+
console.log("📝 Test the multipart parsing at http://localhost:3000/test-parsing");
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -187,16 +187,66 @@ function parseQuery(searchParams: URLSearchParams, schema?: z.ZodTypeAny): any {
|
|
|
187
187
|
|
|
188
188
|
function formDataToObject(fd: FormData): Record<string, any> {
|
|
189
189
|
const obj: Record<string, any> = {};
|
|
190
|
+
|
|
190
191
|
for (const [key, value] of fd.entries()) {
|
|
191
|
-
|
|
192
|
-
const existing = obj[key];
|
|
193
|
-
if (Array.isArray(existing)) obj[key] = [...existing, value];
|
|
194
|
-
else obj[key] = [existing, value];
|
|
195
|
-
} else obj[key] = value;
|
|
192
|
+
setNestedValue(obj, key, value);
|
|
196
193
|
}
|
|
194
|
+
|
|
197
195
|
return obj;
|
|
198
196
|
}
|
|
199
197
|
|
|
198
|
+
function setNestedValue(obj: Record<string, any>, key: string, value: any): void {
|
|
199
|
+
// Handle array notation like items[0], items[1]
|
|
200
|
+
const arrayMatch = key.match(/^(.+)\[(\d+)\]$/);
|
|
201
|
+
if (arrayMatch) {
|
|
202
|
+
const [, arrayKey, index] = arrayMatch;
|
|
203
|
+
const arrayIndex = parseInt(index, 10);
|
|
204
|
+
|
|
205
|
+
if (!obj[arrayKey]) {
|
|
206
|
+
obj[arrayKey] = [];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Ensure it's an array
|
|
210
|
+
if (!Array.isArray(obj[arrayKey])) {
|
|
211
|
+
obj[arrayKey] = [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Set the value at the specific index
|
|
215
|
+
obj[arrayKey][arrayIndex] = value;
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Handle nested object notation like profile[name], profile[age]
|
|
220
|
+
const nestedMatch = key.match(/^(.+)\[([^\]]+)\]$/);
|
|
221
|
+
if (nestedMatch) {
|
|
222
|
+
const [, parentKey, nestedKey] = nestedMatch;
|
|
223
|
+
|
|
224
|
+
if (!obj[parentKey]) {
|
|
225
|
+
obj[parentKey] = {};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Ensure it's an object
|
|
229
|
+
if (typeof obj[parentKey] !== 'object' || Array.isArray(obj[parentKey])) {
|
|
230
|
+
obj[parentKey] = {};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
obj[parentKey][nestedKey] = value;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Handle simple keys - check for duplicates to convert to arrays
|
|
238
|
+
if (key in obj) {
|
|
239
|
+
const existing = obj[key];
|
|
240
|
+
if (Array.isArray(existing)) {
|
|
241
|
+
obj[key] = [...existing, value];
|
|
242
|
+
} else {
|
|
243
|
+
obj[key] = [existing, value];
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
obj[key] = value;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
200
250
|
function buildMatcher(path: string): { regex: RegExp | null; names: string[] } {
|
|
201
251
|
if (!path.includes(":") && !path.includes("*")) return { regex: null, names: [] };
|
|
202
252
|
const names: string[] = [];
|
package/test-url-encoding.ts
CHANGED
|
@@ -17,3 +17,4 @@ app.get("/api/resources/:resourceType/:id", (ctx) => {
|
|
|
17
17
|
app.start();
|
|
18
18
|
console.log(`Test server running on http://localhost:${app.server?.port}`);
|
|
19
19
|
console.log(`Test URL: http://localhost:${app.server?.port}/api/resources/Doctype%20Permission/01992af8-1c69-7000-9219-9b83c2feb2d6`);
|
|
20
|
+
|