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>
This commit is contained in:
2026-05-23 21:04:11 +03:00
parent c918f34595
commit 801d0cce5f
19 changed files with 1263 additions and 223 deletions

View File

@@ -2,8 +2,9 @@ import { Response } from 'express';
import { AuthRequest } from '../middleware/auth';
import { mainPool } from '../config/database';
import { v4 as uuidv4 } from 'uuid';
import { ExportedEndpoint, ExportedScriptQuery, ScriptExecutionError } from '../types';
import { ExportedEndpoint, ExportedScriptQuery, ScriptExecutionError, Environment } from '../types';
import { encryptEndpointData, decryptEndpointData } from '../services/endpointCrypto';
import { versionService } from '../services/VersionService';
export const getEndpoints = async (req: AuthRequest, res: Response) => {
try {
@@ -242,6 +243,15 @@ export const updateEndpoint = async (req: AuthRequest, res: Response) => {
return res.status(404).json({ error: 'Endpoint not found' });
}
// Auto-create published version
try {
await versionService.createVersionFromEndpoint(
id, req.user!.id, req.body.change_message || 'Updated', 'published'
);
} catch (vErr) {
console.error('Auto-version creation failed:', vErr);
}
res.json(result.rows[0]);
} catch (error: any) {
console.error('Update endpoint error:', error);
@@ -287,8 +297,10 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => {
aql_endpoint,
aql_body,
aql_query_params,
environment: reqEnv,
} = req.body;
const environment: Environment = reqEnv === 'prod' ? 'prod' : 'test';
const execType = execution_type || 'sql';
if (execType === 'sql') {
@@ -314,7 +326,7 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => {
}
const { sqlExecutor } = require('../services/SqlExecutor');
const result = await sqlExecutor.executeQuery(database_id, processedQuery, parameters || []);
const result = await sqlExecutor.executeQuery(database_id, processedQuery, parameters || [], environment);
res.json({
success: true,
@@ -346,6 +358,7 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => {
scriptQueries: script_queries || [],
requestParams,
endpointParameters: endpoint_parameters || [],
environment,
});
res.json({
@@ -377,7 +390,7 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => {
body: aql_body,
queryParams: aql_query_params,
parameters: requestParams,
});
}, environment);
res.json({
success: true,
@@ -744,3 +757,73 @@ export const importEndpoint = async (req: AuthRequest, res: Response) => {
res.status(500).json({ error: 'Internal server error' });
}
};
// Version management handlers
export const getVersionHistory = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const versions = await versionService.getVersionHistory(id);
res.json(versions);
} catch (error) {
console.error('Get version history error:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const getVersion = async (req: AuthRequest, res: Response) => {
try {
const { versionId } = req.params;
const version = await versionService.getVersion(versionId);
if (!version) return res.status(404).json({ error: 'Version not found' });
res.json(version);
} catch (error) {
console.error('Get version error:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const publishVersion = async (req: AuthRequest, res: Response) => {
try {
const { versionId } = req.params;
await versionService.publishVersion(versionId, req.user!.id);
res.json({ message: 'Version published' });
} catch (error: any) {
console.error('Publish version error:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
};
export const rollbackVersion = async (req: AuthRequest, res: Response) => {
try {
const { id, versionId } = req.params;
const newVersion = await versionService.rollbackToVersion(id, versionId, req.user!.id);
res.json(newVersion);
} catch (error: any) {
console.error('Rollback version error:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
};
export const saveDraftVersion = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const draft = await versionService.saveDraft(id, req.body, req.user!.id, req.body.change_message);
res.json(draft);
} catch (error: any) {
console.error('Save draft error:', error);
res.status(500).json({ error: error.message || 'Internal server error' });
}
};
export const getDraftVersion = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const draft = await versionService.getDraft(id);
if (!draft) return res.status(404).json({ error: 'No draft found' });
res.json(draft);
} catch (error) {
console.error('Get draft error:', error);
res.status(500).json({ error: 'Internal server error' });
}
};