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/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,12 +62,68 @@ 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
- type JsonObject = {
68
- [Key in string]?: JsonValue;
69
- };
68
+ /**
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.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * const formData = new FormData()
79
+ * formData.append('__proto__.polluted', 'yes')
80
+ * parseFormData(formData)
81
+ * // throws ForbiddenKeyError('__proto__')
82
+ * ```
83
+ */
84
+ export declare class ForbiddenKeyError extends Error {
85
+ key: string;
86
+ constructor(key: string);
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; };
70
127
  type JsonArray = Array<JsonValue>;
71
128
  type JsonValue = string | number | JsonObject | JsonArray | boolean | null | File;
72
129
  type JsonLeafValue = Exclude<JsonValue, JsonArray | JsonObject>;
@@ -119,8 +176,8 @@ type JsonLeafValue = Exclude<JsonValue, JsonArray | JsonObject>;
119
176
  * @returns the path and the transformed value
120
177
  */
121
178
  declare function defaultTransform(entry: [path: string, value: string | File]): {
122
- path: string;
123
- value: JsonLeafValue;
179
+ path: string;
180
+ value: JsonLeafValue;
124
181
  };
125
182
  type DefaultTransform = typeof defaultTransform;
126
183
  /**
@@ -128,6 +185,20 @@ type DefaultTransform = typeof defaultTransform;
128
185
  * @param transformEntry - a function to transform the FormData entry into a path and a value
129
186
  * (default: `defaultTransform`)
130
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
+ * ```
131
202
  *
132
203
  * @example
133
204
  * ```ts
@@ -144,13 +215,11 @@ type DefaultTransform = typeof defaultTransform;
144
215
  * formData.append('a', 'a')
145
216
  * formData.append('b', 'b')
146
217
  * parseFormData(formData, {
147
- * transformEntry: ([path, value], defaultTransform) => {
218
+ * transformEntry: (entry, defaultTransform) => {
219
+ * const {path, value} = defaultTransform(entry)
148
220
  * return {
149
221
  * path,
150
- * value:
151
- * typeof value === 'string'
152
- * ? value.toUpperCase()
153
- * : defaultTransform(value),
222
+ * value: typeof value === 'string' ? value.toUpperCase() : value,
154
223
  * }
155
224
  * },
156
225
  * })
@@ -158,11 +227,12 @@ type DefaultTransform = typeof defaultTransform;
158
227
  * ```
159
228
  */
160
229
  export type ParseFormDataOptions = {
161
- removeEmptyString?: boolean;
162
- transformEntry?: (entry: [path: string, value: string | File], defaultTransform: DefaultTransform) => {
163
- path: string;
164
- value: JsonLeafValue;
165
- };
230
+ removeEmptyString?: boolean;
231
+ depth?: number;
232
+ transformEntry?: (entry: [path: string, value: string | File], defaultTransform: DefaultTransform) => {
233
+ path: string;
234
+ value: JsonLeafValue;
235
+ };
166
236
  };
167
237
  /**
168
238
  *
@@ -177,9 +247,9 @@ export type ParseFormDataOptions = {
177
247
  * - `&` => parse to `Boolean`
178
248
  *
179
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`)
180
252
  * - `[\d*]` after path part => push to array in order `\d` or push to end if `[]`
181
- *
182
-
183
253
  *
184
254
  * @example
185
255
  * ```ts
@@ -188,7 +258,7 @@ export type ParseFormDataOptions = {
188
258
  * formData.append('&b', 'true')
189
259
  * formData.append('-c', 'null')
190
260
  * formData.append('d', 'foo')
191
- * parseFormData(formData, defaultTransform)
261
+ * parseFormData(formData)
192
262
  * // => {a: 1, b: true, c: null, d: 'foo'}
193
263
  * ```
194
264
  *
@@ -203,6 +273,15 @@ export type ParseFormDataOptions = {
203
273
  * @example
204
274
  * ```ts
205
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()
206
285
  * formData.append('a[0]', 'foo')
207
286
  * formData.append('a[1]', 'bar')
208
287
  * parseFormData(formData)
@@ -222,7 +301,12 @@ export type ParseFormDataOptions = {
222
301
  * ```ts
223
302
  * const formData = new FormData()
224
303
  * formData.append('a[0]', 'foo')
225
- * 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
+ * })
226
310
  * // => {a: ['foobar']}
227
311
  * ```
228
312
  *
@@ -236,20 +320,49 @@ export type ParseFormDataOptions = {
236
320
  * ```
237
321
  *
238
322
  * @param {Iterable<[string, string | File]>} formData - an iterator of an [`path`, `value`] tuple
239
- * - `path` := `^(\+|\-|\&)?([^\.]+?(\[\d*\])*)(\.[^\.]+?(\[\d*\])*)*$` (e.g. `+a[][1].b`)
240
- * - `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)
241
328
  * @param {ParseFormDataOptions} options - options for parsing the form data
242
329
  * - `transformEntry` - a function to transform the path and the value before
243
330
  * inserting the value at the path in the resulting object (default: `defaultTransform`)
244
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`)
245
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]')`
246
338
  * @throws `DuplicateKeyError` if
247
- * - a path part is an object and the path part is already defined as an object
248
- * - 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 `[]`
249
346
  * @throws `MixedArrayError` if at a specific path part an unordered array is
250
347
  * defined and at a later path part an ordered array is defined or vice versa
251
348
  * - e.g. `a[0]` and `a[]`
252
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]` => `{}`
253
366
  */
254
- export declare function parseFormData(formData: Iterable<[string, string | File]>, { removeEmptyString, transformEntry, }?: ParseFormDataOptions): JsonObject;
255
- export {};
367
+ export declare function parseFormData(formData: Iterable<[string, string | File]>, { removeEmptyString, transformEntry, depth }?: ParseFormDataOptions): JsonObject;
368
+ export {};