sqlite-hub 1.1.2 → 1.4.0

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 (157) hide show
  1. package/README.md +106 -73
  2. package/bin/sqlite-hub.js +176 -2
  3. package/docs/API.md +122 -0
  4. package/docs/CLI.md +227 -0
  5. package/docs/DESIGN_GUIDELINES.md +45 -0
  6. package/docs/changelog.md +141 -0
  7. package/docs/shortkeys.md +27 -0
  8. package/docs/todo.md +16 -0
  9. package/examples/api/generate-types.js +52 -0
  10. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  11. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  12. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  13. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  16. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  17. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  18. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  19. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  21. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  22. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  24. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  25. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  26. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
  27. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  28. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  29. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  30. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  31. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  32. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  33. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  34. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  35. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  36. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  37. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  38. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  39. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  40. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  41. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  42. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  43. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  44. package/frontend/js/api.js +18 -0
  45. package/frontend/js/app.js +209 -13
  46. package/frontend/js/components/connectionCard.js +1 -1
  47. package/frontend/js/components/emptyState.js +4 -4
  48. package/frontend/js/components/generateTypesDropdown.js +33 -0
  49. package/frontend/js/components/metricCard.js +1 -1
  50. package/frontend/js/components/modal.js +211 -18
  51. package/frontend/js/components/pageHeader.js +1 -1
  52. package/frontend/js/components/queryHistoryDetail.js +3 -3
  53. package/frontend/js/components/queryHistoryHeader.js +1 -1
  54. package/frontend/js/components/queryHistoryPanel.js +1 -1
  55. package/frontend/js/components/queryResults.js +1 -1
  56. package/frontend/js/components/rowEditorPanel.js +65 -16
  57. package/frontend/js/components/topNav.js +1 -4
  58. package/frontend/js/components/workspaceOpenDropdown.js +52 -0
  59. package/frontend/js/router.js +28 -6
  60. package/frontend/js/store.js +489 -9
  61. package/frontend/js/utils/emailPreview.js +28 -0
  62. package/frontend/js/utils/markdownDocuments.js +17 -1
  63. package/frontend/js/views/backups.js +556 -36
  64. package/frontend/js/views/charts.js +9 -9
  65. package/frontend/js/views/connections.js +2 -2
  66. package/frontend/js/views/data.js +43 -17
  67. package/frontend/js/views/documents.js +3 -3
  68. package/frontend/js/views/editor.js +6 -6
  69. package/frontend/js/views/mediaTagging.js +7 -5
  70. package/frontend/js/views/overview.js +3 -3
  71. package/frontend/js/views/settings.js +4 -3
  72. package/frontend/js/views/structure.js +27 -10
  73. package/frontend/js/views/tableDesigner.js +23 -0
  74. package/frontend/styles/base.css +1 -40
  75. package/frontend/styles/components.css +114 -232
  76. package/frontend/styles/structure-graph.css +32 -68
  77. package/frontend/styles/tailwind.css +8 -4
  78. package/frontend/styles/tailwind.generated.css +1 -1
  79. package/frontend/styles/tokens.css +29 -21
  80. package/frontend/styles/views.css +94 -261
  81. package/package.json +18 -2
  82. package/server/routes/backups.js +18 -2
  83. package/server/routes/externalApi.js +36 -0
  84. package/server/routes/structure.js +22 -0
  85. package/server/services/databaseCommandService.js +14 -0
  86. package/server/services/sqlite/backupDiff.js +914 -0
  87. package/server/services/sqlite/backupService.js +77 -1
  88. package/server/services/sqlite/structureService.js +7 -0
  89. package/server/services/typeGenerationService.js +663 -0
  90. package/tailwind.config.cjs +0 -1
  91. package/.github/funding.yml +0 -2
  92. package/.github/workflows/ci.yml +0 -36
  93. package/database.sqlite +0 -0
  94. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  95. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  96. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  97. package/frontend/assets/mockups/connections_1200.webp +0 -0
  98. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  99. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  100. package/frontend/assets/mockups/documents_1200.webp +0 -0
  101. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  102. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  103. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  104. package/frontend/assets/mockups/overview_1200.webp +0 -0
  105. package/frontend/assets/mockups/settings_1200.webp +0 -0
  106. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  107. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  108. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  109. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  110. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  111. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  112. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  113. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  114. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  115. package/tests/api-token-auth.test.js +0 -279
  116. package/tests/backup-manager.test.js +0 -140
  117. package/tests/backups-view.test.js +0 -64
  118. package/tests/charts-height-preset-storage.test.js +0 -60
  119. package/tests/charts-route-state.test.js +0 -144
  120. package/tests/check-constraint-options.test.js +0 -90
  121. package/tests/cli-args.test.js +0 -113
  122. package/tests/cli-service-delegation.test.js +0 -140
  123. package/tests/connection-removal.test.js +0 -52
  124. package/tests/connections-file-dialog-route.test.js +0 -89
  125. package/tests/copy-column-modal.test.js +0 -131
  126. package/tests/database-command-service.test.js +0 -174
  127. package/tests/database-documents.test.js +0 -85
  128. package/tests/documents-view.test.js +0 -132
  129. package/tests/dropdown-button.test.js +0 -75
  130. package/tests/export-blob.test.js +0 -99
  131. package/tests/export-filenames.test.js +0 -38
  132. package/tests/file-path-preview.test.js +0 -165
  133. package/tests/form-controls.test.js +0 -34
  134. package/tests/json-preview.test.js +0 -49
  135. package/tests/local-request-security.test.js +0 -85
  136. package/tests/markdown-documents.test.js +0 -79
  137. package/tests/native-file-dialog.test.js +0 -105
  138. package/tests/query-editor.test.js +0 -28
  139. package/tests/query-history-detail.test.js +0 -37
  140. package/tests/query-history-header.test.js +0 -30
  141. package/tests/query-results-truncation.test.js +0 -20
  142. package/tests/risky-sql.test.js +0 -30
  143. package/tests/row-editor-json.test.js +0 -82
  144. package/tests/row-editor-null-values.test.js +0 -155
  145. package/tests/row-editor-timestamp-preview.test.js +0 -192
  146. package/tests/security-paths.test.js +0 -84
  147. package/tests/settings-api-tokens-route.test.js +0 -97
  148. package/tests/settings-metadata.test.js +0 -114
  149. package/tests/settings-view.test.js +0 -107
  150. package/tests/sql-formatter.test.js +0 -173
  151. package/tests/sql-highlight.test.js +0 -38
  152. package/tests/sql-identifier-safety.test.js +0 -171
  153. package/tests/sql-result-limit.test.js +0 -66
  154. package/tests/structure-view.test.js +0 -56
  155. package/tests/table-designer-v2-unique-constraints.test.js +0 -78
  156. package/tests/table-scroll-state.test.js +0 -70
  157. package/tests/text-cell-stats.test.js +0 -38
@@ -1,36 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- pull_request:
5
- push:
6
- branches:
7
- - main
8
-
9
- permissions:
10
- contents: read
11
-
12
- jobs:
13
- build-test-audit:
14
- runs-on: ubuntu-latest
15
-
16
- steps:
17
- - name: Checkout
18
- uses: actions/checkout@v4
19
-
20
- - name: Setup Node
21
- uses: actions/setup-node@v4
22
- with:
23
- node-version: 26
24
- cache: npm
25
-
26
- - name: Install dependencies
27
- run: npm ci
28
-
29
- - name: Build
30
- run: npm run build
31
-
32
- - name: Test
33
- run: npm test
34
-
35
- - name: Dependency audit
36
- run: npm run audit
package/database.sqlite DELETED
File without changes
@@ -1,279 +0,0 @@
1
- const assert = require("node:assert/strict");
2
- const express = require("express");
3
- const fs = require("node:fs");
4
- const os = require("node:os");
5
- const path = require("node:path");
6
- const test = require("node:test");
7
-
8
- const { createExternalApiRouter } = require("../server/routes/externalApi");
9
- const { ApiTokenService } = require("../server/services/apiTokenService");
10
- const { AppStateStore } = require("../server/services/storage/appStateStore");
11
- const { ReadOnlyError } = require("../server/utils/errors");
12
- const { errorMiddleware } = require("../server/utils/errors");
13
-
14
- function createConnection(id, label, databasePath) {
15
- return {
16
- id,
17
- label,
18
- path: databasePath,
19
- lastOpenedAt: new Date().toISOString(),
20
- lastModifiedAt: new Date().toISOString(),
21
- sizeBytes: 0,
22
- readOnly: false,
23
- logoPath: null,
24
- };
25
- }
26
-
27
- async function startApi(t) {
28
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-hub-api-token-"));
29
- const store = new AppStateStore(path.join(directory, "state.db"));
30
- const databaseA = createConnection("db-a", "Database A", path.join(directory, "a.db"));
31
- const databaseB = createConnection("db-b", "Database B", path.join(directory, "b.db"));
32
-
33
- store.upsertRecentConnection(databaseA);
34
- store.upsertRecentConnection(databaseB, { makeActive: false });
35
-
36
- const tokenService = new ApiTokenService({ appStateStore: store });
37
- const serviceCalls = [];
38
- const databaseService = {
39
- listTables(databaseId) {
40
- serviceCalls.push(databaseId);
41
- return [{ name: "companies" }];
42
- },
43
- executeRawQuery(databaseId, sql, options = {}) {
44
- serviceCalls.push(
45
- `${databaseId}:query:${sql}:${options.storeName ?? ""}:${options.executedBy ?? ""}`
46
- );
47
- if (sql === "READONLY") {
48
- throw new ReadOnlyError("Cannot execute raw SQL against a read-only database.");
49
- }
50
- return {
51
- result: {
52
- sql,
53
- statementCount: 1,
54
- statements: [],
55
- rows: [],
56
- columns: [],
57
- affectedRowCount: 0,
58
- resultKind: "unknown",
59
- timingMs: 2,
60
- historyId: 42,
61
- storedQuery: options.storeName
62
- ? {
63
- id: 42,
64
- title: options.storeName,
65
- isSaved: true,
66
- }
67
- : null,
68
- },
69
- };
70
- },
71
- executeSavedQuery(databaseId, queryName, options = {}) {
72
- serviceCalls.push(`${databaseId}:saved:${queryName}:${options.executedBy ?? ""}`);
73
- return {
74
- query: {
75
- id: 7,
76
- title: queryName,
77
- rawSql: "SELECT 1",
78
- },
79
- result: {
80
- sql: "SELECT 1",
81
- statementCount: 1,
82
- statements: [],
83
- rows: [],
84
- columns: [],
85
- affectedRowCount: 0,
86
- resultKind: "resultSet",
87
- timingMs: 3,
88
- historyId: 7,
89
- },
90
- };
91
- },
92
- };
93
- const app = express();
94
-
95
- app.use(express.json());
96
- app.use(
97
- "/api/v1",
98
- createExternalApiRouter({
99
- databaseService,
100
- tokenService,
101
- appInfoService: async ({ port, url }) => ({
102
- packageName: "sqlite-hub",
103
- appVersion: "1.0.1",
104
- sqliteVersion: "3.50.0",
105
- port,
106
- url,
107
- versionCheck: {
108
- packageName: "sqlite-hub",
109
- currentVersion: "1.0.1",
110
- latestVersion: "1.0.1",
111
- updateAvailable: false,
112
- checkedAt: "2026-06-20T10:00:00.000Z",
113
- source: "npm",
114
- releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.0.1",
115
- status: "current",
116
- },
117
- }),
118
- })
119
- );
120
- app.use(errorMiddleware);
121
-
122
- const server = await new Promise((resolve) => {
123
- const listener = app.listen(0, "127.0.0.1", () => resolve(listener));
124
- });
125
-
126
- t.after(async () => {
127
- await new Promise((resolve, reject) => {
128
- server.close((error) => (error ? reject(error) : resolve()));
129
- });
130
- store.db.close();
131
- fs.rmSync(directory, { recursive: true, force: true });
132
- });
133
-
134
- return {
135
- baseUrl: `http://127.0.0.1:${server.address().port}/api/v1`,
136
- databaseA,
137
- databaseB,
138
- serviceCalls,
139
- store,
140
- tokenService,
141
- };
142
- }
143
-
144
- test("tokens are hashed, deletable, and isolated per database", async (t) => {
145
- const fixture = await startApi(t);
146
- const created = fixture.tokenService.createToken(fixture.databaseA.id, "Automation");
147
-
148
- assert.match(created.token, /^shub_[A-Za-z0-9_-]+$/);
149
- assert.equal(fixture.tokenService.listTokens(fixture.databaseA.id)[0].token, undefined);
150
-
151
- const stored = fixture.store.db
152
- .prepare("SELECT token_hash, token_prefix FROM api_tokens WHERE id = ?")
153
- .get(created.id);
154
-
155
- assert.notEqual(stored.token_hash, created.token);
156
- assert.equal(stored.token_prefix, created.tokenPrefix);
157
- assert.equal(fixture.tokenService.authenticate(fixture.databaseA.id, created.token).id, created.id);
158
- assert.throws(
159
- () => fixture.tokenService.authenticate(fixture.databaseB.id, created.token),
160
- /invalid for this database/
161
- );
162
-
163
- const validResponse = await fetch(
164
- `${fixture.baseUrl}/databases/${fixture.databaseA.id}/tables`,
165
- { headers: { Authorization: `Bearer ${created.token}` } }
166
- );
167
- const validPayload = await validResponse.json();
168
-
169
- assert.equal(validResponse.status, 200);
170
- assert.deepEqual(validPayload.data.items, [{ name: "companies" }]);
171
- assert.deepEqual(fixture.serviceCalls, [fixture.databaseA.id]);
172
-
173
- const invalidResponse = await fetch(
174
- `${fixture.baseUrl}/databases/${fixture.databaseA.id}/tables`,
175
- { headers: { Authorization: "Bearer invalid" } }
176
- );
177
- assert.equal(invalidResponse.status, 401);
178
-
179
- const missingResponse = await fetch(
180
- `${fixture.baseUrl}/databases/${fixture.databaseA.id}/tables`
181
- );
182
- assert.equal(missingResponse.status, 401);
183
-
184
- const wrongDatabaseResponse = await fetch(
185
- `${fixture.baseUrl}/databases/${fixture.databaseB.id}/tables`,
186
- { headers: { Authorization: `Bearer ${created.token}` } }
187
- );
188
- assert.equal(wrongDatabaseResponse.status, 401);
189
- assert.deepEqual(fixture.serviceCalls, [fixture.databaseA.id]);
190
-
191
- fixture.tokenService.deleteToken(fixture.databaseA.id, created.id);
192
- assert.equal(fixture.tokenService.listTokens(fixture.databaseA.id).length, 0);
193
- assert.throws(
194
- () => fixture.tokenService.authenticate(fixture.databaseA.id, created.token),
195
- /invalid for this database/
196
- );
197
- });
198
-
199
- test("public API info returns app and version status without a token", async (t) => {
200
- const fixture = await startApi(t);
201
- const response = await fetch(`${fixture.baseUrl}/info`);
202
- const payload = await response.json();
203
-
204
- assert.equal(response.status, 200);
205
- assert.equal(payload.data.packageName, "sqlite-hub");
206
- assert.equal(payload.data.appVersion, "1.0.1");
207
- assert.equal(payload.data.sqliteVersion, "3.50.0");
208
- assert.equal(payload.data.versionCheck.status, "current");
209
- assert.equal(payload.data.versionCheck.updateAvailable, false);
210
- assert.match(payload.data.url, /^http:\/\/127\.0\.0\.1:\d+$/);
211
- });
212
-
213
- test("query API executes raw SQL with a database token", async (t) => {
214
- const fixture = await startApi(t);
215
- const created = fixture.tokenService.createToken(fixture.databaseA.id, "Automation");
216
- const response = await fetch(`${fixture.baseUrl}/query`, {
217
- method: "POST",
218
- headers: {
219
- Authorization: `Bearer ${created.token}`,
220
- "Content-Type": "application/json",
221
- },
222
- body: JSON.stringify({
223
- databaseId: fixture.databaseA.id,
224
- sql: "SELECT 1",
225
- store: "Stored API Query",
226
- }),
227
- });
228
- const payload = await response.json();
229
-
230
- assert.equal(response.status, 200);
231
- assert.equal(payload.data.sql, "SELECT 1");
232
- assert.equal(payload.data.historyId, 42);
233
- assert.equal(payload.data.storedQuery.title, "Stored API Query");
234
- assert.equal(payload.metadata.stored, true);
235
- assert.equal(payload.metadata.databaseId, fixture.databaseA.id);
236
- assert.deepEqual(fixture.serviceCalls, [
237
- `${fixture.databaseA.id}:query:SELECT 1:Stored API Query:api`,
238
- ]);
239
- });
240
-
241
- test("query API rejects read-only raw SQL execution", async (t) => {
242
- const fixture = await startApi(t);
243
- const created = fixture.tokenService.createToken(fixture.databaseA.id, "Automation");
244
- const response = await fetch(`${fixture.baseUrl}/query`, {
245
- method: "POST",
246
- headers: {
247
- Authorization: `Bearer ${created.token}`,
248
- "Content-Type": "application/json",
249
- },
250
- body: JSON.stringify({
251
- databaseId: fixture.databaseA.id,
252
- sql: "READONLY",
253
- }),
254
- });
255
- const payload = await response.json();
256
-
257
- assert.equal(response.status, 403);
258
- assert.equal(payload.error.code, "SQLITE_READONLY");
259
- });
260
-
261
- test("saved query API records executions as api", async (t) => {
262
- const fixture = await startApi(t);
263
- const created = fixture.tokenService.createToken(fixture.databaseA.id, "Automation");
264
- const response = await fetch(
265
- `${fixture.baseUrl}/databases/${fixture.databaseA.id}/queries/Hype-Reversal/execute`,
266
- {
267
- method: "POST",
268
- headers: { Authorization: `Bearer ${created.token}` },
269
- }
270
- );
271
- const payload = await response.json();
272
-
273
- assert.equal(response.status, 200);
274
- assert.equal(payload.data.historyId, 7);
275
- assert.equal(payload.metadata.query.title, "Hype-Reversal");
276
- assert.deepEqual(fixture.serviceCalls, [
277
- `${fixture.databaseA.id}:saved:Hype-Reversal:api`,
278
- ]);
279
- });
@@ -1,140 +0,0 @@
1
- const assert = require("node:assert/strict");
2
- const fs = require("node:fs");
3
- const os = require("node:os");
4
- const path = require("node:path");
5
- const test = require("node:test");
6
- const Database = require("better-sqlite3");
7
-
8
- const { BackupService } = require("../server/services/sqlite/backupService");
9
- const { ConnectionManager } = require("../server/services/sqlite/connectionManager");
10
- const { AppStateStore } = require("../server/services/storage/appStateStore");
11
-
12
- function createFixture(t) {
13
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-hub-backups-"));
14
- const databasePath = path.join(directory, "source.sqlite");
15
- const db = new Database(databasePath);
16
-
17
- db.exec(`
18
- CREATE TABLE companies (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
19
- INSERT INTO companies (name) VALUES ('Acme'), ('Globex');
20
- `);
21
- db.close();
22
-
23
- const store = new AppStateStore(path.join(directory, "state.db"));
24
- const connectionManager = new ConnectionManager({ appStateStore: store });
25
- const connection = connectionManager.openConnection({
26
- filePath: databasePath,
27
- label: "Source",
28
- id: "conn_source",
29
- makeActive: true,
30
- });
31
- const backupService = new BackupService({
32
- appStateStore: store,
33
- connectionManager,
34
- backupRootDirectory: path.join(directory, "SQLite Hub", "backups"),
35
- });
36
-
37
- t.after(() => {
38
- connectionManager.closeCurrent();
39
- store.db.close();
40
- fs.rmSync(directory, { recursive: true, force: true });
41
- });
42
-
43
- return {
44
- backupService,
45
- connection,
46
- connectionManager,
47
- directory,
48
- store,
49
- };
50
- }
51
-
52
- test("backups table is created with recent_connections foreign key and checks", (t) => {
53
- const { store } = createFixture(t);
54
- const tables = new Set(
55
- store.db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table'").all().map(row => row.name)
56
- );
57
- const foreignKeys = store.db.prepare("PRAGMA foreign_key_list(backups)").all();
58
-
59
- assert.equal(tables.has("backups"), true);
60
- assert.equal(foreignKeys[0].table, "recent_connections");
61
- assert.equal(foreignKeys[0].from, "connectionId");
62
- assert.equal(foreignKeys[0].on_delete, "SET NULL");
63
-
64
- assert.throws(
65
- () =>
66
- store.createBackupRecord({
67
- id: "invalid",
68
- connectionId: "conn_source",
69
- name: "Invalid",
70
- path: path.join(os.tmpdir(), "invalid.sqlite"),
71
- status: "unknown",
72
- type: "manual",
73
- sourcePath: path.join(os.tmpdir(), "source.sqlite"),
74
- createdAt: new Date().toISOString(),
75
- }),
76
- /CHECK constraint failed/
77
- );
78
- });
79
-
80
- test("manual backup creates file, metadata, manifest, and survives connection removal", async (t) => {
81
- const { backupService, connection, store } = createFixture(t);
82
-
83
- const backup = await backupService.createActiveBackup({
84
- name: "Before migration",
85
- notes: "Schema update",
86
- });
87
- const manifestPath = path.join(
88
- backupService.backupRootDirectory,
89
- connection.id,
90
- "manifest.json"
91
- );
92
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
93
- const stored = store.getBackup(backup.id);
94
-
95
- assert.equal(backup.status, "verified");
96
- assert.equal(backup.connectionId, connection.id);
97
- assert.equal(backup.notes, "Schema update");
98
- assert.equal(fs.existsSync(backup.path), true);
99
- assert.match(path.relative(backupService.backupRootDirectory, backup.path), /^conn_source\//);
100
- assert.equal(stored.checksumSha256.length, 64);
101
- assert.equal(stored.tableCount, 1);
102
- assert.equal(stored.rowCount, 2);
103
- assert.equal(manifest.databaseId, connection.id);
104
- assert.equal(manifest.backups[0].id, backup.id);
105
- assert.equal(manifest.backups[0].status, "verified");
106
-
107
- store.removeRecentConnection(connection.id);
108
- assert.equal(store.getBackup(backup.id).connectionId, null);
109
- });
110
-
111
- test("backup notes can be updated after creation and manifest stays in sync", async (t) => {
112
- const { backupService, connection, store } = createFixture(t);
113
-
114
- const backup = await backupService.createActiveBackup({
115
- name: "Before migration",
116
- notes: "Initial note",
117
- });
118
- const updated = backupService.updateBackupNotes(backup.id, "Reviewed after restore test");
119
- const manifestPath = path.join(
120
- backupService.backupRootDirectory,
121
- connection.id,
122
- "manifest.json"
123
- );
124
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
125
-
126
- assert.equal(updated.notes, "Reviewed after restore test");
127
- assert.equal(store.getBackup(backup.id).notes, "Reviewed after restore test");
128
- assert.equal(manifest.backups[0].notes, "Reviewed after restore test");
129
- });
130
-
131
- test("delete backup removes file, record, and manifest entry", async (t) => {
132
- const { backupService, store } = createFixture(t);
133
- const backup = await backupService.createActiveBackup({ name: "Manual backup" });
134
- const backupPath = backup.path;
135
-
136
- backupService.deleteBackup(backup.id);
137
-
138
- assert.equal(fs.existsSync(backupPath), false);
139
- assert.equal(store.getBackup(backup.id), null);
140
- });
@@ -1,64 +0,0 @@
1
- const assert = require("node:assert/strict");
2
- const path = require("node:path");
3
- const { pathToFileURL } = require("node:url");
4
- const test = require("node:test");
5
-
6
- async function loadModule() {
7
- return import(pathToFileURL(path.resolve(__dirname, "../frontend/js/views/backups.js")).href);
8
- }
9
-
10
- function createState(overrides = {}) {
11
- return {
12
- connections: {
13
- active: { id: "db-one", label: "Customers", readOnly: false },
14
- },
15
- backups: {
16
- items: [],
17
- loading: false,
18
- operationLoading: false,
19
- error: null,
20
- },
21
- ...overrides,
22
- };
23
- }
24
-
25
- test("backup manager renders empty state and create action", async () => {
26
- const { renderBackupsView } = await loadModule();
27
- const rendered = renderBackupsView(createState()).main;
28
-
29
- assert.match(rendered, /No backups yet/);
30
- assert.match(rendered, /data-action="open-create-backup-modal"/);
31
- });
32
-
33
- test("backup manager renders backup rows with status and actions", async () => {
34
- const { renderBackupsView } = await loadModule();
35
- const rendered = renderBackupsView(
36
- createState({
37
- backups: {
38
- items: [
39
- {
40
- id: "backup-one",
41
- name: "Before migration",
42
- notes: "Schema update",
43
- sizeBytes: 1024,
44
- status: "verified",
45
- fileExists: true,
46
- fileName: "backup.sqlite",
47
- path: "/tmp/backup.sqlite",
48
- createdAt: "2026-06-21T11:42:18.000Z",
49
- },
50
- ],
51
- loading: false,
52
- operationLoading: false,
53
- error: null,
54
- },
55
- })
56
- ).main;
57
-
58
- assert.match(rendered, /Before migration/);
59
- assert.match(rendered, /Verified/);
60
- assert.match(rendered, /data-action="open-edit-backup-notes-modal"/);
61
- assert.match(rendered, /data-action="open-restore-backup-modal"/);
62
- assert.match(rendered, /data-action="download-backup"/);
63
- assert.match(rendered, /data-action="open-delete-backup-modal"/);
64
- });
@@ -1,60 +0,0 @@
1
- const assert = require("node:assert/strict");
2
- const path = require("node:path");
3
- const { pathToFileURL } = require("node:url");
4
- const test = require("node:test");
5
-
6
- const CHARTS_HEIGHT_STORAGE_KEY = "sqlite_hub_charts_height_preset";
7
-
8
- function createMockStorage(initialValues = {}) {
9
- const entries = new Map(Object.entries(initialValues));
10
-
11
- return {
12
- getItem(key) {
13
- return entries.has(key) ? entries.get(key) : null;
14
- },
15
- setItem(key, value) {
16
- entries.set(key, String(value));
17
- },
18
- };
19
- }
20
-
21
- async function importStoreWithMockStorage(storage) {
22
- const originalDescriptor = Object.getOwnPropertyDescriptor(globalThis, "localStorage");
23
-
24
- Object.defineProperty(globalThis, "localStorage", {
25
- configurable: true,
26
- value: storage,
27
- });
28
-
29
- const moduleUrl = pathToFileURL(path.resolve(__dirname, "../frontend/js/store.js")).href;
30
- const store = await import(`${moduleUrl}?charts-height=${Date.now()}`);
31
-
32
- return {
33
- restore() {
34
- if (originalDescriptor) {
35
- Object.defineProperty(globalThis, "localStorage", originalDescriptor);
36
- } else {
37
- delete globalThis.localStorage;
38
- }
39
- },
40
- store,
41
- };
42
- }
43
-
44
- test("charts height preset is read from and written to localStorage", async () => {
45
- const storage = createMockStorage({
46
- [CHARTS_HEIGHT_STORAGE_KEY]: "large",
47
- });
48
- const { restore, store } = await importStoreWithMockStorage(storage);
49
-
50
- try {
51
- assert.equal(store.getState().charts.chartHeightPreset, "large");
52
-
53
- store.setChartsHeightPreset("small");
54
-
55
- assert.equal(store.getState().charts.chartHeightPreset, "small");
56
- assert.equal(storage.getItem(CHARTS_HEIGHT_STORAGE_KEY), "small");
57
- } finally {
58
- restore();
59
- }
60
- });