bxo 0.0.5-dev.54 → 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 +154 -5
- package/src/utils/response-handler.ts +70 -0
- package/tests/integration/bxo.test.ts +185 -0
- package/tests/unit/utils.test.ts +160 -0
package/package.json
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -78,6 +78,120 @@ export function validateResponse(
|
|
|
78
78
|
return data;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Helper function to parse form keys with nested object and array notation (Axios-compatible)
|
|
82
|
+
function parseFormKey(key: string): {
|
|
83
|
+
baseKey: string;
|
|
84
|
+
path: string[];
|
|
85
|
+
isArray: boolean;
|
|
86
|
+
isJson: boolean;
|
|
87
|
+
hasIndexes: boolean;
|
|
88
|
+
} {
|
|
89
|
+
// Check for special endings like "{}" for JSON serialization FIRST
|
|
90
|
+
if (key.endsWith('{}')) {
|
|
91
|
+
const actualBaseKey = key.slice(0, -2);
|
|
92
|
+
return { baseKey: actualBaseKey, path: [], isArray: false, isJson: true, hasIndexes: false };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const bracketMatch = key.match(/^([^\[]+)(\[.*\])*$/);
|
|
96
|
+
if (!bracketMatch) {
|
|
97
|
+
return { baseKey: key, path: [], isArray: false, isJson: false, hasIndexes: false };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const baseKey = bracketMatch[1];
|
|
101
|
+
if (!baseKey) {
|
|
102
|
+
return { baseKey: key, path: [], isArray: false, isJson: false, hasIndexes: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const bracketPart = key.slice(baseKey.length);
|
|
106
|
+
|
|
107
|
+
if (!bracketPart) {
|
|
108
|
+
return { baseKey, path: [], isArray: false, isJson: false, hasIndexes: false };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check for special endings like "{}" for JSON serialization
|
|
112
|
+
if (baseKey.endsWith('{}')) {
|
|
113
|
+
const actualBaseKey = baseKey.slice(0, -2);
|
|
114
|
+
return { baseKey: actualBaseKey, path: [], isArray: false, isJson: true, hasIndexes: false };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If no bracket part, return simple key
|
|
118
|
+
if (!bracketPart) {
|
|
119
|
+
return { baseKey, path: [], isArray: false, isJson: false, hasIndexes: false };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if this is an array notation (e.g., "recordIds[]")
|
|
123
|
+
if (bracketPart === '[]') {
|
|
124
|
+
return { baseKey, path: [], isArray: true, isJson: false, hasIndexes: false };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Extract all bracket contents
|
|
128
|
+
const path: string[] = [];
|
|
129
|
+
const bracketRegex = /\[([^\]]*)\]/g;
|
|
130
|
+
let match;
|
|
131
|
+
let hasIndexes = false;
|
|
132
|
+
|
|
133
|
+
while ((match = bracketRegex.exec(bracketPart)) !== null) {
|
|
134
|
+
if (match[1] !== undefined) {
|
|
135
|
+
path.push(match[1]);
|
|
136
|
+
// Check if this is a numeric index
|
|
137
|
+
if (/^\d+$/.test(match[1])) {
|
|
138
|
+
hasIndexes = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check if the last path element is empty (indicating array without indexes)
|
|
144
|
+
const isArray = path.length > 0 && path[path.length - 1] === '';
|
|
145
|
+
|
|
146
|
+
return { baseKey, path, isArray, isJson: false, hasIndexes };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Helper function to set nested value in object (Axios-compatible)
|
|
150
|
+
function setNestedValue(obj: any, baseKey: string, path: string[], value: any, isArray: boolean = false): void {
|
|
151
|
+
if (!(baseKey in obj)) {
|
|
152
|
+
obj[baseKey] = isArray ? [] : {};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let current = obj[baseKey];
|
|
156
|
+
|
|
157
|
+
// Navigate to the parent of the target location
|
|
158
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
159
|
+
const key = path[i];
|
|
160
|
+
if (key && (!(key in current) || typeof current[key] !== 'object')) {
|
|
161
|
+
// Check if next key is numeric (array index)
|
|
162
|
+
const nextKey = path[i + 1];
|
|
163
|
+
const isNextKeyNumeric = nextKey && /^\d+$/.test(nextKey);
|
|
164
|
+
current[key] = isNextKeyNumeric ? [] : {};
|
|
165
|
+
}
|
|
166
|
+
if (key) {
|
|
167
|
+
current = current[key];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Set the final value
|
|
172
|
+
const lastKey = path[path.length - 1];
|
|
173
|
+
if (lastKey) {
|
|
174
|
+
if (/^\d+$/.test(lastKey)) {
|
|
175
|
+
// Numeric key - treat as array index
|
|
176
|
+
const index = parseInt(lastKey, 10);
|
|
177
|
+
if (Array.isArray(current)) {
|
|
178
|
+
// Ensure array is large enough
|
|
179
|
+
while (current.length <= index) {
|
|
180
|
+
current.push(undefined);
|
|
181
|
+
}
|
|
182
|
+
current[index] = value;
|
|
183
|
+
} else {
|
|
184
|
+
// Convert to array if needed
|
|
185
|
+
const newArray = [];
|
|
186
|
+
newArray[index] = value;
|
|
187
|
+
current[lastKey] = newArray;
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
current[lastKey] = value;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
81
195
|
// Parse request body based on content type
|
|
82
196
|
export async function parseRequestBody(request: Request): Promise<any> {
|
|
83
197
|
const contentType = request.headers.get('content-type');
|
|
@@ -94,12 +208,47 @@ export async function parseRequestBody(request: Request): Promise<any> {
|
|
|
94
208
|
const formBody: Record<string, any> = {};
|
|
95
209
|
|
|
96
210
|
for (const [key, value] of formData.entries()) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 {
|
|
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);
|
|
230
|
+
} else {
|
|
231
|
+
formBody[parsedKey.baseKey] = [formBody[parsedKey.baseKey], value];
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
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);
|
|
100
239
|
} else {
|
|
101
|
-
// Handle regular form fields
|
|
102
|
-
|
|
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
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
// First occurrence of this key
|
|
250
|
+
formBody[key] = value;
|
|
251
|
+
}
|
|
103
252
|
}
|
|
104
253
|
}
|
|
105
254
|
|
|
@@ -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: {
|
|
@@ -196,6 +196,191 @@ describe('BXO Framework Integration', () => {
|
|
|
196
196
|
expect(data.formData.email).toBe('john@example.com');
|
|
197
197
|
expect(data.message).toBe('Form submitted');
|
|
198
198
|
});
|
|
199
|
+
|
|
200
|
+
it('should handle form data with arrays', async () => {
|
|
201
|
+
app.post('/api/records', (ctx) => {
|
|
202
|
+
return { formData: ctx.body, message: 'Records submitted' };
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const formData = new FormData();
|
|
206
|
+
formData.append('app', 'zodula');
|
|
207
|
+
formData.append('model', 'zodula_User');
|
|
208
|
+
formData.append('recordIds[]', 'asdfasdfdsa');
|
|
209
|
+
formData.append('recordIds[]', 'jarupak.sri@gmail.com');
|
|
210
|
+
formData.append('fields', '*');
|
|
211
|
+
|
|
212
|
+
const response = await fetch(`${baseUrl}/api/records`, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
body: formData
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const data = await response.json() as {
|
|
218
|
+
formData: {
|
|
219
|
+
app: string,
|
|
220
|
+
model: string,
|
|
221
|
+
recordIds: string[],
|
|
222
|
+
fields: string
|
|
223
|
+
},
|
|
224
|
+
message: string
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
expect(response.status).toBe(200);
|
|
228
|
+
expect(data.formData.app).toBe('zodula');
|
|
229
|
+
expect(data.formData.model).toBe('zodula_User');
|
|
230
|
+
expect(Array.isArray(data.formData.recordIds)).toBe(true);
|
|
231
|
+
expect(data.formData.recordIds).toEqual(['asdfasdfdsa', 'jarupak.sri@gmail.com']);
|
|
232
|
+
expect(data.formData.fields).toBe('*');
|
|
233
|
+
expect(data.message).toBe('Records submitted');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should handle form data with nested objects', async () => {
|
|
237
|
+
app.post('/api/nested', (ctx) => {
|
|
238
|
+
return { formData: ctx.body, message: 'Nested data submitted' };
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const formData = new FormData();
|
|
242
|
+
formData.append('test[test]', 'test');
|
|
243
|
+
formData.append('test[new]', 'new');
|
|
244
|
+
formData.append('test[hi][hi]', 'hi');
|
|
245
|
+
|
|
246
|
+
const response = await fetch(`${baseUrl}/api/nested`, {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
body: formData
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const data = await response.json() as {
|
|
252
|
+
formData: {
|
|
253
|
+
test: {
|
|
254
|
+
test: string,
|
|
255
|
+
new: string,
|
|
256
|
+
hi: {
|
|
257
|
+
hi: string
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
message: string
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
expect(response.status).toBe(200);
|
|
265
|
+
expect(data.formData.test).toEqual({
|
|
266
|
+
test: 'test',
|
|
267
|
+
new: 'new',
|
|
268
|
+
hi: {
|
|
269
|
+
hi: 'hi'
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
expect(data.message).toBe('Nested data submitted');
|
|
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
|
+
});
|
|
199
384
|
});
|
|
200
385
|
|
|
201
386
|
describe('Response Handling', () => {
|
package/tests/unit/utils.test.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
parseCookies,
|
|
6
6
|
validateData,
|
|
7
7
|
validateResponse,
|
|
8
|
+
parseRequestBody,
|
|
8
9
|
cookiesToHeaders,
|
|
9
10
|
mergeHeadersWithCookies,
|
|
10
11
|
createRedirectResponse,
|
|
@@ -200,6 +201,165 @@ describe('Utility Functions', () => {
|
|
|
200
201
|
});
|
|
201
202
|
});
|
|
202
203
|
|
|
204
|
+
describe('parseRequestBody', () => {
|
|
205
|
+
it('should parse JSON body correctly', async () => {
|
|
206
|
+
const jsonData = { name: 'john', age: 25 };
|
|
207
|
+
const request = new Request('http://localhost/test', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/json' },
|
|
210
|
+
body: JSON.stringify(jsonData)
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = await parseRequestBody(request);
|
|
214
|
+
expect(result).toEqual(jsonData);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should parse form data with arrays correctly', async () => {
|
|
218
|
+
const formData = new FormData();
|
|
219
|
+
formData.append('app', 'zodula');
|
|
220
|
+
formData.append('model', 'zodula_User');
|
|
221
|
+
formData.append('recordIds[]', 'asdfasdfdsa');
|
|
222
|
+
formData.append('recordIds[]', 'jarupak.sri@gmail.com');
|
|
223
|
+
formData.append('fields', '*');
|
|
224
|
+
|
|
225
|
+
const request = new Request('http://localhost/test', {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
body: formData
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const result = await parseRequestBody(request);
|
|
231
|
+
|
|
232
|
+
expect(result.app).toBe('zodula');
|
|
233
|
+
expect(result.model).toBe('zodula_User');
|
|
234
|
+
expect(Array.isArray(result.recordIds)).toBe(true);
|
|
235
|
+
expect(result.recordIds).toEqual(['asdfasdfdsa', 'jarupak.sri@gmail.com']);
|
|
236
|
+
expect(result.fields).toBe('*');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle single form field correctly', async () => {
|
|
240
|
+
const formData = new FormData();
|
|
241
|
+
formData.append('name', 'john');
|
|
242
|
+
formData.append('email', 'john@example.com');
|
|
243
|
+
|
|
244
|
+
const request = new Request('http://localhost/test', {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
body: formData
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const result = await parseRequestBody(request);
|
|
250
|
+
|
|
251
|
+
expect(result.name).toBe('john');
|
|
252
|
+
expect(result.email).toBe('john@example.com');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should handle file uploads in form data', async () => {
|
|
256
|
+
const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
|
257
|
+
const formData = new FormData();
|
|
258
|
+
formData.append('name', 'john');
|
|
259
|
+
formData.append('file', file);
|
|
260
|
+
|
|
261
|
+
const request = new Request('http://localhost/test', {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
body: formData
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const result = await parseRequestBody(request);
|
|
267
|
+
|
|
268
|
+
expect(result.name).toBe('john');
|
|
269
|
+
expect(result.file).toBeInstanceOf(File);
|
|
270
|
+
expect(result.file.name).toBe('test.txt');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should parse nested object form data correctly', async () => {
|
|
274
|
+
const formData = new FormData();
|
|
275
|
+
formData.append('test[test]', 'test');
|
|
276
|
+
formData.append('test[new]', 'new');
|
|
277
|
+
formData.append('test[hi][hi]', 'hi');
|
|
278
|
+
|
|
279
|
+
const request = new Request('http://localhost/test', {
|
|
280
|
+
method: 'POST',
|
|
281
|
+
body: formData
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const result = await parseRequestBody(request);
|
|
285
|
+
|
|
286
|
+
expect(result.test).toEqual({
|
|
287
|
+
test: 'test',
|
|
288
|
+
new: 'new',
|
|
289
|
+
hi: {
|
|
290
|
+
hi: 'hi'
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should parse Axios-compatible multipart data with arrays and objects', async () => {
|
|
296
|
+
const formData = new FormData();
|
|
297
|
+
formData.append('x', '1');
|
|
298
|
+
formData.append('arr[]', '1');
|
|
299
|
+
formData.append('arr[]', '2');
|
|
300
|
+
formData.append('arr[]', '3');
|
|
301
|
+
formData.append('arr2[0]', '1');
|
|
302
|
+
formData.append('arr2[1][0]', '2');
|
|
303
|
+
formData.append('arr2[2]', '3');
|
|
304
|
+
formData.append('users[0][name]', 'Peter');
|
|
305
|
+
formData.append('users[0][surname]', 'Griffin');
|
|
306
|
+
formData.append('users[1][name]', 'Thomas');
|
|
307
|
+
formData.append('users[1][surname]', 'Anderson');
|
|
308
|
+
formData.append('obj2{}', '[{"x":1}]');
|
|
309
|
+
|
|
310
|
+
const request = new Request('http://localhost/test', {
|
|
311
|
+
method: 'POST',
|
|
312
|
+
body: formData
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const result = await parseRequestBody(request);
|
|
316
|
+
|
|
317
|
+
expect(result.x).toBe('1');
|
|
318
|
+
expect(result.arr).toEqual(['1', '2', '3']);
|
|
319
|
+
expect(result.arr2).toEqual(['1', ['2'], '3']);
|
|
320
|
+
expect(result.users).toEqual([
|
|
321
|
+
{ name: 'Peter', surname: 'Griffin' },
|
|
322
|
+
{ name: 'Thomas', surname: 'Anderson' }
|
|
323
|
+
]);
|
|
324
|
+
expect(result.obj2).toEqual([{ x: 1 }]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should handle JSON serialization with special endings', async () => {
|
|
328
|
+
const formData = new FormData();
|
|
329
|
+
formData.append('myObj{}', '{"x": 1, "s": "foo"}');
|
|
330
|
+
formData.append('settings{}', '{"theme": "dark", "notifications": true}');
|
|
331
|
+
|
|
332
|
+
const request = new Request('http://localhost/test', {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
body: formData
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const result = await parseRequestBody(request);
|
|
338
|
+
|
|
339
|
+
expect(result.myObj).toEqual({ x: 1, s: 'foo' });
|
|
340
|
+
expect(result.settings).toEqual({ theme: 'dark', notifications: true });
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should handle mixed array indexing styles', async () => {
|
|
344
|
+
const formData = new FormData();
|
|
345
|
+
formData.append('tags[]', 'javascript');
|
|
346
|
+
formData.append('tags[]', 'typescript');
|
|
347
|
+
formData.append('scores[0]', '100');
|
|
348
|
+
formData.append('scores[1]', '95');
|
|
349
|
+
formData.append('scores[2]', '88');
|
|
350
|
+
|
|
351
|
+
const request = new Request('http://localhost/test', {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
body: formData
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const result = await parseRequestBody(request);
|
|
357
|
+
|
|
358
|
+
expect(result.tags).toEqual(['javascript', 'typescript']);
|
|
359
|
+
expect(result.scores).toEqual(['100', '95', '88']);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
203
363
|
describe('File Upload Utilities', () => {
|
|
204
364
|
it('should identify file uploads correctly', () => {
|
|
205
365
|
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|