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