bxo 0.0.5-dev.54 → 0.0.5-dev.55

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.54",
4
+ "version": "0.0.5-dev.55",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "exports": {
@@ -78,6 +78,116 @@ 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
+ current[index] = value;
179
+ } else {
180
+ // Convert to array if needed
181
+ const newArray = [];
182
+ newArray[index] = value;
183
+ current[lastKey] = newArray;
184
+ }
185
+ } else {
186
+ current[lastKey] = value;
187
+ }
188
+ }
189
+ }
190
+
81
191
  // Parse request body based on content type
82
192
  export async function parseRequestBody(request: Request): Promise<any> {
83
193
  const contentType = request.headers.get('content-type');
@@ -98,8 +208,48 @@ export async function parseRequestBody(request: Request): Promise<any> {
98
208
  // Return File instances directly
99
209
  formBody[key] = value;
100
210
  } else {
101
- // Handle regular form fields
102
- formBody[key] = value;
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);
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);
245
+ } else {
246
+ formBody[key] = [formBody[key], value];
247
+ }
248
+ } else {
249
+ // First occurrence of this key
250
+ formBody[key] = value;
251
+ }
252
+ }
103
253
  }
104
254
  }
105
255
 
@@ -196,6 +196,81 @@ 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
+ });
199
274
  });
200
275
 
201
276
  describe('Response Handling', () => {
@@ -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' });