Skip to content

Commit 939fd08

Browse files
feat: implement database synchronization system with multi-branch support and conflict detection
1 parent fd76ac9 commit 939fd08

9 files changed

Lines changed: 116 additions & 66 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "api-documenter",
3-
"version": "1.0.22",
3+
"version": "1.0.23",
44
"description": "Self-hosted Postman alternative with folder-level RBAC and offline-first documentation",
55
"main": "./out/main/index.js",
66
"author": "Praneeth Kulukuri",

server/routes/apis/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,18 @@ export default async function handler(req: any, res: any) {
3232
let apis: any[] = [];
3333
if (folderId) {
3434
await checkFolderAccess(context, folderId, 'read');
35-
apis = await db.query('SELECT * FROM api_collections WHERE folder_id = ? AND branch = ?', [folderId, activeBranch]);
35+
apis = await db.query('SELECT * FROM api_collections WHERE folder_id = ? AND branch = ? AND (is_deleted = 0 OR is_deleted = false OR is_deleted IS NULL)', [folderId, activeBranch]);
3636
} else {
3737
// Return all APIs user has access to
3838
const isWildcard = Array.isArray(user.allowedFolders)
3939
? user.allowedFolders.includes('*')
4040
: !!(user.allowedFolders as Record<string, any>)['*'];
4141

4242
if (user.role === 'admin' || isWildcard) {
43-
apis = await db.query('SELECT * FROM api_collections WHERE project_id = ? AND branch = ?', [user.projectId, activeBranch]);
43+
apis = await db.query('SELECT * FROM api_collections WHERE project_id = ? AND branch = ? AND (is_deleted = 0 OR is_deleted = false OR is_deleted IS NULL)', [user.projectId, activeBranch]);
4444
} else {
4545
// Resolve actual folder IDs by checking both ID and name in permissions
46-
const allFolders = await db.query('SELECT id, name FROM folders WHERE project_id = ? AND branch = ?', [user.projectId, activeBranch]);
46+
const allFolders = await db.query('SELECT id, name FROM folders WHERE project_id = ? AND branch = ? AND (is_deleted = 0 OR is_deleted = false OR is_deleted IS NULL)', [user.projectId, activeBranch]);
4747
const allowedFolderIds = allFolders.filter((f: any) => {
4848
return (user.allowedFolders as any[]).some((p: any) => {
4949
const idOrName = typeof p === 'string' ? p : p.folderId;
@@ -56,7 +56,7 @@ export default async function handler(req: any, res: any) {
5656
} else {
5757
const placeholders = allowedFolderIds.map(() => '?').join(',');
5858
apis = await db.query(
59-
`SELECT * FROM api_collections WHERE project_id = ? AND branch = ? AND folder_id IN (${placeholders})`,
59+
`SELECT * FROM api_collections WHERE project_id = ? AND branch = ? AND folder_id IN (${placeholders}) AND (is_deleted = 0 OR is_deleted = false OR is_deleted IS NULL)`,
6060
[user.projectId, activeBranch, ...allowedFolderIds]
6161
);
6262
}

server/routes/sync.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,11 @@ export default async function handler(req: any, res: any) {
6868
console.log(`[Sync] Processing folder: ${payload.name} (${operation})`);
6969

7070
// Conflict detection
71-
const existing = await db.query<any>('SELECT version FROM folders WHERE id = ? AND branch = ?', [payload.id, branchName]);
72-
if (existing.length > 0 && baseVersion > 0 && (existing[0].version || 1) > baseVersion) {
73-
results.push({ id: entry.id, status: 'conflict', dbVersion: existing[0].version, localVersion: payload.version, baseVersion });
71+
const existing = await db.query<any>('SELECT version, is_deleted FROM folders WHERE id = ? AND branch = ?', [payload.id, branchName]);
72+
if (existing.length > 0 && existing[0].is_deleted && operation === 'delete') {
73+
// Already deleted remotely, no conflict
74+
} else if (existing.length > 0 && baseVersion > 0 && (existing[0].version || 1) > baseVersion) {
75+
results.push({ id: entry.id, status: 'conflict', dbVersion: existing[0].version, localVersion: payload.version, baseVersion, isDeleted: existing[0].is_deleted ? true : false });
7476
continue;
7577
}
7678

@@ -96,9 +98,11 @@ export default async function handler(req: any, res: any) {
9698
console.log(`[Sync] Processing API: ${payload.name} in folder ${folderId} (${operation})`);
9799

98100
// Conflict detection
99-
const existing = await db.query<any>('SELECT version FROM api_collections WHERE id = ? AND branch = ?', [payload.id, branchName]);
100-
if (existing.length > 0 && baseVersion > 0 && (existing[0].version || 1) > baseVersion) {
101-
results.push({ id: entry.id, status: 'conflict', dbVersion: existing[0].version, localVersion: payload.version, baseVersion });
101+
const existing = await db.query<any>('SELECT version, is_deleted FROM api_collections WHERE id = ? AND branch = ?', [payload.id, branchName]);
102+
if (existing.length > 0 && existing[0].is_deleted && operation === 'delete') {
103+
// Already deleted remotely, no conflict
104+
} else if (existing.length > 0 && baseVersion > 0 && (existing[0].version || 1) > baseVersion) {
105+
results.push({ id: entry.id, status: 'conflict', dbVersion: existing[0].version, localVersion: payload.version, baseVersion, isDeleted: existing[0].is_deleted ? true : false });
102106
continue;
103107
}
104108

@@ -145,9 +149,11 @@ export default async function handler(req: any, res: any) {
145149
console.log(`[Sync] Processing environment: ${payload.name} (${operation})`);
146150

147151
// Conflict detection
148-
const existing = await db.query<any>('SELECT version FROM environments WHERE id = ? AND branch = ?', [payload.id, branchName]);
149-
if (existing.length > 0 && baseVersion > 0 && (existing[0].version || 1) > baseVersion) {
150-
results.push({ id: entry.id, status: 'conflict', dbVersion: existing[0].version, localVersion: payload.version, baseVersion });
152+
const existing = await db.query<any>('SELECT version, is_deleted FROM environments WHERE id = ? AND branch = ?', [payload.id, branchName]);
153+
if (existing.length > 0 && existing[0].is_deleted && operation === 'delete') {
154+
// Already deleted remotely, no conflict
155+
} else if (existing.length > 0 && baseVersion > 0 && (existing[0].version || 1) > baseVersion) {
156+
results.push({ id: entry.id, status: 'conflict', dbVersion: existing[0].version, localVersion: payload.version, baseVersion, isDeleted: existing[0].is_deleted ? true : false });
151157
continue;
152158
}
153159

server/src/db/proxyDb.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export async function verifyUserRole(adapter: DbAdapter, user: string | null, to
162162

163163
export async function getAllFolders(adapter: DbAdapter, projectId: string) {
164164
const rows = await adapter.query<any>(
165-
'SELECT id, name, description FROM folders WHERE project_id = ? ORDER BY order_index',
165+
'SELECT id, name, description FROM folders WHERE project_id = ? AND (is_deleted IS NULL OR is_deleted = FALSE OR is_deleted = 0) ORDER BY order_index',
166166
[projectId]
167167
);
168168
return rows;
@@ -181,20 +181,23 @@ export async function getApisByFolders(adapter: DbAdapter, projectId: string, al
181181
SELECT ac.*, f.name as folder_name, f.description as folder_description
182182
FROM api_collections ac
183183
JOIN folders f ON ac.folder_id = f.id
184-
WHERE ac.project_id = ? AND f.name IN (${placeholders})
184+
WHERE ac.project_id = ?
185+
AND f.name IN (${placeholders})
186+
AND (ac.is_deleted IS NULL OR ac.is_deleted = FALSE OR ac.is_deleted = 0)
187+
AND (f.is_deleted IS NULL OR f.is_deleted = FALSE OR f.is_deleted = 0)
185188
ORDER BY f.order_index, ac.created_at
186189
`, [projectId, ...allowedFolders]);
187190

188191
return rows;
189192
}
190193

191194
export async function getFolderById(adapter: DbAdapter, folderId: string, projectId: string) {
192-
const rows = await adapter.query<any>('SELECT * FROM folders WHERE id = ? AND project_id = ?', [folderId, projectId]);
195+
const rows = await adapter.query<any>('SELECT * FROM folders WHERE id = ? AND project_id = ? AND (is_deleted IS NULL OR is_deleted = FALSE OR is_deleted = 0)', [folderId, projectId]);
193196
return rows[0];
194197
}
195198

196199
export async function getApisByFolder(adapter: DbAdapter, folderId: string, projectId: string) {
197-
const rows = await adapter.query<any>('SELECT * FROM api_collections WHERE folder_id = ? AND project_id = ?', [folderId, projectId]);
200+
const rows = await adapter.query<any>('SELECT * FROM api_collections WHERE folder_id = ? AND project_id = ? AND (is_deleted IS NULL OR is_deleted = FALSE OR is_deleted = 0)', [folderId, projectId]);
198201
return rows;
199202
}
200203

0 commit comments

Comments
 (0)