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.
- package/README.md +106 -73
- package/bin/sqlite-hub.js +176 -2
- package/docs/API.md +122 -0
- package/docs/CLI.md +227 -0
- package/docs/DESIGN_GUIDELINES.md +45 -0
- package/docs/changelog.md +141 -0
- package/docs/shortkeys.md +27 -0
- package/docs/todo.md +16 -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 +18 -0
- package/frontend/js/app.js +209 -13
- package/frontend/js/components/connectionCard.js +1 -1
- 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 +211 -18
- 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/queryResults.js +1 -1
- package/frontend/js/components/rowEditorPanel.js +65 -16
- package/frontend/js/components/topNav.js +1 -4
- package/frontend/js/components/workspaceOpenDropdown.js +52 -0
- package/frontend/js/router.js +28 -6
- package/frontend/js/store.js +489 -9
- package/frontend/js/utils/emailPreview.js +28 -0
- package/frontend/js/utils/markdownDocuments.js +17 -1
- package/frontend/js/views/backups.js +556 -36
- package/frontend/js/views/charts.js +9 -9
- package/frontend/js/views/connections.js +2 -2
- package/frontend/js/views/data.js +43 -17
- package/frontend/js/views/documents.js +3 -3
- package/frontend/js/views/editor.js +6 -6
- package/frontend/js/views/mediaTagging.js +7 -5
- package/frontend/js/views/overview.js +3 -3
- package/frontend/js/views/settings.js +4 -3
- package/frontend/js/views/structure.js +27 -10
- package/frontend/js/views/tableDesigner.js +23 -0
- package/frontend/styles/base.css +1 -40
- package/frontend/styles/components.css +114 -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 +29 -21
- package/frontend/styles/views.css +94 -261
- package/package.json +18 -2
- package/server/routes/backups.js +18 -2
- package/server/routes/externalApi.js +36 -0
- package/server/routes/structure.js +22 -0
- package/server/services/databaseCommandService.js +14 -0
- package/server/services/sqlite/backupDiff.js +914 -0
- package/server/services/sqlite/backupService.js +77 -1
- package/server/services/sqlite/structureService.js +7 -0
- 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 -64
- 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 -75
- 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 -79
- 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 -155
- 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 -107
- 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 -56
- 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,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, /<script>/);
|
|
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
|
-
});
|