json-rest-api 1.0.0 → 1.0.1

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.
Files changed (67) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/LICENSE +674 -0
  3. package/README.md +655 -309
  4. package/RELATIONSHIPS.md +1013 -0
  5. package/TODO_rest-api-plugin-extras.js +142 -0
  6. package/connectors_ideas/express-plugin.js +120 -0
  7. package/connectors_ideas/fastify.js +219 -0
  8. package/connectors_ideas/http-plugin.js +138 -0
  9. package/connectors_ideas/koa-plugin.js +113 -0
  10. package/docs/fileUploads.md +722 -0
  11. package/docs/mini-guides/file-uploads-complete.md +159 -0
  12. package/examples/.gitkeep +0 -0
  13. package/examples/express-example.js +262 -0
  14. package/examples/express-plugin.js +366 -0
  15. package/examples/file-upload-example.js +205 -0
  16. package/examples/filename-strategies.js +140 -0
  17. package/examples/knex-basic.js +66 -0
  18. package/index.js +31 -56
  19. package/lib/payload-validators.js +685 -0
  20. package/lib/rest-api-errors.js +58 -0
  21. package/package.json +41 -14
  22. package/payloadValdators.js +686 -0
  23. package/plugins/core/connectors/express-plugin.js +449 -0
  24. package/plugins/core/connectors/http-plugin.js +450 -0
  25. package/plugins/core/file-handling-plugin.js +338 -0
  26. package/plugins/core/lib/busboy-detector.js +167 -0
  27. package/plugins/core/lib/formidable-detector.js +130 -0
  28. package/plugins/core/rest-api-knex-plugin.js +200 -0
  29. package/plugins/core/rest-api-plugin.js +1419 -0
  30. package/plugins/storage/local-storage.js +218 -0
  31. package/plugins/storage/s3-storage.js +125 -0
  32. package/test-examples/package-lock.json +906 -0
  33. package/test-examples/package.json +15 -0
  34. package/test-examples/test-basic-api.js +11 -0
  35. package/test-examples/test-books-resource.js +116 -0
  36. package/test-examples/test-complete-file-upload.js +140 -0
  37. package/test-examples/test-debug.js +28 -0
  38. package/test-examples/test-express-plugin.js +177 -0
  39. package/test-examples/test-express-simple.js +28 -0
  40. package/test-examples/test-express-with-import.js +71 -0
  41. package/test-examples/test-file-upload.js +132 -0
  42. package/test-examples/test-http-plugin.js +159 -0
  43. package/test-examples/test-programmatic-api.js +188 -0
  44. package/test-examples/test-rest-api-plugin.js +13 -0
  45. package/TODO.todo +0 -113
  46. package/api.js +0 -628
  47. package/docs/API.md +0 -1306
  48. package/docs/FIXES_APPLIED.md +0 -59
  49. package/docs/GUIDES.md +0 -1792
  50. package/docs/RESOURCES_IN_EXPRESS.md +0 -456
  51. package/docs/example-versioning.js +0 -168
  52. package/docs/example.js +0 -195
  53. package/plugins/API guide.md +0 -364
  54. package/plugins/api-registry.js +0 -142
  55. package/plugins/api-versioning.md +0 -135
  56. package/plugins/http.js +0 -273
  57. package/plugins/logging.js +0 -255
  58. package/plugins/memory.js +0 -182
  59. package/plugins/mysql.js +0 -753
  60. package/plugins/openapi.js +0 -401
  61. package/plugins/positioning.js +0 -220
  62. package/plugins/security.js +0 -261
  63. package/plugins/validation.js +0 -173
  64. package/plugins/versioning.js +0 -288
  65. package/resource-helper.js +0 -79
  66. package/schema.js +0 -481
  67. package/test-basic.js +0 -107
@@ -0,0 +1,338 @@
1
+ /**
2
+ * File Handling Plugin for JSON REST API
3
+ *
4
+ * This plugin provides automatic file upload handling based on schema definitions.
5
+ * It works with any protocol connector (HTTP, Express, WebSocket, etc.) by using
6
+ * a detector registry pattern.
7
+ *
8
+ * Features:
9
+ * - Schema-driven: Detects file fields from type: 'file' in schemas
10
+ * - Protocol-agnostic: Works with any connector that registers a detector
11
+ * - Storage pluggable: Different fields can use different storage backends
12
+ * - Zero configuration: Just define file fields in your schema
13
+ *
14
+ * Usage:
15
+ * ```javascript
16
+ * // 1. Define schema with file fields
17
+ * const imageSchema = {
18
+ * title: { type: 'string' },
19
+ * uploadedImage: {
20
+ * type: 'file',
21
+ * storage: S3Storage,
22
+ * accepts: ['image/jpeg', 'image/png']
23
+ * }
24
+ * };
25
+ *
26
+ * // 2. Use plugins (order matters - file-handling depends on rest-api)
27
+ * api.use(RestApiPlugin);
28
+ * api.use(FileHandlingPlugin);
29
+ * api.use(ExpressPlugin); // Or any other connector
30
+ *
31
+ * // 3. Files are automatically handled!
32
+ * ```
33
+ */
34
+
35
+ import { RestApiValidationError } from '../../lib/rest-api-errors.js';
36
+
37
+ export const FileHandlingPlugin = {
38
+ name: 'file-handling',
39
+ dependencies: ['rest-api'],
40
+
41
+ install({ addHook, helpers, scopes, log, vars, on, api }) {
42
+ // Track which scopes have file fields
43
+ const fileScopes = new Map(); // scopeName -> fileField[]
44
+
45
+ // Registry of file detectors from various protocols
46
+ const detectorRegistry = [];
47
+
48
+ /**
49
+ * Register a file detector from a protocol plugin
50
+ *
51
+ * @param {Object} detector - The detector object
52
+ * @param {string} detector.name - Name of the detector (e.g., 'http-multipart')
53
+ * @param {Function} detector.detect - Function to check if this detector applies
54
+ * @param {Function} detector.parse - Function to parse files from the request
55
+ */
56
+ api.rest.registerFileDetector = (detector) => {
57
+ if (!detector || !detector.name || !detector.detect || !detector.parse) {
58
+ throw new Error('File detector must have name, detect(), and parse() properties');
59
+ }
60
+
61
+ detectorRegistry.push(detector);
62
+ log.debug(`Registered file detector: ${detector.name}`);
63
+ };
64
+
65
+ // Store detectors array for inspection
66
+ api.rest.fileDetectors = detectorRegistry;
67
+
68
+ /**
69
+ * Analyze a scope's schema to find file fields
70
+ */
71
+ const analyzeScopeSchema = (scopeName, scopeOptions) => {
72
+ const schema = scopeOptions?.schema;
73
+ if (!schema) return;
74
+
75
+ const fileFields = [];
76
+
77
+ // Look for fields with type: 'file'
78
+ for (const [fieldName, fieldConfig] of Object.entries(schema)) {
79
+ if (fieldConfig && fieldConfig.type === 'file') {
80
+ fileFields.push({
81
+ field: fieldName,
82
+ storage: fieldConfig.storage,
83
+ accepts: fieldConfig.accepts || ['*'],
84
+ maxSize: fieldConfig.maxSize,
85
+ required: fieldConfig.required || false
86
+ });
87
+ }
88
+ }
89
+
90
+ if (fileFields.length > 0) {
91
+ fileScopes.set(scopeName, fileFields);
92
+ log.info(`Scope '${scopeName}' has ${fileFields.length} file field(s): ${fileFields.map(f => f.field).join(', ')}`);
93
+ }
94
+ };
95
+
96
+ // Analyze existing scopes
97
+ for (const [scopeName, scope] of Object.entries(scopes)) {
98
+ if (scope._scopeOptions) {
99
+ analyzeScopeSchema(scopeName, scope._scopeOptions);
100
+ }
101
+ }
102
+
103
+ // Listen for new scopes being added
104
+ on('scope:added', 'analyzeFileFields', ({ eventData }) => {
105
+ const { scopeName, scopeOptions } = eventData;
106
+ analyzeScopeSchema(scopeName, scopeOptions);
107
+ });
108
+
109
+ /**
110
+ * Process files for a scope if it has file fields
111
+ */
112
+ const processFiles = async (scopeName, params) => {
113
+ const fileFields = fileScopes.get(scopeName);
114
+ if (!fileFields || fileFields.length === 0) {
115
+ return; // This scope doesn't have file fields
116
+ }
117
+
118
+ // Try each detector to see if we have files
119
+ let parsed = null;
120
+ let detectorUsed = null;
121
+
122
+ for (const detector of detectorRegistry) {
123
+ try {
124
+ if (await detector.detect(params)) {
125
+ log.debug(`Detector '${detector.name}' matched for scope '${scopeName}'`);
126
+ parsed = await detector.parse(params);
127
+ detectorUsed = detector.name;
128
+ break;
129
+ }
130
+ } catch (error) {
131
+ log.warn(`Detector '${detector.name}' failed:`, error);
132
+ }
133
+ }
134
+
135
+ if (!parsed) {
136
+ // No files detected - check if any were required
137
+ for (const fieldConfig of fileFields) {
138
+ if (fieldConfig.required && !params.inputRecord?.data?.attributes?.[fieldConfig.field]) {
139
+ throw new RestApiValidationError(
140
+ `Required file field '${fieldConfig.field}' is missing`,
141
+ {
142
+ fields: [fieldConfig.field],
143
+ violations: [{
144
+ field: fieldConfig.field,
145
+ message: 'This field is required'
146
+ }]
147
+ }
148
+ );
149
+ }
150
+ }
151
+ return;
152
+ }
153
+
154
+ log.debug(`Processing files with detector '${detectorUsed}'`);
155
+ const { fields, files } = parsed;
156
+
157
+ // Process each file field defined in schema
158
+ for (const fieldConfig of fileFields) {
159
+ const file = files[fieldConfig.field];
160
+
161
+ if (!file) {
162
+ if (fieldConfig.required) {
163
+ throw new RestApiValidationError(
164
+ `Required file field '${fieldConfig.field}' is missing`,
165
+ {
166
+ fields: [fieldConfig.field],
167
+ violations: [{
168
+ field: fieldConfig.field,
169
+ message: 'This field is required'
170
+ }]
171
+ }
172
+ );
173
+ }
174
+ continue;
175
+ }
176
+
177
+ // Validate mime type
178
+ if (fieldConfig.accepts[0] !== '*') {
179
+ const acceptable = fieldConfig.accepts.some(pattern => {
180
+ if (pattern.endsWith('/*')) {
181
+ // e.g., 'image/*'
182
+ const prefix = pattern.slice(0, -2);
183
+ return file.mimetype.startsWith(prefix + '/');
184
+ }
185
+ return file.mimetype === pattern;
186
+ });
187
+
188
+ if (!acceptable) {
189
+ throw new RestApiValidationError(
190
+ `Invalid file type for field '${fieldConfig.field}'`,
191
+ {
192
+ fields: [fieldConfig.field],
193
+ violations: [{
194
+ field: fieldConfig.field,
195
+ message: `Expected ${fieldConfig.accepts.join(' or ')}, got ${file.mimetype}`
196
+ }]
197
+ }
198
+ );
199
+ }
200
+ }
201
+
202
+ // Validate file size
203
+ if (fieldConfig.maxSize) {
204
+ const maxBytes = parseSize(fieldConfig.maxSize);
205
+ if (file.size > maxBytes) {
206
+ throw new RestApiValidationError(
207
+ `File too large for field '${fieldConfig.field}'`,
208
+ {
209
+ fields: [fieldConfig.field],
210
+ violations: [{
211
+ field: fieldConfig.field,
212
+ message: `Maximum size is ${fieldConfig.maxSize}, got ${formatSize(file.size)}`
213
+ }]
214
+ }
215
+ );
216
+ }
217
+ }
218
+
219
+ // Upload to storage
220
+ if (!fieldConfig.storage) {
221
+ throw new Error(`No storage configured for file field '${fieldConfig.field}'`);
222
+ }
223
+
224
+ try {
225
+ const storedUrl = await fieldConfig.storage.upload(file);
226
+ fields[fieldConfig.field] = storedUrl;
227
+ log.debug(`Uploaded file for field '${fieldConfig.field}' to: ${storedUrl}`);
228
+ } catch (error) {
229
+ // Cleanup if file has cleanup function
230
+ if (file.cleanup) {
231
+ try {
232
+ await file.cleanup();
233
+ } catch (cleanupError) {
234
+ log.warn(`Failed to cleanup file after upload error:`, cleanupError);
235
+ }
236
+ }
237
+
238
+ throw new RestApiValidationError(
239
+ `Failed to upload file for field '${fieldConfig.field}': ${error.message}`,
240
+ {
241
+ fields: [fieldConfig.field],
242
+ violations: [{
243
+ field: fieldConfig.field,
244
+ message: error.message
245
+ }]
246
+ }
247
+ );
248
+ }
249
+ }
250
+
251
+ // Replace inputRecord with processed data
252
+ if (!params.inputRecord) {
253
+ params.inputRecord = { data: { attributes: {} } };
254
+ }
255
+ if (!params.inputRecord.data) {
256
+ params.inputRecord.data = { attributes: {} };
257
+ }
258
+ if (!params.inputRecord.data.attributes) {
259
+ params.inputRecord.data.attributes = {};
260
+ }
261
+
262
+ // Merge fields into attributes
263
+ Object.assign(params.inputRecord.data.attributes, fields);
264
+
265
+ // Cleanup any remaining temp files
266
+ for (const file of Object.values(files)) {
267
+ if (file.cleanup) {
268
+ try {
269
+ await file.cleanup();
270
+ } catch (error) {
271
+ log.warn(`Failed to cleanup temp file:`, error);
272
+ }
273
+ }
274
+ }
275
+ };
276
+
277
+ /**
278
+ * Hook into REST API methods to process files
279
+ */
280
+ addHook('beforeProcessing', 'processFiles', {}, async (context) => {
281
+ // Only process for mutation methods
282
+ if (!['post', 'put', 'patch'].includes(context.method)) {
283
+ return;
284
+ }
285
+
286
+ // Process files if this scope has file fields
287
+ await processFiles(context.scopeName, context.params);
288
+ });
289
+
290
+ log.info('File handling plugin initialized successfully');
291
+ }
292
+ };
293
+
294
+ /**
295
+ * Parse size string to bytes
296
+ * @param {string} size - Size string like '10mb', '1.5GB'
297
+ * @returns {number} Size in bytes
298
+ */
299
+ function parseSize(size) {
300
+ const units = {
301
+ b: 1,
302
+ kb: 1024,
303
+ mb: 1024 * 1024,
304
+ gb: 1024 * 1024 * 1024
305
+ };
306
+
307
+ const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*([a-z]+)$/);
308
+ if (!match) {
309
+ throw new Error(`Invalid size format: ${size}`);
310
+ }
311
+
312
+ const [, num, unit] = match;
313
+ const multiplier = units[unit];
314
+
315
+ if (!multiplier) {
316
+ throw new Error(`Unknown size unit: ${unit}`);
317
+ }
318
+
319
+ return parseFloat(num) * multiplier;
320
+ }
321
+
322
+ /**
323
+ * Format bytes to human readable size
324
+ * @param {number} bytes - Size in bytes
325
+ * @returns {string} Human readable size
326
+ */
327
+ function formatSize(bytes) {
328
+ const units = ['B', 'KB', 'MB', 'GB'];
329
+ let size = bytes;
330
+ let unitIndex = 0;
331
+
332
+ while (size >= 1024 && unitIndex < units.length - 1) {
333
+ size /= 1024;
334
+ unitIndex++;
335
+ }
336
+
337
+ return `${size.toFixed(1)}${units[unitIndex]}`;
338
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Busboy File Detector for HTTP Multipart Uploads
3
+ *
4
+ * This detector handles multipart/form-data uploads using the busboy library.
5
+ * It's suitable for both HTTP and Express connectors.
6
+ *
7
+ * Features:
8
+ * - Streaming parser (memory efficient)
9
+ * - Configurable file size limits
10
+ * - Automatic field parsing
11
+ * - No temporary files by default (keeps in memory)
12
+ *
13
+ * Usage:
14
+ * ```javascript
15
+ * import { createBusboyDetector } from 'jsonrestapi/plugins/core/lib/busboy-detector.js';
16
+ *
17
+ * api.use(HttpPlugin, {
18
+ * fileParser: 'busboy',
19
+ * fileParserOptions: {
20
+ * limits: { fileSize: 10 * 1024 * 1024 } // 10MB
21
+ * }
22
+ * });
23
+ * ```
24
+ */
25
+
26
+ import Busboy from 'busboy';
27
+ import { Readable } from 'stream';
28
+
29
+ /**
30
+ * Creates a busboy-based file detector
31
+ *
32
+ * @param {Object} options - Busboy configuration options
33
+ * @param {Object} options.limits - Size limits
34
+ * @param {number} options.limits.fileSize - Max file size in bytes
35
+ * @param {number} options.limits.files - Max number of files
36
+ * @param {number} options.limits.fields - Max number of fields
37
+ * @returns {Object} Detector object with detect() and parse() methods
38
+ */
39
+ export function createBusboyDetector(options = {}) {
40
+ return {
41
+ name: 'busboy-multipart',
42
+
43
+ /**
44
+ * Check if this detector can handle the request
45
+ * @param {Object} params - Request parameters
46
+ * @returns {boolean} True if this is a multipart request
47
+ */
48
+ detect: (params) => {
49
+ const req = params._httpReq || params._expressReq;
50
+ if (!req || !req.headers) return false;
51
+
52
+ const contentType = req.headers['content-type'] || '';
53
+ return contentType.includes('multipart/form-data');
54
+ },
55
+
56
+ /**
57
+ * Parse multipart data from the request
58
+ * @param {Object} params - Request parameters
59
+ * @returns {Promise<{fields: Object, files: Object}>} Parsed data
60
+ */
61
+ parse: async (params) => {
62
+ const req = params._httpReq || params._expressReq;
63
+
64
+ return new Promise((resolve, reject) => {
65
+ const busboy = new Busboy({
66
+ headers: req.headers,
67
+ ...options
68
+ });
69
+
70
+ const fields = {};
71
+ const files = {};
72
+ const filePromises = [];
73
+
74
+ // Handle fields
75
+ busboy.on('field', (fieldname, val) => {
76
+ // Handle array notation (field[] or field[0])
77
+ const arrayMatch = fieldname.match(/^(.+)\[\d*\]$/);
78
+ if (arrayMatch) {
79
+ const baseName = arrayMatch[1];
80
+ if (!fields[baseName]) {
81
+ fields[baseName] = [];
82
+ }
83
+ if (Array.isArray(fields[baseName])) {
84
+ fields[baseName].push(val);
85
+ }
86
+ } else {
87
+ fields[fieldname] = val;
88
+ }
89
+ });
90
+
91
+ // Handle files
92
+ busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
93
+ const chunks = [];
94
+ let size = 0;
95
+
96
+ const filePromise = new Promise((fileResolve, fileReject) => {
97
+ file.on('data', (chunk) => {
98
+ chunks.push(chunk);
99
+ size += chunk.length;
100
+ });
101
+
102
+ file.on('limit', () => {
103
+ fileReject(new Error(`File size limit exceeded for field '${fieldname}'`));
104
+ });
105
+
106
+ file.on('end', () => {
107
+ files[fieldname] = {
108
+ filename: filename,
109
+ mimetype: mimetype,
110
+ encoding: encoding,
111
+ size: size,
112
+ data: Buffer.concat(chunks)
113
+ };
114
+ fileResolve();
115
+ });
116
+
117
+ file.on('error', fileReject);
118
+ });
119
+
120
+ filePromises.push(filePromise);
121
+ });
122
+
123
+ // Handle completion
124
+ busboy.on('finish', async () => {
125
+ try {
126
+ // Wait for all files to be fully read
127
+ await Promise.all(filePromises);
128
+ resolve({ fields, files });
129
+ } catch (error) {
130
+ reject(error);
131
+ }
132
+ });
133
+
134
+ // Handle errors
135
+ busboy.on('error', (error) => {
136
+ reject(error);
137
+ });
138
+
139
+ // Handle limit errors
140
+ busboy.on('partsLimit', () => {
141
+ reject(new Error('Parts limit exceeded'));
142
+ });
143
+
144
+ busboy.on('filesLimit', () => {
145
+ reject(new Error('Files limit exceeded'));
146
+ });
147
+
148
+ busboy.on('fieldsLimit', () => {
149
+ reject(new Error('Fields limit exceeded'));
150
+ });
151
+
152
+ // Pipe request to busboy
153
+ if (req.pipe) {
154
+ req.pipe(busboy);
155
+ } else if (req.on) {
156
+ // For already buffered requests
157
+ const stream = new Readable();
158
+ stream.push(req.body || req);
159
+ stream.push(null);
160
+ stream.pipe(busboy);
161
+ } else {
162
+ reject(new Error('Request object is not a stream'));
163
+ }
164
+ });
165
+ }
166
+ };
167
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Formidable File Detector for HTTP Multipart Uploads
3
+ *
4
+ * This detector handles multipart/form-data uploads using the formidable library.
5
+ * Unlike busboy, formidable saves files to disk temporarily, which is better
6
+ * for large files.
7
+ *
8
+ * Features:
9
+ * - Disk-based storage (better for large files)
10
+ * - Automatic temp file cleanup
11
+ * - Progress tracking support
12
+ * - Built-in file type detection
13
+ *
14
+ * Usage:
15
+ * ```javascript
16
+ * import { createFormidableDetector } from 'jsonrestapi/plugins/core/lib/formidable-detector.js';
17
+ *
18
+ * api.use(ExpressPlugin, {
19
+ * fileParser: 'formidable',
20
+ * fileParserOptions: {
21
+ * uploadDir: './uploads/temp',
22
+ * keepExtensions: true,
23
+ * maxFileSize: 200 * 1024 * 1024 // 200MB
24
+ * }
25
+ * });
26
+ * ```
27
+ */
28
+
29
+ import formidable from 'formidable';
30
+ import { promises as fs } from 'fs';
31
+ import path from 'path';
32
+
33
+ /**
34
+ * Creates a formidable-based file detector
35
+ *
36
+ * @param {Object} options - Formidable configuration options
37
+ * @param {string} options.uploadDir - Directory for temporary files
38
+ * @param {boolean} options.keepExtensions - Keep file extensions
39
+ * @param {number} options.maxFileSize - Max file size in bytes
40
+ * @param {boolean} options.multiples - Allow multiple files per field
41
+ * @returns {Object} Detector object with detect() and parse() methods
42
+ */
43
+ export function createFormidableDetector(options = {}) {
44
+ // Set default upload directory
45
+ const uploadDir = options.uploadDir || path.join(process.cwd(), 'uploads', 'temp');
46
+
47
+ return {
48
+ name: 'formidable-multipart',
49
+
50
+ /**
51
+ * Check if this detector can handle the request
52
+ * @param {Object} params - Request parameters
53
+ * @returns {boolean} True if this is a multipart request
54
+ */
55
+ detect: (params) => {
56
+ const req = params._httpReq || params._expressReq;
57
+ if (!req || !req.headers) return false;
58
+
59
+ const contentType = req.headers['content-type'] || '';
60
+ return contentType.includes('multipart/form-data');
61
+ },
62
+
63
+ /**
64
+ * Parse multipart data from the request
65
+ * @param {Object} params - Request parameters
66
+ * @returns {Promise<{fields: Object, files: Object}>} Parsed data
67
+ */
68
+ parse: async (params) => {
69
+ const req = params._httpReq || params._expressReq;
70
+
71
+ // Ensure upload directory exists
72
+ await fs.mkdir(uploadDir, { recursive: true });
73
+
74
+ // Create form parser
75
+ const form = formidable({
76
+ uploadDir,
77
+ keepExtensions: true,
78
+ ...options
79
+ });
80
+
81
+ // Parse the request
82
+ const [fields, fileUploads] = await form.parse(req);
83
+
84
+ // Normalize formidable's file format to our standard format
85
+ const files = {};
86
+
87
+ for (const [fieldname, uploadedFiles] of Object.entries(fileUploads)) {
88
+ // Formidable returns arrays for multiple files
89
+ const fileArray = Array.isArray(uploadedFiles) ? uploadedFiles : [uploadedFiles];
90
+
91
+ // For now, just take the first file (TODO: handle multiple files per field)
92
+ const file = fileArray[0];
93
+
94
+ if (file) {
95
+ // Read file data into memory (for small files)
96
+ // For large files, you might want to keep them on disk
97
+ const data = await fs.readFile(file.filepath);
98
+
99
+ files[fieldname] = {
100
+ filename: file.originalFilename || 'unknown',
101
+ mimetype: file.mimetype || 'application/octet-stream',
102
+ size: file.size,
103
+ data: data,
104
+ filepath: file.filepath, // Keep for reference
105
+
106
+ // Cleanup function to remove temp file
107
+ cleanup: async () => {
108
+ try {
109
+ await fs.unlink(file.filepath);
110
+ } catch (error) {
111
+ // File might already be deleted, ignore
112
+ if (error.code !== 'ENOENT') {
113
+ throw error;
114
+ }
115
+ }
116
+ }
117
+ };
118
+ }
119
+ }
120
+
121
+ // Normalize fields (formidable returns arrays for repeated fields)
122
+ const normalizedFields = {};
123
+ for (const [key, value] of Object.entries(fields)) {
124
+ normalizedFields[key] = Array.isArray(value) && value.length === 1 ? value[0] : value;
125
+ }
126
+
127
+ return { fields: normalizedFields, files };
128
+ }
129
+ };
130
+ }