sqlite-hub 1.2.0 → 1.5.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 (169) hide show
  1. package/README.md +113 -71
  2. package/bin/sqlite-hub.js +723 -116
  3. package/docs/API.md +152 -0
  4. package/docs/CLI.md +249 -0
  5. package/docs/CLI_API_PARITY.md +61 -0
  6. package/docs/changelog.md +149 -0
  7. package/docs/guidelines/AGENTS.md +32 -0
  8. package/docs/guidelines/DESIGN.md +55 -0
  9. package/docs/shortkeys.md +27 -0
  10. package/docs/todo.md +4 -0
  11. package/examples/api/generate-types.js +52 -0
  12. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  13. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  19. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  21. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  22. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  24. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  25. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  27. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  28. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
  29. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  35. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  36. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  37. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  39. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  40. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  41. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  42. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  43. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  44. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  45. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  46. package/frontend/js/api.js +63 -0
  47. package/frontend/js/app.js +310 -10
  48. package/frontend/js/components/connectionCard.js +4 -2
  49. package/frontend/js/components/emptyState.js +4 -4
  50. package/frontend/js/components/generateTypesDropdown.js +33 -0
  51. package/frontend/js/components/metricCard.js +1 -1
  52. package/frontend/js/components/modal.js +555 -21
  53. package/frontend/js/components/pageHeader.js +1 -1
  54. package/frontend/js/components/queryHistoryDetail.js +3 -3
  55. package/frontend/js/components/queryHistoryHeader.js +1 -1
  56. package/frontend/js/components/queryHistoryPanel.js +1 -1
  57. package/frontend/js/components/rowEditorPanel.js +13 -3
  58. package/frontend/js/components/sidebar.js +41 -7
  59. package/frontend/js/components/topNav.js +2 -14
  60. package/frontend/js/router.js +38 -6
  61. package/frontend/js/store.js +870 -8
  62. package/frontend/js/utils/inputClear.js +36 -0
  63. package/frontend/js/utils/syntheticData.js +240 -0
  64. package/frontend/js/views/backups.js +581 -37
  65. package/frontend/js/views/charts.js +9 -9
  66. package/frontend/js/views/connections.js +2 -2
  67. package/frontend/js/views/data.js +17 -1
  68. package/frontend/js/views/documents.js +3 -3
  69. package/frontend/js/views/editor.js +6 -6
  70. package/frontend/js/views/logs.js +339 -0
  71. package/frontend/js/views/mediaTagging.js +7 -5
  72. package/frontend/js/views/overview.js +3 -3
  73. package/frontend/js/views/settings.js +80 -30
  74. package/frontend/js/views/structure.js +12 -1
  75. package/frontend/js/views/tableAdvisor.js +385 -0
  76. package/frontend/js/views/tableDesigner.js +6 -0
  77. package/frontend/styles/base.css +1 -40
  78. package/frontend/styles/components.css +258 -232
  79. package/frontend/styles/structure-graph.css +32 -68
  80. package/frontend/styles/tailwind.css +8 -4
  81. package/frontend/styles/tailwind.generated.css +1 -1
  82. package/frontend/styles/tokens.css +28 -21
  83. package/frontend/styles/views.css +102 -261
  84. package/package.json +18 -2
  85. package/server/routes/backups.js +18 -2
  86. package/server/routes/data.js +45 -0
  87. package/server/routes/externalApi.js +321 -2
  88. package/server/routes/logs.js +127 -0
  89. package/server/routes/structure.js +22 -0
  90. package/server/server.js +3 -0
  91. package/server/services/databaseCommandService.js +59 -0
  92. package/server/services/sqlite/backupDiff.js +914 -0
  93. package/server/services/sqlite/backupService.js +77 -1
  94. package/server/services/sqlite/dataBrowserService.js +36 -0
  95. package/server/services/sqlite/introspection.js +226 -22
  96. package/server/services/sqlite/structureService.js +7 -0
  97. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  98. package/server/services/sqlite/tableAdvisor.js +790 -0
  99. package/server/services/storage/appStateStore.js +773 -169
  100. package/server/services/typeGenerationService.js +663 -0
  101. package/tailwind.config.cjs +0 -1
  102. package/.github/funding.yml +0 -2
  103. package/.github/workflows/ci.yml +0 -36
  104. package/database.sqlite +0 -0
  105. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  106. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  107. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  108. package/frontend/assets/mockups/connections_1200.webp +0 -0
  109. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  110. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  111. package/frontend/assets/mockups/documents_1200.webp +0 -0
  112. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  113. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  114. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  115. package/frontend/assets/mockups/overview_1200.webp +0 -0
  116. package/frontend/assets/mockups/settings_1200.webp +0 -0
  117. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  118. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  119. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  120. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  121. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  122. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  123. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  124. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  125. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  126. package/tests/api-token-auth.test.js +0 -279
  127. package/tests/backup-manager.test.js +0 -140
  128. package/tests/backups-view.test.js +0 -70
  129. package/tests/charts-height-preset-storage.test.js +0 -60
  130. package/tests/charts-route-state.test.js +0 -144
  131. package/tests/check-constraint-options.test.js +0 -90
  132. package/tests/cli-args.test.js +0 -113
  133. package/tests/cli-service-delegation.test.js +0 -140
  134. package/tests/connection-removal.test.js +0 -52
  135. package/tests/connections-file-dialog-route.test.js +0 -89
  136. package/tests/copy-column-modal.test.js +0 -131
  137. package/tests/database-command-service.test.js +0 -174
  138. package/tests/database-documents.test.js +0 -85
  139. package/tests/documents-view.test.js +0 -132
  140. package/tests/dropdown-button.test.js +0 -101
  141. package/tests/email-preview.test.js +0 -89
  142. package/tests/export-blob.test.js +0 -99
  143. package/tests/export-filenames.test.js +0 -38
  144. package/tests/file-path-preview.test.js +0 -165
  145. package/tests/form-controls.test.js +0 -34
  146. package/tests/json-preview.test.js +0 -49
  147. package/tests/local-request-security.test.js +0 -85
  148. package/tests/markdown-documents.test.js +0 -103
  149. package/tests/native-file-dialog.test.js +0 -105
  150. package/tests/query-editor.test.js +0 -28
  151. package/tests/query-history-detail.test.js +0 -37
  152. package/tests/query-history-header.test.js +0 -30
  153. package/tests/query-results-truncation.test.js +0 -20
  154. package/tests/risky-sql.test.js +0 -30
  155. package/tests/row-editor-json.test.js +0 -82
  156. package/tests/row-editor-null-values.test.js +0 -184
  157. package/tests/row-editor-timestamp-preview.test.js +0 -192
  158. package/tests/security-paths.test.js +0 -84
  159. package/tests/settings-api-tokens-route.test.js +0 -97
  160. package/tests/settings-metadata.test.js +0 -114
  161. package/tests/settings-view.test.js +0 -108
  162. package/tests/sql-formatter.test.js +0 -173
  163. package/tests/sql-highlight.test.js +0 -38
  164. package/tests/sql-identifier-safety.test.js +0 -171
  165. package/tests/sql-result-limit.test.js +0 -66
  166. package/tests/structure-view.test.js +0 -60
  167. package/tests/table-designer-v2-unique-constraints.test.js +0 -78
  168. package/tests/table-scroll-state.test.js +0 -70
  169. package/tests/text-cell-stats.test.js +0 -38
package/package.json CHANGED
@@ -1,12 +1,26 @@
1
1
  {
2
2
  "name": "sqlite-hub",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "description": "SQLite-only local management app backend and SPA shell",
5
5
  "license": "MIT",
6
6
  "main": "server/server.js",
7
7
  "bin": {
8
8
  "sqlite-hub": "bin/sqlite-hub.js"
9
9
  },
10
+ "files": [
11
+ "bin/",
12
+ "docs/",
13
+ "examples/",
14
+ "frontend/",
15
+ "server/data/.gitkeep",
16
+ "server/data/db_logos/.gitkeep",
17
+ "server/middleware/",
18
+ "server/routes/",
19
+ "server/server.js",
20
+ "server/services/",
21
+ "server/utils/",
22
+ "tailwind.config.cjs"
23
+ ],
10
24
  "scripts": {
11
25
  "build": "npm run build:css",
12
26
  "build:css": "tailwindcss -i ./frontend/styles/tailwind.css -o ./frontend/styles/tailwind.generated.css --minify",
@@ -15,7 +29,9 @@
15
29
  "start": "node bin/sqlite-hub.js",
16
30
  "dev": "node --watch server/server.js --port:4180",
17
31
  "prepack": "npm run build:css",
18
- "publish": "bash scripts/publish_brew.sh && sleep 10 && npm login --auth-type=web && bash scripts/publish_npm.sh"
32
+ "publish": "bash scripts/publish_brew.sh && sleep 10 && bash scripts/publish_npm.sh",
33
+ "screenshots": "node scripts/screenshot_sqlite_hub.js --width:1920 --height:1280",
34
+ "screenshots:backup-drawer": "node scripts/screenshot_backup_drawer.js"
19
35
  },
20
36
  "dependencies": {
21
37
  "better-sqlite3": "^12.10.1",
@@ -27,6 +27,19 @@ function createBackupsRouter({ backupService }) {
27
27
  })
28
28
  );
29
29
 
30
+ router.get(
31
+ "/:backupId/diff",
32
+ route((req, res) => {
33
+ res.json(
34
+ successResponse({
35
+ data: backupService.diffBackupWithCurrent(req.params.backupId, {
36
+ sampleLimit: req.query.sampleLimit,
37
+ }),
38
+ })
39
+ );
40
+ })
41
+ );
42
+
30
43
  router.post(
31
44
  "/",
32
45
  route(async (req, res) => {
@@ -49,11 +62,14 @@ function createBackupsRouter({ backupService }) {
49
62
  router.patch(
50
63
  "/:backupId",
51
64
  route((req, res) => {
52
- const backup = backupService.updateBackupNotes(req.params.backupId, req.body?.notes ?? "");
65
+ const backup = backupService.updateBackupDetails(req.params.backupId, {
66
+ name: req.body?.name,
67
+ notes: req.body?.notes,
68
+ });
53
69
 
54
70
  res.json(
55
71
  successResponse({
56
- message: "Backup notes updated.",
72
+ message: "Backup updated.",
57
73
  data: backup,
58
74
  })
59
75
  );
@@ -20,6 +20,21 @@ function createDataRouter({ dataBrowserService }) {
20
20
  })
21
21
  );
22
22
 
23
+ router.get(
24
+ "/:tableName/advisor",
25
+ route((req, res) => {
26
+ const data = dataBrowserService.analyzeTable(req.params.tableName);
27
+
28
+ res.json(
29
+ successResponse({
30
+ message: "Table analysis complete.",
31
+ data,
32
+ readOnly: true,
33
+ })
34
+ );
35
+ })
36
+ );
37
+
23
38
  router.get(
24
39
  "/:tableName",
25
40
  route((req, res) => {
@@ -56,6 +71,36 @@ function createDataRouter({ dataBrowserService }) {
56
71
  })
57
72
  );
58
73
 
74
+ router.post(
75
+ "/:tableName/generate/preview",
76
+ route((req, res) => {
77
+ const data = dataBrowserService.previewSyntheticRows(req.params.tableName, req.body ?? {});
78
+
79
+ res.json(
80
+ successResponse({
81
+ message: "Synthetic data preview generated.",
82
+ data,
83
+ readOnly: false,
84
+ })
85
+ );
86
+ })
87
+ );
88
+
89
+ router.post(
90
+ "/:tableName/generate/insert",
91
+ route((req, res) => {
92
+ const data = dataBrowserService.insertSyntheticRows(req.params.tableName, req.body ?? {});
93
+
94
+ res.json(
95
+ successResponse({
96
+ message: `Generated ${data.insertedRowCount} rows for ${data.tableName}.`,
97
+ data,
98
+ readOnly: false,
99
+ })
100
+ );
101
+ })
102
+ );
103
+
59
104
  router.patch(
60
105
  "/:tableName/rows",
61
106
  route((req, res) => {
@@ -29,6 +29,232 @@ function readStoreName(req) {
29
29
  ).trim();
30
30
  }
31
31
 
32
+ function decodePathPart(value = "") {
33
+ try {
34
+ return decodeURIComponent(value);
35
+ } catch {
36
+ return value;
37
+ }
38
+ }
39
+
40
+ function readPathParts(req) {
41
+ return String(req.path ?? "")
42
+ .split("/")
43
+ .map((part) => part.trim())
44
+ .filter(Boolean)
45
+ .map(decodePathPart);
46
+ }
47
+
48
+ function buildExternalApiAccessDescriptor(req) {
49
+ const method = String(req.method ?? "GET").toUpperCase();
50
+ const parts = readPathParts(req);
51
+
52
+ if (parts[0] === "info") {
53
+ return {
54
+ action: "api.info.get",
55
+ targetType: "app",
56
+ targetName: "info",
57
+ };
58
+ }
59
+
60
+ if (parts[0] === "query") {
61
+ return {
62
+ action: "api.query.execute",
63
+ databaseKey: readDatabaseId(req),
64
+ targetType: "query",
65
+ targetName: readStoreName(req) || "raw query",
66
+ };
67
+ }
68
+
69
+ if (parts[0] !== "databases") {
70
+ return {
71
+ action: `api.${method.toLowerCase()}`,
72
+ targetType: "request",
73
+ targetName: req.path,
74
+ };
75
+ }
76
+
77
+ const databaseKey = parts[1] ?? "";
78
+ const collection = parts[2] ?? "";
79
+ const itemName = parts[3] ?? "";
80
+ const subAction = parts[4] ?? "";
81
+
82
+ if (!collection) {
83
+ return {
84
+ action: "api.database.get",
85
+ databaseKey,
86
+ targetType: "database",
87
+ targetName: databaseKey,
88
+ };
89
+ }
90
+
91
+ if (collection === "tables") {
92
+ if (!itemName) {
93
+ return {
94
+ action: "api.tables.list",
95
+ databaseKey,
96
+ targetType: "database",
97
+ targetName: databaseKey,
98
+ };
99
+ }
100
+
101
+ if (subAction === "row") {
102
+ return {
103
+ action: "api.table.row.export",
104
+ databaseKey,
105
+ targetType: "table",
106
+ targetName: itemName,
107
+ };
108
+ }
109
+
110
+ if (subAction === "types") {
111
+ return {
112
+ action: "api.table.types.generate",
113
+ databaseKey,
114
+ targetType: "table",
115
+ targetName: itemName,
116
+ };
117
+ }
118
+
119
+ return {
120
+ action: "api.table.get",
121
+ databaseKey,
122
+ targetType: "table",
123
+ targetName: itemName,
124
+ };
125
+ }
126
+
127
+ if (collection === "backups") {
128
+ return {
129
+ action: method === "GET" ? "api.backups.list" : "api.backup.create",
130
+ databaseKey,
131
+ targetType: "database",
132
+ targetName: databaseKey,
133
+ };
134
+ }
135
+
136
+ if (collection === "queries") {
137
+ if (!itemName) {
138
+ return {
139
+ action: "api.queries.list",
140
+ databaseKey,
141
+ targetType: "database",
142
+ targetName: databaseKey,
143
+ };
144
+ }
145
+
146
+ if (subAction === "execute") {
147
+ return {
148
+ action: "api.query.execute.saved",
149
+ databaseKey,
150
+ targetType: "query",
151
+ targetName: itemName,
152
+ };
153
+ }
154
+
155
+ if (subAction === "export") {
156
+ return {
157
+ action: "api.query.export",
158
+ databaseKey,
159
+ targetType: "query",
160
+ targetName: itemName,
161
+ };
162
+ }
163
+
164
+ if (subAction === "notes") {
165
+ return {
166
+ action: "api.query.notes.get",
167
+ databaseKey,
168
+ targetType: "query",
169
+ targetName: itemName,
170
+ };
171
+ }
172
+
173
+ return {
174
+ action: "api.query.get",
175
+ databaseKey,
176
+ targetType: "query",
177
+ targetName: itemName,
178
+ };
179
+ }
180
+
181
+ if (collection === "documents") {
182
+ if (!itemName) {
183
+ return {
184
+ action: "api.documents.list",
185
+ databaseKey,
186
+ targetType: "database",
187
+ targetName: databaseKey,
188
+ };
189
+ }
190
+
191
+ if (subAction === "export") {
192
+ return {
193
+ action: "api.document.export",
194
+ databaseKey,
195
+ targetType: "document",
196
+ targetName: itemName,
197
+ };
198
+ }
199
+
200
+ return {
201
+ action: "api.document.get",
202
+ databaseKey,
203
+ targetType: "document",
204
+ targetName: itemName,
205
+ };
206
+ }
207
+
208
+ return {
209
+ action: `api.${collection}.${method.toLowerCase()}`,
210
+ databaseKey,
211
+ targetType: collection,
212
+ targetName: itemName || databaseKey,
213
+ };
214
+ }
215
+
216
+ function createExternalApiAccessLogger(appStateStore) {
217
+ return (req, res, next) => {
218
+ const startedAtMs = Date.now();
219
+ const startedAt = new Date(startedAtMs).toISOString();
220
+
221
+ res.on("finish", () => {
222
+ if (!appStateStore?.recordAccessLog) {
223
+ return;
224
+ }
225
+
226
+ const descriptor = buildExternalApiAccessDescriptor(req);
227
+ const failed = res.statusCode >= 400;
228
+
229
+ try {
230
+ appStateStore.recordAccessLog({
231
+ source: "api",
232
+ action: descriptor.action,
233
+ databaseKey: descriptor.databaseKey,
234
+ targetType: descriptor.targetType,
235
+ targetName: descriptor.targetName,
236
+ status: failed ? "error" : "success",
237
+ startedAt,
238
+ durationMs: Date.now() - startedAtMs,
239
+ errorMessage: failed ? req.accessLogError || `HTTP ${res.statusCode}` : null,
240
+ metadata: {
241
+ method: req.method,
242
+ path: req.path,
243
+ route: req.route?.path ? String(req.route.path) : null,
244
+ statusCode: res.statusCode,
245
+ apiTokenId: req.apiToken?.id ?? null,
246
+ apiTokenName: req.apiToken?.name ?? null,
247
+ },
248
+ });
249
+ } catch {
250
+ // Access logging must not change API behavior.
251
+ }
252
+ });
253
+
254
+ next();
255
+ };
256
+ }
257
+
32
258
  function authenticateDatabaseRequest(req, tokenService, databaseId) {
33
259
  const token = readBearerToken(req.get("authorization"));
34
260
 
@@ -38,12 +264,24 @@ function authenticateDatabaseRequest(req, tokenService, databaseId) {
38
264
  });
39
265
  }
40
266
 
41
- return tokenService.authenticate(databaseId, token);
267
+ const apiToken = tokenService.authenticate(databaseId, token);
268
+
269
+ req.apiToken = apiToken;
270
+ return apiToken;
42
271
  }
43
272
 
44
- function createExternalApiRouter({ databaseService, tokenService, appInfoService = buildAppInfo }) {
273
+ function createExternalApiRouter({
274
+ databaseService,
275
+ tokenService,
276
+ appStateStore = null,
277
+ appInfoService = buildAppInfo,
278
+ }) {
45
279
  const router = express.Router();
46
280
 
281
+ if (appStateStore?.recordAccessLog) {
282
+ router.use(createExternalApiAccessLogger(appStateStore));
283
+ }
284
+
47
285
  router.get(
48
286
  "/info",
49
287
  route(async (req, res) => {
@@ -131,6 +369,46 @@ function createExternalApiRouter({ databaseService, tokenService, appInfoService
131
369
  })
132
370
  );
133
371
 
372
+ router.get(
373
+ "/databases/:databaseId/backups",
374
+ route((req, res) => {
375
+ const backups = databaseService.listBackups(req.params.databaseId);
376
+
377
+ res.json(
378
+ successResponse({
379
+ data: {
380
+ items: backups,
381
+ },
382
+ metadata: {
383
+ databaseId: req.params.databaseId,
384
+ total: backups.length,
385
+ },
386
+ })
387
+ );
388
+ })
389
+ );
390
+
391
+ router.post(
392
+ "/databases/:databaseId/backups",
393
+ route(async (req, res) => {
394
+ const backup = await databaseService.createBackup(req.params.databaseId, {
395
+ name: req.body?.name,
396
+ notes: req.body?.notes,
397
+ context: req.body?.context ?? "api",
398
+ });
399
+
400
+ res.json(
401
+ successResponse({
402
+ message: "Backup created and verified.",
403
+ data: backup,
404
+ metadata: {
405
+ databaseId: req.params.databaseId,
406
+ },
407
+ })
408
+ );
409
+ })
410
+ );
411
+
134
412
  router.post(
135
413
  "/databases/:databaseId/tables/:tableName/row",
136
414
  route((req, res) => {
@@ -154,6 +432,42 @@ function createExternalApiRouter({ databaseService, tokenService, appInfoService
154
432
  })
155
433
  );
156
434
 
435
+ router.post(
436
+ "/databases/:databaseId/tables/:tableName/types",
437
+ route((req, res) => {
438
+ const target = String(req.body?.target ?? "").trim();
439
+
440
+ if (!["typescript", "rust", "kotlin", "swift"].includes(target)) {
441
+ throw new ValidationError(
442
+ `Unsupported type target "${req.body?.target}". Supported targets: typescript, rust, kotlin, swift.`,
443
+ { code: "INVALID_TYPE_TARGET" }
444
+ );
445
+ }
446
+
447
+ const result = databaseService.generateTableTypes(
448
+ req.params.databaseId,
449
+ req.params.tableName,
450
+ target,
451
+ req.body?.options ?? {}
452
+ );
453
+ const { warnings, metadata, ...data } = result;
454
+
455
+ res.json(
456
+ successResponse({
457
+ message: "Types generated.",
458
+ data,
459
+ metadata: {
460
+ databaseId: req.params.databaseId,
461
+ tableName: req.params.tableName,
462
+ ...metadata,
463
+ },
464
+ warnings,
465
+ readOnly: true,
466
+ })
467
+ );
468
+ })
469
+ );
470
+
157
471
  router.get(
158
472
  "/databases/:databaseId/queries",
159
473
  route((req, res) => {
@@ -300,6 +614,11 @@ function createExternalApiRouter({ databaseService, tokenService, appInfoService
300
614
  })
301
615
  );
302
616
 
617
+ router.use((error, req, res, next) => {
618
+ req.accessLogError = error?.message ?? null;
619
+ next(error);
620
+ });
621
+
303
622
  return router;
304
623
  }
305
624
 
@@ -0,0 +1,127 @@
1
+ const express = require("express");
2
+ const { DatabaseRequiredError, route, successResponse } = require("../utils/errors");
3
+
4
+ const RANGE_MS = {
5
+ "1h": 60 * 60 * 1000,
6
+ "24h": 24 * 60 * 60 * 1000,
7
+ "7d": 7 * 24 * 60 * 60 * 1000,
8
+ "30d": 30 * 24 * 60 * 60 * 1000,
9
+ };
10
+
11
+ function normalizeOption(value, allowedValues, fallback = "all") {
12
+ const normalized = String(value ?? fallback).trim().toLowerCase();
13
+ return allowedValues.includes(normalized) ? normalized : fallback;
14
+ }
15
+
16
+ function parseLimit(value) {
17
+ const numeric = Number(value);
18
+ return Number.isInteger(numeric) && numeric > 0 ? Math.min(numeric, 200) : 100;
19
+ }
20
+
21
+ function parseOffset(value) {
22
+ const numeric = Number(value);
23
+ return Number.isInteger(numeric) && numeric >= 0 ? numeric : 0;
24
+ }
25
+
26
+ function normalizeTimestamp(value) {
27
+ const normalized = String(value ?? "").trim();
28
+
29
+ if (!normalized) {
30
+ return null;
31
+ }
32
+
33
+ const timestamp = new Date(normalized).getTime();
34
+ return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
35
+ }
36
+
37
+ function resolveTimeWindow(query = {}, now = new Date()) {
38
+ const explicitFrom = normalizeTimestamp(query.from);
39
+ const explicitTo = normalizeTimestamp(query.to);
40
+ const range = normalizeOption(query.range, ["1h", "24h", "7d", "30d", "all"], "all");
41
+
42
+ if (explicitFrom || explicitTo) {
43
+ return {
44
+ range,
45
+ from: explicitFrom,
46
+ to: explicitTo,
47
+ };
48
+ }
49
+
50
+ if (range === "all") {
51
+ return {
52
+ range,
53
+ from: null,
54
+ to: null,
55
+ };
56
+ }
57
+
58
+ return {
59
+ range,
60
+ from: new Date(now.getTime() - RANGE_MS[range]).toISOString(),
61
+ to: now.toISOString(),
62
+ };
63
+ }
64
+
65
+ function resolveActiveDatabase(connectionManager) {
66
+ const activeDatabase = connectionManager.getActiveConnection() ?? null;
67
+
68
+ if (!activeDatabase?.id) {
69
+ throw new DatabaseRequiredError();
70
+ }
71
+
72
+ return activeDatabase;
73
+ }
74
+
75
+ function createLogsRouter({ appStateStore, connectionManager, now = () => new Date() }) {
76
+ const router = express.Router();
77
+
78
+ router.get(
79
+ "/",
80
+ route((req, res) => {
81
+ const timeWindow = resolveTimeWindow(req.query, now());
82
+ const activeDatabase = resolveActiveDatabase(connectionManager);
83
+ const kind = normalizeOption(req.query.kind, ["all", "query", "access"], "all");
84
+ const actor = normalizeOption(req.query.actor, ["all", "user", "cli", "api", "mcp"], "all");
85
+ const status = normalizeOption(req.query.status, ["all", "success", "error"], "all");
86
+ const queryType = normalizeOption(
87
+ req.query.queryType,
88
+ ["all", "select", "insert", "update", "delete", "pragma", "create", "alter", "drop", "other"],
89
+ "all"
90
+ );
91
+ const destructive = normalizeOption(req.query.destructive, ["all", "yes", "no"], "all");
92
+ const search = String(req.query.search ?? "").trim();
93
+ const result = appStateStore.listActivityLogs({
94
+ kind,
95
+ actor: actor === "all" ? null : actor,
96
+ status: status === "all" ? null : status,
97
+ databaseKey: activeDatabase.id,
98
+ queryType: queryType === "all" ? null : queryType,
99
+ destructive,
100
+ from: timeWindow.from,
101
+ to: timeWindow.to,
102
+ search,
103
+ limit: parseLimit(req.query.limit),
104
+ offset: parseOffset(req.query.offset),
105
+ });
106
+
107
+ res.json(
108
+ successResponse({
109
+ data: result,
110
+ metadata: {
111
+ ...result.filters,
112
+ range: timeWindow.range,
113
+ activeDatabase,
114
+ },
115
+ })
116
+ );
117
+ })
118
+ );
119
+
120
+ return router;
121
+ }
122
+
123
+ module.exports = {
124
+ createLogsRouter,
125
+ resolveActiveDatabase,
126
+ resolveTimeWindow,
127
+ };
@@ -30,6 +30,28 @@ function createStructureRouter({ structureService }) {
30
30
  })
31
31
  );
32
32
 
33
+ router.post(
34
+ "/:tableName/types",
35
+ route((req, res) => {
36
+ const result = structureService.generateTableTypes(
37
+ req.params.tableName,
38
+ req.body?.target,
39
+ req.body?.options ?? {}
40
+ );
41
+ const { warnings, metadata, ...data } = result;
42
+
43
+ res.json(
44
+ successResponse({
45
+ message: "Types generated.",
46
+ data,
47
+ metadata,
48
+ warnings,
49
+ readOnly: true,
50
+ })
51
+ );
52
+ })
53
+ );
54
+
33
55
  return router;
34
56
  }
35
57
 
package/server/server.js CHANGED
@@ -36,6 +36,7 @@ const { createSettingsRouter } = require("./routes/settings");
36
36
  const { createExportRouter } = require("./routes/export");
37
37
  const { createDocumentsRouter } = require("./routes/documents");
38
38
  const { createExternalApiRouter } = require("./routes/externalApi");
39
+ const { createLogsRouter } = require("./routes/logs");
39
40
 
40
41
  const PACKAGE_ROOT = path.resolve(__dirname, "..");
41
42
  const FRONTEND_ROOT = path.join(PACKAGE_ROOT, "frontend");
@@ -133,11 +134,13 @@ app.use(
133
134
  );
134
135
  app.use("/api/export", createExportRouter({ exportService }));
135
136
  app.use("/api/documents", createDocumentsRouter({ appStateStore, connectionManager }));
137
+ app.use("/api/logs", createLogsRouter({ appStateStore, connectionManager }));
136
138
  app.use(
137
139
  "/api/v1",
138
140
  createExternalApiRouter({
139
141
  databaseService: databaseCommandService,
140
142
  tokenService: apiTokenService,
143
+ appStateStore,
141
144
  })
142
145
  );
143
146