parse-nested-form-data 1.0.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.
@@ -0,0 +1,398 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@remix-run/web-file')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', '@remix-run/web-file'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.parseNestedFormData = {}, global.RemixRunWebFile));
5
+ })(this, (function (exports, webFile) { 'use strict';
6
+
7
+ /**
8
+ * @name parse-nested-form-data
9
+ * @license MIT license.
10
+ * @copyright (c) 2022 Christian Schurr
11
+ * @author Christian Schurr <chris@schurr.dev>
12
+ */
13
+
14
+ /**
15
+ * Thrown when a path is used multiple times or has missmatching path parts.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const formData = new FormData()
20
+ * formData.append('a[0]', 'b')
21
+ * formData.append('a[0]', 'c')
22
+ * parseFormData(formData)
23
+ * // throws DuplicateKeyError('a[0]')
24
+ * ```
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const formData = new FormData()
29
+ * formData.append('a', 'b')
30
+ * formData.append('a', 'c')
31
+ * parseFormData(formData)
32
+ * // throws DuplicateKeyError('a')
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const formData = new FormData()
37
+ * formData.append('a', 'b')
38
+ * formData.append('a[]', 'c')
39
+ * parseFormData(formData)
40
+ * // throws DuplicateKeyError('a[]')
41
+ * ```
42
+ *
43
+ */
44
+ class DuplicateKeyError extends Error {
45
+ constructor(key) {
46
+ super(`Duplicate key at path part ${key}`);
47
+ this.key = void 0;
48
+ this.key = key;
49
+ }
50
+ }
51
+ /**
52
+ * Thrown when an array is used at the same path with an order parameter and
53
+ * without an order parameter.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const formData = new FormData()
58
+ * formData.append('a[0]', 'a')
59
+ * formData.append('a[]', 'b')
60
+ * parseFormData(formData)
61
+ * // => throws `MixedArrayError(a[])`
62
+ * ```
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * const formData = new FormData()
67
+ * formData.append('a[]', 'a')
68
+ * formData.append('a[0]', 'b')
69
+ * parseFormData(formData)
70
+ * // => throws `MixedArrayError(a[0])`
71
+ * ```
72
+ */
73
+ class MixedArrayError extends Error {
74
+ constructor(key) {
75
+ super(`Mixed array at path part ${key}`);
76
+ this.key = void 0;
77
+ this.key = key;
78
+ }
79
+ }
80
+ function isJsonObject(val) {
81
+ return typeof val === 'object' && !Array.isArray(val) && val !== null && !(val instanceof webFile.File);
82
+ }
83
+
84
+ /**
85
+ * Default Transformer for `parseFormData`.
86
+ *
87
+ * Transforms a FormData Entry into a path and a `JsonLeafValue`.
88
+ *
89
+ * - `path` starts with `+` -> transform value to `number`
90
+ * - `path` starts with `&` -> transform value to `boolean`
91
+ * - `path` starts with `-` -> transform value to `null`
92
+ *
93
+ * @example
94
+ * ```ts
95
+ * const entry = ['a[0]', 'b']
96
+ * const result = defaultTransform(entry)
97
+ * // => {path: 'a[0]', value: 'b'}
98
+ * ```
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * const entry = ['+a[0]', '1']
103
+ * const result = defaultTransform(entry)
104
+ * // => {path: 'a[0]', value: 1}
105
+ * ```
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * const entry = ['&a[0]', 'true']
110
+ * const result = defaultTransform(entry)
111
+ * // => {path: 'a[0]', value: true}
112
+ * ```
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * const entry = ['-a[0]', 'null']
117
+ * const result = defaultTransform(entry)
118
+ * // => {path: 'a[0]', value: null}
119
+ * ```
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * const entry = ['a[0]', new File([''], 'file.txt')]
124
+ * const result = defaultTransform(entry)
125
+ * // => {path: 'a[0]', value: File}
126
+ * ```
127
+ *
128
+ *
129
+ * @param entry [path, value]: the FormData entry
130
+ * @returns the path and the transformed value
131
+ */
132
+ function defaultTransform(entry) {
133
+ let path = entry[0];
134
+ let value = entry[1];
135
+ if (path.startsWith('+')) {
136
+ path = path.slice(1);
137
+ value = Number(value);
138
+ } else if (path.startsWith('&')) {
139
+ path = path.slice(1);
140
+ value = value === 'on' || value === 'true' || Boolean(Number(value));
141
+ } else if (path.startsWith('-')) {
142
+ path = path.slice(1);
143
+ value = null;
144
+ }
145
+ return {
146
+ path,
147
+ value
148
+ };
149
+ }
150
+ /**
151
+ *
152
+ * Transforms a FormData path into an array of `PathPart`s.
153
+ *
154
+ * @param path - the path to extract the path parts from
155
+ * @returns {Array<PathPart>} the extracted path parts
156
+ *
157
+ * @example
158
+ * ```ts
159
+ * const path = 'a[0].b'
160
+ * const result = extractPathParts(path)
161
+ * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},
162
+ * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},
163
+ * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]
164
+ * ```
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const path = 'a.b'
169
+ * const result = extractPathParts(path)
170
+ * // => [{path: 'a', type: 'object, default: {}, pathToPart: 'a'},
171
+ * // {path: 'b', type: 'object', default: {}, pathToPart: 'a.b'}]
172
+ * ```
173
+ *
174
+ * @example
175
+ * ```ts
176
+ * const path = 'a[][0]'
177
+ * const result = extractPathParts(path)
178
+ * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},
179
+ * // {path: '', type: 'array', default: [], pathToPart: 'a[]'},
180
+ * // {path: '0', type: 'array', default: {}, pathToPart: 'a[][0]'}]
181
+ * ```
182
+ *
183
+ *
184
+ */
185
+ function extractPathParts(path) {
186
+ const re = /((?<array>\d*)\]|(?<pathPart>[^.[]+))(?<nextType>\[|\.|$)/g;
187
+ return Array.from(path.matchAll(re)).map(match => {
188
+ // self casted RegexExpMatchArray to custom
189
+ const typedMatch = match;
190
+ const {
191
+ array,
192
+ pathPart,
193
+ nextType
194
+ } = typedMatch.groups;
195
+ const type = array === undefined ? 'object' : 'array';
196
+ const nextDefault = nextType === '[' ? [] : {};
197
+ return {
198
+ path: array ?? pathPart,
199
+ type,
200
+ default: nextDefault,
201
+ pathToPart: path.slice(0, typedMatch.index + typedMatch[1].length)
202
+ };
203
+ });
204
+ }
205
+
206
+ /**
207
+ *
208
+ * Returns the value accessed via `pathPart` in the `currentPathObject`
209
+ * and a setter function to set the value in the `currentPathObject` via the
210
+ * provided `pathPart`.
211
+ *
212
+ * @param pathPart - the path part to get the setter and getter for
213
+ * @param currentPathObject - the object at the current path (before the path part)
214
+ * @param arraysWithOrder - a set of arrays that have an order
215
+ * @returns the setter and getter for the path part
216
+ *
217
+ * @example
218
+ * ```ts
219
+ * const pathPart = {path: 'a', type: 'object', default: {}, pathToPart: 'a'}
220
+ * const currentPathObject = {}
221
+ * const arraysWithOrder = new Set()
222
+ * const [value, setValue] = getSetterAndGetter(pathPart, currentPathObject, arraysWithOrder)
223
+ * setValue('b')
224
+ * // => currentPathObject = {a: 'b'}
225
+ * ```
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * const pathPart = {path: '0', type: 'array', default: [], pathToPart: 'a[0]'}
230
+ * const currentPathObject = {a: []}
231
+ * const arraysWithOrder = new Set()
232
+ * const [value, setValue] = getSetterAndGetter(pathPart, currentPathObject, arraysWithOrder)
233
+ * setValue('b')
234
+ * // => currentPathObject = {a: ['b']}
235
+ * ```
236
+ *
237
+ */
238
+ function handlePathPart(pathPart, currentPathObject, arraysWithOrder) {
239
+ if (pathPart.type === 'object') {
240
+ if (Array.isArray(currentPathObject)) {
241
+ throw new DuplicateKeyError(pathPart.pathToPart);
242
+ }
243
+ const currentObject = currentPathObject;
244
+ return [currentObject[pathPart.path], val => currentObject[pathPart.path] = val];
245
+ }
246
+ if (!Array.isArray(currentPathObject)) {
247
+ throw new DuplicateKeyError(pathPart.pathToPart);
248
+ }
249
+ const currentArray = currentPathObject;
250
+ const isOrdered = pathPart.path !== '';
251
+ const isOrderedArray = arraysWithOrder.has(currentArray);
252
+ if (isOrdered) {
253
+ arraysWithOrder.add(currentArray);
254
+ }
255
+ if (!isOrdered && isOrderedArray || isOrdered && !isOrderedArray && currentArray.length > 0) {
256
+ throw new MixedArrayError(pathPart.pathToPart);
257
+ }
258
+ const order = isOrdered ? Number(pathPart.path) : currentArray.length;
259
+ return [currentArray[order], val => currentArray[order] = val];
260
+ }
261
+
262
+ /**
263
+ *
264
+ * Parses a FormData object to a JSON object. This is done by parsing the `name`
265
+ * attribute of each `FormDataEntryValue` and then inserting the value at the
266
+ * path. Also by default the start of the path is used to transform the value.
267
+ *
268
+ *
269
+ * In front of the whole `key`:
270
+ * - `+` => parse to `Number`
271
+ * - `-` => set value to `null`
272
+ * - `&` => parse to `Boolean`
273
+ *
274
+ * - `.` between path parts => nest into `objects`
275
+ * - `[\d*]` after path part => push to array in order `\d` or push to end if `[]`
276
+ *
277
+
278
+ *
279
+ * @example
280
+ * ```ts
281
+ * const formData = new FormData()
282
+ * formData.append('+a', '1')
283
+ * formData.append('&b', 'true')
284
+ * formData.append('-c', 'null')
285
+ * formData.append('d', 'foo')
286
+ * parseFormData(formData, defaultTransform)
287
+ * // => {a: 1, b: true, c: null, d: 'foo'}
288
+ * ```
289
+ *
290
+ * @example
291
+ * ```ts
292
+ * const formData = new FormData()
293
+ * formData.append('a.b', 'foo')
294
+ * parseFormData(formData)
295
+ * // => {a: {b: 'foo'}}
296
+ * ```
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * const formData = new FormData()
301
+ * formData.append('a[0]', 'foo')
302
+ * formData.append('a[1]', 'bar')
303
+ * parseFormData(formData)
304
+ * // => {a: ['foo', 'bar']}
305
+ * ```
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * const formData = new FormData()
310
+ * formData.append('a[]', 'foo')
311
+ * formData.append('a[]', 'bar')
312
+ * parseFormData(formData)
313
+ * // => {a: ['foo', 'bar']}
314
+ * ```
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * const formData = new FormData()
319
+ * formData.append('a[0]', 'foo')
320
+ * parseFormData(formData, {transformEntry: (path, value) => {path, value: value + 'bar'}})
321
+ * // => {a: ['foobar']}
322
+ * ```
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * const formData = new FormData()
327
+ * formData.append('a[0]', 'foo')
328
+ * formData.append('a[1]', '')
329
+ * parseFormData(formData, {removeEmptyString: true})
330
+ * // => {a: ['foo']}
331
+ * ```
332
+ *
333
+ * @param {Iterable<[string, string | File]>} formData - an iterator of an [`path`, `value`] tuple
334
+ * - `path` := `^(\+|\-|\&)?([^\.]+?(\[\d*\])*)(\.[^\.]+?(\[\d*\])*)*$` (e.g. `+a[][1].b`)
335
+ * - `value` := `string` or `File`
336
+ * @param {ParseFormDataOptions} options - options for parsing the form data
337
+ * - `transformEntry` - a function to transform the path and the value before
338
+ * inserting the value at the path in the resulting object (default: `defaultTransform`)
339
+ * - `removeEmptyString` - if `true` removes all entries where the value is an empty string
340
+ * @returns {JsonObject} the parsed JSON object
341
+ * @throws `DuplicateKeyError` if
342
+ * - a path part is an object and the path part is already defined as an object
343
+ * - a path part is an array and the path part is already defined as an array
344
+ * @throws `MixedArrayError` if at a specific path part an unordered array is
345
+ * defined and at a later path part an ordered array is defined or vice versa
346
+ * - e.g. `a[0]` and `a[]`
347
+ * - e.g. `a[]` and `a[0]`
348
+ */
349
+ function parseFormData(formData, _temp) {
350
+ let {
351
+ removeEmptyString = false,
352
+ transformEntry = defaultTransform
353
+ } = _temp === void 0 ? {} : _temp;
354
+ const result = {};
355
+
356
+ // all arrays we need to squash (in place) later
357
+ const arraysWithOrder = new Set();
358
+ for (const entry of Array.from(formData)) {
359
+ if (removeEmptyString && entry[1] === '') continue;
360
+ const {
361
+ path,
362
+ value
363
+ } = transformEntry(entry, defaultTransform);
364
+ const pathParts = extractPathParts(path);
365
+ let currentPathObject = result;
366
+ pathParts.forEach((pathPart, idx) => {
367
+ const [nextPathValue, setNextPathValue] = handlePathPart(pathPart, currentPathObject, arraysWithOrder);
368
+ if (pathParts.length - 1 === idx) {
369
+ if (nextPathValue !== undefined) {
370
+ throw new DuplicateKeyError(pathPart.pathToPart);
371
+ }
372
+ setNextPathValue(value);
373
+ } else {
374
+ if (nextPathValue !== undefined && !isJsonObject(nextPathValue) && !Array.isArray(nextPathValue)) {
375
+ throw new DuplicateKeyError(pathPart.pathToPart);
376
+ }
377
+ const nextPathObject = nextPathValue ?? pathPart.default;
378
+ currentPathObject = nextPathObject;
379
+ setNextPathValue(nextPathObject);
380
+ }
381
+ });
382
+ }
383
+ for (const orderedArray of Array.from(arraysWithOrder)) {
384
+ // replace array with a squashed array
385
+ // array.flat(0) will remove all empty slots (e.g. [0, , 1] => [0, 1])
386
+ orderedArray.splice(0, orderedArray.length, ...orderedArray.flat(0));
387
+ }
388
+ return result;
389
+ }
390
+
391
+ exports.DuplicateKeyError = DuplicateKeyError;
392
+ exports.MixedArrayError = MixedArrayError;
393
+ exports.parseFormData = parseFormData;
394
+
395
+ Object.defineProperty(exports, '__esModule', { value: true });
396
+
397
+ }));
398
+ //# sourceMappingURL=parse-nested-form-data.umd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-nested-form-data.umd.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @name parse-nested-form-data\n * @license MIT license.\n * @copyright (c) 2022 Christian Schurr\n * @author Christian Schurr <chris@schurr.dev>\n */\n\nimport {File} from '@remix-run/web-file'\n\n/**\n * Thrown when a path is used multiple times or has missmatching path parts.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'b')\n * formData.append('a[0]', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a[0]')\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'b')\n * formData.append('a', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a')\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'b')\n * formData.append('a[]', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a[]')\n * ```\n *\n */\nexport class DuplicateKeyError extends Error {\n key: string\n constructor(key: string) {\n super(`Duplicate key at path part ${key}`)\n this.key = key\n }\n}\n/**\n * Thrown when an array is used at the same path with an order parameter and\n * without an order parameter.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'a')\n * formData.append('a[]', 'b')\n * parseFormData(formData)\n * // => throws `MixedArrayError(a[])`\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[]', 'a')\n * formData.append('a[0]', 'b')\n * parseFormData(formData)\n * // => throws `MixedArrayError(a[0])`\n * ```\n */\nexport class MixedArrayError extends Error {\n key: string\n constructor(key: string) {\n super(`Mixed array at path part ${key}`)\n this.key = key\n }\n}\n\ntype JsonObject = {[Key in string]?: JsonValue}\ntype JsonArray = Array<JsonValue>\ntype JsonValue =\n | string\n | number\n | JsonObject\n | JsonArray\n | boolean\n | null\n | File\ntype JsonLeafValue = Exclude<JsonValue, JsonArray | JsonObject>\n\nfunction isJsonObject(val: JsonValue): val is JsonObject {\n return (\n typeof val === 'object' &&\n !Array.isArray(val) &&\n val !== null &&\n !(val instanceof File)\n )\n}\n\n/**\n * Default Transformer for `parseFormData`.\n *\n * Transforms a FormData Entry into a path and a `JsonLeafValue`.\n *\n * - `path` starts with `+` -> transform value to `number`\n * - `path` starts with `&` -> transform value to `boolean`\n * - `path` starts with `-` -> transform value to `null`\n *\n * @example\n * ```ts\n * const entry = ['a[0]', 'b']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['+a[0]', '1']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: 1}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['&a[0]', 'true']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: true}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['-a[0]', 'null']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: null}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['a[0]', new File([''], 'file.txt')]\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: File}\n * ```\n *\n *\n * @param entry [path, value]: the FormData entry\n * @returns the path and the transformed value\n */\nfunction defaultTransform(entry: [path: string, value: string | File]): {\n path: string\n value: JsonLeafValue\n} {\n let path = entry[0]\n let value: JsonLeafValue = entry[1]\n if (path.startsWith('+')) {\n path = path.slice(1)\n value = Number(value)\n } else if (path.startsWith('&')) {\n path = path.slice(1)\n value = value === 'on' || value === 'true' || Boolean(Number(value))\n } else if (path.startsWith('-')) {\n path = path.slice(1)\n value = null\n }\n return {path, value}\n}\n\ntype DefaultTransform = typeof defaultTransform\n/**\n * Options to change the behavior of `parseFormData`.\n * @param transformEntry - a function to transform the FormData entry into a path and a value\n * (default: `defaultTransform`)\n * @param removeEmptyString - skip empty values '' (default: `false`)\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', '')\n * formData.append('b', 'b')\n * const result = parseFormData(formData, {removeEmptyString: true})\n * // => {b: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'a')\n * formData.append('b', 'b')\n * parseFormData(formData, {\n * transformEntry: ([path, value], defaultTransform) => {\n * return {\n * path,\n * value:\n * typeof value === 'string'\n * ? value.toUpperCase()\n * : defaultTransform(value),\n * }\n * },\n * })\n * // => {a: 'A', b: 'B'}\n * ```\n */\nexport type ParseFormDataOptions = {\n removeEmptyString?: boolean\n transformEntry?: (\n entry: [path: string, value: string | File],\n defaultTransform: DefaultTransform,\n ) => {path: string; value: JsonLeafValue}\n}\n\n/**\n * A parsed part of a FormData path. It consists of four parts:\n * @param path - the key to access the value in the intermediate result\n * @param type - the type of the path part (array - e.g. [\\d*], or object .e.g. .key)\n * @param default - the default value of the next path part ([] for array, {} for object)\n * @param pathToPart - the full path to the current path part\n *\n * @example\n * ```ts\n * const path = 'a[0].b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]\n * ```\n */\ntype PathPart = {\n path: string\n type: 'object' | 'array'\n default: {} | []\n pathToPart: string\n}\n/**\n *\n * Transforms a FormData path into an array of `PathPart`s.\n *\n * @param path - the path to extract the path parts from\n * @returns {Array<PathPart>} the extracted path parts\n *\n * @example\n * ```ts\n * const path = 'a[0].b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a.b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: {}, pathToPart: 'a'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a.b'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a[][0]'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},\n * // {path: '', type: 'array', default: [], pathToPart: 'a[]'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[][0]'}]\n * ```\n *\n *\n */\nfunction extractPathParts(path: string): Array<PathPart> {\n const re = /((?<array>\\d*)\\]|(?<pathPart>[^.[]+))(?<nextType>\\[|\\.|$)/g\n\n return Array.from(path.matchAll(re)).map<PathPart>(match => {\n // self casted RegexExpMatchArray to custom\n const typedMatch = match as unknown as [string, string] &\n RegExpExecArray & {\n groups:\n | {\n pathPart: undefined\n array: string\n nextType: '[' | '.' | ''\n }\n | {\n pathPart: string\n array: undefined\n nextType: '[' | '.' | ''\n }\n }\n const {array, pathPart, nextType} = typedMatch.groups\n const type = array === undefined ? 'object' : 'array'\n const nextDefault = nextType === '[' ? [] : {}\n return {\n path: array ?? pathPart,\n type,\n default: nextDefault,\n pathToPart: path.slice(0, typedMatch.index + typedMatch[1].length),\n }\n })\n}\n\n/**\n *\n * Returns the value accessed via `pathPart` in the `currentPathObject`\n * and a setter function to set the value in the `currentPathObject` via the\n * provided `pathPart`.\n *\n * @param pathPart - the path part to get the setter and getter for\n * @param currentPathObject - the object at the current path (before the path part)\n * @param arraysWithOrder - a set of arrays that have an order\n * @returns the setter and getter for the path part\n *\n * @example\n * ```ts\n * const pathPart = {path: 'a', type: 'object', default: {}, pathToPart: 'a'}\n * const currentPathObject = {}\n * const arraysWithOrder = new Set()\n * const [value, setValue] = getSetterAndGetter(pathPart, currentPathObject, arraysWithOrder)\n * setValue('b')\n * // => currentPathObject = {a: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const pathPart = {path: '0', type: 'array', default: [], pathToPart: 'a[0]'}\n * const currentPathObject = {a: []}\n * const arraysWithOrder = new Set()\n * const [value, setValue] = getSetterAndGetter(pathPart, currentPathObject, arraysWithOrder)\n * setValue('b')\n * // => currentPathObject = {a: ['b']}\n * ```\n *\n */\nfunction handlePathPart(\n pathPart: PathPart,\n currentPathObject: JsonArray | JsonObject,\n arraysWithOrder: Set<JsonArray>,\n): [\n nextPathValue: JsonValue | undefined,\n setNextPathValue: (value: JsonValue) => void,\n] {\n if (pathPart.type === 'object') {\n if (Array.isArray(currentPathObject)) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n const currentObject = currentPathObject\n return [\n currentObject[pathPart.path],\n val => (currentObject[pathPart.path] = val),\n ]\n }\n if (!Array.isArray(currentPathObject)) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n const currentArray = currentPathObject\n const isOrdered = pathPart.path !== ''\n\n const isOrderedArray = arraysWithOrder.has(currentArray)\n if (isOrdered) {\n arraysWithOrder.add(currentArray)\n }\n if (\n (!isOrdered && isOrderedArray) ||\n (isOrdered && !isOrderedArray && currentArray.length > 0)\n ) {\n throw new MixedArrayError(pathPart.pathToPart)\n }\n\n const order = isOrdered ? Number(pathPart.path) : currentArray.length\n return [currentArray[order], val => (currentArray[order] = val)]\n}\n\n/**\n *\n * Parses a FormData object to a JSON object. This is done by parsing the `name`\n * attribute of each `FormDataEntryValue` and then inserting the value at the\n * path. Also by default the start of the path is used to transform the value.\n *\n *\n * In front of the whole `key`:\n * - `+` => parse to `Number`\n * - `-` => set value to `null`\n * - `&` => parse to `Boolean`\n *\n * - `.` between path parts => nest into `objects`\n * - `[\\d*]` after path part => push to array in order `\\d` or push to end if `[]`\n *\n\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('+a', '1')\n * formData.append('&b', 'true')\n * formData.append('-c', 'null')\n * formData.append('d', 'foo')\n * parseFormData(formData, defaultTransform)\n * // => {a: 1, b: true, c: null, d: 'foo'}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a.b', 'foo')\n * parseFormData(formData)\n * // => {a: {b: 'foo'}}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * formData.append('a[1]', 'bar')\n * parseFormData(formData)\n * // => {a: ['foo', 'bar']}\n * ```\n * \n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[]', 'foo')\n * formData.append('a[]', 'bar')\n * parseFormData(formData)\n * // => {a: ['foo', 'bar']}\n * ```\n * \n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * parseFormData(formData, {transformEntry: (path, value) => {path, value: value + 'bar'}})\n * // => {a: ['foobar']}\n * ```\n * \n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * formData.append('a[1]', '')\n * parseFormData(formData, {removeEmptyString: true})\n * // => {a: ['foo']}\n * ```\n *\n * @param {Iterable<[string, string | File]>} formData - an iterator of an [`path`, `value`] tuple\n * - `path` := `^(\\+|\\-|\\&)?([^\\.]+?(\\[\\d*\\])*)(\\.[^\\.]+?(\\[\\d*\\])*)*$` (e.g. `+a[][1].b`)\n * - `value` := `string` or `File`\n * @param {ParseFormDataOptions} options - options for parsing the form data\n * - `transformEntry` - a function to transform the path and the value before\n * inserting the value at the path in the resulting object (default: `defaultTransform`)\n * - `removeEmptyString` - if `true` removes all entries where the value is an empty string\n * @returns {JsonObject} the parsed JSON object\n * @throws `DuplicateKeyError` if\n * - a path part is an object and the path part is already defined as an object\n * - a path part is an array and the path part is already defined as an array\n * @throws `MixedArrayError` if at a specific path part an unordered array is \n * defined and at a later path part an ordered array is defined or vice versa\n * - e.g. `a[0]` and `a[]`\n * - e.g. `a[]` and `a[0]`\n */\nexport function parseFormData(\n formData: Iterable<[string, string | File]>,\n {\n removeEmptyString = false,\n transformEntry = defaultTransform,\n }: ParseFormDataOptions = {},\n): JsonObject {\n const result: JsonObject = {}\n\n // all arrays we need to squash (in place) later\n const arraysWithOrder: Set<Array<JsonValue>> = new Set()\n\n for (const entry of Array.from(formData)) {\n if (removeEmptyString && entry[1] === '') continue\n\n const {path, value} = transformEntry(entry, defaultTransform)\n const pathParts = extractPathParts(path)\n\n let currentPathObject: JsonObject | JsonArray = result\n pathParts.forEach((pathPart, idx) => {\n const [nextPathValue, setNextPathValue] = handlePathPart(\n pathPart,\n currentPathObject,\n arraysWithOrder,\n )\n\n if (pathParts.length - 1 === idx) {\n if (nextPathValue !== undefined) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n setNextPathValue(value)\n } else {\n if (\n nextPathValue !== undefined &&\n !isJsonObject(nextPathValue) &&\n !Array.isArray(nextPathValue)\n ) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n\n const nextPathObject = nextPathValue ?? pathPart.default\n currentPathObject = nextPathObject\n setNextPathValue(nextPathObject)\n }\n })\n }\n\n for (const orderedArray of Array.from(arraysWithOrder)) {\n // replace array with a squashed array\n // array.flat(0) will remove all empty slots (e.g. [0, , 1] => [0, 1])\n orderedArray.splice(0, orderedArray.length, ...orderedArray.flat(0))\n }\n\n return result\n}\n"],"names":["DuplicateKeyError","Error","constructor","key","MixedArrayError","isJsonObject","val","Array","isArray","File","defaultTransform","entry","path","value","startsWith","slice","Number","Boolean","extractPathParts","re","from","matchAll","map","match","typedMatch","array","pathPart","nextType","groups","type","undefined","nextDefault","default","pathToPart","index","length","handlePathPart","currentPathObject","arraysWithOrder","currentObject","currentArray","isOrdered","isOrderedArray","has","add","order","parseFormData","formData","removeEmptyString","transformEntry","result","Set","pathParts","forEach","idx","nextPathValue","setNextPathValue","nextPathObject","orderedArray","splice","flat"],"mappings":";;;;;;EAAA;EACA;EACA;EACA;EACA;EACA;;EAIA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACO,MAAMA,iBAAiB,SAASC,KAAK,CAAC;IAE3CC,WAAW,CAACC,GAAW,EAAE;EACvB,IAAA,KAAK,CAAE,CAAA,2BAAA,EAA6BA,GAAI,CAAA,CAAC,CAAC,CAAA;EAAA,IAAA,IAAA,CAF5CA,GAAG,GAAA,KAAA,CAAA,CAAA;MAGD,IAAI,CAACA,GAAG,GAAGA,GAAG,CAAA;EAChB,GAAA;EACF,CAAA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACO,MAAMC,eAAe,SAASH,KAAK,CAAC;IAEzCC,WAAW,CAACC,GAAW,EAAE;EACvB,IAAA,KAAK,CAAE,CAAA,yBAAA,EAA2BA,GAAI,CAAA,CAAC,CAAC,CAAA;EAAA,IAAA,IAAA,CAF1CA,GAAG,GAAA,KAAA,CAAA,CAAA;MAGD,IAAI,CAACA,GAAG,GAAGA,GAAG,CAAA;EAChB,GAAA;EACF,CAAA;EAcA,SAASE,YAAY,CAACC,GAAc,EAAqB;IACvD,OACE,OAAOA,GAAG,KAAK,QAAQ,IACvB,CAACC,KAAK,CAACC,OAAO,CAACF,GAAG,CAAC,IACnBA,GAAG,KAAK,IAAI,IACZ,EAAEA,GAAG,YAAYG,YAAI,CAAC,CAAA;EAE1B,CAAA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,SAASC,gBAAgB,CAACC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,CAACH,YAAY,CAAC,CAAA;EACxD,EAAA,IAAIC,SAAS,EAAE;EACbH,IAAAA,eAAe,CAACM,GAAG,CAACJ,YAAY,CAAC,CAAA;EACnC,GAAA;EACA,EAAA,IACG,CAACC,SAAS,IAAIC,cAAc,IAC5BD,SAAS,IAAI,CAACC,cAAc,IAAIF,YAAY,CAACL,MAAM,GAAG,CAAE,EACzD;EACA,IAAA,MAAM,IAAI/B,eAAe,CAACsB,QAAQ,CAACO,UAAU,CAAC,CAAA;EAChD,GAAA;EAEA,EAAA,MAAMY,KAAK,GAAGJ,SAAS,GAAGzB,MAAM,CAACU,QAAQ,CAACd,IAAI,CAAC,GAAG4B,YAAY,CAACL,MAAM,CAAA;EACrE,EAAA,OAAO,CAACK,YAAY,CAACK,KAAK,CAAC,EAAEvC,GAAG,IAAKkC,YAAY,CAACK,KAAK,CAAC,GAAGvC,GAAI,CAAC,CAAA;EAClE,CAAA;;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACO,SAASwC,aAAa,CAC3BC,QAA2C,EAK/B,KAAA,EAAA;IAAA,IAJZ;EACEC,IAAAA,iBAAiB,GAAG,KAAK;EACzBC,IAAAA,cAAc,GAAGvC,gBAAAA;KACI,GAAA,KAAA,KAAA,KAAA,CAAA,GAAG,EAAE,GAAA,KAAA,CAAA;IAE5B,MAAMwC,MAAkB,GAAG,EAAE,CAAA;;EAE7B;EACA,EAAA,MAAMZ,eAAsC,GAAG,IAAIa,GAAG,EAAE,CAAA;IAExD,KAAK,MAAMxC,KAAK,IAAIJ,KAAK,CAACa,IAAI,CAAC2B,QAAQ,CAAC,EAAE;MACxC,IAAIC,iBAAiB,IAAIrC,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,SAAA;MAE1C,MAAM;QAACC,IAAI;EAAEC,MAAAA,KAAAA;EAAK,KAAC,GAAGoC,cAAc,CAACtC,KAAK,EAAED,gBAAgB,CAAC,CAAA;EAC7D,IAAA,MAAM0C,SAAS,GAAGlC,gBAAgB,CAACN,IAAI,CAAC,CAAA;MAExC,IAAIyB,iBAAyC,GAAGa,MAAM,CAAA;EACtDE,IAAAA,SAAS,CAACC,OAAO,CAAC,CAAC3B,QAAQ,EAAE4B,GAAG,KAAK;EACnC,MAAA,MAAM,CAACC,aAAa,EAAEC,gBAAgB,CAAC,GAAGpB,cAAc,CACtDV,QAAQ,EACRW,iBAAiB,EACjBC,eAAe,CAChB,CAAA;EAED,MAAA,IAAIc,SAAS,CAACjB,MAAM,GAAG,CAAC,KAAKmB,GAAG,EAAE;UAChC,IAAIC,aAAa,KAAKzB,SAAS,EAAE;EAC/B,UAAA,MAAM,IAAI9B,iBAAiB,CAAC0B,QAAQ,CAACO,UAAU,CAAC,CAAA;EAClD,SAAA;UACAuB,gBAAgB,CAAC3C,KAAK,CAAC,CAAA;EACzB,OAAC,MAAM;EACL,QAAA,IACE0C,aAAa,KAAKzB,SAAS,IAC3B,CAACzB,YAAY,CAACkD,aAAa,CAAC,IAC5B,CAAChD,KAAK,CAACC,OAAO,CAAC+C,aAAa,CAAC,EAC7B;EACA,UAAA,MAAM,IAAIvD,iBAAiB,CAAC0B,QAAQ,CAACO,UAAU,CAAC,CAAA;EAClD,SAAA;EAEA,QAAA,MAAMwB,cAAc,GAAGF,aAAa,IAAI7B,QAAQ,CAACM,OAAO,CAAA;EACxDK,QAAAA,iBAAiB,GAAGoB,cAAc,CAAA;UAClCD,gBAAgB,CAACC,cAAc,CAAC,CAAA;EAClC,OAAA;EACF,KAAC,CAAC,CAAA;EACJ,GAAA;IAEA,KAAK,MAAMC,YAAY,IAAInD,KAAK,CAACa,IAAI,CAACkB,eAAe,CAAC,EAAE;EACtD;EACA;EACAoB,IAAAA,YAAY,CAACC,MAAM,CAAC,CAAC,EAAED,YAAY,CAACvB,MAAM,EAAE,GAAGuB,YAAY,CAACE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;EACtE,GAAA;EAEA,EAAA,OAAOV,MAAM,CAAA;EACf;;;;;;;;;;;;"}
@@ -0,0 +1,8 @@
1
+ !function(t,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("@remix-run/web-file")):"function"==typeof define&&define.amd?define(["exports","@remix-run/web-file"],r):r((t="undefined"!=typeof globalThis?globalThis:t||self).parseNestedFormData={},t.RemixRunWebFile)}(this,(function(t,r){"use strict";
2
+ /**
3
+ * @name parse-nested-form-data
4
+ * @license MIT license.
5
+ * @copyright (c) 2022 Christian Schurr
6
+ * @author Christian Schurr <chris@schurr.dev>
7
+ */class e extends Error{constructor(t){super(`Duplicate key at path part ${t}`),this.key=void 0,this.key=t}}class a extends Error{constructor(t){super(`Mixed array at path part ${t}`),this.key=void 0,this.key=t}}function o(t){let r=t[0],e=t[1];return r.startsWith("+")?(r=r.slice(1),e=Number(e)):r.startsWith("&")?(r=r.slice(1),e="on"===e||"true"===e||Boolean(Number(e))):r.startsWith("-")&&(r=r.slice(1),e=null),{path:r,value:e}}function n(t){return Array.from(t.matchAll(/((?<array>\d*)\]|(?<pathPart>[^.[]+))(?<nextType>\[|\.|$)/g)).map((r=>{const e=r,{array:a,pathPart:o,nextType:n}=e.groups;return{path:a??o,type:void 0===a?"object":"array",default:"["===n?[]:{},pathToPart:t.slice(0,e.index+e[1].length)}}))}function i(t,r,o){if("object"===t.type){if(Array.isArray(r))throw new e(t.pathToPart);const a=r;return[a[t.path],r=>a[t.path]=r]}if(!Array.isArray(r))throw new e(t.pathToPart);const n=r,i=""!==t.path,s=o.has(n);if(i&&o.add(n),!i&&s||i&&!s&&n.length>0)throw new a(t.pathToPart);const f=i?Number(t.path):n.length;return[n[f],t=>n[f]=t]}t.DuplicateKeyError=e,t.MixedArrayError=a,t.parseFormData=function(t,a){let{removeEmptyString:s=!1,transformEntry:f=o}=void 0===a?{}:a;const l={},p=new Set;for(const a of Array.from(t)){if(s&&""===a[1])continue;const{path:t,value:u}=f(a,o),c=n(t);let h=l;c.forEach(((t,a)=>{const[o,n]=i(t,h,p);if(c.length-1===a){if(void 0!==o)throw new e(t.pathToPart);n(u)}else{if(void 0!==o&&("object"!=typeof(s=o)||Array.isArray(s)||null===s||s instanceof r.File)&&!Array.isArray(o))throw new e(t.pathToPart);const a=o??t.default;h=a,n(a)}var s}))}for(const t of Array.from(p))t.splice(0,t.length,...t.flat(0));return l},Object.defineProperty(t,"__esModule",{value:!0})}));
8
+ //# sourceMappingURL=parse-nested-form-data.umd.min.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-nested-form-data.umd.min.js","sources":["../src/index.ts"],"sourcesContent":["/**\n * @name parse-nested-form-data\n * @license MIT license.\n * @copyright (c) 2022 Christian Schurr\n * @author Christian Schurr <chris@schurr.dev>\n */\n\nimport {File} from '@remix-run/web-file'\n\n/**\n * Thrown when a path is used multiple times or has missmatching path parts.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'b')\n * formData.append('a[0]', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a[0]')\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'b')\n * formData.append('a', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a')\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'b')\n * formData.append('a[]', 'c')\n * parseFormData(formData)\n * // throws DuplicateKeyError('a[]')\n * ```\n *\n */\nexport class DuplicateKeyError extends Error {\n key: string\n constructor(key: string) {\n super(`Duplicate key at path part ${key}`)\n this.key = key\n }\n}\n/**\n * Thrown when an array is used at the same path with an order parameter and\n * without an order parameter.\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'a')\n * formData.append('a[]', 'b')\n * parseFormData(formData)\n * // => throws `MixedArrayError(a[])`\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[]', 'a')\n * formData.append('a[0]', 'b')\n * parseFormData(formData)\n * // => throws `MixedArrayError(a[0])`\n * ```\n */\nexport class MixedArrayError extends Error {\n key: string\n constructor(key: string) {\n super(`Mixed array at path part ${key}`)\n this.key = key\n }\n}\n\ntype JsonObject = {[Key in string]?: JsonValue}\ntype JsonArray = Array<JsonValue>\ntype JsonValue =\n | string\n | number\n | JsonObject\n | JsonArray\n | boolean\n | null\n | File\ntype JsonLeafValue = Exclude<JsonValue, JsonArray | JsonObject>\n\nfunction isJsonObject(val: JsonValue): val is JsonObject {\n return (\n typeof val === 'object' &&\n !Array.isArray(val) &&\n val !== null &&\n !(val instanceof File)\n )\n}\n\n/**\n * Default Transformer for `parseFormData`.\n *\n * Transforms a FormData Entry into a path and a `JsonLeafValue`.\n *\n * - `path` starts with `+` -> transform value to `number`\n * - `path` starts with `&` -> transform value to `boolean`\n * - `path` starts with `-` -> transform value to `null`\n *\n * @example\n * ```ts\n * const entry = ['a[0]', 'b']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['+a[0]', '1']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: 1}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['&a[0]', 'true']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: true}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['-a[0]', 'null']\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: null}\n * ```\n *\n * @example\n * ```ts\n * const entry = ['a[0]', new File([''], 'file.txt')]\n * const result = defaultTransform(entry)\n * // => {path: 'a[0]', value: File}\n * ```\n *\n *\n * @param entry [path, value]: the FormData entry\n * @returns the path and the transformed value\n */\nfunction defaultTransform(entry: [path: string, value: string | File]): {\n path: string\n value: JsonLeafValue\n} {\n let path = entry[0]\n let value: JsonLeafValue = entry[1]\n if (path.startsWith('+')) {\n path = path.slice(1)\n value = Number(value)\n } else if (path.startsWith('&')) {\n path = path.slice(1)\n value = value === 'on' || value === 'true' || Boolean(Number(value))\n } else if (path.startsWith('-')) {\n path = path.slice(1)\n value = null\n }\n return {path, value}\n}\n\ntype DefaultTransform = typeof defaultTransform\n/**\n * Options to change the behavior of `parseFormData`.\n * @param transformEntry - a function to transform the FormData entry into a path and a value\n * (default: `defaultTransform`)\n * @param removeEmptyString - skip empty values '' (default: `false`)\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', '')\n * formData.append('b', 'b')\n * const result = parseFormData(formData, {removeEmptyString: true})\n * // => {b: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a', 'a')\n * formData.append('b', 'b')\n * parseFormData(formData, {\n * transformEntry: ([path, value], defaultTransform) => {\n * return {\n * path,\n * value:\n * typeof value === 'string'\n * ? value.toUpperCase()\n * : defaultTransform(value),\n * }\n * },\n * })\n * // => {a: 'A', b: 'B'}\n * ```\n */\nexport type ParseFormDataOptions = {\n removeEmptyString?: boolean\n transformEntry?: (\n entry: [path: string, value: string | File],\n defaultTransform: DefaultTransform,\n ) => {path: string; value: JsonLeafValue}\n}\n\n/**\n * A parsed part of a FormData path. It consists of four parts:\n * @param path - the key to access the value in the intermediate result\n * @param type - the type of the path part (array - e.g. [\\d*], or object .e.g. .key)\n * @param default - the default value of the next path part ([] for array, {} for object)\n * @param pathToPart - the full path to the current path part\n *\n * @example\n * ```ts\n * const path = 'a[0].b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]\n * ```\n */\ntype PathPart = {\n path: string\n type: 'object' | 'array'\n default: {} | []\n pathToPart: string\n}\n/**\n *\n * Transforms a FormData path into an array of `PathPart`s.\n *\n * @param path - the path to extract the path parts from\n * @returns {Array<PathPart>} the extracted path parts\n *\n * @example\n * ```ts\n * const path = 'a[0].b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[0]'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a[0].b'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a.b'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: {}, pathToPart: 'a'},\n * // {path: 'b', type: 'object', default: {}, pathToPart: 'a.b'}]\n * ```\n *\n * @example\n * ```ts\n * const path = 'a[][0]'\n * const result = extractPathParts(path)\n * // => [{path: 'a', type: 'object, default: [], pathToPart: 'a'},\n * // {path: '', type: 'array', default: [], pathToPart: 'a[]'},\n * // {path: '0', type: 'array', default: {}, pathToPart: 'a[][0]'}]\n * ```\n *\n *\n */\nfunction extractPathParts(path: string): Array<PathPart> {\n const re = /((?<array>\\d*)\\]|(?<pathPart>[^.[]+))(?<nextType>\\[|\\.|$)/g\n\n return Array.from(path.matchAll(re)).map<PathPart>(match => {\n // self casted RegexExpMatchArray to custom\n const typedMatch = match as unknown as [string, string] &\n RegExpExecArray & {\n groups:\n | {\n pathPart: undefined\n array: string\n nextType: '[' | '.' | ''\n }\n | {\n pathPart: string\n array: undefined\n nextType: '[' | '.' | ''\n }\n }\n const {array, pathPart, nextType} = typedMatch.groups\n const type = array === undefined ? 'object' : 'array'\n const nextDefault = nextType === '[' ? [] : {}\n return {\n path: array ?? pathPart,\n type,\n default: nextDefault,\n pathToPart: path.slice(0, typedMatch.index + typedMatch[1].length),\n }\n })\n}\n\n/**\n *\n * Returns the value accessed via `pathPart` in the `currentPathObject`\n * and a setter function to set the value in the `currentPathObject` via the\n * provided `pathPart`.\n *\n * @param pathPart - the path part to get the setter and getter for\n * @param currentPathObject - the object at the current path (before the path part)\n * @param arraysWithOrder - a set of arrays that have an order\n * @returns the setter and getter for the path part\n *\n * @example\n * ```ts\n * const pathPart = {path: 'a', type: 'object', default: {}, pathToPart: 'a'}\n * const currentPathObject = {}\n * const arraysWithOrder = new Set()\n * const [value, setValue] = getSetterAndGetter(pathPart, currentPathObject, arraysWithOrder)\n * setValue('b')\n * // => currentPathObject = {a: 'b'}\n * ```\n *\n * @example\n * ```ts\n * const pathPart = {path: '0', type: 'array', default: [], pathToPart: 'a[0]'}\n * const currentPathObject = {a: []}\n * const arraysWithOrder = new Set()\n * const [value, setValue] = getSetterAndGetter(pathPart, currentPathObject, arraysWithOrder)\n * setValue('b')\n * // => currentPathObject = {a: ['b']}\n * ```\n *\n */\nfunction handlePathPart(\n pathPart: PathPart,\n currentPathObject: JsonArray | JsonObject,\n arraysWithOrder: Set<JsonArray>,\n): [\n nextPathValue: JsonValue | undefined,\n setNextPathValue: (value: JsonValue) => void,\n] {\n if (pathPart.type === 'object') {\n if (Array.isArray(currentPathObject)) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n const currentObject = currentPathObject\n return [\n currentObject[pathPart.path],\n val => (currentObject[pathPart.path] = val),\n ]\n }\n if (!Array.isArray(currentPathObject)) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n const currentArray = currentPathObject\n const isOrdered = pathPart.path !== ''\n\n const isOrderedArray = arraysWithOrder.has(currentArray)\n if (isOrdered) {\n arraysWithOrder.add(currentArray)\n }\n if (\n (!isOrdered && isOrderedArray) ||\n (isOrdered && !isOrderedArray && currentArray.length > 0)\n ) {\n throw new MixedArrayError(pathPart.pathToPart)\n }\n\n const order = isOrdered ? Number(pathPart.path) : currentArray.length\n return [currentArray[order], val => (currentArray[order] = val)]\n}\n\n/**\n *\n * Parses a FormData object to a JSON object. This is done by parsing the `name`\n * attribute of each `FormDataEntryValue` and then inserting the value at the\n * path. Also by default the start of the path is used to transform the value.\n *\n *\n * In front of the whole `key`:\n * - `+` => parse to `Number`\n * - `-` => set value to `null`\n * - `&` => parse to `Boolean`\n *\n * - `.` between path parts => nest into `objects`\n * - `[\\d*]` after path part => push to array in order `\\d` or push to end if `[]`\n *\n\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('+a', '1')\n * formData.append('&b', 'true')\n * formData.append('-c', 'null')\n * formData.append('d', 'foo')\n * parseFormData(formData, defaultTransform)\n * // => {a: 1, b: true, c: null, d: 'foo'}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a.b', 'foo')\n * parseFormData(formData)\n * // => {a: {b: 'foo'}}\n * ```\n *\n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * formData.append('a[1]', 'bar')\n * parseFormData(formData)\n * // => {a: ['foo', 'bar']}\n * ```\n * \n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[]', 'foo')\n * formData.append('a[]', 'bar')\n * parseFormData(formData)\n * // => {a: ['foo', 'bar']}\n * ```\n * \n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * parseFormData(formData, {transformEntry: (path, value) => {path, value: value + 'bar'}})\n * // => {a: ['foobar']}\n * ```\n * \n * @example\n * ```ts\n * const formData = new FormData()\n * formData.append('a[0]', 'foo')\n * formData.append('a[1]', '')\n * parseFormData(formData, {removeEmptyString: true})\n * // => {a: ['foo']}\n * ```\n *\n * @param {Iterable<[string, string | File]>} formData - an iterator of an [`path`, `value`] tuple\n * - `path` := `^(\\+|\\-|\\&)?([^\\.]+?(\\[\\d*\\])*)(\\.[^\\.]+?(\\[\\d*\\])*)*$` (e.g. `+a[][1].b`)\n * - `value` := `string` or `File`\n * @param {ParseFormDataOptions} options - options for parsing the form data\n * - `transformEntry` - a function to transform the path and the value before\n * inserting the value at the path in the resulting object (default: `defaultTransform`)\n * - `removeEmptyString` - if `true` removes all entries where the value is an empty string\n * @returns {JsonObject} the parsed JSON object\n * @throws `DuplicateKeyError` if\n * - a path part is an object and the path part is already defined as an object\n * - a path part is an array and the path part is already defined as an array\n * @throws `MixedArrayError` if at a specific path part an unordered array is \n * defined and at a later path part an ordered array is defined or vice versa\n * - e.g. `a[0]` and `a[]`\n * - e.g. `a[]` and `a[0]`\n */\nexport function parseFormData(\n formData: Iterable<[string, string | File]>,\n {\n removeEmptyString = false,\n transformEntry = defaultTransform,\n }: ParseFormDataOptions = {},\n): JsonObject {\n const result: JsonObject = {}\n\n // all arrays we need to squash (in place) later\n const arraysWithOrder: Set<Array<JsonValue>> = new Set()\n\n for (const entry of Array.from(formData)) {\n if (removeEmptyString && entry[1] === '') continue\n\n const {path, value} = transformEntry(entry, defaultTransform)\n const pathParts = extractPathParts(path)\n\n let currentPathObject: JsonObject | JsonArray = result\n pathParts.forEach((pathPart, idx) => {\n const [nextPathValue, setNextPathValue] = handlePathPart(\n pathPart,\n currentPathObject,\n arraysWithOrder,\n )\n\n if (pathParts.length - 1 === idx) {\n if (nextPathValue !== undefined) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n setNextPathValue(value)\n } else {\n if (\n nextPathValue !== undefined &&\n !isJsonObject(nextPathValue) &&\n !Array.isArray(nextPathValue)\n ) {\n throw new DuplicateKeyError(pathPart.pathToPart)\n }\n\n const nextPathObject = nextPathValue ?? pathPart.default\n currentPathObject = nextPathObject\n setNextPathValue(nextPathObject)\n }\n })\n }\n\n for (const orderedArray of Array.from(arraysWithOrder)) {\n // replace array with a squashed array\n // array.flat(0) will remove all empty slots (e.g. [0, , 1] => [0, 1])\n orderedArray.splice(0, orderedArray.length, ...orderedArray.flat(0))\n }\n\n return result\n}\n"],"names":["DuplicateKeyError","Error","constructor","key","super","this","MixedArrayError","defaultTransform","entry","path","value","startsWith","slice","Number","Boolean","extractPathParts","Array","from","matchAll","map","match","typedMatch","array","pathPart","nextType","groups","type","undefined","default","pathToPart","index","length","handlePathPart","currentPathObject","arraysWithOrder","isArray","currentObject","val","currentArray","isOrdered","isOrderedArray","has","add","order","formData","_temp","removeEmptyString","transformEntry","result","Set","pathParts","forEach","idx","nextPathValue","setNextPathValue","File","nextPathObject","orderedArray","splice","flat"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "parse-nested-form-data",
3
+ "version": "1.0.0",
4
+ "description": "A tiny node module for parsing FormData by name into objects and arrays",
5
+ "main": "dist/parse-nested-form-data.cjs.js",
6
+ "module": "dist/parse-nested-form-data.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "keywords": [
9
+ "FormData",
10
+ "parse",
11
+ "nested",
12
+ "form",
13
+ "array",
14
+ "object",
15
+ "parse"
16
+ ],
17
+ "author": "Christian Schurr <chris@schurr.dev>",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/milamer/parse-nested-form-data"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/milamer/parse-nested-form-data/issues"
25
+ },
26
+ "homepage": "https://github.com/milamer/parse-nested-form-data#readme",
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "scripts": {
31
+ "clean": "rimraf dist coverage node_modules",
32
+ "build:toc": "npx doctoc .",
33
+ "build": "kcd-scripts build --bundle --environment BUILD_NAME:parseNestedFormData",
34
+ "lint": "kcd-scripts lint",
35
+ "setup": "npm install && npm run validate -s",
36
+ "test": "kcd-scripts test",
37
+ "typecheck": "kcd-scripts typecheck",
38
+ "test:update": "npm test -- --updateSnapshot --coverage",
39
+ "validate": "kcd-scripts validate"
40
+ },
41
+ "dependencies": {
42
+ "@babel/runtime": "^7.19.0"
43
+ },
44
+ "devDependencies": {
45
+ "@remix-run/web-file": "^3.0.2",
46
+ "@remix-run/web-form-data": "^3.0.3",
47
+ "kcd-scripts": "^12.3.0",
48
+ "rimraf": "^3.0.2",
49
+ "typescript": "^4.8.4"
50
+ },
51
+ "peerDependencies": {
52
+ "@remix-run/web-file": ">=3.0.0"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "@remix-run/web-form-data": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "eslintConfig": {
60
+ "extends": "./node_modules/kcd-scripts/eslint.js",
61
+ "rules": {
62
+ "import/extensions": "off",
63
+ "max-lines-per-function": "off",
64
+ "one-var": [
65
+ "error",
66
+ "never"
67
+ ],
68
+ "@typescript-eslint/no-unsafe-assignment": "off",
69
+ "@typescript-eslint/no-loop-func": "off"
70
+ }
71
+ },
72
+ "eslintIgnore": [
73
+ "node_modules",
74
+ "coverage",
75
+ "dist"
76
+ ]
77
+ }