bxo 0.0.5-dev.55 → 0.0.5-dev.56
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 +1 -1
- package/src/utils/index.ts +37 -38
- package/src/utils/response-handler.ts +70 -0
- package/tests/integration/bxo.test.ts +110 -0
package/package.json
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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', () => {
|