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 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 CORS example:
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
@@ -5,7 +5,7 @@
5
5
  ".": "./src/index.ts",
6
6
  "./plugins": "./plugins/index.ts"
7
7
  },
8
- "version": "0.0.5-dev.79",
8
+ "version": "0.0.5-dev.80",
9
9
  "type": "module",
10
10
  "devDependencies": {
11
11
  "@types/bun": "latest"
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
- if (key in obj) {
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[] = [];
@@ -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
+