UNPKG

plugin-postgresql-connector

Version:

NocoBase plugin for connecting to external PostgreSQL databases

350 lines 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.QueryExecutor = exports.ProcedureQueryStrategy = exports.ModifyQueryStrategy = exports.SelectQueryStrategy = exports.QueryStrategy = void 0; const sql_formatter_1 = require("sql-formatter"); class QueryStrategy { getDataTypeName(dataTypeID) { const typeMap = { 16: 'BOOLEAN', 17: 'BYTEA', 20: 'BIGINT', 21: 'SMALLINT', 23: 'INTEGER', 25: 'TEXT', 700: 'REAL', 701: 'DOUBLE_PRECISION', 1043: 'VARCHAR', 1082: 'DATE', 1083: 'TIME', 1114: 'TIMESTAMP', 1184: 'TIMESTAMPTZ', 1700: 'NUMERIC', 2950: 'UUID', 3802: 'JSONB', 114: 'JSON', }; return typeMap[dataTypeID] || 'UNKNOWN'; } formatExecutionTime(startTime) { return Date.now() - startTime; } } exports.QueryStrategy = QueryStrategy; class SelectQueryStrategy extends QueryStrategy { async execute(client, query, params = [], options = {}) { const startTime = Date.now(); try { // Apply row limit if specified let finalQuery = query; if (options.maxRows && !query.toLowerCase().includes('limit')) { finalQuery = `${query} LIMIT ${options.maxRows}`; } const result = await client.query(finalQuery, params); return { rows: result.rows, rowCount: result.rowCount || 0, fields: result.fields?.map(field => ({ name: field.name, dataTypeID: field.dataTypeID, dataTypeSize: field.dataTypeSize, dataTypeName: this.getDataTypeName(field.dataTypeID), })), executionTime: this.formatExecutionTime(startTime), query: finalQuery, formattedQuery: options.formatQuery ? (0, sql_formatter_1.format)(finalQuery) : undefined, }; } catch (error) { throw new Error(`SELECT query failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } exports.SelectQueryStrategy = SelectQueryStrategy; class ModifyQueryStrategy extends QueryStrategy { async execute(client, query, params = [], options = {}) { const startTime = Date.now(); try { const result = await client.query(query, params); return { rows: result.rows || [], rowCount: result.rowCount || 0, fields: result.fields?.map(field => ({ name: field.name, dataTypeID: field.dataTypeID, dataTypeSize: field.dataTypeSize, dataTypeName: this.getDataTypeName(field.dataTypeID), })), executionTime: this.formatExecutionTime(startTime), query, formattedQuery: options.formatQuery ? (0, sql_formatter_1.format)(query) : undefined, }; } catch (error) { throw new Error(`Modify query failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } exports.ModifyQueryStrategy = ModifyQueryStrategy; class ProcedureQueryStrategy extends QueryStrategy { async execute(client, query, params = [], options = {}) { const startTime = Date.now(); try { // For stored procedures, we might need to use CALL syntax const result = await client.query(query, params); return { rows: result.rows || [], rowCount: result.rowCount || 0, fields: result.fields?.map(field => ({ name: field.name, dataTypeID: field.dataTypeID, dataTypeSize: field.dataTypeSize, dataTypeName: this.getDataTypeName(field.dataTypeID), })), executionTime: this.formatExecutionTime(startTime), query, formattedQuery: options.formatQuery ? (0, sql_formatter_1.format)(query) : undefined, }; } catch (error) { throw new Error(`Procedure execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } exports.ProcedureQueryStrategy = ProcedureQueryStrategy; class QueryExecutor { constructor(connectionManager) { this.connectionManager = connectionManager; this.queryCache = new Map(); this.cacheTTL = 300000; // 5 minutes } async executeQuery(connectionId, query, params = [], options = {}) { const cleanQuery = this.validateAndCleanQuery(query); const queryType = this.detectQueryType(cleanQuery); // Check cache for SELECT queries if (queryType === 'SELECT' && this.shouldUseCache(cleanQuery, params)) { const cached = this.getCachedResult(cleanQuery, params); if (cached) { return cached; } } const client = await this.connectionManager.getConnection(connectionId); try { const startTime = Date.now(); // Set query timeout if specified if (options.timeout) { await client.query(`SET statement_timeout = ${options.timeout}`); } // Apply row limit if specified and not already present let finalQuery = cleanQuery; if (options.maxRows && !cleanQuery.toLowerCase().includes('limit')) { finalQuery = `${cleanQuery} LIMIT ${options.maxRows}`; } const result = await client.query(finalQuery, params); const executionTime = Date.now() - startTime; const queryResult = { rows: result.rows, rowCount: result.rowCount || 0, fields: result.fields?.map(field => ({ name: field.name, dataTypeID: field.dataTypeID, dataTypeSize: field.dataTypeSize, dataTypeName: this.getDataTypeName(field.dataTypeID), })), executionTime, query: finalQuery, formattedQuery: options.formatQuery ? (0, sql_formatter_1.format)(finalQuery) : undefined, }; // Cache SELECT query results if (queryType === 'SELECT') { this.cacheResult(cleanQuery, params, queryResult); } return queryResult; } finally { client.release(); // Reset timeout if (options.timeout) { await client.query('RESET statement_timeout'); } } } async executeProcedure(connectionId, procedureName, params = [], options = {}) { const paramPlaceholders = params.map((_, index) => `$${index + 1}`).join(', '); const query = `CALL ${procedureName}(${paramPlaceholders})`; return this.executeQuery(connectionId, query, params, options); } async executeFunction(connectionId, functionName, params = [], options = {}) { const paramPlaceholders = params.map((_, index) => `$${index + 1}`).join(', '); const query = `SELECT ${functionName}(${paramPlaceholders})`; return this.executeQuery(connectionId, query, params, options); } async getViewData(connectionId, viewName, limit = 100, options = {}) { const query = `SELECT * FROM ${viewName} LIMIT $1`; return this.executeQuery(connectionId, query, [limit], options); } async getTableData(connectionId, tableName, limit = 100, offset = 0, orderBy, options = {}) { let query = `SELECT * FROM ${tableName}`; const params = []; let paramIndex = 1; if (orderBy) { query += ` ORDER BY ${orderBy}`; } query += ` LIMIT $${paramIndex++}`; params.push(limit); if (offset > 0) { query += ` OFFSET $${paramIndex}`; params.push(offset); } return this.executeQuery(connectionId, query, params, options); } validateAndCleanQuery(query) { const trimmedQuery = query.trim(); if (!trimmedQuery) { throw new Error('Query cannot be empty'); } // Basic security checks const dangerousPatterns = [ /\b(DROP|TRUNCATE|ALTER)\s+/i, /\b(CREATE|DROP)\s+(USER|ROLE)\s+/i, /\b(GRANT|REVOKE)\s+/i, /\bSHUTDOWN\b/i, /\bRESTART\b/i, ]; for (const pattern of dangerousPatterns) { if (pattern.test(trimmedQuery)) { throw new Error('Potentially dangerous SQL statement detected'); } } return trimmedQuery; } detectQueryType(query) { const upperQuery = query.toUpperCase().trim(); if (upperQuery.startsWith('SELECT')) return 'SELECT'; if (upperQuery.startsWith('INSERT')) return 'INSERT'; if (upperQuery.startsWith('UPDATE')) return 'UPDATE'; if (upperQuery.startsWith('DELETE')) return 'DELETE'; if (upperQuery.startsWith('CALL')) return 'CALL'; if (upperQuery.startsWith('WITH')) return 'SELECT'; // CTE queries // Check for function calls if (upperQuery.startsWith('SELECT') && upperQuery.includes('(')) { return 'FUNCTION'; } return 'SELECT'; // Default to SELECT for safety } getDataTypeName(dataTypeID) { const typeMap = { 16: 'BOOLEAN', 17: 'BYTEA', 20: 'BIGINT', 21: 'SMALLINT', 23: 'INTEGER', 25: 'TEXT', 700: 'REAL', 701: 'DOUBLE_PRECISION', 1043: 'VARCHAR', 1082: 'DATE', 1083: 'TIME', 1114: 'TIMESTAMP', 1184: 'TIMESTAMPTZ', 1700: 'NUMERIC', 2950: 'UUID', 3802: 'JSONB', 114: 'JSON', }; return typeMap[dataTypeID] || 'UNKNOWN'; } shouldUseCache(query, params) { // Only cache simple SELECT queries without parameters that might change frequently const upperQuery = query.toUpperCase(); // Don't cache queries with NOW(), CURRENT_TIMESTAMP, etc. const nonCacheablePatterns = [ /\bNOW\(\)/i, /\bCURRENT_TIMESTAMP\b/i, /\bCURRENT_DATE\b/i, /\bRANDOM\(\)/i, /\bGEN_RANDOM_UUID\(\)/i, ]; return !nonCacheablePatterns.some(pattern => pattern.test(upperQuery)); } getCachedResult(query, params) { const cacheKey = this.generateCacheKey(query, params); const cached = this.queryCache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this.cacheTTL) { return { ...cached.result }; // Return a copy } if (cached) { this.queryCache.delete(cacheKey); } return null; } cacheResult(query, params, result) { const cacheKey = this.generateCacheKey(query, params); this.queryCache.set(cacheKey, { result: { ...result }, // Store a copy timestamp: Date.now(), }); // Cleanup old cache entries if cache gets too large if (this.queryCache.size > 100) { this.cleanupCache(); } } generateCacheKey(query, params) { return `${query}:${JSON.stringify(params)}`; } cleanupCache() { const now = Date.now(); for (const [key, value] of this.queryCache) { if (now - value.timestamp > this.cacheTTL) { this.queryCache.delete(key); } } } /** * Get query execution statistics */ getStatistics() { return { cacheSize: this.queryCache.size, supportedOperations: ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'CALL', 'PROCEDURE', 'FUNCTION'], cacheTTL: this.cacheTTL, }; } /** * Clear query cache */ clearCache() { this.queryCache.clear(); } /** * Analyze query complexity (basic implementation) */ analyzeQuery(query) { const upperQuery = query.toUpperCase(); const complexity = { joins: (upperQuery.match(/\bJOIN\b/g) || []).length, subqueries: (upperQuery.match(/\bSELECT\b/g) || []).length - 1, aggregations: (upperQuery.match(/\b(COUNT|SUM|AVG|MIN|MAX|GROUP BY)\b/g) || []).length, orderBy: upperQuery.includes('ORDER BY'), limit: upperQuery.includes('LIMIT'), estimated_complexity: 'low', }; // Simple complexity estimation const score = complexity.joins * 2 + complexity.subqueries * 3 + complexity.aggregations; if (score > 10) { complexity.estimated_complexity = 'high'; } else if (score > 5) { complexity.estimated_complexity = 'medium'; } return complexity; } } exports.QueryExecutor = QueryExecutor; exports.default = QueryExecutor; //# sourceMappingURL=QueryExecutor.js.map