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 +112 -0
- package/dist/client/index.d.mts +78 -1
- package/dist/client/index.d.ts +78 -1
- package/dist/client/index.js +48 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +44 -1
- package/dist/client/index.mjs.map +1 -1
- package/dist/server/index.js +45 -30
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +45 -30
- package/dist/server/index.mjs.map +1 -1
- package/package.json +1 -1
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:
|
package/dist/client/index.d.mts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
package/dist/client/index.js.map
CHANGED
|
@@ -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"]}
|
package/dist/client/index.mjs
CHANGED
|
@@ -79,6 +79,49 @@ function payload(data, options = {}) {
|
|
|
79
79
|
return formData;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
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"]}
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/server/index.js.map
CHANGED
|
@@ -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"]}
|
package/dist/server/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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":";;;AAIA,IAAM,eAAA,GAA2C;AAAA,EAC/C,WAAA,EAAa,KAAK,IAAA,GAAO,IAAA;AAAA;AAAA,EACzB,QAAA,EAAU,EAAA;AAAA,EACV,aAAA,EAAe,IAAA;AAAA,EACf,gBAAA,EAAkB,IAAA;AAAA,EAClB,iBAAA,EAAmB;AACrB,CAAA;AASA,SAAS,SAAA,CACP,OACA,OAAA,EACoC;AAEpC,EAAA,IAAI,QAAQ,aAAA,EAAe;AACzB,IAAA,IACG,KAAA,CAAM,UAAA,CAAW,GAAG,CAAA,IAAK,MAAM,QAAA,CAAS,GAAG,CAAA,IAC3C,KAAA,CAAM,WAAW,GAAG,CAAA,IAAK,KAAA,CAAM,QAAA,CAAS,GAAG,CAAA,EAC5C;AACA,MAAA,IAAI;AACF,QAAA,OAAO,IAAA,CAAK,MAAM,KAAK,CAAA;AAAA,MACzB,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAGA,EAAA,IAAI,QAAQ,iBAAA,EAAmB;AAC7B,IAAA,IAAI,KAAA,KAAU,QAAQ,OAAO,IAAA;AAC7B,IAAA,IAAI,KAAA,KAAU,SAAS,OAAO,KAAA;AAC9B,IAAA,IAAI,KAAA,KAAU,KAAK,OAAO,IAAA;AAC1B,IAAA,IAAI,KAAA,KAAU,KAAK,OAAO,KAAA;AAAA,EAC5B;AAGA,EAAA,IAAI,QAAQ,gBAAA,EAAkB;AAC5B,IAAA,MAAM,GAAA,GAAM,OAAO,KAAK,CAAA;AACxB,IAAA,IAAI,CAAC,KAAA,CAAM,GAAG,KAAK,KAAA,CAAM,IAAA,OAAW,EAAA,EAAI;AACtC,MAAA,OAAO,GAAA;AAAA,IACT;AAAA,EACF;AAGA,EAAA,OAAO,KAAA;AACT;AAoBO,SAAS,cAAA,CACd,GAAA,EACA,OAAA,GAAkC,EAAC,EACX;AACxB,EAAA,MAAM,IAAA,GAAO,EAAE,GAAG,eAAA,EAAiB,GAAG,OAAA,EAAQ;AAE9C,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAM,UAAyB,EAAC;AAChC,IAAA,MAAM,QAAsB,EAAC;AAC7B,IAAA,IAAI,SAAA,GAAY,CAAA;AAEhB,IAAA,MAAM,SAAS,MAAA,CAAO;AAAA,MACpB,SAAS,GAAA,CAAI,OAAA;AAAA,MACb,MAAA,EAAQ;AAAA,QACN,UAAU,IAAA,CAAK,WAAA;AAAA,QACf,OAAO,IAAA,CAAK;AAAA;AACd,KACD,CAAA;AAGD,IAAA,MAAA,CAAO,EAAA,CAAG,OAAA,EAAS,CAAC,SAAA,EAAmB,KAAA,KAAkB;AACvD,MAAA,MAAM,WAAA,GAAc,SAAA,CAAU,KAAA,EAAO,IAAI,CAAA;AAGzC,MAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,KAAM,MAAA,EAAW;AACpC,QAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,OAAA,CAAQ,SAAS,CAAC,CAAA,EAAG;AACrC,UAAC,OAAA,CAAQ,SAAS,CAAA,CAAY,IAAA,CAAK,WAAW,CAAA;AAAA,QAChD,CAAA,MAAO;AACL,UAAA,OAAA,CAAQ,SAAS,CAAA,GAAI,CAAC,OAAA,CAAQ,SAAS,GAAG,WAAW,CAAA;AAAA,QACvD;AAAA,MACF,CAAA,MAAO;AACL,QAAA,OAAA,CAAQ,SAAS,CAAA,GAAI,WAAA;AAAA,MACvB;AAAA,IACF,CAAC,CAAA;AAGD,IAAA,MAAA,CAAO,EAAA;AAAA,MACL,MAAA;AAAA,MACA,CACE,SAAA,EACA,IAAA,EACA,IAAA,KACG;AACH,QAAA,SAAA,EAAA;AAGA,QAAA,IAAI,SAAA,GAAY,KAAK,QAAA,EAAU;AAC7B,UAAA,IAAA,CAAK,MAAA,EAAO;AACZ,UAAA,OAAO,MAAA;AAAA,YACL,IAAI,KAAA,CAAM,CAAA,yBAAA,EAA4B,IAAA,CAAK,QAAQ,CAAA,UAAA,CAAY;AAAA,WACjE;AAAA,QACF;AAEA,QAAA,MAAM,SAAmB,EAAC;AAC1B,QAAA,IAAI,IAAA,GAAO,CAAA;AAEX,QAAA,IAAA,CAAK,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAAkB;AACjC,UAAA,IAAA,IAAQ,KAAA,CAAM,MAAA;AAGd,UAAA,IAAI,IAAA,GAAO,KAAK,WAAA,EAAa;AAC3B,YAAA,IAAA,CAAK,MAAA,EAAO;AACZ,YAAA,OAAO,MAAA;AAAA,cACL,IAAI,KAAA;AAAA,gBACF,CAAA,2BAAA,EAA8B,KAAK,WAAW,CAAA,MAAA;AAAA;AAChD,aACF;AAAA,UACF;AAEA,UAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,QACnB,CAAC,CAAA;AAED,QAAA,IAAA,CAAK,EAAA,CAAG,OAAO,MAAM;AACnB,UAAA,MAAM,UAAA,GAAyB;AAAA,YAC7B,SAAA;AAAA,YACA,cAAc,IAAA,CAAK,QAAA;AAAA,YACnB,UAAU,IAAA,CAAK,QAAA;AAAA,YACf,UAAU,IAAA,CAAK,QAAA;AAAA,YACf,IAAA;AAAA,YACA,MAAA,EAAQ,MAAA,CAAO,MAAA,CAAO,MAAM;AAAA,WAC9B;AAEA,UAAA,KAAA,CAAM,KAAK,UAAU,CAAA;AAAA,QACvB,CAAC,CAAA;AAED,QAAA,IAAA,CAAK,EAAA,CAAG,SAAS,MAAM,CAAA;AAAA,MACzB;AAAA,KACF;AAGA,IAAA,MAAA,CAAO,EAAA,CAAG,SAAS,MAAM;AACvB,MAAA,MAAA,CAAO,IAAI,KAAA,CAAM,0BAA0B,CAAC,CAAA;AAAA,IAC9C,CAAC,CAAA;AAGD,IAAA,MAAA,CAAO,EAAA,CAAG,UAAU,MAAM;AAExB,MAAA,KAAA,CAAM,OAAA,CAAQ,CAAC,IAAA,KAAS;AACtB,QAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,SAAS,CAAA,KAAM,MAAA,EAAW;AAEzC,UAAA,IAAI,MAAM,OAAA,CAAQ,OAAA,CAAQ,IAAA,CAAK,SAAS,CAAC,CAAA,EAAG;AAC1C,YAAC,OAAA,CAAQ,IAAA,CAAK,SAAS,CAAA,CAAmB,KAAK,IAAI,CAAA;AAAA,UACrD,CAAA,MAAO;AACL,YAAA,OAAA,CAAQ,IAAA,CAAK,SAAS,CAAA,GAAI,CAAC,QAAQ,IAAA,CAAK,SAAS,GAAiB,IAAI,CAAA;AAAA,UACxE;AAAA,QACF,CAAA,MAAO;AACL,UAAA,OAAA,CAAQ,IAAA,CAAK,SAAS,CAAA,GAAI,IAAA;AAAA,QAC5B;AAAA,MACF,CAAC,CAAA;AAED,MAAA,OAAA,CAAQ,OAAO,CAAA;AAAA,IACjB,CAAC,CAAA;AAGD,IAAA,MAAA,CAAO,EAAA,CAAG,SAAS,MAAM,CAAA;AAGzB,IAAA,GAAA,CAAI,KAAK,MAAM,CAAA;AAAA,EACjB,CAAC,CAAA;AACH;;;ACpKO,SAAS,MAAA,CAAO,OAAA,GAAyB,EAAC,EAAuB;AACtE,EAAA,OAAO,OAAO,GAAA,EAAc,GAAA,EAAe,IAAA,KAAuB;AAEhE,IAAA,MAAM,WAAA,GAAc,GAAA,CAAI,OAAA,CAAQ,cAAc,CAAA,IAAK,EAAA;AACnD,IAAA,IAAI,CAAC,WAAA,CAAY,QAAA,CAAS,qBAAqB,CAAA,EAAG;AAChD,MAAA,OAAO,IAAA,EAAK;AAAA,IACd;AAEA,IAAA,IAAI;AACF,MAAA,GAAA,CAAI,OAAA,GAAU,MAAM,cAAA,CAAe,GAAA,EAAK,OAAO,CAAA;AAC/C,MAAA,IAAA,EAAK;AAAA,IACP,SAAS,KAAA,EAAO;AAEd,MAAA,IAAA,CAAK,KAAK,CAAA;AAAA,IACZ;AAAA,EACF,CAAA;AACF","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"]}
|