cline-sdk
Version:
Comprehensive SDK for Cline - AI-powered development assistant with database integration, execution modes, and custom functions
467 lines • 18.5 kB
JavaScript
"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