Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(utils/body): add dot notation support for parseBody #2675

Merged
merged 23 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
61 changes: 55 additions & 6 deletions deno_dist/utils/body.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HonoRequest } from '../request.ts'

export type BodyData = Record<string, string | File | (string | File)[]>
export type BodyData = Record<string, string | object | File | (string | File)[]>
export type ParseBodyOptions = {
/**
* Determines whether all fields with multiple values should be parsed as arrays.
Expand All @@ -18,11 +18,26 @@ export type ParseBodyOptions = {
* parseBody should return { file: ['aaa', 'bbb'], message: 'hello' }
*/
all?: boolean
/**
* Determines whether all fields with dot notation should be parsed as nested objects.
* @default false
* @example
* const data = new FormData()
* data.append('obj.key1', 'value1')
* data.append('obj.key2', 'value2')
*
* If dot is false:
* parseBody should return { 'obj.key1': 'value1', 'obj.key2': 'value2' }
*
* If dot is true:
* parseBody should return { obj: { key1: 'value1', key2: 'value2' } }
*/
dot?: boolean
}

export const parseBody = async <T extends BodyData = BodyData>(
request: HonoRequest | Request,
options: ParseBodyOptions = { all: false }
options: ParseBodyOptions = { all: false, dot: false }
): Promise<T> => {
const headers = request instanceof HonoRequest ? request.raw.headers : request.headers
const contentType = headers.get('Content-Type')
Expand Down Expand Up @@ -60,23 +75,57 @@ function convertFormDataToBodyData<T extends BodyData = BodyData>(
const shouldParseAllValues = options.all || key.endsWith('[]')

if (!shouldParseAllValues) {
form[key] = value
if (options.dot && key.includes('.')) {
setNestedValue(form, key.split('.'), value)
} else {
form[key] = value
}
} else {
handleParsingAllValues(form, key, value)
handleParsingAllValues(form, key, value, options.dot)
}
})

return form as T
}

const handleParsingAllValues = (form: BodyData, key: string, value: FormDataEntryValue): void => {
const handleParsingAllValues = (
form: BodyData,
key: string,
value: FormDataEntryValue,
dot?: boolean
): void => {
const formKey = form[key]

if (formKey && Array.isArray(formKey)) {
;(form[key] as (string | File)[]).push(value)
} else if (formKey) {
form[key] = [formKey, value]
} else {
form[key] = value
if (dot) {
setNestedValue(form, key.split('.'), value)
} else {
form[key] = value
}
}
}

const setNestedValue = (form: BodyData, keys: string[], value: FormDataEntryValue): BodyData => {
let nestedForm = form

keys.forEach((key, index) => {
if (index === keys.length - 1) {
nestedForm[key] = value
} else {
if (
!nestedForm[key] ||
typeof nestedForm[key] !== 'object' ||
Array.isArray(nestedForm[key])
) {
nestedForm[key] = nestedForm[key] || {}
}
nestedForm = nestedForm[key] as BodyData
}
})

return nestedForm
}
43 changes: 43 additions & 0 deletions src/utils/body.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,49 @@ describe('Parse Body Util', () => {
})
})

it('should not parse nested values in default', async () => {
const data = new FormData()
data.append('obj.key1', 'value1')
data.append('obj.key2', 'value2')

const req = createRequest(FORM_URL, 'POST', data)

expect(await parseBody(req, { dot: false })).toEqual({
'obj.key1': 'value1',
'obj.key2': 'value2',
})
})

it('should parse nested values if `dot` option is true', async () => {
const data = new FormData()
data.append('obj.key1', 'value1')
data.append('obj.key2', 'value2')

const req = createRequest(FORM_URL, 'POST', data)

expect(await parseBody(req, { dot: true })).toEqual({
obj: { key1: 'value1', key2: 'value2' },
})
})

it('should parse nested values if values are `File`', async () => {
const file1 = new File(['foo'], 'file1', {
type: 'application/octet-stream',
})
const file2 = new File(['bar'], 'file2', {
type: 'application/octet-stream',
})
const data = new FormData()
data.append('file.file1', file1)
data.append('file.file2', file2)

const req = createRequest(FORM_URL, 'POST', data)

expect(await parseBody(req, { all: true, dot: true })).toEqual({
file: { file1, file2 },
})
})

it('should parse multiple values if values are `File`', async () => {
const file1 = new File(['foo'], 'file1', {
type: 'application/octet-stream',
Expand Down
61 changes: 55 additions & 6 deletions src/utils/body.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HonoRequest } from '../request'

export type BodyData = Record<string, string | File | (string | File)[]>
export type BodyData = Record<string, string | object | File | (string | File)[]>
fzn0x marked this conversation as resolved.
Show resolved Hide resolved
export type ParseBodyOptions = {
/**
* Determines whether all fields with multiple values should be parsed as arrays.
Expand All @@ -18,11 +18,26 @@ export type ParseBodyOptions = {
* parseBody should return { file: ['aaa', 'bbb'], message: 'hello' }
*/
all?: boolean
fzn0x marked this conversation as resolved.
Show resolved Hide resolved
/**
* Determines whether all fields with dot notation should be parsed as nested objects.
* @default false
* @example
* const data = new FormData()
* data.append('obj.key1', 'value1')
* data.append('obj.key2', 'value2')
*
* If dot is false:
* parseBody should return { 'obj.key1': 'value1', 'obj.key2': 'value2' }
*
* If dot is true:
* parseBody should return { obj: { key1: 'value1', key2: 'value2' } }
*/
dot?: boolean
fzn0x marked this conversation as resolved.
Show resolved Hide resolved
fzn0x marked this conversation as resolved.
Show resolved Hide resolved
}

export const parseBody = async <T extends BodyData = BodyData>(
request: HonoRequest | Request,
options: ParseBodyOptions = { all: false }
options: ParseBodyOptions = { all: false, dot: false }
fzn0x marked this conversation as resolved.
Show resolved Hide resolved
): Promise<T> => {
const headers = request instanceof HonoRequest ? request.raw.headers : request.headers
const contentType = headers.get('Content-Type')
Expand Down Expand Up @@ -60,23 +75,57 @@ function convertFormDataToBodyData<T extends BodyData = BodyData>(
const shouldParseAllValues = options.all || key.endsWith('[]')

if (!shouldParseAllValues) {
form[key] = value
if (options.dot && key.includes('.')) {
fzn0x marked this conversation as resolved.
Show resolved Hide resolved
setNestedValue(form, key.split('.'), value)
fzn0x marked this conversation as resolved.
Show resolved Hide resolved
} else {
form[key] = value
}
} else {
handleParsingAllValues(form, key, value)
handleParsingAllValues(form, key, value, options.dot)
}
})

return form as T
}

const handleParsingAllValues = (form: BodyData, key: string, value: FormDataEntryValue): void => {
const handleParsingAllValues = (
fzn0x marked this conversation as resolved.
Show resolved Hide resolved
form: BodyData,
key: string,
value: FormDataEntryValue,
dot?: boolean
): void => {
const formKey = form[key]

if (formKey && Array.isArray(formKey)) {
;(form[key] as (string | File)[]).push(value)
} else if (formKey) {
form[key] = [formKey, value]
} else {
form[key] = value
if (dot) {
setNestedValue(form, key.split('.'), value)
} else {
form[key] = value
}
}
}

const setNestedValue = (form: BodyData, keys: string[], value: FormDataEntryValue): BodyData => {
let nestedForm = form

keys.forEach((key, index) => {
if (index === keys.length - 1) {
nestedForm[key] = value
} else {
if (
!nestedForm[key] ||
typeof nestedForm[key] !== 'object' ||
Array.isArray(nestedForm[key])
) {
nestedForm[key] = nestedForm[key] || {}
}
nestedForm = nestedForm[key] as BodyData
}
})

return nestedForm
}