Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions example/nested-multipart-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Elysia, t } from '../src'

/**
* Example: Nested File Uploads with Multipart Forms
*
* Elysia supports nested file uploads using dot notation in multipart forms.
* This allows you to organize files and data in a nested structure while
* still using standard multipart/form-data encoding.
*
* How it works:
* 1. Client sends files with dot notation keys (e.g., "user.avatar")
* 2. Elysia automatically reconstructs the nested object structure
* 3. Your handler receives a properly nested object
*/

const app = new Elysia()
// Basic nested file upload
.post(
'/user/profile',
({ body }) => ({
message: 'Profile created!',
user: {
name: body.user.name,
avatarSize: body.user.avatar.size
}
}),
{
body: t.Object({
user: t.Object({
name: t.String(),
avatar: t.File()
})
})
}
)

// Deeply nested files
.post(
'/user/portfolio',
({ body }) => ({
bio: body.user.profile.bio,
photoCount: body.user.profile.photos.length
}),
{
body: t.Object({
user: t.Object({
profile: t.Object({
bio: t.String(),
photos: t.Files()
})
})
})
}
)

// Mixed flat and nested fields
.post(
'/post',
({ body }) => ({
title: body.title,
authorName: body.author.name,
imageSize: body.author.avatar.size
}),
{
body: t.Object({
title: t.String(),
author: t.Object({
name: t.String(),
avatar: t.File()
})
})
}
)
.listen(3000)

console.log(`🦊 Server running at http://${app.server?.hostname}:${app.server?.port}`)

/**
* Client-side usage (with fetch):
*
* const formData = new FormData()
* formData.append('user.name', 'John')
* formData.append('user.avatar', fileBlob)
*
* await fetch('http://localhost:3000/user/profile', {
* method: 'POST',
* body: formData
* })
*
*
* Eden client usage (future):
*
* await client.user.profile.post({
* user: {
* name: 'John',
* avatar: fileBlob // Eden will flatten this automatically
* }
* })
*/
80 changes: 75 additions & 5 deletions src/adapter/web-standard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,79 @@ export const WebStandardAdapter: ElysiaAdapter = {

return (
fnLiteral +
`const dangerousKeys=new Set(['__proto__','constructor','prototype'])\n` +
`const isDangerousKey=(k)=>{` +
`if(dangerousKeys.has(k))return true;` +
`const m=k.match(/^(.+)\\[(\\d+)\\]$/);` +
`return m?dangerousKeys.has(m[1]):false` +
`}\n` +
`const parseArrayKey=(k)=>{` +
`const m=k.match(/^(.+)\\[(\\d+)\\]$/);` +
`return m?{name:m[1],index:parseInt(m[2],10)}:null` +
`}\n` +
`for(const key of form.keys()){` +
`if(c.body[key]) continue\n` +
`if(c.body[key])continue\n` +
`const value=form.getAll(key)\n` +
`if(value.length===1)` +
`c.body[key]=value[0]\n` +
`else c.body[key]=value` +
`let finalValue\n` +
`if(value.length===1){\n` +
`const sv=value[0]\n` +
`if(typeof sv==='string'&&(sv.charCodeAt(0)===123||sv.charCodeAt(0)===91)){\n` +
`try{\n` +
`const p=JSON.parse(sv)\n` +
`if(p&&typeof p==='object')finalValue=p\n` +
`}catch{}\n` +
`}\n` +
`if(finalValue===undefined)finalValue=sv\n` +
`}else finalValue=value\n` +
`if(Array.isArray(finalValue)){\n` +
`const stringValue=finalValue.find((entry)=>typeof entry==='string')\n` +
`const files=typeof File==='undefined'?[]:finalValue.filter((entry)=>entry instanceof File)\n` +
`if(stringValue&&files.length&&stringValue.charCodeAt(0)===123){\n` +
`try{\n` +
`const parsed=JSON.parse(stringValue)\n` +
`if(parsed&&typeof parsed==='object'&&!Array.isArray(parsed)){\n` +
`if(!('file' in parsed)&&files.length===1)parsed.file=files[0]\n` +
`else if(!('files' in parsed)&&files.length>1)parsed.files=files\n` +
`finalValue=parsed\n` +
`}\n` +
`}catch{}\n` +
`}\n` +
`}\n` +
`if(key.includes('.')||key.includes('[')){` +
`const keys=key.split('.')\n` +
`const lastKey=keys.pop()\n` +
`if(isDangerousKey(lastKey)||keys.some(isDangerousKey))continue\n` +
`let current=c.body\n` +
`for(const k of keys){` +
`const arrayInfo=parseArrayKey(k)\n` +
`if(arrayInfo){` +
`if(!Array.isArray(current[arrayInfo.name]))current[arrayInfo.name]=[]\n` +
`const existing=current[arrayInfo.name][arrayInfo.index]\n` +
`const isFile=typeof File!=='undefined'&&existing instanceof File\n` +
`if(!existing||typeof existing!=='object'||Array.isArray(existing)||isFile){\n` +
`let parsed\n` +
`if(typeof existing==='string'&&existing.charCodeAt(0)===123){\n` +
`try{` +
`parsed=JSON.parse(existing)\n` +
`if(!parsed||typeof parsed!=='object'||Array.isArray(parsed))parsed=undefined` +
`}catch{}\n` +
`}\n` +
`current[arrayInfo.name][arrayInfo.index]=parsed||{}\n` +
`}\n` +
`current=current[arrayInfo.name][arrayInfo.index]` +
`}else{` +
`if(!current[k]||typeof current[k]!=='object')current[k]={}\n` +
`current=current[k]` +
`}` +
`}\n` +
`const arrayInfo=parseArrayKey(lastKey)\n` +
`if(arrayInfo){` +
`if(!Array.isArray(current[arrayInfo.name]))current[arrayInfo.name]=[]\n` +
`current[arrayInfo.name][arrayInfo.index]=finalValue` +
`}else{` +
`current[lastKey]=finalValue` +
`}` +
`}else c.body[key]=finalValue` +
`}`
)
}
Expand Down Expand Up @@ -125,7 +192,10 @@ export const WebStandardAdapter: ElysiaAdapter = {
},
error404(hasEventHook, hasErrorHook, afterHandle = '') {
let findDynamicRoute =
`if(route===null){` + afterHandle + (hasErrorHook ? '' : 'c.set.status=404') + '\nreturn '
`if(route===null){` +
afterHandle +
(hasErrorHook ? '' : 'c.set.status=404') +
'\nreturn '

if (hasErrorHook)
findDynamicRoute += `app.handleError(c,notFound,false,${this.parameters})`
Expand Down
Loading
Loading