parse-nested-form-data 1.0.0 → 1.1.0
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/README.md +315 -174
- package/dist/index.d.ts +143 -30
- package/dist/parse-nested-form-data.cjs.js +578 -369
- package/dist/parse-nested-form-data.esm.js +572 -366
- package/dist/parse-nested-form-data.umd.js +607 -397
- package/dist/parse-nested-form-data.umd.js.map +1 -1
- package/dist/parse-nested-form-data.umd.min.js +7 -7
- package/dist/parse-nested-form-data.umd.min.js.map +1 -1
- package/package.json +108 -53
- package/CHANGELOG.md +0 -13
- package/dist/__tests__/index.d.ts +0 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1>parse-nested-form-data</h1>
|
|
3
3
|
|
|
4
|
-
<p>
|
|
4
|
+
<p>Parse FormData into nested objects and arrays, based on the field names</p>
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
[![Build Status][build-badge]][build]
|
|
11
11
|
[![Code Coverage][coverage-badge]][coverage]
|
|
12
12
|
[![version][version-badge]][package]
|
|
13
|
+
[![Size][size-badge]][size]
|
|
13
14
|
[![downloads][downloads-badge]][npmtrends]
|
|
14
15
|
[![MIT License][license-badge]][license]
|
|
15
16
|
[![All Contributors][all-contributors-badge]](#contributors-)
|
|
@@ -17,33 +18,53 @@
|
|
|
17
18
|
[![Code of Conduct][coc-badge]][coc]
|
|
18
19
|
<!-- prettier-ignore-end -->
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
A form sends flat name-value pairs. This library turns them into nested objects
|
|
22
|
+
and arrays, with numbers, booleans and `null`. The field names tell it how.
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<form method="post">
|
|
26
|
+
<input name="user.name" value="Ada" />
|
|
27
|
+
<input name="+user.age" value="36" />
|
|
28
|
+
<input name="&user.admin" type="checkbox" checked />
|
|
29
|
+
<input name="tags[]" value="math" />
|
|
30
|
+
<input name="tags[]" value="code" />
|
|
31
|
+
</form>
|
|
32
|
+
```
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
```ts
|
|
35
|
+
parseFormData(formData)
|
|
36
|
+
// {
|
|
37
|
+
// user: {name: 'Ada', age: 36, admin: true},
|
|
38
|
+
// tags: ['math', 'code'],
|
|
39
|
+
// }
|
|
40
|
+
```
|
|
27
41
|
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
It's under 2 kB (minified and brotli-compressed), has no dependencies and works
|
|
43
|
+
in all modern JavaScript runtimes (see
|
|
44
|
+
[Supported runtimes](#supported-runtimes)).
|
|
30
45
|
|
|
31
46
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
|
32
47
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
|
33
48
|
|
|
34
|
-
- [
|
|
35
|
-
- [
|
|
36
|
-
|
|
37
|
-
- [
|
|
49
|
+
- [Install](#install)
|
|
50
|
+
- [Usage](#usage)
|
|
51
|
+
- [Field names](#field-names)
|
|
52
|
+
- [Numbers](#numbers)
|
|
53
|
+
- [Booleans](#booleans)
|
|
38
54
|
- [Null](#null)
|
|
39
|
-
- [
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
42
|
-
|
|
43
|
-
- [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
- [Arrays](#arrays)
|
|
56
|
+
- [Objects](#objects)
|
|
57
|
+
- [Files](#files)
|
|
58
|
+
- [API](#api)
|
|
59
|
+
- [parseFormData(formData, options?)](#parseformdataformdata-options)
|
|
60
|
+
- [formData](#formdata)
|
|
61
|
+
- [options](#options)
|
|
62
|
+
- [removeEmptyString](#removeemptystring)
|
|
63
|
+
- [depth](#depth)
|
|
64
|
+
- [transformEntry](#transformentry)
|
|
65
|
+
- [Errors](#errors)
|
|
66
|
+
- [Security](#security)
|
|
67
|
+
- [Supported runtimes](#supported-runtimes)
|
|
47
68
|
- [Issues](#issues)
|
|
48
69
|
- [🐛 Bugs](#-bugs)
|
|
49
70
|
- [💡 Feature Requests](#-feature-requests)
|
|
@@ -53,257 +74,377 @@ object out of the values. You can tell the parser to do not parse empty values.
|
|
|
53
74
|
|
|
54
75
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
|
55
76
|
|
|
56
|
-
##
|
|
57
|
-
|
|
58
|
-
This module is distributed via [npm][npm] which is bundled with [node][node] and
|
|
59
|
-
should be installed as one of your project's `dependencies`:
|
|
77
|
+
## Install
|
|
60
78
|
|
|
61
79
|
```sh
|
|
62
80
|
npm install parse-nested-form-data
|
|
63
81
|
```
|
|
64
82
|
|
|
65
|
-
##
|
|
83
|
+
## Usage
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import {parseFormData} from 'parse-nested-form-data'
|
|
87
|
+
|
|
88
|
+
const data = parseFormData(await request.formData())
|
|
89
|
+
```
|
|
66
90
|
|
|
67
|
-
|
|
68
|
-
|
|
91
|
+
`parseFormData` reads each entry, puts its value at the place its name tells,
|
|
92
|
+
and returns a new object. The input stays unchanged.
|
|
69
93
|
|
|
70
|
-
|
|
71
|
-
- `[]`: to create an array (pushes value)
|
|
72
|
-
- `[$order]`: to create an array (sets value at index and squashes array)
|
|
73
|
-
- `&`: to transform the value to a boolean
|
|
74
|
-
- `-`: to transform the value to null
|
|
75
|
-
- `+`: to transform the value to a number
|
|
94
|
+
## Field names
|
|
76
95
|
|
|
77
|
-
|
|
96
|
+
| In the name | Meaning | Name and value | Result |
|
|
97
|
+
| ---------------- | ------------------------------------ | -------------- | --------------- |
|
|
98
|
+
| `.key` | A key in an object | `a.b` = `x` | `{a: {b: 'x'}}` |
|
|
99
|
+
| `[key]` | A key in an object, same as `.key` | `a[b]` = `x` | `{a: {b: 'x'}}` |
|
|
100
|
+
| `[]` | Add the value to the end of an array | `a[]` = `x` | `{a: ['x']}` |
|
|
101
|
+
| `[0]`, `[1]`, … | Put the value at this array position | `a[0]` = `x` | `{a: ['x']}` |
|
|
102
|
+
| `+` at the start | Change the value to a number | `+a` = `36` | `{a: 36}` |
|
|
103
|
+
| `&` at the start | Change the value to a boolean | `&a` = `on` | `{a: true}` |
|
|
104
|
+
| `-` at the start | Change the value to `null` | `-a` = `x` | `{a: null}` |
|
|
78
105
|
|
|
79
|
-
|
|
106
|
+
You can combine them, for example `+orders[0].items[].price`.
|
|
80
107
|
|
|
81
|
-
|
|
108
|
+
In this README, the name without its `+`, `&` or `-` is the **path**. Each
|
|
109
|
+
`key`, `.key`, `[key]`, `[]` and `[0]` in it is a **path part**: `a.b[0]` has
|
|
110
|
+
three path parts.
|
|
82
111
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
112
|
+
### Numbers
|
|
113
|
+
|
|
114
|
+
A name that starts with `+` changes the value with `Number(value)`. A value that
|
|
115
|
+
isn't a number gives `NaN`. An empty string gives `0`.
|
|
86
116
|
|
|
87
117
|
```ts
|
|
88
118
|
const formData = new FormData()
|
|
89
|
-
formData.append('
|
|
90
|
-
formData.append('
|
|
91
|
-
formData.append('
|
|
92
|
-
formData.append('&isFalse1', '0')
|
|
93
|
-
formData.append('&isTrue2', 'on')
|
|
94
|
-
formData.append('&isFalse2', 'off')
|
|
119
|
+
formData.append('+a', '1')
|
|
120
|
+
formData.append('+b', '1.5')
|
|
121
|
+
formData.append('+c', 'x')
|
|
95
122
|
parseFormData(formData)
|
|
96
|
-
// {
|
|
97
|
-
// isTrue: true,
|
|
98
|
-
// isFalse: false,
|
|
99
|
-
// isTrue1: true,
|
|
100
|
-
// isFalse1: false,
|
|
101
|
-
// isTrue2: true,
|
|
102
|
-
// isFalse2: false,
|
|
103
|
-
// }
|
|
123
|
+
// {a: 1, b: 1.5, c: NaN}
|
|
104
124
|
```
|
|
105
125
|
|
|
106
|
-
###
|
|
126
|
+
### Booleans
|
|
107
127
|
|
|
108
|
-
|
|
128
|
+
A name that starts with `&` changes the value to a boolean. The value `on`,
|
|
129
|
+
`true` or any number other than `0` gives `true`. All other values give `false`.
|
|
109
130
|
|
|
110
131
|
```ts
|
|
111
132
|
const formData = new FormData()
|
|
112
|
-
formData.append('
|
|
113
|
-
formData.append('
|
|
114
|
-
formData.append('
|
|
133
|
+
formData.append('&a', 'on')
|
|
134
|
+
formData.append('&b', 'true')
|
|
135
|
+
formData.append('&c', '1')
|
|
136
|
+
formData.append('&d', 'off')
|
|
137
|
+
formData.append('&e', 'false')
|
|
138
|
+
formData.append('&f', '0')
|
|
115
139
|
parseFormData(formData)
|
|
116
|
-
// {
|
|
117
|
-
// number: 1,
|
|
118
|
-
// number2: 1.1,
|
|
119
|
-
// number3: NaN,
|
|
120
|
-
// }
|
|
140
|
+
// {a: true, b: true, c: true, d: false, e: false, f: false}
|
|
121
141
|
```
|
|
122
142
|
|
|
143
|
+
A checkbox sends `on` when it's checked. When it isn't checked, the browser
|
|
144
|
+
sends nothing, so the key is missing from the result.
|
|
145
|
+
|
|
123
146
|
### Null
|
|
124
147
|
|
|
125
|
-
|
|
148
|
+
A name that starts with `-` changes the value to `null`, whatever the value is.
|
|
126
149
|
|
|
127
150
|
```ts
|
|
128
151
|
const formData = new FormData()
|
|
129
|
-
formData.append('-
|
|
130
|
-
formData.append('-ignored', 'ignored')
|
|
152
|
+
formData.append('-a', 'x')
|
|
131
153
|
parseFormData(formData)
|
|
132
|
-
// {
|
|
133
|
-
// null: null,
|
|
134
|
-
// ignored: null,
|
|
135
|
-
// }
|
|
154
|
+
// {a: null}
|
|
136
155
|
```
|
|
137
156
|
|
|
138
|
-
###
|
|
157
|
+
### Arrays
|
|
139
158
|
|
|
140
|
-
|
|
141
|
-
If the path of the name prop ends with `[$order]` the value will be set at the
|
|
142
|
-
index of the order. Cannot be mixed with `[]`.
|
|
159
|
+
`[]` adds the value to the end of the array, in the order of the entries:
|
|
143
160
|
|
|
144
161
|
```ts
|
|
145
162
|
const formData = new FormData()
|
|
146
|
-
formData.append('
|
|
147
|
-
formData.append('
|
|
148
|
-
formData.append('array[2]', '3')
|
|
149
|
-
formData.append('array[3]', '4')
|
|
163
|
+
formData.append('a[]', '1')
|
|
164
|
+
formData.append('a[]', '2')
|
|
150
165
|
parseFormData(formData)
|
|
151
|
-
// {
|
|
152
|
-
// array: ['1', '2', '3', '4'],
|
|
153
|
-
// }
|
|
166
|
+
// {a: ['1', '2']}
|
|
154
167
|
```
|
|
155
168
|
|
|
169
|
+
`[0]`, `[1]`, … put the value at that position. The order of the entries doesn't
|
|
170
|
+
matter. Gaps are removed, so `a[1]` and `a[5]` give an array of two values:
|
|
171
|
+
|
|
156
172
|
```ts
|
|
157
173
|
const formData = new FormData()
|
|
158
|
-
formData.append('
|
|
159
|
-
formData.append('
|
|
160
|
-
formData.append('array[]', '3')
|
|
174
|
+
formData.append('a[5]', 'second')
|
|
175
|
+
formData.append('a[1]', 'first')
|
|
161
176
|
parseFormData(formData)
|
|
162
|
-
// {
|
|
163
|
-
// array: ['1', '2', '3'],
|
|
164
|
-
// }
|
|
177
|
+
// {a: ['first', 'second']}
|
|
165
178
|
```
|
|
166
179
|
|
|
167
|
-
|
|
180
|
+
One array can't use both `[]` and `[0]`. That throws a `MixedArrayError`.
|
|
181
|
+
|
|
182
|
+
The largest position is `4294967294`, the largest index a JavaScript array can
|
|
183
|
+
hold. A larger number throws an `InvalidPathError`.
|
|
184
|
+
|
|
185
|
+
Arrays can hold objects and other arrays:
|
|
168
186
|
|
|
169
187
|
```ts
|
|
170
188
|
const formData = new FormData()
|
|
171
|
-
formData.append('
|
|
172
|
-
formData.append('
|
|
173
|
-
formData.append('
|
|
189
|
+
formData.append('a[0].b', '1')
|
|
190
|
+
formData.append('a[1].b', '2')
|
|
191
|
+
formData.append('c[0][]', '3')
|
|
192
|
+
formData.append('c[0][]', '4')
|
|
193
|
+
formData.append('c[1][]', '5')
|
|
174
194
|
parseFormData(formData)
|
|
175
|
-
// {
|
|
176
|
-
// array: [{a: '1'}, {a: '2'}, {a: '3'}],
|
|
177
|
-
// }
|
|
195
|
+
// {a: [{b: '1'}, {b: '2'}], c: [['3', '4'], ['5']]}
|
|
178
196
|
```
|
|
179
197
|
|
|
180
|
-
|
|
198
|
+
### Objects
|
|
199
|
+
|
|
200
|
+
`.key` and `[key]` mean the same thing, so `a.b` and `a[b]` are the same key.
|
|
201
|
+
Only digits in brackets (`[0]`) make an array. You can mix both forms with each
|
|
202
|
+
other and with arrays:
|
|
181
203
|
|
|
182
204
|
```ts
|
|
183
205
|
const formData = new FormData()
|
|
184
|
-
formData.append('
|
|
185
|
-
formData.append('
|
|
186
|
-
formData.append('
|
|
187
|
-
formData.append('array[1][]', '4')
|
|
206
|
+
formData.append('a[b]', '1')
|
|
207
|
+
formData.append('a.c[0].d', '2')
|
|
208
|
+
formData.append('a[c][1][d]', '3')
|
|
188
209
|
parseFormData(formData)
|
|
189
|
-
// {
|
|
190
|
-
// array: [['1', '2'], ['3', '4']],
|
|
191
|
-
// }
|
|
210
|
+
// {a: {b: '1', c: [{d: '2'}, {d: '3'}]}}
|
|
192
211
|
```
|
|
193
212
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
If the name prop contains a `.` the value will be nested in an object.
|
|
213
|
+
The text in brackets is the key exactly as written. It can contain `.`, `[` and
|
|
214
|
+
`]`, if the brackets inside are balanced:
|
|
197
215
|
|
|
198
216
|
```ts
|
|
199
217
|
const formData = new FormData()
|
|
200
|
-
formData.append('
|
|
201
|
-
formData.append('
|
|
218
|
+
formData.append('a[b.c]', '1')
|
|
219
|
+
formData.append('a[d[e]]', '2')
|
|
202
220
|
parseFormData(formData)
|
|
203
|
-
// {
|
|
204
|
-
// object: {
|
|
205
|
-
// a: '1',
|
|
206
|
-
// b: '2',
|
|
207
|
-
// },
|
|
208
|
-
// }
|
|
221
|
+
// {a: {'b.c': '1', 'd[e]': '2'}}
|
|
209
222
|
```
|
|
210
223
|
|
|
211
|
-
|
|
224
|
+
Brackets in the wrong place don't cause an error:
|
|
225
|
+
|
|
226
|
+
- A `[` without a matching `]` stays in the key. `a[[0]` is the key `[0` in `a`.
|
|
227
|
+
`a[0` is the key `a[0`.
|
|
228
|
+
- A `]` without a matching `[` stays in the key. `a]b` is the key `a]b`.
|
|
229
|
+
- Text after `]` starts the next key. `a[0]b` is the same as `a[0].b`.
|
|
230
|
+
- Empty keys are skipped. `a..b` and `a.[b]` are the same as `a.b`, and `[a]` is
|
|
231
|
+
the same as `a`.
|
|
212
232
|
|
|
213
|
-
###
|
|
233
|
+
### Files
|
|
234
|
+
|
|
235
|
+
`File` values stay as they are. This includes native files and files from a
|
|
236
|
+
polyfill such as `@remix-run/web-file`. A file is always the end of a path:
|
|
214
237
|
|
|
215
238
|
```ts
|
|
216
239
|
const formData = new FormData()
|
|
217
|
-
formData.append('
|
|
218
|
-
formData
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
240
|
+
formData.append('avatar', new File(['...'], 'avatar.png'))
|
|
241
|
+
parseFormData(formData)
|
|
242
|
+
// {avatar: File}
|
|
243
|
+
|
|
244
|
+
formData.append('avatar.name', 'me.png')
|
|
245
|
+
parseFormData(formData)
|
|
246
|
+
// throws DuplicateKeyError('avatar')
|
|
223
247
|
```
|
|
224
248
|
|
|
225
|
-
##
|
|
249
|
+
## API
|
|
250
|
+
|
|
251
|
+
### parseFormData(formData, options?)
|
|
252
|
+
|
|
253
|
+
Returns a new object with the parsed values.
|
|
226
254
|
|
|
227
|
-
|
|
255
|
+
#### formData
|
|
256
|
+
|
|
257
|
+
Type: `Iterable<[string, string | File]>`
|
|
258
|
+
|
|
259
|
+
A `FormData` object, or any other list of `[name, value]` pairs, for example
|
|
260
|
+
`[['a.b', 'x']]` or a `URLSearchParams` object.
|
|
261
|
+
|
|
262
|
+
#### options
|
|
263
|
+
|
|
264
|
+
Type: `object`
|
|
265
|
+
|
|
266
|
+
##### removeEmptyString
|
|
267
|
+
|
|
268
|
+
Type: `boolean`\
|
|
269
|
+
Default: `false`
|
|
270
|
+
|
|
271
|
+
Skip all entries whose value is an empty string (`''`).
|
|
228
272
|
|
|
229
273
|
```ts
|
|
230
274
|
const formData = new FormData()
|
|
231
|
-
formData.append('a
|
|
232
|
-
formData.append('
|
|
233
|
-
formData
|
|
234
|
-
|
|
235
|
-
formData.append('+b[100]', '5')
|
|
236
|
-
parseFormData(formData)
|
|
237
|
-
// {
|
|
238
|
-
// a: [
|
|
239
|
-
// {
|
|
240
|
-
// b: {
|
|
241
|
-
// c: '1',
|
|
242
|
-
// d: '2',
|
|
243
|
-
// },
|
|
244
|
-
// },
|
|
245
|
-
// '3',
|
|
246
|
-
// ],
|
|
247
|
-
// b: ['4', 5],
|
|
248
|
-
// }
|
|
275
|
+
formData.append('a', '')
|
|
276
|
+
formData.append('b', 'x')
|
|
277
|
+
parseFormData(formData, {removeEmptyString: true})
|
|
278
|
+
// {b: 'x'}
|
|
249
279
|
```
|
|
250
280
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
The second argument is an option object with following properties:
|
|
281
|
+
##### depth
|
|
254
282
|
|
|
255
|
-
|
|
283
|
+
Type: `number`\
|
|
284
|
+
Default: `Infinity`
|
|
256
285
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
286
|
+
The maximum number of path parts in one path. All parts count, so `a.b[0]` and
|
|
287
|
+
`a[b][]` both have three. A longer path throws a [`DepthLimitError`](#errors).
|
|
288
|
+
The value must be a positive integer or `Infinity`. Other values throw a
|
|
289
|
+
`TypeError`.
|
|
260
290
|
|
|
261
291
|
```ts
|
|
262
292
|
const formData = new FormData()
|
|
263
|
-
formData.append('
|
|
264
|
-
formData
|
|
265
|
-
|
|
266
|
-
formData.append('foo[0].baz', '4')
|
|
293
|
+
formData.append('user.address.street', 'Main St')
|
|
294
|
+
parseFormData(formData, {depth: 3})
|
|
295
|
+
// {user: {address: {street: 'Main St'}}}
|
|
267
296
|
|
|
268
|
-
|
|
269
|
-
|
|
297
|
+
formData.append('user.address.geo.lat', '52.5')
|
|
298
|
+
parseFormData(formData, {depth: 3})
|
|
299
|
+
// throws DepthLimitError('user.address.geo.lat')
|
|
270
300
|
```
|
|
271
301
|
|
|
272
|
-
|
|
302
|
+
The parser can handle any depth. But a field like `a.a.a…` with thousands of
|
|
303
|
+
parts makes an object that crashes code that walks it recursively, for example
|
|
304
|
+
`JSON.stringify`, validators or deep clones. If the input comes from untrusted
|
|
305
|
+
clients, set a limit. `depth: 32` is enough for almost every form. Version 2.0
|
|
306
|
+
will change the default to a smaller number.
|
|
307
|
+
|
|
308
|
+
The depth is checked after `transformEntry`. An entry that `removeEmptyString`
|
|
309
|
+
skips is never checked.
|
|
310
|
+
|
|
311
|
+
##### transformEntry
|
|
312
|
+
|
|
313
|
+
Type:
|
|
314
|
+
`(entry: [path: string, value: string | File], defaultTransform) => {path: string, value: string | number | boolean | null | File}`\
|
|
315
|
+
Default:
|
|
316
|
+
the built-in transform for `+`, `&` and `-`
|
|
317
|
+
|
|
318
|
+
Changes each entry before it's parsed. It gets the entry as `[name, value]` and
|
|
319
|
+
the built-in transform. It must return the path and the value for the result.
|
|
320
|
+
Call `defaultTransform(entry)` to keep `+`, `&` and `-` working:
|
|
273
321
|
|
|
274
322
|
```ts
|
|
275
323
|
const formData = new FormData()
|
|
276
|
-
formData.append('a', '
|
|
277
|
-
formData.append('b', '
|
|
324
|
+
formData.append('a', 'x')
|
|
325
|
+
formData.append('+b', '1')
|
|
278
326
|
parseFormData(formData, {
|
|
279
|
-
transformEntry: (
|
|
327
|
+
transformEntry: (entry, defaultTransform) => {
|
|
328
|
+
const {path, value} = defaultTransform(entry)
|
|
280
329
|
return {
|
|
281
330
|
path,
|
|
282
|
-
value:
|
|
283
|
-
typeof value === 'string'
|
|
284
|
-
? value.toUpperCase()
|
|
285
|
-
: defaultTransform(value),
|
|
331
|
+
value: typeof value === 'string' ? value.toUpperCase() : value,
|
|
286
332
|
}
|
|
287
333
|
},
|
|
288
334
|
})
|
|
289
|
-
//
|
|
335
|
+
// {a: 'X', b: 1}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Errors
|
|
339
|
+
|
|
340
|
+
`parseFormData` stops at the first error. All error classes are exported:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import {
|
|
344
|
+
DepthLimitError,
|
|
345
|
+
DuplicateKeyError,
|
|
346
|
+
ForbiddenKeyError,
|
|
347
|
+
InvalidPathError,
|
|
348
|
+
MixedArrayError,
|
|
349
|
+
} from 'parse-nested-form-data'
|
|
290
350
|
```
|
|
291
351
|
|
|
352
|
+
| Error | Thrown when | Example names |
|
|
353
|
+
| ------------------- | ------------------------------------------------------------------------ | ---------------------------------------- |
|
|
354
|
+
| `DuplicateKeyError` | Two entries set the same path, or use a path part in two different ways. | `a` and `a`; `a` and `a.b`; `a.b`, `a[]` |
|
|
355
|
+
| `MixedArrayError` | One array uses both `[]` and `[0]`. | `a[]` and `a[0]` |
|
|
356
|
+
| `ForbiddenKeyError` | A path part is `__proto__`, `constructor` or `prototype`. | `__proto__.x`; `a[constructor]` |
|
|
357
|
+
| `DepthLimitError` | A path has more path parts than the `depth` option allows. | `a.b.c` with `depth: 2` |
|
|
358
|
+
| `InvalidPathError` | An array position is larger than `4294967294`. | `a[4294967295]` |
|
|
359
|
+
| `TypeError` | The `depth` option isn't a positive integer or `Infinity`. | `depth: 0` |
|
|
360
|
+
|
|
361
|
+
Each library error has a `key` property. It's the path up to the path part that
|
|
362
|
+
caused the error. For example, `a.b` = `y` and then `a.b[0]` = `x` throw
|
|
363
|
+
`DuplicateKeyError('a.b')`. In `ForbiddenKeyError`, `DepthLimitError` and
|
|
364
|
+
`InvalidPathError`, a `key` longer than 100 characters is cut to 100 characters
|
|
365
|
+
plus `...`.
|
|
366
|
+
|
|
367
|
+
`key` is the path after `transformEntry`, not the field name. So it doesn't have
|
|
368
|
+
the `+`, `&` or `-`: `+a.b.c` with `depth: 2` throws `DepthLimitError('a.b.c')`.
|
|
369
|
+
A custom `transformEntry` can change the path completely. If you use `key` to
|
|
370
|
+
find the form field, undo your own transform first.
|
|
371
|
+
|
|
372
|
+
The entries are parsed in order, so the error of an earlier entry comes first.
|
|
373
|
+
In one entry, the depth is checked first: a path that's too deep throws
|
|
374
|
+
`DepthLimitError`, even if it has `__proto__` in it.
|
|
375
|
+
|
|
376
|
+
```ts
|
|
377
|
+
try {
|
|
378
|
+
parseFormData(formData)
|
|
379
|
+
} catch (error) {
|
|
380
|
+
if (error instanceof DuplicateKeyError) {
|
|
381
|
+
return badRequest(`The field ${error.key} is used more than once`)
|
|
382
|
+
}
|
|
383
|
+
throw error
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Security
|
|
388
|
+
|
|
389
|
+
`__proto__`, `constructor` and `prototype` could change `Object.prototype`. So a
|
|
390
|
+
path part with one of these names throws a `ForbiddenKeyError`, in every
|
|
391
|
+
position, with or without brackets:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
const formData = new FormData()
|
|
395
|
+
formData.append('user.__proto__.isAdmin', 'true')
|
|
396
|
+
parseFormData(formData)
|
|
397
|
+
// throws ForbiddenKeyError('user.__proto__')
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Other names that exist on `Object.prototype` at parse time (`toString`,
|
|
401
|
+
`hasOwnProperty`, …, and names that a newer runtime or other code adds) would
|
|
402
|
+
hide a built-in method of the result. So the parser removes every entry with
|
|
403
|
+
such a name in its path. It parses the entry like any other entry first, so the
|
|
404
|
+
entry causes the same errors as any other name. Because of this, an error
|
|
405
|
+
doesn't tell the client which properties exist on the server's
|
|
406
|
+
`Object.prototype`.
|
|
407
|
+
|
|
408
|
+
```ts
|
|
409
|
+
const formData = new FormData()
|
|
410
|
+
formData.append('user.name', 'Ada')
|
|
411
|
+
formData.append('user.toString', 'x')
|
|
412
|
+
formData.append('settings.valueOf', 'x')
|
|
413
|
+
parseFormData(formData)
|
|
414
|
+
// {user: {name: 'Ada'}}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
For input from untrusted clients, also set the [`depth`](#depth) option.
|
|
418
|
+
|
|
419
|
+
## Supported runtimes
|
|
420
|
+
|
|
421
|
+
The library uses only ES2020 and no runtime APIs. It works in:
|
|
422
|
+
|
|
423
|
+
- browsers
|
|
424
|
+
- Node.js (CI tests versions 22, 24 and 26)
|
|
425
|
+
- Deno
|
|
426
|
+
- Bun
|
|
427
|
+
- Cloudflare Workers
|
|
428
|
+
- Vercel (Edge and Node.js runtimes)
|
|
429
|
+
|
|
430
|
+
The package has ESM, CommonJS and UMD builds. In a `<script>` tag, the UMD build
|
|
431
|
+
sets the global `parseNestedFormData`.
|
|
432
|
+
|
|
292
433
|
## Issues
|
|
293
434
|
|
|
294
|
-
_Looking to contribute? Look for the [Good First Issue][good-first-issue]
|
|
295
|
-
|
|
435
|
+
_Looking to contribute? Look for the [Good First Issue][good-first-issue] label,
|
|
436
|
+
and see [CONTRIBUTING.md](CONTRIBUTING.md) for how to set up the project._
|
|
296
437
|
|
|
297
438
|
### 🐛 Bugs
|
|
298
439
|
|
|
299
|
-
Please
|
|
440
|
+
Please open an issue for bugs, missing documentation or unexpected behavior.
|
|
300
441
|
|
|
301
442
|
[**See Bugs**][bugs]
|
|
302
443
|
|
|
303
444
|
### 💡 Feature Requests
|
|
304
445
|
|
|
305
|
-
Please
|
|
306
|
-
|
|
446
|
+
Please open an issue to suggest new features. Vote on feature requests with a
|
|
447
|
+
👍. This helps maintainers decide what to work on.
|
|
307
448
|
|
|
308
449
|
[**See Feature Requests**][requests]
|
|
309
450
|
|
|
@@ -337,22 +478,22 @@ Special thanks to Kent C. Dodds and his [match-sorter][match-sorter] package
|
|
|
337
478
|
where most of the setup is from.
|
|
338
479
|
|
|
339
480
|
<!-- prettier-ignore-start -->
|
|
340
|
-
[
|
|
341
|
-
[node]: https://nodejs.org
|
|
342
|
-
[build-badge]: https://img.shields.io/github/workflow/status/milamer/parse-nested-form-data/validate?logo=github&style=flat-square
|
|
481
|
+
[build-badge]: https://img.shields.io/github/actions/workflow/status/milamer/parse-nested-form-data/validate.yml?branch=main&logo=github&style=flat-square
|
|
343
482
|
[build]: https://github.com/milamer/parse-nested-form-data/actions?query=workflow%3Avalidate
|
|
344
483
|
[coverage-badge]: https://img.shields.io/codecov/c/github/milamer/parse-nested-form-data.svg?style=flat-square
|
|
345
484
|
[coverage]: https://codecov.io/github/milamer/parse-nested-form-data
|
|
346
485
|
[version-badge]: https://img.shields.io/npm/v/parse-nested-form-data.svg?style=flat-square
|
|
347
486
|
[package]: https://www.npmjs.com/package/parse-nested-form-data
|
|
487
|
+
[size-badge]: https://img.shields.io/badge/min%2Bbrotli-%3C%202%20kB-brightgreen?style=flat-square
|
|
488
|
+
[size]: https://github.com/milamer/parse-nested-form-data/blob/main/.size-limit.json
|
|
348
489
|
[downloads-badge]: https://img.shields.io/npm/dm/parse-nested-form-data.svg?style=flat-square
|
|
349
490
|
[npmtrends]: https://www.npmtrends.com/parse-nested-form-data
|
|
350
491
|
[license-badge]: https://img.shields.io/npm/l/parse-nested-form-data.svg?style=flat-square
|
|
351
|
-
[license]: https://github.com/milamer/parse-nested-form-data/blob/
|
|
492
|
+
[license]: https://github.com/milamer/parse-nested-form-data/blob/main/LICENSE
|
|
352
493
|
[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
|
|
353
494
|
[prs]: http://makeapullrequest.com
|
|
354
495
|
[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
|
|
355
|
-
[coc]: https://github.com/milamer/parse-nested-form-data/blob/
|
|
496
|
+
[coc]: https://github.com/milamer/parse-nested-form-data/blob/main/CODE_OF_CONDUCT.md
|
|
356
497
|
[emojis]: https://github.com/all-contributors/all-contributors#emoji-key
|
|
357
498
|
[all-contributors]: https://github.com/all-contributors/all-contributors
|
|
358
499
|
[all-contributors-badge]: https://img.shields.io/github/all-contributors/milamer/parse-nested-form-data?color=orange&style=flat-square
|