Skip to content

Commit

Permalink
🏭 Support nested objects in formData. #68
Browse files Browse the repository at this point in the history
  • Loading branch information
elbywan committed Jan 25, 2020
1 parent a7940c8 commit 90d9555
Show file tree
Hide file tree
Showing 13 changed files with 183 additions and 51 deletions.
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<a href="https://www.paypal.me/elbywan"><img src="https://img.shields.io/badge/buy%20me%20a-coffee-yellow.svg" alt="coffee-badge" height="20"></a>
</h1>
<h4 align="center">
A tiny (&lt; 2.7Kb g-zipped) wrapper built around fetch with an intuitive syntax.
A tiny (&lt; 2.8Kb g-zipped) wrapper built around fetch with an intuitive syntax.
</h4>
<h5 align="center">
<i>f[ETCH] [WR]apper</i>
Expand Down Expand Up @@ -603,7 +603,7 @@ wretch("...").post(jsonObject)

```
#### formData(formObject: Object)
#### formData(formObject: Object, recursive: string[] | boolean = false)
Converts the javascript object to a FormData and sets the request body.
Expand All @@ -615,6 +615,29 @@ const form = {
wretch("...").formData(form).post()
```
The `recursive` argument when set to `true` will enable recursion through all nested objects and produce `object[key]` keys.
It can be set to an array of string to exclude specific keys.
> Warning: Be careful to exclude `Blob` instances in the Browser, and `ReadableStream` and `Buffer` instances when using the node.js compatible `form-data` package.
```js
const form = {
duck: "Muscovy",
duckProperties: {
beak: {
color: "yellow"
},
legs: 2
},
ignored: {
key: 0
}
}
// Will append the following keys to the FormData payload:
// "duck", "duckProperties[beak][color]", "duckProperties[legs]"
wretch("...").formData(form, ["ignored"]).post()
```
#### formUrl(input: Object | string)
Converts the input parameter to an url encoded string and sets the content-type header and body.
Expand Down
2 changes: 1 addition & 1 deletion dist/bundle/wretch.esm.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/bundle/wretch.esm.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/bundle/wretch.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/bundle/wretch.js.map

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion dist/wretcher.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,11 @@ export declare class Wretcher {
/**
* Converts the javascript object to a FormData and sets the request body.
* @param formObject An object which will be converted to a FormData
* @param recursive If `true`, will recurse through all nested objects
* Can be set as an array of string to exclude specific keys.
* See https:/elbywan/wretch/issues/68 for more details.
*/
formData(formObject: object): Wretcher;
formData(formObject: object, recursive?: string[] | boolean): Wretcher;
/**
* Converts the input to an url encoded string and sets the content-type header and body.
* If the input argument is already a string, skips the conversion part.
Expand Down
37 changes: 26 additions & 11 deletions dist/wretcher.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/wretcher.js.map

Large diffs are not rendered by default.

33 changes: 22 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"conventional-changelog-wretch": "file:scripts/conventional-changelog-wretch",
"coveralls": "^3.0.6",
"dtrace-provider": "^0.8.8",
"form-data": "^2.5.1",
"form-data": "^3.0.0",
"jest": "^24.1.0",
"node-fetch": "^2.6.0",
"restify": "^8.4.0",
Expand Down
40 changes: 30 additions & 10 deletions src/wretcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,12 @@ export class Wretcher {
/**
* Converts the javascript object to a FormData and sets the request body.
* @param formObject An object which will be converted to a FormData
* @param recursive If `true`, will recurse through all nested objects
* Can be set as an array of string to exclude specific keys.
* See https:/elbywan/wretch/issues/68 for more details.
*/
formData(formObject: object) {
return this.body(convertFormData(formObject))
formData(formObject: object, recursive: string[] | boolean = false) {
return this.body(convertFormData(formObject, recursive))
}
/**
* Converts the input to an url encoded string and sets the content-type header and body.
Expand Down Expand Up @@ -309,16 +312,33 @@ const appendQueryParams = (url: string, qp: object | string, replace: boolean) =
return url + "&" + queryString
}

function convertFormData(formObject: object) {
const formData = conf.polyfill("FormData", { instance: true })
for(const key in formObject) {
if(formObject[key] instanceof Array) {
for(const item of formObject[key])
formData.append(key + "[]", item)
function convertFormData(
formObject: object,
recursive: string[] | boolean = false,
formData = conf.polyfill("FormData", { instance: true }),
ancestors = []
) {
Object.entries(formObject).forEach(([key, value]) => {
let formKey = ancestors.reduce((acc, ancestor) => (
acc ? `${acc}[${ancestor}]` : ancestor
), null)
formKey = formKey ? `${formKey}[${key}]` : key
if(value instanceof Array) {
for(const item of value)
formData.append(formKey + "[]", item)
} else if(
recursive &&
typeof value === "object" &&
(
!(recursive instanceof Array) ||
!recursive.includes(key)
)
) {
convertFormData(value, recursive, formData, [...ancestors, key])
} else {
formData.append(key, formObject[key])
formData.append(formKey, value)
}
}
})

return formData
}
Expand Down
17 changes: 14 additions & 3 deletions test/browser/spec/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,23 @@ describe("Wretch", function() {
it("should send a FormData object", async function() {
const form = {
hello: "world",
duck: "Muscovy"
duck: "Muscovy",
duckProperties: {
beak: {
color: "yellow"
},
nbOfLegs: 2
}
}
const decoded = await wretch(`${_URL}/formData/decode`).formData(form).post().json()
const decoded = await wretch(`${_URL}/formData/decode`)
.formData(form, ["duckImage"])
.post()
.json()
expect(decoded).toEqual({
hello: "world",
duck: "Muscovy"
duck: "Muscovy",
"duckProperties[beak][color]": "yellow",
"duckProperties[nbOfLegs]": "2"
})
const f = { arr: [ 1, 2, 3 ]}
const d = await wretch(`${_URL}/formData/decode`).formData(f).post().json()
Expand Down
63 changes: 56 additions & 7 deletions test/node/wretch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,26 +132,75 @@ describe("Wretch", function () {
})

it("should send a FormData object", async function () {
const form = {
// Test with a nested object with an excluded field.
let form : any = {
hello: "world",
duck: "Muscovy",
duckImage: fs.createReadStream(duckImagePath)
duckImage: fs.createReadStream(duckImagePath),
duckProperties: {
beak: {
color: "yellow"
},
nbOfLegs: 2
}
}
const decoded = await wretch(`${_URL}/formData/decode`).formData(form).post().json()
let decoded = await wretch(`${_URL}/formData/decode`)
.formData(form, ["duckImage"])
.post()
.json()
expect(decoded).toMatchObject({
hello: "world",
duck: "Muscovy",
duckImage: {
data: duckImage,
type: "Buffer"
},
"duckProperties[beak][color]": "yellow",
"duckProperties[nbOfLegs]": "2"
})

// Test with full nesting.
form = {
hello: "world",
nested: {
property: 1
}
}
decoded = await wretch(`${_URL}/formData/decode`)
.formData(form, true)
.post()
.json()
expect(decoded).toMatchObject({
hello: "world"
})
// form-data package has an implementation which differs from the browser standard.

// Test without nesting.
form = {
hello: "world",
// Unfortunately, form-data has issues casting objects to strings.
// This means that we cannot test this properly for now…
// See: https:/form-data/form-data/pull/362
// nested: {
// property: 1
// }
}
decoded = await wretch(`${_URL}/formData/decode`)
.formData(form)
.post()
.json()
expect(decoded).toMatchObject({
hello: "world"
})

// Test for arrays.
const f = { arr: [ 1, 2, 3 ]}
const d = await wretch(`${_URL}/formData/decode`).formData(f).post().json()
// expect(d).toEqual({
// "arr[]": [1, 2, 3]
// })
expect(d).toEqual({
// browser FormData output:
// "arr[]": [1, 2, 3]
// form-data package has an implementation which differs from the browser standard.
"arr[]": "3"
})
})

it("should perform OPTIONS and HEAD requests", async function () {
Expand Down

0 comments on commit 90d9555

Please sign in to comment.