plugin-postgresql-connector
Version:
NocoBase plugin for connecting to external PostgreSQL databases
336 lines (304 loc) • 9.42 kB
text/typescript
import { useState, useCallback, useEffect } from 'react';
import { useRequest } from '@nocobase/client';
import { message } from 'antd';
export interface QueryResult {
rows: any[];
rowCount: number;
fields: Array<{
name: string;
dataTypeID: number;
dataTypeName?: string;
dataTypeSize?: number;
}>;
executionTime: number;
query: string;
metadata?: any;
}
export interface SavedQuery {
id: string;
connectionId: string;
name: string;
query: string;
queryType: 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'PROCEDURE' | 'FUNCTION';
description?: string;
category?: string;
parameters?: any[];
createdAt?: string;
updatedAt?: string;
}
export interface QueryExecutionOptions {
maxRows?: number;
timeout?: number;
formatQuery?: boolean;
includeMetadata?: boolean;
}
export const useQuery = (connectionId?: string) => {
const [queryResult, setQueryResult] = useState<QueryResult | null>(null);
const [savedQueries, setSavedQueries] = useState<SavedQuery[]>([]);
const [queryHistory, setQueryHistory] = useState<string[]>([]);
const [executionError, setExecutionError] = useState<string | null>(null);
// Execute SQL query
const { run: executeQuery, loading: executingQuery } = useRequest(
(data: {
connectionId: string;
query: string;
parameters?: any[];
options?: QueryExecutionOptions;
}) => ({
url: '/postgresql-query/execute',
method: 'POST',
data,
}),
{
manual: true,
onSuccess: (data: any) => {
setQueryResult(data.data);
setExecutionError(null);
addToHistory(data.data.query);
message.success(`Query thực thi thành công! (${data.data.executionTime}ms)`);
},
onError: (error: any) => {
setExecutionError(error.message);
setQueryResult(null);
message.error(`Query thất bại: ${error.message}`);
},
}
);
// Execute stored procedure
const { run: executeProcedure, loading: executingProcedure } = useRequest(
(data: {
connectionId: string;
procedureName: string;
parameters?: any[];
}) => ({
url: '/postgresql-query/executeProcedure',
method: 'POST',
data,
}),
{
manual: true,
onSuccess: (data: any) => {
setQueryResult(data.data);
setExecutionError(null);
message.success(`Procedure thực thi thành công! (${data.data.executionTime}ms)`);
},
onError: (error: any) => {
setExecutionError(error.message);
message.error(`Procedure thất bại: ${error.message}`);
},
}
);
// Execute function
const { run: executeFunction, loading: executingFunction } = useRequest(
(data: {
connectionId: string;
functionName: string;
parameters?: any[];
}) => ({
url: '/postgresql-query/executeFunction',
method: 'POST',
data,
}),
{
manual: true,
onSuccess: (data: any) => {
setQueryResult(data.data);
setExecutionError(null);
message.success(`Function thực thi thành công! (${data.data.executionTime}ms)`);
},
onError: (error: any) => {
setExecutionError(error.message);
message.error(`Function thất bại: ${error.message}`);
},
}
);
// Get table data
const { run: getTableData, loading: loadingTableData } = useRequest(
(data: {
connectionId: string;
tableName: string;
limit?: number;
}) => ({
url: '/postgresql-query/getTableData',
method: 'GET',
params: data,
}),
{
manual: true,
onSuccess: (data: any) => {
setQueryResult(data.data);
setExecutionError(null);
},
onError: (error: any) => {
setExecutionError(error.message);
message.error(`Không thể tải dữ liệu table: ${error.message}`);
},
}
);
// Save query
const { run: saveQuery, loading: savingQuery } = useRequest(
(data: Omit<SavedQuery, 'id' | 'createdAt' | 'updatedAt'>) => ({
url: '/postgresql-saved-queries',
method: 'POST',
data,
}),
{
manual: true,
onSuccess: () => {
message.success('Query đã được lưu thành công!');
if (connectionId) {
fetchSavedQueries(connectionId);
}
},
onError: (error: any) => {
message.error(`Lưu query thất bại: ${error.message}`);
},
}
);
// Fetch saved queries
const { run: fetchSavedQueries, loading: loadingSavedQueries } = useRequest(
(connId: string) => ({
url: '/postgresql-saved-queries',
params: { connectionId: connId },
}),
{
manual: true,
onSuccess: (data: any) => {
setSavedQueries(data.data || []);
},
onError: (error: any) => {
message.error(`Không thể tải saved queries: ${error.message}`);
},
}
);
// Delete saved query
const { run: deleteSavedQuery, loading: deletingQuery } = useRequest(
(queryId: string) => ({
url: `/postgresql-saved-queries/${queryId}`,
method: 'DELETE',
}),
{
manual: true,
onSuccess: () => {
message.success('Query đã được xóa!');
if (connectionId) {
fetchSavedQueries(connectionId);
}
},
onError: (error: any) => {
message.error(`Xóa query thất bại: ${error.message}`);
},
}
);
// Utility functions
const addToHistory = useCallback((query: string) => {
setQueryHistory(prev => {
const newHistory = [query, ...prev.filter(q => q !== query)];
return newHistory.slice(0, 50); // Keep last 50 queries
});
}, []);
const clearHistory = useCallback(() => {
setQueryHistory([]);
}, []);
const detectQueryType = useCallback((query: string): SavedQuery['queryType'] => {
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.includes('CALL ')) return 'PROCEDURE';
if (upperQuery.includes('SELECT ') && upperQuery.includes('(')) return 'FUNCTION';
return 'SELECT';
}, []);
const formatQuery = useCallback((query: string): string => {
// Basic SQL formatting
return query
.replace(/\s+/g, ' ')
.replace(/,\s*/g, ',\n ')
.replace(/\bFROM\b/gi, '\nFROM')
.replace(/\bWHERE\b/gi, '\nWHERE')
.replace(/\bJOIN\b/gi, '\nJOIN')
.replace(/\bORDER BY\b/gi, '\nORDER BY')
.replace(/\bGROUP BY\b/gi, '\nGROUP BY')
.replace(/\bHAVING\b/gi, '\nHAVING')
.trim();
}, []);
const validateQuery = useCallback((query: string): string[] => {
const errors: string[] = [];
if (!query.trim()) {
errors.push('Query không được để trống');
return errors;
}
// Check for dangerous queries
const upperQuery = query.toUpperCase();
const dangerousKeywords = ['DROP', 'TRUNCATE', 'ALTER', 'CREATE'];
dangerousKeywords.forEach(keyword => {
if (upperQuery.includes(keyword)) {
errors.push(`Query chứa từ khóa nguy hiểm: ${keyword}`);
}
});
// Check for basic SQL syntax
if (!upperQuery.match(/\b(SELECT|INSERT|UPDATE|DELETE|CALL)\b/)) {
errors.push('Query không hợp lệ - thiếu keyword chính');
}
return errors;
}, []);
const exportQueryResult = useCallback((format: 'csv' | 'json' | 'excel') => {
if (!queryResult?.rows?.length) {
message.warning('Không có dữ liệu để export');
return;
}
// This would integrate with export utilities
message.info(`Export ${format.toUpperCase()} feature coming soon`);
}, [queryResult]);
const getQueryStats = useCallback(() => {
if (!queryResult) return null;
return {
rowCount: queryResult.rowCount,
columnCount: queryResult.fields?.length || 0,
executionTime: queryResult.executionTime,
dataSize: JSON.stringify(queryResult.rows).length,
};
}, [queryResult]);
// Auto-fetch saved queries when connectionId changes
useEffect(() => {
if (connectionId) {
fetchSavedQueries(connectionId);
}
}, [connectionId, fetchSavedQueries]);
return {
// State
queryResult,
savedQueries,
queryHistory,
executionError,
// Loading states
executingQuery,
executingProcedure,
executingFunction,
loadingTableData,
savingQuery,
loadingSavedQueries,
deletingQuery,
// Actions
executeQuery,
executeProcedure,
executeFunction,
getTableData,
saveQuery,
fetchSavedQueries,
deleteSavedQuery,
// Utilities
addToHistory,
clearHistory,
detectQueryType,
formatQuery,
validateQuery,
exportQueryResult,
getQueryStats,
// State setters
setQueryResult,
setExecutionError,
};
};
export default useQuery;