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,30 +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/utils/riskySql.js")).href);
8
- }
9
-
10
- test("risky SQL detector ignores selects and detects schema changes", async () => {
11
- const { detectRiskySqlOperations } = await loadModule();
12
-
13
- assert.deepEqual(detectRiskySqlOperations("SELECT * FROM companies"), []);
14
- assert.equal(detectRiskySqlOperations(" ALTER TABLE companies ADD COLUMN ticker TEXT")[0].type, "schema_change");
15
- });
16
-
17
- test("risky SQL detector handles comments, case, multiple statements, and drop table names", async () => {
18
- const { detectRiskySqlOperations } = await loadModule();
19
- const operations = detectRiskySqlOperations(`
20
- -- DROP TABLE ignored;
21
- select 1;
22
- /* comment */ drop table if exists "users";
23
- create index idx_users_name on users(name);
24
- `);
25
-
26
- assert.equal(operations.length, 2);
27
- assert.equal(operations[0].type, "drop_table");
28
- assert.equal(operations[0].tableName, "users");
29
- assert.equal(operations[1].type, "migration");
30
- });
@@ -1,82 +0,0 @@
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 rowEditorJsonModulePromise = null;
7
-
8
- function loadRowEditorJsonModule() {
9
- if (!rowEditorJsonModulePromise) {
10
- const source = readFileSync(
11
- path.resolve(__dirname, "../frontend/js/utils/rowEditorJson.js"),
12
- "utf8"
13
- );
14
- const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
15
-
16
- rowEditorJsonModulePromise = import(url);
17
- }
18
-
19
- return rowEditorJsonModulePromise;
20
- }
21
-
22
- test("row editor JSON for data rows preserves column order and omits identity metadata", async () => {
23
- const { buildDataRowEditorJsonObject, stringifyRowEditorJson } = await loadRowEditorJsonModule();
24
- const row = {
25
- id: 1,
26
- title: "Event",
27
- created_at: 1717682400,
28
- payload: { __type: "blob", sizeBytes: 4, hexPreview: "ffff" },
29
- __identity: { kind: "primaryKey", values: { id: 1 } },
30
- };
31
- const rowObject = buildDataRowEditorJsonObject({
32
- row,
33
- columns: ["id", "title", "created_at", "payload"],
34
- });
35
-
36
- assert.deepEqual(Object.keys(rowObject), ["id", "title", "created_at", "payload"]);
37
- assert.deepEqual(rowObject, {
38
- id: 1,
39
- title: "Event",
40
- created_at: 1717682400,
41
- payload: { __type: "blob", sizeBytes: 4, hexPreview: "ffff" },
42
- });
43
- assert.equal(
44
- stringifyRowEditorJson(rowObject),
45
- [
46
- "{",
47
- ' "id": 1,',
48
- ' "title": "Event",',
49
- ' "created_at": 1717682400,',
50
- ' "payload": {',
51
- ' "__type": "blob",',
52
- ' "sizeBytes": 4,',
53
- ' "hexPreview": "ffff"',
54
- " }",
55
- "}",
56
- ].join("\n")
57
- );
58
- });
59
-
60
- test("row editor JSON for SQL result rows maps visible source columns once", async () => {
61
- const { buildEditorRowEditorJsonObject } = await loadRowEditorJsonModule();
62
- const rowObject = buildEditorRowEditorJsonObject({
63
- row: {
64
- id: 1,
65
- title_alias: "Event",
66
- duplicate_title: "Ignored",
67
- hidden_note: "Hidden",
68
- __identity: { kind: "primaryKey", values: { id: 1 } },
69
- },
70
- editingColumns: [
71
- { resultName: "id", sourceColumn: "id", visible: true },
72
- { resultName: "title_alias", sourceColumn: "title", visible: true },
73
- { resultName: "duplicate_title", sourceColumn: "title", visible: true },
74
- { resultName: "hidden_note", sourceColumn: "note", visible: false },
75
- ],
76
- });
77
-
78
- assert.deepEqual(rowObject, {
79
- id: 1,
80
- title: "Event",
81
- });
82
- });
@@ -1,155 +0,0 @@
1
- const Database = require("better-sqlite3");
2
- const assert = require("node:assert/strict");
3
- const path = require("node:path");
4
- const test = require("node:test");
5
- const { pathToFileURL } = require("node:url");
6
- const { DataBrowserService } = require("../server/services/sqlite/dataBrowserService");
7
-
8
- async function loadFrontendModule(relativePath) {
9
- return import(pathToFileURL(path.resolve(__dirname, relativePath)).href);
10
- }
11
-
12
- test("row editor values preserve NULL and empty string as distinct values", async () => {
13
- const {
14
- buildRowEditorSubmittedValues,
15
- getRowEditorValueState,
16
- getRowEditorValueStateLabel,
17
- } = await loadFrontendModule("../frontend/js/utils/rowEditorValues.js");
18
- const formData = new FormData();
19
-
20
- formData.append("field:title", "");
21
- formData.append("field:description", "");
22
- formData.append("field:count", "42");
23
-
24
- assert.deepEqual(buildRowEditorSubmittedValues(formData, {
25
- title: { initialState: "empty", dirty: false },
26
- description: { initialState: "null", dirty: false },
27
- }), {
28
- title: "",
29
- description: null,
30
- count: "42",
31
- });
32
- assert.equal(
33
- buildRowEditorSubmittedValues(formData, {
34
- description: { initialState: "null", dirty: true },
35
- }).description,
36
- ""
37
- );
38
- assert.equal(getRowEditorValueState(null), "null");
39
- assert.equal(getRowEditorValueState(""), "empty");
40
- assert.equal(getRowEditorValueState("text"), "value");
41
- assert.equal(getRowEditorValueStateLabel("null"), "NULL");
42
- assert.equal(getRowEditorValueStateLabel("empty"), "EMPTY STRING");
43
- });
44
-
45
- test("row editor renders visible NULL and empty-string states", async () => {
46
- const { renderRowEditorPanel } = await loadFrontendModule(
47
- "../frontend/js/components/rowEditorPanel.js"
48
- );
49
- const html = renderRowEditorPanel({
50
- title: "Values",
51
- closeAction: "close",
52
- formName: "save-data-row",
53
- editableFields: [
54
- {
55
- name: "nullable_text",
56
- label: "nullable_text",
57
- rawValue: null,
58
- value: "",
59
- notNull: false,
60
- },
61
- {
62
- name: "empty_text",
63
- label: "empty_text",
64
- rawValue: "",
65
- value: "",
66
- notNull: false,
67
- },
68
- {
69
- name: "required_text",
70
- label: "required_text",
71
- rawValue: "value",
72
- value: "value",
73
- notNull: true,
74
- },
75
- ],
76
- });
77
-
78
- assert.match(html, /data-value-state="null"[^>]*>NULL</s);
79
- assert.match(html, /data-value-state="empty"[^>]*>EMPTY STRING</s);
80
- assert.match(html, /data-value-state="value"[^>]*>VALUE</s);
81
- assert.doesNotMatch(html, /field-null:|row-editor-null-toggle|Set NULL/);
82
- assert.doesNotMatch(html, /name="field:nullable_text"[\s\S]*?disabled/);
83
- });
84
-
85
- test("row editor delete button renders a trash icon", async () => {
86
- const { renderRowEditorPanel } = await loadFrontendModule(
87
- "../frontend/js/components/rowEditorPanel.js"
88
- );
89
- const html = renderRowEditorPanel({
90
- title: "Values",
91
- closeAction: "close",
92
- formName: "save-data-row",
93
- deleteAction: "delete-data-row",
94
- deleteEnabled: true,
95
- deleteRowIndex: 0,
96
- editableFields: [
97
- {
98
- name: "title",
99
- label: "title",
100
- value: "Acme",
101
- },
102
- ],
103
- });
104
-
105
- assert.match(html, /class="delete-button"[^>]*data-action="delete-data-row"/);
106
- assert.match(html, /<span class="material-symbols-outlined text-sm">delete<\/span>\s*Delete Row/);
107
- });
108
-
109
- test("data updates can change NULL to empty string and empty string to NULL", () => {
110
- const db = new Database(":memory:");
111
-
112
- try {
113
- db.exec(`
114
- CREATE TABLE values_test (
115
- id INTEGER PRIMARY KEY,
116
- nullable_text TEXT,
117
- empty_text TEXT
118
- );
119
- INSERT INTO values_test (id, nullable_text, empty_text) VALUES (1, NULL, '');
120
- `);
121
- const service = new DataBrowserService({
122
- connectionManager: {
123
- assertWritable() {},
124
- getActiveDatabase: () => db,
125
- },
126
- });
127
- const identity = service.getTableData("values_test", { limit: 10 }).rows[0].__identity;
128
- const preview = service.previewTableRowUpdate("values_test", {
129
- identity,
130
- values: {
131
- nullable_text: "",
132
- empty_text: null,
133
- },
134
- });
135
-
136
- assert.deepEqual(preview.changes, [
137
- { column: "nullable_text", oldValue: "NULL", newValue: "" },
138
- { column: "empty_text", oldValue: "", newValue: "NULL" },
139
- ]);
140
-
141
- service.updateTableRow("values_test", {
142
- identity,
143
- values: {
144
- nullable_text: "",
145
- empty_text: null,
146
- },
147
- });
148
- const row = db.prepare("SELECT nullable_text, empty_text FROM values_test WHERE id = 1").get();
149
-
150
- assert.equal(row.nullable_text, "");
151
- assert.equal(row.empty_text, null);
152
- } finally {
153
- db.close();
154
- }
155
- });
@@ -1,192 +0,0 @@
1
- const Database = require("better-sqlite3");
2
- const assert = require("node:assert/strict");
3
- const { readFileSync } = require("node:fs");
4
- const path = require("node:path");
5
- const test = require("node:test");
6
- const { DataBrowserService } = require("../server/services/sqlite/dataBrowserService");
7
-
8
- let timestampPreviewModulePromise = null;
9
-
10
- function loadTimestampPreviewModule() {
11
- if (!timestampPreviewModulePromise) {
12
- const source = readFileSync(
13
- path.resolve(__dirname, "../frontend/js/utils/timestampPreview.js"),
14
- "utf8"
15
- );
16
- const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
17
-
18
- timestampPreviewModulePromise = import(url);
19
- }
20
-
21
- return timestampPreviewModulePromise;
22
- }
23
-
24
- function createTableMeta() {
25
- return {
26
- columns: [
27
- { name: "id", primaryKeyPosition: 1 },
28
- { name: "parent_id", primaryKeyPosition: 0 },
29
- { name: "created_at", primaryKeyPosition: 0 },
30
- { name: "updated_at", primaryKeyPosition: 0 },
31
- { name: "published_at", primaryKeyPosition: 0 },
32
- { name: "price", primaryKeyPosition: 0 },
33
- ],
34
- foreignKeys: [
35
- {
36
- mappings: [{ from: "parent_id", to: "id" }],
37
- },
38
- ],
39
- };
40
- }
41
-
42
- test("row editor timestamp preview protects primary and foreign keys", async () => {
43
- const {
44
- getTimestampPreviewForField,
45
- isProtectedKeyColumn,
46
- } = await loadTimestampPreviewModule();
47
- const tableMeta = createTableMeta();
48
-
49
- assert.equal(isProtectedKeyColumn("id", tableMeta), true);
50
- assert.equal(isProtectedKeyColumn("parent_id", tableMeta), true);
51
-
52
- assert.equal(
53
- getTimestampPreviewForField({
54
- columnName: "id",
55
- value: "1717682400",
56
- tableMeta,
57
- }).kind,
58
- "protected-key"
59
- );
60
- assert.equal(
61
- getTimestampPreviewForField({
62
- columnName: "parent_id",
63
- value: "1717682400",
64
- tableMeta,
65
- }).kind,
66
- "protected-key"
67
- );
68
- });
69
-
70
- test("row editor timestamp preview formats plausible non-key timestamp values", async () => {
71
- const { getTimestampPreviewForField } = await loadTimestampPreviewModule();
72
- const tableMeta = createTableMeta();
73
-
74
- const createdAtPreview = getTimestampPreviewForField({
75
- columnName: "created_at",
76
- value: "1717682400",
77
- tableMeta,
78
- });
79
- const updatedAtPreview = getTimestampPreviewForField({
80
- columnName: "updated_at",
81
- value: "1717682400000",
82
- tableMeta,
83
- });
84
- const pricePreview = getTimestampPreviewForField({
85
- columnName: "price",
86
- value: "1717682400",
87
- tableMeta,
88
- });
89
- const publishedAtPreview = getTimestampPreviewForField({
90
- columnName: "published_at",
91
- value: "1717682400000000",
92
- tableMeta,
93
- });
94
-
95
- assert.equal(createdAtPreview.kind, "timestamp");
96
- assert.equal(createdAtPreview.sourceFormat, "unix-seconds");
97
- assert.equal(updatedAtPreview.kind, "timestamp");
98
- assert.equal(updatedAtPreview.sourceFormat, "unix-milliseconds");
99
- assert.equal(publishedAtPreview.kind, "timestamp");
100
- assert.equal(publishedAtPreview.sourceFormat, "unix-microseconds");
101
- assert.equal(pricePreview.kind, "timestamp");
102
- assert.match(createdAtPreview.formatted, /^\d{2}\.\d{2}\.\d{4}, \d{2}:\d{2}:\d{2}$/);
103
- });
104
-
105
- test("row editor timestamp preview ignores ids and invalid timestamps", async () => {
106
- const { getTimestampPreviewForField } = await loadTimestampPreviewModule();
107
- const tableMeta = createTableMeta();
108
-
109
- assert.equal(
110
- getTimestampPreviewForField({
111
- columnName: "external_id",
112
- value: "1717682400",
113
- tableMeta,
114
- }).kind,
115
- "none"
116
- );
117
- assert.equal(
118
- getTimestampPreviewForField({
119
- columnName: "created_at",
120
- value: "42",
121
- tableMeta,
122
- }).kind,
123
- "none"
124
- );
125
- assert.equal(
126
- getTimestampPreviewForField({
127
- columnName: "created_at",
128
- value: "not a timestamp",
129
- tableMeta,
130
- }).kind,
131
- "none"
132
- );
133
- });
134
-
135
- test("row editor timestamp preview accepts ISO and SQLite datetime strings", async () => {
136
- const { getTimestampPreviewForField } = await loadTimestampPreviewModule();
137
- const tableMeta = createTableMeta();
138
-
139
- assert.equal(
140
- getTimestampPreviewForField({
141
- columnName: "created_at",
142
- value: "2026-06-06T18:49:00Z",
143
- tableMeta,
144
- }).kind,
145
- "timestamp"
146
- );
147
- assert.equal(
148
- getTimestampPreviewForField({
149
- columnName: "updated_at",
150
- value: "2026-06-06 18:49:00",
151
- tableMeta,
152
- }).kind,
153
- "timestamp"
154
- );
155
- });
156
-
157
- test("row update stores the submitted raw value without date conversion", () => {
158
- const db = new Database(":memory:");
159
-
160
- try {
161
- db.exec(`
162
- CREATE TABLE events (
163
- id INTEGER PRIMARY KEY,
164
- created_at TEXT
165
- );
166
- INSERT INTO events (id, created_at) VALUES (1, 'before');
167
- `);
168
-
169
- const service = new DataBrowserService({
170
- connectionManager: {
171
- assertWritable() {},
172
- getActiveDatabase: () => db,
173
- },
174
- });
175
- const tableData = service.getTableData("events", { limit: 10, offset: 0 });
176
- const identity = tableData.rows[0].__identity;
177
-
178
- service.updateTableRow("events", {
179
- identity,
180
- values: {
181
- created_at: "1717682400",
182
- },
183
- });
184
-
185
- assert.equal(
186
- db.prepare("SELECT created_at FROM events WHERE id = 1").get().created_at,
187
- "1717682400"
188
- );
189
- } finally {
190
- db.close();
191
- }
192
- });
@@ -1,84 +0,0 @@
1
- const fs = require("node:fs");
2
- const os = require("node:os");
3
- const path = require("node:path");
4
- const { pathToFileURL } = require("node:url");
5
- const assert = require("node:assert/strict");
6
- const test = require("node:test");
7
- const { ValidationError } = require("../server/utils/errors");
8
- const {
9
- resolvePathInsideDirectory,
10
- resolveUserPath,
11
- } = require("../server/utils/fileValidation");
12
- const { MediaTaggingService } = require("../server/services/sqlite/mediaTaggingService");
13
-
14
- function createTempDirectory(t) {
15
- const directoryPath = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-hub-security-"));
16
- t.after(() => fs.rmSync(directoryPath, { recursive: true, force: true }));
17
- return directoryPath;
18
- }
19
-
20
- test("resolveUserPath rejects parent traversal segments", () => {
21
- assert.throws(() => resolveUserPath("../outside.db"), ValidationError);
22
- assert.throws(() => resolveUserPath("~/../outside.db"), ValidationError);
23
- assert.throws(() => resolveUserPath("data/\0outside.db"), ValidationError);
24
- });
25
-
26
- test("resolvePathInsideDirectory resolves only paths under the supplied root", (t) => {
27
- const root = createTempDirectory(t);
28
- const insidePath = path.join(root, "media", "photo.jpg");
29
-
30
- assert.equal(
31
- resolvePathInsideDirectory(root, "media/photo.jpg", "Media file path"),
32
- insidePath
33
- );
34
- assert.equal(
35
- resolvePathInsideDirectory(root, insidePath, "Media file path"),
36
- insidePath
37
- );
38
- assert.throws(
39
- () =>
40
- resolvePathInsideDirectory(
41
- root,
42
- path.join(path.dirname(root), "secret.txt"),
43
- "Media file path"
44
- ),
45
- ValidationError
46
- );
47
- assert.throws(
48
- () => resolvePathInsideDirectory(root, "../secret.txt", "Media file path"),
49
- ValidationError
50
- );
51
- });
52
-
53
- test("media file resolution is scoped to the active database directory", (t) => {
54
- const root = createTempDirectory(t);
55
- const mediaDirectory = path.join(root, "media");
56
- const mediaPath = path.join(mediaDirectory, "photo.jpg");
57
-
58
- fs.mkdirSync(mediaDirectory, { recursive: true });
59
- fs.writeFileSync(mediaPath, "image");
60
-
61
- const service = new MediaTaggingService({
62
- connectionManager: {
63
- getActiveConnection: () => ({
64
- path: path.join(root, "database.sqlite"),
65
- }),
66
- },
67
- appStateStore: {},
68
- });
69
-
70
- assert.equal(service.resolveMediaFilePath("media/photo.jpg"), mediaPath);
71
- assert.equal(
72
- service.resolveMediaFilePath(pathToFileURL(mediaPath).toString()),
73
- mediaPath
74
- );
75
- assert.deepEqual(service.getMediaFileForRequest("media/photo.jpg"), {
76
- directory: mediaDirectory,
77
- fileName: "photo.jpg",
78
- });
79
- assert.throws(() => service.resolveMediaFilePath("../secret.jpg"), ValidationError);
80
- assert.throws(
81
- () => service.resolveMediaFilePath(path.join(path.dirname(root), "secret.jpg")),
82
- ValidationError
83
- );
84
- });
@@ -1,97 +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 { createSettingsRouter } = require("../server/routes/settings");
9
- const { ApiTokenService } = require("../server/services/apiTokenService");
10
- const { AppStateStore } = require("../server/services/storage/appStateStore");
11
- const { errorMiddleware } = require("../server/utils/errors");
12
-
13
- test("settings routes create and delete tokens for the active database", async (t) => {
14
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-hub-settings-token-"));
15
- const store = new AppStateStore(path.join(directory, "state.db"));
16
- const connection = {
17
- id: "db-active",
18
- label: "Active Database",
19
- path: path.join(directory, "active.db"),
20
- lastOpenedAt: new Date().toISOString(),
21
- lastModifiedAt: new Date().toISOString(),
22
- sizeBytes: 0,
23
- readOnly: false,
24
- logoPath: null,
25
- };
26
-
27
- store.upsertRecentConnection(connection);
28
-
29
- const tokenService = new ApiTokenService({ appStateStore: store });
30
- const connectionManager = {
31
- getActiveConnection: () => connection,
32
- };
33
- const app = express();
34
-
35
- app.use(express.json());
36
- app.use(
37
- "/api/settings",
38
- createSettingsRouter({
39
- appStateStore: store,
40
- connectionManager,
41
- tokenService,
42
- versionCheckService: async () => ({
43
- packageName: "sqlite-hub",
44
- currentVersion: "1.0.1",
45
- latestVersion: "1.1.0",
46
- updateAvailable: true,
47
- checkedAt: "2026-06-19T20:00:00.000Z",
48
- source: "npm",
49
- releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.1.0",
50
- }),
51
- })
52
- );
53
- app.use(errorMiddleware);
54
-
55
- const server = await new Promise((resolve) => {
56
- const listener = app.listen(0, "127.0.0.1", () => resolve(listener));
57
- });
58
-
59
- t.after(async () => {
60
- await new Promise((resolve, reject) => {
61
- server.close((error) => (error ? reject(error) : resolve()));
62
- });
63
- store.db.close();
64
- fs.rmSync(directory, { recursive: true, force: true });
65
- });
66
-
67
- const baseUrl = `http://127.0.0.1:${server.address().port}/api/settings`;
68
- const createResponse = await fetch(`${baseUrl}/api-tokens`, {
69
- method: "POST",
70
- headers: { "Content-Type": "application/json" },
71
- body: JSON.stringify({ name: "Settings token" }),
72
- });
73
- const created = await createResponse.json();
74
-
75
- assert.equal(createResponse.status, 201);
76
- assert.equal(created.data.databaseKey, connection.id);
77
- assert.match(created.data.token, /^shub_/);
78
- assert.equal(created.metadata.apiTokens.length, 1);
79
- assert.equal(created.metadata.apiTokens[0].token, undefined);
80
-
81
- const deleteResponse = await fetch(`${baseUrl}/api-tokens/${created.data.id}`, {
82
- method: "DELETE",
83
- });
84
- const deleted = await deleteResponse.json();
85
-
86
- assert.equal(deleteResponse.status, 200);
87
- assert.deepEqual(deleted.data, { id: created.data.id, deleted: true });
88
- assert.equal(deleted.metadata.apiTokens.length, 0);
89
-
90
- const versionResponse = await fetch(`${baseUrl}/version-check`);
91
- const versionCheck = await versionResponse.json();
92
-
93
- assert.equal(versionResponse.status, 200);
94
- assert.equal(versionCheck.data.latestVersion, "1.1.0");
95
- assert.equal(versionCheck.data.updateAvailable, true);
96
- assert.equal(versionCheck.metadata.appVersion, require("../package.json").version);
97
- });