plugin-postgresql-connector
Version:
NocoBase plugin for connecting to external PostgreSQL databases
350 lines • 13.4 kB
JavaScript
;
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