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.
- package/README.md +113 -71
- package/bin/sqlite-hub.js +723 -116
- package/docs/API.md +152 -0
- package/docs/CLI.md +249 -0
- package/docs/CLI_API_PARITY.md +61 -0
- package/docs/changelog.md +149 -0
- package/docs/guidelines/AGENTS.md +32 -0
- package/docs/guidelines/DESIGN.md +55 -0
- package/docs/shortkeys.md +27 -0
- package/docs/todo.md +4 -0
- package/examples/api/generate-types.js +52 -0
- package/frontend/assets/mockups/backups_1_1920.webp +0 -0
- package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_1_1920.webp +0 -0
- package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
- package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_1_1920.webp +0 -0
- package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
- package/frontend/assets/mockups/data_1_1920.webp +0 -0
- package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
- package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_1_1920.webp +0 -0
- package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
- package/frontend/assets/mockups/overview_1_1920.webp +0 -0
- package/frontend/assets/mockups/settings_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_1_1920.webp +0 -0
- package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
- package/frontend/js/api.js +63 -0
- package/frontend/js/app.js +310 -10
- package/frontend/js/components/connectionCard.js +4 -2
- package/frontend/js/components/emptyState.js +4 -4
- package/frontend/js/components/generateTypesDropdown.js +33 -0
- package/frontend/js/components/metricCard.js +1 -1
- package/frontend/js/components/modal.js +555 -21
- package/frontend/js/components/pageHeader.js +1 -1
- package/frontend/js/components/queryHistoryDetail.js +3 -3
- package/frontend/js/components/queryHistoryHeader.js +1 -1
- package/frontend/js/components/queryHistoryPanel.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +13 -3
- package/frontend/js/components/sidebar.js +41 -7
- package/frontend/js/components/topNav.js +2 -14
- package/frontend/js/router.js +38 -6
- package/frontend/js/store.js +870 -8
- package/frontend/js/utils/inputClear.js +36 -0
- package/frontend/js/utils/syntheticData.js +240 -0
- package/frontend/js/views/backups.js +581 -37
- package/frontend/js/views/charts.js +9 -9
- package/frontend/js/views/connections.js +2 -2
- package/frontend/js/views/data.js +17 -1
- package/frontend/js/views/documents.js +3 -3
- package/frontend/js/views/editor.js +6 -6
- package/frontend/js/views/logs.js +339 -0
- package/frontend/js/views/mediaTagging.js +7 -5
- package/frontend/js/views/overview.js +3 -3
- package/frontend/js/views/settings.js +80 -30
- package/frontend/js/views/structure.js +12 -1
- package/frontend/js/views/tableAdvisor.js +385 -0
- package/frontend/js/views/tableDesigner.js +6 -0
- package/frontend/styles/base.css +1 -40
- package/frontend/styles/components.css +258 -232
- package/frontend/styles/structure-graph.css +32 -68
- package/frontend/styles/tailwind.css +8 -4
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +28 -21
- package/frontend/styles/views.css +102 -261
- package/package.json +18 -2
- package/server/routes/backups.js +18 -2
- package/server/routes/data.js +45 -0
- package/server/routes/externalApi.js +321 -2
- package/server/routes/logs.js +127 -0
- package/server/routes/structure.js +22 -0
- package/server/server.js +3 -0
- package/server/services/databaseCommandService.js +59 -0
- package/server/services/sqlite/backupDiff.js +914 -0
- package/server/services/sqlite/backupService.js +77 -1
- package/server/services/sqlite/dataBrowserService.js +36 -0
- package/server/services/sqlite/introspection.js +226 -22
- package/server/services/sqlite/structureService.js +7 -0
- package/server/services/sqlite/syntheticDataGenerator.js +728 -0
- package/server/services/sqlite/tableAdvisor.js +790 -0
- package/server/services/storage/appStateStore.js +773 -169
- package/server/services/typeGenerationService.js +663 -0
- package/tailwind.config.cjs +0 -1
- package/.github/funding.yml +0 -2
- package/.github/workflows/ci.yml +0 -36
- package/database.sqlite +0 -0
- package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
- package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
- package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
- package/frontend/assets/mockups/connections_1200.webp +0 -0
- package/frontend/assets/mockups/data_1_1200.webp +0 -0
- package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
- package/frontend/assets/mockups/documents_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
- package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
- package/frontend/assets/mockups/overview_1200.webp +0 -0
- package/frontend/assets/mockups/settings_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
- package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
- package/frontend/assets/mockups/structure_1_1200.webp +0 -0
- package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
- package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
- package/tests/api-token-auth.test.js +0 -279
- package/tests/backup-manager.test.js +0 -140
- package/tests/backups-view.test.js +0 -70
- package/tests/charts-height-preset-storage.test.js +0 -60
- package/tests/charts-route-state.test.js +0 -144
- package/tests/check-constraint-options.test.js +0 -90
- package/tests/cli-args.test.js +0 -113
- package/tests/cli-service-delegation.test.js +0 -140
- package/tests/connection-removal.test.js +0 -52
- package/tests/connections-file-dialog-route.test.js +0 -89
- package/tests/copy-column-modal.test.js +0 -131
- package/tests/database-command-service.test.js +0 -174
- package/tests/database-documents.test.js +0 -85
- package/tests/documents-view.test.js +0 -132
- package/tests/dropdown-button.test.js +0 -101
- package/tests/email-preview.test.js +0 -89
- package/tests/export-blob.test.js +0 -99
- package/tests/export-filenames.test.js +0 -38
- package/tests/file-path-preview.test.js +0 -165
- package/tests/form-controls.test.js +0 -34
- package/tests/json-preview.test.js +0 -49
- package/tests/local-request-security.test.js +0 -85
- package/tests/markdown-documents.test.js +0 -103
- package/tests/native-file-dialog.test.js +0 -105
- package/tests/query-editor.test.js +0 -28
- package/tests/query-history-detail.test.js +0 -37
- package/tests/query-history-header.test.js +0 -30
- package/tests/query-results-truncation.test.js +0 -20
- package/tests/risky-sql.test.js +0 -30
- package/tests/row-editor-json.test.js +0 -82
- package/tests/row-editor-null-values.test.js +0 -184
- package/tests/row-editor-timestamp-preview.test.js +0 -192
- package/tests/security-paths.test.js +0 -84
- package/tests/settings-api-tokens-route.test.js +0 -97
- package/tests/settings-metadata.test.js +0 -114
- package/tests/settings-view.test.js +0 -108
- package/tests/sql-formatter.test.js +0 -173
- package/tests/sql-highlight.test.js +0 -38
- package/tests/sql-identifier-safety.test.js +0 -171
- package/tests/sql-result-limit.test.js +0 -66
- package/tests/structure-view.test.js +0 -60
- package/tests/table-designer-v2-unique-constraints.test.js +0 -78
- package/tests/table-scroll-state.test.js +0 -70
- package/tests/text-cell-stats.test.js +0 -38
|
@@ -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
|
-
});
|
package/tests/risky-sql.test.js
DELETED
|
@@ -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,184 +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("row editor JSON actions render as an export dropdown", async () => {
|
|
110
|
-
const { renderRowEditorPanel } = await loadFrontendModule(
|
|
111
|
-
"../frontend/js/components/rowEditorPanel.js"
|
|
112
|
-
);
|
|
113
|
-
const html = renderRowEditorPanel({
|
|
114
|
-
title: "Values",
|
|
115
|
-
closeAction: "close",
|
|
116
|
-
formName: "save-data-row",
|
|
117
|
-
jsonActionsEnabled: true,
|
|
118
|
-
editableFields: [
|
|
119
|
-
{
|
|
120
|
-
name: "title",
|
|
121
|
-
label: "title",
|
|
122
|
-
value: "Acme",
|
|
123
|
-
},
|
|
124
|
-
],
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
assert.match(html, /data-dropdown-button/);
|
|
128
|
-
assert.match(html, /Export/);
|
|
129
|
-
assert.match(html, /data-action="copy-row-editor-json"/);
|
|
130
|
-
assert.match(html, /Copy to clipboard/);
|
|
131
|
-
assert.match(html, /data-action="export-row-editor-json"/);
|
|
132
|
-
assert.match(html, /JSON file/);
|
|
133
|
-
assert.match(html, /data-action="insert-row-editor-json-into-document"/);
|
|
134
|
-
assert.match(html, /Markdown document/);
|
|
135
|
-
assert.doesNotMatch(html, /Copy as JSON|Export as JSON/);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("data updates can change NULL to empty string and empty string to NULL", () => {
|
|
139
|
-
const db = new Database(":memory:");
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
db.exec(`
|
|
143
|
-
CREATE TABLE values_test (
|
|
144
|
-
id INTEGER PRIMARY KEY,
|
|
145
|
-
nullable_text TEXT,
|
|
146
|
-
empty_text TEXT
|
|
147
|
-
);
|
|
148
|
-
INSERT INTO values_test (id, nullable_text, empty_text) VALUES (1, NULL, '');
|
|
149
|
-
`);
|
|
150
|
-
const service = new DataBrowserService({
|
|
151
|
-
connectionManager: {
|
|
152
|
-
assertWritable() {},
|
|
153
|
-
getActiveDatabase: () => db,
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
const identity = service.getTableData("values_test", { limit: 10 }).rows[0].__identity;
|
|
157
|
-
const preview = service.previewTableRowUpdate("values_test", {
|
|
158
|
-
identity,
|
|
159
|
-
values: {
|
|
160
|
-
nullable_text: "",
|
|
161
|
-
empty_text: null,
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
assert.deepEqual(preview.changes, [
|
|
166
|
-
{ column: "nullable_text", oldValue: "NULL", newValue: "" },
|
|
167
|
-
{ column: "empty_text", oldValue: "", newValue: "NULL" },
|
|
168
|
-
]);
|
|
169
|
-
|
|
170
|
-
service.updateTableRow("values_test", {
|
|
171
|
-
identity,
|
|
172
|
-
values: {
|
|
173
|
-
nullable_text: "",
|
|
174
|
-
empty_text: null,
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
const row = db.prepare("SELECT nullable_text, empty_text FROM values_test WHERE id = 1").get();
|
|
178
|
-
|
|
179
|
-
assert.equal(row.nullable_text, "");
|
|
180
|
-
assert.equal(row.empty_text, null);
|
|
181
|
-
} finally {
|
|
182
|
-
db.close();
|
|
183
|
-
}
|
|
184
|
-
});
|