modified: backend/src/server.ts
This commit is contained in:
296
backend/src/controllers/syncController.ts
Normal file
296
backend/src/controllers/syncController.ts
Normal 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' });
|
||||
}
|
||||
};
|
||||
13
backend/src/routes/sync.ts
Normal file
13
backend/src/routes/sync.ts
Normal 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;
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user