navis.js 5.0.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/examples/v5.1-features-demo.js +192 -0
- package/examples/v5.2-features-demo.js +153 -0
- package/package.json +1 -1
- package/src/core/versioning.js +124 -0
- package/src/db/db-pool.js +195 -0
- package/src/docs/swagger.js +188 -0
- package/src/index.js +30 -0
- package/src/middleware/upload.js +151 -0
- package/src/sse/server-sent-events.js +171 -0
- package/src/testing/test-helper.js +167 -0
- package/src/websocket/websocket-server.js +301 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Connection Pool
|
|
3
|
+
* v5.2: Database integration helpers with connection pooling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class DatabasePool {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.type = options.type || 'postgres';
|
|
9
|
+
this.connectionString = options.connectionString || process.env.DATABASE_URL;
|
|
10
|
+
this.pool = null;
|
|
11
|
+
this.maxConnections = options.maxConnections || 10;
|
|
12
|
+
this.minConnections = options.minConnections || 2;
|
|
13
|
+
this.idleTimeout = options.idleTimeout || 30000;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize connection pool
|
|
18
|
+
*/
|
|
19
|
+
async connect() {
|
|
20
|
+
if (this.pool) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
switch (this.type.toLowerCase()) {
|
|
25
|
+
case 'postgres':
|
|
26
|
+
case 'postgresql':
|
|
27
|
+
await this._connectPostgres();
|
|
28
|
+
break;
|
|
29
|
+
case 'mysql':
|
|
30
|
+
case 'mariadb':
|
|
31
|
+
await this._connectMySQL();
|
|
32
|
+
break;
|
|
33
|
+
case 'mongodb':
|
|
34
|
+
await this._connectMongoDB();
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`Unsupported database type: ${this.type}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Connect to PostgreSQL
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
async _connectPostgres() {
|
|
46
|
+
try {
|
|
47
|
+
const { Pool } = require('pg');
|
|
48
|
+
this.pool = new Pool({
|
|
49
|
+
connectionString: this.connectionString,
|
|
50
|
+
max: this.maxConnections,
|
|
51
|
+
min: this.minConnections,
|
|
52
|
+
idleTimeoutMillis: this.idleTimeout,
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new Error('pg package not installed. Install with: npm install pg');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Connect to MySQL
|
|
61
|
+
* @private
|
|
62
|
+
*/
|
|
63
|
+
async _connectMySQL() {
|
|
64
|
+
try {
|
|
65
|
+
const mysql = require('mysql2/promise');
|
|
66
|
+
this.pool = mysql.createPool({
|
|
67
|
+
uri: this.connectionString,
|
|
68
|
+
connectionLimit: this.maxConnections,
|
|
69
|
+
waitForConnections: true,
|
|
70
|
+
idleTimeout: this.idleTimeout,
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error('mysql2 package not installed. Install with: npm install mysql2');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Connect to MongoDB
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
async _connectMongoDB() {
|
|
82
|
+
try {
|
|
83
|
+
const { MongoClient } = require('mongodb');
|
|
84
|
+
const client = new MongoClient(this.connectionString);
|
|
85
|
+
await client.connect();
|
|
86
|
+
this.pool = client;
|
|
87
|
+
this.db = client.db();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error('mongodb package not installed. Install with: npm install mongodb');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Execute a query
|
|
95
|
+
* @param {string} query - SQL query or MongoDB operation
|
|
96
|
+
* @param {Array} params - Query parameters
|
|
97
|
+
* @returns {Promise<*>} - Query result
|
|
98
|
+
*/
|
|
99
|
+
async query(query, params = []) {
|
|
100
|
+
if (!this.pool) {
|
|
101
|
+
await this.connect();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
switch (this.type.toLowerCase()) {
|
|
105
|
+
case 'postgres':
|
|
106
|
+
case 'postgresql':
|
|
107
|
+
return await this.pool.query(query, params);
|
|
108
|
+
case 'mysql':
|
|
109
|
+
case 'mariadb':
|
|
110
|
+
const [rows] = await this.pool.execute(query, params);
|
|
111
|
+
return rows;
|
|
112
|
+
case 'mongodb':
|
|
113
|
+
// MongoDB uses different query syntax
|
|
114
|
+
// This is a placeholder - implement based on your needs
|
|
115
|
+
return await this.db.collection(query).find(params[0] || {}).toArray();
|
|
116
|
+
default:
|
|
117
|
+
throw new Error(`Unsupported database type: ${this.type}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get a connection from pool
|
|
123
|
+
* @returns {Promise<Object>} - Database connection
|
|
124
|
+
*/
|
|
125
|
+
async getConnection() {
|
|
126
|
+
if (!this.pool) {
|
|
127
|
+
await this.connect();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this.type === 'mongodb') {
|
|
131
|
+
return this.pool;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return await this.pool.getConnection();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Close connection pool
|
|
139
|
+
*/
|
|
140
|
+
async close() {
|
|
141
|
+
if (this.pool) {
|
|
142
|
+
if (this.pool.end) {
|
|
143
|
+
await this.pool.end();
|
|
144
|
+
} else if (this.pool.close) {
|
|
145
|
+
await this.pool.close();
|
|
146
|
+
}
|
|
147
|
+
this.pool = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Ping database connection
|
|
153
|
+
* @returns {Promise<boolean>} - True if connection is alive
|
|
154
|
+
*/
|
|
155
|
+
async ping() {
|
|
156
|
+
try {
|
|
157
|
+
if (!this.pool) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
switch (this.type.toLowerCase()) {
|
|
162
|
+
case 'postgres':
|
|
163
|
+
case 'postgresql':
|
|
164
|
+
await this.pool.query('SELECT 1');
|
|
165
|
+
return true;
|
|
166
|
+
case 'mysql':
|
|
167
|
+
case 'mariadb':
|
|
168
|
+
await this.pool.execute('SELECT 1');
|
|
169
|
+
return true;
|
|
170
|
+
case 'mongodb':
|
|
171
|
+
await this.db.admin().ping();
|
|
172
|
+
return true;
|
|
173
|
+
default:
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Create database pool
|
|
184
|
+
* @param {Object} options - Database options
|
|
185
|
+
* @returns {DatabasePool} - Database pool instance
|
|
186
|
+
*/
|
|
187
|
+
function createPool(options = {}) {
|
|
188
|
+
return new DatabasePool(options);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
DatabasePool,
|
|
193
|
+
createPool,
|
|
194
|
+
};
|
|
195
|
+
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI/Swagger Documentation Generator
|
|
3
|
+
* v5.1: Auto-generate OpenAPI 3.0 specification
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class SwaggerGenerator {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.info = {
|
|
9
|
+
title: options.title || 'Navis.js API',
|
|
10
|
+
version: options.version || '1.0.0',
|
|
11
|
+
description: options.description || '',
|
|
12
|
+
...options.info,
|
|
13
|
+
};
|
|
14
|
+
this.servers = options.servers || [{ url: '/', description: 'Default server' }];
|
|
15
|
+
this.paths = {};
|
|
16
|
+
this.components = {
|
|
17
|
+
schemas: {},
|
|
18
|
+
securitySchemes: {},
|
|
19
|
+
};
|
|
20
|
+
this.tags = options.tags || [];
|
|
21
|
+
this.basePath = options.basePath || '/';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Add a route to the specification
|
|
26
|
+
* @param {string} method - HTTP method
|
|
27
|
+
* @param {string} path - Route path
|
|
28
|
+
* @param {Object} spec - OpenAPI operation spec
|
|
29
|
+
*/
|
|
30
|
+
addRoute(method, path, spec = {}) {
|
|
31
|
+
const normalizedPath = this._normalizePath(path);
|
|
32
|
+
|
|
33
|
+
if (!this.paths[normalizedPath]) {
|
|
34
|
+
this.paths[normalizedPath] = {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.paths[normalizedPath][method.toLowerCase()] = {
|
|
38
|
+
summary: spec.summary || '',
|
|
39
|
+
description: spec.description || '',
|
|
40
|
+
tags: spec.tags || [],
|
|
41
|
+
parameters: spec.parameters || [],
|
|
42
|
+
requestBody: spec.requestBody || null,
|
|
43
|
+
responses: spec.responses || {
|
|
44
|
+
'200': { description: 'Success' },
|
|
45
|
+
},
|
|
46
|
+
security: spec.security || [],
|
|
47
|
+
...spec,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Add a schema component
|
|
53
|
+
* @param {string} name - Schema name
|
|
54
|
+
* @param {Object} schema - JSON Schema
|
|
55
|
+
*/
|
|
56
|
+
addSchema(name, schema) {
|
|
57
|
+
this.components.schemas[name] = schema;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add a security scheme
|
|
62
|
+
* @param {string} name - Security scheme name
|
|
63
|
+
* @param {Object} scheme - Security scheme
|
|
64
|
+
*/
|
|
65
|
+
addSecurityScheme(name, scheme) {
|
|
66
|
+
this.components.securitySchemes[name] = scheme;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generate OpenAPI specification
|
|
71
|
+
* @returns {Object} - OpenAPI 3.0 specification
|
|
72
|
+
*/
|
|
73
|
+
generate() {
|
|
74
|
+
return {
|
|
75
|
+
openapi: '3.0.0',
|
|
76
|
+
info: this.info,
|
|
77
|
+
servers: this.servers,
|
|
78
|
+
paths: this.paths,
|
|
79
|
+
components: this.components,
|
|
80
|
+
tags: this.tags,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Generate JSON string
|
|
86
|
+
* @returns {string} - JSON string
|
|
87
|
+
*/
|
|
88
|
+
toJSON() {
|
|
89
|
+
return JSON.stringify(this.generate(), null, 2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Normalize path for OpenAPI (convert :param to {param})
|
|
94
|
+
* @private
|
|
95
|
+
*/
|
|
96
|
+
_normalizePath(path) {
|
|
97
|
+
return path.replace(/:([^/]+)/g, '{$1}');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Swagger middleware
|
|
103
|
+
* @param {Object} options - Swagger options
|
|
104
|
+
* @returns {Function} - Middleware function
|
|
105
|
+
*/
|
|
106
|
+
function swagger(options = {}) {
|
|
107
|
+
const {
|
|
108
|
+
title = 'Navis.js API',
|
|
109
|
+
version = '1.0.0',
|
|
110
|
+
description = '',
|
|
111
|
+
path = '/swagger.json',
|
|
112
|
+
uiPath = '/docs',
|
|
113
|
+
servers = [],
|
|
114
|
+
tags = [],
|
|
115
|
+
} = options;
|
|
116
|
+
|
|
117
|
+
const generator = new SwaggerGenerator({
|
|
118
|
+
title,
|
|
119
|
+
version,
|
|
120
|
+
description,
|
|
121
|
+
servers,
|
|
122
|
+
tags,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Add default security schemes
|
|
126
|
+
generator.addSecurityScheme('bearerAuth', {
|
|
127
|
+
type: 'http',
|
|
128
|
+
scheme: 'bearer',
|
|
129
|
+
bearerFormat: 'JWT',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
generator,
|
|
134
|
+
middleware: async (req, res, next) => {
|
|
135
|
+
const requestPath = req.path || req.url;
|
|
136
|
+
|
|
137
|
+
// Serve OpenAPI JSON
|
|
138
|
+
if (requestPath === path) {
|
|
139
|
+
res.statusCode = 200;
|
|
140
|
+
res.headers = res.headers || {};
|
|
141
|
+
res.headers['Content-Type'] = 'application/json';
|
|
142
|
+
res.body = generator.generate();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Serve Swagger UI (basic HTML)
|
|
147
|
+
if (requestPath === uiPath) {
|
|
148
|
+
const spec = generator.toJSON();
|
|
149
|
+
const html = `
|
|
150
|
+
<!DOCTYPE html>
|
|
151
|
+
<html>
|
|
152
|
+
<head>
|
|
153
|
+
<title>${title} - API Documentation</title>
|
|
154
|
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
|
|
155
|
+
</head>
|
|
156
|
+
<body>
|
|
157
|
+
<div id="swagger-ui"></div>
|
|
158
|
+
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-bundle.js"></script>
|
|
159
|
+
<script>
|
|
160
|
+
SwaggerUIBundle({
|
|
161
|
+
url: '${path}',
|
|
162
|
+
dom_id: '#swagger-ui',
|
|
163
|
+
presets: [
|
|
164
|
+
SwaggerUIBundle.presets.apis,
|
|
165
|
+
SwaggerUIBundle.presets.standalone
|
|
166
|
+
]
|
|
167
|
+
});
|
|
168
|
+
</script>
|
|
169
|
+
</body>
|
|
170
|
+
</html>
|
|
171
|
+
`;
|
|
172
|
+
res.statusCode = 200;
|
|
173
|
+
res.headers = res.headers || {};
|
|
174
|
+
res.headers['Content-Type'] = 'text/html';
|
|
175
|
+
res.body = html;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
next();
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
SwaggerGenerator,
|
|
186
|
+
swagger,
|
|
187
|
+
};
|
|
188
|
+
|
package/src/index.js
CHANGED
|
@@ -57,6 +57,17 @@ const compress = require('./middleware/compression');
|
|
|
57
57
|
const { HealthChecker, createHealthChecker } = require('./health/health-checker');
|
|
58
58
|
const gracefulShutdown = require('./core/graceful-shutdown');
|
|
59
59
|
|
|
60
|
+
// v5.1: Developer Experience
|
|
61
|
+
const { SwaggerGenerator, swagger } = require('./docs/swagger');
|
|
62
|
+
const { VersionManager, createVersionManager, headerVersioning } = require('./core/versioning');
|
|
63
|
+
const { upload, saveFile } = require('./middleware/upload');
|
|
64
|
+
const { TestApp, testApp } = require('./testing/test-helper');
|
|
65
|
+
|
|
66
|
+
// v5.2: Real-time Features
|
|
67
|
+
const WebSocketServer = require('./websocket/websocket-server');
|
|
68
|
+
const { SSEServer, createSSEServer, sse } = require('./sse/server-sent-events');
|
|
69
|
+
const { DatabasePool, createPool } = require('./db/db-pool');
|
|
70
|
+
|
|
60
71
|
module.exports = {
|
|
61
72
|
// Core
|
|
62
73
|
NavisApp,
|
|
@@ -121,6 +132,25 @@ module.exports = {
|
|
|
121
132
|
createHealthChecker,
|
|
122
133
|
gracefulShutdown,
|
|
123
134
|
|
|
135
|
+
// v5.1: Developer Experience
|
|
136
|
+
SwaggerGenerator,
|
|
137
|
+
swagger,
|
|
138
|
+
VersionManager,
|
|
139
|
+
createVersionManager,
|
|
140
|
+
headerVersioning,
|
|
141
|
+
upload,
|
|
142
|
+
saveFile,
|
|
143
|
+
TestApp,
|
|
144
|
+
testApp,
|
|
145
|
+
|
|
146
|
+
// v5.2: Real-time Features
|
|
147
|
+
WebSocketServer,
|
|
148
|
+
SSEServer,
|
|
149
|
+
createSSEServer,
|
|
150
|
+
sse,
|
|
151
|
+
DatabasePool,
|
|
152
|
+
createPool,
|
|
153
|
+
|
|
124
154
|
// Utilities
|
|
125
155
|
response: {
|
|
126
156
|
success,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Upload Middleware
|
|
3
|
+
* v5.1: Multipart form data and file upload handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* File upload middleware
|
|
12
|
+
* @param {Object} options - Upload options
|
|
13
|
+
* @returns {Function} - Middleware function
|
|
14
|
+
*/
|
|
15
|
+
function upload(options = {}) {
|
|
16
|
+
const {
|
|
17
|
+
dest = '/tmp/uploads',
|
|
18
|
+
limits = {
|
|
19
|
+
fileSize: 5 * 1024 * 1024, // 5MB default
|
|
20
|
+
files: 10, // Max 10 files
|
|
21
|
+
},
|
|
22
|
+
fileFilter = null, // Function to filter files
|
|
23
|
+
preserveExtension = true,
|
|
24
|
+
generateFilename = (file) => {
|
|
25
|
+
// Generate unique filename
|
|
26
|
+
const ext = preserveExtension ? path.extname(file.originalName || '') : '';
|
|
27
|
+
return `${crypto.randomBytes(16).toString('hex')}${ext}`;
|
|
28
|
+
},
|
|
29
|
+
} = options;
|
|
30
|
+
|
|
31
|
+
// Ensure destination directory exists
|
|
32
|
+
if (!fs.existsSync(dest)) {
|
|
33
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return async (req, res, next) => {
|
|
37
|
+
// Check content type
|
|
38
|
+
const contentType = req.headers['content-type'] || req.headers['Content-Type'] || '';
|
|
39
|
+
|
|
40
|
+
if (!contentType.includes('multipart/form-data')) {
|
|
41
|
+
return next();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Parse multipart form data (simplified implementation)
|
|
45
|
+
// In production, use a library like busboy or multer
|
|
46
|
+
try {
|
|
47
|
+
req.files = [];
|
|
48
|
+
req.body = req.body || {};
|
|
49
|
+
|
|
50
|
+
// For Lambda, body is already parsed
|
|
51
|
+
if (req.event && req.event.isBase64Encoded) {
|
|
52
|
+
// Handle base64 encoded body
|
|
53
|
+
const body = Buffer.from(req.event.body, 'base64').toString();
|
|
54
|
+
// Parse multipart data (simplified)
|
|
55
|
+
// In production, use proper multipart parser
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// For Node.js HTTP, parse from request stream
|
|
59
|
+
if (req.on && typeof req.on === 'function') {
|
|
60
|
+
await parseMultipart(req, dest, limits, fileFilter, generateFilename, (files, fields) => {
|
|
61
|
+
req.files = files;
|
|
62
|
+
req.body = { ...req.body, ...fields };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
next();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
res.statusCode = 400;
|
|
69
|
+
res.body = { error: error.message || 'File upload failed' };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse multipart form data (simplified)
|
|
76
|
+
* @private
|
|
77
|
+
*/
|
|
78
|
+
function parseMultipart(req, dest, limits, fileFilter, generateFilename, callback) {
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const files = [];
|
|
81
|
+
const fields = {};
|
|
82
|
+
let totalSize = 0;
|
|
83
|
+
let fileCount = 0;
|
|
84
|
+
|
|
85
|
+
// Simplified multipart parser
|
|
86
|
+
// In production, use busboy or multer
|
|
87
|
+
let buffer = '';
|
|
88
|
+
|
|
89
|
+
req.on('data', (chunk) => {
|
|
90
|
+
buffer += chunk.toString();
|
|
91
|
+
totalSize += chunk.length;
|
|
92
|
+
|
|
93
|
+
if (totalSize > limits.fileSize) {
|
|
94
|
+
reject(new Error('File size exceeds limit'));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
req.on('end', () => {
|
|
100
|
+
// Basic multipart parsing (simplified)
|
|
101
|
+
// This is a placeholder - in production use proper parser
|
|
102
|
+
try {
|
|
103
|
+
callback(files, fields);
|
|
104
|
+
resolve();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
reject(error);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
req.on('error', reject);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Save uploaded file
|
|
116
|
+
* @param {Object} file - File object
|
|
117
|
+
* @param {string} dest - Destination directory
|
|
118
|
+
* @param {Function} generateFilename - Filename generator
|
|
119
|
+
* @returns {Promise<string>} - File path
|
|
120
|
+
*/
|
|
121
|
+
async function saveFile(file, dest, generateFilename) {
|
|
122
|
+
const filename = generateFilename(file);
|
|
123
|
+
const filepath = path.join(dest, filename);
|
|
124
|
+
|
|
125
|
+
// Ensure directory exists
|
|
126
|
+
const dir = path.dirname(filepath);
|
|
127
|
+
if (!fs.existsSync(dir)) {
|
|
128
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Write file
|
|
132
|
+
if (file.buffer) {
|
|
133
|
+
fs.writeFileSync(filepath, file.buffer);
|
|
134
|
+
} else if (file.stream) {
|
|
135
|
+
// Handle stream
|
|
136
|
+
const writeStream = fs.createWriteStream(filepath);
|
|
137
|
+
file.stream.pipe(writeStream);
|
|
138
|
+
await new Promise((resolve, reject) => {
|
|
139
|
+
writeStream.on('finish', resolve);
|
|
140
|
+
writeStream.on('error', reject);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return filepath;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
upload,
|
|
149
|
+
saveFile,
|
|
150
|
+
};
|
|
151
|
+
|