UNPKG

cline-sdk

Version:

Comprehensive SDK for Cline - AI-powered development assistant with database integration, execution modes, and custom functions

467 lines 18.5 kB
"use strict"; /** * PostgreSQL Database Manager * * Comprehensive database management system for Cline SDK that provides: * - Database connection management * - Schema introspection and management * - CRUD operations with permission control * - Row Level Security (RLS) management * - AI-powered database interaction */ Object.defineProperty(exports, "__esModule", { value: true }); exports.PostgreSQLManager = void 0; const pg_1 = require("pg"); const events_1 = require("events"); class PostgreSQLManager extends events_1.EventEmitter { constructor(config, permissions) { super(); this.isConnected = false; this.schemas = new Map(); this.config = config; this.permissions = permissions; const poolConfig = { host: config.host, port: config.port, database: config.database, user: config.username, password: config.password, ssl: config.ssl, max: config.maxConnections || 20, idleTimeoutMillis: config.idleTimeoutMillis || 30000, connectionTimeoutMillis: config.connectionTimeoutMillis || 10000, }; this.pool = new pg_1.Pool(poolConfig); // Set up pool event handlers this.pool.on("connect", (client) => { console.log("🐘 New PostgreSQL client connected"); this.isConnected = true; this.emit("connected", client); }); this.pool.on("error", (err, client) => { console.error("❌ PostgreSQL pool error:", err); this.isConnected = false; this.emit("error", err); }); this.pool.on("remove", (client) => { console.log("🔌 PostgreSQL client removed"); this.emit("disconnected", client); }); } /** * Test database connection */ async testConnection() { try { const client = await this.pool.connect(); const result = await client.query("SELECT version()"); const version = result.rows[0].version; client.release(); this.isConnected = true; return { connected: true, message: "Successfully connected to PostgreSQL", version: version, }; } catch (error) { this.isConnected = false; return { connected: false, message: error instanceof Error ? error.message : "Unknown connection error", }; } } /** * Get all database schemas and tables */ async introspectDatabase() { if (!this.isConnected) { throw new Error("Database not connected"); } const client = await this.pool.connect(); try { // Get all schemas const schemasResult = await client.query(` SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast') ORDER BY schema_name `); const schemas = new Map(); for (const schemaRow of schemasResult.rows) { const schemaName = schemaRow.schema_name; const tables = await this.getTablesInSchema(client, schemaName); schemas.set(schemaName, tables); } this.schemas = schemas; this.emit("schemaLoaded", schemas); return schemas; } finally { client.release(); } } /** * Get detailed information about tables in a schema */ async getTablesInSchema(client, schemaName) { const tables = []; // Get all tables in schema const tablesResult = await client.query(` SELECT t.table_name, obj_description(c.oid) as table_comment FROM information_schema.tables t LEFT JOIN pg_class c ON c.relname = t.table_name LEFT JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = t.table_schema WHERE t.table_schema = $1 AND t.table_type = 'BASE TABLE' ORDER BY t.table_name `, [schemaName]); for (const tableRow of tablesResult.rows) { const tableName = tableRow.table_name; const tableComment = tableRow.table_comment; // Get columns const columns = await this.getTableColumns(client, schemaName, tableName); // Get primary keys const primaryKeys = await this.getPrimaryKeys(client, schemaName, tableName); // Get foreign keys const foreignKeys = await this.getForeignKeys(client, schemaName, tableName); // Get indexes const indexes = await this.getIndexes(client, schemaName, tableName); // Get constraints const constraints = await this.getConstraints(client, schemaName, tableName); // Get permissions (this would need to be customized based on your permission system) const permissions = await this.getTablePermissions(client, schemaName, tableName); // Check if RLS is enabled const rlsInfo = await this.getRLSInfo(client, schemaName, tableName); const tableSchema = { tableName, schemaName, columns, primaryKeys, foreignKeys, indexes, constraints, permissions, description: tableComment, rowLevelSecurity: rlsInfo.enabled, policies: rlsInfo.policies, }; tables.push(tableSchema); } return tables; } /** * Get column information for a table */ async getTableColumns(client, schemaName, tableName) { const result = await client.query(` SELECT c.column_name, c.data_type, c.is_nullable, c.column_default, c.character_maximum_length, col_description(pgc.oid, c.ordinal_position) as column_comment, CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key, CASE WHEN fk.column_name IS NOT NULL THEN true ELSE false END as is_foreign_key, fk.foreign_table_name, fk.foreign_column_name FROM information_schema.columns c LEFT JOIN pg_class pgc ON pgc.relname = c.table_name LEFT JOIN pg_namespace pgn ON pgn.oid = pgc.relnamespace AND pgn.nspname = c.table_schema LEFT JOIN ( SELECT ku.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name WHERE tc.table_schema = $1 AND tc.table_name = $2 AND tc.constraint_type = 'PRIMARY KEY' ) pk ON pk.column_name = c.column_name LEFT JOIN ( SELECT ku.column_name, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name WHERE tc.table_schema = $1 AND tc.table_name = $2 AND tc.constraint_type = 'FOREIGN KEY' ) fk ON fk.column_name = c.column_name WHERE c.table_schema = $1 AND c.table_name = $2 ORDER BY c.ordinal_position `, [schemaName, tableName]); return result.rows.map((row) => ({ columnName: row.column_name, dataType: row.data_type, isNullable: row.is_nullable === "YES", defaultValue: row.column_default, maxLength: row.character_maximum_length, description: row.column_comment, isPrimaryKey: row.is_primary_key, isForeignKey: row.is_foreign_key, referencedTable: row.foreign_table_name, referencedColumn: row.foreign_column_name, })); } /** * Get primary keys for a table */ async getPrimaryKeys(client, schemaName, tableName) { const result = await client.query(` SELECT ku.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name WHERE tc.table_schema = $1 AND tc.table_name = $2 AND tc.constraint_type = 'PRIMARY KEY' ORDER BY ku.ordinal_position `, [schemaName, tableName]); return result.rows.map((row) => row.column_name); } /** * Get foreign keys for a table */ async getForeignKeys(client, schemaName, tableName) { const result = await client.query(` SELECT tc.constraint_name, ku.column_name, ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name, rc.delete_rule, rc.update_rule FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name JOIN information_schema.referential_constraints rc ON tc.constraint_name = rc.constraint_name WHERE tc.table_schema = $1 AND tc.table_name = $2 AND tc.constraint_type = 'FOREIGN KEY' `, [schemaName, tableName]); return result.rows.map((row) => ({ constraintName: row.constraint_name, columnName: row.column_name, referencedTable: row.foreign_table_name, referencedColumn: row.foreign_column_name, onDelete: row.delete_rule, onUpdate: row.update_rule, })); } /** * Get indexes for a table */ async getIndexes(client, schemaName, tableName) { const result = await client.query(` SELECT i.indexname, i.indexdef, i.indexname LIKE '%_pkey' OR i.indexname LIKE '%unique%' as is_unique FROM pg_indexes i WHERE i.schemaname = $1 AND i.tablename = $2 `, [schemaName, tableName]); return result.rows.map((row) => ({ indexName: row.indexname, columns: [], // Would need additional parsing of indexdef isUnique: row.is_unique, indexType: "btree", // Default, would need additional query to determine actual type })); } /** * Get constraints for a table */ async getConstraints(client, schemaName, tableName) { const result = await client.query(` SELECT tc.constraint_name, tc.constraint_type, string_agg(ku.column_name, ', ') as columns, cc.check_clause FROM information_schema.table_constraints tc LEFT JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name LEFT JOIN information_schema.check_constraints cc ON tc.constraint_name = cc.constraint_name WHERE tc.table_schema = $1 AND tc.table_name = $2 GROUP BY tc.constraint_name, tc.constraint_type, cc.check_clause `, [schemaName, tableName]); return result.rows.map((row) => ({ constraintName: row.constraint_name, constraintType: row.constraint_type, columns: row.columns ? row.columns.split(", ") : [], definition: row.check_clause || "", })); } /** * Get table permissions (simplified - would need actual permission checking) */ async getTablePermissions(client, schemaName, tableName) { // This would need to be implemented based on your specific permission system // For now, returning based on the configured permissions return { select: this.permissions.allowRead, insert: this.permissions.allowWrite, update: this.permissions.allowWrite, delete: this.permissions.allowDelete, truncate: this.permissions.allowDelete, references: true, trigger: this.permissions.allowSchemaChanges, }; } /** * Get Row Level Security information */ async getRLSInfo(client, schemaName, tableName) { // Check if RLS is enabled const rlsResult = await client.query(` SELECT relrowsecurity FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = $1 AND c.relname = $2 `, [schemaName, tableName]); const rlsEnabled = rlsResult.rows[0]?.relrowsecurity || false; // Get policies if RLS is enabled let policies = []; if (rlsEnabled) { const policiesResult = await client.query(` SELECT pol.polname as policy_name, pol.polcmd as command, pol.polroles::regrole[] as roles, pol.polqual as using_clause, pol.polwithcheck as with_check_clause FROM pg_policy pol JOIN pg_class c ON c.oid = pol.polrelid JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = $1 AND c.relname = $2 `, [schemaName, tableName]); policies = policiesResult.rows.map((row) => ({ policyName: row.policy_name, command: row.command, roles: row.roles || [], using: row.using_clause, withCheck: row.with_check_clause, })); } return { enabled: rlsEnabled, policies }; } /** * Execute a SQL query with permission checking */ async executeQuery(query, params = []) { const startTime = Date.now(); try { // Check permissions before executing this.validateQueryPermissions(query); const client = await this.pool.connect(); try { const result = await client.query(query, params); const executionTime = Date.now() - startTime; this.emit("queryExecuted", { query, params, result, executionTime }); return { success: true, data: result.rows, rowCount: result.rowCount || 0, executionTime, query, }; } finally { client.release(); } } catch (error) { const executionTime = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : "Unknown error"; this.emit("queryError", { query, params, error: errorMessage, executionTime }); return { success: false, error: errorMessage, executionTime, query, }; } } /** * Validate query permissions */ validateQueryPermissions(query) { const upperQuery = query.toUpperCase().trim(); // Check read permissions if (upperQuery.startsWith("SELECT") && !this.permissions.allowRead) { throw new Error("Read operations are not permitted"); } // Check write permissions if ((upperQuery.startsWith("INSERT") || upperQuery.startsWith("UPDATE")) && !this.permissions.allowWrite) { throw new Error("Write operations are not permitted"); } // Check delete permissions if ((upperQuery.startsWith("DELETE") || upperQuery.startsWith("TRUNCATE")) && !this.permissions.allowDelete) { throw new Error("Delete operations are not permitted"); } // Check schema change permissions if ((upperQuery.startsWith("CREATE") || upperQuery.startsWith("ALTER") || upperQuery.startsWith("DROP")) && !this.permissions.allowSchemaChanges) { throw new Error("Schema modifications are not permitted"); } // Check RLS management permissions if (upperQuery.includes("POLICY") && !this.permissions.allowRLSManagement) { throw new Error("Row Level Security management is not permitted"); } } /** * Get database schema information for AI context */ getSchemaContext() { let context = "Database Schema Information:\n\n"; for (const [schemaName, tables] of this.schemas) { context += `Schema: ${schemaName}\n`; context += "=".repeat(schemaName.length + 8) + "\n\n"; for (const table of tables) { context += `Table: ${table.tableName}\n`; if (table.description) { context += `Description: ${table.description}\n`; } context += `Columns:\n`; for (const column of table.columns) { context += ` - ${column.columnName} (${column.dataType})`; if (column.isPrimaryKey) context += " [PK]"; if (column.isForeignKey) context += ` [FK -> ${column.referencedTable}.${column.referencedColumn}]`; if (!column.isNullable) context += " [NOT NULL]"; if (column.description) context += ` - ${column.description}`; context += "\n"; } if (table.rowLevelSecurity) { context += `Row Level Security: Enabled\n`; if (table.policies.length > 0) { context += `Policies: ${table.policies.map((p) => p.policyName).join(", ")}\n`; } } context += "\n"; } } return context; } /** * Close database connections */ async close() { await this.pool.end(); this.isConnected = false; console.log("🔌 PostgreSQL connection pool closed"); } /** * Get connection status */ getConnectionStatus() { return { connected: this.isConnected, activeConnections: this.pool.totalCount, config: { host: this.config.host, port: this.config.port, database: this.config.database, username: this.config.username, }, }; } } exports.PostgreSQLManager = PostgreSQLManager; //# sourceMappingURL=postgres-manager.js.map