Phase 1: Test/Prod Database Configurations - Migration 010: test_* columns on databases table, environment column on request_logs - DatabasePoolManager: dual-pool strategy (prod + test), getPool(id, env) with fallback - SqlExecutor, ScriptExecutor, IsolatedScriptExecutor, AqlExecutor: environment param threaded through all execution paths - dynamicApiController: X-Environment header detection, environment in logging - databaseManagementController: CRUD for test credentials, testConnection with ?env=test - Frontend: test env form in DatabaseModal, env toggle in EndpointEditor test panel Phase 2: Endpoint Versioning - Migration 011: endpoint_versions table with full snapshots, backfill v1 for existing endpoints - VersionService: createVersion, saveDraft, publishVersion, rollbackToVersion, getHistory - endpointController: auto-versioning on update, 6 new version management handlers - dynamicApiController: draft serving when environment=test and draft exists - Frontend: Save Draft button, version history panel with publish/rollback actions Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
288 lines
10 KiB
TypeScript
288 lines
10 KiB
TypeScript
import { Response } from 'express';
|
||
import { AuthRequest } from '../middleware/auth';
|
||
import { mainPool } from '../config/database';
|
||
import { databasePoolManager } from '../services/DatabasePoolManager';
|
||
import { generateSchemaForDatabase } from './schemaController';
|
||
import { Environment } from '../types';
|
||
|
||
export const getDatabases = async (req: AuthRequest, res: Response) => {
|
||
try {
|
||
const result = await mainPool.query(
|
||
`SELECT id, name, type, host, port, database_name, username, ssl, is_active,
|
||
aql_base_url, aql_auth_type, aql_auth_value, aql_headers,
|
||
has_test_env, test_host, test_port, test_database_name, test_username, test_ssl,
|
||
test_aql_base_url, test_aql_headers,
|
||
created_at, updated_at
|
||
FROM databases ORDER BY name`
|
||
);
|
||
|
||
res.json(result.rows);
|
||
} catch (error) {
|
||
console.error('Get databases error:', error);
|
||
res.status(500).json({ error: 'Ошибка получения списка баз данных' });
|
||
}
|
||
};
|
||
|
||
export const getDatabase = async (req: AuthRequest, res: Response) => {
|
||
try {
|
||
const { id } = req.params;
|
||
|
||
const result = await mainPool.query(
|
||
`SELECT id, name, type, host, port, database_name, username, ssl, is_active,
|
||
aql_base_url, aql_auth_type, aql_auth_value, aql_headers,
|
||
has_test_env, test_host, test_port, test_database_name, test_username, test_ssl,
|
||
test_aql_base_url, test_aql_headers,
|
||
created_at, updated_at
|
||
FROM databases WHERE id = $1`,
|
||
[id]
|
||
);
|
||
|
||
if (result.rows.length === 0) {
|
||
return res.status(404).json({ error: 'База данных не найдена' });
|
||
}
|
||
|
||
res.json(result.rows[0]);
|
||
} catch (error) {
|
||
console.error('Get database error:', error);
|
||
res.status(500).json({ error: 'Ошибка получения базы данных' });
|
||
}
|
||
};
|
||
|
||
export const createDatabase = async (req: AuthRequest, res: Response) => {
|
||
try {
|
||
const {
|
||
name, type, host, port, database_name, username, password, ssl,
|
||
aql_base_url, aql_auth_type, aql_auth_value, aql_headers,
|
||
has_test_env, test_host, test_port, test_database_name, test_username, test_password, test_ssl,
|
||
test_aql_base_url, test_aql_auth_value, test_aql_headers
|
||
} = req.body;
|
||
|
||
const dbType = type || 'postgresql';
|
||
|
||
if (dbType !== 'aql') {
|
||
if (!name || !host || !port || !database_name || !username || !password) {
|
||
return res.status(400).json({ error: 'Не заполнены обязательные поля' });
|
||
}
|
||
} else {
|
||
if (!name || !aql_base_url) {
|
||
return res.status(400).json({ error: 'Не заполнены обязательные поля для AQL базы' });
|
||
}
|
||
}
|
||
|
||
const result = await mainPool.query(
|
||
`INSERT INTO databases (
|
||
name, type, host, port, database_name, username, password, ssl, is_active,
|
||
aql_base_url, aql_auth_type, aql_auth_value, aql_headers,
|
||
has_test_env, test_host, test_port, test_database_name, test_username, test_password, test_ssl,
|
||
test_aql_base_url, test_aql_auth_value, test_aql_headers
|
||
)
|
||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)
|
||
RETURNING *`,
|
||
[
|
||
name,
|
||
dbType,
|
||
host || '',
|
||
port || 0,
|
||
database_name || '',
|
||
username || '',
|
||
password || '',
|
||
ssl || false,
|
||
aql_base_url || null,
|
||
aql_auth_type || null,
|
||
aql_auth_value || null,
|
||
aql_headers ? JSON.stringify(aql_headers) : null,
|
||
has_test_env || false,
|
||
test_host || null,
|
||
test_port || null,
|
||
test_database_name || null,
|
||
test_username || null,
|
||
test_password || null,
|
||
test_ssl || null,
|
||
test_aql_base_url || null,
|
||
test_aql_auth_value || null,
|
||
test_aql_headers ? JSON.stringify(test_aql_headers) : null,
|
||
]
|
||
);
|
||
|
||
const newDb = result.rows[0];
|
||
|
||
if (dbType !== 'aql') {
|
||
await databasePoolManager.reloadPool(newDb.id);
|
||
|
||
if (dbType === 'postgresql') {
|
||
generateSchemaForDatabase(newDb.id).catch(err => {
|
||
console.error('Background schema generation failed:', err.message);
|
||
});
|
||
}
|
||
}
|
||
|
||
delete newDb.password;
|
||
delete newDb.aql_auth_value;
|
||
delete newDb.test_password;
|
||
delete newDb.test_aql_auth_value;
|
||
|
||
res.status(201).json(newDb);
|
||
} catch (error: any) {
|
||
console.error('Create database error:', error);
|
||
if (error.code === '23505') {
|
||
return res.status(400).json({ error: 'База данных с таким именем уже существует' });
|
||
}
|
||
res.status(500).json({ error: 'Ошибка создания базы данных' });
|
||
}
|
||
};
|
||
|
||
export const updateDatabase = async (req: AuthRequest, res: Response) => {
|
||
try {
|
||
const { id } = req.params;
|
||
const {
|
||
name, type, host, port, database_name, username, password, ssl, is_active,
|
||
aql_base_url, aql_auth_type, aql_auth_value, aql_headers,
|
||
has_test_env, test_host, test_port, test_database_name, test_username, test_password, test_ssl,
|
||
test_aql_base_url, test_aql_auth_value, test_aql_headers
|
||
} = req.body;
|
||
|
||
// Build SET clauses and params dynamically to handle optional password fields
|
||
const setClauses: string[] = [];
|
||
const params: any[] = [];
|
||
let idx = 1;
|
||
|
||
const addField = (clause: string, value: any) => {
|
||
setClauses.push(`${clause} = $${idx}`);
|
||
params.push(value);
|
||
idx++;
|
||
};
|
||
|
||
const addCoalesce = (field: string, value: any) => {
|
||
setClauses.push(`${field} = COALESCE($${idx}, ${field})`);
|
||
params.push(value);
|
||
idx++;
|
||
};
|
||
|
||
addCoalesce('name', name);
|
||
addCoalesce('type', type);
|
||
addCoalesce('host', host);
|
||
addCoalesce('port', port);
|
||
addCoalesce('database_name', database_name);
|
||
addCoalesce('username', username);
|
||
if (password) { addField('password', password); }
|
||
addCoalesce('ssl', ssl);
|
||
addCoalesce('is_active', is_active);
|
||
addCoalesce('aql_base_url', aql_base_url);
|
||
addCoalesce('aql_auth_type', aql_auth_type);
|
||
if (aql_auth_value) { addField('aql_auth_value', aql_auth_value); }
|
||
addCoalesce('aql_headers', aql_headers ? JSON.stringify(aql_headers) : null);
|
||
addCoalesce('has_test_env', has_test_env);
|
||
// Test fields: use direct SET (nullable — user may want to clear them)
|
||
addField('test_host', test_host || null);
|
||
addField('test_port', test_port || null);
|
||
addField('test_database_name', test_database_name || null);
|
||
addField('test_username', test_username || null);
|
||
if (test_password) { addField('test_password', test_password); }
|
||
addField('test_ssl', test_ssl !== undefined ? test_ssl : null);
|
||
addField('test_aql_base_url', test_aql_base_url || null);
|
||
if (test_aql_auth_value) { addField('test_aql_auth_value', test_aql_auth_value); }
|
||
addField('test_aql_headers', test_aql_headers ? JSON.stringify(test_aql_headers) : null);
|
||
|
||
setClauses.push('updated_at = CURRENT_TIMESTAMP');
|
||
|
||
params.push(id);
|
||
const whereIdx = idx;
|
||
|
||
const query = `
|
||
UPDATE databases
|
||
SET ${setClauses.join(', ')}
|
||
WHERE id = $${whereIdx}
|
||
RETURNING id, name, type, host, port, database_name, username, ssl, is_active,
|
||
aql_base_url, aql_auth_type, aql_headers,
|
||
has_test_env, test_host, test_port, test_database_name, test_username, test_ssl,
|
||
test_aql_base_url, test_aql_headers,
|
||
created_at, updated_at`;
|
||
|
||
const result = await mainPool.query(query, params);
|
||
|
||
if (result.rows.length === 0) {
|
||
return res.status(404).json({ error: 'База данных не найдена' });
|
||
}
|
||
|
||
if (result.rows[0].type !== 'aql') {
|
||
await databasePoolManager.reloadPool(id);
|
||
}
|
||
|
||
res.json(result.rows[0]);
|
||
} catch (error: any) {
|
||
console.error('Update database error:', error);
|
||
if (error.code === '23505') {
|
||
return res.status(400).json({ error: 'База данных с таким именем уже существует' });
|
||
}
|
||
res.status(500).json({ error: 'Ошибка обновления базы данных' });
|
||
}
|
||
};
|
||
|
||
export const deleteDatabase = async (req: AuthRequest, res: Response) => {
|
||
try {
|
||
const { id } = req.params;
|
||
|
||
const endpointCheck = await mainPool.query(
|
||
'SELECT COUNT(*) FROM endpoints WHERE database_id = $1',
|
||
[id]
|
||
);
|
||
|
||
if (parseInt(endpointCheck.rows[0].count) > 0) {
|
||
return res.status(400).json({
|
||
error: 'Невозможно удалить базу данных, используемую в эндпоинтах'
|
||
});
|
||
}
|
||
|
||
const result = await mainPool.query(
|
||
'DELETE FROM databases WHERE id = $1 RETURNING id',
|
||
[id]
|
||
);
|
||
|
||
if (result.rows.length === 0) {
|
||
return res.status(404).json({ error: 'База данных не найдена' });
|
||
}
|
||
|
||
databasePoolManager.removePool(id);
|
||
|
||
res.json({ message: 'База данных удалена успешно' });
|
||
} catch (error) {
|
||
console.error('Delete database error:', error);
|
||
res.status(500).json({ error: 'Ошибка удаления базы данных' });
|
||
}
|
||
};
|
||
|
||
export const testDatabaseConnection = async (req: AuthRequest, res: Response) => {
|
||
try {
|
||
const { id } = req.params;
|
||
const env = (req.query.env as string)?.toLowerCase() === 'test' ? 'test' : 'prod' as Environment;
|
||
|
||
const dbResult = await mainPool.query('SELECT type FROM databases WHERE id = $1', [id]);
|
||
|
||
if (dbResult.rows.length === 0) {
|
||
return res.status(404).json({ success: false, error: 'База данных не найдена' });
|
||
}
|
||
|
||
const dbType = dbResult.rows[0].type;
|
||
|
||
if (dbType === 'aql') {
|
||
const { aqlExecutor } = require('../services/AqlExecutor');
|
||
const result = await aqlExecutor.testConnection(id, env);
|
||
|
||
res.json({
|
||
success: result.success,
|
||
message: result.success ? 'Подключение успешно' : result.error || 'Ошибка подключения',
|
||
});
|
||
} else {
|
||
const isConnected = await databasePoolManager.testConnection(id, env);
|
||
|
||
res.json({
|
||
success: isConnected,
|
||
message: isConnected ? 'Подключение успешно' : 'Ошибка подключения',
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('Test connection error:', error);
|
||
res.status(500).json({ error: 'Ошибка тестирования подключения' });
|
||
}
|
||
};
|