parse-nested-form-data 1.0.1 → 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 +309 -174
- package/dist/index.d.ts +131 -36
- package/dist/parse-nested-form-data.cjs.js +576 -445
- package/dist/parse-nested-form-data.esm.js +571 -442
- package/dist/parse-nested-form-data.umd.js +607 -475
- 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 -54
- 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,263 +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
|
|
66
84
|
|
|
67
85
|
```ts
|
|
68
86
|
import {parseFormData} from 'parse-nested-form-data'
|
|
87
|
+
|
|
88
|
+
const data = parseFormData(await request.formData())
|
|
69
89
|
```
|
|
70
90
|
|
|
71
|
-
|
|
91
|
+
`parseFormData` reads each entry, puts its value at the place its name tells,
|
|
92
|
+
and returns a new object. The input stays unchanged.
|
|
72
93
|
|
|
73
|
-
|
|
74
|
-
values. The name prop is a string that can contain the following characters:
|
|
94
|
+
## Field names
|
|
75
95
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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}` |
|
|
82
105
|
|
|
83
|
-
|
|
106
|
+
You can combine them, for example `+orders[0].items[].price`.
|
|
84
107
|
|
|
85
|
-
|
|
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.
|
|
86
111
|
|
|
87
|
-
|
|
112
|
+
### Numbers
|
|
88
113
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
- `on`
|
|
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`.
|
|
92
116
|
|
|
93
117
|
```ts
|
|
94
118
|
const formData = new FormData()
|
|
95
|
-
formData.append('
|
|
96
|
-
formData.append('
|
|
97
|
-
formData.append('
|
|
98
|
-
formData.append('&isFalse1', '0')
|
|
99
|
-
formData.append('&isTrue2', 'on')
|
|
100
|
-
formData.append('&isFalse2', 'off')
|
|
119
|
+
formData.append('+a', '1')
|
|
120
|
+
formData.append('+b', '1.5')
|
|
121
|
+
formData.append('+c', 'x')
|
|
101
122
|
parseFormData(formData)
|
|
102
|
-
// {
|
|
103
|
-
// isTrue: true,
|
|
104
|
-
// isFalse: false,
|
|
105
|
-
// isTrue1: true,
|
|
106
|
-
// isFalse1: false,
|
|
107
|
-
// isTrue2: true,
|
|
108
|
-
// isFalse2: false,
|
|
109
|
-
// }
|
|
123
|
+
// {a: 1, b: 1.5, c: NaN}
|
|
110
124
|
```
|
|
111
125
|
|
|
112
|
-
###
|
|
126
|
+
### Booleans
|
|
113
127
|
|
|
114
|
-
|
|
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`.
|
|
115
130
|
|
|
116
131
|
```ts
|
|
117
132
|
const formData = new FormData()
|
|
118
|
-
formData.append('
|
|
119
|
-
formData.append('
|
|
120
|
-
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')
|
|
121
139
|
parseFormData(formData)
|
|
122
|
-
// {
|
|
123
|
-
// number: 1,
|
|
124
|
-
// number2: 1.1,
|
|
125
|
-
// number3: NaN,
|
|
126
|
-
// }
|
|
140
|
+
// {a: true, b: true, c: true, d: false, e: false, f: false}
|
|
127
141
|
```
|
|
128
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
|
+
|
|
129
146
|
### Null
|
|
130
147
|
|
|
131
|
-
|
|
148
|
+
A name that starts with `-` changes the value to `null`, whatever the value is.
|
|
132
149
|
|
|
133
150
|
```ts
|
|
134
151
|
const formData = new FormData()
|
|
135
|
-
formData.append('-
|
|
136
|
-
formData.append('-ignored', 'ignored')
|
|
152
|
+
formData.append('-a', 'x')
|
|
137
153
|
parseFormData(formData)
|
|
138
|
-
// {
|
|
139
|
-
// null: null,
|
|
140
|
-
// ignored: null,
|
|
141
|
-
// }
|
|
154
|
+
// {a: null}
|
|
142
155
|
```
|
|
143
156
|
|
|
144
|
-
###
|
|
157
|
+
### Arrays
|
|
145
158
|
|
|
146
|
-
|
|
147
|
-
If the path of the name prop ends with `[$order]` the value will be set at the
|
|
148
|
-
index of the order. Cannot be mixed with `[]`.
|
|
159
|
+
`[]` adds the value to the end of the array, in the order of the entries:
|
|
149
160
|
|
|
150
161
|
```ts
|
|
151
162
|
const formData = new FormData()
|
|
152
|
-
formData.append('
|
|
153
|
-
formData.append('
|
|
154
|
-
formData.append('array[2]', '3')
|
|
155
|
-
formData.append('array[3]', '4')
|
|
163
|
+
formData.append('a[]', '1')
|
|
164
|
+
formData.append('a[]', '2')
|
|
156
165
|
parseFormData(formData)
|
|
157
|
-
// {
|
|
158
|
-
// array: ['1', '2', '3', '4'],
|
|
159
|
-
// }
|
|
166
|
+
// {a: ['1', '2']}
|
|
160
167
|
```
|
|
161
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
|
+
|
|
162
172
|
```ts
|
|
163
173
|
const formData = new FormData()
|
|
164
|
-
formData.append('
|
|
165
|
-
formData.append('
|
|
166
|
-
formData.append('array[]', '3')
|
|
174
|
+
formData.append('a[5]', 'second')
|
|
175
|
+
formData.append('a[1]', 'first')
|
|
167
176
|
parseFormData(formData)
|
|
168
|
-
// {
|
|
169
|
-
// array: ['1', '2', '3'],
|
|
170
|
-
// }
|
|
177
|
+
// {a: ['first', 'second']}
|
|
171
178
|
```
|
|
172
179
|
|
|
173
|
-
|
|
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:
|
|
174
186
|
|
|
175
187
|
```ts
|
|
176
188
|
const formData = new FormData()
|
|
177
|
-
formData.append('
|
|
178
|
-
formData.append('
|
|
179
|
-
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')
|
|
180
194
|
parseFormData(formData)
|
|
181
|
-
// {
|
|
182
|
-
// array: [{a: '1'}, {a: '2'}, {a: '3'}],
|
|
183
|
-
// }
|
|
195
|
+
// {a: [{b: '1'}, {b: '2'}], c: [['3', '4'], ['5']]}
|
|
184
196
|
```
|
|
185
197
|
|
|
186
|
-
|
|
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:
|
|
187
203
|
|
|
188
204
|
```ts
|
|
189
205
|
const formData = new FormData()
|
|
190
|
-
formData.append('
|
|
191
|
-
formData.append('
|
|
192
|
-
formData.append('
|
|
193
|
-
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')
|
|
194
209
|
parseFormData(formData)
|
|
195
|
-
// {
|
|
196
|
-
// array: [['1', '2'], ['3', '4']],
|
|
197
|
-
// }
|
|
210
|
+
// {a: {b: '1', c: [{d: '2'}, {d: '3'}]}}
|
|
198
211
|
```
|
|
199
212
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
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:
|
|
203
215
|
|
|
204
216
|
```ts
|
|
205
217
|
const formData = new FormData()
|
|
206
|
-
formData.append('
|
|
207
|
-
formData.append('
|
|
218
|
+
formData.append('a[b.c]', '1')
|
|
219
|
+
formData.append('a[d[e]]', '2')
|
|
208
220
|
parseFormData(formData)
|
|
209
|
-
// {
|
|
210
|
-
// object: {
|
|
211
|
-
// a: '1',
|
|
212
|
-
// b: '2',
|
|
213
|
-
// },
|
|
214
|
-
// }
|
|
221
|
+
// {a: {'b.c': '1', 'd[e]': '2'}}
|
|
215
222
|
```
|
|
216
223
|
|
|
217
|
-
|
|
224
|
+
Brackets in the wrong place don't cause an error:
|
|
218
225
|
|
|
219
|
-
|
|
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`.
|
|
232
|
+
|
|
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:
|
|
220
237
|
|
|
221
238
|
```ts
|
|
222
239
|
const formData = new FormData()
|
|
223
|
-
formData.append('
|
|
224
|
-
formData
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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')
|
|
229
247
|
```
|
|
230
248
|
|
|
231
|
-
##
|
|
249
|
+
## API
|
|
250
|
+
|
|
251
|
+
### parseFormData(formData, options?)
|
|
252
|
+
|
|
253
|
+
Returns a new object with the parsed values.
|
|
254
|
+
|
|
255
|
+
#### formData
|
|
256
|
+
|
|
257
|
+
Type: `Iterable<[string, string | File]>`
|
|
232
258
|
|
|
233
|
-
|
|
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 (`''`).
|
|
234
272
|
|
|
235
273
|
```ts
|
|
236
274
|
const formData = new FormData()
|
|
237
|
-
formData.append('a
|
|
238
|
-
formData.append('
|
|
239
|
-
formData
|
|
240
|
-
|
|
241
|
-
formData.append('+b[100]', '5')
|
|
242
|
-
parseFormData(formData)
|
|
243
|
-
// {
|
|
244
|
-
// a: [
|
|
245
|
-
// {
|
|
246
|
-
// b: {
|
|
247
|
-
// c: '1',
|
|
248
|
-
// d: '2',
|
|
249
|
-
// },
|
|
250
|
-
// },
|
|
251
|
-
// '3',
|
|
252
|
-
// ],
|
|
253
|
-
// b: ['4', 5],
|
|
254
|
-
// }
|
|
275
|
+
formData.append('a', '')
|
|
276
|
+
formData.append('b', 'x')
|
|
277
|
+
parseFormData(formData, {removeEmptyString: true})
|
|
278
|
+
// {b: 'x'}
|
|
255
279
|
```
|
|
256
280
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
The second argument is an option object with following properties:
|
|
260
|
-
|
|
261
|
-
### removeEmptyString: `[boolean]`
|
|
281
|
+
##### depth
|
|
262
282
|
|
|
263
|
-
|
|
283
|
+
Type: `number`\
|
|
284
|
+
Default: `Infinity`
|
|
264
285
|
|
|
265
|
-
|
|
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`.
|
|
266
290
|
|
|
267
291
|
```ts
|
|
268
292
|
const formData = new FormData()
|
|
269
|
-
formData.append('
|
|
270
|
-
formData
|
|
271
|
-
|
|
272
|
-
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'}}}
|
|
273
296
|
|
|
274
|
-
|
|
275
|
-
|
|
297
|
+
formData.append('user.address.geo.lat', '52.5')
|
|
298
|
+
parseFormData(formData, {depth: 3})
|
|
299
|
+
// throws DepthLimitError('user.address.geo.lat')
|
|
276
300
|
```
|
|
277
301
|
|
|
278
|
-
|
|
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:
|
|
279
321
|
|
|
280
322
|
```ts
|
|
281
323
|
const formData = new FormData()
|
|
282
|
-
formData.append('a', '
|
|
283
|
-
formData.append('b', '
|
|
324
|
+
formData.append('a', 'x')
|
|
325
|
+
formData.append('+b', '1')
|
|
284
326
|
parseFormData(formData, {
|
|
285
|
-
transformEntry: (
|
|
327
|
+
transformEntry: (entry, defaultTransform) => {
|
|
328
|
+
const {path, value} = defaultTransform(entry)
|
|
286
329
|
return {
|
|
287
330
|
path,
|
|
288
|
-
value:
|
|
289
|
-
typeof value === 'string'
|
|
290
|
-
? value.toUpperCase()
|
|
291
|
-
: defaultTransform(value),
|
|
331
|
+
value: typeof value === 'string' ? value.toUpperCase() : value,
|
|
292
332
|
}
|
|
293
333
|
},
|
|
294
334
|
})
|
|
295
|
-
//
|
|
335
|
+
// {a: 'X', b: 1}
|
|
296
336
|
```
|
|
297
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'
|
|
350
|
+
```
|
|
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
|
+
|
|
298
433
|
## Issues
|
|
299
434
|
|
|
300
|
-
_Looking to contribute? Look for the [Good First Issue][good-first-issue]
|
|
301
|
-
|
|
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._
|
|
302
437
|
|
|
303
438
|
### 🐛 Bugs
|
|
304
439
|
|
|
305
|
-
Please
|
|
440
|
+
Please open an issue for bugs, missing documentation or unexpected behavior.
|
|
306
441
|
|
|
307
442
|
[**See Bugs**][bugs]
|
|
308
443
|
|
|
309
444
|
### 💡 Feature Requests
|
|
310
445
|
|
|
311
|
-
Please
|
|
312
|
-
|
|
446
|
+
Please open an issue to suggest new features. Vote on feature requests with a
|
|
447
|
+
👍. This helps maintainers decide what to work on.
|
|
313
448
|
|
|
314
449
|
[**See Feature Requests**][requests]
|
|
315
450
|
|
|
@@ -343,22 +478,22 @@ Special thanks to Kent C. Dodds and his [match-sorter][match-sorter] package
|
|
|
343
478
|
where most of the setup is from.
|
|
344
479
|
|
|
345
480
|
<!-- prettier-ignore-start -->
|
|
346
|
-
[npm]: https://www.npmjs.com
|
|
347
|
-
[node]: https://nodejs.org
|
|
348
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
|
|
349
482
|
[build]: https://github.com/milamer/parse-nested-form-data/actions?query=workflow%3Avalidate
|
|
350
483
|
[coverage-badge]: https://img.shields.io/codecov/c/github/milamer/parse-nested-form-data.svg?style=flat-square
|
|
351
484
|
[coverage]: https://codecov.io/github/milamer/parse-nested-form-data
|
|
352
485
|
[version-badge]: https://img.shields.io/npm/v/parse-nested-form-data.svg?style=flat-square
|
|
353
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
|
|
354
489
|
[downloads-badge]: https://img.shields.io/npm/dm/parse-nested-form-data.svg?style=flat-square
|
|
355
490
|
[npmtrends]: https://www.npmtrends.com/parse-nested-form-data
|
|
356
491
|
[license-badge]: https://img.shields.io/npm/l/parse-nested-form-data.svg?style=flat-square
|
|
357
|
-
[license]: https://github.com/milamer/parse-nested-form-data/blob/
|
|
492
|
+
[license]: https://github.com/milamer/parse-nested-form-data/blob/main/LICENSE
|
|
358
493
|
[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
|
|
359
494
|
[prs]: http://makeapullrequest.com
|
|
360
495
|
[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square
|
|
361
|
-
[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
|
|
362
497
|
[emojis]: https://github.com/all-contributors/all-contributors#emoji-key
|
|
363
498
|
[all-contributors]: https://github.com/all-contributors/all-contributors
|
|
364
499
|
[all-contributors-badge]: https://img.shields.io/github/all-contributors/milamer/parse-nested-form-data?color=orange&style=flat-square
|