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,38 +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 exportFilenamesModulePromise = null;
7
-
8
- function loadExportFilenamesModule() {
9
- if (!exportFilenamesModulePromise) {
10
- const source = readFileSync(
11
- path.resolve(__dirname, "../frontend/js/utils/exportFilenames.js"),
12
- "utf8"
13
- );
14
- const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
15
-
16
- exportFilenamesModulePromise = import(url);
17
- }
18
-
19
- return exportFilenamesModulePromise;
20
- }
21
-
22
- test("text export filenames use the selected format extension", async () => {
23
- const { buildTextExportFilename } = await loadExportFilenamesModule();
24
-
25
- assert.equal(buildTextExportFilename("query-results.csv", { format: "tsv" }), "query-results.tsv");
26
- assert.equal(buildTextExportFilename("white_house_live_streams", { format: "md" }), "white_house_live_streams.md");
27
- assert.equal(buildTextExportFilename("companies.tsv", { format: "json" }), "companies.json");
28
- assert.equal(buildTextExportFilename("companies.json", { format: "parquet" }), "companies.parquet");
29
- });
30
-
31
- test("text export filenames sanitize unsafe names and fallback when empty", async () => {
32
- const { buildTextExportFilename } = await loadExportFilenamesModule();
33
-
34
- assert.equal(buildTextExportFilename("", { format: "csv", fallback: "table" }), "table.csv");
35
- assert.equal(buildTextExportFilename("../bad:name?.csv", { format: "csv", fallback: "table" }), "bad name.csv");
36
- assert.equal(buildTextExportFilename("bad:name?.json", { format: "json", fallback: "table" }), "bad name.json");
37
- assert.equal(buildTextExportFilename("bad:name?.parquet", { format: "parquet", fallback: "table" }), "bad name.parquet");
38
- });
@@ -1,165 +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 filePathPreviewModulePromise = null;
7
-
8
- function loadFilePathPreviewModule() {
9
- if (!filePathPreviewModulePromise) {
10
- const timestampPreviewSource = readFileSync(
11
- path.resolve(__dirname, "../frontend/js/utils/timestampPreview.js"),
12
- "utf8"
13
- );
14
- const timestampPreviewUrl = `data:text/javascript;base64,${Buffer.from(timestampPreviewSource).toString("base64")}`;
15
- const filePathPreviewSource = readFileSync(
16
- path.resolve(__dirname, "../frontend/js/utils/filePathPreview.js"),
17
- "utf8"
18
- ).replace(
19
- 'import { isProtectedKeyColumn } from "./timestampPreview.js";',
20
- `import { isProtectedKeyColumn } from "${timestampPreviewUrl}";`
21
- );
22
- const filePathPreviewUrl = `data:text/javascript;base64,${Buffer.from(filePathPreviewSource).toString("base64")}`;
23
-
24
- filePathPreviewModulePromise = import(filePathPreviewUrl);
25
- }
26
-
27
- return filePathPreviewModulePromise;
28
- }
29
-
30
- function createTableMeta() {
31
- return {
32
- columns: [
33
- { name: "id", primaryKeyPosition: 1 },
34
- { name: "parent_id", primaryKeyPosition: 0 },
35
- { name: "file_path", primaryKeyPosition: 0 },
36
- { name: "filename", primaryKeyPosition: 0 },
37
- { name: "note", primaryKeyPosition: 0 },
38
- ],
39
- foreignKeys: [
40
- {
41
- mappings: [{ from: "parent_id", to: "id" }],
42
- },
43
- ],
44
- };
45
- }
46
-
47
- test("filepath preview detects supported path styles", async () => {
48
- const { detectFilePathValue } = await loadFilePathPreviewModule();
49
- const tableMeta = createTableMeta();
50
- const cases = [
51
- {
52
- value: "/Users/oli/project/file.txt",
53
- pathType: "unix",
54
- fileName: "file.txt",
55
- directory: "/Users/oli/project",
56
- extension: "txt",
57
- },
58
- {
59
- value: "/home/oli/data/app.sqlite",
60
- pathType: "unix",
61
- fileName: "app.sqlite",
62
- directory: "/home/oli/data",
63
- extension: "sqlite",
64
- },
65
- {
66
- value: "C:\\Users\\Oliver\\Desktop\\file.pdf",
67
- pathType: "windows",
68
- fileName: "file.pdf",
69
- directory: "C:\\Users\\Oliver\\Desktop",
70
- extension: "pdf",
71
- },
72
- {
73
- value: "./data/export.csv",
74
- pathType: "relative",
75
- fileName: "export.csv",
76
- directory: "./data",
77
- extension: "csv",
78
- },
79
- {
80
- value: "../logs/app.log",
81
- pathType: "relative",
82
- fileName: "app.log",
83
- directory: "../logs",
84
- extension: "log",
85
- },
86
- {
87
- value: "~/Documents/report.md",
88
- pathType: "home",
89
- fileName: "report.md",
90
- directory: "~/Documents",
91
- extension: "md",
92
- },
93
- {
94
- value: "data/images/avatar.png",
95
- pathType: "relative",
96
- fileName: "avatar.png",
97
- directory: "data/images",
98
- extension: "png",
99
- },
100
- ];
101
-
102
- for (const item of cases) {
103
- const preview = detectFilePathValue(item.value, "file_path", tableMeta);
104
-
105
- assert.equal(preview?.type, "filepath");
106
- assert.equal(preview.pathType, item.pathType);
107
- assert.equal(preview.fileName, item.fileName);
108
- assert.equal(preview.directory, item.directory);
109
- assert.equal(preview.extension, item.extension);
110
- assert.ok(preview.confidence >= 0.7);
111
- }
112
- });
113
-
114
- test("filepath preview does not detect URLs, primitive text, JSON, emails, or weak filenames", async () => {
115
- const { detectFilePathValue } = await loadFilePathPreviewModule();
116
- const tableMeta = createTableMeta();
117
- const rejectedValues = [
118
- "https://example.com/file.txt",
119
- "http://localhost:3000/test",
120
- "mailto:oliver@example.com",
121
- "tel:+431234567",
122
- "ftp://example.com/file.txt",
123
- "oliver@example.com",
124
- "1717682400",
125
- "true",
126
- "false",
127
- '{"path":"/tmp/file.txt"}',
128
- "hello world",
129
- "file",
130
- "invoice.pdf",
131
- ];
132
-
133
- for (const value of rejectedValues) {
134
- assert.equal(detectFilePathValue(value, "note", tableMeta), null);
135
- }
136
- });
137
-
138
- test("filepath preview protects primary keys and foreign keys", async () => {
139
- const { detectFilePathValue } = await loadFilePathPreviewModule();
140
- const tableMeta = createTableMeta();
141
-
142
- assert.equal(detectFilePathValue("/Users/oli/project/file.txt", "id", tableMeta), null);
143
- assert.equal(detectFilePathValue("/Users/oli/project/file.txt", "parent_id", tableMeta), null);
144
- });
145
-
146
- test("filepath preview allows bare filenames only for strong file columns", async () => {
147
- const { detectFilePathValue } = await loadFilePathPreviewModule();
148
- const tableMeta = createTableMeta();
149
-
150
- assert.equal(detectFilePathValue("invoice.pdf", "note", tableMeta), null);
151
-
152
- const preview = detectFilePathValue("invoice.pdf", "filename", tableMeta);
153
-
154
- assert.equal(preview?.pathType, "relative");
155
- assert.equal(preview.fileName, "invoice.pdf");
156
- assert.equal(preview.directory, null);
157
- assert.equal(preview.extension, "pdf");
158
- });
159
-
160
- test("filepath preview compacts long paths while preserving filename", async () => {
161
- const { compactPathForDisplay } = await loadFilePathPreviewModule();
162
- const display = compactPathForDisplay("/Users/oli/projects/sqlite-hub/data/app.sqlite", 24);
163
-
164
- assert.equal(display, ".../data/app.sqlite");
165
- });
@@ -1,34 +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
- let formControlsModulePromise = null;
7
-
8
- function loadFormControlsModule() {
9
- if (!formControlsModulePromise) {
10
- formControlsModulePromise = import(
11
- pathToFileURL(path.resolve(__dirname, '../frontend/js/components/formControls.js')).href
12
- );
13
- }
14
-
15
- return formControlsModulePromise;
16
- }
17
-
18
- test('standard text input uses the shared application styling', async () => {
19
- const { renderTextInput } = await loadFormControlsModule();
20
- const markup = renderTextInput({
21
- className: 'flex-1',
22
- dataAttributes: { tokenName: true },
23
- maxlength: 80,
24
- placeholder: 'Token name',
25
- });
26
-
27
- assert.match(markup, /control-input/);
28
- assert.match(markup, /border-outline-variant\/20/);
29
- assert.match(markup, /bg-surface-container-lowest/);
30
- assert.match(markup, /placeholder:text-on-surface-variant\/35/);
31
- assert.match(markup, /focus:border-primary-container/);
32
- assert.match(markup, /data-token-name/);
33
- assert.match(markup, /maxlength="80"/);
34
- });
@@ -1,49 +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 jsonPreviewModulePromise = null;
7
-
8
- function loadJsonPreviewModule() {
9
- if (!jsonPreviewModulePromise) {
10
- const source = readFileSync(
11
- path.resolve(__dirname, "../frontend/js/utils/jsonPreview.js"),
12
- "utf8"
13
- );
14
- const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
15
-
16
- jsonPreviewModulePromise = import(url);
17
- }
18
-
19
- return jsonPreviewModulePromise;
20
- }
21
-
22
- test("JSON preview formats objects and arrays with line breaks and indentation", async () => {
23
- const { formatJsonPreview } = await loadJsonPreviewModule();
24
-
25
- assert.equal(
26
- formatJsonPreview('{"event":"created","metadata":{"source":"api"}}'),
27
- [
28
- "{",
29
- ' "event": "created",',
30
- ' "metadata": {',
31
- ' "source": "api"',
32
- " }",
33
- "}",
34
- ].join("\n")
35
- );
36
- assert.equal(
37
- formatJsonPreview([1, { active: true }]),
38
- ["[", " 1,", " {", ' "active": true', " }", "]"].join("\n")
39
- );
40
- });
41
-
42
- test("JSON preview ignores invalid JSON and scalar values", async () => {
43
- const { formatJsonPreview } = await loadJsonPreviewModule();
44
-
45
- assert.equal(formatJsonPreview("plain text"), null);
46
- assert.equal(formatJsonPreview("{invalid"), null);
47
- assert.equal(formatJsonPreview("42"), null);
48
- assert.equal(formatJsonPreview(null), null);
49
- });
@@ -1,85 +0,0 @@
1
- const assert = require("node:assert/strict");
2
- const test = require("node:test");
3
- const {
4
- LOOPBACK_HOST,
5
- listenOnLoopback,
6
- localRequestSecurity,
7
- } = require("../server/middleware/localRequestSecurity");
8
-
9
- function runMiddleware({ method = "GET", protocol = "http", headers = {} } = {}) {
10
- const normalizedHeaders = Object.fromEntries(
11
- Object.entries(headers).map(([name, value]) => [name.toLowerCase(), value])
12
- );
13
- const req = {
14
- method,
15
- protocol,
16
- get(name) {
17
- return normalizedHeaders[String(name).toLowerCase()];
18
- },
19
- };
20
-
21
- return new Promise((resolve) => {
22
- localRequestSecurity(req, {}, (error) => resolve(error ?? null));
23
- });
24
- }
25
-
26
- test("server listener binds explicitly to the IPv4 loopback address", () => {
27
- let receivedArguments = null;
28
- const listener = {};
29
- const app = {
30
- listen(...args) {
31
- receivedArguments = args;
32
- return listener;
33
- },
34
- };
35
-
36
- assert.equal(listenOnLoopback(app, 4173), listener);
37
- assert.deepEqual(receivedArguments, [4173, LOOPBACK_HOST]);
38
- });
39
-
40
- test("local API security accepts same-origin browser and CLI requests", async () => {
41
- assert.equal(
42
- await runMiddleware({
43
- method: "POST",
44
- headers: {
45
- host: "127.0.0.1:4173",
46
- origin: "http://127.0.0.1:4173",
47
- "sec-fetch-site": "same-origin",
48
- },
49
- }),
50
- null
51
- );
52
- assert.equal(
53
- await runMiddleware({
54
- method: "POST",
55
- headers: { host: "localhost:4173" },
56
- }),
57
- null
58
- );
59
- });
60
-
61
- test("local API security rejects foreign hosts and cross-origin mutations", async () => {
62
- const foreignHostError = await runMiddleware({
63
- headers: { host: "example.test:4173" },
64
- });
65
- const crossOriginError = await runMiddleware({
66
- method: "POST",
67
- headers: {
68
- host: "127.0.0.1:4173",
69
- origin: "https://example.test",
70
- "sec-fetch-site": "cross-site",
71
- },
72
- });
73
- const wrongSchemeError = await runMiddleware({
74
- method: "PATCH",
75
- headers: {
76
- host: "localhost:4173",
77
- origin: "https://localhost:4173",
78
- },
79
- });
80
-
81
- for (const error of [foreignHostError, crossOriginError, wrongSchemeError]) {
82
- assert.equal(error?.statusCode, 403);
83
- assert.equal(error?.code, "LOCAL_REQUEST_REQUIRED");
84
- }
85
- });
@@ -1,79 +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
- let markdownModulePromise = null;
7
-
8
- function loadMarkdownModule() {
9
- if (!markdownModulePromise) {
10
- markdownModulePromise = import(
11
- pathToFileURL(path.resolve(__dirname, "../frontend/js/utils/markdownDocuments.js")).href
12
- );
13
- }
14
-
15
- return markdownModulePromise;
16
- }
17
-
18
- test("markdown preview renders clickable todo checkboxes with source line indexes", async () => {
19
- const { renderMarkdownPreview } = await loadMarkdownModule();
20
- const html = renderMarkdownPreview(["# Tasks", "- [ ] item1", "- [x] item2", "```", "- [ ] code", "```"].join("\n"));
21
-
22
- assert.match(html, /document-markdown-task-list/);
23
- assert.match(html, /data-action="toggle-document-todo"/);
24
- assert.match(html, /data-line-index="1"/);
25
- assert.match(html, /data-line-index="2"/);
26
- assert.doesNotMatch(html, /data-line-index="4"/);
27
- });
28
-
29
- test("markdown todo toggle updates only real task lines", async () => {
30
- const { toggleMarkdownTodoLine } = await loadMarkdownModule();
31
- const markdown = ["- [ ] item1", "```", "- [ ] code", "```", "- [x] item2"].join("\n");
32
-
33
- assert.equal(toggleMarkdownTodoLine(markdown, 0).split("\n")[0], "- [x] item1");
34
- assert.equal(toggleMarkdownTodoLine(markdown, 2), markdown);
35
- assert.equal(toggleMarkdownTodoLine(markdown, 4).split("\n")[4], "- [ ] item2");
36
- });
37
-
38
- test("markdown preview fallback renders unordered and ordered lists", async () => {
39
- const { renderMarkdownPreview } = await loadMarkdownModule();
40
- const previousMarked = globalThis.marked;
41
-
42
- globalThis.marked = undefined;
43
-
44
- try {
45
- const html = renderMarkdownPreview(["- alpha", "- beta", "", "1. first", "2. second"].join("\n"));
46
-
47
- assert.match(html, /<ul><li>alpha<\/li><li>beta<\/li><\/ul>/);
48
- assert.match(html, /<ol><li>first<\/li><li>second<\/li><\/ol>/);
49
- } finally {
50
- globalThis.marked = previousMarked;
51
- }
52
- });
53
-
54
- test("markdown preview escapes raw html content", async () => {
55
- const { renderMarkdownPreview } = await loadMarkdownModule();
56
- const html = renderMarkdownPreview('<img src=x onerror="alert(1)">\n- [ ] <script>alert(1)</script>');
57
-
58
- assert.doesNotMatch(html, /<script>/i);
59
- assert.doesNotMatch(html, /<img/i);
60
- assert.match(html, /&lt;script&gt;/);
61
- });
62
-
63
- test("markdown preview strips executable link targets from rendered markdown", async () => {
64
- const { renderMarkdownPreview } = await loadMarkdownModule();
65
- const previousMarked = globalThis.marked;
66
-
67
- globalThis.marked = {
68
- parse: () => '<p><a href="javascript:alert(1)">unsafe</a></p>',
69
- };
70
-
71
- try {
72
- const html = renderMarkdownPreview("[unsafe](javascript:alert(1))");
73
-
74
- assert.doesNotMatch(html, /href="javascript:/i);
75
- assert.match(html, /href="#"/);
76
- } finally {
77
- globalThis.marked = previousMarked;
78
- }
79
- });
@@ -1,105 +0,0 @@
1
- const assert = require("node:assert/strict");
2
- const test = require("node:test");
3
- const {
4
- NativeFileDialogService,
5
- buildDialogAttempts,
6
- buildOpenDialogAttempts,
7
- normalizeOpenedDatabasePath,
8
- normalizeSelectedDatabasePath,
9
- } = require("../server/services/nativeFileDialogService");
10
-
11
- test("native database dialog normalizes paths and adds a default extension", () => {
12
- assert.equal(normalizeSelectedDatabasePath("/tmp/customer-data"), "/tmp/customer-data.sqlite");
13
- assert.equal(normalizeSelectedDatabasePath("/tmp/customer-data.db\n"), "/tmp/customer-data.db");
14
- assert.equal(normalizeSelectedDatabasePath(""), null);
15
- });
16
-
17
- test("native database dialog builds platform-specific save commands", () => {
18
- const macAttempt = buildDialogAttempts({ platform: "darwin", homeDirectory: "/Users/test" })[0];
19
- const windowsAttempt = buildDialogAttempts({ platform: "win32", homeDirectory: "C:\\Users\\test" })[0];
20
- const linuxAttempts = buildDialogAttempts({ platform: "linux", homeDirectory: "/home/test" });
21
-
22
- assert.equal(macAttempt.command, "osascript");
23
- assert.match(macAttempt.args.join(" "), /choose file name/);
24
- assert.equal(windowsAttempt.command, "powershell.exe");
25
- assert.match(windowsAttempt.args.at(-1), /SaveFileDialog/);
26
- assert.deepEqual(linuxAttempts.map((attempt) => attempt.command), ["zenity", "kdialog"]);
27
- });
28
-
29
- test("native database dialog builds platform-specific open commands", () => {
30
- const macAttempt = buildOpenDialogAttempts({ platform: "darwin", homeDirectory: "/Users/test" })[0];
31
- const windowsAttempt = buildOpenDialogAttempts({ platform: "win32", homeDirectory: "C:\\Users\\test" })[0];
32
- const linuxAttempts = buildOpenDialogAttempts({ platform: "linux", homeDirectory: "/home/test" });
33
-
34
- assert.match(macAttempt.args.join(" "), /choose file with prompt/);
35
- assert.match(windowsAttempt.args.at(-1), /OpenFileDialog/);
36
- assert.deepEqual(linuxAttempts.map((attempt) => attempt.command), ["zenity", "kdialog"]);
37
- });
38
-
39
- test("open database dialog preserves the selected filename", () => {
40
- assert.equal(normalizeOpenedDatabasePath("/tmp/catalog\n"), "/tmp/catalog");
41
- assert.equal(normalizeOpenedDatabasePath(""), null);
42
- });
43
-
44
- test("native database dialog returns null when the user cancels", async () => {
45
- const service = new NativeFileDialogService({
46
- platform: "darwin",
47
- homeDirectory: "/Users/test",
48
- executeFile: async () => {
49
- const error = new Error("User canceled.");
50
- error.code = 1;
51
- throw error;
52
- },
53
- });
54
-
55
- assert.equal(await service.chooseCreateDatabasePath(), null);
56
- });
57
-
58
- test("native database dialog does not hide macOS script failures as cancellations", async () => {
59
- const service = new NativeFileDialogService({
60
- platform: "darwin",
61
- homeDirectory: "/Users/test",
62
- executeFile: async () => {
63
- const error = new Error("AppleScript syntax error");
64
- error.code = 1;
65
- throw error;
66
- },
67
- });
68
-
69
- await assert.rejects(service.chooseCreateDatabasePath(), (error) => {
70
- assert.equal(error.code, "NATIVE_FILE_DIALOG_FAILED");
71
- return true;
72
- });
73
- });
74
-
75
- test("native database dialog falls back from zenity to kdialog", async () => {
76
- const commands = [];
77
- const service = new NativeFileDialogService({
78
- platform: "linux",
79
- homeDirectory: "/home/test",
80
- executeFile: async (command) => {
81
- commands.push(command);
82
-
83
- if (command === "zenity") {
84
- const error = new Error("not found");
85
- error.code = "ENOENT";
86
- throw error;
87
- }
88
-
89
- return { stdout: "/home/test/catalog.sqlite3\n" };
90
- },
91
- });
92
-
93
- assert.equal(await service.chooseCreateDatabasePath(), "/home/test/catalog.sqlite3");
94
- assert.deepEqual(commands, ["zenity", "kdialog"]);
95
- });
96
-
97
- test("native open database dialog returns the selected existing path", async () => {
98
- const service = new NativeFileDialogService({
99
- platform: "linux",
100
- homeDirectory: "/home/test",
101
- executeFile: async () => ({ stdout: "/home/test/catalog.db\n" }),
102
- });
103
-
104
- assert.equal(await service.chooseOpenDatabasePath(), "/home/test/catalog.db");
105
- });
@@ -1,28 +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
- let queryEditorModulePromise = null;
7
-
8
- function loadQueryEditorModule() {
9
- if (!queryEditorModulePromise) {
10
- queryEditorModulePromise = import(
11
- pathToFileURL(path.resolve(__dirname, "../frontend/js/components/queryEditor.js")).href
12
- );
13
- }
14
-
15
- return queryEditorModulePromise;
16
- }
17
-
18
- test("query editor groups editor actions in a reusable dropdown", async () => {
19
- const { renderQueryEditor } = await loadQueryEditorModule();
20
- const markup = renderQueryEditor({ query: "select * from companies;" });
21
-
22
- assert.match(markup, /data-dropdown-button/);
23
- assert.match(markup, /Editor actions/);
24
- assert.match(markup, /data-action="format-current-query"/);
25
- assert.match(markup, /data-action="clear-query"/);
26
- assert.match(markup, /data-action="copy-current-query"/);
27
- assert.match(markup, /Copy to Clipboard/);
28
- });
@@ -1,37 +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
- let queryHistoryDetailModulePromise = null;
7
-
8
- function loadQueryHistoryDetailModule() {
9
- if (!queryHistoryDetailModulePromise) {
10
- queryHistoryDetailModulePromise = import(
11
- pathToFileURL(path.resolve(__dirname, "../frontend/js/components/queryHistoryDetail.js")).href
12
- );
13
- }
14
-
15
- return queryHistoryDetailModulePromise;
16
- }
17
-
18
- test("query history detail delete button renders a trash icon", async () => {
19
- const { renderQueryHistoryDetail } = await loadQueryHistoryDetailModule();
20
- const html = renderQueryHistoryDetail({
21
- item: {
22
- id: 42,
23
- displayTitle: "Recent Query",
24
- rawSql: "select * from companies;",
25
- queryType: "select",
26
- isSaved: false,
27
- isDestructive: false,
28
- executionCount: 1,
29
- createdAt: "2026-06-21T10:00:00.000Z",
30
- updatedAt: "2026-06-21T10:00:00.000Z",
31
- },
32
- runs: [],
33
- });
34
-
35
- assert.match(html, /data-action="open-delete-query-history-modal"/);
36
- assert.match(html, /<span class="material-symbols-outlined text-sm">delete<\/span>Delete/);
37
- });
@@ -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
- let queryHistoryHeaderModulePromise = null;
7
-
8
- function loadQueryHistoryHeaderModule() {
9
- if (!queryHistoryHeaderModulePromise) {
10
- queryHistoryHeaderModulePromise = import(
11
- pathToFileURL(path.resolve(__dirname, "../frontend/js/components/queryHistoryHeader.js")).href
12
- );
13
- }
14
-
15
- return queryHistoryHeaderModulePromise;
16
- }
17
-
18
- test("query history search renders a magnifier icon", async () => {
19
- const { renderQueryHistorySearch } = await loadQueryHistoryHeaderModule();
20
- const markup = renderQueryHistorySearch({
21
- bind: "charts-history-search",
22
- value: "revenue",
23
- });
24
-
25
- assert.match(markup, /query-history-search/);
26
- assert.match(markup, /query-history-search__icon/);
27
- assert.match(markup, />search<\/span>/);
28
- assert.match(markup, /data-bind="charts-history-search"/);
29
- assert.match(markup, /value="revenue"/);
30
- });
@@ -1,20 +0,0 @@
1
- const assert = require("node:assert/strict");
2
- const path = require("node:path");
3
- const test = require("node:test");
4
- const { pathToFileURL } = require("node:url");
5
-
6
- test("query results render an explicit truncation notice", async () => {
7
- const moduleUrl = pathToFileURL(
8
- path.resolve(__dirname, "../frontend/js/components/queryResults.js")
9
- ).href;
10
- const { renderQueryResultsPane } = await import(moduleUrl);
11
- const html = renderQueryResultsPane({
12
- columns: ["id"],
13
- rows: [{ id: 1 }, { id: 2 }],
14
- truncated: true,
15
- rowLimit: 2,
16
- });
17
-
18
- assert.match(html, /Showing the first 2 rows/);
19
- assert.match(html, /export it to process the complete result set/);
20
- });