modified: backend/src/controllers/databaseManagementController.ts
modified: backend/src/controllers/schemaController.ts modified: frontend/src/pages/DatabaseSchema.tsx
This commit is contained in:
@@ -2,6 +2,7 @@ import { Response } from 'express';
|
|||||||
import { AuthRequest } from '../middleware/auth';
|
import { AuthRequest } from '../middleware/auth';
|
||||||
import { mainPool } from '../config/database';
|
import { mainPool } from '../config/database';
|
||||||
import { databasePoolManager } from '../services/DatabasePoolManager';
|
import { databasePoolManager } from '../services/DatabasePoolManager';
|
||||||
|
import { generateSchemaForDatabase } from './schemaController';
|
||||||
|
|
||||||
// Только админы могут управлять базами данных
|
// Только админы могут управлять базами данных
|
||||||
export const getDatabases = async (req: AuthRequest, res: Response) => {
|
export const getDatabases = async (req: AuthRequest, res: Response) => {
|
||||||
@@ -92,6 +93,13 @@ export const createDatabase = async (req: AuthRequest, res: Response) => {
|
|||||||
// Добавить пул подключений (только для не-AQL баз)
|
// Добавить пул подключений (только для не-AQL баз)
|
||||||
if (dbType !== 'aql') {
|
if (dbType !== 'aql') {
|
||||||
await databasePoolManager.reloadPool(newDb.id);
|
await databasePoolManager.reloadPool(newDb.id);
|
||||||
|
|
||||||
|
// Generate schema in background for PostgreSQL databases
|
||||||
|
if (dbType === 'postgresql') {
|
||||||
|
generateSchemaForDatabase(newDb.id).catch(err => {
|
||||||
|
console.error('Background schema generation failed:', err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Не возвращаем пароль
|
// Не возвращаем пароль
|
||||||
|
|||||||
@@ -33,11 +33,17 @@ interface SchemaData {
|
|||||||
|
|
||||||
// Parse PostgreSQL schema
|
// Parse PostgreSQL schema
|
||||||
async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
console.log(`[Schema] Starting schema parse for database ${databaseId}`);
|
||||||
|
|
||||||
const pool = databasePoolManager.getPool(databaseId);
|
const pool = databasePoolManager.getPool(databaseId);
|
||||||
if (!pool) {
|
if (!pool) {
|
||||||
throw new Error('Database not found or not active');
|
throw new Error('Database not found or not active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[Schema] Fetching tables list...`);
|
||||||
|
const tablesStartTime = Date.now();
|
||||||
|
|
||||||
// Get all tables with comments via pg_catalog
|
// Get all tables with comments via pg_catalog
|
||||||
const tablesResult = await pool.query(`
|
const tablesResult = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -52,9 +58,17 @@ async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
|||||||
ORDER BY t.table_schema, t.table_name
|
ORDER BY t.table_schema, t.table_name
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
console.log(`[Schema] Found ${tablesResult.rows.length} tables in ${Date.now() - tablesStartTime}ms`);
|
||||||
|
|
||||||
const tables: TableInfo[] = [];
|
const tables: TableInfo[] = [];
|
||||||
|
let processedCount = 0;
|
||||||
|
|
||||||
for (const table of tablesResult.rows) {
|
for (const table of tablesResult.rows) {
|
||||||
|
processedCount++;
|
||||||
|
if (processedCount % 50 === 0 || processedCount === tablesResult.rows.length) {
|
||||||
|
console.log(`[Schema] Processing table ${processedCount}/${tablesResult.rows.length}: ${table.table_schema}.${table.table_name}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Get columns for each table
|
// Get columns for each table
|
||||||
const columnsResult = await pool.query(`
|
const columnsResult = await pool.query(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -121,6 +135,9 @@ async function parsePostgresSchema(databaseId: string): Promise<SchemaData> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalTime = Date.now() - startTime;
|
||||||
|
console.log(`[Schema] Completed! ${tables.length} tables, ${tables.reduce((acc, t) => acc + t.columns.length, 0)} columns, ${tables.reduce((acc, t) => acc + t.foreign_keys.length, 0)} FKs in ${totalTime}ms`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tables,
|
tables,
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
@@ -188,3 +205,15 @@ export const refreshSchema = async (req: Request, res: Response) => {
|
|||||||
res.status(500).json({ success: false, error: error.message });
|
res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Generate schema for a database (called from other controllers)
|
||||||
|
export const generateSchemaForDatabase = async (databaseId: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const schema = await parsePostgresSchema(databaseId);
|
||||||
|
await saveSchemaToCache(databaseId, schema);
|
||||||
|
console.log(`Schema generated for database ${databaseId}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`Error generating schema for database ${databaseId}:`, error.message);
|
||||||
|
// Don't throw - schema generation is not critical
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import Dagre from '@dagrejs/dagre';
|
import Dagre from '@dagrejs/dagre';
|
||||||
import { Database as DatabaseIcon, RefreshCw, Loader2, Key, Link } from 'lucide-react';
|
import { Database as DatabaseIcon, Loader2, Key, Link, RefreshCw } from 'lucide-react';
|
||||||
import { databasesApi, schemaApi, TableInfo, SchemaData } from '@/services/api';
|
import { databasesApi, schemaApi, TableInfo, SchemaData } from '@/services/api';
|
||||||
import { Database } from '@/types';
|
import { Database } from '@/types';
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ function TableNode({ data }: { data: TableInfo & Record<string, unknown> }) {
|
|||||||
{data.columns.map((col) => (
|
{data.columns.map((col) => (
|
||||||
<div
|
<div
|
||||||
key={col.name}
|
key={col.name}
|
||||||
className="px-3 py-1.5 text-xs flex items-center gap-2 hover:bg-gray-50 cursor-default group relative"
|
className="px-3 py-1.5 text-xs flex items-center gap-2 hover:bg-gray-50 cursor-default"
|
||||||
title={col.comment || undefined}
|
title={col.comment || undefined}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
@@ -54,15 +54,6 @@ function TableNode({ data }: { data: TableInfo & Record<string, unknown> }) {
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-gray-400 truncate flex-1 text-right">{col.type}</span>
|
<span className="text-gray-400 truncate flex-1 text-right">{col.type}</span>
|
||||||
{!col.nullable && <span className="text-red-400 text-[10px] flex-shrink-0">NN</span>}
|
{!col.nullable && <span className="text-red-400 text-[10px] flex-shrink-0">NN</span>}
|
||||||
|
|
||||||
{/* Tooltip for column comment */}
|
|
||||||
{col.comment && (
|
|
||||||
<div className="absolute left-full ml-2 top-0 z-50 hidden group-hover:block">
|
|
||||||
<div className="bg-gray-900 text-white text-xs rounded px-2 py-1 max-w-64 whitespace-normal shadow-lg">
|
|
||||||
{col.comment}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{data.columns.length === 0 && (
|
{data.columns.length === 0 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user