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/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * @author Christian Schurr <chris@schurr.dev>
6
6
  */
7
7
  /**
8
- * Thrown when a path is used multiple times or has missmatching path parts.
8
+ * Thrown when a path is used multiple times or has mismatching path parts.
9
9
  *
10
10
  * @example
11
11
  * ```ts
@@ -23,6 +23,7 @@
23
23
  * formData.append('a', 'c')
24
24
  * parseFormData(formData)
25
25
  * // throws DuplicateKeyError('a')
26
+ * ```
26
27
  *
27
28
  * @example
28
29
  * ```ts
@@ -35,8 +36,8 @@
35
36
  *
36
37
  */
37
38
  export declare class DuplicateKeyError extends Error {
38
- key: string;
39
- constructor(key: string);
39
+ key: string;
40
+ constructor(key: string);
40
41
  }
41
42
  /**
42
43
  * Thrown when an array is used at the same path with an order parameter and
@@ -61,14 +62,16 @@ export declare class DuplicateKeyError extends Error {
61
62
  * ```
62
63
  */
63
64
  export declare class MixedArrayError extends Error {
64
- key: string;
65
- constructor(key: string);
65
+ key: string;
66
+ constructor(key: string);
66
67
  }
67
68
  /**
68
- * Thrown when a path part would access or assign a property on
69
- * `Object.prototype` (`__proto__`, `constructor`, `prototype`). Rejected
70
- * regardless of whether pollution would actually occur, to keep input
71
- * unambiguous.
69
+ * Thrown when an object path part is `__proto__`, `constructor` or
70
+ * `prototype`, which could modify `Object.prototype`. The names are rejected
71
+ * at any position in the path. Entries with other names on `Object.prototype`
72
+ * (`toString`, ...) are removed instead (see `parseFormData`).
73
+ * `key` is the path up to the forbidden part, cut to 100 characters plus `...`
74
+ * when it's longer.
72
75
  *
73
76
  * @example
74
77
  * ```ts
@@ -79,12 +82,48 @@ export declare class MixedArrayError extends Error {
79
82
  * ```
80
83
  */
81
84
  export declare class ForbiddenKeyError extends Error {
82
- key: string;
83
- constructor(key: string);
85
+ key: string;
86
+ constructor(key: string);
84
87
  }
85
- type JsonObject = {
86
- [Key in string]?: JsonValue;
87
- };
88
+ /**
89
+ * Thrown when a path has more path parts than the `depth` option allows.
90
+ * `key` is the path up to the first path part over the limit, cut to
91
+ * 100 characters plus `...` when it's longer.
92
+ * Within one entry the depth is checked first, so a path that's too deep throws
93
+ * `DepthLimitError` even if it would also throw another error, like a
94
+ * `ForbiddenKeyError` for `__proto__`.
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * const formData = new FormData()
99
+ * formData.append('a.b[0].c', 'x')
100
+ * parseFormData(formData, {depth: 2})
101
+ * // throws DepthLimitError('a.b[0]')
102
+ * ```
103
+ */
104
+ export declare class DepthLimitError extends Error {
105
+ key: string;
106
+ constructor(key: string);
107
+ }
108
+ /**
109
+ * Thrown when an array index in a path is larger than `4294967294`, the
110
+ * largest index a JavaScript array can hold.
111
+ * `key` is the path up to that array part, cut to 100 characters plus `...`
112
+ * when it's longer.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * const formData = new FormData()
117
+ * formData.append('a[4294967295]', 'b')
118
+ * parseFormData(formData)
119
+ * // throws InvalidPathError('a[4294967295]')
120
+ * ```
121
+ */
122
+ export declare class InvalidPathError extends Error {
123
+ key: string;
124
+ constructor(key: string);
125
+ }
126
+ type JsonObject = { [Key in string]?: JsonValue; };
88
127
  type JsonArray = Array<JsonValue>;
89
128
  type JsonValue = string | number | JsonObject | JsonArray | boolean | null | File;
90
129
  type JsonLeafValue = Exclude<JsonValue, JsonArray | JsonObject>;
@@ -137,8 +176,8 @@ type JsonLeafValue = Exclude<JsonValue, JsonArray | JsonObject>;
137
176
  * @returns the path and the transformed value
138
177
  */
139
178
  declare function defaultTransform(entry: [path: string, value: string | File]): {
140
- path: string;
141
- value: JsonLeafValue;
179
+ path: string;
180
+ value: JsonLeafValue;
142
181
  };
143
182
  type DefaultTransform = typeof defaultTransform;
144
183
  /**
@@ -146,6 +185,20 @@ type DefaultTransform = typeof defaultTransform;
146
185
  * @param transformEntry - a function to transform the FormData entry into a path and a value
147
186
  * (default: `defaultTransform`)
148
187
  * @param removeEmptyString - skip empty values '' (default: `false`)
188
+ * @param depth - the maximum number of path parts in a path, counting every
189
+ * part (`a.b[0]` has 3); a positive integer or `Infinity` (default: `Infinity`).
190
+ * For untrusted input, `32` is recommended; 2.0 will lower the default.
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * const formData = new FormData()
195
+ * formData.append('a.b', 'x')
196
+ * parseFormData(formData, {depth: 2})
197
+ * // => {a: {b: 'x'}}
198
+ * formData.append('a.c.d', 'y')
199
+ * parseFormData(formData, {depth: 2})
200
+ * // throws DepthLimitError('a.c.d')
201
+ * ```
149
202
  *
150
203
  * @example
151
204
  * ```ts
@@ -162,13 +215,11 @@ type DefaultTransform = typeof defaultTransform;
162
215
  * formData.append('a', 'a')
163
216
  * formData.append('b', 'b')
164
217
  * parseFormData(formData, {
165
- * transformEntry: ([path, value], defaultTransform) => {
218
+ * transformEntry: (entry, defaultTransform) => {
219
+ * const {path, value} = defaultTransform(entry)
166
220
  * return {
167
221
  * path,
168
- * value:
169
- * typeof value === 'string'
170
- * ? value.toUpperCase()
171
- * : defaultTransform(value),
222
+ * value: typeof value === 'string' ? value.toUpperCase() : value,
172
223
  * }
173
224
  * },
174
225
  * })
@@ -176,11 +227,12 @@ type DefaultTransform = typeof defaultTransform;
176
227
  * ```
177
228
  */
178
229
  export type ParseFormDataOptions = {
179
- removeEmptyString?: boolean;
180
- transformEntry?: (entry: [path: string, value: string | File], defaultTransform: DefaultTransform) => {
181
- path: string;
182
- value: JsonLeafValue;
183
- };
230
+ removeEmptyString?: boolean;
231
+ depth?: number;
232
+ transformEntry?: (entry: [path: string, value: string | File], defaultTransform: DefaultTransform) => {
233
+ path: string;
234
+ value: JsonLeafValue;
235
+ };
184
236
  };
185
237
  /**
186
238
  *
@@ -195,9 +247,9 @@ export type ParseFormDataOptions = {
195
247
  * - `&` => parse to `Boolean`
196
248
  *
197
249
  * - `.` between path parts => nest into `objects`
250
+ * - `[key]` after path part => nest into `objects`, same as `.key`; the text
251
+ * in brackets is the key as it is (`a[b.c]` => key `b.c`)
198
252
  * - `[\d*]` after path part => push to array in order `\d` or push to end if `[]`
199
- *
200
-
201
253
  *
202
254
  * @example
203
255
  * ```ts
@@ -206,7 +258,7 @@ export type ParseFormDataOptions = {
206
258
  * formData.append('&b', 'true')
207
259
  * formData.append('-c', 'null')
208
260
  * formData.append('d', 'foo')
209
- * parseFormData(formData, defaultTransform)
261
+ * parseFormData(formData)
210
262
  * // => {a: 1, b: true, c: null, d: 'foo'}
211
263
  * ```
212
264
  *
@@ -221,6 +273,15 @@ export type ParseFormDataOptions = {
221
273
  * @example
222
274
  * ```ts
223
275
  * const formData = new FormData()
276
+ * formData.append('a[b]', 'foo')
277
+ * formData.append('a.c', 'bar')
278
+ * parseFormData(formData)
279
+ * // => {a: {b: 'foo', c: 'bar'}}
280
+ * ```
281
+ *
282
+ * @example
283
+ * ```ts
284
+ * const formData = new FormData()
224
285
  * formData.append('a[0]', 'foo')
225
286
  * formData.append('a[1]', 'bar')
226
287
  * parseFormData(formData)
@@ -240,7 +301,12 @@ export type ParseFormDataOptions = {
240
301
  * ```ts
241
302
  * const formData = new FormData()
242
303
  * formData.append('a[0]', 'foo')
243
- * parseFormData(formData, {transformEntry: (path, value) => {path, value: value + 'bar'}})
304
+ * parseFormData(formData, {
305
+ * transformEntry: ([path, value]) => ({
306
+ * path,
307
+ * value: typeof value === 'string' ? value + 'bar' : value,
308
+ * }),
309
+ * })
244
310
  * // => {a: ['foobar']}
245
311
  * ```
246
312
  *
@@ -254,20 +320,49 @@ export type ParseFormDataOptions = {
254
320
  * ```
255
321
  *
256
322
  * @param {Iterable<[string, string | File]>} formData - an iterator of an [`path`, `value`] tuple
257
- * - `path` := `^(\+|\-|\&)?([^\.]+?(\[\d*\])*)(\.[^\.]+?(\[\d*\])*)*$` (e.g. `+a[][1].b`)
258
- * - `value` := `string` or `File`
323
+ * - `path` := an optional prefix (`+`, `-`, `&`), then path parts: keys after
324
+ * `.` or in brackets (`[key]`), and array parts `[]` or `[\d+]` (e.g.
325
+ * `+a[][1].b[c]`)
326
+ * - `value` := `string` or `File` (native or polyfilled `File` values are kept
327
+ * as they are)
259
328
  * @param {ParseFormDataOptions} options - options for parsing the form data
260
329
  * - `transformEntry` - a function to transform the path and the value before
261
330
  * inserting the value at the path in the resulting object (default: `defaultTransform`)
262
331
  * - `removeEmptyString` - if `true` removes all entries where the value is an empty string
332
+ * - `depth` - the maximum number of path parts in a path, counting every part
333
+ * (default: `Infinity`)
263
334
  * @returns {JsonObject} the parsed JSON object
335
+ * @throws `TypeError` if `depth` isn't a positive integer or `Infinity`
336
+ * @throws `DepthLimitError` if a path has more path parts than `depth`
337
+ * - e.g. `a.b[0]` with `depth: 2` => `DepthLimitError('a.b[0]')`
264
338
  * @throws `DuplicateKeyError` if
265
- * - a path part is an object and the path part is already defined as an object
266
- * - a path part is an array and the path part is already defined as an array
339
+ * - two entries set a value at the same path
340
+ * - e.g. `a` and `a`, `a[0]` and `a[0]`, `a.b` and `a[b]`
341
+ * - an entry uses a path part as an object, array or value and another entry
342
+ * uses it differently
343
+ * - e.g. `a` and `a.b`, `a.b` and `a[]`, `a[]` and `a.b`
344
+ * - the path starts with an array part
345
+ * - e.g. `[0]` or `[]`
267
346
  * @throws `MixedArrayError` if at a specific path part an unordered array is
268
347
  * defined and at a later path part an ordered array is defined or vice versa
269
348
  * - e.g. `a[0]` and `a[]`
270
349
  * - e.g. `a[]` and `a[0]`
350
+ * @throws `ForbiddenKeyError` if an object path part is `__proto__`,
351
+ * `constructor` or `prototype`
352
+ * - e.g. `__proto__.polluted`
353
+ * @throws `InvalidPathError` if an array index is larger than `4294967294`
354
+ * - e.g. `a[4294967295]`
355
+ *
356
+ * Every error's `key` is the path after `transformEntry`, not the field name
357
+ * (e.g. `+a.b.c` with `depth: 2` => `DepthLimitError('a.b.c')`).
358
+ *
359
+ * An entry with any other object path part named like a property on
360
+ * `Object.prototype` (`toString`, `hasOwnProperty`, ...) is removed from the
361
+ * result. It's parsed like any other entry first, so it causes the same errors
362
+ * as an unused name: an error doesn't tell the client which properties exist
363
+ * on the server's `Object.prototype`.
364
+ * - e.g. `user.toString` => `{}`
365
+ * - e.g. `user[toString]` => `{}`
271
366
  */
272
- export declare function parseFormData(formData: Iterable<[string, string | File]>, { removeEmptyString, transformEntry, }?: ParseFormDataOptions): JsonObject;
273
- export {};
367
+ export declare function parseFormData(formData: Iterable<[string, string | File]>, { removeEmptyString, transformEntry, depth }?: ParseFormDataOptions): JsonObject;
368
+ export {};