bxo 0.0.5-dev.55 → 0.0.5-dev.57

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bxo",
3
3
  "module": "index.ts",
4
- "version": "0.0.5-dev.55",
4
+ "version": "0.0.5-dev.57",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "exports": {
package/src/index.ts CHANGED
@@ -47,7 +47,9 @@ export type {
47
47
  Plugin,
48
48
  Context,
49
49
  FileUpload,
50
- FormData
50
+ FormData,
51
+ InferResponseType,
52
+ InferZodType
51
53
  } from './types';
52
54
 
53
55
  // Re-export everything from the main BXO class for backward compatibility
@@ -175,6 +175,10 @@ function setNestedValue(obj: any, baseKey: string, path: string[], value: any, i
175
175
  // Numeric key - treat as array index
176
176
  const index = parseInt(lastKey, 10);
177
177
  if (Array.isArray(current)) {
178
+ // Ensure array is large enough
179
+ while (current.length <= index) {
180
+ current.push(undefined);
181
+ }
178
182
  current[index] = value;
179
183
  } else {
180
184
  // Convert to array if needed
@@ -204,51 +208,46 @@ export async function parseRequestBody(request: Request): Promise<any> {
204
208
  const formBody: Record<string, any> = {};
205
209
 
206
210
  for (const [key, value] of formData.entries()) {
207
- if (value instanceof File) {
208
- // Return File instances directly
209
- formBody[key] = value;
210
- } else {
211
- // Parse the key to handle nested objects and arrays (Axios-compatible)
212
- const parsedKey = parseFormKey(key);
213
-
214
- if (parsedKey.isJson) {
215
- // Handle JSON serialization (e.g., "obj{}")
216
- if (typeof value === 'string') {
217
- try {
218
- formBody[parsedKey.baseKey] = JSON.parse(value);
219
- } catch {
220
- formBody[parsedKey.baseKey] = value;
221
- }
222
- } else {
211
+ // Parse the key to handle nested objects and arrays (Axios-compatible)
212
+ const parsedKey = parseFormKey(key);
213
+
214
+ if (parsedKey.isJson) {
215
+ // Handle JSON serialization (e.g., "obj{}")
216
+ if (typeof value === 'string') {
217
+ try {
218
+ formBody[parsedKey.baseKey] = JSON.parse(value);
219
+ } catch {
223
220
  formBody[parsedKey.baseKey] = value;
224
221
  }
225
- } else if (parsedKey.isArray) {
226
- // Handle array notation like "recordIds[]"
227
- if (parsedKey.baseKey in formBody) {
228
- if (Array.isArray(formBody[parsedKey.baseKey])) {
229
- formBody[parsedKey.baseKey].push(value);
230
- } else {
231
- formBody[parsedKey.baseKey] = [formBody[parsedKey.baseKey], value];
232
- }
222
+ } else {
223
+ formBody[parsedKey.baseKey] = value;
224
+ }
225
+ } else if (parsedKey.isArray) {
226
+ // Handle array notation like "recordIds[]"
227
+ if (parsedKey.baseKey in formBody) {
228
+ if (Array.isArray(formBody[parsedKey.baseKey])) {
229
+ formBody[parsedKey.baseKey].push(value);
233
230
  } else {
234
- formBody[parsedKey.baseKey] = [value];
231
+ formBody[parsedKey.baseKey] = [formBody[parsedKey.baseKey], value];
235
232
  }
236
- } else if (parsedKey.path.length > 0) {
237
- // Handle nested object notation like "test[new]", "test[hi][hi]", "arr[0]", "users[0][name]"
238
- setNestedValue(formBody, parsedKey.baseKey, parsedKey.path, value, parsedKey.hasIndexes);
239
233
  } else {
240
- // Handle regular form fields - check if this key already exists
241
- if (key in formBody) {
242
- // If key already exists, convert to array or append to existing array
243
- if (Array.isArray(formBody[key])) {
244
- formBody[key].push(value);
245
- } else {
246
- formBody[key] = [formBody[key], value];
247
- }
234
+ formBody[parsedKey.baseKey] = [value];
235
+ }
236
+ } else if (parsedKey.path.length > 0) {
237
+ // Handle nested object notation like "test[new]", "test[hi][hi]", "arr[0]", "users[0][name]"
238
+ setNestedValue(formBody, parsedKey.baseKey, parsedKey.path, value, parsedKey.hasIndexes);
239
+ } else {
240
+ // Handle regular form fields - check if this key already exists
241
+ if (key in formBody) {
242
+ // If key already exists, convert to array or append to existing array
243
+ if (Array.isArray(formBody[key])) {
244
+ formBody[key].push(value);
248
245
  } else {
249
- // First occurrence of this key
250
- formBody[key] = value;
246
+ formBody[key] = [formBody[key], value];
251
247
  }
248
+ } else {
249
+ // First occurrence of this key
250
+ formBody[key] = value;
252
251
  }
253
252
  }
254
253
  }
@@ -134,6 +134,40 @@ export function processResponse(
134
134
  });
135
135
  }
136
136
 
137
+ // Check if response contains File objects
138
+ const containsFiles = (obj: any): boolean => {
139
+ if (obj instanceof File) return true;
140
+ if (Array.isArray(obj)) {
141
+ return obj.some(item => containsFiles(item));
142
+ }
143
+ if (obj && typeof obj === 'object') {
144
+ return Object.values(obj).some(value => containsFiles(value));
145
+ }
146
+ return false;
147
+ };
148
+
149
+ if (containsFiles(response)) {
150
+ // For responses containing files, we need to handle them specially
151
+ // For now, we'll convert File objects to a serializable format
152
+ const serializableResponse = JSON.parse(JSON.stringify(response, (key, value) => {
153
+ if (value instanceof File) {
154
+ return {
155
+ type: 'File',
156
+ name: value.name,
157
+ size: value.size,
158
+ lastModified: value.lastModified
159
+ };
160
+ }
161
+ return value;
162
+ }));
163
+
164
+ headers.set('Content-Type', 'application/json');
165
+ return new Response(JSON.stringify(serializableResponse), {
166
+ status: ctx.set.status || 200,
167
+ headers: headers
168
+ });
169
+ }
170
+
137
171
  headers.set('Content-Type', 'application/json');
138
172
  return new Response(JSON.stringify(response), {
139
173
  status: ctx.set.status || 200,
@@ -157,6 +191,42 @@ export function processResponse(
157
191
  });
158
192
  }
159
193
 
194
+ // Check if response contains File objects (for the no-cookies case)
195
+ const containsFiles = (obj: any): boolean => {
196
+ if (obj instanceof File) return true;
197
+ if (Array.isArray(obj)) {
198
+ return obj.some(item => containsFiles(item));
199
+ }
200
+ if (obj && typeof obj === 'object') {
201
+ return Object.values(obj).some(value => containsFiles(value));
202
+ }
203
+ return false;
204
+ };
205
+
206
+ if (containsFiles(response)) {
207
+ // For responses containing files, we need to handle them specially
208
+ // For now, we'll convert File objects to a serializable format
209
+ const serializableResponse = JSON.parse(JSON.stringify(response, (key, value) => {
210
+ if (value instanceof File) {
211
+ return {
212
+ type: 'File',
213
+ name: value.name,
214
+ size: value.size,
215
+ lastModified: value.lastModified
216
+ };
217
+ }
218
+ return value;
219
+ }));
220
+
221
+ return new Response(JSON.stringify(serializableResponse), {
222
+ ...responseInit,
223
+ headers: {
224
+ 'Content-Type': 'application/json',
225
+ ...responseInit.headers
226
+ }
227
+ });
228
+ }
229
+
160
230
  return new Response(JSON.stringify(response), {
161
231
  ...responseInit,
162
232
  headers: {
@@ -271,6 +271,116 @@ describe('BXO Framework Integration', () => {
271
271
  });
272
272
  expect(data.message).toBe('Nested data submitted');
273
273
  });
274
+
275
+ it('should handle form data with nested arrays and files', async () => {
276
+ app.post('/api/records-with-files', (ctx) => {
277
+ return { formData: ctx.body, message: 'Records with files submitted' };
278
+ });
279
+
280
+ const formData = new FormData();
281
+ formData.append('records[0][qrPayment]', new File(['test file content'], 'test.png', { type: 'image/png' }));
282
+ formData.append('records[1][qrPayment]', new File(['test file content 2'], 'test2.png', { type: 'image/png' }));
283
+ formData.append('records[0][name]', 'Record 1');
284
+ formData.append('records[1][name]', 'Record 2');
285
+
286
+ const response = await fetch(`${baseUrl}/api/records-with-files`, {
287
+ method: 'POST',
288
+ body: formData
289
+ });
290
+
291
+ const data = await response.json() as {
292
+ formData: {
293
+ records: Array<{
294
+ qrPayment: {
295
+ type: string;
296
+ name: string;
297
+ size: number;
298
+ lastModified: number;
299
+ },
300
+ name: string
301
+ }>
302
+ },
303
+ message: string
304
+ };
305
+
306
+ expect(response.status).toBe(200);
307
+ expect(Array.isArray(data.formData.records)).toBe(true);
308
+ expect(data.formData.records).toHaveLength(2);
309
+ expect(data.formData.records[0]?.name).toBe('Record 1');
310
+ expect(data.formData.records[1]?.name).toBe('Record 2');
311
+ expect(data.formData.records[0]?.qrPayment).toEqual({
312
+ type: 'File',
313
+ name: 'test.png',
314
+ size: 17,
315
+ lastModified: 0
316
+ });
317
+ expect(data.formData.records[1]?.qrPayment).toEqual({
318
+ type: 'File',
319
+ name: 'test2.png',
320
+ size: 19,
321
+ lastModified: 0
322
+ });
323
+ expect(data.message).toBe('Records with files submitted');
324
+ });
325
+
326
+ it('should have File objects in ctx.body', async () => {
327
+ app.post('/api/files-test', (ctx) => {
328
+ // Check if the files are actual File instances
329
+ const body = ctx.body as {
330
+ records: Array<{
331
+ qrPayment: File;
332
+ name: string;
333
+ }>;
334
+ };
335
+ const records = body.records;
336
+ console.log('File instance check:');
337
+ console.log('records[0].qrPayment instanceof File:', records[0]?.qrPayment instanceof File);
338
+ console.log('records[1].qrPayment instanceof File:', records[1]?.qrPayment instanceof File);
339
+ console.log('File name:', records[0]?.qrPayment.name);
340
+ console.log('File size:', records[0]?.qrPayment.size);
341
+ console.log('File type:', records[0]?.qrPayment.type);
342
+
343
+ return {
344
+ message: 'Files received',
345
+ fileInfo: {
346
+ isFile0: records[0]?.qrPayment instanceof File,
347
+ isFile1: records[1]?.qrPayment instanceof File,
348
+ name0: records[0]?.qrPayment.name,
349
+ size0: records[0]?.qrPayment.size,
350
+ type0: records[0]?.qrPayment.type
351
+ }
352
+ };
353
+ });
354
+
355
+ const formData = new FormData();
356
+ formData.append('records[0][qrPayment]', new File(['test file content'], 'test.png', { type: 'image/png' }));
357
+ formData.append('records[1][qrPayment]', new File(['test file content 2'], 'test2.png', { type: 'image/png' }));
358
+ formData.append('records[0][name]', 'Record 1');
359
+ formData.append('records[1][name]', 'Record 2');
360
+
361
+ const response = await fetch(`${baseUrl}/api/files-test`, {
362
+ method: 'POST',
363
+ body: formData
364
+ });
365
+
366
+ const data = await response.json() as {
367
+ message: string;
368
+ fileInfo: {
369
+ isFile0: boolean;
370
+ isFile1: boolean;
371
+ name0: string;
372
+ size0: number;
373
+ type0: string;
374
+ };
375
+ };
376
+
377
+ expect(response.status).toBe(200);
378
+ expect(data.fileInfo.isFile0).toBe(true);
379
+ expect(data.fileInfo.isFile1).toBe(true);
380
+ expect(data.fileInfo.name0).toBe('test.png');
381
+ expect(data.fileInfo.size0).toBe(17);
382
+ expect(data.fileInfo.type0).toBe('image/png');
383
+ });
274
384
  });
275
385
 
276
386
  describe('Response Handling', () => {