diff --git a/src/adapter/web-standard/index.ts b/src/adapter/web-standard/index.ts index 703b3273..f4e3b015 100644 --- a/src/adapter/web-standard/index.ts +++ b/src/adapter/web-standard/index.ts @@ -52,16 +52,35 @@ export const WebStandardAdapter: ElysiaAdapter = { `if(c.body[key]) continue\n` + `const value=form.getAll(key)\n` + `const finalValue=value.length===1?value[0]:value\n` + - `if(key.includes('.')){` + + `if(key.includes('.')||key.includes('[')){` + `const keys=key.split('.')\n` + `const lastKey=keys.pop()\n` + `let current=c.body\n` + `for(const k of keys){` + + `const arrayMatch=k.match(/^(.+)\\[(\\d+)\\]$/)\n` + + `if(arrayMatch){` + + `const arrayKey=arrayMatch[1]\n` + + `const index=parseInt(arrayMatch[2],10)\n` + + `if(!(arrayKey in current))current[arrayKey]=[]\n` + + `if(!Array.isArray(current[arrayKey]))current[arrayKey]=[]\n` + + `if(!current[arrayKey][index])current[arrayKey][index]={}\n` + + `current=current[arrayKey][index]` + + `}else{` + `if(!(k in current)||typeof current[k]!=='object'||current[k]===null)` + `current[k]={}\n` + `current=current[k]` + + `}` + `}\n` + + `const lastArrayMatch=lastKey.match(/^(.+)\\[(\\d+)\\]$/)\n` + + `if(lastArrayMatch){` + + `const arrayKey=lastArrayMatch[1]\n` + + `const index=parseInt(lastArrayMatch[2],10)\n` + + `if(!(arrayKey in current))current[arrayKey]=[]\n` + + `if(!Array.isArray(current[arrayKey]))current[arrayKey]=[]\n` + + `current[arrayKey][index]=finalValue` + + `}else{` + `current[lastKey]=finalValue` + + `}` + `}else c.body[key]=finalValue` + `}` ) diff --git a/src/dynamic-handle.ts b/src/dynamic-handle.ts index 60c002af..ca5859a7 100644 --- a/src/dynamic-handle.ts +++ b/src/dynamic-handle.ts @@ -29,17 +29,50 @@ const setNestedValue = ( path: string, value: any ) => { + // Split by dots, but preserve array indices const keys = path.split('.') const lastKey = keys.pop()! let current = obj for (const key of keys) { - if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) - current[key] = {} - current = current[key] + // Check if key has array index notation: key[0], key[1], etc. + const arrayMatch = key.match(/^(.+)\[(\d+)\]$/) + + if (arrayMatch) { + const [, arrayKey, indexStr] = arrayMatch + const index = parseInt(indexStr, 10) + + // Initialize array if needed + if (!(arrayKey in current)) current[arrayKey] = [] + + // Ensure it's an array + if (!Array.isArray(current[arrayKey])) current[arrayKey] = [] + + // Initialize object at index if needed + if (!current[arrayKey][index]) current[arrayKey][index] = {} + + current = current[arrayKey][index] + } else { + // Regular object property + if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) + current[key] = {} + current = current[key] + } } - current[lastKey] = value + // Handle array index in last key + const arrayMatch = lastKey.match(/^(.+)\[(\d+)\]$/) + if (arrayMatch) { + const [, arrayKey, indexStr] = arrayMatch + const index = parseInt(indexStr, 10) + + if (!(arrayKey in current)) current[arrayKey] = [] + if (!Array.isArray(current[arrayKey])) current[arrayKey] = [] + + current[arrayKey][index] = value + } else { + current[lastKey] = value + } } const injectDefaultValues = ( @@ -159,7 +192,7 @@ export const createDynamicHandler = (app: AnyElysia) => { const value = form.getAll(key) const finalValue = value.length === 1 ? value[0] : value - if (key.includes('.')) + if (key.includes('.') || key.includes('[')) setNestedValue(body, key, finalValue) else body[key] = finalValue } @@ -220,7 +253,7 @@ export const createDynamicHandler = (app: AnyElysia) => { const value = form.getAll(key) const finalValue = value.length === 1 ? value[0] : value - if (key.includes('.')) + if (key.includes('.') || key.includes('[')) setNestedValue(body, key, finalValue) else body[key] = finalValue } @@ -290,7 +323,7 @@ export const createDynamicHandler = (app: AnyElysia) => { const value = form.getAll(key) const finalValue = value.length === 1 ? value[0] : value - if (key.includes('.')) + if (key.includes('.') || key.includes('[')) setNestedValue(body, key, finalValue) else body[key] = finalValue } diff --git a/test/validator/body.test.ts b/test/validator/body.test.ts index 83121e31..680ceee6 100644 --- a/test/validator/body.test.ts +++ b/test/validator/body.test.ts @@ -1298,4 +1298,70 @@ describe('Body Validator', () => { expect(result.nestedName).toBe('Jane') expect(result.nestedFileSize).toBeGreaterThan(0) }) + + it('handle complex nested array with files', async () => { + const app = new Elysia().post( + '/', + ({ body }) => ({ + productName: body.name, + createFilesCount: body.images.create.length, + updateCount: body.images.update.length, + images: { + create: body.images.create.map((f) => f.size), + update: body.images.update.map((f) => ({ + id: f.id, + altText: f.altText, + imgSize: f.img.size + })) + } + }), + { + body: t.Object({ + name: t.String(), + images: t.Object({ + create: t.Files(), + update: t.Array( + t.Object({ + id: t.String(), + img: t.File(), + altText: t.String() + }) + ) + }) + }) + } + ) + + const formData = new FormData() + formData.append('name', 'My Product') + formData.append('images.create[0]', Bun.file('test/images/millenium.jpg')) + formData.append('images.create[1]', Bun.file('test/images/kozeki-ui.webp')) + formData.append('images.update[0].id', '1') + formData.append('images.update[0].img', Bun.file('test/images/midori.png')) + formData.append('images.update[0].altText', 'First image') + formData.append('images.update[1].id', '2') + formData.append('images.update[1].img', Bun.file('test/images/aris-yuzu.jpg')) + formData.append('images.update[1].altText', 'Second image') + + const response = await app.handle( + new Request('http://localhost/', { + method: 'POST', + body: formData + }) + ) + + const result = await response.json() + expect(response.status).toBe(200) + expect(result.productName).toBe('My Product') + expect(result.createFilesCount).toBe(2) + expect(result.updateCount).toBe(2) + expect(result.images.create[0]).toBeGreaterThan(0) + expect(result.images.create[1]).toBeGreaterThan(0) + expect(result.images.update[0].id).toBe('1') + expect(result.images.update[0].altText).toBe('First image') + expect(result.images.update[0].imgSize).toBeGreaterThan(0) + expect(result.images.update[1].id).toBe('2') + expect(result.images.update[1].altText).toBe('Second image') + expect(result.images.update[1].imgSize).toBeGreaterThan(0) + }) })