formdata-io 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -134,6 +134,111 @@ const formData = payload({
134
134
  });
135
135
  ```
136
136
 
137
+ #### Base64 Converters
138
+
139
+ Utilities for bidirectional conversion between Files/Blobs and base64 strings, giving you flexibility to choose between FormData/multipart or JSON/base64 approaches.
140
+
141
+ **`fileToBase64(file: File | Blob): Promise<Base64String>`**
142
+
143
+ Converts File or Blob to base64 data URI with MIME type preservation.
144
+
145
+ ```typescript
146
+ import { fileToBase64 } from 'formdata-io/client';
147
+
148
+ const file = new File(['content'], 'doc.txt', { type: 'text/plain' });
149
+ const base64 = await fileToBase64(file);
150
+ // → "data:text/plain;base64,Y29udGVudA=="
151
+
152
+ // Use in JSON API (no FormData/multipart)
153
+ await fetch('/api/user', {
154
+ method: 'POST',
155
+ headers: { 'Content-Type': 'application/json' },
156
+ body: JSON.stringify({ name: 'João', avatar: base64 })
157
+ });
158
+ ```
159
+
160
+ **`base64ToBlob(dataUri: Base64String): Blob`**
161
+
162
+ Converts base64 data URI to Blob with MIME type extraction.
163
+
164
+ ```typescript
165
+ import { base64ToBlob } from 'formdata-io/client';
166
+
167
+ const dataUri = "data:image/png;base64,iVBORw0KG...";
168
+ const blob = base64ToBlob(dataUri);
169
+ // → Blob { type: "image/png", size: 1234 }
170
+ ```
171
+
172
+ **`base64ToFile(dataUri: Base64String, filename: string): File`**
173
+
174
+ Converts base64 data URI to File with filename and metadata.
175
+
176
+ ```typescript
177
+ import { base64ToFile } from 'formdata-io/client';
178
+
179
+ const dataUri = "data:application/pdf;base64,JVBERi0x...";
180
+ const file = base64ToFile(dataUri, 'document.pdf');
181
+ // → File { name: "document.pdf", type: "application/pdf" }
182
+ ```
183
+
184
+ **`blobToFile(blob: Blob, filename: string): File`**
185
+
186
+ Converts Blob to File with specified filename.
187
+
188
+ ```typescript
189
+ import { blobToFile } from 'formdata-io/client';
190
+
191
+ const blob = new Blob(['content'], { type: 'text/plain' });
192
+ const file = blobToFile(blob, 'output.txt');
193
+ // → File { name: "output.txt", type: "text/plain" }
194
+ ```
195
+
196
+ **`fileToBlob(file: File): Blob`**
197
+
198
+ Converts File to Blob for type conversion.
199
+
200
+ ```typescript
201
+ import { fileToBlob } from 'formdata-io/client';
202
+
203
+ const file = new File(['data'], 'file.txt', { type: 'text/plain' });
204
+ const blob = fileToBlob(file);
205
+ // → Blob { type: "text/plain", size: 4 }
206
+ ```
207
+
208
+ **Supported Formats:**
209
+ - ✅ Images: JPEG, PNG, SVG, WebP, GIF
210
+ - ✅ Documents: PDF, DOCX, XLSX, PPTX
211
+ - ✅ Text files: CSV, TXT, JSON, XML
212
+ - ✅ Media: Video (MP4, WebM), Audio (MP3, WAV)
213
+ - ✅ Any Blob/File type with MIME type preservation
214
+
215
+ **Use Cases:**
216
+
217
+ ```typescript
218
+ // Option 1: JSON API (no FormData/multipart)
219
+ import { fileToBase64 } from 'formdata-io/client';
220
+
221
+ const avatar = await fileToBase64(file);
222
+ await fetch('/api/user', {
223
+ method: 'POST',
224
+ headers: { 'Content-Type': 'application/json' },
225
+ body: JSON.stringify({ name: 'João', avatar })
226
+ });
227
+
228
+ // Option 2: Traditional multipart (existing behavior)
229
+ import { payload } from 'formdata-io/client';
230
+
231
+ const formData = payload({ avatar: file });
232
+ await fetch('/api/upload', { method: 'POST', body: formData });
233
+
234
+ // Bidirectional conversion (File → base64 → File roundtrip)
235
+ import { fileToBase64, base64ToFile } from 'formdata-io/client';
236
+
237
+ const original = new File(['content'], 'test.txt', { type: 'text/plain' });
238
+ const base64 = await fileToBase64(original);
239
+ const restored = base64ToFile(base64, 'test.txt');
240
+ ```
241
+
137
242
  ### Server API
138
243
 
139
244
  #### `parser(options?)`
@@ -241,6 +346,13 @@ The `payload()` function converts JavaScript objects to FormData by:
241
346
  3. **Object serialization**: Nested objects are JSON-serialized
242
347
  4. **Type conversion**: Booleans, numbers, dates converted to strings
243
348
 
349
+ **Base64 Converters** provide alternative file handling:
350
+
351
+ 1. **Bidirectional conversion**: File ↔ Base64 ↔ Blob transformations
352
+ 2. **MIME type preservation**: Data URIs maintain original file types
353
+ 3. **JSON API support**: Enable file uploads via JSON payloads
354
+ 4. **Flexibility**: Choose between FormData/multipart or JSON/base64 approaches
355
+
244
356
  ### Server Side
245
357
 
246
358
  The `parser()` middleware uses [busboy](https://github.com/mscdex/busboy) for stream-based parsing:
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Base64-encoded data URI string format
3
+ *
4
+ * @example "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA..."
5
+ */
6
+ type Base64String = `data:${string};base64,${string}`;
1
7
  /**
2
8
  * Supported value types for FormData conversion
3
9
  */
@@ -87,4 +93,75 @@ interface PayloadOptions {
87
93
  */
88
94
  declare function payload(data: FormDataPayload, options?: Partial<PayloadOptions>): FormData;
89
95
 
90
- export { type FormDataPayload, type FormDataValue, type PayloadOptions, payload };
96
+ /**
97
+ * Converts a File or Blob to a base64-encoded data URI string.
98
+ *
99
+ * @param file - The File or Blob to convert
100
+ * @returns Promise resolving to a data URI string (e.g., "data:image/png;base64,...")
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * const file = new File(['content'], 'example.txt', { type: 'text/plain' })
105
+ * const base64 = await fileToBase64(file)
106
+ * // "data:text/plain;base64,Y29udGVudA=="
107
+ * ```
108
+ */
109
+ declare function fileToBase64(file: File | Blob): Promise<Base64String>;
110
+ /**
111
+ * Converts a base64 data URI string to a Blob.
112
+ *
113
+ * @param dataUri - Base64 data URI string (e.g., "data:image/png;base64,...")
114
+ * @returns Blob object with the decoded data and MIME type
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * const blob = base64ToBlob("data:text/plain;base64,Y29udGVudA==")
119
+ * // Blob { type: 'text/plain', size: 7 }
120
+ * ```
121
+ */
122
+ declare function base64ToBlob(dataUri: Base64String): Blob;
123
+ /**
124
+ * Converts a base64 data URI string to a File object.
125
+ *
126
+ * @param dataUri - Base64 data URI string (e.g., "data:image/png;base64,...")
127
+ * @param filename - Name for the created File object
128
+ * @returns File object with the decoded data, MIME type, and filename
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const file = base64ToFile("data:text/plain;base64,Y29udGVudA==", "example.txt")
133
+ * // File { name: 'example.txt', type: 'text/plain', size: 7 }
134
+ * ```
135
+ */
136
+ declare function base64ToFile(dataUri: Base64String, filename: string): File;
137
+ /**
138
+ * Converts a Blob to a File object with a specified filename.
139
+ *
140
+ * @param blob - Blob to convert
141
+ * @param filename - Name for the created File object
142
+ * @returns File object with the same content and MIME type as the Blob
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * const blob = new Blob(['content'], { type: 'text/plain' })
147
+ * const file = blobToFile(blob, 'example.txt')
148
+ * // File { name: 'example.txt', type: 'text/plain', size: 7 }
149
+ * ```
150
+ */
151
+ declare function blobToFile(blob: Blob, filename: string): File;
152
+ /**
153
+ * Converts a File to a Blob (utility function for type conversion).
154
+ *
155
+ * @param file - File to convert
156
+ * @returns Blob with the same content and MIME type as the File
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * const file = new File(['content'], 'example.txt', { type: 'text/plain' })
161
+ * const blob = fileToBlob(file)
162
+ * // Blob { type: 'text/plain', size: 7 }
163
+ * ```
164
+ */
165
+ declare function fileToBlob(file: File): Blob;
166
+
167
+ export { type Base64String, type FormDataPayload, type FormDataValue, type PayloadOptions, base64ToBlob, base64ToFile, blobToFile, fileToBase64, fileToBlob, payload };
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Base64-encoded data URI string format
3
+ *
4
+ * @example "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA..."
5
+ */
6
+ type Base64String = `data:${string};base64,${string}`;
1
7
  /**
2
8
  * Supported value types for FormData conversion
3
9
  */
@@ -87,4 +93,75 @@ interface PayloadOptions {
87
93
  */
88
94
  declare function payload(data: FormDataPayload, options?: Partial<PayloadOptions>): FormData;
89
95
 
90
- export { type FormDataPayload, type FormDataValue, type PayloadOptions, payload };
96
+ /**
97
+ * Converts a File or Blob to a base64-encoded data URI string.
98
+ *
99
+ * @param file - The File or Blob to convert
100
+ * @returns Promise resolving to a data URI string (e.g., "data:image/png;base64,...")
101
+ *
102
+ * @example
103
+ * ```typescript
104
+ * const file = new File(['content'], 'example.txt', { type: 'text/plain' })
105
+ * const base64 = await fileToBase64(file)
106
+ * // "data:text/plain;base64,Y29udGVudA=="
107
+ * ```
108
+ */
109
+ declare function fileToBase64(file: File | Blob): Promise<Base64String>;
110
+ /**
111
+ * Converts a base64 data URI string to a Blob.
112
+ *
113
+ * @param dataUri - Base64 data URI string (e.g., "data:image/png;base64,...")
114
+ * @returns Blob object with the decoded data and MIME type
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * const blob = base64ToBlob("data:text/plain;base64,Y29udGVudA==")
119
+ * // Blob { type: 'text/plain', size: 7 }
120
+ * ```
121
+ */
122
+ declare function base64ToBlob(dataUri: Base64String): Blob;
123
+ /**
124
+ * Converts a base64 data URI string to a File object.
125
+ *
126
+ * @param dataUri - Base64 data URI string (e.g., "data:image/png;base64,...")
127
+ * @param filename - Name for the created File object
128
+ * @returns File object with the decoded data, MIME type, and filename
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const file = base64ToFile("data:text/plain;base64,Y29udGVudA==", "example.txt")
133
+ * // File { name: 'example.txt', type: 'text/plain', size: 7 }
134
+ * ```
135
+ */
136
+ declare function base64ToFile(dataUri: Base64String, filename: string): File;
137
+ /**
138
+ * Converts a Blob to a File object with a specified filename.
139
+ *
140
+ * @param blob - Blob to convert
141
+ * @param filename - Name for the created File object
142
+ * @returns File object with the same content and MIME type as the Blob
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * const blob = new Blob(['content'], { type: 'text/plain' })
147
+ * const file = blobToFile(blob, 'example.txt')
148
+ * // File { name: 'example.txt', type: 'text/plain', size: 7 }
149
+ * ```
150
+ */
151
+ declare function blobToFile(blob: Blob, filename: string): File;
152
+ /**
153
+ * Converts a File to a Blob (utility function for type conversion).
154
+ *
155
+ * @param file - File to convert
156
+ * @returns Blob with the same content and MIME type as the File
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * const file = new File(['content'], 'example.txt', { type: 'text/plain' })
161
+ * const blob = fileToBlob(file)
162
+ * // Blob { type: 'text/plain', size: 7 }
163
+ * ```
164
+ */
165
+ declare function fileToBlob(file: File): Blob;
166
+
167
+ export { type Base64String, type FormDataPayload, type FormDataValue, type PayloadOptions, base64ToBlob, base64ToFile, blobToFile, fileToBase64, fileToBlob, payload };
@@ -81,6 +81,54 @@ function payload(data, options = {}) {
81
81
  return formData;
82
82
  }
83
83
 
84
+ // src/client/converters.ts
85
+ async function fileToBase64(file) {
86
+ return new Promise((resolve, reject) => {
87
+ const reader = new FileReader();
88
+ reader.onload = () => {
89
+ const result = reader.result;
90
+ resolve(result);
91
+ };
92
+ reader.onerror = () => {
93
+ reject(new Error("Failed to read file"));
94
+ };
95
+ reader.readAsDataURL(file);
96
+ });
97
+ }
98
+ function base64ToBlob(dataUri) {
99
+ const parts = dataUri.split(",");
100
+ if (parts.length !== 2) {
101
+ throw new Error("Invalid data URI format");
102
+ }
103
+ const mimeMatch = parts[0].match(/:(.*?);/);
104
+ if (!mimeMatch) {
105
+ throw new Error("Invalid data URI: missing MIME type");
106
+ }
107
+ const mimeType = mimeMatch[1];
108
+ const base64Data = parts[1];
109
+ const binaryString = atob(base64Data);
110
+ const bytes = new Uint8Array(binaryString.length);
111
+ for (let i = 0; i < binaryString.length; i++) {
112
+ bytes[i] = binaryString.charCodeAt(i);
113
+ }
114
+ return new Blob([bytes], { type: mimeType });
115
+ }
116
+ function base64ToFile(dataUri, filename) {
117
+ const blob = base64ToBlob(dataUri);
118
+ return new File([blob], filename, { type: blob.type });
119
+ }
120
+ function blobToFile(blob, filename) {
121
+ return new File([blob], filename, { type: blob.type });
122
+ }
123
+ function fileToBlob(file) {
124
+ return new Blob([file], { type: file.type });
125
+ }
126
+
127
+ exports.base64ToBlob = base64ToBlob;
128
+ exports.base64ToFile = base64ToFile;
129
+ exports.blobToFile = blobToFile;
130
+ exports.fileToBase64 = fileToBase64;
131
+ exports.fileToBlob = fileToBlob;
84
132
  exports.payload = payload;
85
133
  //# sourceMappingURL=index.js.map
86
134
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/utils.ts","../../src/client/payload.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { FormDataValue, FormDataPayload, PayloadOptions } from './types';\n\n/**\n * Checks if a value is a File or Blob object\n *\n * Handles environments where File/Blob may not be defined (e.g., JSDOM, SSR)\n *\n * @param value - Value to check\n * @returns True if value is File or Blob\n */\nexport function isFileOrBlob(value: unknown): value is File | Blob {\n if (typeof File !== 'undefined' && value instanceof File) return true;\n if (typeof Blob !== 'undefined' && value instanceof Blob) return true;\n return false;\n}\n\n/**\n * Checks if a value is a plain object (not File, Blob, Date, Array, or null)\n *\n * Order matters: File/Blob checked before Date since File extends Blob\n *\n * @param value - Value to check\n * @returns True if value is a plain object\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null) return false;\n if (isFileOrBlob(value)) return false;\n if (value instanceof Date) return false;\n if (Array.isArray(value)) return false;\n return true;\n}\n\n/**\n * Converts a primitive value to string according to options\n *\n * @param value - Value to convert\n * @param options - Conversion options\n * @returns String representation or undefined to skip\n */\nexport function valueToString(\n value: FormDataValue,\n options: PayloadOptions\n): string | undefined {\n // null: '' or skip (based on nullsAsUndefineds)\n if (value === null) {\n return options.nullsAsUndefineds ? undefined : '';\n }\n\n // undefined: always skip\n if (value === undefined) {\n return undefined;\n }\n\n // boolean: '1'/'0' or 'true'/'false' (based on booleansAsIntegers)\n if (typeof value === 'boolean') {\n if (options.booleansAsIntegers) {\n return value ? '1' : '0';\n }\n return value.toString();\n }\n\n // Date: ISO 8601 string\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n // number: string representation (handles NaN, Infinity correctly)\n if (typeof value === 'number') {\n return value.toString();\n }\n\n // string: return as-is\n if (typeof value === 'string') {\n return value;\n }\n\n // Shouldn't reach here but return undefined for safety\n return undefined;\n}\n\n/**\n * Builds field name with support for array/object notation\n *\n * @param parentKey - Parent field name\n * @param key - Current key (string or number for arrays)\n * @param options - Configuration options\n * @returns Formatted field name\n *\n * @example\n * ```typescript\n * buildFieldName('', 'name', opts) // 'name'\n * buildFieldName('user', 'email', opts) // 'user[email]'\n * buildFieldName('tags', 0, { indices: false }) // 'tags'\n * buildFieldName('tags', 0, { indices: true }) // 'tags[0]'\n * ```\n */\nexport function buildFieldName(\n parentKey: string,\n key: string | number,\n options: PayloadOptions\n): string {\n // Array index handling\n if (typeof key === 'number') {\n return options.indices ? `${parentKey}[${key}]` : parentKey;\n }\n\n // Object key handling\n return parentKey ? `${parentKey}[${key}]` : key;\n}\n","import type { FormDataPayload, PayloadOptions } from './types';\nimport {\n isFileOrBlob,\n isPlainObject,\n valueToString,\n buildFieldName,\n} from './utils';\n\nconst DEFAULT_OPTIONS: PayloadOptions = {\n indices: false,\n nullsAsUndefineds: false,\n booleansAsIntegers: true,\n};\n\n/**\n * Converts a JavaScript object to FormData\n *\n * @param data - Object to be converted\n * @param options - Configuration options\n * @returns FormData instance ready for submission\n *\n * @example\n * ```typescript\n * const formData = payload({\n * name: \"João Silva\",\n * age: 25,\n * avatar: fileInput.files[0],\n * tags: [\"admin\", \"user\"],\n * metadata: { source: \"web\" }\n * });\n *\n * // Use with fetch\n * fetch('/upload', { method: 'POST', body: formData });\n *\n * // Use with axios\n * axios.post('/upload', formData);\n * ```\n *\n * @example\n * ```typescript\n * // With custom options\n * const formData = payload(data, {\n * indices: true, // tags[0]=admin&tags[1]=user\n * booleansAsIntegers: false // active=true instead of active=1\n * });\n * ```\n */\nexport function payload(\n data: FormDataPayload,\n options: Partial<PayloadOptions> = {}\n): FormData {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n const formData = new FormData();\n\n /**\n * Recursively append values to FormData\n *\n * @param key - Field name\n * @param value - Value to append\n */\n function append(key: string, value: unknown): void {\n // Case 1: File or Blob - direct append (terminal case)\n if (isFileOrBlob(value)) {\n formData.append(key, value);\n return;\n }\n\n // Case 2: Array - recurse with optional indexing\n if (Array.isArray(value)) {\n value.forEach((item, index) => {\n const fieldName = buildFieldName(key, index, opts);\n append(fieldName, item);\n });\n return;\n }\n\n // Case 3: Plain object - serialize as JSON (terminal case)\n if (isPlainObject(value)) {\n const jsonString = JSON.stringify(value);\n formData.append(key, jsonString);\n return;\n }\n\n // Case 4: Primitives (string, number, boolean, null, undefined, Date)\n const stringValue = valueToString(value as any, opts);\n if (stringValue !== undefined) {\n formData.append(key, stringValue);\n }\n }\n\n // Iterate over root-level keys\n Object.entries(data).forEach(([key, value]) => {\n append(key, value);\n });\n\n return formData;\n}\n"]}
1
+ {"version":3,"sources":["../../src/client/utils.ts","../../src/client/payload.ts","../../src/client/converters.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { FormDataValue, FormDataPayload, PayloadOptions } from './types';\n\n/**\n * Checks if a value is a File or Blob object\n *\n * Handles environments where File/Blob may not be defined (e.g., JSDOM, SSR)\n *\n * @param value - Value to check\n * @returns True if value is File or Blob\n */\nexport function isFileOrBlob(value: unknown): value is File | Blob {\n if (typeof File !== 'undefined' && value instanceof File) return true;\n if (typeof Blob !== 'undefined' && value instanceof Blob) return true;\n return false;\n}\n\n/**\n * Checks if a value is a plain object (not File, Blob, Date, Array, or null)\n *\n * Order matters: File/Blob checked before Date since File extends Blob\n *\n * @param value - Value to check\n * @returns True if value is a plain object\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null) return false;\n if (isFileOrBlob(value)) return false;\n if (value instanceof Date) return false;\n if (Array.isArray(value)) return false;\n return true;\n}\n\n/**\n * Converts a primitive value to string according to options\n *\n * @param value - Value to convert\n * @param options - Conversion options\n * @returns String representation or undefined to skip\n */\nexport function valueToString(\n value: FormDataValue,\n options: PayloadOptions\n): string | undefined {\n // null: '' or skip (based on nullsAsUndefineds)\n if (value === null) {\n return options.nullsAsUndefineds ? undefined : '';\n }\n\n // undefined: always skip\n if (value === undefined) {\n return undefined;\n }\n\n // boolean: '1'/'0' or 'true'/'false' (based on booleansAsIntegers)\n if (typeof value === 'boolean') {\n if (options.booleansAsIntegers) {\n return value ? '1' : '0';\n }\n return value.toString();\n }\n\n // Date: ISO 8601 string\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n // number: string representation (handles NaN, Infinity correctly)\n if (typeof value === 'number') {\n return value.toString();\n }\n\n // string: return as-is\n if (typeof value === 'string') {\n return value;\n }\n\n // Shouldn't reach here but return undefined for safety\n return undefined;\n}\n\n/**\n * Builds field name with support for array/object notation\n *\n * @param parentKey - Parent field name\n * @param key - Current key (string or number for arrays)\n * @param options - Configuration options\n * @returns Formatted field name\n *\n * @example\n * ```typescript\n * buildFieldName('', 'name', opts) // 'name'\n * buildFieldName('user', 'email', opts) // 'user[email]'\n * buildFieldName('tags', 0, { indices: false }) // 'tags'\n * buildFieldName('tags', 0, { indices: true }) // 'tags[0]'\n * ```\n */\nexport function buildFieldName(\n parentKey: string,\n key: string | number,\n options: PayloadOptions\n): string {\n // Array index handling\n if (typeof key === 'number') {\n return options.indices ? `${parentKey}[${key}]` : parentKey;\n }\n\n // Object key handling\n return parentKey ? `${parentKey}[${key}]` : key;\n}\n","import type { FormDataPayload, PayloadOptions } from './types';\nimport {\n isFileOrBlob,\n isPlainObject,\n valueToString,\n buildFieldName,\n} from './utils';\n\nconst DEFAULT_OPTIONS: PayloadOptions = {\n indices: false,\n nullsAsUndefineds: false,\n booleansAsIntegers: true,\n};\n\n/**\n * Converts a JavaScript object to FormData\n *\n * @param data - Object to be converted\n * @param options - Configuration options\n * @returns FormData instance ready for submission\n *\n * @example\n * ```typescript\n * const formData = payload({\n * name: \"João Silva\",\n * age: 25,\n * avatar: fileInput.files[0],\n * tags: [\"admin\", \"user\"],\n * metadata: { source: \"web\" }\n * });\n *\n * // Use with fetch\n * fetch('/upload', { method: 'POST', body: formData });\n *\n * // Use with axios\n * axios.post('/upload', formData);\n * ```\n *\n * @example\n * ```typescript\n * // With custom options\n * const formData = payload(data, {\n * indices: true, // tags[0]=admin&tags[1]=user\n * booleansAsIntegers: false // active=true instead of active=1\n * });\n * ```\n */\nexport function payload(\n data: FormDataPayload,\n options: Partial<PayloadOptions> = {}\n): FormData {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n const formData = new FormData();\n\n /**\n * Recursively append values to FormData\n *\n * @param key - Field name\n * @param value - Value to append\n */\n function append(key: string, value: unknown): void {\n // Case 1: File or Blob - direct append (terminal case)\n if (isFileOrBlob(value)) {\n formData.append(key, value);\n return;\n }\n\n // Case 2: Array - recurse with optional indexing\n if (Array.isArray(value)) {\n value.forEach((item, index) => {\n const fieldName = buildFieldName(key, index, opts);\n append(fieldName, item);\n });\n return;\n }\n\n // Case 3: Plain object - serialize as JSON (terminal case)\n if (isPlainObject(value)) {\n const jsonString = JSON.stringify(value);\n formData.append(key, jsonString);\n return;\n }\n\n // Case 4: Primitives (string, number, boolean, null, undefined, Date)\n const stringValue = valueToString(value as any, opts);\n if (stringValue !== undefined) {\n formData.append(key, stringValue);\n }\n }\n\n // Iterate over root-level keys\n Object.entries(data).forEach(([key, value]) => {\n append(key, value);\n });\n\n return formData;\n}\n","import type { Base64String } from './types'\n\n/**\n * Converts a File or Blob to a base64-encoded data URI string.\n *\n * @param file - The File or Blob to convert\n * @returns Promise resolving to a data URI string (e.g., \"data:image/png;base64,...\")\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'example.txt', { type: 'text/plain' })\n * const base64 = await fileToBase64(file)\n * // \"data:text/plain;base64,Y29udGVudA==\"\n * ```\n */\nexport async function fileToBase64(file: File | Blob): Promise<Base64String> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader()\n\n reader.onload = () => {\n const result = reader.result as string\n resolve(result as Base64String)\n }\n\n reader.onerror = () => {\n reject(new Error('Failed to read file'))\n }\n\n reader.readAsDataURL(file)\n })\n}\n\n/**\n * Converts a base64 data URI string to a Blob.\n *\n * @param dataUri - Base64 data URI string (e.g., \"data:image/png;base64,...\")\n * @returns Blob object with the decoded data and MIME type\n *\n * @example\n * ```typescript\n * const blob = base64ToBlob(\"data:text/plain;base64,Y29udGVudA==\")\n * // Blob { type: 'text/plain', size: 7 }\n * ```\n */\nexport function base64ToBlob(dataUri: Base64String): Blob {\n // Extract MIME type and base64 data\n const parts = dataUri.split(',')\n if (parts.length !== 2) {\n throw new Error('Invalid data URI format')\n }\n\n const mimeMatch = parts[0].match(/:(.*?);/)\n if (!mimeMatch) {\n throw new Error('Invalid data URI: missing MIME type')\n }\n\n const mimeType = mimeMatch[1]\n const base64Data = parts[1]\n\n // Decode base64 to binary\n const binaryString = atob(base64Data)\n const bytes = new Uint8Array(binaryString.length)\n\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i)\n }\n\n return new Blob([bytes], { type: mimeType })\n}\n\n/**\n * Converts a base64 data URI string to a File object.\n *\n * @param dataUri - Base64 data URI string (e.g., \"data:image/png;base64,...\")\n * @param filename - Name for the created File object\n * @returns File object with the decoded data, MIME type, and filename\n *\n * @example\n * ```typescript\n * const file = base64ToFile(\"data:text/plain;base64,Y29udGVudA==\", \"example.txt\")\n * // File { name: 'example.txt', type: 'text/plain', size: 7 }\n * ```\n */\nexport function base64ToFile(dataUri: Base64String, filename: string): File {\n const blob = base64ToBlob(dataUri)\n return new File([blob], filename, { type: blob.type })\n}\n\n/**\n * Converts a Blob to a File object with a specified filename.\n *\n * @param blob - Blob to convert\n * @param filename - Name for the created File object\n * @returns File object with the same content and MIME type as the Blob\n *\n * @example\n * ```typescript\n * const blob = new Blob(['content'], { type: 'text/plain' })\n * const file = blobToFile(blob, 'example.txt')\n * // File { name: 'example.txt', type: 'text/plain', size: 7 }\n * ```\n */\nexport function blobToFile(blob: Blob, filename: string): File {\n return new File([blob], filename, { type: blob.type })\n}\n\n/**\n * Converts a File to a Blob (utility function for type conversion).\n *\n * @param file - File to convert\n * @returns Blob with the same content and MIME type as the File\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'example.txt', { type: 'text/plain' })\n * const blob = fileToBlob(file)\n * // Blob { type: 'text/plain', size: 7 }\n * ```\n */\nexport function fileToBlob(file: File): Blob {\n return new Blob([file], { type: file.type })\n}\n"]}
@@ -79,6 +79,49 @@ function payload(data, options = {}) {
79
79
  return formData;
80
80
  }
81
81
 
82
- export { payload };
82
+ // src/client/converters.ts
83
+ async function fileToBase64(file) {
84
+ return new Promise((resolve, reject) => {
85
+ const reader = new FileReader();
86
+ reader.onload = () => {
87
+ const result = reader.result;
88
+ resolve(result);
89
+ };
90
+ reader.onerror = () => {
91
+ reject(new Error("Failed to read file"));
92
+ };
93
+ reader.readAsDataURL(file);
94
+ });
95
+ }
96
+ function base64ToBlob(dataUri) {
97
+ const parts = dataUri.split(",");
98
+ if (parts.length !== 2) {
99
+ throw new Error("Invalid data URI format");
100
+ }
101
+ const mimeMatch = parts[0].match(/:(.*?);/);
102
+ if (!mimeMatch) {
103
+ throw new Error("Invalid data URI: missing MIME type");
104
+ }
105
+ const mimeType = mimeMatch[1];
106
+ const base64Data = parts[1];
107
+ const binaryString = atob(base64Data);
108
+ const bytes = new Uint8Array(binaryString.length);
109
+ for (let i = 0; i < binaryString.length; i++) {
110
+ bytes[i] = binaryString.charCodeAt(i);
111
+ }
112
+ return new Blob([bytes], { type: mimeType });
113
+ }
114
+ function base64ToFile(dataUri, filename) {
115
+ const blob = base64ToBlob(dataUri);
116
+ return new File([blob], filename, { type: blob.type });
117
+ }
118
+ function blobToFile(blob, filename) {
119
+ return new File([blob], filename, { type: blob.type });
120
+ }
121
+ function fileToBlob(file) {
122
+ return new Blob([file], { type: file.type });
123
+ }
124
+
125
+ export { base64ToBlob, base64ToFile, blobToFile, fileToBase64, fileToBlob, payload };
83
126
  //# sourceMappingURL=index.mjs.map
84
127
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/utils.ts","../../src/client/payload.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { FormDataValue, FormDataPayload, PayloadOptions } from './types';\n\n/**\n * Checks if a value is a File or Blob object\n *\n * Handles environments where File/Blob may not be defined (e.g., JSDOM, SSR)\n *\n * @param value - Value to check\n * @returns True if value is File or Blob\n */\nexport function isFileOrBlob(value: unknown): value is File | Blob {\n if (typeof File !== 'undefined' && value instanceof File) return true;\n if (typeof Blob !== 'undefined' && value instanceof Blob) return true;\n return false;\n}\n\n/**\n * Checks if a value is a plain object (not File, Blob, Date, Array, or null)\n *\n * Order matters: File/Blob checked before Date since File extends Blob\n *\n * @param value - Value to check\n * @returns True if value is a plain object\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null) return false;\n if (isFileOrBlob(value)) return false;\n if (value instanceof Date) return false;\n if (Array.isArray(value)) return false;\n return true;\n}\n\n/**\n * Converts a primitive value to string according to options\n *\n * @param value - Value to convert\n * @param options - Conversion options\n * @returns String representation or undefined to skip\n */\nexport function valueToString(\n value: FormDataValue,\n options: PayloadOptions\n): string | undefined {\n // null: '' or skip (based on nullsAsUndefineds)\n if (value === null) {\n return options.nullsAsUndefineds ? undefined : '';\n }\n\n // undefined: always skip\n if (value === undefined) {\n return undefined;\n }\n\n // boolean: '1'/'0' or 'true'/'false' (based on booleansAsIntegers)\n if (typeof value === 'boolean') {\n if (options.booleansAsIntegers) {\n return value ? '1' : '0';\n }\n return value.toString();\n }\n\n // Date: ISO 8601 string\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n // number: string representation (handles NaN, Infinity correctly)\n if (typeof value === 'number') {\n return value.toString();\n }\n\n // string: return as-is\n if (typeof value === 'string') {\n return value;\n }\n\n // Shouldn't reach here but return undefined for safety\n return undefined;\n}\n\n/**\n * Builds field name with support for array/object notation\n *\n * @param parentKey - Parent field name\n * @param key - Current key (string or number for arrays)\n * @param options - Configuration options\n * @returns Formatted field name\n *\n * @example\n * ```typescript\n * buildFieldName('', 'name', opts) // 'name'\n * buildFieldName('user', 'email', opts) // 'user[email]'\n * buildFieldName('tags', 0, { indices: false }) // 'tags'\n * buildFieldName('tags', 0, { indices: true }) // 'tags[0]'\n * ```\n */\nexport function buildFieldName(\n parentKey: string,\n key: string | number,\n options: PayloadOptions\n): string {\n // Array index handling\n if (typeof key === 'number') {\n return options.indices ? `${parentKey}[${key}]` : parentKey;\n }\n\n // Object key handling\n return parentKey ? `${parentKey}[${key}]` : key;\n}\n","import type { FormDataPayload, PayloadOptions } from './types';\nimport {\n isFileOrBlob,\n isPlainObject,\n valueToString,\n buildFieldName,\n} from './utils';\n\nconst DEFAULT_OPTIONS: PayloadOptions = {\n indices: false,\n nullsAsUndefineds: false,\n booleansAsIntegers: true,\n};\n\n/**\n * Converts a JavaScript object to FormData\n *\n * @param data - Object to be converted\n * @param options - Configuration options\n * @returns FormData instance ready for submission\n *\n * @example\n * ```typescript\n * const formData = payload({\n * name: \"João Silva\",\n * age: 25,\n * avatar: fileInput.files[0],\n * tags: [\"admin\", \"user\"],\n * metadata: { source: \"web\" }\n * });\n *\n * // Use with fetch\n * fetch('/upload', { method: 'POST', body: formData });\n *\n * // Use with axios\n * axios.post('/upload', formData);\n * ```\n *\n * @example\n * ```typescript\n * // With custom options\n * const formData = payload(data, {\n * indices: true, // tags[0]=admin&tags[1]=user\n * booleansAsIntegers: false // active=true instead of active=1\n * });\n * ```\n */\nexport function payload(\n data: FormDataPayload,\n options: Partial<PayloadOptions> = {}\n): FormData {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n const formData = new FormData();\n\n /**\n * Recursively append values to FormData\n *\n * @param key - Field name\n * @param value - Value to append\n */\n function append(key: string, value: unknown): void {\n // Case 1: File or Blob - direct append (terminal case)\n if (isFileOrBlob(value)) {\n formData.append(key, value);\n return;\n }\n\n // Case 2: Array - recurse with optional indexing\n if (Array.isArray(value)) {\n value.forEach((item, index) => {\n const fieldName = buildFieldName(key, index, opts);\n append(fieldName, item);\n });\n return;\n }\n\n // Case 3: Plain object - serialize as JSON (terminal case)\n if (isPlainObject(value)) {\n const jsonString = JSON.stringify(value);\n formData.append(key, jsonString);\n return;\n }\n\n // Case 4: Primitives (string, number, boolean, null, undefined, Date)\n const stringValue = valueToString(value as any, opts);\n if (stringValue !== undefined) {\n formData.append(key, stringValue);\n }\n }\n\n // Iterate over root-level keys\n Object.entries(data).forEach(([key, value]) => {\n append(key, value);\n });\n\n return formData;\n}\n"]}
1
+ {"version":3,"sources":["../../src/client/utils.ts","../../src/client/payload.ts","../../src/client/converters.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { FormDataValue, FormDataPayload, PayloadOptions } from './types';\n\n/**\n * Checks if a value is a File or Blob object\n *\n * Handles environments where File/Blob may not be defined (e.g., JSDOM, SSR)\n *\n * @param value - Value to check\n * @returns True if value is File or Blob\n */\nexport function isFileOrBlob(value: unknown): value is File | Blob {\n if (typeof File !== 'undefined' && value instanceof File) return true;\n if (typeof Blob !== 'undefined' && value instanceof Blob) return true;\n return false;\n}\n\n/**\n * Checks if a value is a plain object (not File, Blob, Date, Array, or null)\n *\n * Order matters: File/Blob checked before Date since File extends Blob\n *\n * @param value - Value to check\n * @returns True if value is a plain object\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null) return false;\n if (isFileOrBlob(value)) return false;\n if (value instanceof Date) return false;\n if (Array.isArray(value)) return false;\n return true;\n}\n\n/**\n * Converts a primitive value to string according to options\n *\n * @param value - Value to convert\n * @param options - Conversion options\n * @returns String representation or undefined to skip\n */\nexport function valueToString(\n value: FormDataValue,\n options: PayloadOptions\n): string | undefined {\n // null: '' or skip (based on nullsAsUndefineds)\n if (value === null) {\n return options.nullsAsUndefineds ? undefined : '';\n }\n\n // undefined: always skip\n if (value === undefined) {\n return undefined;\n }\n\n // boolean: '1'/'0' or 'true'/'false' (based on booleansAsIntegers)\n if (typeof value === 'boolean') {\n if (options.booleansAsIntegers) {\n return value ? '1' : '0';\n }\n return value.toString();\n }\n\n // Date: ISO 8601 string\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n // number: string representation (handles NaN, Infinity correctly)\n if (typeof value === 'number') {\n return value.toString();\n }\n\n // string: return as-is\n if (typeof value === 'string') {\n return value;\n }\n\n // Shouldn't reach here but return undefined for safety\n return undefined;\n}\n\n/**\n * Builds field name with support for array/object notation\n *\n * @param parentKey - Parent field name\n * @param key - Current key (string or number for arrays)\n * @param options - Configuration options\n * @returns Formatted field name\n *\n * @example\n * ```typescript\n * buildFieldName('', 'name', opts) // 'name'\n * buildFieldName('user', 'email', opts) // 'user[email]'\n * buildFieldName('tags', 0, { indices: false }) // 'tags'\n * buildFieldName('tags', 0, { indices: true }) // 'tags[0]'\n * ```\n */\nexport function buildFieldName(\n parentKey: string,\n key: string | number,\n options: PayloadOptions\n): string {\n // Array index handling\n if (typeof key === 'number') {\n return options.indices ? `${parentKey}[${key}]` : parentKey;\n }\n\n // Object key handling\n return parentKey ? `${parentKey}[${key}]` : key;\n}\n","import type { FormDataPayload, PayloadOptions } from './types';\nimport {\n isFileOrBlob,\n isPlainObject,\n valueToString,\n buildFieldName,\n} from './utils';\n\nconst DEFAULT_OPTIONS: PayloadOptions = {\n indices: false,\n nullsAsUndefineds: false,\n booleansAsIntegers: true,\n};\n\n/**\n * Converts a JavaScript object to FormData\n *\n * @param data - Object to be converted\n * @param options - Configuration options\n * @returns FormData instance ready for submission\n *\n * @example\n * ```typescript\n * const formData = payload({\n * name: \"João Silva\",\n * age: 25,\n * avatar: fileInput.files[0],\n * tags: [\"admin\", \"user\"],\n * metadata: { source: \"web\" }\n * });\n *\n * // Use with fetch\n * fetch('/upload', { method: 'POST', body: formData });\n *\n * // Use with axios\n * axios.post('/upload', formData);\n * ```\n *\n * @example\n * ```typescript\n * // With custom options\n * const formData = payload(data, {\n * indices: true, // tags[0]=admin&tags[1]=user\n * booleansAsIntegers: false // active=true instead of active=1\n * });\n * ```\n */\nexport function payload(\n data: FormDataPayload,\n options: Partial<PayloadOptions> = {}\n): FormData {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n const formData = new FormData();\n\n /**\n * Recursively append values to FormData\n *\n * @param key - Field name\n * @param value - Value to append\n */\n function append(key: string, value: unknown): void {\n // Case 1: File or Blob - direct append (terminal case)\n if (isFileOrBlob(value)) {\n formData.append(key, value);\n return;\n }\n\n // Case 2: Array - recurse with optional indexing\n if (Array.isArray(value)) {\n value.forEach((item, index) => {\n const fieldName = buildFieldName(key, index, opts);\n append(fieldName, item);\n });\n return;\n }\n\n // Case 3: Plain object - serialize as JSON (terminal case)\n if (isPlainObject(value)) {\n const jsonString = JSON.stringify(value);\n formData.append(key, jsonString);\n return;\n }\n\n // Case 4: Primitives (string, number, boolean, null, undefined, Date)\n const stringValue = valueToString(value as any, opts);\n if (stringValue !== undefined) {\n formData.append(key, stringValue);\n }\n }\n\n // Iterate over root-level keys\n Object.entries(data).forEach(([key, value]) => {\n append(key, value);\n });\n\n return formData;\n}\n","import type { Base64String } from './types'\n\n/**\n * Converts a File or Blob to a base64-encoded data URI string.\n *\n * @param file - The File or Blob to convert\n * @returns Promise resolving to a data URI string (e.g., \"data:image/png;base64,...\")\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'example.txt', { type: 'text/plain' })\n * const base64 = await fileToBase64(file)\n * // \"data:text/plain;base64,Y29udGVudA==\"\n * ```\n */\nexport async function fileToBase64(file: File | Blob): Promise<Base64String> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader()\n\n reader.onload = () => {\n const result = reader.result as string\n resolve(result as Base64String)\n }\n\n reader.onerror = () => {\n reject(new Error('Failed to read file'))\n }\n\n reader.readAsDataURL(file)\n })\n}\n\n/**\n * Converts a base64 data URI string to a Blob.\n *\n * @param dataUri - Base64 data URI string (e.g., \"data:image/png;base64,...\")\n * @returns Blob object with the decoded data and MIME type\n *\n * @example\n * ```typescript\n * const blob = base64ToBlob(\"data:text/plain;base64,Y29udGVudA==\")\n * // Blob { type: 'text/plain', size: 7 }\n * ```\n */\nexport function base64ToBlob(dataUri: Base64String): Blob {\n // Extract MIME type and base64 data\n const parts = dataUri.split(',')\n if (parts.length !== 2) {\n throw new Error('Invalid data URI format')\n }\n\n const mimeMatch = parts[0].match(/:(.*?);/)\n if (!mimeMatch) {\n throw new Error('Invalid data URI: missing MIME type')\n }\n\n const mimeType = mimeMatch[1]\n const base64Data = parts[1]\n\n // Decode base64 to binary\n const binaryString = atob(base64Data)\n const bytes = new Uint8Array(binaryString.length)\n\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i)\n }\n\n return new Blob([bytes], { type: mimeType })\n}\n\n/**\n * Converts a base64 data URI string to a File object.\n *\n * @param dataUri - Base64 data URI string (e.g., \"data:image/png;base64,...\")\n * @param filename - Name for the created File object\n * @returns File object with the decoded data, MIME type, and filename\n *\n * @example\n * ```typescript\n * const file = base64ToFile(\"data:text/plain;base64,Y29udGVudA==\", \"example.txt\")\n * // File { name: 'example.txt', type: 'text/plain', size: 7 }\n * ```\n */\nexport function base64ToFile(dataUri: Base64String, filename: string): File {\n const blob = base64ToBlob(dataUri)\n return new File([blob], filename, { type: blob.type })\n}\n\n/**\n * Converts a Blob to a File object with a specified filename.\n *\n * @param blob - Blob to convert\n * @param filename - Name for the created File object\n * @returns File object with the same content and MIME type as the Blob\n *\n * @example\n * ```typescript\n * const blob = new Blob(['content'], { type: 'text/plain' })\n * const file = blobToFile(blob, 'example.txt')\n * // File { name: 'example.txt', type: 'text/plain', size: 7 }\n * ```\n */\nexport function blobToFile(blob: Blob, filename: string): File {\n return new File([blob], filename, { type: blob.type })\n}\n\n/**\n * Converts a File to a Blob (utility function for type conversion).\n *\n * @param file - File to convert\n * @returns Blob with the same content and MIME type as the File\n *\n * @example\n * ```typescript\n * const file = new File(['content'], 'example.txt', { type: 'text/plain' })\n * const blob = fileToBlob(file)\n * // Blob { type: 'text/plain', size: 7 }\n * ```\n */\nexport function fileToBlob(file: File): Blob {\n return new Blob([file], { type: file.type })\n}\n"]}
@@ -15,6 +15,18 @@ var DEFAULT_OPTIONS = {
15
15
  autoParseNumbers: true,
16
16
  autoParseBooleans: true
17
17
  };
18
+ function normalizeField(payload, fieldname, value) {
19
+ const existing = payload[fieldname];
20
+ if (existing !== void 0) {
21
+ if (Array.isArray(existing)) {
22
+ existing.push(value);
23
+ } else {
24
+ payload[fieldname] = [existing, value];
25
+ }
26
+ } else {
27
+ payload[fieldname] = value;
28
+ }
29
+ }
18
30
  function autoParse(value, options) {
19
31
  if (options.autoParseJSON) {
20
32
  if (value.startsWith("{") && value.endsWith("}") || value.startsWith("[") && value.endsWith("]")) {
@@ -44,6 +56,11 @@ function parseMultipart(req, options = {}) {
44
56
  const payload = {};
45
57
  const files = [];
46
58
  let fileCount = 0;
59
+ let hasError = false;
60
+ const contentType = req.headers["content-type"];
61
+ if (!contentType || typeof contentType !== "string") {
62
+ return reject(new Error("Missing or invalid content-type header"));
63
+ }
47
64
  const busboy = Busboy__default.default({
48
65
  headers: req.headers,
49
66
  limits: {
@@ -53,52 +70,58 @@ function parseMultipart(req, options = {}) {
53
70
  });
54
71
  busboy.on("field", (fieldname, value) => {
55
72
  const parsedValue = autoParse(value, opts);
56
- if (payload[fieldname] !== void 0) {
57
- if (Array.isArray(payload[fieldname])) {
58
- payload[fieldname].push(parsedValue);
59
- } else {
60
- payload[fieldname] = [payload[fieldname], parsedValue];
61
- }
62
- } else {
63
- payload[fieldname] = parsedValue;
64
- }
73
+ normalizeField(payload, fieldname, parsedValue);
65
74
  });
66
75
  busboy.on(
67
76
  "file",
68
77
  (fieldname, file, info) => {
69
78
  fileCount++;
70
79
  if (fileCount > opts.maxFiles) {
80
+ hasError = true;
71
81
  file.resume();
72
- return reject(
82
+ reject(
73
83
  new Error(`Maximum number of files (${opts.maxFiles}) exceeded`)
74
84
  );
85
+ return;
75
86
  }
76
87
  const chunks = [];
77
88
  let size = 0;
89
+ let fileSizeExceeded = false;
78
90
  file.on("data", (chunk) => {
91
+ if (hasError || fileSizeExceeded) {
92
+ return;
93
+ }
79
94
  size += chunk.length;
80
95
  if (size > opts.maxFileSize) {
96
+ fileSizeExceeded = true;
97
+ hasError = true;
81
98
  file.resume();
82
- return reject(
99
+ reject(
83
100
  new Error(
84
101
  `File size exceeds limit of ${opts.maxFileSize} bytes`
85
102
  )
86
103
  );
104
+ return;
87
105
  }
88
106
  chunks.push(chunk);
89
107
  });
90
108
  file.on("end", () => {
91
- const parsedFile = {
92
- fieldname,
93
- originalname: info.filename,
94
- encoding: info.encoding,
95
- mimetype: info.mimeType,
96
- size,
97
- buffer: Buffer.concat(chunks)
98
- };
99
- files.push(parsedFile);
109
+ if (!hasError && !fileSizeExceeded) {
110
+ const parsedFile = {
111
+ fieldname,
112
+ originalname: info.filename,
113
+ encoding: info.encoding,
114
+ mimetype: info.mimeType,
115
+ size,
116
+ buffer: Buffer.concat(chunks)
117
+ };
118
+ files.push(parsedFile);
119
+ }
120
+ });
121
+ file.on("error", (err) => {
122
+ hasError = true;
123
+ reject(err);
100
124
  });
101
- file.on("error", reject);
102
125
  }
103
126
  );
104
127
  busboy.on("limit", () => {
@@ -106,15 +129,7 @@ function parseMultipart(req, options = {}) {
106
129
  });
107
130
  busboy.on("finish", () => {
108
131
  files.forEach((file) => {
109
- if (payload[file.fieldname] !== void 0) {
110
- if (Array.isArray(payload[file.fieldname])) {
111
- payload[file.fieldname].push(file);
112
- } else {
113
- payload[file.fieldname] = [payload[file.fieldname], file];
114
- }
115
- } else {
116
- payload[file.fieldname] = file;
117
- }
132
+ normalizeField(payload, file.fieldname, file);
118
133
  });
119
134
  resolve(payload);
120
135
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/parser.ts","../../src/server/middleware.ts"],"names":["Busboy"],"mappings":"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","file":"index.js","sourcesContent":["import Busboy from 'busboy';\nimport type { Request } from 'express';\nimport type { ParsedFile, ParsedPayload, ParserOptions } from './types';\n\nconst DEFAULT_OPTIONS: Required<ParserOptions> = {\n maxFileSize: 10 * 1024 * 1024, // 10MB\n maxFiles: 10,\n autoParseJSON: true,\n autoParseNumbers: true,\n autoParseBooleans: true,\n};\n\n/**\n * Attempts to automatically parse a string value to appropriate type\n *\n * @param value - String value to parse\n * @param options - Parser options\n * @returns Parsed value (JSON object, number, boolean, or original string)\n */\nfunction autoParse(\n value: string,\n options: Required<ParserOptions>\n): string | number | boolean | object {\n // 1. Try JSON parsing (if starts/ends with {} or [])\n if (options.autoParseJSON) {\n if (\n (value.startsWith('{') && value.endsWith('}')) ||\n (value.startsWith('[') && value.endsWith(']'))\n ) {\n try {\n return JSON.parse(value);\n } catch {\n // Fallback to string if not valid JSON\n }\n }\n }\n\n // 2. Try boolean conversion\n if (options.autoParseBooleans) {\n if (value === 'true') return true;\n if (value === 'false') return false;\n if (value === '1') return true;\n if (value === '0') return false;\n }\n\n // 3. Try number conversion\n if (options.autoParseNumbers) {\n const num = Number(value);\n if (!isNaN(num) && value.trim() !== '') {\n return num;\n }\n }\n\n // 4. Return as string\n return value;\n}\n\n/**\n * Parses multipart/form-data request using busboy\n *\n * @param req - Express request object\n * @param options - Parser configuration options\n * @returns Promise resolving to normalized payload\n *\n * @example\n * ```typescript\n * const payload = await parseMultipart(req, {\n * maxFileSize: 5 * 1024 * 1024, // 5MB\n * maxFiles: 5\n * });\n *\n * console.log(payload.name); // Auto-parsed text field\n * console.log(payload.avatar); // ParsedFile object\n * ```\n */\nexport function parseMultipart(\n req: Request,\n options: Partial<ParserOptions> = {}\n): Promise<ParsedPayload> {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n return new Promise((resolve, reject) => {\n const payload: ParsedPayload = {};\n const files: ParsedFile[] = [];\n let fileCount = 0;\n\n const busboy = Busboy({\n headers: req.headers as any,\n limits: {\n fileSize: opts.maxFileSize,\n files: opts.maxFiles,\n },\n });\n\n // Handler for text fields\n busboy.on('field', (fieldname: string, value: string) => {\n const parsedValue = autoParse(value, opts);\n\n // If field already exists, convert to array\n if (payload[fieldname] !== undefined) {\n if (Array.isArray(payload[fieldname])) {\n (payload[fieldname] as any[]).push(parsedValue);\n } else {\n payload[fieldname] = [payload[fieldname], parsedValue];\n }\n } else {\n payload[fieldname] = parsedValue;\n }\n });\n\n // Handler for file uploads\n busboy.on(\n 'file',\n (\n fieldname: string,\n file: NodeJS.ReadableStream,\n info: { filename: string; encoding: string; mimeType: string }\n ) => {\n fileCount++;\n\n // Enforce file count limit\n if (fileCount > opts.maxFiles) {\n file.resume(); // CRITICAL: drain stream to prevent backpressure\n return reject(\n new Error(`Maximum number of files (${opts.maxFiles}) exceeded`)\n );\n }\n\n const chunks: Buffer[] = [];\n let size = 0;\n\n file.on('data', (chunk: Buffer) => {\n size += chunk.length;\n\n // Enforce size limit\n if (size > opts.maxFileSize) {\n file.resume(); // CRITICAL: drain stream\n return reject(\n new Error(\n `File size exceeds limit of ${opts.maxFileSize} bytes`\n )\n );\n }\n\n chunks.push(chunk);\n });\n\n file.on('end', () => {\n const parsedFile: ParsedFile = {\n fieldname,\n originalname: info.filename,\n encoding: info.encoding,\n mimetype: info.mimeType,\n size,\n buffer: Buffer.concat(chunks),\n };\n\n files.push(parsedFile);\n });\n\n file.on('error', reject);\n }\n );\n\n // Handler for limit exceeded\n busboy.on('limit', () => {\n reject(new Error('File size limit exceeded'));\n });\n\n // Handler for parsing completion\n busboy.on('finish', () => {\n // Normalize files into payload\n files.forEach((file) => {\n if (payload[file.fieldname] !== undefined) {\n // Field already exists, convert to array\n if (Array.isArray(payload[file.fieldname])) {\n (payload[file.fieldname] as ParsedFile[]).push(file);\n } else {\n payload[file.fieldname] = [payload[file.fieldname] as ParsedFile, file];\n }\n } else {\n payload[file.fieldname] = file;\n }\n });\n\n resolve(payload);\n });\n\n // Handler for parsing errors\n busboy.on('error', reject);\n\n // Pipe request stream to busboy\n req.pipe(busboy);\n });\n}\n","import type { Request, Response, NextFunction } from 'express';\nimport { parseMultipart } from './parser';\nimport type { ParserOptions, MiddlewareFunction } from './types';\n\n/**\n * Creates Express middleware for parsing multipart/form-data\n *\n * @param options - Configuration options\n * @returns Express middleware function\n *\n * @example\n * ```typescript\n * import express from 'express';\n * import { parser } from 'formdata-io/server';\n *\n * const app = express();\n *\n * // Use with default options (10MB, 10 files)\n * app.post('/upload', parser(), (req, res) => {\n * const { name, avatar } = req.payload;\n * res.json({ ok: true });\n * });\n *\n * // Use with custom options\n * app.post('/photos', parser({ maxFileSize: 5 * 1024 * 1024 }), (req, res) => {\n * const photos = req.payload?.photos;\n * res.json({ count: Array.isArray(photos) ? photos.length : 1 });\n * });\n * ```\n */\nexport function parser(options: ParserOptions = {}): MiddlewareFunction {\n return async (req: Request, res: Response, next: NextFunction) => {\n // Only process multipart/form-data requests\n const contentType = req.headers['content-type'] || '';\n if (!contentType.includes('multipart/form-data')) {\n return next();\n }\n\n try {\n req.payload = await parseMultipart(req, options);\n next();\n } catch (error) {\n // Pass error to Express error handler\n next(error);\n }\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/server/parser.ts","../../src/server/middleware.ts"],"names":["Busboy"],"mappings":"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","file":"index.js","sourcesContent":["import Busboy from 'busboy';\nimport type { Request } from 'express';\nimport type { ParsedFile, ParsedPayload, ParserOptions } from './types';\n\nconst DEFAULT_OPTIONS: Required<ParserOptions> = {\n maxFileSize: 10 * 1024 * 1024, // 10MB\n maxFiles: 10,\n autoParseJSON: true,\n autoParseNumbers: true,\n autoParseBooleans: true,\n};\n\n/**\n * Type representing a single parsed value (not an array)\n */\ntype ParsedValue = string | number | boolean | object | ParsedFile;\n\n/**\n * Normalizes a field into the payload, converting to array if needed\n *\n * @param payload - Current payload object\n * @param fieldname - Field name\n * @param value - Value to add (string, number, boolean, object, or ParsedFile)\n */\nfunction normalizeField(\n payload: ParsedPayload,\n fieldname: string,\n value: ParsedValue\n): void {\n const existing = payload[fieldname];\n\n if (existing !== undefined) {\n // Field already exists, convert to array\n if (Array.isArray(existing)) {\n (existing as ParsedValue[]).push(value);\n } else {\n payload[fieldname] = [existing as ParsedValue, value];\n }\n } else {\n payload[fieldname] = value;\n }\n}\n\n/**\n * Attempts to automatically parse a string value to appropriate type\n *\n * Parsing order: JSON → Boolean → Number → String (fallback)\n *\n * @param value - String value to parse\n * @param options - Parser options controlling auto-conversion behavior\n * @returns Parsed value (JSON object, number, boolean, or original string)\n *\n * @example\n * ```typescript\n * autoParse('{\"key\":\"value\"}', opts) // → { key: \"value\" }\n * autoParse('true', opts) // → true\n * autoParse('123', opts) // → 123\n * autoParse('hello', opts) // → \"hello\"\n * ```\n */\nfunction autoParse(\n value: string,\n options: Required<ParserOptions>\n): string | number | boolean | object {\n // 1. Try JSON parsing (if starts/ends with {} or [])\n if (options.autoParseJSON) {\n if (\n (value.startsWith('{') && value.endsWith('}')) ||\n (value.startsWith('[') && value.endsWith(']'))\n ) {\n try {\n return JSON.parse(value);\n } catch {\n // Fallback to string if not valid JSON\n }\n }\n }\n\n // 2. Try boolean conversion\n if (options.autoParseBooleans) {\n if (value === 'true') return true;\n if (value === 'false') return false;\n if (value === '1') return true;\n if (value === '0') return false;\n }\n\n // 3. Try number conversion\n if (options.autoParseNumbers) {\n const num = Number(value);\n if (!isNaN(num) && value.trim() !== '') {\n return num;\n }\n }\n\n // 4. Return as string\n return value;\n}\n\n/**\n * Parses multipart/form-data request using busboy\n *\n * @param req - Express request object\n * @param options - Parser configuration options\n * @returns Promise resolving to normalized payload\n *\n * @example\n * ```typescript\n * const payload = await parseMultipart(req, {\n * maxFileSize: 5 * 1024 * 1024, // 5MB\n * maxFiles: 5\n * });\n *\n * console.log(payload.name); // Auto-parsed text field\n * console.log(payload.avatar); // ParsedFile object\n * ```\n */\nexport function parseMultipart(\n req: Request,\n options: Partial<ParserOptions> = {}\n): Promise<ParsedPayload> {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n return new Promise((resolve, reject) => {\n const payload: ParsedPayload = {};\n const files: ParsedFile[] = [];\n let fileCount = 0;\n let hasError = false;\n\n // Validate content-type header\n const contentType = req.headers['content-type'];\n if (!contentType || typeof contentType !== 'string') {\n return reject(new Error('Missing or invalid content-type header'));\n }\n\n const busboy = Busboy({\n headers: req.headers,\n limits: {\n fileSize: opts.maxFileSize,\n files: opts.maxFiles,\n },\n });\n\n // Handler for text fields\n busboy.on('field', (fieldname: string, value: string) => {\n const parsedValue = autoParse(value, opts);\n normalizeField(payload, fieldname, parsedValue);\n });\n\n // Handler for file uploads\n busboy.on(\n 'file',\n (\n fieldname: string,\n file: NodeJS.ReadableStream,\n info: { filename: string; encoding: string; mimeType: string }\n ) => {\n fileCount++;\n\n // Enforce file count limit\n if (fileCount > opts.maxFiles) {\n hasError = true;\n file.resume(); // Drain stream to prevent backpressure\n reject(\n new Error(`Maximum number of files (${opts.maxFiles}) exceeded`)\n );\n return;\n }\n\n const chunks: Buffer[] = [];\n let size = 0;\n let fileSizeExceeded = false;\n\n file.on('data', (chunk: Buffer) => {\n // Skip processing if error already occurred\n if (hasError || fileSizeExceeded) {\n return;\n }\n\n size += chunk.length;\n\n // Enforce size limit\n if (size > opts.maxFileSize) {\n fileSizeExceeded = true;\n hasError = true;\n file.resume(); // Drain remaining stream data\n reject(\n new Error(\n `File size exceeds limit of ${opts.maxFileSize} bytes`\n )\n );\n return;\n }\n\n chunks.push(chunk);\n });\n\n file.on('end', () => {\n // Only add file if no errors occurred\n if (!hasError && !fileSizeExceeded) {\n const parsedFile: ParsedFile = {\n fieldname,\n originalname: info.filename,\n encoding: info.encoding,\n mimetype: info.mimeType,\n size,\n buffer: Buffer.concat(chunks),\n };\n\n files.push(parsedFile);\n }\n });\n\n file.on('error', (err) => {\n hasError = true;\n reject(err);\n });\n }\n );\n\n // Handler for limit exceeded\n busboy.on('limit', () => {\n reject(new Error('File size limit exceeded'));\n });\n\n // Handler for parsing completion\n busboy.on('finish', () => {\n // Normalize files into payload\n files.forEach((file) => {\n normalizeField(payload, file.fieldname, file);\n });\n\n resolve(payload);\n });\n\n // Handler for parsing errors\n busboy.on('error', reject);\n\n // Pipe request stream to busboy\n req.pipe(busboy);\n });\n}\n","import type { Request, Response, NextFunction } from 'express';\nimport { parseMultipart } from './parser';\nimport type { ParserOptions, MiddlewareFunction } from './types';\n\n/**\n * Creates Express middleware for parsing multipart/form-data\n *\n * @param options - Configuration options\n * @returns Express middleware function\n *\n * @example\n * ```typescript\n * import express from 'express';\n * import { parser } from 'formdata-io/server';\n *\n * const app = express();\n *\n * // Use with default options (10MB, 10 files)\n * app.post('/upload', parser(), (req, res) => {\n * const { name, avatar } = req.payload;\n * res.json({ ok: true });\n * });\n *\n * // Use with custom options\n * app.post('/photos', parser({ maxFileSize: 5 * 1024 * 1024 }), (req, res) => {\n * const photos = req.payload?.photos;\n * res.json({ count: Array.isArray(photos) ? photos.length : 1 });\n * });\n * ```\n */\nexport function parser(options: ParserOptions = {}): MiddlewareFunction {\n return async (req: Request, res: Response, next: NextFunction) => {\n // Only process multipart/form-data requests\n const contentType = req.headers['content-type'] || '';\n if (!contentType.includes('multipart/form-data')) {\n return next();\n }\n\n try {\n req.payload = await parseMultipart(req, options);\n next();\n } catch (error) {\n // Pass error to Express error handler\n next(error);\n }\n };\n}\n"]}
@@ -9,6 +9,18 @@ var DEFAULT_OPTIONS = {
9
9
  autoParseNumbers: true,
10
10
  autoParseBooleans: true
11
11
  };
12
+ function normalizeField(payload, fieldname, value) {
13
+ const existing = payload[fieldname];
14
+ if (existing !== void 0) {
15
+ if (Array.isArray(existing)) {
16
+ existing.push(value);
17
+ } else {
18
+ payload[fieldname] = [existing, value];
19
+ }
20
+ } else {
21
+ payload[fieldname] = value;
22
+ }
23
+ }
12
24
  function autoParse(value, options) {
13
25
  if (options.autoParseJSON) {
14
26
  if (value.startsWith("{") && value.endsWith("}") || value.startsWith("[") && value.endsWith("]")) {
@@ -38,6 +50,11 @@ function parseMultipart(req, options = {}) {
38
50
  const payload = {};
39
51
  const files = [];
40
52
  let fileCount = 0;
53
+ let hasError = false;
54
+ const contentType = req.headers["content-type"];
55
+ if (!contentType || typeof contentType !== "string") {
56
+ return reject(new Error("Missing or invalid content-type header"));
57
+ }
41
58
  const busboy = Busboy({
42
59
  headers: req.headers,
43
60
  limits: {
@@ -47,52 +64,58 @@ function parseMultipart(req, options = {}) {
47
64
  });
48
65
  busboy.on("field", (fieldname, value) => {
49
66
  const parsedValue = autoParse(value, opts);
50
- if (payload[fieldname] !== void 0) {
51
- if (Array.isArray(payload[fieldname])) {
52
- payload[fieldname].push(parsedValue);
53
- } else {
54
- payload[fieldname] = [payload[fieldname], parsedValue];
55
- }
56
- } else {
57
- payload[fieldname] = parsedValue;
58
- }
67
+ normalizeField(payload, fieldname, parsedValue);
59
68
  });
60
69
  busboy.on(
61
70
  "file",
62
71
  (fieldname, file, info) => {
63
72
  fileCount++;
64
73
  if (fileCount > opts.maxFiles) {
74
+ hasError = true;
65
75
  file.resume();
66
- return reject(
76
+ reject(
67
77
  new Error(`Maximum number of files (${opts.maxFiles}) exceeded`)
68
78
  );
79
+ return;
69
80
  }
70
81
  const chunks = [];
71
82
  let size = 0;
83
+ let fileSizeExceeded = false;
72
84
  file.on("data", (chunk) => {
85
+ if (hasError || fileSizeExceeded) {
86
+ return;
87
+ }
73
88
  size += chunk.length;
74
89
  if (size > opts.maxFileSize) {
90
+ fileSizeExceeded = true;
91
+ hasError = true;
75
92
  file.resume();
76
- return reject(
93
+ reject(
77
94
  new Error(
78
95
  `File size exceeds limit of ${opts.maxFileSize} bytes`
79
96
  )
80
97
  );
98
+ return;
81
99
  }
82
100
  chunks.push(chunk);
83
101
  });
84
102
  file.on("end", () => {
85
- const parsedFile = {
86
- fieldname,
87
- originalname: info.filename,
88
- encoding: info.encoding,
89
- mimetype: info.mimeType,
90
- size,
91
- buffer: Buffer.concat(chunks)
92
- };
93
- files.push(parsedFile);
103
+ if (!hasError && !fileSizeExceeded) {
104
+ const parsedFile = {
105
+ fieldname,
106
+ originalname: info.filename,
107
+ encoding: info.encoding,
108
+ mimetype: info.mimeType,
109
+ size,
110
+ buffer: Buffer.concat(chunks)
111
+ };
112
+ files.push(parsedFile);
113
+ }
114
+ });
115
+ file.on("error", (err) => {
116
+ hasError = true;
117
+ reject(err);
94
118
  });
95
- file.on("error", reject);
96
119
  }
97
120
  );
98
121
  busboy.on("limit", () => {
@@ -100,15 +123,7 @@ function parseMultipart(req, options = {}) {
100
123
  });
101
124
  busboy.on("finish", () => {
102
125
  files.forEach((file) => {
103
- if (payload[file.fieldname] !== void 0) {
104
- if (Array.isArray(payload[file.fieldname])) {
105
- payload[file.fieldname].push(file);
106
- } else {
107
- payload[file.fieldname] = [payload[file.fieldname], file];
108
- }
109
- } else {
110
- payload[file.fieldname] = file;
111
- }
126
+ normalizeField(payload, file.fieldname, file);
112
127
  });
113
128
  resolve(payload);
114
129
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/parser.ts","../../src/server/middleware.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import Busboy from 'busboy';\nimport type { Request } from 'express';\nimport type { ParsedFile, ParsedPayload, ParserOptions } from './types';\n\nconst DEFAULT_OPTIONS: Required<ParserOptions> = {\n maxFileSize: 10 * 1024 * 1024, // 10MB\n maxFiles: 10,\n autoParseJSON: true,\n autoParseNumbers: true,\n autoParseBooleans: true,\n};\n\n/**\n * Attempts to automatically parse a string value to appropriate type\n *\n * @param value - String value to parse\n * @param options - Parser options\n * @returns Parsed value (JSON object, number, boolean, or original string)\n */\nfunction autoParse(\n value: string,\n options: Required<ParserOptions>\n): string | number | boolean | object {\n // 1. Try JSON parsing (if starts/ends with {} or [])\n if (options.autoParseJSON) {\n if (\n (value.startsWith('{') && value.endsWith('}')) ||\n (value.startsWith('[') && value.endsWith(']'))\n ) {\n try {\n return JSON.parse(value);\n } catch {\n // Fallback to string if not valid JSON\n }\n }\n }\n\n // 2. Try boolean conversion\n if (options.autoParseBooleans) {\n if (value === 'true') return true;\n if (value === 'false') return false;\n if (value === '1') return true;\n if (value === '0') return false;\n }\n\n // 3. Try number conversion\n if (options.autoParseNumbers) {\n const num = Number(value);\n if (!isNaN(num) && value.trim() !== '') {\n return num;\n }\n }\n\n // 4. Return as string\n return value;\n}\n\n/**\n * Parses multipart/form-data request using busboy\n *\n * @param req - Express request object\n * @param options - Parser configuration options\n * @returns Promise resolving to normalized payload\n *\n * @example\n * ```typescript\n * const payload = await parseMultipart(req, {\n * maxFileSize: 5 * 1024 * 1024, // 5MB\n * maxFiles: 5\n * });\n *\n * console.log(payload.name); // Auto-parsed text field\n * console.log(payload.avatar); // ParsedFile object\n * ```\n */\nexport function parseMultipart(\n req: Request,\n options: Partial<ParserOptions> = {}\n): Promise<ParsedPayload> {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n return new Promise((resolve, reject) => {\n const payload: ParsedPayload = {};\n const files: ParsedFile[] = [];\n let fileCount = 0;\n\n const busboy = Busboy({\n headers: req.headers as any,\n limits: {\n fileSize: opts.maxFileSize,\n files: opts.maxFiles,\n },\n });\n\n // Handler for text fields\n busboy.on('field', (fieldname: string, value: string) => {\n const parsedValue = autoParse(value, opts);\n\n // If field already exists, convert to array\n if (payload[fieldname] !== undefined) {\n if (Array.isArray(payload[fieldname])) {\n (payload[fieldname] as any[]).push(parsedValue);\n } else {\n payload[fieldname] = [payload[fieldname], parsedValue];\n }\n } else {\n payload[fieldname] = parsedValue;\n }\n });\n\n // Handler for file uploads\n busboy.on(\n 'file',\n (\n fieldname: string,\n file: NodeJS.ReadableStream,\n info: { filename: string; encoding: string; mimeType: string }\n ) => {\n fileCount++;\n\n // Enforce file count limit\n if (fileCount > opts.maxFiles) {\n file.resume(); // CRITICAL: drain stream to prevent backpressure\n return reject(\n new Error(`Maximum number of files (${opts.maxFiles}) exceeded`)\n );\n }\n\n const chunks: Buffer[] = [];\n let size = 0;\n\n file.on('data', (chunk: Buffer) => {\n size += chunk.length;\n\n // Enforce size limit\n if (size > opts.maxFileSize) {\n file.resume(); // CRITICAL: drain stream\n return reject(\n new Error(\n `File size exceeds limit of ${opts.maxFileSize} bytes`\n )\n );\n }\n\n chunks.push(chunk);\n });\n\n file.on('end', () => {\n const parsedFile: ParsedFile = {\n fieldname,\n originalname: info.filename,\n encoding: info.encoding,\n mimetype: info.mimeType,\n size,\n buffer: Buffer.concat(chunks),\n };\n\n files.push(parsedFile);\n });\n\n file.on('error', reject);\n }\n );\n\n // Handler for limit exceeded\n busboy.on('limit', () => {\n reject(new Error('File size limit exceeded'));\n });\n\n // Handler for parsing completion\n busboy.on('finish', () => {\n // Normalize files into payload\n files.forEach((file) => {\n if (payload[file.fieldname] !== undefined) {\n // Field already exists, convert to array\n if (Array.isArray(payload[file.fieldname])) {\n (payload[file.fieldname] as ParsedFile[]).push(file);\n } else {\n payload[file.fieldname] = [payload[file.fieldname] as ParsedFile, file];\n }\n } else {\n payload[file.fieldname] = file;\n }\n });\n\n resolve(payload);\n });\n\n // Handler for parsing errors\n busboy.on('error', reject);\n\n // Pipe request stream to busboy\n req.pipe(busboy);\n });\n}\n","import type { Request, Response, NextFunction } from 'express';\nimport { parseMultipart } from './parser';\nimport type { ParserOptions, MiddlewareFunction } from './types';\n\n/**\n * Creates Express middleware for parsing multipart/form-data\n *\n * @param options - Configuration options\n * @returns Express middleware function\n *\n * @example\n * ```typescript\n * import express from 'express';\n * import { parser } from 'formdata-io/server';\n *\n * const app = express();\n *\n * // Use with default options (10MB, 10 files)\n * app.post('/upload', parser(), (req, res) => {\n * const { name, avatar } = req.payload;\n * res.json({ ok: true });\n * });\n *\n * // Use with custom options\n * app.post('/photos', parser({ maxFileSize: 5 * 1024 * 1024 }), (req, res) => {\n * const photos = req.payload?.photos;\n * res.json({ count: Array.isArray(photos) ? photos.length : 1 });\n * });\n * ```\n */\nexport function parser(options: ParserOptions = {}): MiddlewareFunction {\n return async (req: Request, res: Response, next: NextFunction) => {\n // Only process multipart/form-data requests\n const contentType = req.headers['content-type'] || '';\n if (!contentType.includes('multipart/form-data')) {\n return next();\n }\n\n try {\n req.payload = await parseMultipart(req, options);\n next();\n } catch (error) {\n // Pass error to Express error handler\n next(error);\n }\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/server/parser.ts","../../src/server/middleware.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import Busboy from 'busboy';\nimport type { Request } from 'express';\nimport type { ParsedFile, ParsedPayload, ParserOptions } from './types';\n\nconst DEFAULT_OPTIONS: Required<ParserOptions> = {\n maxFileSize: 10 * 1024 * 1024, // 10MB\n maxFiles: 10,\n autoParseJSON: true,\n autoParseNumbers: true,\n autoParseBooleans: true,\n};\n\n/**\n * Type representing a single parsed value (not an array)\n */\ntype ParsedValue = string | number | boolean | object | ParsedFile;\n\n/**\n * Normalizes a field into the payload, converting to array if needed\n *\n * @param payload - Current payload object\n * @param fieldname - Field name\n * @param value - Value to add (string, number, boolean, object, or ParsedFile)\n */\nfunction normalizeField(\n payload: ParsedPayload,\n fieldname: string,\n value: ParsedValue\n): void {\n const existing = payload[fieldname];\n\n if (existing !== undefined) {\n // Field already exists, convert to array\n if (Array.isArray(existing)) {\n (existing as ParsedValue[]).push(value);\n } else {\n payload[fieldname] = [existing as ParsedValue, value];\n }\n } else {\n payload[fieldname] = value;\n }\n}\n\n/**\n * Attempts to automatically parse a string value to appropriate type\n *\n * Parsing order: JSON → Boolean → Number → String (fallback)\n *\n * @param value - String value to parse\n * @param options - Parser options controlling auto-conversion behavior\n * @returns Parsed value (JSON object, number, boolean, or original string)\n *\n * @example\n * ```typescript\n * autoParse('{\"key\":\"value\"}', opts) // → { key: \"value\" }\n * autoParse('true', opts) // → true\n * autoParse('123', opts) // → 123\n * autoParse('hello', opts) // → \"hello\"\n * ```\n */\nfunction autoParse(\n value: string,\n options: Required<ParserOptions>\n): string | number | boolean | object {\n // 1. Try JSON parsing (if starts/ends with {} or [])\n if (options.autoParseJSON) {\n if (\n (value.startsWith('{') && value.endsWith('}')) ||\n (value.startsWith('[') && value.endsWith(']'))\n ) {\n try {\n return JSON.parse(value);\n } catch {\n // Fallback to string if not valid JSON\n }\n }\n }\n\n // 2. Try boolean conversion\n if (options.autoParseBooleans) {\n if (value === 'true') return true;\n if (value === 'false') return false;\n if (value === '1') return true;\n if (value === '0') return false;\n }\n\n // 3. Try number conversion\n if (options.autoParseNumbers) {\n const num = Number(value);\n if (!isNaN(num) && value.trim() !== '') {\n return num;\n }\n }\n\n // 4. Return as string\n return value;\n}\n\n/**\n * Parses multipart/form-data request using busboy\n *\n * @param req - Express request object\n * @param options - Parser configuration options\n * @returns Promise resolving to normalized payload\n *\n * @example\n * ```typescript\n * const payload = await parseMultipart(req, {\n * maxFileSize: 5 * 1024 * 1024, // 5MB\n * maxFiles: 5\n * });\n *\n * console.log(payload.name); // Auto-parsed text field\n * console.log(payload.avatar); // ParsedFile object\n * ```\n */\nexport function parseMultipart(\n req: Request,\n options: Partial<ParserOptions> = {}\n): Promise<ParsedPayload> {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n return new Promise((resolve, reject) => {\n const payload: ParsedPayload = {};\n const files: ParsedFile[] = [];\n let fileCount = 0;\n let hasError = false;\n\n // Validate content-type header\n const contentType = req.headers['content-type'];\n if (!contentType || typeof contentType !== 'string') {\n return reject(new Error('Missing or invalid content-type header'));\n }\n\n const busboy = Busboy({\n headers: req.headers,\n limits: {\n fileSize: opts.maxFileSize,\n files: opts.maxFiles,\n },\n });\n\n // Handler for text fields\n busboy.on('field', (fieldname: string, value: string) => {\n const parsedValue = autoParse(value, opts);\n normalizeField(payload, fieldname, parsedValue);\n });\n\n // Handler for file uploads\n busboy.on(\n 'file',\n (\n fieldname: string,\n file: NodeJS.ReadableStream,\n info: { filename: string; encoding: string; mimeType: string }\n ) => {\n fileCount++;\n\n // Enforce file count limit\n if (fileCount > opts.maxFiles) {\n hasError = true;\n file.resume(); // Drain stream to prevent backpressure\n reject(\n new Error(`Maximum number of files (${opts.maxFiles}) exceeded`)\n );\n return;\n }\n\n const chunks: Buffer[] = [];\n let size = 0;\n let fileSizeExceeded = false;\n\n file.on('data', (chunk: Buffer) => {\n // Skip processing if error already occurred\n if (hasError || fileSizeExceeded) {\n return;\n }\n\n size += chunk.length;\n\n // Enforce size limit\n if (size > opts.maxFileSize) {\n fileSizeExceeded = true;\n hasError = true;\n file.resume(); // Drain remaining stream data\n reject(\n new Error(\n `File size exceeds limit of ${opts.maxFileSize} bytes`\n )\n );\n return;\n }\n\n chunks.push(chunk);\n });\n\n file.on('end', () => {\n // Only add file if no errors occurred\n if (!hasError && !fileSizeExceeded) {\n const parsedFile: ParsedFile = {\n fieldname,\n originalname: info.filename,\n encoding: info.encoding,\n mimetype: info.mimeType,\n size,\n buffer: Buffer.concat(chunks),\n };\n\n files.push(parsedFile);\n }\n });\n\n file.on('error', (err) => {\n hasError = true;\n reject(err);\n });\n }\n );\n\n // Handler for limit exceeded\n busboy.on('limit', () => {\n reject(new Error('File size limit exceeded'));\n });\n\n // Handler for parsing completion\n busboy.on('finish', () => {\n // Normalize files into payload\n files.forEach((file) => {\n normalizeField(payload, file.fieldname, file);\n });\n\n resolve(payload);\n });\n\n // Handler for parsing errors\n busboy.on('error', reject);\n\n // Pipe request stream to busboy\n req.pipe(busboy);\n });\n}\n","import type { Request, Response, NextFunction } from 'express';\nimport { parseMultipart } from './parser';\nimport type { ParserOptions, MiddlewareFunction } from './types';\n\n/**\n * Creates Express middleware for parsing multipart/form-data\n *\n * @param options - Configuration options\n * @returns Express middleware function\n *\n * @example\n * ```typescript\n * import express from 'express';\n * import { parser } from 'formdata-io/server';\n *\n * const app = express();\n *\n * // Use with default options (10MB, 10 files)\n * app.post('/upload', parser(), (req, res) => {\n * const { name, avatar } = req.payload;\n * res.json({ ok: true });\n * });\n *\n * // Use with custom options\n * app.post('/photos', parser({ maxFileSize: 5 * 1024 * 1024 }), (req, res) => {\n * const photos = req.payload?.photos;\n * res.json({ count: Array.isArray(photos) ? photos.length : 1 });\n * });\n * ```\n */\nexport function parser(options: ParserOptions = {}): MiddlewareFunction {\n return async (req: Request, res: Response, next: NextFunction) => {\n // Only process multipart/form-data requests\n const contentType = req.headers['content-type'] || '';\n if (!contentType.includes('multipart/form-data')) {\n return next();\n }\n\n try {\n req.payload = await parseMultipart(req, options);\n next();\n } catch (error) {\n // Pass error to Express error handler\n next(error);\n }\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "formdata-io",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "TypeScript-first library for seamless FormData handling in frontend and backend",
5
5
  "author": "Douglas Ladmo",
6
6
  "license": "MIT",