- Add HTTP transport (StreamableHTTPServerTransport) - Add shared server module (src/server/) - Configure production for hub.descomplicar.pt - Add SSH tunnel script (start-tunnel.sh) - Fix connection leak in pg-client.ts - Fix atomicity bug in comments deletion - Update docs with test plan for 164 tools Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
169 lines
4.3 KiB
TypeScript
169 lines
4.3 KiB
TypeScript
/**
|
|
* MCP Outline PostgreSQL - PostgreSQL Client
|
|
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
|
*/
|
|
|
|
import { Pool, PoolConfig, QueryResult, QueryResultRow } from 'pg';
|
|
import { DatabaseConfig } from './config/database.js';
|
|
import { logger } from './utils/logger.js';
|
|
|
|
export class PgClient {
|
|
private pool: Pool;
|
|
private isConnected: boolean = false;
|
|
|
|
constructor(config: DatabaseConfig) {
|
|
const poolConfig: PoolConfig = config.connectionString
|
|
? {
|
|
connectionString: config.connectionString,
|
|
max: config.max,
|
|
idleTimeoutMillis: config.idleTimeoutMillis,
|
|
connectionTimeoutMillis: config.connectionTimeoutMillis
|
|
}
|
|
: {
|
|
host: config.host,
|
|
port: config.port,
|
|
user: config.user,
|
|
password: config.password,
|
|
database: config.database,
|
|
ssl: config.ssl ? { rejectUnauthorized: false } : false,
|
|
max: config.max,
|
|
idleTimeoutMillis: config.idleTimeoutMillis,
|
|
connectionTimeoutMillis: config.connectionTimeoutMillis
|
|
};
|
|
|
|
this.pool = new Pool(poolConfig);
|
|
|
|
// Handle pool errors
|
|
this.pool.on('error', (err) => {
|
|
logger.error('Unexpected PostgreSQL pool error', { error: err.message });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the underlying pool for direct access
|
|
*/
|
|
getPool(): Pool {
|
|
return this.pool;
|
|
}
|
|
|
|
/**
|
|
* Test database connection
|
|
*/
|
|
async testConnection(): Promise<boolean> {
|
|
let client = null;
|
|
try {
|
|
client = await this.pool.connect();
|
|
await client.query('SELECT 1');
|
|
this.isConnected = true;
|
|
logger.info('PostgreSQL connection successful');
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('PostgreSQL connection failed', {
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
this.isConnected = false;
|
|
return false;
|
|
} finally {
|
|
if (client) {
|
|
client.release();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute a query with parameters
|
|
*/
|
|
async query<T extends QueryResultRow = any>(sql: string, params?: any[]): Promise<T[]> {
|
|
const start = Date.now();
|
|
try {
|
|
const result = await this.pool.query<T>(sql, params);
|
|
const duration = Date.now() - start;
|
|
|
|
logger.debug('Query executed', {
|
|
sql: sql.substring(0, 100),
|
|
duration,
|
|
rowCount: result.rowCount
|
|
});
|
|
|
|
return result.rows;
|
|
} catch (error) {
|
|
const duration = Date.now() - start;
|
|
logger.error('Query failed', {
|
|
sql: sql.substring(0, 100),
|
|
duration,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute a query and return the full result
|
|
*/
|
|
async queryRaw<T extends QueryResultRow = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {
|
|
return this.pool.query<T>(sql, params);
|
|
}
|
|
|
|
/**
|
|
* Execute a query and return a single row
|
|
*/
|
|
async queryOne<T extends QueryResultRow = any>(sql: string, params?: any[]): Promise<T | null> {
|
|
const rows = await this.query<T>(sql, params);
|
|
return rows.length > 0 ? rows[0] : null;
|
|
}
|
|
|
|
/**
|
|
* Execute multiple queries in a transaction
|
|
*/
|
|
async transaction<T>(callback: (client: any) => Promise<T>): Promise<T> {
|
|
const client = await this.pool.connect();
|
|
try {
|
|
await client.query('BEGIN');
|
|
const result = await callback(client);
|
|
await client.query('COMMIT');
|
|
return result;
|
|
} catch (error) {
|
|
try {
|
|
await client.query('ROLLBACK');
|
|
} catch (rollbackError) {
|
|
logger.error('Rollback failed', {
|
|
error: rollbackError instanceof Error ? rollbackError.message : String(rollbackError),
|
|
});
|
|
}
|
|
throw error;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Close the pool
|
|
*/
|
|
async close(): Promise<void> {
|
|
await this.pool.end();
|
|
this.isConnected = false;
|
|
logger.info('PostgreSQL pool closed');
|
|
}
|
|
|
|
/**
|
|
* Check if connected
|
|
*/
|
|
isPoolConnected(): boolean {
|
|
return this.isConnected;
|
|
}
|
|
}
|
|
|
|
// Export a singleton factory
|
|
let instance: PgClient | null = null;
|
|
|
|
export function createPgClient(config: DatabaseConfig): PgClient {
|
|
if (!instance) {
|
|
instance = new PgClient(config);
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
export function getPgClient(): PgClient | null {
|
|
return instance;
|
|
}
|