axiodb 22.3.1 → 22.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/electron/electron-builder.json +58 -0
  2. package/electron/main/main.cts +585 -0
  3. package/electron/main/preload.cts +51 -0
  4. package/electron/package-lock.json +8123 -0
  5. package/electron/package.json +52 -0
  6. package/electron/public/AXioDB.png +0 -0
  7. package/electron/resources/after-install.sh +45 -0
  8. package/electron/resources/after-remove.sh +29 -0
  9. package/electron/resources/icon.png +0 -0
  10. package/electron/resources/icons/128x128.png +0 -0
  11. package/electron/resources/icons/16x16.png +0 -0
  12. package/electron/resources/icons/24x24.png +0 -0
  13. package/electron/resources/icons/256x256.png +0 -0
  14. package/electron/resources/icons/32x32.png +0 -0
  15. package/electron/resources/icons/48x48.png +0 -0
  16. package/electron/resources/icons/512x512.png +0 -0
  17. package/electron/resources/icons/64x64.png +0 -0
  18. package/electron/src/App.jsx +128 -0
  19. package/electron/src/api/authApi.js +83 -0
  20. package/electron/src/api/client.js +102 -0
  21. package/electron/src/assets/AXioDB.png +0 -0
  22. package/electron/src/components/auth/CreateRoleModal.jsx +189 -0
  23. package/electron/src/components/auth/CreateUserModal.jsx +131 -0
  24. package/electron/src/components/auth/ForcePasswordChangeModal.jsx +132 -0
  25. package/electron/src/components/auth/ProtectedRoute.jsx +42 -0
  26. package/electron/src/components/auth/ResetPasswordModal.jsx +90 -0
  27. package/electron/src/components/auth/UserAvatarMenu.jsx +103 -0
  28. package/electron/src/components/collection/CreateCollectionModal.jsx +116 -0
  29. package/electron/src/components/collection/DeleteCollectionModal.jsx +85 -0
  30. package/electron/src/components/collection/SchemaViewModal.jsx +369 -0
  31. package/electron/src/components/dashboard/CollectionsChart.jsx +94 -0
  32. package/electron/src/components/dashboard/DatabaseTreeView.jsx +130 -0
  33. package/electron/src/components/dashboard/InMemoryCacheCard.jsx +60 -0
  34. package/electron/src/components/dashboard/StorageDonut.jsx +76 -0
  35. package/electron/src/components/dashboard/StorageUsageCard.jsx +59 -0
  36. package/electron/src/components/dashboard/TotalCollectionsCard.jsx +47 -0
  37. package/electron/src/components/dashboard/TotalDatabasesCard.jsx +43 -0
  38. package/electron/src/components/dashboard/TotalDocumentsCard.jsx +44 -0
  39. package/electron/src/components/database/CreateDatabaseModal.jsx +109 -0
  40. package/electron/src/components/database/DeleteDatabaseModal.jsx +97 -0
  41. package/electron/src/components/query/CodeEditor.jsx +343 -0
  42. package/electron/src/components/query/ObjectEditor.jsx +115 -0
  43. package/electron/src/components/query/ObjectView.jsx +44 -0
  44. package/electron/src/components/query/QueryEditor.jsx +32 -0
  45. package/electron/src/components/query/queryLanguage.js +755 -0
  46. package/electron/src/components/ui/Button.jsx +58 -0
  47. package/electron/src/components/ui/Card.jsx +29 -0
  48. package/electron/src/components/ui/ErrorBoundary.jsx +108 -0
  49. package/electron/src/components/ui/Feedback.jsx +66 -0
  50. package/electron/src/components/ui/Field.jsx +65 -0
  51. package/electron/src/components/ui/MetricCard.jsx +104 -0
  52. package/electron/src/components/ui/Modal.jsx +119 -0
  53. package/electron/src/components/ui/Page.jsx +40 -0
  54. package/electron/src/config/key.js +11 -0
  55. package/electron/src/index.css +181 -0
  56. package/electron/src/index.html +13 -0
  57. package/electron/src/layout/Sidebar.jsx +478 -0
  58. package/electron/src/layout/StatusBar.jsx +70 -0
  59. package/electron/src/layout/Titlebar.jsx +128 -0
  60. package/electron/src/main.jsx +42 -0
  61. package/electron/src/pages/ConnectionHub.jsx +464 -0
  62. package/electron/src/pages/Dashboard.jsx +128 -0
  63. package/electron/src/pages/Documents.jsx +823 -0
  64. package/electron/src/pages/Import.jsx +527 -0
  65. package/electron/src/pages/UserManagement.jsx +294 -0
  66. package/electron/src/pages/Welcome.jsx +157 -0
  67. package/electron/src/store/authStore.js +30 -0
  68. package/electron/src/store/connectionStore.js +103 -0
  69. package/electron/src/store/dbStore.js +165 -0
  70. package/electron/src/store/store.js +8 -0
  71. package/electron/src/utils/format.js +39 -0
  72. package/electron/vite.config.js +28 -0
  73. package/lib/Services/Indexation.operation.js +1 -1
  74. package/package.json +1 -1
@@ -0,0 +1,58 @@
1
+ {
2
+ "appId": "com.axiodb.control",
3
+ "productName": "AxioDB Control",
4
+ "copyright": "Copyright © 2026 Ankan Saha",
5
+ "directories": {
6
+ "output": "release",
7
+ "buildResources": "resources"
8
+ },
9
+ "files": [
10
+ "dist/**/*",
11
+ "dist-electron/**/*",
12
+ "resources/**/*",
13
+ "package.json"
14
+ ],
15
+ "extraResources": [
16
+ {
17
+ "from": "resources/icon.png",
18
+ "to": "icon.png"
19
+ }
20
+ ],
21
+ "linux": {
22
+ "target": [
23
+ { "target": "deb", "arch": ["x64", "arm64"] },
24
+ { "target": "AppImage", "arch": ["x64", "arm64"] }
25
+ ],
26
+ "maintainer": "Ankan Saha <dev@axiodb.in>",
27
+ "category": "Development",
28
+ "icon": "resources/icons",
29
+ "synopsis": "Desktop GUI management client for AxioDB",
30
+ "description": "Next-generation desktop interface for managing AxioDB databases, collections, documents, queries, and monitoring.",
31
+ "desktop": {
32
+ "StartupWMClass": "AxioDB Control"
33
+ }
34
+ },
35
+ "deb": {
36
+ "afterInstall": "resources/after-install.sh",
37
+ "afterRemove": "resources/after-remove.sh"
38
+ },
39
+ "win": {
40
+ "target": [
41
+ { "target": "nsis", "arch": ["x64"] }
42
+ ],
43
+ "icon": "resources/icon.png"
44
+ },
45
+ "mac": {
46
+ "target": [
47
+ { "target": "zip", "arch": ["x64", "arm64"] }
48
+ ],
49
+ "category": "public.app-category.developer-tools",
50
+ "icon": "resources/icon.png"
51
+ },
52
+ "nsis": {
53
+ "oneClick": false,
54
+ "allowToChangeInstallationDirectory": true,
55
+ "createDesktopShortcut": true,
56
+ "createStartMenuShortcut": true
57
+ }
58
+ }
@@ -0,0 +1,585 @@
1
+ import { app, BrowserWindow, ipcMain, dialog, shell, nativeImage } from "electron";
2
+ import path from "path";
3
+ import http from "http";
4
+ import https from "https";
5
+ import fs from "fs";
6
+
7
+ // Set application identity for Linux desktop and Windows taskbar
8
+ app.setName("AxioDB Control");
9
+ if (process.platform === "win32") {
10
+ app.setAppUserModelId("com.axiodb.control");
11
+ }
12
+
13
+ // In-memory session cookie store for AxioDB sessions
14
+ const cookieStore = new Map<string, string>();
15
+
16
+ let mainWindow: BrowserWindow | null = null;
17
+ let splashWindow: BrowserWindow | null = null;
18
+
19
+ const isDev = process.env.NODE_ENV === "development" || process.argv.includes("--dev");
20
+
21
+ function getWindowIcon() {
22
+ const possiblePaths = [
23
+ path.join(process.resourcesPath, "icon.png"),
24
+ path.join(process.resourcesPath, "resources/icon.png"),
25
+ path.join(process.resourcesPath, "app.asar.unpacked/resources/icon.png"),
26
+ path.join(__dirname, "../resources/icon.png"),
27
+ path.join(__dirname, "../resources/icons/512x512.png"),
28
+ path.join(__dirname, "../../resources/icon.png"),
29
+ path.join(__dirname, "../public/AXioDB.png"),
30
+ path.join(__dirname, "../src/assets/AXioDB.png"),
31
+ "/usr/share/icons/hicolor/512x512/apps/axiodb-control.png",
32
+ "/usr/share/pixmaps/axiodb-control.png",
33
+ ];
34
+
35
+ for (const p of possiblePaths) {
36
+ if (fs.existsSync(p)) {
37
+ const img = nativeImage.createFromPath(p);
38
+ if (!img.isEmpty()) {
39
+ return img;
40
+ }
41
+ }
42
+ }
43
+ return undefined;
44
+ }
45
+
46
+ function createSplashWindow(): BrowserWindow {
47
+ const splash = new BrowserWindow({
48
+ width: 400,
49
+ height: 300,
50
+ frame: false,
51
+ transparent: false,
52
+ center: true,
53
+ resizable: false,
54
+ closable: false,
55
+ minimizable: false,
56
+ maximizable: false,
57
+ backgroundColor: "#f8fafc",
58
+ skipTaskbar: true,
59
+ });
60
+
61
+ const html = `<!DOCTYPE html>
62
+ <html>
63
+ <head><meta charset="utf-8">
64
+ <style>
65
+ body{margin:0;background:#f8fafc;height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
66
+ .logo{display:flex;align-items:center;gap:10px;margin-bottom:26px}
67
+ .logo-text{font-size:22px;font-weight:700;background:linear-gradient(135deg,#0f172a,#33415a);-webkit-background-clip:text;color:transparent}
68
+ .balls{display:flex;align-items:flex-end;gap:7px;margin-bottom:18px}
69
+ .ball{width:14px;height:14px;border-radius:50%;display:inline-block}
70
+ .ball-0{background:linear-gradient(135deg,#3b82f8,#60a5fa);animation:bounce 0.6s ease-in-out 0ms infinite both}
71
+ .ball-1{background:linear-gradient(135deg,#10b981,#34d399);animation:bounce 0.6s ease-in-out 100ms infinite both}
72
+ .ball-2{background:linear-gradient(135deg,#a855f7,#c084fc);animation:bounce 0.6s ease-in-out 200ms infinite both}
73
+ .ball-3{background:linear-gradient(135deg,#f59e0b,#fbbf24);animation:bounce 0.6s ease-in-out 300ms infinite both}
74
+ .ball-4{background:linear-gradient(135deg,#ec4899,#f871f3);animation:bounce 0.6s ease-in-out 400ms infinite both}
75
+ .text{font-size:13px;color:#64748b;font-weight:500}
76
+ @keyframes bounce{0%,80%,100%{transform:scaleY(0.7);opacity:0.5}50%{transform:scaleY(1);opacity:1}}
77
+ @keyframes pulseLogo{0%,100%{opacity:0.4}50%{opacity:1}}
78
+ </style>
79
+ </head>
80
+ <body>
81
+ <div class="logo">
82
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
83
+ <ellipse cx="12" cy="6" rx="8" ry="3" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
84
+ <path d="M4 6v12c0 1.66 3.58 3 8 3s8-1.34 8-3V6" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
85
+ <circle cx="12" cy="18" r="4" fill="#10b981"/>
86
+ </svg>
87
+ <span class="logo-text">AxioDB Control</span>
88
+ </div>
89
+ <div class="balls">
90
+ <div class="ball ball-0"></div>
91
+ <div class="ball ball-1"></div>
92
+ <div class="ball ball-2"></div>
93
+ <div class="ball ball-3"></div>
94
+ <div class="ball ball-4"></div>
95
+ </div>
96
+ <div class="text">Booting AxioDB Control…</div>
97
+ </body>
98
+ </html>`;
99
+
100
+ splash.loadURL(`data:text/html;charset=utf-8;base64,${Buffer.from(html).toString("base64")}`);
101
+ return splash;
102
+ }
103
+
104
+ function createWindow() {
105
+ const icon = getWindowIcon();
106
+
107
+ mainWindow = new BrowserWindow({
108
+ width: 1320,
109
+ height: 880,
110
+ minWidth: 1040,
111
+ minHeight: 680,
112
+ frame: false,
113
+ backgroundColor: "#f8fafc",
114
+ show: false,
115
+ icon,
116
+ title: "AxioDB Control",
117
+ webPreferences: {
118
+ preload: path.join(__dirname, "preload.cjs"),
119
+ contextIsolation: true,
120
+ nodeIntegration: false,
121
+ sandbox: false,
122
+ },
123
+ });
124
+
125
+ mainWindow.once("ready-to-show", () => {
126
+ setTimeout(() => {
127
+ mainWindow?.show();
128
+ if (splashWindow) {
129
+ splashWindow.close();
130
+ splashWindow = null;
131
+ }
132
+ }, 500);
133
+ });
134
+
135
+ if (icon) {
136
+ mainWindow.setIcon(icon);
137
+ }
138
+
139
+ if (isDev) {
140
+ mainWindow.loadURL("http://localhost:5173");
141
+ } else {
142
+ mainWindow.loadFile(path.join(__dirname, "../dist/index.html"));
143
+ }
144
+
145
+ mainWindow.on("maximize", () => {
146
+ mainWindow?.webContents.send("window:maximize-change", true);
147
+ });
148
+
149
+ mainWindow.on("unmaximize", () => {
150
+ mainWindow?.webContents.send("window:maximize-change", false);
151
+ });
152
+
153
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
154
+ shell.openExternal(url);
155
+ return { action: "deny" };
156
+ });
157
+
158
+ mainWindow.webContents.on("console-message", (_event, level, message, line, sourceId) => {
159
+ const levelNames = ["DEBUG", "INFO", "WARN", "ERROR"];
160
+ const lvl = levelNames[level] || `L${level}`;
161
+ const src = sourceId ? `${path.basename(sourceId)}:${line}` : "";
162
+ console.log(`[Renderer:${lvl}] ${message} ${src}`.trim());
163
+ });
164
+
165
+ mainWindow.webContents.on("render-process-gone", (_event, details) => {
166
+ console.error("[Main] Renderer process gone:", details.reason, "exitCode:", details.exitCode);
167
+ });
168
+ }
169
+
170
+ app.whenReady().then(() => {
171
+ splashWindow = createSplashWindow();
172
+ createWindow();
173
+
174
+ app.on("activate", () => {
175
+ if (BrowserWindow.getAllWindows().length === 0) {
176
+ splashWindow = createSplashWindow();
177
+ createWindow();
178
+ }
179
+ });
180
+ });
181
+
182
+ app.on("window-all-closed", () => {
183
+ if (process.platform !== "darwin") {
184
+ app.quit();
185
+ }
186
+ });
187
+
188
+ // Window controls
189
+ ipcMain.handle("window:minimize", () => {
190
+ mainWindow?.minimize();
191
+ });
192
+
193
+ ipcMain.handle("window:maximize", () => {
194
+ if (!mainWindow) return false;
195
+ if (mainWindow.isMaximized()) {
196
+ mainWindow.unmaximize();
197
+ return false;
198
+ } else {
199
+ mainWindow.maximize();
200
+ return true;
201
+ }
202
+ });
203
+
204
+ ipcMain.handle("window:close", () => {
205
+ mainWindow?.close();
206
+ });
207
+
208
+ ipcMain.handle("window:isMaximized", () => {
209
+ return mainWindow ? mainWindow.isMaximized() : false;
210
+ });
211
+
212
+ // App Info
213
+ ipcMain.handle("app:getVersion", () => {
214
+ return app.getVersion();
215
+ });
216
+
217
+ ipcMain.handle("shell:openExternal", async (_event, url: string) => {
218
+ if (url.startsWith("http://") || url.startsWith("https://")) {
219
+ await shell.openExternal(url);
220
+ }
221
+ });
222
+
223
+ // Native File Picker for Backup Archive Import
224
+ ipcMain.handle("dialog:selectImportFile", async () => {
225
+ if (!mainWindow) return null;
226
+ const result = await dialog.showOpenDialog(mainWindow, {
227
+ title: "Select AxioDB Backup Archive",
228
+ properties: ["openFile"],
229
+ filters: [
230
+ { name: "Database Archives", extensions: ["zip", "tar", "gz", "tgz"] },
231
+ { name: "All Files", extensions: ["*"] },
232
+ ],
233
+ });
234
+
235
+ if (result.canceled || result.filePaths.length === 0) {
236
+ return null;
237
+ }
238
+
239
+ const filePath = result.filePaths[0];
240
+ const stats = fs.statSync(filePath);
241
+ return {
242
+ filePath,
243
+ name: path.basename(filePath),
244
+ size: stats.size,
245
+ };
246
+ });
247
+
248
+ // Read file buffer for import
249
+ ipcMain.handle("file:readBuffer", async (_event, filePath: string) => {
250
+ return fs.readFileSync(filePath);
251
+ });
252
+
253
+ // Clear session cookies (e.g. on disconnect or logout)
254
+ ipcMain.handle("network:clearCookies", () => {
255
+ cookieStore.clear();
256
+ return true;
257
+ });
258
+
259
+ // Streaming database archive import via HTTP multipart
260
+ ipcMain.handle("network:uploadDatabase", async (_event, { filePath, url }: { filePath: string; url: string }) => {
261
+ return new Promise((resolve, reject) => {
262
+ try {
263
+ const fullUrl = new URL(url);
264
+ const isHttps = fullUrl.protocol === "https:";
265
+ const client = isHttps ? https : http;
266
+ const hostKey = `${fullUrl.protocol}//${fullUrl.host}`;
267
+ const savedCookie = cookieStore.get(hostKey);
268
+ const boundary = "----AxioDBUpload" + Date.now() + Math.random().toString(36).slice(2);
269
+ const fileName = path.basename(filePath);
270
+ const stat = fs.statSync(filePath);
271
+
272
+ const header = Buffer.from(
273
+ `--${boundary}\r\n` +
274
+ `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n` +
275
+ `Content-Type: application/gzip\r\n\r\n`
276
+ );
277
+ const footer = Buffer.from(`\r\n--${boundary}--\r\n`);
278
+ const totalSize = header.length + stat.size + footer.length;
279
+
280
+ const requestHeaders: Record<string, string> = {
281
+ "User-Agent": "AxioDB-Control/22.3.1 (Desktop)",
282
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
283
+ "Content-Length": String(totalSize),
284
+ };
285
+ if (savedCookie) {
286
+ requestHeaders["Cookie"] = savedCookie;
287
+ }
288
+
289
+ const req = client.request(
290
+ {
291
+ hostname: fullUrl.hostname,
292
+ port: fullUrl.port || (isHttps ? 443 : 80),
293
+ path: `${fullUrl.pathname}${fullUrl.search}`,
294
+ method: "POST",
295
+ headers: requestHeaders,
296
+ timeout: 120000,
297
+ },
298
+ (res) => {
299
+ const chunks: Buffer[] = [];
300
+ res.on("data", (c) => chunks.push(Buffer.from(c)));
301
+ res.on("end", () => {
302
+ const bodyText = Buffer.concat(chunks).toString("utf8");
303
+ let parsedData: unknown = null;
304
+ try {
305
+ parsedData = JSON.parse(bodyText);
306
+ } catch {
307
+ parsedData = bodyText;
308
+ }
309
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
310
+ resolve({ status: res.statusCode, data: parsedData });
311
+ } else {
312
+ const errMsg =
313
+ parsedData && typeof parsedData === "object" && "message" in parsedData
314
+ ? String((parsedData as { message: unknown }).message)
315
+ : `Upload failed with status ${res.statusCode}`;
316
+ reject(new Error(errMsg));
317
+ }
318
+ });
319
+ }
320
+ );
321
+
322
+ req.on("error", (err) => reject(err));
323
+
324
+ req.write(header);
325
+ const fileStream = fs.createReadStream(filePath);
326
+ fileStream.pipe(req, { end: false });
327
+ fileStream.on("end", () => {
328
+ req.write(footer);
329
+ req.end();
330
+ });
331
+ fileStream.on("error", (err) => {
332
+ req.destroy(err);
333
+ reject(err);
334
+ });
335
+ } catch (err: unknown) {
336
+ reject(err instanceof Error ? err : new Error(String(err)));
337
+ }
338
+ });
339
+ });
340
+
341
+ // Network Request IPC Handler
342
+ // Executes requests natively in Node.js to bypass browser CORS and handle session cookies seamlessly
343
+ ipcMain.handle("network:request", async (_event, config: {
344
+ url: string;
345
+ method?: string;
346
+ headers?: Record<string, string>;
347
+ data?: unknown;
348
+ params?: Record<string, string | number | boolean>;
349
+ timeout?: number;
350
+ }) => {
351
+ return new Promise((resolve, reject) => {
352
+ try {
353
+ const fullUrl = new URL(config.url);
354
+ if (config.params) {
355
+ for (const [key, value] of Object.entries(config.params)) {
356
+ if (value !== undefined && value !== null) {
357
+ fullUrl.searchParams.append(key, String(value));
358
+ }
359
+ }
360
+ }
361
+
362
+ const isHttps = fullUrl.protocol === "https:";
363
+ const client = isHttps ? https : http;
364
+
365
+ const hostKey = `${fullUrl.protocol}//${fullUrl.host}`;
366
+ const savedCookie = cookieStore.get(hostKey);
367
+
368
+ const requestHeaders: Record<string, string> = {
369
+ "User-Agent": "AxioDB-Control/22.3.1 (Desktop)",
370
+ "Accept": "application/json, text/plain, */*",
371
+ };
372
+
373
+ if (config.headers && typeof config.headers === "object") {
374
+ for (const [key, value] of Object.entries(config.headers)) {
375
+ if (value !== undefined && value !== null && typeof value !== "object") {
376
+ requestHeaders[key] = String(value);
377
+ }
378
+ }
379
+ }
380
+
381
+ // Attach stored session cookie
382
+ if (savedCookie) {
383
+ requestHeaders["Cookie"] = savedCookie;
384
+ }
385
+
386
+ let payload: Buffer | null = null;
387
+ if (config.data !== undefined && config.data !== null) {
388
+ if (Buffer.isBuffer(config.data)) {
389
+ payload = config.data;
390
+ } else if (typeof config.data === "string") {
391
+ payload = Buffer.from(config.data, "utf8");
392
+ } else {
393
+ payload = Buffer.from(JSON.stringify(config.data), "utf8");
394
+ }
395
+ requestHeaders["Content-Length"] = String(payload.length);
396
+ if (!requestHeaders["Content-Type"]) {
397
+ requestHeaders["Content-Type"] = "application/json";
398
+ }
399
+ }
400
+
401
+ const options: http.RequestOptions = {
402
+ hostname: fullUrl.hostname,
403
+ port: fullUrl.port || (isHttps ? 443 : 80),
404
+ path: `${fullUrl.pathname}${fullUrl.search}`,
405
+ method: (config.method || "GET").toUpperCase(),
406
+ headers: requestHeaders,
407
+ timeout: config.timeout || 30000,
408
+ };
409
+
410
+ const req = client.request(options, (res) => {
411
+ // Collect Set-Cookie headers
412
+ const setCookieHeaders = res.headers["set-cookie"];
413
+ if (setCookieHeaders && setCookieHeaders.length > 0) {
414
+ const cookiePairs: string[] = [];
415
+ for (const headerStr of setCookieHeaders) {
416
+ const firstPart = headerStr.split(";")[0].trim();
417
+ if (firstPart) {
418
+ cookiePairs.push(firstPart);
419
+ }
420
+ }
421
+ if (cookiePairs.length > 0) {
422
+ cookieStore.set(hostKey, cookiePairs.join("; "));
423
+ }
424
+ }
425
+
426
+ const chunks: Buffer[] = [];
427
+ res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
428
+ res.on("end", () => {
429
+ const bodyBuffer = Buffer.concat(chunks);
430
+ const bodyText = bodyBuffer.toString("utf8");
431
+
432
+ let parsedData: unknown = bodyText;
433
+ const contentType = res.headers["content-type"] || "";
434
+ if (contentType.includes("application/json")) {
435
+ try {
436
+ parsedData = JSON.parse(bodyText);
437
+ } catch {
438
+ parsedData = bodyText;
439
+ }
440
+ }
441
+
442
+ const responsePayload = {
443
+ status: res.statusCode || 200,
444
+ statusText: res.statusMessage || "OK",
445
+ headers: res.headers,
446
+ data: parsedData,
447
+ };
448
+
449
+ // Always resolve responsePayload so renderer / Axios receives full HTTP response (status, headers, data)
450
+ // and Axios's built-in status validator creates an authentic AxiosError with error.response intact
451
+ resolve(responsePayload);
452
+ });
453
+ });
454
+
455
+ req.on("error", (err) => {
456
+ reject(err);
457
+ });
458
+
459
+ req.on("timeout", () => {
460
+ req.destroy();
461
+ reject(new Error(`Request timed out after ${options.timeout}ms`));
462
+ });
463
+
464
+ if (payload) {
465
+ req.write(payload);
466
+ }
467
+ req.end();
468
+ } catch (err) {
469
+ reject(err);
470
+ }
471
+ });
472
+ });
473
+
474
+ // Binary file download — streams a binary HTTP response straight to disk via a save dialog.
475
+ // The export endpoint returns a tar.gz archive that cannot survive the JSON round-trip of
476
+ // `network:request`, so this handler pipes the raw Buffer stream into a WriteStream.
477
+ ipcMain.handle(
478
+ "network:exportDatabase",
479
+ async (_event, { dbName, baseUrl }: { dbName: string; baseUrl: string }) => {
480
+ if (!mainWindow) throw new Error("No main window available");
481
+
482
+ const saveResult = await dialog.showSaveDialog(mainWindow, {
483
+ title: "Export Database",
484
+ defaultPath: `${dbName}.tar.gz`,
485
+ filters: [
486
+ { name: "GZIP archives (*.tar.gz, *.tgz)", extensions: ["tar.gz", "tgz"] },
487
+ { name: "All files", extensions: ["*"] },
488
+ ],
489
+ });
490
+
491
+ if (saveResult.canceled || !saveResult.filePath) {
492
+ return { canceled: true };
493
+ }
494
+
495
+ return new Promise((resolve, reject) => {
496
+ try {
497
+ const fullUrl = new URL(`${baseUrl.replace(/\/+$/, "")}/api/db/export-database/`);
498
+ fullUrl.searchParams.append("dbName", dbName);
499
+
500
+ const isHttps = fullUrl.protocol === "https:";
501
+ const client = isHttps ? https : http;
502
+ const hostKey = `${fullUrl.protocol}//${fullUrl.host}`;
503
+ const savedCookie = cookieStore.get(hostKey);
504
+
505
+ const requestHeaders: Record<string, string> = {
506
+ "User-Agent": "AxioDB-Control/22.3.1 (Desktop)",
507
+ Accept: "*/*",
508
+ };
509
+ if (savedCookie) {
510
+ requestHeaders["Cookie"] = savedCookie;
511
+ }
512
+
513
+ const req = client.request(
514
+ {
515
+ hostname: fullUrl.hostname,
516
+ port: fullUrl.port || (isHttps ? 443 : 80),
517
+ path: `${fullUrl.pathname}${fullUrl.search}`,
518
+ method: "GET",
519
+ headers: requestHeaders,
520
+ timeout: 180000,
521
+ },
522
+ (res) => {
523
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
524
+ let errorBody = "";
525
+ res.on("data", (chunk) => {
526
+ errorBody += chunk.toString("utf8");
527
+ });
528
+ res.on("end", () => {
529
+ try {
530
+ const parsed = JSON.parse(errorBody);
531
+ reject(
532
+ new Error(
533
+ (parsed.message as string) ||
534
+ `Export failed with status ${res.statusCode}`,
535
+ ),
536
+ );
537
+ } catch {
538
+ reject(
539
+ new Error(`Export failed with status ${res.statusCode}`),
540
+ );
541
+ }
542
+ });
543
+ return;
544
+ }
545
+
546
+ const fileStream = fs.createWriteStream(saveResult.filePath);
547
+ let downloadedBytes = 0;
548
+
549
+ res.on("data", (chunk) => {
550
+ downloadedBytes += chunk.length;
551
+ });
552
+
553
+ res.pipe(fileStream);
554
+
555
+ fileStream.on("finish", () => {
556
+ fileStream.close(() => {
557
+ resolve({
558
+ canceled: false,
559
+ success: true,
560
+ filePath: saveResult.filePath,
561
+ bytes: downloadedBytes,
562
+ });
563
+ });
564
+ });
565
+
566
+ fileStream.on("error", (err) => {
567
+ fs.unlink(saveResult.filePath, () => {});
568
+ reject(err);
569
+ });
570
+ },
571
+ );
572
+
573
+ req.on("error", (err) => reject(err));
574
+ req.on("timeout", () => {
575
+ req.destroy();
576
+ reject(new Error("Export timed out after 180000ms"));
577
+ });
578
+
579
+ req.end();
580
+ } catch (err: unknown) {
581
+ reject(err instanceof Error ? err : new Error(String(err)));
582
+ }
583
+ });
584
+ },
585
+ );
@@ -0,0 +1,51 @@
1
+ import { contextBridge, ipcRenderer } from "electron";
2
+
3
+ export interface RequestConfig {
4
+ url: string;
5
+ method?: string;
6
+ headers?: Record<string, string>;
7
+ data?: unknown;
8
+ params?: Record<string, string | number | boolean>;
9
+ timeout?: number;
10
+ }
11
+
12
+ export interface NetworkResponse<T = unknown> {
13
+ status: number;
14
+ statusText: string;
15
+ headers: Record<string, string | string[] | undefined>;
16
+ data: T;
17
+ }
18
+
19
+ contextBridge.exposeInMainWorld("electronAPI", {
20
+ isElectron: true,
21
+ platform: process.platform,
22
+
23
+ // Window Controls
24
+ minimize: () => ipcRenderer.invoke("window:minimize"),
25
+ maximize: () => ipcRenderer.invoke("window:maximize"),
26
+ close: () => ipcRenderer.invoke("window:close"),
27
+ isMaximized: () => ipcRenderer.invoke("window:isMaximized"),
28
+ onMaximizeChange: (callback: (isMax: boolean) => void) => {
29
+ const handler = (_event: Electron.IpcRendererEvent, isMax: boolean) => callback(isMax);
30
+ ipcRenderer.on("window:maximize-change", handler);
31
+ return () => ipcRenderer.removeListener("window:maximize-change", handler);
32
+ },
33
+
34
+ // App & Shell
35
+ getVersion: () => ipcRenderer.invoke("app:getVersion"),
36
+ openExternal: (url: string) => ipcRenderer.invoke("shell:openExternal", url),
37
+
38
+ // File picker & upload
39
+ selectImportFile: () => ipcRenderer.invoke("dialog:selectImportFile"),
40
+ readFileBuffer: (filePath: string) => ipcRenderer.invoke("file:readBuffer", filePath),
41
+ uploadDatabase: (filePath: string, url: string) =>
42
+ ipcRenderer.invoke("network:uploadDatabase", { filePath, url }),
43
+
44
+ // Binary file download (export)
45
+ exportDatabase: (dbName: string, baseUrl: string) =>
46
+ ipcRenderer.invoke("network:exportDatabase", { dbName, baseUrl }),
47
+
48
+ // Network IPC client
49
+ request: (config: RequestConfig) => ipcRenderer.invoke("network:request", config),
50
+ clearCookies: () => ipcRenderer.invoke("network:clearCookies"),
51
+ });