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:
@@ -3,7 +3,7 @@ import { ApiKeyRequest } from '../middleware/apiKey';
|
||||
import { mainPool } from '../config/database';
|
||||
import { sqlExecutor } from '../services/SqlExecutor';
|
||||
import { scriptExecutor } from '../services/ScriptExecutor';
|
||||
import { EndpointParameter, ScriptQuery, ScriptExecutionError } from '../types';
|
||||
import { EndpointParameter, ScriptQuery, ScriptExecutionError, Environment } from '../types';
|
||||
|
||||
export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response) => {
|
||||
const startTime = Date.now();
|
||||
@@ -11,6 +11,9 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
|
||||
let endpointId: string | null = null;
|
||||
|
||||
try {
|
||||
const environment: Environment =
|
||||
(req.headers['x-environment'] as string)?.toLowerCase() === 'test' ? 'test' : 'prod';
|
||||
|
||||
// Extract the path from the request (remove /api/v1 prefix)
|
||||
const requestPath = req.path; // This already has the path without /api/v1
|
||||
const requestMethod = req.method.toUpperCase();
|
||||
@@ -29,9 +32,21 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
|
||||
});
|
||||
}
|
||||
|
||||
const endpoint = endpointResult.rows[0];
|
||||
let endpoint = endpointResult.rows[0];
|
||||
endpointId = endpoint.id;
|
||||
|
||||
// If test environment requested and a draft exists, serve draft content
|
||||
if (environment === 'test' && endpoint.draft_version_id) {
|
||||
const draftResult = await mainPool.query(
|
||||
`SELECT * FROM endpoint_versions WHERE id = $1 AND status = 'draft'`,
|
||||
[endpoint.draft_version_id]
|
||||
);
|
||||
if (draftResult.rows.length > 0) {
|
||||
const draft = draftResult.rows[0];
|
||||
endpoint = { ...endpoint, ...draft, id: endpoint.id, folder_id: endpoint.folder_id, user_id: endpoint.user_id };
|
||||
}
|
||||
}
|
||||
|
||||
// Check if logging is enabled (on endpoint OR on API key, but log only once)
|
||||
const endpointLogging = endpoint.enable_logging || false;
|
||||
const apiKeyLogging = req.apiKey?.enable_logging || false;
|
||||
@@ -171,7 +186,7 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
|
||||
body: aqlBody,
|
||||
queryParams: aqlQueryParams,
|
||||
parameters: requestParams,
|
||||
});
|
||||
}, environment);
|
||||
} else if (executionType === 'script') {
|
||||
// Execute script
|
||||
const scriptLanguage = endpoint.script_language;
|
||||
@@ -199,6 +214,7 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
|
||||
scriptQueries,
|
||||
requestParams,
|
||||
endpointParameters: parameters,
|
||||
environment,
|
||||
});
|
||||
|
||||
result = {
|
||||
@@ -231,7 +247,8 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
|
||||
result = await sqlExecutor.executeQuery(
|
||||
endpoint.database_id,
|
||||
processedQuery,
|
||||
queryParams
|
||||
queryParams,
|
||||
environment
|
||||
);
|
||||
}
|
||||
|
||||
@@ -264,6 +281,7 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
|
||||
error_message: null,
|
||||
ip_address: req.ip || req.socket.remoteAddress || 'unknown',
|
||||
user_agent: req.headers['user-agent'] || 'unknown',
|
||||
environment,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,6 +313,7 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
|
||||
error_message: error.message,
|
||||
ip_address: req.ip || req.socket.remoteAddress || 'unknown',
|
||||
user_agent: req.headers['user-agent'] || 'unknown',
|
||||
environment: (req.headers['x-environment'] as string)?.toLowerCase() === 'test' ? 'test' : 'prod',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -315,6 +334,7 @@ async function logRequest(data: {
|
||||
error_message: string | null;
|
||||
ip_address: string;
|
||||
user_agent: string;
|
||||
environment?: string;
|
||||
}) {
|
||||
try {
|
||||
await mainPool.query(
|
||||
@@ -322,8 +342,8 @@ async function logRequest(data: {
|
||||
endpoint_id, api_key_id, method, path,
|
||||
request_params, request_body, response_status,
|
||||
response_data, execution_time, error_message,
|
||||
ip_address, user_agent
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
|
||||
ip_address, user_agent, environment
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
|
||||
[
|
||||
data.endpoint_id,
|
||||
data.api_key_id,
|
||||
@@ -337,6 +357,7 @@ async function logRequest(data: {
|
||||
data.error_message,
|
||||
data.ip_address,
|
||||
data.user_agent,
|
||||
data.environment || 'prod',
|
||||
]
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user