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
@@ -1,131 +0,0 @@
1
- const assert = require("node:assert/strict");
2
- const { readFileSync } = require("node:fs");
3
- const path = require("node:path");
4
- const { pathToFileURL } = require("node:url");
5
- const test = require("node:test");
6
-
7
- let modalModulePromise = null;
8
-
9
- function loadModalModule() {
10
- if (!modalModulePromise) {
11
- modalModulePromise = import(
12
- pathToFileURL(path.resolve(__dirname, "../frontend/js/components/modal.js")).href
13
- );
14
- }
15
-
16
- return modalModulePromise;
17
- }
18
-
19
- function buildState(copyMode) {
20
- return {
21
- modal: {
22
- kind: "copy-column",
23
- scope: "editor",
24
- columnName: "task",
25
- copyMode,
26
- separator: ",",
27
- wrapper: '"',
28
- lineBreaks: false,
29
- error: null,
30
- submitting: false,
31
- },
32
- editor: {
33
- result: {
34
- columns: ["task"],
35
- rows: [{ task: "item1" }, { task: "item2" }],
36
- },
37
- },
38
- charts: {
39
- result: null,
40
- },
41
- connections: {
42
- recent: [],
43
- active: null,
44
- },
45
- };
46
- }
47
-
48
- test("markdown todo column export renders an editable preview textarea", async () => {
49
- const { renderCopyColumnModal } = await loadModalModule();
50
- const state = buildState("markdown-todo");
51
- const html = renderCopyColumnModal(state.modal, state);
52
-
53
- assert.match(html, /Editable Preview/);
54
- assert.match(html, /<textarea[^>]+name="editedText"/);
55
- assert.match(html, /- \[ \] item1/);
56
- assert.match(html, /- \[ \] item2/);
57
- assert.match(html, /Export to document folder/);
58
- });
59
-
60
- test("regular copy column preview stays read-only", async () => {
61
- const { renderCopyColumnModal } = await loadModalModule();
62
- const state = buildState("column");
63
- const html = renderCopyColumnModal(state.modal, state);
64
-
65
- assert.match(html, /<pre class="copy-column-preview custom-scrollbar">/);
66
- assert.doesNotMatch(html, /name="editedText"/);
67
- });
68
-
69
- test("modal footer close buttons are not right-aligned", () => {
70
- const source = readFileSync(
71
- path.resolve(__dirname, "../frontend/js/components/modal.js"),
72
- "utf8"
73
- );
74
-
75
- assert.doesNotMatch(
76
- source,
77
- /<div class="[^"]*justify-end[^"]*pt-2[^"]*"[\s\S]{0,500}?data-action="close-modal"/
78
- );
79
- assert.doesNotMatch(
80
- source,
81
- /'<div class="[^']*justify-end[^']*pt-2[^']*>'[\s\S]{0,500}?data-action="close-modal"/
82
- );
83
- });
84
-
85
- test("create database modal offers a native path picker and manual fallback", async () => {
86
- const { renderCreateDatabaseForm } = await loadModalModule();
87
- const html = renderCreateDatabaseForm({
88
- kind: "create-connection",
89
- error: null,
90
- submitting: false,
91
- });
92
-
93
- assert.match(html, /data-action="choose-create-database-path"/);
94
- assert.match(html, /data-create-database-path/);
95
- assert.match(html, /name="path"/);
96
- assert.match(html, /enter an absolute path manually/);
97
- });
98
-
99
- test("open database modal offers a native file picker and manual fallback", async () => {
100
- const { renderOpenConnectionForm } = await loadModalModule();
101
- const html = renderOpenConnectionForm({
102
- kind: "open-connection",
103
- error: null,
104
- submitting: false,
105
- });
106
-
107
- assert.match(html, /data-action="choose-open-database-path"/);
108
- assert.match(html, /data-open-database-path/);
109
- assert.match(html, /name="path"/);
110
- assert.match(html, /enter an absolute path manually/);
111
- });
112
-
113
- test("API token deletion uses the shared confirmation modal", async () => {
114
- const { renderDeleteApiTokenForm } = await loadModalModule();
115
- const html = renderDeleteApiTokenForm({
116
- kind: "delete-api-token",
117
- tokenId: "token-one",
118
- tokenName: "Automation",
119
- tokenPrefix: "shub_example",
120
- databaseLabel: "Database One",
121
- error: null,
122
- submitting: false,
123
- });
124
-
125
- assert.match(html, /Delete API token/);
126
- assert.match(html, /Automation/);
127
- assert.match(html, /Database One/);
128
- assert.match(html, /shub_example\.\.\./);
129
- assert.match(html, /data-form="delete-api-token-confirm"/);
130
- assert.match(html, /Delete Token/);
131
- });
@@ -1,174 +0,0 @@
1
- const assert = require("node:assert/strict");
2
- const fs = require("node:fs");
3
- const os = require("node:os");
4
- const path = require("node:path");
5
- const test = require("node:test");
6
- const Database = require("better-sqlite3");
7
-
8
- const { DatabaseCommandService } = require("../server/services/databaseCommandService");
9
- const { AppStateStore } = require("../server/services/storage/appStateStore");
10
-
11
- function createFixture(t, options = {}) {
12
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-hub-command-service-"));
13
- const databasePath = path.join(directory, "sample.db");
14
- const targetDb = new Database(databasePath);
15
-
16
- targetDb.exec(`
17
- CREATE TABLE companies (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
18
- INSERT INTO companies (name) VALUES ('Acme'), ('Globex');
19
- `);
20
- targetDb.close();
21
-
22
- const store = new AppStateStore(path.join(directory, "state.db"));
23
- const connection = {
24
- id: "db-sample",
25
- label: "Sample",
26
- path: databasePath,
27
- lastOpenedAt: new Date().toISOString(),
28
- lastModifiedAt: new Date().toISOString(),
29
- sizeBytes: fs.statSync(databasePath).size,
30
- readOnly: Boolean(options.readOnly),
31
- logoPath: null,
32
- };
33
-
34
- store.upsertRecentConnection(connection);
35
- store.db
36
- .prepare(
37
- `
38
- INSERT INTO query_history (
39
- database_key,
40
- normalized_sql,
41
- raw_sql,
42
- title,
43
- notes,
44
- query_type,
45
- tables_detected,
46
- is_saved,
47
- first_executed_at,
48
- last_used_at
49
- )
50
- VALUES (?, ?, ?, ?, ?, 'select', '["companies"]', 1, ?, ?)
51
- `
52
- )
53
- .run(
54
- connection.id,
55
- "select id, name from companies order by id",
56
- "SELECT id, name FROM companies ORDER BY id",
57
- "Company List",
58
- "Used by CLI and API",
59
- new Date().toISOString(),
60
- new Date().toISOString()
61
- );
62
- store.db
63
- .prepare(
64
- `
65
- INSERT INTO query_history (
66
- database_key,
67
- normalized_sql,
68
- raw_sql,
69
- query_type,
70
- tables_detected,
71
- is_saved,
72
- first_executed_at,
73
- last_used_at
74
- )
75
- VALUES (?, ?, ?, 'other', '[]', 0, ?, ?)
76
- `
77
- )
78
- .run(
79
- connection.id,
80
- "company list",
81
- "Company List",
82
- new Date().toISOString(),
83
- new Date().toISOString()
84
- );
85
- store.createDatabaseDocument(connection.id, {
86
- filename: "Readme.md",
87
- content: "# Sample\n",
88
- });
89
-
90
- t.after(() => {
91
- store.db.close();
92
- fs.rmSync(directory, { recursive: true, force: true });
93
- });
94
-
95
- return {
96
- connection,
97
- service: new DatabaseCommandService({ appStateStore: store }),
98
- store,
99
- };
100
- }
101
-
102
- test("database command service provides shared CLI and API operations", (t) => {
103
- const { connection, service, store } = createFixture(t);
104
-
105
- assert.equal(service.getDatabase("sample").id, connection.id);
106
- assert.deepEqual(service.listTables(connection.id), [{ name: "companies", columnCount: 2 }]);
107
- assert.equal(service.getTable(connection.id, "companies").rowCount, 2);
108
-
109
- const row = service.getTableRow(connection.id, "companies", "1");
110
- assert.deepEqual(row.data, { id: 1, name: "Acme" });
111
- assert.equal(row.identity.kind, "primaryKey");
112
-
113
- const queries = service.listSavedQueries(connection.id);
114
- assert.equal(queries.total, 1);
115
- const savedQuery = service.getSavedQuery(connection.id, "Company List");
116
- assert.equal(savedQuery.notes, "Used by CLI and API");
117
- assert.equal(savedQuery.rawSql, "SELECT id, name FROM companies ORDER BY id");
118
-
119
- const execution = service.executeSavedQuery(connection.id, "Company List");
120
- assert.equal(execution.result.statements[0].rowCount, 2);
121
- assert.equal(execution.result.historyId, savedQuery.id);
122
-
123
- const savedRun = store.db
124
- .prepare("SELECT executed_by, status FROM query_runs WHERE history_id = ? ORDER BY id DESC LIMIT 1")
125
- .get(savedQuery.id);
126
- assert.deepEqual(savedRun, { executed_by: "user", status: "success" });
127
-
128
- const exported = service.exportSavedQuery(connection.id, "Company List", "csv");
129
- assert.equal(exported.result.rowCount, 2);
130
- assert.match(exported.result.content, /Acme/);
131
-
132
- assert.equal(service.listDocuments(connection.id).length, 1);
133
- assert.equal(service.getDocument(connection.id, "Readme").content, "# Sample\n");
134
- });
135
-
136
- test("raw query execution writes SQL Editor query history", (t) => {
137
- const { connection, service, store } = createFixture(t);
138
- const beforeCount = Number(
139
- store.db.prepare("SELECT COUNT(*) AS count FROM query_history").get().count
140
- );
141
- const { result, storedQuery } = service.executeRawQuery(
142
- connection.id,
143
- "INSERT INTO companies (name) VALUES ('Initech')",
144
- { storeName: "Add Initech" }
145
- );
146
- const afterCount = Number(
147
- store.db.prepare("SELECT COUNT(*) AS count FROM query_history").get().count
148
- );
149
- const historyRow = store.db
150
- .prepare("SELECT raw_sql, query_type, title, is_saved FROM query_history WHERE id = ?")
151
- .get(result.historyId);
152
- const runRow = store.db
153
- .prepare("SELECT executed_by FROM query_runs WHERE history_id = ? ORDER BY id DESC LIMIT 1")
154
- .get(result.historyId);
155
-
156
- assert.equal(result.affectedRowCount, 1);
157
- assert.equal(afterCount, beforeCount + 1);
158
- assert.equal(historyRow.raw_sql, "INSERT INTO companies (name) VALUES ('Initech')");
159
- assert.equal(historyRow.query_type, "insert");
160
- assert.equal(historyRow.title, "Add Initech");
161
- assert.equal(historyRow.is_saved, 1);
162
- assert.equal(runRow.executed_by, "user");
163
- assert.equal(storedQuery.title, "Add Initech");
164
- assert.equal(storedQuery.isSaved, true);
165
- });
166
-
167
- test("raw query execution is blocked for read-only connections", (t) => {
168
- const { connection, service } = createFixture(t, { readOnly: true });
169
-
170
- assert.throws(
171
- () => service.executeRawQuery(connection.id, "SELECT 1"),
172
- /read-only database/
173
- );
174
- });
@@ -1,85 +0,0 @@
1
- const assert = require("node:assert/strict");
2
- const fs = require("node:fs");
3
- const os = require("node:os");
4
- const path = require("node:path");
5
- const test = require("node:test");
6
-
7
- const { AppStateStore } = require("../server/services/storage/appStateStore");
8
- const { ensureDatabaseDocuments } = require("../server/routes/documents");
9
-
10
- function createStore(t) {
11
- const directory = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-hub-documents-"));
12
- const store = new AppStateStore(path.join(directory, "state.db"));
13
-
14
- t.after(() => {
15
- store.db.close();
16
- fs.rmSync(directory, { recursive: true, force: true });
17
- });
18
-
19
- return store;
20
- }
21
-
22
- test("database documents are scoped to one database and normalize filenames", (t) => {
23
- const store = createStore(t);
24
- const first = store.createDatabaseDocument("db-one", {
25
- filename: "../Daily Notes",
26
- content: "# Tasks\n\n- [ ] item1",
27
- });
28
- const second = store.createDatabaseDocument("db-one", {
29
- filename: "Daily Notes.md",
30
- content: "second",
31
- });
32
-
33
- assert.equal(first.filename, "Daily Notes.md");
34
- assert.equal(second.filename, "Daily Notes 2.md");
35
- assert.equal(store.listDatabaseDocuments("db-one").length, 2);
36
- assert.equal(store.listDatabaseDocuments("db-two").length, 0);
37
- assert.throws(() => store.getDatabaseDocument("db-two", first.id), /Document was not found/);
38
- });
39
-
40
- test("database document updates preserve raw markdown content", (t) => {
41
- const store = createStore(t);
42
- const document = store.createDatabaseDocument("db-one", {
43
- filename: "todo",
44
- content: "- [ ] item1",
45
- });
46
- const updated = store.updateDatabaseDocument("db-one", document.id, {
47
- filename: "todo-renamed",
48
- content: "- [x] item1\n1717682400",
49
- });
50
-
51
- assert.equal(updated.filename, "todo-renamed.md");
52
- assert.equal(updated.content, "- [x] item1\n1717682400");
53
-
54
- const deleted = store.deleteDatabaseDocument("db-one", document.id);
55
- assert.deepEqual(deleted, { id: document.id, deleted: true });
56
- assert.equal(store.listDatabaseDocuments("db-one").length, 0);
57
- });
58
-
59
- test("document folder creates one initial document for an empty database namespace", (t) => {
60
- const store = createStore(t);
61
- const connectionManager = {
62
- getActiveConnection: () => ({
63
- id: "db-one",
64
- label: "trump.sqlite",
65
- path: "/tmp/trump.sqlite",
66
- }),
67
- };
68
-
69
- const firstList = ensureDatabaseDocuments({
70
- appStateStore: store,
71
- connectionManager,
72
- databaseKey: "db-one",
73
- });
74
- const secondList = ensureDatabaseDocuments({
75
- appStateStore: store,
76
- connectionManager,
77
- databaseKey: "db-one",
78
- });
79
- const document = store.getDatabaseDocument("db-one", firstList[0].id);
80
-
81
- assert.equal(firstList.length, 1);
82
- assert.equal(secondList.length, 1);
83
- assert.equal(document.filename, "trump.sqlite.md");
84
- assert.equal(document.content, "# trump.sqlite\n");
85
- });
@@ -1,132 +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 documentsViewModulePromise = null;
7
-
8
- function loadDocumentsViewModule() {
9
- if (!documentsViewModulePromise) {
10
- documentsViewModulePromise = import(
11
- pathToFileURL(path.resolve(__dirname, "../frontend/js/views/documents.js")).href
12
- );
13
- }
14
-
15
- return documentsViewModulePromise;
16
- }
17
-
18
- function buildDocumentsState() {
19
- return {
20
- connections: {
21
- active: { id: "db-one", label: "Database One" },
22
- },
23
- documents: {
24
- items: [],
25
- selectedId: null,
26
- selected: null,
27
- searchQuery: "",
28
- draftFilename: "",
29
- draftContent: "",
30
- dirty: false,
31
- documentsVisible: true,
32
- editorVisible: true,
33
- previewVisible: true,
34
- loading: false,
35
- detailLoading: false,
36
- saving: false,
37
- deleting: false,
38
- error: null,
39
- saveError: null,
40
- },
41
- };
42
- }
43
-
44
- function buildSelectedDocumentsState(overrides = {}) {
45
- const state = buildDocumentsState();
46
- state.documents.items = [
47
- {
48
- contentLength: 11,
49
- filename: "notes.md",
50
- id: "doc-one",
51
- title: "Notes",
52
- updatedAt: "2026-06-21T10:00:00.000Z",
53
- },
54
- ];
55
- state.documents.selectedId = "doc-one";
56
- state.documents.selected = state.documents.items[0];
57
- state.documents.draftFilename = "notes.md";
58
- state.documents.draftContent = "# Notes";
59
- Object.assign(state.documents, overrides);
60
-
61
- return state;
62
- }
63
-
64
- test("documents view uses a new document dropdown for blank and markdown import", async () => {
65
- const { renderDocumentsView } = await loadDocumentsViewModule();
66
- const { main } = renderDocumentsView(buildDocumentsState());
67
-
68
- assert.match(main, /data-dropdown-button/);
69
- assert.match(main, /New Document/);
70
- assert.match(main, /Blank Page/);
71
- assert.match(main, /data-action="create-document"/);
72
- assert.match(main, /Import \.md/);
73
- assert.match(main, /data-action="import-document-markdown"/);
74
- assert.match(main, /data-bind="document-import-file"/);
75
- assert.doesNotMatch(main, /data-form="new-document"/);
76
- });
77
-
78
- test("documents panes keep both headers visible and move pane toggles into headers", async () => {
79
- const { renderDocumentsView } = await loadDocumentsViewModule();
80
- const { main } = renderDocumentsView(
81
- buildSelectedDocumentsState({
82
- editorVisible: false,
83
- previewVisible: true,
84
- }),
85
- );
86
-
87
- assert.match(main, /documents-workspace--editor-collapsed/);
88
- assert.match(main, /documents-pane--editor documents-pane--collapsed/);
89
- assert.match(main, /data-pane="editor"/);
90
- assert.match(main, /Show Editor/);
91
- assert.match(main, /data-pane="preview"/);
92
- assert.match(main, /Hide Preview/);
93
- assert.doesNotMatch(main, />Editor<\/span>/);
94
- assert.doesNotMatch(main, />Preview<\/span>/);
95
- });
96
-
97
- test("documents subnavi can be hidden while the show documents button stays available", async () => {
98
- const { renderDocumentsView } = await loadDocumentsViewModule();
99
- const { main } = renderDocumentsView(
100
- buildSelectedDocumentsState({
101
- documentsVisible: false,
102
- }),
103
- );
104
-
105
- assert.doesNotMatch(main, /documents-view__sidebar/);
106
- assert.match(main, /data-action="toggle-documents-panel"/);
107
- assert.match(main, /Show Documents/);
108
- assert.match(main, /aria-pressed="true"/);
109
- });
110
-
111
- test("selected document actions render in one titlebar without a save button", async () => {
112
- const { renderDocumentsView } = await loadDocumentsViewModule();
113
- const { main } = renderDocumentsView(buildSelectedDocumentsState());
114
- const orderedFragments = [
115
- 'data-action="toggle-documents-panel"',
116
- 'data-bind="document-field"',
117
- "New Document",
118
- "Insert",
119
- "Export .md",
120
- "Delete",
121
- ];
122
- const indexes = orderedFragments.map(fragment => main.indexOf(fragment));
123
-
124
- indexes.forEach(index => assert.notEqual(index, -1));
125
-
126
- for (let index = 1; index < indexes.length; index += 1) {
127
- assert.ok(indexes[index - 1] < indexes[index]);
128
- }
129
-
130
- assert.doesNotMatch(main, /documents-toolbar/);
131
- assert.doesNotMatch(main, /data-action="save-document"/);
132
- });
@@ -1,101 +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 dropdownButtonModulePromise = null;
7
-
8
- function loadDropdownButtonModule() {
9
- if (!dropdownButtonModulePromise) {
10
- dropdownButtonModulePromise = import(
11
- pathToFileURL(path.resolve(__dirname, "../frontend/js/components/dropdownButton.js")).href
12
- );
13
- }
14
-
15
- return dropdownButtonModulePromise;
16
- }
17
-
18
- test("dropdown button renders reusable action items", async () => {
19
- const { renderDropdownButton } = await loadDropdownButtonModule();
20
- const markup = renderDropdownButton({
21
- icon: "add_box",
22
- label: "Insert",
23
- title: "Insert content",
24
- items: [
25
- {
26
- action: "open-document-insert-table-modal",
27
- dataAttributes: { sourceView: "documents" },
28
- icon: "table_chart",
29
- label: "Insert Table",
30
- },
31
- {
32
- action: "open-document-insert-note-modal",
33
- icon: "note_add",
34
- label: "Insert Note",
35
- },
36
- ],
37
- });
38
-
39
- assert.match(markup, /data-dropdown-button/);
40
- assert.match(markup, /dropdown-button__toggle/);
41
- assert.match(markup, /dropdown-button__panel/);
42
- assert.match(markup, /data-action="open-document-insert-table-modal"/);
43
- assert.match(markup, /data-source-view="documents"/);
44
- assert.match(markup, /Insert Note/);
45
- });
46
-
47
- test("dropdown button renders disabled as a real disabled button", async () => {
48
- const { renderDropdownButton } = await loadDropdownButtonModule();
49
- const markup = renderDropdownButton({
50
- disabled: true,
51
- label: "Insert",
52
- items: [{ action: "noop", label: "Noop" }],
53
- });
54
-
55
- assert.doesNotMatch(markup, /<details/);
56
- assert.match(markup, /disabled/);
57
- assert.match(markup, /aria-disabled="true"/);
58
- });
59
-
60
- test("dropdown button supports local action attributes", async () => {
61
- const { renderDropdownButton } = await loadDropdownButtonModule();
62
- const markup = renderDropdownButton({
63
- label: "Format",
64
- items: [
65
- {
66
- action: "fit",
67
- actionAttribute: "data-structure-graph-action",
68
- label: "Fit Graph",
69
- },
70
- ],
71
- });
72
-
73
- assert.match(markup, /data-structure-graph-action="fit"/);
74
- assert.doesNotMatch(markup, /data-action="fit"/);
75
- });
76
-
77
- test("workspace open dropdown renders navigation and SQL editor actions", async () => {
78
- const { renderWorkspaceOpenDropdown } = await import(
79
- pathToFileURL(path.resolve(__dirname, "../frontend/js/components/workspaceOpenDropdown.js")).href
80
- );
81
- const markup = renderWorkspaceOpenDropdown({
82
- tableName: "companies",
83
- destinations: [
84
- {
85
- icon: "account_tree",
86
- key: "structure",
87
- label: "Structure",
88
- target: tableName => `/structure/${encodeURIComponent(tableName)}`,
89
- },
90
- {
91
- key: "sql-editor",
92
- },
93
- ],
94
- });
95
-
96
- assert.match(markup, /data-dropdown-button/);
97
- assert.match(markup, /data-action="navigate"/);
98
- assert.match(markup, /data-to="\/structure\/companies"/);
99
- assert.match(markup, /data-action="open-table-in-sql-editor"/);
100
- assert.match(markup, /data-table-name="companies"/);
101
- });