modified: backend/src/server.ts

This commit is contained in:
2026-03-14 16:33:30 +03:00
parent f0cbe99cb0
commit 4ae281209f
4 changed files with 312 additions and 0 deletions

View File

@@ -0,0 +1,296 @@
import { Response } from 'express';
import { AuthRequest } from '../middleware/auth';
import { mainPool } from '../config/database';
/**
* GET /api/sync/pull
* Returns all endpoints + folders + database name mappings for CLI sync
*/
export const syncPull = async (req: AuthRequest, res: Response) => {
try {
// Get all endpoints with folder and database names
const endpointsResult = await mainPool.query(`
SELECT
e.*,
f.name as folder_name,
d.name as database_name,
d.type as database_type
FROM endpoints e
LEFT JOIN folders f ON e.folder_id = f.id
LEFT JOIN databases d ON e.database_id = d.id
ORDER BY e.created_at ASC
`);
// Get all folders with parent info
const foldersResult = await mainPool.query(`
SELECT f.*, pf.name as parent_name
FROM folders f
LEFT JOIN folders pf ON f.parent_id = pf.id
ORDER BY f.created_at ASC
`);
// Get database name->id mapping (active only)
const databasesResult = await mainPool.query(
'SELECT id, name, type FROM databases WHERE is_active = true ORDER BY name'
);
// Resolve script_queries database names
const endpoints = await Promise.all(endpointsResult.rows.map(async (ep: any) => {
const scriptQueries = ep.script_queries || [];
const resolvedScriptQueries = [];
for (const sq of scriptQueries) {
let sqDbName: string | null = null;
let sqDbType: string | null = null;
if (sq.database_id) {
const sqDb = databasesResult.rows.find((d: any) => d.id === sq.database_id);
if (sqDb) {
sqDbName = sqDb.name;
sqDbType = sqDb.type;
}
}
resolvedScriptQueries.push({
...sq,
database_name: sqDbName,
database_type: sqDbType,
});
}
return {
id: ep.id,
name: ep.name,
description: ep.description,
method: ep.method,
path: ep.path,
execution_type: ep.execution_type || 'sql',
database_name: ep.database_name,
database_type: ep.database_type,
database_id: ep.database_id,
sql_query: ep.sql_query,
parameters: ep.parameters || [],
script_language: ep.script_language,
script_code: ep.script_code,
script_queries: resolvedScriptQueries,
aql_method: ep.aql_method,
aql_endpoint: ep.aql_endpoint,
aql_body: ep.aql_body,
aql_query_params: ep.aql_query_params,
is_public: ep.is_public,
enable_logging: ep.enable_logging,
detailed_response: ep.detailed_response,
response_schema: ep.response_schema,
folder_id: ep.folder_id,
folder_name: ep.folder_name,
created_at: ep.created_at,
updated_at: ep.updated_at,
};
}));
const folders = foldersResult.rows.map((f: any) => ({
id: f.id,
name: f.name,
parent_id: f.parent_id,
parent_name: f.parent_name,
created_at: f.created_at,
updated_at: f.updated_at,
}));
res.json({
endpoints,
folders,
databases: databasesResult.rows,
server_time: new Date().toISOString(),
});
} catch (error) {
console.error('Sync pull error:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
/**
* POST /api/sync/status
* Accepts {endpoints: [{id, updated_at}], folders: [{id, updated_at}]}
* Returns which items changed on server since the given timestamps
*/
export const syncStatus = async (req: AuthRequest, res: Response) => {
try {
const { endpoints: clientEndpoints, folders: clientFolders } = req.body;
// Get current server state
const serverEndpoints = await mainPool.query(
'SELECT id, name, updated_at FROM endpoints'
);
const serverFolders = await mainPool.query(
'SELECT id, name, updated_at FROM folders'
);
const clientEndpointMap = new Map(
(clientEndpoints || []).map((e: any) => [e.id, e.updated_at])
);
const clientFolderMap = new Map(
(clientFolders || []).map((f: any) => [f.id, f.updated_at])
);
// Find changed/new/deleted endpoints
const changedEndpoints: any[] = [];
const newEndpoints: any[] = [];
for (const ep of serverEndpoints.rows) {
const clientUpdatedAt = clientEndpointMap.get(ep.id);
if (!clientUpdatedAt) {
newEndpoints.push({ id: ep.id, name: ep.name });
} else if (new Date(ep.updated_at).getTime() > new Date(clientUpdatedAt).getTime()) {
changedEndpoints.push({ id: ep.id, name: ep.name, server_updated_at: ep.updated_at });
}
}
const serverEndpointIds = new Set(serverEndpoints.rows.map((e: any) => e.id));
const deletedEndpoints = (clientEndpoints || [])
.filter((e: any) => !serverEndpointIds.has(e.id))
.map((e: any) => ({ id: e.id }));
// Find changed/new/deleted folders
const changedFolders: any[] = [];
const newFolders: any[] = [];
for (const f of serverFolders.rows) {
const clientUpdatedAt = clientFolderMap.get(f.id);
if (!clientUpdatedAt) {
newFolders.push({ id: f.id, name: f.name });
} else if (new Date(f.updated_at).getTime() > new Date(clientUpdatedAt).getTime()) {
changedFolders.push({ id: f.id, name: f.name, server_updated_at: f.updated_at });
}
}
const serverFolderIds = new Set(serverFolders.rows.map((f: any) => f.id));
const deletedFolders = (clientFolders || [])
.filter((f: any) => !serverFolderIds.has(f.id))
.map((f: any) => ({ id: f.id }));
res.json({
endpoints: {
changed: changedEndpoints,
new: newEndpoints,
deleted: deletedEndpoints,
},
folders: {
changed: changedFolders,
new: newFolders,
deleted: deletedFolders,
},
});
} catch (error) {
console.error('Sync status error:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
/**
* POST /api/sync/push
* Accepts array of endpoints to create/update with conflict detection
*/
export const syncPush = async (req: AuthRequest, res: Response) => {
try {
const { endpoints: pushEndpoints, force } = req.body;
if (!pushEndpoints || !Array.isArray(pushEndpoints)) {
return res.status(400).json({ error: 'endpoints array is required' });
}
const conflicts: any[] = [];
const results: any[] = [];
for (const ep of pushEndpoints) {
// Check if endpoint exists
if (ep.id) {
const existing = await mainPool.query(
'SELECT id, updated_at, name FROM endpoints WHERE id = $1',
[ep.id]
);
if (existing.rows.length > 0) {
const serverUpdatedAt = new Date(existing.rows[0].updated_at).getTime();
const clientBaseUpdatedAt = ep._base_updated_at
? new Date(ep._base_updated_at).getTime()
: 0;
// Conflict: server was modified after client's last sync
if (!force && clientBaseUpdatedAt && serverUpdatedAt > clientBaseUpdatedAt) {
conflicts.push({
id: ep.id,
name: existing.rows[0].name,
server_updated_at: existing.rows[0].updated_at,
client_base_updated_at: ep._base_updated_at,
});
continue;
}
// Update existing endpoint
const result = await mainPool.query(
`UPDATE endpoints
SET name = $1, description = $2, method = $3, path = $4,
database_id = $5, sql_query = $6, parameters = $7,
folder_id = $8, is_public = $9, enable_logging = $10,
execution_type = $11, script_language = $12, script_code = $13,
script_queries = $14, aql_method = $15, aql_endpoint = $16,
aql_body = $17, aql_query_params = $18, detailed_response = $19,
response_schema = $20, updated_at = CURRENT_TIMESTAMP
WHERE id = $21
RETURNING *`,
[
ep.name, ep.description || '', ep.method, ep.path,
ep.database_id || null, ep.sql_query || '', JSON.stringify(ep.parameters || []),
ep.folder_id || null, ep.is_public || false, ep.enable_logging || false,
ep.execution_type || 'sql', ep.script_language || null, ep.script_code || null,
JSON.stringify(ep.script_queries || []), ep.aql_method || null,
ep.aql_endpoint || null, ep.aql_body || null,
JSON.stringify(ep.aql_query_params || {}), ep.detailed_response || false,
ep.response_schema ? JSON.stringify(ep.response_schema) : null,
ep.id,
]
);
results.push({ action: 'updated', endpoint: result.rows[0] });
} else {
// Endpoint with this ID doesn't exist on server — create it
const result = await mainPool.query(
`INSERT INTO endpoints (
id, name, description, method, path, database_id, sql_query, parameters,
folder_id, user_id, is_public, enable_logging,
execution_type, script_language, script_code, script_queries,
aql_method, aql_endpoint, aql_body, aql_query_params, detailed_response, response_schema
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22)
RETURNING *`,
[
ep.id, ep.name, ep.description || '', ep.method, ep.path,
ep.database_id || null, ep.sql_query || '', JSON.stringify(ep.parameters || []),
ep.folder_id || null, req.user!.id, ep.is_public || false, ep.enable_logging || false,
ep.execution_type || 'sql', ep.script_language || null, ep.script_code || null,
JSON.stringify(ep.script_queries || []), ep.aql_method || null,
ep.aql_endpoint || null, ep.aql_body || null,
JSON.stringify(ep.aql_query_params || {}), ep.detailed_response || false,
ep.response_schema ? JSON.stringify(ep.response_schema) : null,
]
);
results.push({ action: 'created', endpoint: result.rows[0] });
}
}
}
if (conflicts.length > 0) {
return res.status(409).json({
error: 'Conflicts detected',
conflicts,
applied: results,
});
}
res.json({
success: true,
results,
});
} catch (error: any) {
console.error('Sync push error:', error);
if (error.code === '23505') {
return res.status(400).json({ error: 'Endpoint path already exists' });
}
res.status(500).json({ error: 'Internal server error' });
}
};

View File

@@ -0,0 +1,13 @@
import express from 'express';
import { authMiddleware } from '../middleware/auth';
import { syncPull, syncPush, syncStatus } from '../controllers/syncController';
const router = express.Router();
router.use(authMiddleware);
router.get('/pull', syncPull);
router.post('/status', syncStatus);
router.post('/push', syncPush);
export default router;

View File

@@ -20,6 +20,7 @@ import userRoutes from './routes/users';
import logsRoutes from './routes/logs';
import sqlInterfaceRoutes from './routes/sqlInterface';
import dynamicRoutes from './routes/dynamic';
import syncRoutes from './routes/sync';
const app: Express = express();
@@ -93,6 +94,7 @@ app.use('/api/db-management', databaseManagementRoutes);
app.use('/api/users', userRoutes);
app.use('/api/logs', logsRoutes);
app.use('/api/workbench', sqlInterfaceRoutes);
app.use('/api/sync', syncRoutes);
// Dynamic API routes (user-created endpoints)
app.use('/api/v1', dynamicRoutes);