UNPKG

plugin-postgresql-connector

Version:

NocoBase plugin for connecting to external PostgreSQL databases

487 lines 20.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.QueryController = void 0; const SavedQuery_1 = require("../models/SavedQuery"); const joi_1 = __importDefault(require("joi")); const executeQuerySchema = joi_1.default.object({ connectionId: joi_1.default.string().uuid().required(), query: joi_1.default.string().min(1).required(), parameters: joi_1.default.array().items(joi_1.default.any()).default([]), options: joi_1.default.object({ timeout: joi_1.default.number().integer().min(1000).max(300000).default(30000), maxRows: joi_1.default.number().integer().min(1).max(10000).default(1000), formatQuery: joi_1.default.boolean().default(false), includeMetadata: joi_1.default.boolean().default(true), }).default({}), }); const executeProcedureSchema = joi_1.default.object({ connectionId: joi_1.default.string().uuid().required(), procedureName: joi_1.default.string().min(1).required(), parameters: joi_1.default.array().items(joi_1.default.any()).default([]), options: joi_1.default.object({ timeout: joi_1.default.number().integer().min(1000).max(300000).default(30000), formatQuery: joi_1.default.boolean().default(false), includeMetadata: joi_1.default.boolean().default(true), }).default({}), }); const executeFunctionSchema = joi_1.default.object({ connectionId: joi_1.default.string().uuid().required(), functionName: joi_1.default.string().min(1).required(), parameters: joi_1.default.array().items(joi_1.default.any()).default([]), options: joi_1.default.object({ timeout: joi_1.default.number().integer().min(1000).max(300000).default(30000), formatQuery: joi_1.default.boolean().default(false), includeMetadata: joi_1.default.boolean().default(true), }).default({}), }); const getViewDataSchema = joi_1.default.object({ connectionId: joi_1.default.string().uuid().required(), viewName: joi_1.default.string().min(1).required(), limit: joi_1.default.number().integer().min(1).max(10000).default(100), options: joi_1.default.object({ formatQuery: joi_1.default.boolean().default(false), includeMetadata: joi_1.default.boolean().default(true), }).default({}), }); const getTableDataSchema = joi_1.default.object({ connectionId: joi_1.default.string().uuid().required(), tableName: joi_1.default.string().min(1).required(), limit: joi_1.default.number().integer().min(1).max(10000).default(100), offset: joi_1.default.number().integer().min(0).default(0), orderBy: joi_1.default.string().optional(), options: joi_1.default.object({ formatQuery: joi_1.default.boolean().default(false), includeMetadata: joi_1.default.boolean().default(true), }).default({}), }); const saveQuerySchema = joi_1.default.object({ connectionId: joi_1.default.string().uuid().required(), name: joi_1.default.string().min(1).max(255).required(), query: joi_1.default.string().min(1).required(), queryType: joi_1.default.string().valid('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'PROCEDURE', 'FUNCTION', 'VIEW').required(), parameters: joi_1.default.array().items(joi_1.default.object({ name: joi_1.default.string().required(), value: joi_1.default.any(), type: joi_1.default.string().optional(), })).default([]), description: joi_1.default.string().max(1000).optional(), category: joi_1.default.string().max(100).optional(), tags: joi_1.default.array().items(joi_1.default.string()).default([]), }); class QueryController { constructor(connectionManager, queryExecutor) { this.connectionManager = connectionManager; this.queryExecutor = queryExecutor; } async execute(ctx) { const { error, value } = executeQuerySchema.validate(ctx.request.body); if (error) { ctx.throw(400, `Validation error: ${error.details[0].message}`); } const { connectionId, query, parameters, options } = value; try { await this.validateConnection(connectionId); const queryAnalysis = this.queryExecutor.analyzeQuery(query); const startTime = Date.now(); const result = await this.queryExecutor.executeQuery(connectionId, query, parameters, options); const totalTime = Date.now() - startTime; ctx.body = { success: true, data: { ...result, analysis: queryAnalysis, totalExecutionTime: totalTime, }, meta: { executedAt: new Date().toISOString(), connectionId, queryLength: query.length, parameterCount: parameters.length, }, }; this.logQueryExecution(ctx, connectionId, query, result.executionTime, true); } catch (error) { this.logQueryExecution(ctx, connectionId, query, 0, false, error); ctx.throw(500, `Query execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async executeProcedure(ctx) { const { error, value } = executeProcedureSchema.validate(ctx.request.body); if (error) { ctx.throw(400, `Validation error: ${error.details[0].message}`); } const { connectionId, procedureName, parameters, options } = value; try { await this.validateConnection(connectionId); const result = await this.queryExecutor.executeProcedure(connectionId, procedureName, parameters, options); ctx.body = { success: true, data: result, meta: { executedAt: new Date().toISOString(), connectionId, procedureName, parameterCount: parameters.length, }, }; this.logQueryExecution(ctx, connectionId, `CALL ${procedureName}`, result.executionTime, true); } catch (error) { this.logQueryExecution(ctx, connectionId, `CALL ${procedureName}`, 0, false, error); ctx.throw(500, `Procedure execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async executeFunction(ctx) { const { error, value } = executeFunctionSchema.validate(ctx.request.body); if (error) { ctx.throw(400, `Validation error: ${error.details[0].message}`); } const { connectionId, functionName, parameters, options } = value; try { await this.validateConnection(connectionId); const result = await this.queryExecutor.executeFunction(connectionId, functionName, parameters, options); ctx.body = { success: true, data: result, meta: { executedAt: new Date().toISOString(), connectionId, functionName, parameterCount: parameters.length, }, }; this.logQueryExecution(ctx, connectionId, `SELECT ${functionName}()`, result.executionTime, true); } catch (error) { this.logQueryExecution(ctx, connectionId, `SELECT ${functionName}()`, 0, false, error); ctx.throw(500, `Function execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getViewData(ctx) { const { error, value } = getViewDataSchema.validate({ connectionId: ctx.query.connectionId, viewName: ctx.query.viewName, limit: ctx.query.limit ? parseInt(ctx.query.limit) : 100, options: ctx.query.options || {}, }); if (error) { ctx.throw(400, `Validation error: ${error.details[0].message}`); } const { connectionId, viewName, limit, options } = value; try { await this.validateConnection(connectionId); const result = await this.queryExecutor.getViewData(connectionId, viewName, limit, options); ctx.body = { success: true, data: result, meta: { executedAt: new Date().toISOString(), connectionId, viewName, limit, }, }; } catch (error) { ctx.throw(500, `Failed to fetch view data: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getTableData(ctx) { const { error, value } = getTableDataSchema.validate({ connectionId: ctx.query.connectionId, tableName: ctx.query.tableName, limit: ctx.query.limit ? parseInt(ctx.query.limit) : 100, offset: ctx.query.offset ? parseInt(ctx.query.offset) : 0, orderBy: ctx.query.orderBy, options: ctx.query.options || {}, }); if (error) { ctx.throw(400, `Validation error: ${error.details[0].message}`); } const { connectionId, tableName, limit, offset, orderBy, options } = value; try { await this.validateConnection(connectionId); const result = await this.queryExecutor.getTableData(connectionId, tableName, limit, offset, orderBy, options); ctx.body = { success: true, data: result, meta: { executedAt: new Date().toISOString(), connectionId, tableName, limit, offset, orderBy, }, }; } catch (error) { ctx.throw(500, `Failed to fetch table data: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async saveQuery(ctx) { const { error, value } = saveQuerySchema.validate(ctx.request.body); if (error) { ctx.throw(400, `Validation error: ${error.details[0].message}`); } const { connectionId, name, query, queryType, parameters, description, category, tags } = value; try { await this.validateConnection(connectionId); // Check if query name already exists for this connection const existingQuery = await SavedQuery_1.SavedQuery.findOne({ where: { connectionId, name, deletedAt: null, }, }); if (existingQuery) { ctx.throw(409, 'A query with this name already exists for this connection'); } // Validate query by attempting to analyze it try { this.queryExecutor.analyzeQuery(query); } catch (analysisError) { ctx.throw(400, `Invalid query syntax: ${analysisError instanceof Error ? analysisError.message : 'Unknown error'}`); } const savedQuery = await SavedQuery_1.SavedQuery.create({ connectionId, name, query, queryType, parameters, description, category, tags, createdBy: ctx.state.currentUser?.id, }); ctx.body = { success: true, data: { id: savedQuery.id, name: savedQuery.name, queryType: savedQuery.queryType, category: savedQuery.category, tags: savedQuery.tags, createdAt: savedQuery.createdAt, }, message: 'Query saved successfully', }; } catch (error) { if (error instanceof Error && error.message.includes('already exists')) { throw error; } ctx.throw(500, `Failed to save query: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getSavedQueries(ctx) { const { connectionId, category, queryType, search, page = 1, limit = 20 } = ctx.query; try { const where = { deletedAt: null }; if (connectionId) { where.connectionId = connectionId; } if (category) { where.category = category; } if (queryType) { where.queryType = queryType; } if (search) { where.name = { [require('sequelize').Op.iLike]: `%${search}%` }; } const offset = (parseInt(page) - 1) * parseInt(limit); const { count, rows } = await SavedQuery_1.SavedQuery.findAndCountAll({ where, attributes: ['id', 'name', 'queryType', 'category', 'tags', 'description', 'lastExecutedAt', 'executionCount', 'createdAt', 'updatedAt'], order: [['updatedAt', 'DESC']], limit: parseInt(limit), offset, }); ctx.body = { success: true, data: rows, meta: { total: count, page: parseInt(page), limit: parseInt(limit), totalPages: Math.ceil(count / parseInt(limit)), }, }; } catch (error) { ctx.throw(500, `Failed to fetch saved queries: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getSavedQuery(ctx) { const { id } = ctx.params; try { const savedQuery = await SavedQuery_1.SavedQuery.findOne({ where: { id, deletedAt: null }, }); if (!savedQuery) { ctx.throw(404, 'Saved query not found'); } ctx.body = { success: true, data: savedQuery, }; } catch (error) { if (error instanceof Error && error.message.includes('not found')) { throw error; } ctx.throw(500, `Failed to fetch saved query: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async deleteSavedQuery(ctx) { const { id } = ctx.params; try { const savedQuery = await SavedQuery_1.SavedQuery.findOne({ where: { id, deletedAt: null }, }); if (!savedQuery) { ctx.throw(404, 'Saved query not found'); } await savedQuery.update({ deletedAt: new Date(), deletedBy: ctx.state.currentUser?.id, }); ctx.body = { success: true, message: 'Query deleted successfully', }; } catch (error) { if (error instanceof Error && error.message.includes('not found')) { throw error; } ctx.throw(500, `Failed to delete saved query: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async executeSavedQuery(ctx) { const { id } = ctx.params; const { parameters = [], options = {} } = ctx.request.body; try { const savedQuery = await SavedQuery_1.SavedQuery.findOne({ where: { id, deletedAt: null }, }); if (!savedQuery) { ctx.throw(404, 'Saved query not found'); } await this.validateConnection(savedQuery.connectionId); let result; switch (savedQuery.queryType) { case 'SELECT': case 'INSERT': case 'UPDATE': case 'DELETE': result = await this.queryExecutor.executeQuery(savedQuery.connectionId, savedQuery.query, parameters, options); break; case 'PROCEDURE': const procName = this.extractProcedureName(savedQuery.query); result = await this.queryExecutor.executeProcedure(savedQuery.connectionId, procName, parameters, options); break; case 'FUNCTION': const funcName = this.extractFunctionName(savedQuery.query); result = await this.queryExecutor.executeFunction(savedQuery.connectionId, funcName, parameters, options); break; case 'VIEW': const viewName = this.extractViewName(savedQuery.query); result = await this.queryExecutor.getViewData(savedQuery.connectionId, viewName, options.maxRows || 100, options); break; default: result = await this.queryExecutor.executeQuery(savedQuery.connectionId, savedQuery.query, parameters, options); } // Update execution statistics await savedQuery.increment('executionCount'); await savedQuery.update({ lastExecutedAt: new Date() }); ctx.body = { success: true, data: result, meta: { executedAt: new Date().toISOString(), savedQueryId: id, savedQueryName: savedQuery.name, queryType: savedQuery.queryType, }, }; this.logQueryExecution(ctx, savedQuery.connectionId, savedQuery.query, result.executionTime, true); } catch (error) { if (error instanceof Error && error.message.includes('not found')) { throw error; } ctx.throw(500, `Failed to execute saved query: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getQueryStatistics(ctx) { try { const stats = this.queryExecutor.getStatistics(); ctx.body = { success: true, data: stats, }; } catch (error) { ctx.throw(500, `Failed to get query statistics: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async clearQueryCache(ctx) { try { this.queryExecutor.clearCache(); ctx.body = { success: true, message: 'Query cache cleared successfully', }; } catch (error) { ctx.throw(500, `Failed to clear query cache: ${error instanceof Error ? error.message : 'Unknown error'}`); } } // Helper methods async validateConnection(connectionId) { const { Connection } = require('../models/Connection'); const connection = await Connection.findOne({ where: { id: connectionId, isActive: true }, }); if (!connection) { throw new Error('Connection not found or inactive'); } } logQueryExecution(ctx, connectionId, query, executionTime, success, error) { const logEntry = { timestamp: new Date().toISOString(), userId: ctx.state.currentUser?.id, connectionId, query: query.substring(0, 1000), executionTime, success, error: error ? error.message : null, userAgent: ctx.get('User-Agent'), ip: ctx.ip, }; console.log('Query Execution Log:', logEntry); } extractProcedureName(query) { const match = query.match(/CALL\s+([a-zA-Z_][a-zA-Z0-9_]*)/i); return match ? match[1] : ''; } extractFunctionName(query) { const match = query.match(/SELECT\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/i); return match ? match[1] : ''; } extractViewName(query) { const match = query.match(/FROM\s+([a-zA-Z_][a-zA-Z0-9_]*)/i); return match ? match[1] : ''; } } exports.QueryController = QueryController; exports.default = QueryController; //# sourceMappingURL=query.js.map