plugin-postgresql-connector
Version:
NocoBase plugin for connecting to external PostgreSQL databases
242 lines (213 loc) • 7.1 kB
text/typescript
import { Pool, PoolClient, PoolConfig } from 'pg';
import CryptoJS from 'crypto-js';
export interface ConnectionConfig {
host: string;
port: number;
database: string;
username: string;
password: string;
ssl: boolean;
connectionOptions?: object;
}
export interface ConnectionResult {
success: boolean;
connectionId?: string;
error?: string;
}
export class ConnectionManager {
private pools: Map<string, Pool> = new Map();
private readonly encryptionKey: string;
private readonly maxConnections: number;
private readonly connectionTimeout: number;
private readonly idleTimeout: number;
constructor() {
this.encryptionKey = process.env.ENCRYPTION_KEY || 'default-key-change-in-production';
this.maxConnections = parseInt(process.env.POSTGRESQL_CONNECTOR_MAX_CONNECTIONS || '10');
this.connectionTimeout = parseInt(process.env.POSTGRESQL_CONNECTOR_CONNECTION_TIMEOUT || '5000');
this.idleTimeout = parseInt(process.env.POSTGRESQL_CONNECTOR_IDLE_TIMEOUT || '30000');
if (this.encryptionKey === 'default-key-change-in-production') {
console.warn('WARNING: Using default encryption key. Please set ENCRYPTION_KEY environment variable.');
}
}
/**
* Create a new connection pool and test the connection
*/
async createConnection(connectionConfig: ConnectionConfig): Promise<ConnectionResult> {
const connectionId = this.generateConnectionId();
try {
// Create connection pool configuration
const poolConfig: PoolConfig = {
host: connectionConfig.host,
port: connectionConfig.port,
database: connectionConfig.database,
user: connectionConfig.username,
password: connectionConfig.password,
max: this.maxConnections,
idleTimeoutMillis: this.idleTimeout,
connectionTimeoutMillis: this.connectionTimeout,
ssl: connectionConfig.ssl ? { rejectUnauthorized: false } : false,
...connectionConfig.connectionOptions,
};
// Create pool
const pool = new Pool(poolConfig);
// Test connection
const client = await pool.connect();
try {
await client.query('SELECT NOW()');
console.log(`Connection ${connectionId} established successfully`);
} finally {
client.release();
}
// Store pool
this.pools.set(connectionId, pool);
return {
success: true,
connectionId,
};
} catch (error) {
console.error(`Connection ${connectionId} failed:`, error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown connection error',
};
}
}
/**
* Get a client from the connection pool
*/
async getConnection(connectionId: string): Promise<PoolClient> {
const pool = this.pools.get(connectionId);
if (!pool) {
throw new Error(`Connection ${connectionId} not found`);
}
try {
return await pool.connect();
} catch (error) {
console.error(`Failed to get connection from pool ${connectionId}:`, error);
throw new Error(`Failed to get connection: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Close a specific connection pool
*/
async closeConnection(connectionId: string): Promise<void> {
const pool = this.pools.get(connectionId);
if (pool) {
try {
await pool.end();
this.pools.delete(connectionId);
console.log(`Connection ${connectionId} closed successfully`);
} catch (error) {
console.error(`Error closing connection ${connectionId}:`, error);
throw error;
}
}
}
/**
* Close all connection pools
*/
async closeAllConnections(): Promise<void> {
const closePromises = Array.from(this.pools.keys()).map(connectionId =>
this.closeConnection(connectionId)
);
await Promise.all(closePromises);
console.log('All connections closed');
}
/**
* Test connection without creating a pool
*/
async testConnection(connectionConfig: ConnectionConfig): Promise<ConnectionResult> {
const poolConfig: PoolConfig = {
host: connectionConfig.host,
port: connectionConfig.port,
database: connectionConfig.database,
user: connectionConfig.username,
password: connectionConfig.password,
max: 1, // Only one connection for testing
idleTimeoutMillis: 1000,
connectionTimeoutMillis: this.connectionTimeout,
ssl: connectionConfig.ssl ? { rejectUnauthorized: false } : false,
};
const pool = new Pool(poolConfig);
try {
const client = await pool.connect();
try {
await client.query('SELECT NOW()');
return { success: true };
} finally {
client.release();
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Connection test failed',
};
} finally {
await pool.end();
}
}
/**
* Get connection pool statistics
*/
getConnectionStats(connectionId: string): object | null {
const pool = this.pools.get(connectionId);
if (!pool) {
return null;
}
return {
totalCount: pool.totalCount,
idleCount: pool.idleCount,
waitingCount: pool.waitingCount,
};
}
/**
* Get all active connection IDs
*/
getActiveConnections(): string[] {
return Array.from(this.pools.keys());
}
/**
* Encrypt password for storage
*/
encryptPassword(password: string): string {
try {
return CryptoJS.AES.encrypt(password, this.encryptionKey).toString();
} catch (error) {
console.error('Password encryption failed:', error);
throw new Error('Failed to encrypt password');
}
}
/**
* Decrypt password from storage
*/
decryptPassword(encryptedPassword: string): string {
try {
const bytes = CryptoJS.AES.decrypt(encryptedPassword, this.encryptionKey);
const decrypted = bytes.toString(CryptoJS.enc.Utf8);
if (!decrypted) {
throw new Error('Failed to decrypt password - invalid key or corrupted data');
}
return decrypted;
} catch (error) {
console.error('Password decryption failed:', error);
throw new Error('Failed to decrypt password');
}
}
/**
* Generate unique connection ID
*/
private generateConnectionId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substr(2, 9);
return `conn_${timestamp}_${random}`;
}
/**
* Cleanup method for graceful shutdown
*/
async destroy(): Promise<void> {
console.log('ConnectionManager: Starting cleanup...');
await this.closeAllConnections();
console.log('ConnectionManager: Cleanup completed');
}
}
export default ConnectionManager;