Files
api_builder/backend/src/controllers/databaseManagementController.ts
eshmeshek 801d0cce5f Add test/prod environments and endpoint versioning
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>
2026-05-23 21:04:11 +03:00

288 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: 'Ошибка тестирования подключения' });
}
};