pglens 1.1.0 → 2.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.
- package/CHANGELOG.md +17 -0
- package/README.md +38 -145
- package/bin/pglens +143 -7
- package/client/app.js +654 -128
- package/client/index.html +133 -35
- package/client/styles.css +671 -37
- package/package.json +2 -2
- package/src/db/connection.js +163 -27
- package/src/routes/api.js +139 -9
- package/src/server.js +72 -30
- package/pglens-1.1.0.tgz +0 -0
package/package.json
CHANGED
package/src/db/connection.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Database Connection Pool Manager
|
|
3
3
|
*
|
|
4
|
-
* Manages PostgreSQL connection
|
|
4
|
+
* Manages PostgreSQL connection pools using postgres library.
|
|
5
5
|
* Provides connection pooling for efficient database access.
|
|
6
|
+
* Supports multiple simultaneous connections.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
const postgres = require('postgres');
|
|
10
|
+
const crypto = require('crypto');
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
// Map to store multiple connections: id -> { pool, name, connectionString }
|
|
13
|
+
const connections = new Map();
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Create a wrapper that provides pg-compatible query interface.
|
|
@@ -25,49 +28,98 @@ function createPoolWrapper(sqlClient) {
|
|
|
25
28
|
};
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Extract database name from connection string
|
|
33
|
+
* @param {string} connectionString
|
|
34
|
+
* @returns {string} Database name or 'Unknown'
|
|
35
|
+
*/
|
|
36
|
+
function getDatabaseName(connectionString) {
|
|
37
|
+
try {
|
|
38
|
+
const url = new URL(connectionString);
|
|
39
|
+
return url.pathname.replace(/^\//, '') || 'postgres';
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return 'postgres';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
28
45
|
/**
|
|
29
46
|
* Create a new connection pool.
|
|
30
|
-
* Closes existing pool if one exists.
|
|
31
47
|
* @param {string} connectionString - PostgreSQL connection string
|
|
32
48
|
* @param {string} sslMode - SSL mode: disable, require, prefer, verify-ca, verify-full
|
|
33
|
-
* @
|
|
49
|
+
* @param {string} [customName] - Optional custom name for the connection
|
|
50
|
+
* @returns {Promise<{id: string, name: string}>} The created connection info
|
|
34
51
|
*/
|
|
35
|
-
function createPool(connectionString, sslMode = 'prefer') {
|
|
36
|
-
if (sql) {
|
|
37
|
-
sql.end();
|
|
38
|
-
}
|
|
39
|
-
|
|
52
|
+
function createPool(connectionString, sslMode = 'prefer', customName = null) {
|
|
40
53
|
const sslConfig = getSslConfig(sslMode);
|
|
41
54
|
const poolConfig = {
|
|
42
55
|
max: 10,
|
|
43
56
|
idle_timeout: 30,
|
|
44
57
|
connect_timeout: 10,
|
|
58
|
+
timeout: 30, // Query timeout in seconds
|
|
45
59
|
};
|
|
46
60
|
|
|
47
61
|
if (sslConfig !== null) {
|
|
48
62
|
poolConfig.ssl = sslConfig;
|
|
49
63
|
}
|
|
50
64
|
|
|
51
|
-
sql = postgres(connectionString, poolConfig);
|
|
65
|
+
const sql = postgres(connectionString, poolConfig);
|
|
52
66
|
|
|
53
67
|
// Test the connection
|
|
54
68
|
return sql`SELECT NOW()`
|
|
55
69
|
.then(() => {
|
|
56
70
|
console.log('✓ Connected to PostgreSQL database');
|
|
57
|
-
|
|
71
|
+
|
|
72
|
+
// Check if this connection string already exists
|
|
73
|
+
for (const [existingId, existingConn] of connections.entries()) {
|
|
74
|
+
if (existingConn.connectionString === connectionString) {
|
|
75
|
+
// If a custom name is provided and different from existing, update it
|
|
76
|
+
if (customName && customName !== existingConn.name) {
|
|
77
|
+
existingConn.name = customName;
|
|
78
|
+
}
|
|
79
|
+
// Return the existing connection details
|
|
80
|
+
sql.end();
|
|
81
|
+
return { id: existingId, name: existingConn.name, reused: true };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const id = crypto.randomUUID();
|
|
86
|
+
const name = customName || getDatabaseName(connectionString);
|
|
87
|
+
|
|
88
|
+
connections.set(id, {
|
|
89
|
+
pool: sql,
|
|
90
|
+
name,
|
|
91
|
+
connectionString,
|
|
92
|
+
sslMode
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return { id, name };
|
|
58
96
|
})
|
|
59
97
|
.catch((err) => {
|
|
60
98
|
console.error('✗ Failed to connect to PostgreSQL database:', err.message);
|
|
61
99
|
const recommendation = getSslModeRecommendation(err, sslMode);
|
|
62
100
|
if (recommendation) {
|
|
63
|
-
console.error(`\n💡 SSL Mode Recommendation: Try using
|
|
64
|
-
console.error(` Current SSL mode: ${sslMode}`);
|
|
65
|
-
console.error(` Suggested command: Add --sslmode ${recommendation} to your command\n`);
|
|
101
|
+
console.error(`\n💡 SSL Mode Recommendation: Try using sslmode '${recommendation}'`);
|
|
66
102
|
}
|
|
67
103
|
throw err;
|
|
68
104
|
});
|
|
69
105
|
}
|
|
70
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Check if a specific connection is active.
|
|
109
|
+
* @param {string} connectionId - Connection ID
|
|
110
|
+
* @returns {Promise<boolean>} True if connected
|
|
111
|
+
*/
|
|
112
|
+
function checkConnection(connectionId) {
|
|
113
|
+
const conn = connections.get(connectionId);
|
|
114
|
+
if (!conn) {
|
|
115
|
+
return Promise.resolve(false);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return conn.pool`SELECT 1`
|
|
119
|
+
.then(() => true)
|
|
120
|
+
.catch(() => false);
|
|
121
|
+
}
|
|
122
|
+
|
|
71
123
|
/**
|
|
72
124
|
* Analyze connection error and recommend appropriate SSL mode.
|
|
73
125
|
* @param {Error} error - Connection error object
|
|
@@ -170,31 +222,115 @@ function getSslConfig(sslMode) {
|
|
|
170
222
|
}
|
|
171
223
|
|
|
172
224
|
/**
|
|
173
|
-
* Get
|
|
174
|
-
* @
|
|
175
|
-
* @
|
|
225
|
+
* Get a connection pool by ID.
|
|
226
|
+
* @param {string} connectionId - The connection ID
|
|
227
|
+
* @returns {object|null} The connection pool wrapper or null if not found
|
|
176
228
|
*/
|
|
177
|
-
function getPool() {
|
|
178
|
-
|
|
179
|
-
|
|
229
|
+
function getPool(connectionId) {
|
|
230
|
+
const conn = connections.get(connectionId);
|
|
231
|
+
if (!conn) {
|
|
232
|
+
return null;
|
|
180
233
|
}
|
|
181
|
-
return createPoolWrapper(
|
|
234
|
+
return createPoolWrapper(conn.pool);
|
|
182
235
|
}
|
|
183
236
|
|
|
184
237
|
/**
|
|
185
|
-
*
|
|
238
|
+
* Get list of all active connections.
|
|
239
|
+
* @returns {Array<{id: string, name: string}>} List of connections
|
|
240
|
+
*/
|
|
241
|
+
function getConnections() {
|
|
242
|
+
const result = [];
|
|
243
|
+
for (const [id, conn] of connections.entries()) {
|
|
244
|
+
result.push({
|
|
245
|
+
id,
|
|
246
|
+
name: conn.name,
|
|
247
|
+
connectionString: conn.connectionString,
|
|
248
|
+
sslMode: conn.sslMode
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Update an existing connection.
|
|
256
|
+
* @param {string} id - Connection ID to update
|
|
257
|
+
* @param {string} connectionString - New connection string
|
|
258
|
+
* @param {string} sslMode - New SSL mode
|
|
259
|
+
* @param {string} name - New name
|
|
260
|
+
* @returns {Promise<{id: string, name: string}>} Updated connection info
|
|
261
|
+
*/
|
|
262
|
+
async function updateConnection(id, connectionString, sslMode, name) {
|
|
263
|
+
const existingConn = connections.get(id);
|
|
264
|
+
if (!existingConn) {
|
|
265
|
+
throw new Error('Connection not found');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const sslConfig = getSslConfig(sslMode);
|
|
269
|
+
const poolConfig = {
|
|
270
|
+
max: 10,
|
|
271
|
+
idle_timeout: 30,
|
|
272
|
+
connect_timeout: 10,
|
|
273
|
+
timeout: 30, // Query timeout in seconds
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (sslConfig !== null) {
|
|
277
|
+
poolConfig.ssl = sslConfig;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const sql = postgres(connectionString, poolConfig);
|
|
281
|
+
|
|
282
|
+
// Test the new connection
|
|
283
|
+
return sql`SELECT NOW()`
|
|
284
|
+
.then(async () => {
|
|
285
|
+
console.log('✓ Updated connection to PostgreSQL database');
|
|
286
|
+
|
|
287
|
+
// Close old pool
|
|
288
|
+
await existingConn.pool.end();
|
|
289
|
+
|
|
290
|
+
// Update map with new pool and details
|
|
291
|
+
connections.set(id, {
|
|
292
|
+
pool: sql,
|
|
293
|
+
name: name || getDatabaseName(connectionString),
|
|
294
|
+
connectionString,
|
|
295
|
+
sslMode
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return { id, name: connections.get(id).name };
|
|
299
|
+
})
|
|
300
|
+
.catch((err) => {
|
|
301
|
+
console.error('✗ Failed to update connection:', err.message);
|
|
302
|
+
throw err;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Close a specific connection pool.
|
|
308
|
+
* @param {string} connectionId - The connection ID to close
|
|
186
309
|
* @returns {Promise} Promise that resolves when pool is closed
|
|
187
310
|
*/
|
|
188
|
-
function closePool() {
|
|
189
|
-
if (
|
|
190
|
-
|
|
311
|
+
async function closePool(connectionId) {
|
|
312
|
+
if (connectionId) {
|
|
313
|
+
const conn = connections.get(connectionId);
|
|
314
|
+
if (conn) {
|
|
315
|
+
await conn.pool.end();
|
|
316
|
+
connections.delete(connectionId);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
// Close all connections (e.g., on server shutdown)
|
|
320
|
+
const promises = [];
|
|
321
|
+
for (const conn of connections.values()) {
|
|
322
|
+
promises.push(conn.pool.end());
|
|
323
|
+
}
|
|
324
|
+
await Promise.all(promises);
|
|
325
|
+
connections.clear();
|
|
191
326
|
}
|
|
192
|
-
return Promise.resolve();
|
|
193
327
|
}
|
|
194
328
|
|
|
195
329
|
module.exports = {
|
|
196
330
|
createPool,
|
|
197
331
|
getPool,
|
|
198
332
|
closePool,
|
|
333
|
+
checkConnection,
|
|
334
|
+
getConnections,
|
|
335
|
+
updateConnection
|
|
199
336
|
};
|
|
200
|
-
|
package/src/routes/api.js
CHANGED
|
@@ -14,10 +14,119 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const express = require('express');
|
|
17
|
-
const { getPool } = require('../db/connection');
|
|
17
|
+
const { getPool, createPool, closePool, checkConnection, getConnections, updateConnection } = require('../db/connection');
|
|
18
18
|
|
|
19
19
|
const router = express.Router();
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Middleware to check if connected to database
|
|
23
|
+
*/
|
|
24
|
+
const requireConnection = async (req, res, next) => {
|
|
25
|
+
const connectionId = req.headers['x-connection-id'];
|
|
26
|
+
if (!connectionId) {
|
|
27
|
+
return res.status(400).json({ error: 'Connection ID header required' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pool = getPool(connectionId);
|
|
31
|
+
if (!pool) {
|
|
32
|
+
return res.status(503).json({ error: 'Not connected to database or invalid connection ID' });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
req.pool = pool;
|
|
36
|
+
next();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* POST /api/connect
|
|
41
|
+
* Connect to a PostgreSQL database
|
|
42
|
+
*/
|
|
43
|
+
router.post('/connect', async (req, res) => {
|
|
44
|
+
const { url, sslMode, name } = req.body;
|
|
45
|
+
|
|
46
|
+
if (!url) {
|
|
47
|
+
return res.status(400).json({ error: 'Connection string is required' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const { id, name: connectionName } = await createPool(url, sslMode || 'prefer', name);
|
|
52
|
+
res.json({ connected: true, connectionId: id, name: connectionName });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
res.status(400).json({
|
|
55
|
+
connected: false,
|
|
56
|
+
error: error.message
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* PUT /api/connections/:id
|
|
63
|
+
* Update an existing connection
|
|
64
|
+
*/
|
|
65
|
+
router.put('/connections/:id', async (req, res) => {
|
|
66
|
+
const { id } = req.params;
|
|
67
|
+
const { url, sslMode, name } = req.body;
|
|
68
|
+
|
|
69
|
+
if (!url) {
|
|
70
|
+
return res.status(400).json({ error: 'Connection string is required' });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const { name: connectionName } = await updateConnection(id, url, sslMode || 'prefer', name);
|
|
75
|
+
res.json({ updated: true, connectionId: id, name: connectionName });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
res.status(400).json({
|
|
78
|
+
updated: false,
|
|
79
|
+
error: error.message
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* GET /api/connections
|
|
86
|
+
* List active connections
|
|
87
|
+
*/
|
|
88
|
+
router.get('/connections', (req, res) => {
|
|
89
|
+
const connections = getConnections();
|
|
90
|
+
res.json({ connections });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* POST /api/disconnect
|
|
95
|
+
* Disconnect from a database
|
|
96
|
+
*/
|
|
97
|
+
router.post('/disconnect', async (req, res) => {
|
|
98
|
+
const connectionId = req.body.connectionId || req.headers['x-connection-id'];
|
|
99
|
+
|
|
100
|
+
if (!connectionId) {
|
|
101
|
+
return res.status(400).json({ error: 'Connection ID required' });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await closePool(connectionId);
|
|
106
|
+
res.json({ connected: false });
|
|
107
|
+
} catch (error) {
|
|
108
|
+
res.status(500).json({ error: error.message });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* GET /api/status
|
|
114
|
+
* Check connection status
|
|
115
|
+
*/
|
|
116
|
+
router.get('/status', async (req, res) => {
|
|
117
|
+
const connectionId = req.headers['x-connection-id'];
|
|
118
|
+
if (!connectionId) {
|
|
119
|
+
return res.json({ connected: false });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const connected = await checkConnection(connectionId);
|
|
124
|
+
res.json({ connected });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
res.json({ connected: false, error: error.message });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
21
130
|
/**
|
|
22
131
|
* Sanitize table name to prevent SQL injection.
|
|
23
132
|
* Only allows alphanumeric characters, underscores, and dots.
|
|
@@ -40,9 +149,9 @@ function sanitizeTableName(tableName) {
|
|
|
40
149
|
*
|
|
41
150
|
* Response: { tables: string[] }
|
|
42
151
|
*/
|
|
43
|
-
router.get('/tables', async (req, res) => {
|
|
152
|
+
router.get('/tables', requireConnection, async (req, res) => {
|
|
44
153
|
try {
|
|
45
|
-
const pool =
|
|
154
|
+
const pool = req.pool;
|
|
46
155
|
const result = await pool.query(`
|
|
47
156
|
SELECT table_name
|
|
48
157
|
FROM information_schema.tables
|
|
@@ -202,7 +311,7 @@ async function getColumnMetadata(pool, tableName) {
|
|
|
202
311
|
SELECT column_name, data_type
|
|
203
312
|
FROM information_schema.columns
|
|
204
313
|
WHERE table_schema = 'public'
|
|
205
|
-
|
|
314
|
+
AND table_name = $1
|
|
206
315
|
ORDER BY ordinal_position;
|
|
207
316
|
`;
|
|
208
317
|
const result = await pool.query(metadataQuery, [tableName]);
|
|
@@ -260,21 +369,28 @@ async function getColumnMetadata(pool, tableName) {
|
|
|
260
369
|
* - isUnique: boolean
|
|
261
370
|
* }
|
|
262
371
|
*/
|
|
263
|
-
router.get('/tables/:tableName', async (req, res) => {
|
|
372
|
+
router.get('/tables/:tableName', requireConnection, async (req, res) => {
|
|
264
373
|
try {
|
|
265
374
|
const tableName = sanitizeTableName(req.params.tableName);
|
|
266
375
|
const page = parseInt(req.query.page || '1', 10);
|
|
267
376
|
const limit = parseInt(req.query.limit || '100', 10);
|
|
268
377
|
const cursor = req.query.cursor;
|
|
378
|
+
const sortColumn = req.query.sortColumn ? req.query.sortColumn : null;
|
|
379
|
+
const sortDirection = req.query.sortDirection === 'desc' ? 'DESC' : 'ASC';
|
|
269
380
|
|
|
270
381
|
if (page < 1 || limit < 1) {
|
|
271
382
|
return res.status(400).json({ error: 'Page and limit must be positive integers' });
|
|
272
383
|
}
|
|
273
384
|
|
|
274
|
-
const pool =
|
|
385
|
+
const pool = req.pool;
|
|
275
386
|
const primaryKeyColumn = await getPrimaryKeyColumn(pool, tableName);
|
|
276
387
|
const columnMetadata = await getColumnMetadata(pool, tableName);
|
|
277
388
|
|
|
389
|
+
// Validate sortColumn if provided
|
|
390
|
+
if (sortColumn && !columnMetadata[sortColumn]) {
|
|
391
|
+
return res.status(400).json({ error: 'Invalid sort column' });
|
|
392
|
+
}
|
|
393
|
+
|
|
278
394
|
const countQuery = `SELECT COUNT(*) as total FROM "${tableName}"`;
|
|
279
395
|
const countResult = await pool.query(countQuery);
|
|
280
396
|
const totalCount = parseInt(countResult.rows[0].total, 10);
|
|
@@ -283,7 +399,22 @@ router.get('/tables/:tableName', async (req, res) => {
|
|
|
283
399
|
let dataResult;
|
|
284
400
|
let nextCursor = null;
|
|
285
401
|
|
|
286
|
-
if (
|
|
402
|
+
if (sortColumn) {
|
|
403
|
+
// Custom Sorting: Use OFFSET-based pagination
|
|
404
|
+
// Always add primary key (if exists) as secondary sort for stability
|
|
405
|
+
const offset = (page - 1) * limit;
|
|
406
|
+
let orderByClause = `ORDER BY "${sortColumn}" ${sortDirection}`;
|
|
407
|
+
|
|
408
|
+
if (primaryKeyColumn && sortColumn !== primaryKeyColumn) {
|
|
409
|
+
orderByClause += `, "${primaryKeyColumn}" ASC`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const query = `SELECT * FROM "${tableName}" ${orderByClause} LIMIT $1 OFFSET $2`;
|
|
413
|
+
dataResult = await pool.query(query, [limit, offset]);
|
|
414
|
+
|
|
415
|
+
// Cursor not valid for custom sorts in this simple implementation
|
|
416
|
+
nextCursor = null;
|
|
417
|
+
} else if (primaryKeyColumn && cursor) {
|
|
287
418
|
// Cursor-based pagination: WHERE id > cursor (most efficient for forward navigation)
|
|
288
419
|
const cursorQuery = `SELECT * FROM "${tableName}" WHERE "${primaryKeyColumn}" > $1 ORDER BY "${primaryKeyColumn}" ASC LIMIT $2`;
|
|
289
420
|
const cursorParams = [cursor, limit];
|
|
@@ -321,7 +452,7 @@ router.get('/tables/:tableName', async (req, res) => {
|
|
|
321
452
|
}
|
|
322
453
|
dataResult = await pool.query(query, queryParams);
|
|
323
454
|
|
|
324
|
-
// Calculate cursor for next page if primary key exists
|
|
455
|
+
// Calculate cursor for next page if primary key exists (only if default sort)
|
|
325
456
|
if (primaryKeyColumn && dataResult.rows.length > 0) {
|
|
326
457
|
const lastRow = dataResult.rows[dataResult.rows.length - 1];
|
|
327
458
|
nextCursor = lastRow[primaryKeyColumn];
|
|
@@ -346,4 +477,3 @@ router.get('/tables/:tableName', async (req, res) => {
|
|
|
346
477
|
});
|
|
347
478
|
|
|
348
479
|
module.exports = router;
|
|
349
|
-
|
package/src/server.js
CHANGED
|
@@ -19,43 +19,85 @@ const apiRoutes = require('./routes/api');
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Start the Express server.
|
|
22
|
-
* @param {string} connectionString - PostgreSQL connection string
|
|
23
|
-
* @param {number} port - Port number to listen on
|
|
24
|
-
* @param {string} sslMode - SSL mode for database connection
|
|
25
22
|
*/
|
|
26
|
-
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const net = require('net');
|
|
26
|
+
|
|
27
|
+
const PORT_FILE = path.join(os.homedir(), '.pglens.port');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if a port is in use.
|
|
31
|
+
* @param {number} port
|
|
32
|
+
* @returns {Promise<boolean>}
|
|
33
|
+
*/
|
|
34
|
+
function isPortInUse(port) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const server = net.createServer();
|
|
37
|
+
server.once('error', (err) => {
|
|
38
|
+
if (err.code === 'EADDRINUSE') {
|
|
39
|
+
resolve(true);
|
|
40
|
+
} else {
|
|
41
|
+
resolve(false);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
server.once('listening', () => {
|
|
45
|
+
server.close();
|
|
46
|
+
resolve(false);
|
|
47
|
+
});
|
|
48
|
+
server.listen(port);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Start the Express server.
|
|
54
|
+
*/
|
|
55
|
+
async function startServer() {
|
|
27
56
|
const app = express();
|
|
57
|
+
let port = 54321;
|
|
58
|
+
|
|
59
|
+
// Try to find an available port starting from 54321
|
|
60
|
+
if (await isPortInUse(port)) {
|
|
61
|
+
port = 0; // Let OS choose a random available port
|
|
62
|
+
}
|
|
28
63
|
|
|
29
64
|
app.use(cors());
|
|
30
65
|
app.use(express.json());
|
|
31
66
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
app.use('/api', apiRoutes);
|
|
68
|
+
|
|
69
|
+
const clientPath = path.join(__dirname, '../client');
|
|
70
|
+
app.use(express.static(clientPath));
|
|
71
|
+
|
|
72
|
+
app.get('*', (_, res) => {
|
|
73
|
+
res.sendFile(path.join(clientPath, 'index.html'));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const server = app.listen(port, () => {
|
|
77
|
+
const actualPort = server.address().port;
|
|
78
|
+
console.log(`✓ Server running on http://localhost:${actualPort}`);
|
|
79
|
+
console.log(` Open your browser to view your database`);
|
|
80
|
+
|
|
81
|
+
// Write port to file for CLI to read
|
|
82
|
+
fs.writeFileSync(PORT_FILE, actualPort.toString());
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const shutdown = () => {
|
|
86
|
+
console.log('\nShutting down...');
|
|
87
|
+
if (fs.existsSync(PORT_FILE)) {
|
|
88
|
+
try {
|
|
89
|
+
fs.unlinkSync(PORT_FILE);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// Ignore removal errors
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
closePool().then(() => {
|
|
95
|
+
process.exit(0);
|
|
58
96
|
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
process.on('SIGINT', shutdown);
|
|
100
|
+
process.on('SIGTERM', shutdown);
|
|
59
101
|
}
|
|
60
102
|
|
|
61
103
|
module.exports = { startServer };
|
package/pglens-1.1.0.tgz
DELETED
|
Binary file
|