formdata-io 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +277 -0
- package/dist/client/index.d.mts +90 -0
- package/dist/client/index.d.ts +90 -0
- package/dist/client/index.js +86 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +84 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/server/index.d.mts +149 -0
- package/dist/server/index.d.ts +149 -0
- package/dist/server/index.js +145 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +138 -0
- package/dist/server/index.mjs.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Dougladmo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# 🚀 FormData IO
|
|
2
|
+
|
|
3
|
+
> TypeScript-first library for seamless FormData handling in frontend and backend.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/formdata-io)
|
|
6
|
+
[](https://bundlephobia.com/package/formdata-io)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
|
|
9
|
+
## Why?
|
|
10
|
+
|
|
11
|
+
Working with `multipart/form-data` in JavaScript is painful:
|
|
12
|
+
|
|
13
|
+
- ❌ Manual `formData.append()` calls everywhere
|
|
14
|
+
- ❌ Backend requires multer config, storage setup, and juggling `req.file` + `req.body`
|
|
15
|
+
- ❌ Poor TypeScript support
|
|
16
|
+
- ❌ Inconsistent APIs between frontend and backend
|
|
17
|
+
|
|
18
|
+
**FormData IO solves this:**
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// Frontend: Write objects, not append calls
|
|
22
|
+
const formData = payload({ name: "João", avatar: file });
|
|
23
|
+
|
|
24
|
+
// Backend: Receive normalized payload, not scattered data
|
|
25
|
+
app.post('/upload', parser(), (req, res) => {
|
|
26
|
+
const { name, avatar } = req.payload; // ✨ Type-safe!
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install formdata-io
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
### Frontend (React, Vue, Vanilla JS)
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { payload } from 'formdata-io/client';
|
|
42
|
+
|
|
43
|
+
const formData = payload({
|
|
44
|
+
name: "João Silva",
|
|
45
|
+
age: 25,
|
|
46
|
+
avatar: fileInput.files[0],
|
|
47
|
+
tags: ["admin", "user"],
|
|
48
|
+
metadata: { source: "web" }
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Use with fetch
|
|
52
|
+
fetch('/api/upload', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
body: formData
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Or with axios
|
|
58
|
+
axios.post('/api/upload', formData);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Backend (Express)
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import express from 'express';
|
|
65
|
+
import { parser } from 'formdata-io/server';
|
|
66
|
+
|
|
67
|
+
const app = express();
|
|
68
|
+
|
|
69
|
+
app.post('/api/upload', parser(), (req, res) => {
|
|
70
|
+
const { name, age, avatar, tags, metadata } = req.payload;
|
|
71
|
+
|
|
72
|
+
console.log(name); // "João Silva"
|
|
73
|
+
console.log(age); // 25 (auto-parsed as number)
|
|
74
|
+
console.log(avatar); // { buffer: Buffer, originalname: "...", ... }
|
|
75
|
+
console.log(tags); // ["admin", "user"]
|
|
76
|
+
console.log(metadata); // { source: "web" } (auto-parsed JSON)
|
|
77
|
+
|
|
78
|
+
res.json({ success: true });
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## API Reference
|
|
83
|
+
|
|
84
|
+
### Client API
|
|
85
|
+
|
|
86
|
+
#### `payload(data, options?)`
|
|
87
|
+
|
|
88
|
+
Converts a JavaScript object to FormData.
|
|
89
|
+
|
|
90
|
+
**Parameters:**
|
|
91
|
+
- `data: FormDataPayload` - Object to convert
|
|
92
|
+
- `options?: PayloadOptions` - Configuration options
|
|
93
|
+
|
|
94
|
+
**Returns:** `FormData`
|
|
95
|
+
|
|
96
|
+
**Options:**
|
|
97
|
+
```typescript
|
|
98
|
+
{
|
|
99
|
+
indices: boolean; // Add indices to array fields (default: false)
|
|
100
|
+
nullsAsUndefineds: boolean; // Skip null values (default: false)
|
|
101
|
+
booleansAsIntegers: boolean; // Convert true/false to 1/0 (default: true)
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Examples:**
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Basic usage
|
|
109
|
+
const formData = payload({
|
|
110
|
+
name: "User",
|
|
111
|
+
file: document.querySelector('input[type="file"]').files[0]
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// With arrays
|
|
115
|
+
const formData = payload({
|
|
116
|
+
tags: ["admin", "user"] // → tags=admin&tags=user
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// With indices
|
|
120
|
+
const formData = payload(
|
|
121
|
+
{ tags: ["admin", "user"] },
|
|
122
|
+
{ indices: true } // → tags[0]=admin&tags[1]=user
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Nested objects (auto-serialized as JSON)
|
|
126
|
+
const formData = payload({
|
|
127
|
+
metadata: { source: "web", version: 2 }
|
|
128
|
+
// → metadata='{"source":"web","version":2}'
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Date handling
|
|
132
|
+
const formData = payload({
|
|
133
|
+
createdAt: new Date() // → createdAt='2024-01-01T00:00:00.000Z'
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Server API
|
|
138
|
+
|
|
139
|
+
#### `parser(options?)`
|
|
140
|
+
|
|
141
|
+
Express middleware for parsing multipart/form-data.
|
|
142
|
+
|
|
143
|
+
**Parameters:**
|
|
144
|
+
- `options?: ParserOptions` - Configuration options
|
|
145
|
+
|
|
146
|
+
**Returns:** Express middleware function
|
|
147
|
+
|
|
148
|
+
**Options:**
|
|
149
|
+
```typescript
|
|
150
|
+
{
|
|
151
|
+
maxFileSize: number; // Max file size in bytes (default: 10MB)
|
|
152
|
+
maxFiles: number; // Max number of files (default: 10)
|
|
153
|
+
autoParseJSON: boolean; // Auto-parse JSON strings (default: true)
|
|
154
|
+
autoParseNumbers: boolean; // Auto-convert numeric strings (default: true)
|
|
155
|
+
autoParseBooleans: boolean; // Auto-convert "true"/"false" (default: true)
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Examples:**
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Default options (10MB, 10 files)
|
|
163
|
+
app.post('/upload', parser(), (req, res) => {
|
|
164
|
+
// req.payload contains all fields and files
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Custom file size limit
|
|
168
|
+
app.post('/photos', parser({ maxFileSize: 50 * 1024 * 1024 }), (req, res) => {
|
|
169
|
+
// Allow up to 50MB files
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Disable auto-parsing
|
|
173
|
+
app.post('/raw', parser({ autoParseJSON: false }), (req, res) => {
|
|
174
|
+
// All fields remain as strings
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### `ParsedFile` Interface
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
interface ParsedFile {
|
|
182
|
+
fieldname: string; // Form field name
|
|
183
|
+
originalname: string; // Original filename
|
|
184
|
+
encoding: string; // File encoding
|
|
185
|
+
mimetype: string; // MIME type
|
|
186
|
+
size: number; // File size in bytes
|
|
187
|
+
buffer: Buffer; // File data
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## TypeScript Support
|
|
192
|
+
|
|
193
|
+
Full TypeScript support with type inference:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import type { ParsedFile } from 'formdata-io/server';
|
|
197
|
+
|
|
198
|
+
app.post('/upload', parser(), (req, res) => {
|
|
199
|
+
const avatar = req.payload?.avatar as ParsedFile;
|
|
200
|
+
|
|
201
|
+
avatar.buffer; // Buffer
|
|
202
|
+
avatar.originalname; // string
|
|
203
|
+
avatar.mimetype; // string
|
|
204
|
+
avatar.size; // number
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Examples
|
|
209
|
+
|
|
210
|
+
See the [examples](./examples) directory for complete working examples.
|
|
211
|
+
|
|
212
|
+
**Run the example:**
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# Terminal 1: Start the server
|
|
216
|
+
npm run build
|
|
217
|
+
node examples/basic/server.ts
|
|
218
|
+
|
|
219
|
+
# Terminal 2: Open client.html in your browser
|
|
220
|
+
open examples/basic/client.html
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Comparison with Alternatives
|
|
224
|
+
|
|
225
|
+
| Feature | FormData IO | multer | busboy | object-to-formdata |
|
|
226
|
+
|---------|-------------|--------|--------|-------------------|
|
|
227
|
+
| Frontend + Backend | ✅ | ❌ | ❌ | ❌ |
|
|
228
|
+
| TypeScript-first | ✅ | ⚠️ | ⚠️ | ❌ |
|
|
229
|
+
| Zero config | ✅ | ❌ | ❌ | ✅ |
|
|
230
|
+
| Auto-parsing | ✅ | ❌ | ❌ | N/A |
|
|
231
|
+
| Bundle size | ~6KB | ~30KB | ~10KB | ~2KB |
|
|
232
|
+
|
|
233
|
+
## How It Works
|
|
234
|
+
|
|
235
|
+
### Client Side
|
|
236
|
+
|
|
237
|
+
The `payload()` function converts JavaScript objects to FormData by:
|
|
238
|
+
|
|
239
|
+
1. **File/Blob detection**: Directly appends File and Blob objects
|
|
240
|
+
2. **Array handling**: Supports both flat (`tags=a&tags=b`) and indexed (`tags[0]=a`) formats
|
|
241
|
+
3. **Object serialization**: Nested objects are JSON-serialized
|
|
242
|
+
4. **Type conversion**: Booleans, numbers, dates converted to strings
|
|
243
|
+
|
|
244
|
+
### Server Side
|
|
245
|
+
|
|
246
|
+
The `parser()` middleware uses [busboy](https://github.com/mscdex/busboy) for stream-based parsing:
|
|
247
|
+
|
|
248
|
+
1. **Stream processing**: Memory-efficient file handling
|
|
249
|
+
2. **Size limits**: Enforced per-file and total limits
|
|
250
|
+
3. **Auto-parsing**: Automatic type conversion (JSON, numbers, booleans)
|
|
251
|
+
4. **Array normalization**: Multiple values with same key become arrays
|
|
252
|
+
|
|
253
|
+
## Security
|
|
254
|
+
|
|
255
|
+
**Built-in protections:**
|
|
256
|
+
- ✅ File size limits (default: 10MB per file)
|
|
257
|
+
- ✅ File count limits (default: 10 files max)
|
|
258
|
+
- ✅ Stream-based processing (no memory exhaustion)
|
|
259
|
+
- ✅ Safe JSON parsing (fallback to string on error)
|
|
260
|
+
|
|
261
|
+
**Your responsibility:**
|
|
262
|
+
- ⚠️ File type validation (check `mimetype` and magic bytes)
|
|
263
|
+
- ⚠️ Filename sanitization (prevent path traversal)
|
|
264
|
+
- ⚠️ Virus scanning (if accepting user files)
|
|
265
|
+
- ⚠️ Storage security (S3 permissions, disk quotas)
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT
|
|
270
|
+
|
|
271
|
+
## Contributing
|
|
272
|
+
|
|
273
|
+
Contributions welcome! Please open an issue or PR.
|
|
274
|
+
|
|
275
|
+
## Credits
|
|
276
|
+
|
|
277
|
+
Built with [busboy](https://github.com/mscdex/busboy) for multipart parsing.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported value types for FormData conversion
|
|
3
|
+
*/
|
|
4
|
+
type FormDataValue = string | number | boolean | null | undefined | File | Blob | Date;
|
|
5
|
+
/**
|
|
6
|
+
* Recursive type for objects that can be converted to FormData
|
|
7
|
+
*
|
|
8
|
+
* Supports nested objects and arrays, with automatic serialization
|
|
9
|
+
*/
|
|
10
|
+
type FormDataPayload = {
|
|
11
|
+
[key: string]: FormDataValue | FormDataValue[] | FormDataPayload | FormDataPayload[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Configuration options for FormData conversion
|
|
15
|
+
*/
|
|
16
|
+
interface PayloadOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Add numeric indices to array field names
|
|
19
|
+
*
|
|
20
|
+
* @default false
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // indices: false → tags=a&tags=b
|
|
25
|
+
* // indices: true → tags[0]=a&tags[1]=b
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
indices?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Skip null values instead of converting to empty strings
|
|
31
|
+
*
|
|
32
|
+
* @default false
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // nullsAsUndefineds: false → optional=""
|
|
37
|
+
* // nullsAsUndefineds: true → optional field not sent
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
nullsAsUndefineds?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Convert boolean values to 1/0 instead of true/false
|
|
43
|
+
*
|
|
44
|
+
* @default true
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* // booleansAsIntegers: true → active=1
|
|
49
|
+
* // booleansAsIntegers: false → active=true
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
booleansAsIntegers?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Converts a JavaScript object to FormData
|
|
57
|
+
*
|
|
58
|
+
* @param data - Object to be converted
|
|
59
|
+
* @param options - Configuration options
|
|
60
|
+
* @returns FormData instance ready for submission
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const formData = payload({
|
|
65
|
+
* name: "João Silva",
|
|
66
|
+
* age: 25,
|
|
67
|
+
* avatar: fileInput.files[0],
|
|
68
|
+
* tags: ["admin", "user"],
|
|
69
|
+
* metadata: { source: "web" }
|
|
70
|
+
* });
|
|
71
|
+
*
|
|
72
|
+
* // Use with fetch
|
|
73
|
+
* fetch('/upload', { method: 'POST', body: formData });
|
|
74
|
+
*
|
|
75
|
+
* // Use with axios
|
|
76
|
+
* axios.post('/upload', formData);
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* // With custom options
|
|
82
|
+
* const formData = payload(data, {
|
|
83
|
+
* indices: true, // tags[0]=admin&tags[1]=user
|
|
84
|
+
* booleansAsIntegers: false // active=true instead of active=1
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
declare function payload(data: FormDataPayload, options?: Partial<PayloadOptions>): FormData;
|
|
89
|
+
|
|
90
|
+
export { type FormDataPayload, type FormDataValue, type PayloadOptions, payload };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supported value types for FormData conversion
|
|
3
|
+
*/
|
|
4
|
+
type FormDataValue = string | number | boolean | null | undefined | File | Blob | Date;
|
|
5
|
+
/**
|
|
6
|
+
* Recursive type for objects that can be converted to FormData
|
|
7
|
+
*
|
|
8
|
+
* Supports nested objects and arrays, with automatic serialization
|
|
9
|
+
*/
|
|
10
|
+
type FormDataPayload = {
|
|
11
|
+
[key: string]: FormDataValue | FormDataValue[] | FormDataPayload | FormDataPayload[];
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Configuration options for FormData conversion
|
|
15
|
+
*/
|
|
16
|
+
interface PayloadOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Add numeric indices to array field names
|
|
19
|
+
*
|
|
20
|
+
* @default false
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* // indices: false → tags=a&tags=b
|
|
25
|
+
* // indices: true → tags[0]=a&tags[1]=b
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
indices?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Skip null values instead of converting to empty strings
|
|
31
|
+
*
|
|
32
|
+
* @default false
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // nullsAsUndefineds: false → optional=""
|
|
37
|
+
* // nullsAsUndefineds: true → optional field not sent
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
nullsAsUndefineds?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Convert boolean values to 1/0 instead of true/false
|
|
43
|
+
*
|
|
44
|
+
* @default true
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* // booleansAsIntegers: true → active=1
|
|
49
|
+
* // booleansAsIntegers: false → active=true
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
booleansAsIntegers?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Converts a JavaScript object to FormData
|
|
57
|
+
*
|
|
58
|
+
* @param data - Object to be converted
|
|
59
|
+
* @param options - Configuration options
|
|
60
|
+
* @returns FormData instance ready for submission
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const formData = payload({
|
|
65
|
+
* name: "João Silva",
|
|
66
|
+
* age: 25,
|
|
67
|
+
* avatar: fileInput.files[0],
|
|
68
|
+
* tags: ["admin", "user"],
|
|
69
|
+
* metadata: { source: "web" }
|
|
70
|
+
* });
|
|
71
|
+
*
|
|
72
|
+
* // Use with fetch
|
|
73
|
+
* fetch('/upload', { method: 'POST', body: formData });
|
|
74
|
+
*
|
|
75
|
+
* // Use with axios
|
|
76
|
+
* axios.post('/upload', formData);
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* // With custom options
|
|
82
|
+
* const formData = payload(data, {
|
|
83
|
+
* indices: true, // tags[0]=admin&tags[1]=user
|
|
84
|
+
* booleansAsIntegers: false // active=true instead of active=1
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
declare function payload(data: FormDataPayload, options?: Partial<PayloadOptions>): FormData;
|
|
89
|
+
|
|
90
|
+
export { type FormDataPayload, type FormDataValue, type PayloadOptions, payload };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/client/utils.ts
|
|
4
|
+
function isFileOrBlob(value) {
|
|
5
|
+
if (typeof File !== "undefined" && value instanceof File) return true;
|
|
6
|
+
if (typeof Blob !== "undefined" && value instanceof Blob) return true;
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
function isPlainObject(value) {
|
|
10
|
+
if (typeof value !== "object" || value === null) return false;
|
|
11
|
+
if (isFileOrBlob(value)) return false;
|
|
12
|
+
if (value instanceof Date) return false;
|
|
13
|
+
if (Array.isArray(value)) return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
function valueToString(value, options) {
|
|
17
|
+
if (value === null) {
|
|
18
|
+
return options.nullsAsUndefineds ? void 0 : "";
|
|
19
|
+
}
|
|
20
|
+
if (value === void 0) {
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === "boolean") {
|
|
24
|
+
if (options.booleansAsIntegers) {
|
|
25
|
+
return value ? "1" : "0";
|
|
26
|
+
}
|
|
27
|
+
return value.toString();
|
|
28
|
+
}
|
|
29
|
+
if (value instanceof Date) {
|
|
30
|
+
return value.toISOString();
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === "number") {
|
|
33
|
+
return value.toString();
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "string") {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
function buildFieldName(parentKey, key, options) {
|
|
41
|
+
if (typeof key === "number") {
|
|
42
|
+
return options.indices ? `${parentKey}[${key}]` : parentKey;
|
|
43
|
+
}
|
|
44
|
+
return parentKey ? `${parentKey}[${key}]` : key;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/client/payload.ts
|
|
48
|
+
var DEFAULT_OPTIONS = {
|
|
49
|
+
indices: false,
|
|
50
|
+
nullsAsUndefineds: false,
|
|
51
|
+
booleansAsIntegers: true
|
|
52
|
+
};
|
|
53
|
+
function payload(data, options = {}) {
|
|
54
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
55
|
+
const formData = new FormData();
|
|
56
|
+
function append(key, value) {
|
|
57
|
+
if (isFileOrBlob(value)) {
|
|
58
|
+
formData.append(key, value);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
value.forEach((item, index) => {
|
|
63
|
+
const fieldName = buildFieldName(key, index, opts);
|
|
64
|
+
append(fieldName, item);
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (isPlainObject(value)) {
|
|
69
|
+
const jsonString = JSON.stringify(value);
|
|
70
|
+
formData.append(key, jsonString);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const stringValue = valueToString(value, opts);
|
|
74
|
+
if (stringValue !== void 0) {
|
|
75
|
+
formData.append(key, stringValue);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
79
|
+
append(key, value);
|
|
80
|
+
});
|
|
81
|
+
return formData;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
exports.payload = payload;
|
|
85
|
+
//# sourceMappingURL=index.js.map
|
|
86
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/client/utils.ts
|
|
2
|
+
function isFileOrBlob(value) {
|
|
3
|
+
if (typeof File !== "undefined" && value instanceof File) return true;
|
|
4
|
+
if (typeof Blob !== "undefined" && value instanceof Blob) return true;
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
function isPlainObject(value) {
|
|
8
|
+
if (typeof value !== "object" || value === null) return false;
|
|
9
|
+
if (isFileOrBlob(value)) return false;
|
|
10
|
+
if (value instanceof Date) return false;
|
|
11
|
+
if (Array.isArray(value)) return false;
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
function valueToString(value, options) {
|
|
15
|
+
if (value === null) {
|
|
16
|
+
return options.nullsAsUndefineds ? void 0 : "";
|
|
17
|
+
}
|
|
18
|
+
if (value === void 0) {
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === "boolean") {
|
|
22
|
+
if (options.booleansAsIntegers) {
|
|
23
|
+
return value ? "1" : "0";
|
|
24
|
+
}
|
|
25
|
+
return value.toString();
|
|
26
|
+
}
|
|
27
|
+
if (value instanceof Date) {
|
|
28
|
+
return value.toISOString();
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "number") {
|
|
31
|
+
return value.toString();
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
function buildFieldName(parentKey, key, options) {
|
|
39
|
+
if (typeof key === "number") {
|
|
40
|
+
return options.indices ? `${parentKey}[${key}]` : parentKey;
|
|
41
|
+
}
|
|
42
|
+
return parentKey ? `${parentKey}[${key}]` : key;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/client/payload.ts
|
|
46
|
+
var DEFAULT_OPTIONS = {
|
|
47
|
+
indices: false,
|
|
48
|
+
nullsAsUndefineds: false,
|
|
49
|
+
booleansAsIntegers: true
|
|
50
|
+
};
|
|
51
|
+
function payload(data, options = {}) {
|
|
52
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
53
|
+
const formData = new FormData();
|
|
54
|
+
function append(key, value) {
|
|
55
|
+
if (isFileOrBlob(value)) {
|
|
56
|
+
formData.append(key, value);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
value.forEach((item, index) => {
|
|
61
|
+
const fieldName = buildFieldName(key, index, opts);
|
|
62
|
+
append(fieldName, item);
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (isPlainObject(value)) {
|
|
67
|
+
const jsonString = JSON.stringify(value);
|
|
68
|
+
formData.append(key, jsonString);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const stringValue = valueToString(value, opts);
|
|
72
|
+
if (stringValue !== void 0) {
|
|
73
|
+
formData.append(key, stringValue);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
77
|
+
append(key, value);
|
|
78
|
+
});
|
|
79
|
+
return formData;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { payload };
|
|
83
|
+
//# sourceMappingURL=index.mjs.map
|
|
84
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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"]}
|