UNPKG

plugin-postgresql-connector

Version:

NocoBase plugin for connecting to external PostgreSQL databases

395 lines (354 loc) 12 kB
import { Context } from '@nocobase/server'; import { ConnectionManager, ConnectionConfig } from '../services/ConnectionManager'; import Connection from '../models/Connection'; import Joi from 'joi'; // Validation schema for connection data const connectionSchema = Joi.object({ name: Joi.string().required().min(1).max(100).trim(), host: Joi.string().required().min(1).max(255).trim(), port: Joi.number().integer().min(1).max(65535).default(5432), database: Joi.string().required().min(1).max(63).trim(), username: Joi.string().required().min(1).max(63).trim(), password: Joi.string().required().min(1), ssl: Joi.boolean().default(false), connectionOptions: Joi.object().default({}), }); const connectionUpdateSchema = connectionSchema.fork([ 'name', 'host', 'database', 'username', 'password' ], (schema) => schema.optional()); export class ConnectionController { constructor(private connectionManager: ConnectionManager) {} /** * Create a new database connection */ async create(ctx: Context) { try { // Validate input data const { error, value } = connectionSchema.validate(ctx.request.body); if (error) { ctx.status = 400; ctx.body = { success: false, error: error.details[0].message, }; return; } // Test connection first const testResult = await this.connectionManager.testConnection(value); if (!testResult.success) { ctx.status = 400; ctx.body = { success: false, error: `Connection test failed: ${testResult.error}`, }; return; } // Check if connection name already exists const existingConnection = await Connection.findOne({ where: { name: value.name, isActive: true }, }); if (existingConnection) { ctx.status = 409; ctx.body = { success: false, error: 'Connection name already exists', }; return; } // Encrypt password before saving const encryptedPassword = this.connectionManager.encryptPassword(value.password); // Save to database const connection = await Connection.create({ ...value, password: encryptedPassword, }); ctx.status = 201; ctx.body = { success: true, data: { id: connection.getDataValue('id'), name: connection.getDataValue('name'), host: connection.getDataValue('host'), port: connection.getDataValue('port'), database: connection.getDataValue('database'), username: connection.getDataValue('username'), ssl: connection.getDataValue('ssl'), isActive: connection.getDataValue('isActive'), createdAt: connection.getDataValue('createdAt'), }, message: 'Connection created successfully', }; } catch (error) { console.error('Create connection error:', error); ctx.status = 500; ctx.body = { success: false, error: `Failed to create connection: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } /** * Test database connection without saving */ async test(ctx: Context) { try { // Validate input data const { error, value } = connectionSchema.validate(ctx.request.body); if (error) { ctx.status = 400; ctx.body = { success: false, error: error.details[0].message, }; return; } // Test connection const testResult = await this.connectionManager.testConnection(value); if (testResult.success) { ctx.body = { success: true, message: 'Connection test successful', }; } else { ctx.status = 400; ctx.body = { success: false, error: testResult.error, }; } } catch (error) { console.error('Test connection error:', error); ctx.status = 500; ctx.body = { success: false, error: `Connection test failed: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } /** * List all active connections */ async list(ctx: Context) { try { const { page = 1, pageSize = 10, search } = ctx.query; const offset = (parseInt(page as string) - 1) * parseInt(pageSize as string); const limit = parseInt(pageSize as string); const whereClause: any = { isActive: true }; // Add search functionality if (search) { whereClause.name = { [Op.iLike]: `%${search}%`, }; } const { count, rows } = await Connection.findAndCountAll({ where: whereClause, attributes: ['id', 'name', 'host', 'port', 'database', 'username', 'ssl', 'isActive', 'createdAt', 'updatedAt'], offset, limit, order: [['createdAt', 'DESC']], }); ctx.body = { success: true, data: { connections: rows, pagination: { total: count, page: parseInt(page as string), pageSize: parseInt(pageSize as string), totalPages: Math.ceil(count / parseInt(pageSize as string)), }, }, }; } catch (error) { console.error('List connections error:', error); ctx.status = 500; ctx.body = { success: false, error: `Failed to list connections: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } /** * Get a specific connection by ID */ async get(ctx: Context) { try { const { id } = ctx.params; const connection = await Connection.findOne({ where: { id, isActive: true }, attributes: ['id', 'name', 'host', 'port', 'database', 'username', 'ssl', 'isActive', 'createdAt', 'updatedAt'], }); if (!connection) { ctx.status = 404; ctx.body = { success: false, error: 'Connection not found', }; return; } ctx.body = { success: true, data: connection, }; } catch (error) { console.error('Get connection error:', error); ctx.status = 500; ctx.body = { success: false, error: `Failed to get connection: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } /** * Update a connection */ async update(ctx: Context) { try { const { id } = ctx.params; // Validate input data const { error, value } = connectionUpdateSchema.validate(ctx.request.body); if (error) { ctx.status = 400; ctx.body = { success: false, error: error.details[0].message, }; return; } // Find connection const connection = await Connection.findOne({ where: { id, isActive: true }, }); if (!connection) { ctx.status = 404; ctx.body = { success: false, error: 'Connection not found', }; return; } // If password is being updated, encrypt it if (value.password) { value.password = this.connectionManager.encryptPassword(value.password); } // If connection details are being updated, test the new connection if (value.host || value.port || value.database || value.username || value.password || value.ssl !== undefined) { const currentConfig = { host: value.host || connection.getDataValue('host'), port: value.port || connection.getDataValue('port'), database: value.database || connection.getDataValue('database'), username: value.username || connection.getDataValue('username'), password: value.password ? this.connectionManager.decryptPassword(value.password) : this.connectionManager.decryptPassword(connection.getDataValue('password')), ssl: value.ssl !== undefined ? value.ssl : connection.getDataValue('ssl'), }; const testResult = await this.connectionManager.testConnection(currentConfig); if (!testResult.success) { ctx.status = 400; ctx.body = { success: false, error: `Connection test failed: ${testResult.error}`, }; return; } } // Update connection await connection.update(value); ctx.body = { success: true, data: { id: connection.getDataValue('id'), name: connection.getDataValue('name'), host: connection.getDataValue('host'), port: connection.getDataValue('port'), database: connection.getDataValue('database'), username: connection.getDataValue('username'), ssl: connection.getDataValue('ssl'), isActive: connection.getDataValue('isActive'), updatedAt: connection.getDataValue('updatedAt'), }, message: 'Connection updated successfully', }; } catch (error) { console.error('Update connection error:', error); ctx.status = 500; ctx.body = { success: false, error: `Failed to update connection: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } /** * Delete (deactivate) a connection */ async delete(ctx: Context) { try { const { id } = ctx.params; const connection = await Connection.findOne({ where: { id, isActive: true }, }); if (!connection) { ctx.status = 404; ctx.body = { success: false, error: 'Connection not found', }; return; } // Soft delete by setting isActive to false await connection.update({ isActive: false }); // Close any active connection pools for this connection try { const activeConnections = this.connectionManager.getActiveConnections(); const connectionToClose = activeConnections.find(connId => connId.includes(id)); if (connectionToClose) { await this.connectionManager.closeConnection(connectionToClose); } } catch (poolError) { console.warn('Warning: Could not close connection pool:', poolError); } ctx.body = { success: true, message: 'Connection deleted successfully', }; } catch (error) { console.error('Delete connection error:', error); ctx.status = 500; ctx.body = { success: false, error: `Failed to delete connection: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } /** * Get connection statistics */ async stats(ctx: Context) { try { const totalConnections = await Connection.count({ where: { isActive: true }, }); const activePoolConnections = this.connectionManager.getActiveConnections().length; ctx.body = { success: true, data: { totalConnections, activePoolConnections, connections: this.connectionManager.getActiveConnections().map(connId => ({ connectionId: connId, stats: this.connectionManager.getConnectionStats(connId), })), }, }; } catch (error) { console.error('Get connection stats error:', error); ctx.status = 500; ctx.body = { success: false, error: `Failed to get connection statistics: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } } export default ConnectionController;