pglens 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +46 -0
- package/.github/ISSUE_TEMPLATE/config.yml +9 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +36 -0
- package/.github/pull_request_template.md +52 -0
- package/CHANGELOG.md +41 -0
- package/CONTRIBUTING.md +391 -0
- package/README.md +221 -0
- package/bin/pglens +18 -0
- package/client/app.js +928 -0
- package/client/index.html +59 -0
- package/client/styles.css +801 -0
- package/package.json +34 -0
- package/pglens-1.0.0.tgz +0 -0
- package/src/db/connection.js +200 -0
- package/src/routes/api.js +200 -0
- package/src/server.js +62 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pglens",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A simple PostgreSQL database viewer tool",
|
|
5
|
+
"main": "src/server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pglens": "./bin/pglens"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/pglens"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"postgresql",
|
|
14
|
+
"database",
|
|
15
|
+
"viewer",
|
|
16
|
+
"cli"
|
|
17
|
+
],
|
|
18
|
+
"author": "Tekeshwar Singh (https://github.com/tsvillain)",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/tsvillain/pglens.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/tsvillain/pglens/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/tsvillain/pglens#readme",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"commander": "^11.1.0",
|
|
30
|
+
"cors": "^2.8.5",
|
|
31
|
+
"express": "^4.18.2",
|
|
32
|
+
"postgres": "^3.4.3"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/pglens-1.0.0.tgz
ADDED
|
Binary file
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database Connection Pool Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages PostgreSQL connection pool using postgres library.
|
|
5
|
+
* Provides connection pooling for efficient database access.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const postgres = require('postgres');
|
|
9
|
+
|
|
10
|
+
let sql = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a wrapper that provides pg-compatible query interface.
|
|
14
|
+
* The postgres package uses a different API, so we wrap it to maintain compatibility.
|
|
15
|
+
* @param {object} sqlClient - The postgres client instance
|
|
16
|
+
* @returns {object} Wrapped client with .query() method
|
|
17
|
+
*/
|
|
18
|
+
function createPoolWrapper(sqlClient) {
|
|
19
|
+
return {
|
|
20
|
+
query: async (queryText, params) => {
|
|
21
|
+
const result = await sqlClient.unsafe(queryText, params || []);
|
|
22
|
+
return { rows: result };
|
|
23
|
+
},
|
|
24
|
+
end: () => sqlClient.end(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a new connection pool.
|
|
30
|
+
* Closes existing pool if one exists.
|
|
31
|
+
* @param {string} connectionString - PostgreSQL connection string
|
|
32
|
+
* @param {string} sslMode - SSL mode: disable, require, prefer, verify-ca, verify-full
|
|
33
|
+
* @returns {Promise} The created connection wrapper
|
|
34
|
+
*/
|
|
35
|
+
function createPool(connectionString, sslMode = 'prefer') {
|
|
36
|
+
if (sql) {
|
|
37
|
+
sql.end();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const sslConfig = getSslConfig(sslMode);
|
|
41
|
+
const poolConfig = {
|
|
42
|
+
max: 10,
|
|
43
|
+
idle_timeout: 30,
|
|
44
|
+
connect_timeout: 10,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (sslConfig !== null) {
|
|
48
|
+
poolConfig.ssl = sslConfig;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
sql = postgres(connectionString, poolConfig);
|
|
52
|
+
|
|
53
|
+
// Test the connection
|
|
54
|
+
return sql`SELECT NOW()`
|
|
55
|
+
.then(() => {
|
|
56
|
+
console.log('ā Connected to PostgreSQL database');
|
|
57
|
+
return createPoolWrapper(sql);
|
|
58
|
+
})
|
|
59
|
+
.catch((err) => {
|
|
60
|
+
console.error('ā Failed to connect to PostgreSQL database:', err.message);
|
|
61
|
+
const recommendation = getSslModeRecommendation(err, sslMode);
|
|
62
|
+
if (recommendation) {
|
|
63
|
+
console.error(`\nš” SSL Mode Recommendation: Try using --sslmode ${recommendation}`);
|
|
64
|
+
console.error(` Current SSL mode: ${sslMode}`);
|
|
65
|
+
console.error(` Suggested command: Add --sslmode ${recommendation} to your command\n`);
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Analyze connection error and recommend appropriate SSL mode.
|
|
73
|
+
* @param {Error} error - Connection error object
|
|
74
|
+
* @param {string} currentSslMode - Current SSL mode that failed
|
|
75
|
+
* @returns {string|null} Recommended SSL mode or null if not SSL-related
|
|
76
|
+
*/
|
|
77
|
+
function getSslModeRecommendation(error, currentSslMode) {
|
|
78
|
+
const errorMessage = error.message?.toLowerCase() || '';
|
|
79
|
+
const errorCode = error.code;
|
|
80
|
+
const errorStack = error.stack?.toLowerCase() || '';
|
|
81
|
+
|
|
82
|
+
// Certificate verification errors
|
|
83
|
+
if (
|
|
84
|
+
errorMessage.includes('certificate') ||
|
|
85
|
+
errorMessage.includes('self signed') ||
|
|
86
|
+
errorMessage.includes('unable to verify') ||
|
|
87
|
+
errorCode === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' ||
|
|
88
|
+
errorCode === 'SELF_SIGNED_CERT_IN_CHAIN'
|
|
89
|
+
) {
|
|
90
|
+
if (currentSslMode === 'verify-full' || currentSslMode === 'verify-ca') {
|
|
91
|
+
return 'require';
|
|
92
|
+
}
|
|
93
|
+
if (currentSslMode === 'prefer') {
|
|
94
|
+
return 'require';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Hostname mismatch errors
|
|
99
|
+
if (
|
|
100
|
+
errorMessage.includes('hostname') ||
|
|
101
|
+
errorMessage.includes('host name') ||
|
|
102
|
+
errorCode === 'ERR_TLS_CERT_ALTNAME_INVALID'
|
|
103
|
+
) {
|
|
104
|
+
if (currentSslMode === 'verify-full') {
|
|
105
|
+
return 'verify-ca';
|
|
106
|
+
}
|
|
107
|
+
return 'require';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// SSL/TLS protocol errors (but not connection refused, which is handled separately)
|
|
111
|
+
if (
|
|
112
|
+
(errorMessage.includes('ssl') ||
|
|
113
|
+
errorMessage.includes('tls') ||
|
|
114
|
+
errorMessage.includes('protocol') ||
|
|
115
|
+
errorStack.includes('ssl')) &&
|
|
116
|
+
errorCode !== 'ECONNREFUSED'
|
|
117
|
+
) {
|
|
118
|
+
// If SSL is enabled and failing, try disabling
|
|
119
|
+
if (currentSslMode !== 'disable' && currentSslMode !== 'prefer') {
|
|
120
|
+
// First try prefer (allows fallback to non-SSL)
|
|
121
|
+
if (currentSslMode === 'require' || currentSslMode === 'verify-ca' || currentSslMode === 'verify-full') {
|
|
122
|
+
return 'prefer';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// If prefer failed, try disable
|
|
126
|
+
if (currentSslMode === 'prefer') {
|
|
127
|
+
return 'disable';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Connection refused might indicate SSL requirement
|
|
132
|
+
if (errorCode === 'ECONNREFUSED' || errorMessage.includes('connection refused')) {
|
|
133
|
+
// If SSL is disabled, server might require SSL
|
|
134
|
+
if (currentSslMode === 'disable') {
|
|
135
|
+
return 'prefer';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Connection timeout with SSL might need different mode
|
|
140
|
+
if (
|
|
141
|
+
(errorCode === 'ETIMEDOUT' || errorMessage.includes('timeout')) &&
|
|
142
|
+
currentSslMode !== 'disable'
|
|
143
|
+
) {
|
|
144
|
+
return 'prefer';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get SSL configuration based on SSL mode.
|
|
152
|
+
* @param {string} sslMode - SSL mode string
|
|
153
|
+
* @returns {object|null} SSL configuration object or null to disable SSL
|
|
154
|
+
*/
|
|
155
|
+
function getSslConfig(sslMode) {
|
|
156
|
+
switch (sslMode?.toLowerCase()) {
|
|
157
|
+
case 'disable':
|
|
158
|
+
return null;
|
|
159
|
+
case 'require':
|
|
160
|
+
return { rejectUnauthorized: false };
|
|
161
|
+
case 'prefer':
|
|
162
|
+
return { rejectUnauthorized: false };
|
|
163
|
+
case 'verify-ca':
|
|
164
|
+
return { rejectUnauthorized: true };
|
|
165
|
+
case 'verify-full':
|
|
166
|
+
return { rejectUnauthorized: true };
|
|
167
|
+
default:
|
|
168
|
+
return { rejectUnauthorized: false };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the current connection pool.
|
|
174
|
+
* @returns {object} The connection pool wrapper
|
|
175
|
+
* @throws {Error} If pool is not initialized
|
|
176
|
+
*/
|
|
177
|
+
function getPool() {
|
|
178
|
+
if (!sql) {
|
|
179
|
+
throw new Error('Database pool not initialized. Call createPool first.');
|
|
180
|
+
}
|
|
181
|
+
return createPoolWrapper(sql);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Close the connection pool gracefully.
|
|
186
|
+
* @returns {Promise} Promise that resolves when pool is closed
|
|
187
|
+
*/
|
|
188
|
+
function closePool() {
|
|
189
|
+
if (sql) {
|
|
190
|
+
return sql.end();
|
|
191
|
+
}
|
|
192
|
+
return Promise.resolve();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
createPool,
|
|
197
|
+
getPool,
|
|
198
|
+
closePool,
|
|
199
|
+
};
|
|
200
|
+
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Routes
|
|
3
|
+
*
|
|
4
|
+
* RESTful API endpoints for database operations.
|
|
5
|
+
*
|
|
6
|
+
* Endpoints:
|
|
7
|
+
* - GET /api/tables - List all tables in the database
|
|
8
|
+
* - GET /api/tables/:tableName - Get paginated table data
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - SQL injection prevention via table name sanitization
|
|
12
|
+
* - Cursor-based pagination for efficient large table navigation
|
|
13
|
+
* - Automatic primary key detection for optimized pagination
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const express = require('express');
|
|
17
|
+
const { getPool } = require('../db/connection');
|
|
18
|
+
|
|
19
|
+
const router = express.Router();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize table name to prevent SQL injection.
|
|
23
|
+
* Only allows alphanumeric characters, underscores, and dots.
|
|
24
|
+
* @param {string} tableName - Table name to sanitize
|
|
25
|
+
* @returns {string} Sanitized table name
|
|
26
|
+
* @throws {Error} If table name contains invalid characters
|
|
27
|
+
*/
|
|
28
|
+
function sanitizeTableName(tableName) {
|
|
29
|
+
if (!/^[a-zA-Z0-9_.]+$/.test(tableName)) {
|
|
30
|
+
throw new Error('Invalid table name');
|
|
31
|
+
}
|
|
32
|
+
return tableName;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* GET /api/tables
|
|
37
|
+
*
|
|
38
|
+
* Returns a list of all tables in the public schema.
|
|
39
|
+
* Only returns BASE TABLE types (excludes views, sequences, etc.).
|
|
40
|
+
*
|
|
41
|
+
* Response: { tables: string[] }
|
|
42
|
+
*/
|
|
43
|
+
router.get('/tables', async (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const pool = getPool();
|
|
46
|
+
const result = await pool.query(`
|
|
47
|
+
SELECT table_name
|
|
48
|
+
FROM information_schema.tables
|
|
49
|
+
WHERE table_schema = 'public'
|
|
50
|
+
AND table_type = 'BASE TABLE'
|
|
51
|
+
ORDER BY table_name;
|
|
52
|
+
`);
|
|
53
|
+
|
|
54
|
+
const tables = result.rows.map(row => row.table_name);
|
|
55
|
+
res.json({ tables });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Error fetching tables:', error);
|
|
58
|
+
res.status(500).json({ error: error.message });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the primary key column name for a table.
|
|
64
|
+
* Used to enable cursor-based pagination for better performance.
|
|
65
|
+
* @param {Pool} pool - Database connection pool
|
|
66
|
+
* @param {string} tableName - Name of the table
|
|
67
|
+
* @returns {Promise<string|null>} Primary key column name or null if no primary key exists
|
|
68
|
+
*/
|
|
69
|
+
async function getPrimaryKeyColumn(pool, tableName) {
|
|
70
|
+
try {
|
|
71
|
+
const pkQuery = `
|
|
72
|
+
SELECT kcu.column_name
|
|
73
|
+
FROM information_schema.table_constraints tc
|
|
74
|
+
JOIN information_schema.key_column_usage kcu
|
|
75
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
76
|
+
AND tc.table_schema = kcu.table_schema
|
|
77
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
78
|
+
AND tc.table_name = $1
|
|
79
|
+
AND tc.table_schema = 'public'
|
|
80
|
+
LIMIT 1;
|
|
81
|
+
`;
|
|
82
|
+
const result = await pool.query(pkQuery, [tableName]);
|
|
83
|
+
const pkColumn = result.rows.length > 0 ? result.rows[0].column_name : null;
|
|
84
|
+
return pkColumn;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Error getting primary key:', error);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* GET /api/tables/:tableName
|
|
93
|
+
*
|
|
94
|
+
* Returns paginated table data with support for cursor-based pagination.
|
|
95
|
+
*
|
|
96
|
+
* Query parameters:
|
|
97
|
+
* - page: Page number (default: 1)
|
|
98
|
+
* - limit: Rows per page (default: 100)
|
|
99
|
+
* - cursor: Cursor value for cursor-based pagination (optional)
|
|
100
|
+
*
|
|
101
|
+
* Pagination strategy:
|
|
102
|
+
* - If table has primary key and cursor is provided: Use cursor-based pagination (WHERE id > cursor)
|
|
103
|
+
* - If table has primary key and page=1: Start from beginning, return cursor for next page
|
|
104
|
+
* - Otherwise: Use OFFSET-based pagination (for backward nav, page jumps, or tables without PK)
|
|
105
|
+
*
|
|
106
|
+
* Response: {
|
|
107
|
+
* rows: Object[],
|
|
108
|
+
* totalCount: number,
|
|
109
|
+
* page: number,
|
|
110
|
+
* limit: number,
|
|
111
|
+
* isApproximate: boolean,
|
|
112
|
+
* nextCursor: string|null,
|
|
113
|
+
* hasPrimaryKey: boolean
|
|
114
|
+
* }
|
|
115
|
+
*/
|
|
116
|
+
router.get('/tables/:tableName', async (req, res) => {
|
|
117
|
+
try {
|
|
118
|
+
const tableName = sanitizeTableName(req.params.tableName);
|
|
119
|
+
const page = parseInt(req.query.page || '1', 10);
|
|
120
|
+
const limit = parseInt(req.query.limit || '100', 10);
|
|
121
|
+
const cursor = req.query.cursor;
|
|
122
|
+
|
|
123
|
+
if (page < 1 || limit < 1) {
|
|
124
|
+
return res.status(400).json({ error: 'Page and limit must be positive integers' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const pool = getPool();
|
|
128
|
+
const primaryKeyColumn = await getPrimaryKeyColumn(pool, tableName);
|
|
129
|
+
|
|
130
|
+
const countQuery = `SELECT COUNT(*) as total FROM "${tableName}"`;
|
|
131
|
+
const countResult = await pool.query(countQuery);
|
|
132
|
+
const totalCount = parseInt(countResult.rows[0].total, 10);
|
|
133
|
+
const isApproximate = false;
|
|
134
|
+
|
|
135
|
+
let dataResult;
|
|
136
|
+
let nextCursor = null;
|
|
137
|
+
|
|
138
|
+
if (primaryKeyColumn && cursor) {
|
|
139
|
+
// Cursor-based pagination: WHERE id > cursor (most efficient for forward navigation)
|
|
140
|
+
const cursorQuery = `SELECT * FROM "${tableName}" WHERE "${primaryKeyColumn}" > $1 ORDER BY "${primaryKeyColumn}" ASC LIMIT $2`;
|
|
141
|
+
const cursorParams = [cursor, limit];
|
|
142
|
+
dataResult = await pool.query(cursorQuery, cursorParams);
|
|
143
|
+
|
|
144
|
+
if (dataResult.rows.length > 0) {
|
|
145
|
+
const lastRow = dataResult.rows[dataResult.rows.length - 1];
|
|
146
|
+
nextCursor = lastRow[primaryKeyColumn];
|
|
147
|
+
}
|
|
148
|
+
} else if (primaryKeyColumn && page === 1) {
|
|
149
|
+
// First page with primary key: start from beginning, return cursor
|
|
150
|
+
const firstPageQuery = `SELECT * FROM "${tableName}" ORDER BY "${primaryKeyColumn}" ASC LIMIT $1`;
|
|
151
|
+
const firstPageParams = [limit];
|
|
152
|
+
dataResult = await pool.query(firstPageQuery, firstPageParams);
|
|
153
|
+
|
|
154
|
+
if (dataResult.rows.length > 0) {
|
|
155
|
+
const lastRow = dataResult.rows[dataResult.rows.length - 1];
|
|
156
|
+
nextCursor = lastRow[primaryKeyColumn];
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
// Fallback to OFFSET-based pagination
|
|
160
|
+
// Used for: backward navigation, page jumps, or tables without primary key
|
|
161
|
+
const offset = (page - 1) * limit;
|
|
162
|
+
let query;
|
|
163
|
+
const queryParams = [];
|
|
164
|
+
|
|
165
|
+
if (primaryKeyColumn) {
|
|
166
|
+
// Order by primary key for consistent results
|
|
167
|
+
query = `SELECT * FROM "${tableName}" ORDER BY "${primaryKeyColumn}" ASC LIMIT $1 OFFSET $2`;
|
|
168
|
+
queryParams.push(limit, offset);
|
|
169
|
+
} else {
|
|
170
|
+
// No primary key: no ordering guarantee
|
|
171
|
+
query = `SELECT * FROM "${tableName}" LIMIT $1 OFFSET $2`;
|
|
172
|
+
queryParams.push(limit, offset);
|
|
173
|
+
}
|
|
174
|
+
dataResult = await pool.query(query, queryParams);
|
|
175
|
+
|
|
176
|
+
// Calculate cursor for next page if primary key exists
|
|
177
|
+
if (primaryKeyColumn && dataResult.rows.length > 0) {
|
|
178
|
+
const lastRow = dataResult.rows[dataResult.rows.length - 1];
|
|
179
|
+
nextCursor = lastRow[primaryKeyColumn];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const responseData = {
|
|
184
|
+
rows: dataResult.rows,
|
|
185
|
+
totalCount,
|
|
186
|
+
page,
|
|
187
|
+
limit,
|
|
188
|
+
isApproximate,
|
|
189
|
+
nextCursor,
|
|
190
|
+
hasPrimaryKey: !!primaryKeyColumn,
|
|
191
|
+
};
|
|
192
|
+
res.json(responseData);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
console.error('Error fetching table data:', error);
|
|
195
|
+
res.status(500).json({ error: error.message });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
module.exports = router;
|
|
200
|
+
|
package/src/server.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pglens Server
|
|
3
|
+
*
|
|
4
|
+
* Express server that serves the web UI and provides API endpoints
|
|
5
|
+
* for querying PostgreSQL database tables.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - RESTful API for table listing and data retrieval
|
|
9
|
+
* - Static file serving for the client application
|
|
10
|
+
* - CORS enabled for development
|
|
11
|
+
* - Graceful shutdown handling
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const express = require('express');
|
|
15
|
+
const cors = require('cors');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { createPool, closePool } = require('./db/connection');
|
|
18
|
+
const apiRoutes = require('./routes/api');
|
|
19
|
+
|
|
20
|
+
/**
|
|
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
|
+
*/
|
|
26
|
+
function startServer(connectionString, port, sslMode) {
|
|
27
|
+
const app = express();
|
|
28
|
+
|
|
29
|
+
app.use(cors());
|
|
30
|
+
app.use(express.json());
|
|
31
|
+
|
|
32
|
+
createPool(connectionString, sslMode)
|
|
33
|
+
.then(() => {
|
|
34
|
+
app.use('/api', apiRoutes);
|
|
35
|
+
|
|
36
|
+
const clientPath = path.join(__dirname, '../client');
|
|
37
|
+
app.use(express.static(clientPath));
|
|
38
|
+
|
|
39
|
+
app.get('*', (_, res) => {
|
|
40
|
+
res.sendFile(path.join(clientPath, 'index.html'));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
app.listen(port, () => {
|
|
44
|
+
console.log(`ā Server running on http://localhost:${port}`);
|
|
45
|
+
console.log(` Open your browser to view your database`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
process.on('SIGINT', () => {
|
|
49
|
+
console.log('\nShutting down...');
|
|
50
|
+
closePool().then(() => {
|
|
51
|
+
process.exit(0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
})
|
|
55
|
+
.catch((error) => {
|
|
56
|
+
console.error('Failed to start server:', error.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { startServer };
|
|
62
|
+
|