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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parsed file object from multipart/form-data
|
|
5
|
+
*/
|
|
6
|
+
interface ParsedFile {
|
|
7
|
+
/** Field name in the form */
|
|
8
|
+
fieldname: string;
|
|
9
|
+
/** Original filename from client */
|
|
10
|
+
originalname: string;
|
|
11
|
+
/** File encoding */
|
|
12
|
+
encoding: string;
|
|
13
|
+
/** MIME type */
|
|
14
|
+
mimetype: string;
|
|
15
|
+
/** File size in bytes */
|
|
16
|
+
size: number;
|
|
17
|
+
/** Buffer containing file data */
|
|
18
|
+
buffer: Buffer;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Normalized payload after parsing multipart/form-data
|
|
22
|
+
*
|
|
23
|
+
* Fields are auto-parsed to appropriate types (JSON, numbers, booleans)
|
|
24
|
+
*/
|
|
25
|
+
type ParsedPayload = {
|
|
26
|
+
[key: string]: string | number | boolean | object | ParsedFile | ParsedFile[];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Extend Express Request with payload property
|
|
30
|
+
*
|
|
31
|
+
* Opt-in via import - non-invasive augmentation
|
|
32
|
+
*/
|
|
33
|
+
declare global {
|
|
34
|
+
namespace Express {
|
|
35
|
+
interface Request {
|
|
36
|
+
payload?: ParsedPayload;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Configuration options for multipart parser
|
|
42
|
+
*/
|
|
43
|
+
interface ParserOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Maximum file size in bytes
|
|
46
|
+
*
|
|
47
|
+
* @default 10485760 (10MB)
|
|
48
|
+
*/
|
|
49
|
+
maxFileSize?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Maximum number of files allowed
|
|
52
|
+
*
|
|
53
|
+
* @default 10
|
|
54
|
+
*/
|
|
55
|
+
maxFiles?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Automatically parse JSON strings in text fields
|
|
58
|
+
*
|
|
59
|
+
* @default true
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* // Field value: '{"key":"value"}'
|
|
64
|
+
* // autoParseJSON: true → { key: "value" }
|
|
65
|
+
* // autoParseJSON: false → '{"key":"value"}'
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
autoParseJSON?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Automatically convert numeric strings to numbers
|
|
71
|
+
*
|
|
72
|
+
* @default true
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* // Field value: '123'
|
|
77
|
+
* // autoParseNumbers: true → 123
|
|
78
|
+
* // autoParseNumbers: false → '123'
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
autoParseNumbers?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Automatically convert boolean strings to booleans
|
|
84
|
+
*
|
|
85
|
+
* @default true
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* // Field value: 'true' or '1'
|
|
90
|
+
* // autoParseBooleans: true → true
|
|
91
|
+
* // autoParseBooleans: false → 'true'
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
autoParseBooleans?: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Express middleware function type
|
|
98
|
+
*/
|
|
99
|
+
type MiddlewareFunction = (req: Request, res: Response, next: NextFunction) => void;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Creates Express middleware for parsing multipart/form-data
|
|
103
|
+
*
|
|
104
|
+
* @param options - Configuration options
|
|
105
|
+
* @returns Express middleware function
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* import express from 'express';
|
|
110
|
+
* import { parser } from 'formdata-io/server';
|
|
111
|
+
*
|
|
112
|
+
* const app = express();
|
|
113
|
+
*
|
|
114
|
+
* // Use with default options (10MB, 10 files)
|
|
115
|
+
* app.post('/upload', parser(), (req, res) => {
|
|
116
|
+
* const { name, avatar } = req.payload;
|
|
117
|
+
* res.json({ ok: true });
|
|
118
|
+
* });
|
|
119
|
+
*
|
|
120
|
+
* // Use with custom options
|
|
121
|
+
* app.post('/photos', parser({ maxFileSize: 5 * 1024 * 1024 }), (req, res) => {
|
|
122
|
+
* const photos = req.payload?.photos;
|
|
123
|
+
* res.json({ count: Array.isArray(photos) ? photos.length : 1 });
|
|
124
|
+
* });
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
declare function parser(options?: ParserOptions): MiddlewareFunction;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parses multipart/form-data request using busboy
|
|
131
|
+
*
|
|
132
|
+
* @param req - Express request object
|
|
133
|
+
* @param options - Parser configuration options
|
|
134
|
+
* @returns Promise resolving to normalized payload
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const payload = await parseMultipart(req, {
|
|
139
|
+
* maxFileSize: 5 * 1024 * 1024, // 5MB
|
|
140
|
+
* maxFiles: 5
|
|
141
|
+
* });
|
|
142
|
+
*
|
|
143
|
+
* console.log(payload.name); // Auto-parsed text field
|
|
144
|
+
* console.log(payload.avatar); // ParsedFile object
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
declare function parseMultipart(req: Request, options?: Partial<ParserOptions>): Promise<ParsedPayload>;
|
|
148
|
+
|
|
149
|
+
export { type MiddlewareFunction, type ParsedFile, type ParsedPayload, type ParserOptions, parseMultipart, parser };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parsed file object from multipart/form-data
|
|
5
|
+
*/
|
|
6
|
+
interface ParsedFile {
|
|
7
|
+
/** Field name in the form */
|
|
8
|
+
fieldname: string;
|
|
9
|
+
/** Original filename from client */
|
|
10
|
+
originalname: string;
|
|
11
|
+
/** File encoding */
|
|
12
|
+
encoding: string;
|
|
13
|
+
/** MIME type */
|
|
14
|
+
mimetype: string;
|
|
15
|
+
/** File size in bytes */
|
|
16
|
+
size: number;
|
|
17
|
+
/** Buffer containing file data */
|
|
18
|
+
buffer: Buffer;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Normalized payload after parsing multipart/form-data
|
|
22
|
+
*
|
|
23
|
+
* Fields are auto-parsed to appropriate types (JSON, numbers, booleans)
|
|
24
|
+
*/
|
|
25
|
+
type ParsedPayload = {
|
|
26
|
+
[key: string]: string | number | boolean | object | ParsedFile | ParsedFile[];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Extend Express Request with payload property
|
|
30
|
+
*
|
|
31
|
+
* Opt-in via import - non-invasive augmentation
|
|
32
|
+
*/
|
|
33
|
+
declare global {
|
|
34
|
+
namespace Express {
|
|
35
|
+
interface Request {
|
|
36
|
+
payload?: ParsedPayload;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Configuration options for multipart parser
|
|
42
|
+
*/
|
|
43
|
+
interface ParserOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Maximum file size in bytes
|
|
46
|
+
*
|
|
47
|
+
* @default 10485760 (10MB)
|
|
48
|
+
*/
|
|
49
|
+
maxFileSize?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Maximum number of files allowed
|
|
52
|
+
*
|
|
53
|
+
* @default 10
|
|
54
|
+
*/
|
|
55
|
+
maxFiles?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Automatically parse JSON strings in text fields
|
|
58
|
+
*
|
|
59
|
+
* @default true
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* // Field value: '{"key":"value"}'
|
|
64
|
+
* // autoParseJSON: true → { key: "value" }
|
|
65
|
+
* // autoParseJSON: false → '{"key":"value"}'
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
autoParseJSON?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Automatically convert numeric strings to numbers
|
|
71
|
+
*
|
|
72
|
+
* @default true
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* // Field value: '123'
|
|
77
|
+
* // autoParseNumbers: true → 123
|
|
78
|
+
* // autoParseNumbers: false → '123'
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
autoParseNumbers?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Automatically convert boolean strings to booleans
|
|
84
|
+
*
|
|
85
|
+
* @default true
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* // Field value: 'true' or '1'
|
|
90
|
+
* // autoParseBooleans: true → true
|
|
91
|
+
* // autoParseBooleans: false → 'true'
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
autoParseBooleans?: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Express middleware function type
|
|
98
|
+
*/
|
|
99
|
+
type MiddlewareFunction = (req: Request, res: Response, next: NextFunction) => void;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Creates Express middleware for parsing multipart/form-data
|
|
103
|
+
*
|
|
104
|
+
* @param options - Configuration options
|
|
105
|
+
* @returns Express middleware function
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* import express from 'express';
|
|
110
|
+
* import { parser } from 'formdata-io/server';
|
|
111
|
+
*
|
|
112
|
+
* const app = express();
|
|
113
|
+
*
|
|
114
|
+
* // Use with default options (10MB, 10 files)
|
|
115
|
+
* app.post('/upload', parser(), (req, res) => {
|
|
116
|
+
* const { name, avatar } = req.payload;
|
|
117
|
+
* res.json({ ok: true });
|
|
118
|
+
* });
|
|
119
|
+
*
|
|
120
|
+
* // Use with custom options
|
|
121
|
+
* app.post('/photos', parser({ maxFileSize: 5 * 1024 * 1024 }), (req, res) => {
|
|
122
|
+
* const photos = req.payload?.photos;
|
|
123
|
+
* res.json({ count: Array.isArray(photos) ? photos.length : 1 });
|
|
124
|
+
* });
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
declare function parser(options?: ParserOptions): MiddlewareFunction;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parses multipart/form-data request using busboy
|
|
131
|
+
*
|
|
132
|
+
* @param req - Express request object
|
|
133
|
+
* @param options - Parser configuration options
|
|
134
|
+
* @returns Promise resolving to normalized payload
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const payload = await parseMultipart(req, {
|
|
139
|
+
* maxFileSize: 5 * 1024 * 1024, // 5MB
|
|
140
|
+
* maxFiles: 5
|
|
141
|
+
* });
|
|
142
|
+
*
|
|
143
|
+
* console.log(payload.name); // Auto-parsed text field
|
|
144
|
+
* console.log(payload.avatar); // ParsedFile object
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
declare function parseMultipart(req: Request, options?: Partial<ParserOptions>): Promise<ParsedPayload>;
|
|
148
|
+
|
|
149
|
+
export { type MiddlewareFunction, type ParsedFile, type ParsedPayload, type ParserOptions, parseMultipart, parser };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var Busboy = require('busboy');
|
|
4
|
+
|
|
5
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
|
+
|
|
7
|
+
var Busboy__default = /*#__PURE__*/_interopDefault(Busboy);
|
|
8
|
+
|
|
9
|
+
// src/server/parser.ts
|
|
10
|
+
var DEFAULT_OPTIONS = {
|
|
11
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
12
|
+
// 10MB
|
|
13
|
+
maxFiles: 10,
|
|
14
|
+
autoParseJSON: true,
|
|
15
|
+
autoParseNumbers: true,
|
|
16
|
+
autoParseBooleans: true
|
|
17
|
+
};
|
|
18
|
+
function autoParse(value, options) {
|
|
19
|
+
if (options.autoParseJSON) {
|
|
20
|
+
if (value.startsWith("{") && value.endsWith("}") || value.startsWith("[") && value.endsWith("]")) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(value);
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (options.autoParseBooleans) {
|
|
28
|
+
if (value === "true") return true;
|
|
29
|
+
if (value === "false") return false;
|
|
30
|
+
if (value === "1") return true;
|
|
31
|
+
if (value === "0") return false;
|
|
32
|
+
}
|
|
33
|
+
if (options.autoParseNumbers) {
|
|
34
|
+
const num = Number(value);
|
|
35
|
+
if (!isNaN(num) && value.trim() !== "") {
|
|
36
|
+
return num;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function parseMultipart(req, options = {}) {
|
|
42
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const payload = {};
|
|
45
|
+
const files = [];
|
|
46
|
+
let fileCount = 0;
|
|
47
|
+
const busboy = Busboy__default.default({
|
|
48
|
+
headers: req.headers,
|
|
49
|
+
limits: {
|
|
50
|
+
fileSize: opts.maxFileSize,
|
|
51
|
+
files: opts.maxFiles
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
busboy.on("field", (fieldname, value) => {
|
|
55
|
+
const parsedValue = autoParse(value, opts);
|
|
56
|
+
if (payload[fieldname] !== void 0) {
|
|
57
|
+
if (Array.isArray(payload[fieldname])) {
|
|
58
|
+
payload[fieldname].push(parsedValue);
|
|
59
|
+
} else {
|
|
60
|
+
payload[fieldname] = [payload[fieldname], parsedValue];
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
payload[fieldname] = parsedValue;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
busboy.on(
|
|
67
|
+
"file",
|
|
68
|
+
(fieldname, file, info) => {
|
|
69
|
+
fileCount++;
|
|
70
|
+
if (fileCount > opts.maxFiles) {
|
|
71
|
+
file.resume();
|
|
72
|
+
return reject(
|
|
73
|
+
new Error(`Maximum number of files (${opts.maxFiles}) exceeded`)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const chunks = [];
|
|
77
|
+
let size = 0;
|
|
78
|
+
file.on("data", (chunk) => {
|
|
79
|
+
size += chunk.length;
|
|
80
|
+
if (size > opts.maxFileSize) {
|
|
81
|
+
file.resume();
|
|
82
|
+
return reject(
|
|
83
|
+
new Error(
|
|
84
|
+
`File size exceeds limit of ${opts.maxFileSize} bytes`
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
chunks.push(chunk);
|
|
89
|
+
});
|
|
90
|
+
file.on("end", () => {
|
|
91
|
+
const parsedFile = {
|
|
92
|
+
fieldname,
|
|
93
|
+
originalname: info.filename,
|
|
94
|
+
encoding: info.encoding,
|
|
95
|
+
mimetype: info.mimeType,
|
|
96
|
+
size,
|
|
97
|
+
buffer: Buffer.concat(chunks)
|
|
98
|
+
};
|
|
99
|
+
files.push(parsedFile);
|
|
100
|
+
});
|
|
101
|
+
file.on("error", reject);
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
busboy.on("limit", () => {
|
|
105
|
+
reject(new Error("File size limit exceeded"));
|
|
106
|
+
});
|
|
107
|
+
busboy.on("finish", () => {
|
|
108
|
+
files.forEach((file) => {
|
|
109
|
+
if (payload[file.fieldname] !== void 0) {
|
|
110
|
+
if (Array.isArray(payload[file.fieldname])) {
|
|
111
|
+
payload[file.fieldname].push(file);
|
|
112
|
+
} else {
|
|
113
|
+
payload[file.fieldname] = [payload[file.fieldname], file];
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
payload[file.fieldname] = file;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
resolve(payload);
|
|
120
|
+
});
|
|
121
|
+
busboy.on("error", reject);
|
|
122
|
+
req.pipe(busboy);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/server/middleware.ts
|
|
127
|
+
function parser(options = {}) {
|
|
128
|
+
return async (req, res, next) => {
|
|
129
|
+
const contentType = req.headers["content-type"] || "";
|
|
130
|
+
if (!contentType.includes("multipart/form-data")) {
|
|
131
|
+
return next();
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
req.payload = await parseMultipart(req, options);
|
|
135
|
+
next();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
next(error);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
exports.parseMultipart = parseMultipart;
|
|
143
|
+
exports.parser = parser;
|
|
144
|
+
//# sourceMappingURL=index.js.map
|
|
145
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import Busboy from 'busboy';
|
|
2
|
+
|
|
3
|
+
// src/server/parser.ts
|
|
4
|
+
var DEFAULT_OPTIONS = {
|
|
5
|
+
maxFileSize: 10 * 1024 * 1024,
|
|
6
|
+
// 10MB
|
|
7
|
+
maxFiles: 10,
|
|
8
|
+
autoParseJSON: true,
|
|
9
|
+
autoParseNumbers: true,
|
|
10
|
+
autoParseBooleans: true
|
|
11
|
+
};
|
|
12
|
+
function autoParse(value, options) {
|
|
13
|
+
if (options.autoParseJSON) {
|
|
14
|
+
if (value.startsWith("{") && value.endsWith("}") || value.startsWith("[") && value.endsWith("]")) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(value);
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (options.autoParseBooleans) {
|
|
22
|
+
if (value === "true") return true;
|
|
23
|
+
if (value === "false") return false;
|
|
24
|
+
if (value === "1") return true;
|
|
25
|
+
if (value === "0") return false;
|
|
26
|
+
}
|
|
27
|
+
if (options.autoParseNumbers) {
|
|
28
|
+
const num = Number(value);
|
|
29
|
+
if (!isNaN(num) && value.trim() !== "") {
|
|
30
|
+
return num;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function parseMultipart(req, options = {}) {
|
|
36
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const payload = {};
|
|
39
|
+
const files = [];
|
|
40
|
+
let fileCount = 0;
|
|
41
|
+
const busboy = Busboy({
|
|
42
|
+
headers: req.headers,
|
|
43
|
+
limits: {
|
|
44
|
+
fileSize: opts.maxFileSize,
|
|
45
|
+
files: opts.maxFiles
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
busboy.on("field", (fieldname, value) => {
|
|
49
|
+
const parsedValue = autoParse(value, opts);
|
|
50
|
+
if (payload[fieldname] !== void 0) {
|
|
51
|
+
if (Array.isArray(payload[fieldname])) {
|
|
52
|
+
payload[fieldname].push(parsedValue);
|
|
53
|
+
} else {
|
|
54
|
+
payload[fieldname] = [payload[fieldname], parsedValue];
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
payload[fieldname] = parsedValue;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
busboy.on(
|
|
61
|
+
"file",
|
|
62
|
+
(fieldname, file, info) => {
|
|
63
|
+
fileCount++;
|
|
64
|
+
if (fileCount > opts.maxFiles) {
|
|
65
|
+
file.resume();
|
|
66
|
+
return reject(
|
|
67
|
+
new Error(`Maximum number of files (${opts.maxFiles}) exceeded`)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
const chunks = [];
|
|
71
|
+
let size = 0;
|
|
72
|
+
file.on("data", (chunk) => {
|
|
73
|
+
size += chunk.length;
|
|
74
|
+
if (size > opts.maxFileSize) {
|
|
75
|
+
file.resume();
|
|
76
|
+
return reject(
|
|
77
|
+
new Error(
|
|
78
|
+
`File size exceeds limit of ${opts.maxFileSize} bytes`
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
chunks.push(chunk);
|
|
83
|
+
});
|
|
84
|
+
file.on("end", () => {
|
|
85
|
+
const parsedFile = {
|
|
86
|
+
fieldname,
|
|
87
|
+
originalname: info.filename,
|
|
88
|
+
encoding: info.encoding,
|
|
89
|
+
mimetype: info.mimeType,
|
|
90
|
+
size,
|
|
91
|
+
buffer: Buffer.concat(chunks)
|
|
92
|
+
};
|
|
93
|
+
files.push(parsedFile);
|
|
94
|
+
});
|
|
95
|
+
file.on("error", reject);
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
busboy.on("limit", () => {
|
|
99
|
+
reject(new Error("File size limit exceeded"));
|
|
100
|
+
});
|
|
101
|
+
busboy.on("finish", () => {
|
|
102
|
+
files.forEach((file) => {
|
|
103
|
+
if (payload[file.fieldname] !== void 0) {
|
|
104
|
+
if (Array.isArray(payload[file.fieldname])) {
|
|
105
|
+
payload[file.fieldname].push(file);
|
|
106
|
+
} else {
|
|
107
|
+
payload[file.fieldname] = [payload[file.fieldname], file];
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
payload[file.fieldname] = file;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
resolve(payload);
|
|
114
|
+
});
|
|
115
|
+
busboy.on("error", reject);
|
|
116
|
+
req.pipe(busboy);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/server/middleware.ts
|
|
121
|
+
function parser(options = {}) {
|
|
122
|
+
return async (req, res, next) => {
|
|
123
|
+
const contentType = req.headers["content-type"] || "";
|
|
124
|
+
if (!contentType.includes("multipart/form-data")) {
|
|
125
|
+
return next();
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
req.payload = await parseMultipart(req, options);
|
|
129
|
+
next();
|
|
130
|
+
} catch (error) {
|
|
131
|
+
next(error);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { parseMultipart, parser };
|
|
137
|
+
//# sourceMappingURL=index.mjs.map
|
|
138
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/server/parser.ts","../../src/server/middleware.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import Busboy from 'busboy';\nimport type { Request } from 'express';\nimport type { ParsedFile, ParsedPayload, ParserOptions } from './types';\n\nconst DEFAULT_OPTIONS: Required<ParserOptions> = {\n maxFileSize: 10 * 1024 * 1024, // 10MB\n maxFiles: 10,\n autoParseJSON: true,\n autoParseNumbers: true,\n autoParseBooleans: true,\n};\n\n/**\n * Attempts to automatically parse a string value to appropriate type\n *\n * @param value - String value to parse\n * @param options - Parser options\n * @returns Parsed value (JSON object, number, boolean, or original string)\n */\nfunction autoParse(\n value: string,\n options: Required<ParserOptions>\n): string | number | boolean | object {\n // 1. Try JSON parsing (if starts/ends with {} or [])\n if (options.autoParseJSON) {\n if (\n (value.startsWith('{') && value.endsWith('}')) ||\n (value.startsWith('[') && value.endsWith(']'))\n ) {\n try {\n return JSON.parse(value);\n } catch {\n // Fallback to string if not valid JSON\n }\n }\n }\n\n // 2. Try boolean conversion\n if (options.autoParseBooleans) {\n if (value === 'true') return true;\n if (value === 'false') return false;\n if (value === '1') return true;\n if (value === '0') return false;\n }\n\n // 3. Try number conversion\n if (options.autoParseNumbers) {\n const num = Number(value);\n if (!isNaN(num) && value.trim() !== '') {\n return num;\n }\n }\n\n // 4. Return as string\n return value;\n}\n\n/**\n * Parses multipart/form-data request using busboy\n *\n * @param req - Express request object\n * @param options - Parser configuration options\n * @returns Promise resolving to normalized payload\n *\n * @example\n * ```typescript\n * const payload = await parseMultipart(req, {\n * maxFileSize: 5 * 1024 * 1024, // 5MB\n * maxFiles: 5\n * });\n *\n * console.log(payload.name); // Auto-parsed text field\n * console.log(payload.avatar); // ParsedFile object\n * ```\n */\nexport function parseMultipart(\n req: Request,\n options: Partial<ParserOptions> = {}\n): Promise<ParsedPayload> {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n return new Promise((resolve, reject) => {\n const payload: ParsedPayload = {};\n const files: ParsedFile[] = [];\n let fileCount = 0;\n\n const busboy = Busboy({\n headers: req.headers as any,\n limits: {\n fileSize: opts.maxFileSize,\n files: opts.maxFiles,\n },\n });\n\n // Handler for text fields\n busboy.on('field', (fieldname: string, value: string) => {\n const parsedValue = autoParse(value, opts);\n\n // If field already exists, convert to array\n if (payload[fieldname] !== undefined) {\n if (Array.isArray(payload[fieldname])) {\n (payload[fieldname] as any[]).push(parsedValue);\n } else {\n payload[fieldname] = [payload[fieldname], parsedValue];\n }\n } else {\n payload[fieldname] = parsedValue;\n }\n });\n\n // Handler for file uploads\n busboy.on(\n 'file',\n (\n fieldname: string,\n file: NodeJS.ReadableStream,\n info: { filename: string; encoding: string; mimeType: string }\n ) => {\n fileCount++;\n\n // Enforce file count limit\n if (fileCount > opts.maxFiles) {\n file.resume(); // CRITICAL: drain stream to prevent backpressure\n return reject(\n new Error(`Maximum number of files (${opts.maxFiles}) exceeded`)\n );\n }\n\n const chunks: Buffer[] = [];\n let size = 0;\n\n file.on('data', (chunk: Buffer) => {\n size += chunk.length;\n\n // Enforce size limit\n if (size > opts.maxFileSize) {\n file.resume(); // CRITICAL: drain stream\n return reject(\n new Error(\n `File size exceeds limit of ${opts.maxFileSize} bytes`\n )\n );\n }\n\n chunks.push(chunk);\n });\n\n file.on('end', () => {\n const parsedFile: ParsedFile = {\n fieldname,\n originalname: info.filename,\n encoding: info.encoding,\n mimetype: info.mimeType,\n size,\n buffer: Buffer.concat(chunks),\n };\n\n files.push(parsedFile);\n });\n\n file.on('error', reject);\n }\n );\n\n // Handler for limit exceeded\n busboy.on('limit', () => {\n reject(new Error('File size limit exceeded'));\n });\n\n // Handler for parsing completion\n busboy.on('finish', () => {\n // Normalize files into payload\n files.forEach((file) => {\n if (payload[file.fieldname] !== undefined) {\n // Field already exists, convert to array\n if (Array.isArray(payload[file.fieldname])) {\n (payload[file.fieldname] as ParsedFile[]).push(file);\n } else {\n payload[file.fieldname] = [payload[file.fieldname] as ParsedFile, file];\n }\n } else {\n payload[file.fieldname] = file;\n }\n });\n\n resolve(payload);\n });\n\n // Handler for parsing errors\n busboy.on('error', reject);\n\n // Pipe request stream to busboy\n req.pipe(busboy);\n });\n}\n","import type { Request, Response, NextFunction } from 'express';\nimport { parseMultipart } from './parser';\nimport type { ParserOptions, MiddlewareFunction } from './types';\n\n/**\n * Creates Express middleware for parsing multipart/form-data\n *\n * @param options - Configuration options\n * @returns Express middleware function\n *\n * @example\n * ```typescript\n * import express from 'express';\n * import { parser } from 'formdata-io/server';\n *\n * const app = express();\n *\n * // Use with default options (10MB, 10 files)\n * app.post('/upload', parser(), (req, res) => {\n * const { name, avatar } = req.payload;\n * res.json({ ok: true });\n * });\n *\n * // Use with custom options\n * app.post('/photos', parser({ maxFileSize: 5 * 1024 * 1024 }), (req, res) => {\n * const photos = req.payload?.photos;\n * res.json({ count: Array.isArray(photos) ? photos.length : 1 });\n * });\n * ```\n */\nexport function parser(options: ParserOptions = {}): MiddlewareFunction {\n return async (req: Request, res: Response, next: NextFunction) => {\n // Only process multipart/form-data requests\n const contentType = req.headers['content-type'] || '';\n if (!contentType.includes('multipart/form-data')) {\n return next();\n }\n\n try {\n req.payload = await parseMultipart(req, options);\n next();\n } catch (error) {\n // Pass error to Express error handler\n next(error);\n }\n };\n}\n"]}
|