sqlite-hub 0.1.3

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 (76) hide show
  1. package/.npmingnore +4 -0
  2. package/README.md +46 -0
  3. package/assets/images/logo.webp +0 -0
  4. package/assets/images/logo_extrasmall.webp +0 -0
  5. package/assets/images/logo_raw.png +0 -0
  6. package/assets/images/logo_small.webp +0 -0
  7. package/assets/mockups/connections.png +0 -0
  8. package/assets/mockups/data.png +0 -0
  9. package/assets/mockups/data_edit.png +0 -0
  10. package/assets/mockups/home.png +0 -0
  11. package/assets/mockups/overview.png +0 -0
  12. package/assets/mockups/sql_editor.png +0 -0
  13. package/assets/mockups/structure.png +0 -0
  14. package/bin/sqlite-hub.js +116 -0
  15. package/changelog.md +3 -0
  16. package/data/.gitkeep +0 -0
  17. package/index.html +100 -0
  18. package/js/api.js +193 -0
  19. package/js/app.js +520 -0
  20. package/js/components/actionBar.js +8 -0
  21. package/js/components/appShell.js +17 -0
  22. package/js/components/badges.js +5 -0
  23. package/js/components/bottomTabs.js +37 -0
  24. package/js/components/connectionCard.js +106 -0
  25. package/js/components/dataGrid.js +47 -0
  26. package/js/components/emptyState.js +159 -0
  27. package/js/components/metricCard.js +32 -0
  28. package/js/components/modal.js +317 -0
  29. package/js/components/pageHeader.js +33 -0
  30. package/js/components/queryEditor.js +121 -0
  31. package/js/components/queryResults.js +107 -0
  32. package/js/components/rowEditorPanel.js +164 -0
  33. package/js/components/sidebar.js +57 -0
  34. package/js/components/statusBar.js +39 -0
  35. package/js/components/toast.js +39 -0
  36. package/js/components/topNav.js +27 -0
  37. package/js/router.js +66 -0
  38. package/js/store.js +1092 -0
  39. package/js/utils/format.js +179 -0
  40. package/js/views/connections.js +133 -0
  41. package/js/views/data.js +400 -0
  42. package/js/views/editor.js +259 -0
  43. package/js/views/landing.js +11 -0
  44. package/js/views/overview.js +220 -0
  45. package/js/views/settings.js +109 -0
  46. package/js/views/structure.js +242 -0
  47. package/package.json +18 -0
  48. package/publish_brew.sh +444 -0
  49. package/publish_npm.sh +241 -0
  50. package/server/routes/connections.js +146 -0
  51. package/server/routes/data.js +59 -0
  52. package/server/routes/export.js +25 -0
  53. package/server/routes/overview.js +39 -0
  54. package/server/routes/settings.js +50 -0
  55. package/server/routes/sql.js +50 -0
  56. package/server/routes/structure.js +38 -0
  57. package/server/server.js +136 -0
  58. package/server/services/sqlite/connectionManager.js +306 -0
  59. package/server/services/sqlite/dataBrowserService.js +255 -0
  60. package/server/services/sqlite/exportService.js +34 -0
  61. package/server/services/sqlite/importService.js +111 -0
  62. package/server/services/sqlite/introspection.js +302 -0
  63. package/server/services/sqlite/overviewService.js +109 -0
  64. package/server/services/sqlite/sqlExecutor.js +434 -0
  65. package/server/services/sqlite/structureService.js +60 -0
  66. package/server/services/storage/appStateStore.js +530 -0
  67. package/server/utils/csv.js +34 -0
  68. package/server/utils/errors.js +175 -0
  69. package/server/utils/fileValidation.js +135 -0
  70. package/server/utils/identifier.js +38 -0
  71. package/server/utils/sqliteTypes.js +112 -0
  72. package/styles/base.css +176 -0
  73. package/styles/components.css +323 -0
  74. package/styles/layout.css +101 -0
  75. package/styles/tokens.css +49 -0
  76. package/styles/views.css +84 -0
package/js/store.js ADDED
@@ -0,0 +1,1092 @@
1
+ import * as api from "./api.js";
2
+ import { inferStatusTone } from "./utils/format.js";
3
+
4
+ const listeners = new Set();
5
+ const DEFAULT_SETTINGS = {
6
+ defaultPageSize: 50,
7
+ maxPageSize: 200,
8
+ csvDelimiter: ",",
9
+ };
10
+ const DATA_PAGE_SIZES = [25, 50, 100];
11
+ const MISSING_DATABASE_ERROR = {
12
+ code: "ACTIVE_DATABASE_REQUIRED",
13
+ message: "No active SQLite database selected.",
14
+ };
15
+
16
+ let routeLoadVersion = 0;
17
+
18
+ const state = {
19
+ ready: false,
20
+ route: { name: "landing", path: "/", params: {} },
21
+ modal: null,
22
+ toasts: [],
23
+ connections: {
24
+ recent: [],
25
+ active: null,
26
+ loading: false,
27
+ error: null,
28
+ },
29
+ settings: {
30
+ data: { ...DEFAULT_SETTINGS },
31
+ loading: false,
32
+ error: null,
33
+ appVersion: null,
34
+ },
35
+ overview: {
36
+ data: null,
37
+ loading: false,
38
+ error: null,
39
+ },
40
+ dataBrowser: {
41
+ tables: [],
42
+ selectedTable: null,
43
+ table: null,
44
+ loading: false,
45
+ tableLoading: false,
46
+ saving: false,
47
+ page: 1,
48
+ pageSize: 50,
49
+ selectedRowIndex: null,
50
+ error: null,
51
+ saveError: null,
52
+ },
53
+ editor: {
54
+ sqlText: "",
55
+ history: [],
56
+ historyLoading: false,
57
+ historyError: null,
58
+ activeTab: "messages",
59
+ executing: false,
60
+ result: null,
61
+ error: null,
62
+ exportLoading: false,
63
+ selectedRowIndex: null,
64
+ saving: false,
65
+ saveError: null,
66
+ },
67
+ structure: {
68
+ data: null,
69
+ selectedName: null,
70
+ detail: null,
71
+ loading: false,
72
+ detailLoading: false,
73
+ error: null,
74
+ },
75
+ };
76
+
77
+ function emitChange() {
78
+ listeners.forEach((listener) => listener(getState()));
79
+ }
80
+
81
+ function clone(value) {
82
+ return structuredClone(value);
83
+ }
84
+
85
+ function normalizeError(error) {
86
+ if (!error) {
87
+ return null;
88
+ }
89
+
90
+ return {
91
+ code: error.code ?? "REQUEST_FAILED",
92
+ message: error.message ?? "Request failed.",
93
+ sqliteCode: error.sqliteCode ?? null,
94
+ details: error.details ?? null,
95
+ warnings: error.warnings ?? [],
96
+ };
97
+ }
98
+
99
+ function requiresActiveDatabase(routeName) {
100
+ return ["overview", "data", "editor", "editorResults", "structure"].includes(routeName);
101
+ }
102
+
103
+ function normalizeDataPageSize(value, fallback = 50) {
104
+ const numericValue = Number(value);
105
+
106
+ if (DATA_PAGE_SIZES.includes(numericValue)) {
107
+ return numericValue;
108
+ }
109
+
110
+ return fallback;
111
+ }
112
+
113
+ function canEditQueryResult(snapshot = state) {
114
+ return Boolean(snapshot.editor.result?.editing?.enabled) && !snapshot.connections.active?.readOnly;
115
+ }
116
+
117
+ function buildUpdatedEditorResultRow(existingRow, updatedSourceRow, editableColumns) {
118
+ const nextRow = {
119
+ ...existingRow,
120
+ __identity: updatedSourceRow?.__identity ?? existingRow?.__identity ?? null,
121
+ };
122
+
123
+ editableColumns.forEach((column) => {
124
+ if (column.sourceColumn === "rowid") {
125
+ nextRow[column.resultName] =
126
+ updatedSourceRow?.__identity?.values?.rowid ?? existingRow?.[column.resultName] ?? null;
127
+ return;
128
+ }
129
+
130
+ nextRow[column.resultName] = updatedSourceRow?.[column.sourceColumn] ?? null;
131
+ });
132
+
133
+ return nextRow;
134
+ }
135
+
136
+ function getCurrentStructureEntry(snapshot = state) {
137
+ const entries = snapshot.structure.data?.entries ?? [];
138
+ return entries.find((entry) => entry.name === snapshot.structure.selectedName) ?? null;
139
+ }
140
+
141
+ function clearRouteSlices() {
142
+ state.overview.error = null;
143
+ state.dataBrowser.error = null;
144
+ state.dataBrowser.saveError = null;
145
+ state.structure.error = null;
146
+ }
147
+
148
+ function setMissingDatabaseState() {
149
+ const error = { ...MISSING_DATABASE_ERROR };
150
+
151
+ state.overview.loading = false;
152
+ state.overview.data = null;
153
+ state.overview.error = error;
154
+
155
+ state.dataBrowser.loading = false;
156
+ state.dataBrowser.tableLoading = false;
157
+ state.dataBrowser.tables = [];
158
+ state.dataBrowser.selectedTable = null;
159
+ state.dataBrowser.table = null;
160
+ state.dataBrowser.page = 1;
161
+ state.dataBrowser.selectedRowIndex = null;
162
+ state.dataBrowser.error = error;
163
+ state.dataBrowser.saveError = null;
164
+
165
+ state.structure.loading = false;
166
+ state.structure.detailLoading = false;
167
+ state.structure.data = null;
168
+ state.structure.detail = null;
169
+ state.structure.error = error;
170
+ }
171
+
172
+ function syncRouteContext() {
173
+ const { route } = state;
174
+
175
+ if (route.name === "editorResults") {
176
+ state.editor.activeTab = "results";
177
+ } else if (route.name === "editor" && state.editor.activeTab === "results") {
178
+ state.editor.activeTab = "messages";
179
+ }
180
+
181
+ if (route.name !== "editorResults") {
182
+ state.editor.selectedRowIndex = null;
183
+ state.editor.saveError = null;
184
+ }
185
+
186
+ if (
187
+ route.name !== "data" ||
188
+ (route.params?.tableName && route.params.tableName !== state.dataBrowser.selectedTable)
189
+ ) {
190
+ if (route.name !== "data" || route.params?.tableName !== state.dataBrowser.selectedTable) {
191
+ state.dataBrowser.page = 1;
192
+ }
193
+ state.dataBrowser.selectedRowIndex = null;
194
+ state.dataBrowser.saveError = null;
195
+ }
196
+
197
+ if (route.name !== "structure") {
198
+ state.structure.detail = null;
199
+ state.structure.selectedName = null;
200
+ }
201
+ }
202
+
203
+ async function refreshConnectionsState() {
204
+ state.connections.loading = true;
205
+ state.connections.error = null;
206
+ emitChange();
207
+
208
+ try {
209
+ const [recentResponse, activeResponse] = await Promise.all([
210
+ api.getRecentConnections(),
211
+ api.getActiveConnection(),
212
+ ]);
213
+
214
+ state.connections.recent = recentResponse.data ?? [];
215
+ state.connections.active = activeResponse.data ?? null;
216
+ state.connections.error = null;
217
+ } catch (error) {
218
+ state.connections.error = normalizeError(error);
219
+ } finally {
220
+ state.connections.loading = false;
221
+ emitChange();
222
+ }
223
+ }
224
+
225
+ async function refreshSettingsState() {
226
+ state.settings.loading = true;
227
+ state.settings.error = null;
228
+ emitChange();
229
+
230
+ try {
231
+ const response = await api.getSettings();
232
+ state.settings.data = {
233
+ ...DEFAULT_SETTINGS,
234
+ ...(response.data ?? {}),
235
+ };
236
+ state.settings.appVersion = response.metadata?.appVersion ?? null;
237
+ } catch (error) {
238
+ state.settings.error = normalizeError(error);
239
+ } finally {
240
+ state.settings.loading = false;
241
+ emitChange();
242
+ }
243
+ }
244
+
245
+ async function refreshSqlHistoryState() {
246
+ state.editor.historyLoading = true;
247
+ state.editor.historyError = null;
248
+ emitChange();
249
+
250
+ try {
251
+ const response = await api.getSqlHistory();
252
+ state.editor.history = response.data ?? [];
253
+ } catch (error) {
254
+ state.editor.historyError = normalizeError(error);
255
+ } finally {
256
+ state.editor.historyLoading = false;
257
+ emitChange();
258
+ }
259
+ }
260
+
261
+ async function loadOverview(version) {
262
+ state.overview.loading = true;
263
+ state.overview.error = null;
264
+ emitChange();
265
+
266
+ try {
267
+ const response = await api.getOverview();
268
+
269
+ if (version !== routeLoadVersion) {
270
+ return;
271
+ }
272
+
273
+ state.overview.data = response.data;
274
+ state.overview.error = null;
275
+ } catch (error) {
276
+ if (version !== routeLoadVersion) {
277
+ return;
278
+ }
279
+
280
+ state.overview.data = null;
281
+ state.overview.error = normalizeError(error);
282
+ } finally {
283
+ if (version === routeLoadVersion) {
284
+ state.overview.loading = false;
285
+ emitChange();
286
+ }
287
+ }
288
+ }
289
+
290
+ async function loadDataTable(version) {
291
+ const tableName = state.dataBrowser.selectedTable;
292
+ const pageSize = normalizeDataPageSize(state.dataBrowser.pageSize, 50);
293
+ const page = Math.max(1, Number(state.dataBrowser.page) || 1);
294
+
295
+ if (!tableName) {
296
+ state.dataBrowser.table = null;
297
+ state.dataBrowser.selectedRowIndex = null;
298
+ return;
299
+ }
300
+
301
+ state.dataBrowser.tableLoading = true;
302
+ state.dataBrowser.saveError = null;
303
+ emitChange();
304
+
305
+ try {
306
+ const response = await api.getDataTable(tableName, {
307
+ limit: pageSize,
308
+ offset: (page - 1) * pageSize,
309
+ });
310
+
311
+ if (version !== routeLoadVersion) {
312
+ return;
313
+ }
314
+
315
+ state.dataBrowser.table = response.data ?? null;
316
+ state.dataBrowser.pageSize = pageSize;
317
+ state.dataBrowser.page = response.data?.page ?? page;
318
+ state.dataBrowser.selectedRowIndex = null;
319
+ } catch (error) {
320
+ if (version !== routeLoadVersion) {
321
+ return;
322
+ }
323
+
324
+ state.dataBrowser.table = null;
325
+ state.dataBrowser.error = normalizeError(error);
326
+ } finally {
327
+ if (version === routeLoadVersion) {
328
+ state.dataBrowser.tableLoading = false;
329
+ emitChange();
330
+ }
331
+ }
332
+ }
333
+
334
+ async function loadData(version, route) {
335
+ state.dataBrowser.loading = true;
336
+ state.dataBrowser.error = null;
337
+ emitChange();
338
+
339
+ try {
340
+ const response = await api.getDataTables();
341
+
342
+ if (version !== routeLoadVersion) {
343
+ return;
344
+ }
345
+
346
+ const tables = response.data?.tables ?? [];
347
+ const requestedTableName = route.params?.tableName ?? null;
348
+
349
+ state.dataBrowser.tables = tables;
350
+ state.dataBrowser.error = null;
351
+
352
+ if (requestedTableName && tables.some((table) => table.name === requestedTableName)) {
353
+ if (requestedTableName !== state.dataBrowser.selectedTable) {
354
+ state.dataBrowser.page = 1;
355
+ }
356
+ state.dataBrowser.selectedTable = requestedTableName;
357
+ } else if (
358
+ !state.dataBrowser.selectedTable ||
359
+ !tables.some((table) => table.name === state.dataBrowser.selectedTable)
360
+ ) {
361
+ state.dataBrowser.selectedTable = tables[0]?.name ?? null;
362
+ state.dataBrowser.page = 1;
363
+ }
364
+
365
+ if (!state.dataBrowser.selectedTable) {
366
+ state.dataBrowser.table = null;
367
+ state.dataBrowser.selectedRowIndex = null;
368
+ return;
369
+ }
370
+
371
+ await loadDataTable(version);
372
+ } catch (error) {
373
+ if (version !== routeLoadVersion) {
374
+ return;
375
+ }
376
+
377
+ state.dataBrowser.tables = [];
378
+ state.dataBrowser.selectedTable = null;
379
+ state.dataBrowser.table = null;
380
+ state.dataBrowser.error = normalizeError(error);
381
+ } finally {
382
+ if (version === routeLoadVersion) {
383
+ state.dataBrowser.loading = false;
384
+ emitChange();
385
+ }
386
+ }
387
+ }
388
+
389
+ async function loadStructureDetail(version) {
390
+ const entry = getCurrentStructureEntry(state);
391
+
392
+ if (!entry) {
393
+ state.structure.detail = null;
394
+ return;
395
+ }
396
+
397
+ if (entry.type !== "table") {
398
+ state.structure.detail = {
399
+ name: entry.name,
400
+ type: entry.type,
401
+ tableName: entry.tableName,
402
+ ddl: entry.sql,
403
+ columns: [],
404
+ foreignKeys: [],
405
+ indexes: [],
406
+ triggers: [],
407
+ identityStrategy: null,
408
+ notSafelyUpdatable: true,
409
+ };
410
+ emitChange();
411
+ return;
412
+ }
413
+
414
+ state.structure.detailLoading = true;
415
+ emitChange();
416
+
417
+ try {
418
+ const response = await api.getStructureDetail(entry.name);
419
+
420
+ if (version !== routeLoadVersion) {
421
+ return;
422
+ }
423
+
424
+ state.structure.detail = response.data;
425
+ } catch (error) {
426
+ if (version !== routeLoadVersion) {
427
+ return;
428
+ }
429
+
430
+ state.structure.error = normalizeError(error);
431
+ state.structure.detail = null;
432
+ } finally {
433
+ if (version === routeLoadVersion) {
434
+ state.structure.detailLoading = false;
435
+ emitChange();
436
+ }
437
+ }
438
+ }
439
+
440
+ async function loadStructure(version) {
441
+ state.structure.loading = true;
442
+ state.structure.error = null;
443
+ emitChange();
444
+
445
+ try {
446
+ const response = await api.getStructureOverview();
447
+
448
+ if (version !== routeLoadVersion) {
449
+ return;
450
+ }
451
+
452
+ state.structure.data = response.data;
453
+ state.structure.selectedName =
454
+ state.structure.selectedName ??
455
+ response.data.grouped.tables[0]?.name ??
456
+ response.data.entries[0]?.name ??
457
+ null;
458
+
459
+ await loadStructureDetail(version);
460
+ } catch (error) {
461
+ if (version !== routeLoadVersion) {
462
+ return;
463
+ }
464
+
465
+ state.structure.data = null;
466
+ state.structure.detail = null;
467
+ state.structure.error = normalizeError(error);
468
+ } finally {
469
+ if (version === routeLoadVersion) {
470
+ state.structure.loading = false;
471
+ emitChange();
472
+ }
473
+ }
474
+ }
475
+
476
+ function invalidateDatabaseCaches() {
477
+ state.overview.data = null;
478
+ state.dataBrowser.tables = [];
479
+ state.dataBrowser.selectedTable = null;
480
+ state.dataBrowser.table = null;
481
+ state.dataBrowser.page = 1;
482
+ state.dataBrowser.selectedRowIndex = null;
483
+ state.dataBrowser.error = null;
484
+ state.dataBrowser.saveError = null;
485
+ state.structure.data = null;
486
+ state.structure.detail = null;
487
+ }
488
+
489
+ async function loadRouteData(route) {
490
+ clearRouteSlices();
491
+
492
+ if (requiresActiveDatabase(route.name) && !state.connections.active) {
493
+ setMissingDatabaseState();
494
+ emitChange();
495
+ return;
496
+ }
497
+
498
+ const version = ++routeLoadVersion;
499
+
500
+ if (route.name === "landing" || route.name === "connections") {
501
+ await refreshConnectionsState();
502
+ return;
503
+ }
504
+
505
+ switch (route.name) {
506
+ case "overview":
507
+ await loadOverview(version);
508
+ return;
509
+ case "data":
510
+ await loadData(version, route);
511
+ return;
512
+ case "editor":
513
+ case "editorResults":
514
+ await refreshSqlHistoryState();
515
+ return;
516
+ case "structure":
517
+ await loadStructure(version);
518
+ return;
519
+ case "settings":
520
+ await refreshSettingsState();
521
+ return;
522
+ default:
523
+ }
524
+ }
525
+
526
+ function pushToast(message, tone = "muted") {
527
+ const id = crypto.randomUUID();
528
+ state.toasts.push({ id, message, tone });
529
+ emitChange();
530
+
531
+ window.setTimeout(() => {
532
+ dismissToast(id);
533
+ }, 3600);
534
+ }
535
+
536
+ function withModalError(error) {
537
+ if (!state.modal) {
538
+ return;
539
+ }
540
+
541
+ state.modal.error = normalizeError(error);
542
+ state.modal.submitting = false;
543
+ emitChange();
544
+ }
545
+
546
+ function startModalSubmission() {
547
+ if (!state.modal) {
548
+ return;
549
+ }
550
+
551
+ state.modal.submitting = true;
552
+ state.modal.error = null;
553
+ emitChange();
554
+ }
555
+
556
+ function closeModalInternal() {
557
+ state.modal = null;
558
+ emitChange();
559
+ }
560
+
561
+ export function getState() {
562
+ return clone(state);
563
+ }
564
+
565
+ export function subscribe(listener) {
566
+ listeners.add(listener);
567
+ return () => listeners.delete(listener);
568
+ }
569
+
570
+ export async function initializeApp() {
571
+ state.ready = false;
572
+ emitChange();
573
+
574
+ await Promise.all([
575
+ refreshConnectionsState(),
576
+ refreshSettingsState(),
577
+ refreshSqlHistoryState(),
578
+ ]);
579
+
580
+ state.ready = true;
581
+ emitChange();
582
+
583
+ await loadRouteData(state.route);
584
+ }
585
+
586
+ export async function setRoute(route) {
587
+ state.route = route;
588
+ syncRouteContext();
589
+ emitChange();
590
+ await loadRouteData(route);
591
+ }
592
+
593
+ export function openModal(kind) {
594
+ state.modal = {
595
+ kind,
596
+ error: null,
597
+ submitting: false,
598
+ };
599
+ emitChange();
600
+ }
601
+
602
+ export function openEditConnectionModal(id) {
603
+ const connection = state.connections.recent.find((entry) => entry.id === id);
604
+
605
+ if (!connection) {
606
+ pushToast("Connection could not be loaded for editing.", "alert");
607
+ return;
608
+ }
609
+
610
+ state.modal = {
611
+ kind: "edit-connection",
612
+ connectionId: connection.id,
613
+ connection,
614
+ error: null,
615
+ submitting: false,
616
+ };
617
+ emitChange();
618
+ }
619
+
620
+ export function closeModal() {
621
+ closeModalInternal();
622
+ }
623
+
624
+ export function dismissToast(id) {
625
+ const nextToasts = state.toasts.filter((toast) => toast.id !== id);
626
+
627
+ if (nextToasts.length === state.toasts.length) {
628
+ return;
629
+ }
630
+
631
+ state.toasts = nextToasts;
632
+ emitChange();
633
+ }
634
+
635
+ export async function submitOpenConnection(payload) {
636
+ startModalSubmission();
637
+
638
+ try {
639
+ const response = await api.openConnection(payload);
640
+ closeModalInternal();
641
+ pushToast(response.message || "Database connected.", "success");
642
+ await refreshConnectionsState();
643
+ invalidateDatabaseCaches();
644
+ return response.data;
645
+ } catch (error) {
646
+ withModalError(error);
647
+ return null;
648
+ }
649
+ }
650
+
651
+ export async function submitCreateConnection(payload) {
652
+ startModalSubmission();
653
+
654
+ try {
655
+ const response = await api.createConnection(payload);
656
+ closeModalInternal();
657
+ pushToast(response.message || "Database created.", "success");
658
+ await refreshConnectionsState();
659
+ invalidateDatabaseCaches();
660
+ return response.data;
661
+ } catch (error) {
662
+ withModalError(error);
663
+ return null;
664
+ }
665
+ }
666
+
667
+ export async function submitImportSql(payload) {
668
+ startModalSubmission();
669
+
670
+ try {
671
+ const response = await api.importSql(payload);
672
+ closeModalInternal();
673
+ pushToast(response.message || "SQL dump imported.", "success");
674
+ await refreshConnectionsState();
675
+ invalidateDatabaseCaches();
676
+ return response.data;
677
+ } catch (error) {
678
+ withModalError(error);
679
+ return null;
680
+ }
681
+ }
682
+
683
+ export async function submitEditConnection(id, payload) {
684
+ startModalSubmission();
685
+
686
+ const wasActive = state.connections.active?.id === id;
687
+
688
+ try {
689
+ const response = await api.updateRecentConnection(id, payload);
690
+ closeModalInternal();
691
+ pushToast(response.message || "Connection updated.", "success");
692
+ await refreshConnectionsState();
693
+ invalidateDatabaseCaches();
694
+
695
+ if (wasActive && state.route.name !== "connections") {
696
+ await loadRouteData(state.route);
697
+ }
698
+
699
+ return response.data;
700
+ } catch (error) {
701
+ withModalError(error);
702
+ return null;
703
+ }
704
+ }
705
+
706
+ export async function selectConnection(id) {
707
+ state.connections.loading = true;
708
+ emitChange();
709
+
710
+ try {
711
+ const response = await api.selectActiveConnection(id);
712
+ await refreshConnectionsState();
713
+ invalidateDatabaseCaches();
714
+ pushToast(response.message || "Active database updated.", "success");
715
+ return response.data;
716
+ } catch (error) {
717
+ state.connections.error = normalizeError(error);
718
+ emitChange();
719
+ return null;
720
+ } finally {
721
+ state.connections.loading = false;
722
+ emitChange();
723
+ }
724
+ }
725
+
726
+ export async function removeConnection(id) {
727
+ state.connections.loading = true;
728
+ emitChange();
729
+
730
+ try {
731
+ const response = await api.removeRecentConnection(id);
732
+ await refreshConnectionsState();
733
+ invalidateDatabaseCaches();
734
+ pushToast(response.message || "Recent connection removed.", "muted");
735
+ return response.data;
736
+ } catch (error) {
737
+ state.connections.error = normalizeError(error);
738
+ emitChange();
739
+ return null;
740
+ } finally {
741
+ state.connections.loading = false;
742
+ emitChange();
743
+ }
744
+ }
745
+
746
+ export function setCurrentQuery(query) {
747
+ const nextQuery = String(query ?? "");
748
+ const previousLineCount = Math.max(1, String(state.editor.sqlText || "").split("\n").length);
749
+ const nextLineCount = Math.max(1, nextQuery.split("\n").length);
750
+
751
+ state.editor.sqlText = nextQuery;
752
+
753
+ if (previousLineCount !== nextLineCount) {
754
+ emitChange();
755
+ }
756
+ }
757
+
758
+ export function clearCurrentQuery() {
759
+ state.editor.sqlText = "";
760
+ state.editor.error = null;
761
+ emitChange();
762
+ }
763
+
764
+ export function clearEditorResults() {
765
+ state.editor.result = null;
766
+ state.editor.error = null;
767
+ state.editor.selectedRowIndex = null;
768
+ state.editor.saving = false;
769
+ state.editor.saveError = null;
770
+ if (state.editor.activeTab === "results") {
771
+ state.editor.activeTab = "messages";
772
+ }
773
+ emitChange();
774
+ }
775
+
776
+ export function setEditorTab(tab) {
777
+ state.editor.activeTab = tab;
778
+ emitChange();
779
+ }
780
+
781
+ export async function executeCurrentQuery() {
782
+ state.editor.executing = true;
783
+ state.editor.error = null;
784
+ state.editor.selectedRowIndex = null;
785
+ state.editor.saving = false;
786
+ state.editor.saveError = null;
787
+ emitChange();
788
+
789
+ try {
790
+ const response = await api.executeSql(state.editor.sqlText);
791
+ state.editor.result = response.data;
792
+ state.editor.error = null;
793
+ state.editor.activeTab = "results";
794
+ invalidateDatabaseCaches();
795
+ await refreshSqlHistoryState();
796
+ pushToast(
797
+ response.message || `Executed ${response.data.statementCount} SQL statement(s).`,
798
+ "success"
799
+ );
800
+ return true;
801
+ } catch (error) {
802
+ state.editor.error = normalizeError(error);
803
+ state.editor.activeTab = "messages";
804
+ emitChange();
805
+ return false;
806
+ } finally {
807
+ state.editor.executing = false;
808
+ emitChange();
809
+ }
810
+ }
811
+
812
+ export async function clearSqlHistoryStateAndData() {
813
+ state.editor.historyLoading = true;
814
+ emitChange();
815
+
816
+ try {
817
+ const response = await api.clearSqlHistory();
818
+ state.editor.history = response.data ?? [];
819
+ pushToast(response.message || "SQL history cleared.", "muted");
820
+ return true;
821
+ } catch (error) {
822
+ state.editor.historyError = normalizeError(error);
823
+ emitChange();
824
+ return false;
825
+ } finally {
826
+ state.editor.historyLoading = false;
827
+ emitChange();
828
+ }
829
+ }
830
+
831
+ export function loadQueryFromHistory(id) {
832
+ const historyEntry = state.editor.history.find((entry) => entry.id === id);
833
+
834
+ if (!historyEntry) {
835
+ return;
836
+ }
837
+
838
+ state.editor.sqlText = historyEntry.sql;
839
+ emitChange();
840
+ }
841
+
842
+ export async function selectStructureEntry(name) {
843
+ state.structure.selectedName = name;
844
+ emitChange();
845
+ await loadStructureDetail(++routeLoadVersion);
846
+ }
847
+
848
+ export function selectDataRow(index) {
849
+ const numericIndex = Number(index);
850
+
851
+ if (!Number.isInteger(numericIndex) || numericIndex < 0) {
852
+ return;
853
+ }
854
+
855
+ state.dataBrowser.selectedRowIndex = numericIndex;
856
+ state.dataBrowser.saveError = null;
857
+ emitChange();
858
+ }
859
+
860
+ export function selectEditorRow(index) {
861
+ const numericIndex = Number(index);
862
+
863
+ if (!Number.isInteger(numericIndex) || numericIndex < 0 || !canEditQueryResult()) {
864
+ return;
865
+ }
866
+
867
+ state.editor.selectedRowIndex = numericIndex;
868
+ state.editor.saveError = null;
869
+ emitChange();
870
+ }
871
+
872
+ export function clearEditorRowSelection() {
873
+ if (state.editor.selectedRowIndex === null) {
874
+ return;
875
+ }
876
+
877
+ state.editor.selectedRowIndex = null;
878
+ state.editor.saveError = null;
879
+ emitChange();
880
+ }
881
+
882
+ export function clearDataRowSelection() {
883
+ if (state.dataBrowser.selectedRowIndex === null) {
884
+ return;
885
+ }
886
+
887
+ state.dataBrowser.selectedRowIndex = null;
888
+ state.dataBrowser.saveError = null;
889
+ emitChange();
890
+ }
891
+
892
+ export async function setDataPage(page) {
893
+ const numericPage = Number(page);
894
+
895
+ if (!Number.isInteger(numericPage) || numericPage < 1) {
896
+ return;
897
+ }
898
+
899
+ if (numericPage === state.dataBrowser.page) {
900
+ return;
901
+ }
902
+
903
+ state.dataBrowser.page = numericPage;
904
+ state.dataBrowser.selectedRowIndex = null;
905
+ state.dataBrowser.saveError = null;
906
+ emitChange();
907
+
908
+ if (state.route.name === "data" && state.dataBrowser.selectedTable) {
909
+ await loadDataTable(++routeLoadVersion);
910
+ }
911
+ }
912
+
913
+ export async function setDataPageSize(pageSize) {
914
+ const normalizedPageSize = normalizeDataPageSize(pageSize, state.dataBrowser.pageSize);
915
+
916
+ if (normalizedPageSize === state.dataBrowser.pageSize) {
917
+ return;
918
+ }
919
+
920
+ state.dataBrowser.pageSize = normalizedPageSize;
921
+ state.dataBrowser.page = 1;
922
+ state.dataBrowser.selectedRowIndex = null;
923
+ state.dataBrowser.saveError = null;
924
+ emitChange();
925
+
926
+ if (state.route.name === "data" && state.dataBrowser.selectedTable) {
927
+ await loadDataTable(++routeLoadVersion);
928
+ }
929
+ }
930
+
931
+ export async function submitDataRowUpdate(rowIndex, values) {
932
+ const numericIndex = Number(rowIndex);
933
+ const tableName = state.dataBrowser.selectedTable;
934
+ const row = state.dataBrowser.table?.rows?.[numericIndex];
935
+
936
+ if (!tableName || !row) {
937
+ pushToast("The selected row could not be loaded.", "alert");
938
+ return null;
939
+ }
940
+
941
+ state.dataBrowser.saving = true;
942
+ state.dataBrowser.saveError = null;
943
+ emitChange();
944
+
945
+ try {
946
+ const response = await api.updateDataTableRow(tableName, {
947
+ identity: row.__identity,
948
+ values,
949
+ });
950
+
951
+ pushToast(response.message || "Table row updated.", "success");
952
+ await loadDataTable(++routeLoadVersion);
953
+ state.dataBrowser.selectedRowIndex = null;
954
+ return response.data;
955
+ } catch (error) {
956
+ state.dataBrowser.saveError = normalizeError(error);
957
+ emitChange();
958
+ return null;
959
+ } finally {
960
+ state.dataBrowser.saving = false;
961
+ emitChange();
962
+ }
963
+ }
964
+
965
+ export async function submitEditorRowUpdate(rowIndex, values) {
966
+ const numericIndex = Number(rowIndex);
967
+ const result = state.editor.result;
968
+ const row = result?.rows?.[numericIndex];
969
+ const tableName = result?.editing?.tableName ?? null;
970
+
971
+ if (!tableName || !row || !canEditQueryResult()) {
972
+ pushToast("The selected query result row could not be loaded.", "alert");
973
+ return null;
974
+ }
975
+
976
+ state.editor.saving = true;
977
+ state.editor.saveError = null;
978
+ emitChange();
979
+
980
+ try {
981
+ const response = await api.updateDataTableRow(tableName, {
982
+ identity: row.__identity,
983
+ values,
984
+ });
985
+ const editableColumns = result.editing?.columns ?? [];
986
+ const nextRows = [...(result.rows ?? [])];
987
+
988
+ nextRows[numericIndex] = buildUpdatedEditorResultRow(
989
+ row,
990
+ response.data?.row ?? null,
991
+ editableColumns
992
+ );
993
+ state.editor.result = {
994
+ ...result,
995
+ rows: nextRows,
996
+ };
997
+ state.editor.selectedRowIndex = null;
998
+ invalidateDatabaseCaches();
999
+ pushToast(response.message || "Query result row updated.", "success");
1000
+ emitChange();
1001
+ return response.data;
1002
+ } catch (error) {
1003
+ state.editor.saveError = normalizeError(error);
1004
+ emitChange();
1005
+ return null;
1006
+ } finally {
1007
+ state.editor.saving = false;
1008
+ emitChange();
1009
+ }
1010
+ }
1011
+
1012
+ export async function exportCurrentQueryCsv() {
1013
+ state.editor.exportLoading = true;
1014
+ emitChange();
1015
+
1016
+ try {
1017
+ await api.downloadQueryCsv(state.editor.sqlText);
1018
+ pushToast("Query export started.", "success");
1019
+ return true;
1020
+ } catch (error) {
1021
+ state.editor.error = normalizeError(error);
1022
+ emitChange();
1023
+ return false;
1024
+ } finally {
1025
+ state.editor.exportLoading = false;
1026
+ emitChange();
1027
+ }
1028
+ }
1029
+
1030
+ export async function refreshCurrentRoute() {
1031
+ await loadRouteData(state.route);
1032
+ }
1033
+
1034
+ export function getCurrentConnection(snapshot = state) {
1035
+ return snapshot.connections.active;
1036
+ }
1037
+
1038
+ export function getQueryMessages(snapshot = state) {
1039
+ if (snapshot.editor.error) {
1040
+ return [
1041
+ {
1042
+ tone: "alert",
1043
+ label: snapshot.editor.error.code,
1044
+ value: snapshot.editor.error.message,
1045
+ },
1046
+ ];
1047
+ }
1048
+
1049
+ if (!snapshot.editor.result) {
1050
+ return [
1051
+ {
1052
+ tone: "muted",
1053
+ label: "IDLE",
1054
+ value: "No SQL statements have been executed yet.",
1055
+ },
1056
+ ];
1057
+ }
1058
+
1059
+ return snapshot.editor.result.statements.map((statement) => ({
1060
+ tone: statement.kind === "resultSet" ? "success" : inferStatusTone(statement.keyword),
1061
+ label: `${statement.keyword} #${statement.index + 1}`,
1062
+ value:
1063
+ statement.kind === "resultSet"
1064
+ ? `${statement.rowCount} row(s) returned.`
1065
+ : `${statement.changes} row(s) affected.`,
1066
+ }));
1067
+ }
1068
+
1069
+ export function getQueryPerformance(snapshot = state) {
1070
+ const result = snapshot.editor.result;
1071
+
1072
+ if (!result) {
1073
+ return {
1074
+ timingMs: null,
1075
+ statementCount: 0,
1076
+ rowCount: 0,
1077
+ affectedRowCount: 0,
1078
+ };
1079
+ }
1080
+
1081
+ return {
1082
+ timingMs: result.timingMs ?? 0,
1083
+ statementCount: result.statementCount ?? result.statements?.length ?? 0,
1084
+ rowCount: result.rows?.length ?? 0,
1085
+ affectedRowCount: result.affectedRowCount ?? 0,
1086
+ };
1087
+ }
1088
+
1089
+ export function getCurrentStructureEntryDetail(snapshot = state) {
1090
+ const entry = getCurrentStructureEntry(snapshot);
1091
+ return entry ? snapshot.structure.detail : null;
1092
+ }