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.
@@ -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":";;;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"]}