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 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
@@ -5,7 +5,7 @@
5
5
  ".": "./src/index.ts",
6
6
  "./plugins": "./plugins/index.ts"
7
7
  },
8
- "version": "0.0.5-dev.80",
8
+ "version": "0.0.5-dev.82",
9
9
  "type": "module",
10
10
  "devDependencies": {
11
11
  "@types/bun": "latest"
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) => {
File without changes
@@ -18,3 +18,4 @@ 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
20
 
21
+