sqlite-hub 0.17.0 → 1.0.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 (83) hide show
  1. package/README.md +101 -19
  2. package/bin/sqlite-hub.js +83 -401
  3. package/examples/api/queries.js +31 -0
  4. package/examples/api/rows.js +27 -0
  5. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  6. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  7. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  8. package/frontend/assets/mockups/connections_1200.webp +0 -0
  9. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  10. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  11. package/frontend/assets/mockups/documents_1200.webp +0 -0
  12. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  13. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  14. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  15. package/frontend/assets/mockups/overview_1200.webp +0 -0
  16. package/frontend/assets/mockups/settings_1200.webp +0 -0
  17. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  18. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  19. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  20. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  21. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  22. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  23. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  24. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  25. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  26. package/frontend/js/api.js +25 -0
  27. package/frontend/js/app.js +183 -26
  28. package/frontend/js/components/formControls.js +38 -0
  29. package/frontend/js/components/modal.js +79 -19
  30. package/frontend/js/components/queryResults.js +18 -1
  31. package/frontend/js/components/rowEditorPanel.js +42 -34
  32. package/frontend/js/store.js +129 -2
  33. package/frontend/js/utils/jsonPreview.js +31 -0
  34. package/frontend/js/utils/rowEditorValues.js +41 -0
  35. package/frontend/js/utils/tableScrollState.js +39 -0
  36. package/frontend/js/views/charts.js +8 -0
  37. package/frontend/js/views/data.js +4 -1
  38. package/frontend/js/views/editor.js +2 -0
  39. package/frontend/js/views/settings.js +141 -5
  40. package/frontend/styles/components.css +6 -0
  41. package/frontend/styles/tailwind.generated.css +2 -2
  42. package/package.json +6 -6
  43. package/server/middleware/apiTokenAuth.js +30 -0
  44. package/server/middleware/localRequestSecurity.js +84 -0
  45. package/server/routes/connections.js +35 -1
  46. package/server/routes/externalApi.js +222 -0
  47. package/server/routes/settings.js +64 -4
  48. package/server/server.js +34 -2
  49. package/server/services/apiTokenService.js +101 -0
  50. package/server/services/databaseCommandService.js +399 -0
  51. package/server/services/nativeFileDialogService.js +260 -0
  52. package/server/services/sqlite/dataBrowserService.js +0 -4
  53. package/server/services/sqlite/exportService.js +5 -1
  54. package/server/services/sqlite/sqlExecutor.js +51 -2
  55. package/server/services/storage/appStateStore.js +113 -0
  56. package/server/utils/errors.js +7 -0
  57. package/server/utils/sqliteTypes.js +17 -8
  58. package/tests/api-token-auth.test.js +127 -0
  59. package/tests/cli-service-delegation.test.js +43 -0
  60. package/tests/connections-file-dialog-route.test.js +89 -0
  61. package/tests/copy-column-modal.test.js +48 -0
  62. package/tests/database-command-service.test.js +102 -0
  63. package/tests/export-blob.test.js +46 -0
  64. package/tests/form-controls.test.js +34 -0
  65. package/tests/json-preview.test.js +49 -0
  66. package/tests/local-request-security.test.js +85 -0
  67. package/tests/native-file-dialog.test.js +105 -0
  68. package/tests/query-results-truncation.test.js +20 -0
  69. package/tests/row-editor-null-values.test.js +131 -0
  70. package/tests/settings-api-tokens-route.test.js +76 -0
  71. package/tests/settings-view.test.js +47 -0
  72. package/tests/sql-identifier-safety.test.js +9 -3
  73. package/tests/sql-result-limit.test.js +66 -0
  74. package/tests/table-scroll-state.test.js +70 -0
  75. package/frontend/assets/mockups/connections.png +0 -0
  76. package/frontend/assets/mockups/data.png +0 -0
  77. package/frontend/assets/mockups/data_row_editor.png +0 -0
  78. package/frontend/assets/mockups/home.png +0 -0
  79. package/frontend/assets/mockups/sql_editor.png +0 -0
  80. package/frontend/assets/mockups/sql_editor_croped.png +0 -0
  81. package/frontend/assets/mockups/sql_editor_querydetail.png +0 -0
  82. package/frontend/assets/mockups/structure.png +0 -0
  83. package/frontend/assets/mockups/structure_inspector.png +0 -0
@@ -29,4 +29,51 @@ test("settings view shows SQLite runtime version and custom port command", async
29
29
  assert.match(rendered.main, /SQLite_Runtime/);
30
30
  assert.match(rendered.main, /3\.53\.1/);
31
31
  assert.match(rendered.main, /sqlite-hub --port:PORT/);
32
+ assert.match(rendered.main, /data-section="information"/);
33
+ assert.match(rendered.main, /data-section="api-tokens"/);
34
+ assert.doesNotMatch(rendered.main, /data-form="create-api-token"/);
35
+ });
36
+
37
+ test("settings view scopes API token controls to the active database", async () => {
38
+ const { renderSettingsView } = await loadSettingsViewModule();
39
+ const rendered = renderSettingsView({
40
+ settings: {
41
+ loading: false,
42
+ error: null,
43
+ appVersion: "1.2.3",
44
+ sqliteVersion: "3.53.1",
45
+ section: "api-tokens",
46
+ tokenDatabase: { id: "db-one", label: "Database One" },
47
+ apiTokens: [
48
+ {
49
+ id: "token-one",
50
+ name: "Automation",
51
+ tokenPrefix: "shub_example",
52
+ createdAt: "2026-06-15T10:00:00.000Z",
53
+ },
54
+ ],
55
+ createdApiToken: null,
56
+ tokenSaving: false,
57
+ },
58
+ });
59
+
60
+ assert.match(rendered.main, /API Tokens/);
61
+ assert.match(rendered.main, /Security \/\/ Database API access/);
62
+ assert.match(rendered.main, /Database One/);
63
+ assert.match(rendered.main, /Database ID/);
64
+ assert.match(rendered.main, /data-database-id/);
65
+ assert.match(rendered.main, /data-action="copy-database-id"/);
66
+ assert.match(rendered.main, /Copy ID/);
67
+ assert.match(rendered.main, /sm:grid-cols-\[minmax\(0,1fr\)_9rem\]/);
68
+ assert.equal((rendered.main.match(/w-full self-center justify-center/g) ?? []).length, 2);
69
+ assert.match(rendered.main, /shub_example\.\.\./);
70
+ assert.match(rendered.main, /data-api-token-name/);
71
+ assert.match(rendered.main, /data-form="create-api-token"/);
72
+ assert.match(rendered.main, /name="name"/);
73
+ assert.match(rendered.main, /type="submit"/);
74
+ assert.match(rendered.main, /data-action="open-delete-api-token-modal"/);
75
+ assert.match(rendered.main, /bg-surface-container-lowest/);
76
+ assert.doesNotMatch(rendered.main, /sqlite-hub --port:PORT/);
77
+ assert.doesNotMatch(rendered.main, /Open Github/);
78
+ assert.doesNotMatch(rendered.main, /token_hash/);
32
79
  });
@@ -114,7 +114,8 @@ test("data browser mutations preserve quoted dynamic identifiers", () => {
114
114
  "UPDATE",
115
115
  quoteIdentifier(tableName),
116
116
  "SET",
117
- quoteIdentifier(valueColumn) + " = ?",
117
+ quoteIdentifier(valueColumn) + " = ?,",
118
+ quoteIdentifier(noteColumn) + " = ?",
118
119
  "WHERE",
119
120
  "rowid IS ?",
120
121
  ].join(" ")
@@ -125,6 +126,11 @@ test("data browser mutations preserve quoted dynamic identifiers", () => {
125
126
  oldValue: "before",
126
127
  newValue: "after",
127
128
  },
129
+ {
130
+ column: noteColumn,
131
+ oldValue: "NULL",
132
+ newValue: "",
133
+ },
128
134
  ]);
129
135
 
130
136
  const updated = service.updateTableRow(tableName, {
@@ -136,7 +142,7 @@ test("data browser mutations preserve quoted dynamic identifiers", () => {
136
142
  });
137
143
 
138
144
  assert.equal(updated.row[valueColumn], "after");
139
- assert.equal(updated.row[noteColumn], null);
145
+ assert.equal(updated.row[noteColumn], "");
140
146
  const persistedRow = db.prepare(
141
147
  [
142
148
  "SELECT",
@@ -148,7 +154,7 @@ test("data browser mutations preserve quoted dynamic identifiers", () => {
148
154
  ).get();
149
155
 
150
156
  assert.equal(persistedRow[valueColumn], "after");
151
- assert.equal(persistedRow[noteColumn], null);
157
+ assert.equal(persistedRow[noteColumn], "");
152
158
 
153
159
  const deleted = service.deleteTableRow(tableName, {
154
160
  identity: updated.row.__identity,
@@ -0,0 +1,66 @@
1
+ const Database = require("better-sqlite3");
2
+ const assert = require("node:assert/strict");
3
+ const test = require("node:test");
4
+ const {
5
+ DEFAULT_RESULT_ROW_LIMIT,
6
+ SqlExecutor,
7
+ } = require("../server/services/sqlite/sqlExecutor");
8
+
9
+ function createExecutor(db) {
10
+ return new SqlExecutor({
11
+ connectionManager: {
12
+ getActiveDatabase: () => db,
13
+ getActiveConnection: () => ({ id: "result-limit-test" }),
14
+ },
15
+ appStateStore: {
16
+ recordQueryExecution: () => 1,
17
+ },
18
+ });
19
+ }
20
+
21
+ test("interactive SQL results stop at the configured row limit", () => {
22
+ const db = new Database(":memory:");
23
+
24
+ try {
25
+ db.exec(`
26
+ CREATE TABLE items (id INTEGER PRIMARY KEY, label TEXT);
27
+ INSERT INTO items (label) VALUES ('one'), ('two'), ('three'), ('four'), ('five');
28
+ `);
29
+
30
+ const result = createExecutor(db).execute("SELECT id, label FROM items ORDER BY id", {
31
+ maxRows: 3,
32
+ });
33
+
34
+ assert.equal(DEFAULT_RESULT_ROW_LIMIT, 5000);
35
+ assert.equal(result.rows.length, 3);
36
+ assert.deepEqual(result.rows.map((row) => row.id), [1, 2, 3]);
37
+ assert.equal(result.truncated, true);
38
+ assert.equal(result.rowLimit, 3);
39
+ assert.equal(result.statements[0].truncated, true);
40
+ assert.equal(result.statements[0].rowCount, 3);
41
+ } finally {
42
+ db.close();
43
+ }
44
+ });
45
+
46
+ test("unlimited SQL execution remains available for export services", () => {
47
+ const db = new Database(":memory:");
48
+
49
+ try {
50
+ db.exec(`
51
+ CREATE TABLE items (id INTEGER PRIMARY KEY);
52
+ INSERT INTO items VALUES (1), (2), (3), (4), (5);
53
+ `);
54
+
55
+ const result = createExecutor(db).execute("SELECT id FROM items ORDER BY id", {
56
+ maxRows: null,
57
+ persistHistory: false,
58
+ });
59
+
60
+ assert.equal(result.rows.length, 5);
61
+ assert.equal(result.truncated, false);
62
+ assert.equal(result.rowLimit, null);
63
+ } finally {
64
+ db.close();
65
+ }
66
+ });
@@ -0,0 +1,70 @@
1
+ const assert = require("node:assert/strict");
2
+ const { readFileSync } = require("node:fs");
3
+ const path = require("node:path");
4
+ const test = require("node:test");
5
+
6
+ let tableScrollStateModulePromise = null;
7
+
8
+ function loadTableScrollStateModule() {
9
+ if (!tableScrollStateModulePromise) {
10
+ const source = readFileSync(
11
+ path.resolve(__dirname, "../frontend/js/utils/tableScrollState.js"),
12
+ "utf8"
13
+ );
14
+ const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
15
+
16
+ tableScrollStateModulePromise = import(url);
17
+ }
18
+
19
+ return tableScrollStateModulePromise;
20
+ }
21
+
22
+ test("table scroll state restores horizontal position for the matching grid", async () => {
23
+ const { captureTableHorizontalScrollState, restoreTableHorizontalScrollState } =
24
+ await loadTableScrollStateModule();
25
+ const key = 'data:events "archive"';
26
+ const previousGrid = { dataset: { tableScrollKey: key }, scrollLeft: 640 };
27
+ const nextGrid = { dataset: { tableScrollKey: key }, scrollLeft: 0 };
28
+ const snapshot = captureTableHorizontalScrollState({
29
+ routeName: "data",
30
+ scrollNodes: [previousGrid],
31
+ });
32
+
33
+ assert.equal(
34
+ restoreTableHorizontalScrollState({
35
+ snapshot,
36
+ routeName: "data",
37
+ scrollNodes: [nextGrid],
38
+ }),
39
+ true
40
+ );
41
+ assert.equal(nextGrid.scrollLeft, 640);
42
+ });
43
+
44
+ test("table scroll state is not restored across routes or different grids", async () => {
45
+ const { captureTableHorizontalScrollState, restoreTableHorizontalScrollState } =
46
+ await loadTableScrollStateModule();
47
+ const snapshot = captureTableHorizontalScrollState({
48
+ routeName: "editorResults",
49
+ scrollNodes: [{ dataset: { tableScrollKey: "editor:12" }, scrollLeft: 320 }],
50
+ });
51
+ const nextGrid = { dataset: { tableScrollKey: "editor:13" }, scrollLeft: 0 };
52
+
53
+ assert.equal(
54
+ restoreTableHorizontalScrollState({
55
+ snapshot,
56
+ routeName: "data",
57
+ scrollNodes: [nextGrid],
58
+ }),
59
+ false
60
+ );
61
+ assert.equal(
62
+ restoreTableHorizontalScrollState({
63
+ snapshot,
64
+ routeName: "editorResults",
65
+ scrollNodes: [nextGrid],
66
+ }),
67
+ false
68
+ );
69
+ assert.equal(nextGrid.scrollLeft, 0);
70
+ });
Binary file
Binary file