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,192 +0,0 @@
|
|
|
1
|
-
const Database = require("better-sqlite3");
|
|
2
|
-
const assert = require("node:assert/strict");
|
|
3
|
-
const { readFileSync } = require("node:fs");
|
|
4
|
-
const path = require("node:path");
|
|
5
|
-
const test = require("node:test");
|
|
6
|
-
const { DataBrowserService } = require("../server/services/sqlite/dataBrowserService");
|
|
7
|
-
|
|
8
|
-
let timestampPreviewModulePromise = null;
|
|
9
|
-
|
|
10
|
-
function loadTimestampPreviewModule() {
|
|
11
|
-
if (!timestampPreviewModulePromise) {
|
|
12
|
-
const source = readFileSync(
|
|
13
|
-
path.resolve(__dirname, "../frontend/js/utils/timestampPreview.js"),
|
|
14
|
-
"utf8"
|
|
15
|
-
);
|
|
16
|
-
const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
|
|
17
|
-
|
|
18
|
-
timestampPreviewModulePromise = import(url);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return timestampPreviewModulePromise;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function createTableMeta() {
|
|
25
|
-
return {
|
|
26
|
-
columns: [
|
|
27
|
-
{ name: "id", primaryKeyPosition: 1 },
|
|
28
|
-
{ name: "parent_id", primaryKeyPosition: 0 },
|
|
29
|
-
{ name: "created_at", primaryKeyPosition: 0 },
|
|
30
|
-
{ name: "updated_at", primaryKeyPosition: 0 },
|
|
31
|
-
{ name: "published_at", primaryKeyPosition: 0 },
|
|
32
|
-
{ name: "price", primaryKeyPosition: 0 },
|
|
33
|
-
],
|
|
34
|
-
foreignKeys: [
|
|
35
|
-
{
|
|
36
|
-
mappings: [{ from: "parent_id", to: "id" }],
|
|
37
|
-
},
|
|
38
|
-
],
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
test("row editor timestamp preview protects primary and foreign keys", async () => {
|
|
43
|
-
const {
|
|
44
|
-
getTimestampPreviewForField,
|
|
45
|
-
isProtectedKeyColumn,
|
|
46
|
-
} = await loadTimestampPreviewModule();
|
|
47
|
-
const tableMeta = createTableMeta();
|
|
48
|
-
|
|
49
|
-
assert.equal(isProtectedKeyColumn("id", tableMeta), true);
|
|
50
|
-
assert.equal(isProtectedKeyColumn("parent_id", tableMeta), true);
|
|
51
|
-
|
|
52
|
-
assert.equal(
|
|
53
|
-
getTimestampPreviewForField({
|
|
54
|
-
columnName: "id",
|
|
55
|
-
value: "1717682400",
|
|
56
|
-
tableMeta,
|
|
57
|
-
}).kind,
|
|
58
|
-
"protected-key"
|
|
59
|
-
);
|
|
60
|
-
assert.equal(
|
|
61
|
-
getTimestampPreviewForField({
|
|
62
|
-
columnName: "parent_id",
|
|
63
|
-
value: "1717682400",
|
|
64
|
-
tableMeta,
|
|
65
|
-
}).kind,
|
|
66
|
-
"protected-key"
|
|
67
|
-
);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("row editor timestamp preview formats plausible non-key timestamp values", async () => {
|
|
71
|
-
const { getTimestampPreviewForField } = await loadTimestampPreviewModule();
|
|
72
|
-
const tableMeta = createTableMeta();
|
|
73
|
-
|
|
74
|
-
const createdAtPreview = getTimestampPreviewForField({
|
|
75
|
-
columnName: "created_at",
|
|
76
|
-
value: "1717682400",
|
|
77
|
-
tableMeta,
|
|
78
|
-
});
|
|
79
|
-
const updatedAtPreview = getTimestampPreviewForField({
|
|
80
|
-
columnName: "updated_at",
|
|
81
|
-
value: "1717682400000",
|
|
82
|
-
tableMeta,
|
|
83
|
-
});
|
|
84
|
-
const pricePreview = getTimestampPreviewForField({
|
|
85
|
-
columnName: "price",
|
|
86
|
-
value: "1717682400",
|
|
87
|
-
tableMeta,
|
|
88
|
-
});
|
|
89
|
-
const publishedAtPreview = getTimestampPreviewForField({
|
|
90
|
-
columnName: "published_at",
|
|
91
|
-
value: "1717682400000000",
|
|
92
|
-
tableMeta,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
assert.equal(createdAtPreview.kind, "timestamp");
|
|
96
|
-
assert.equal(createdAtPreview.sourceFormat, "unix-seconds");
|
|
97
|
-
assert.equal(updatedAtPreview.kind, "timestamp");
|
|
98
|
-
assert.equal(updatedAtPreview.sourceFormat, "unix-milliseconds");
|
|
99
|
-
assert.equal(publishedAtPreview.kind, "timestamp");
|
|
100
|
-
assert.equal(publishedAtPreview.sourceFormat, "unix-microseconds");
|
|
101
|
-
assert.equal(pricePreview.kind, "timestamp");
|
|
102
|
-
assert.match(createdAtPreview.formatted, /^\d{2}\.\d{2}\.\d{4}, \d{2}:\d{2}:\d{2}$/);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("row editor timestamp preview ignores ids and invalid timestamps", async () => {
|
|
106
|
-
const { getTimestampPreviewForField } = await loadTimestampPreviewModule();
|
|
107
|
-
const tableMeta = createTableMeta();
|
|
108
|
-
|
|
109
|
-
assert.equal(
|
|
110
|
-
getTimestampPreviewForField({
|
|
111
|
-
columnName: "external_id",
|
|
112
|
-
value: "1717682400",
|
|
113
|
-
tableMeta,
|
|
114
|
-
}).kind,
|
|
115
|
-
"none"
|
|
116
|
-
);
|
|
117
|
-
assert.equal(
|
|
118
|
-
getTimestampPreviewForField({
|
|
119
|
-
columnName: "created_at",
|
|
120
|
-
value: "42",
|
|
121
|
-
tableMeta,
|
|
122
|
-
}).kind,
|
|
123
|
-
"none"
|
|
124
|
-
);
|
|
125
|
-
assert.equal(
|
|
126
|
-
getTimestampPreviewForField({
|
|
127
|
-
columnName: "created_at",
|
|
128
|
-
value: "not a timestamp",
|
|
129
|
-
tableMeta,
|
|
130
|
-
}).kind,
|
|
131
|
-
"none"
|
|
132
|
-
);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("row editor timestamp preview accepts ISO and SQLite datetime strings", async () => {
|
|
136
|
-
const { getTimestampPreviewForField } = await loadTimestampPreviewModule();
|
|
137
|
-
const tableMeta = createTableMeta();
|
|
138
|
-
|
|
139
|
-
assert.equal(
|
|
140
|
-
getTimestampPreviewForField({
|
|
141
|
-
columnName: "created_at",
|
|
142
|
-
value: "2026-06-06T18:49:00Z",
|
|
143
|
-
tableMeta,
|
|
144
|
-
}).kind,
|
|
145
|
-
"timestamp"
|
|
146
|
-
);
|
|
147
|
-
assert.equal(
|
|
148
|
-
getTimestampPreviewForField({
|
|
149
|
-
columnName: "updated_at",
|
|
150
|
-
value: "2026-06-06 18:49:00",
|
|
151
|
-
tableMeta,
|
|
152
|
-
}).kind,
|
|
153
|
-
"timestamp"
|
|
154
|
-
);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("row update stores the submitted raw value without date conversion", () => {
|
|
158
|
-
const db = new Database(":memory:");
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
db.exec(`
|
|
162
|
-
CREATE TABLE events (
|
|
163
|
-
id INTEGER PRIMARY KEY,
|
|
164
|
-
created_at TEXT
|
|
165
|
-
);
|
|
166
|
-
INSERT INTO events (id, created_at) VALUES (1, 'before');
|
|
167
|
-
`);
|
|
168
|
-
|
|
169
|
-
const service = new DataBrowserService({
|
|
170
|
-
connectionManager: {
|
|
171
|
-
assertWritable() {},
|
|
172
|
-
getActiveDatabase: () => db,
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
const tableData = service.getTableData("events", { limit: 10, offset: 0 });
|
|
176
|
-
const identity = tableData.rows[0].__identity;
|
|
177
|
-
|
|
178
|
-
service.updateTableRow("events", {
|
|
179
|
-
identity,
|
|
180
|
-
values: {
|
|
181
|
-
created_at: "1717682400",
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
assert.equal(
|
|
186
|
-
db.prepare("SELECT created_at FROM events WHERE id = 1").get().created_at,
|
|
187
|
-
"1717682400"
|
|
188
|
-
);
|
|
189
|
-
} finally {
|
|
190
|
-
db.close();
|
|
191
|
-
}
|
|
192
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
const fs = require("node:fs");
|
|
2
|
-
const os = require("node:os");
|
|
3
|
-
const path = require("node:path");
|
|
4
|
-
const { pathToFileURL } = require("node:url");
|
|
5
|
-
const assert = require("node:assert/strict");
|
|
6
|
-
const test = require("node:test");
|
|
7
|
-
const { ValidationError } = require("../server/utils/errors");
|
|
8
|
-
const {
|
|
9
|
-
resolvePathInsideDirectory,
|
|
10
|
-
resolveUserPath,
|
|
11
|
-
} = require("../server/utils/fileValidation");
|
|
12
|
-
const { MediaTaggingService } = require("../server/services/sqlite/mediaTaggingService");
|
|
13
|
-
|
|
14
|
-
function createTempDirectory(t) {
|
|
15
|
-
const directoryPath = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-hub-security-"));
|
|
16
|
-
t.after(() => fs.rmSync(directoryPath, { recursive: true, force: true }));
|
|
17
|
-
return directoryPath;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
test("resolveUserPath rejects parent traversal segments", () => {
|
|
21
|
-
assert.throws(() => resolveUserPath("../outside.db"), ValidationError);
|
|
22
|
-
assert.throws(() => resolveUserPath("~/../outside.db"), ValidationError);
|
|
23
|
-
assert.throws(() => resolveUserPath("data/\0outside.db"), ValidationError);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("resolvePathInsideDirectory resolves only paths under the supplied root", (t) => {
|
|
27
|
-
const root = createTempDirectory(t);
|
|
28
|
-
const insidePath = path.join(root, "media", "photo.jpg");
|
|
29
|
-
|
|
30
|
-
assert.equal(
|
|
31
|
-
resolvePathInsideDirectory(root, "media/photo.jpg", "Media file path"),
|
|
32
|
-
insidePath
|
|
33
|
-
);
|
|
34
|
-
assert.equal(
|
|
35
|
-
resolvePathInsideDirectory(root, insidePath, "Media file path"),
|
|
36
|
-
insidePath
|
|
37
|
-
);
|
|
38
|
-
assert.throws(
|
|
39
|
-
() =>
|
|
40
|
-
resolvePathInsideDirectory(
|
|
41
|
-
root,
|
|
42
|
-
path.join(path.dirname(root), "secret.txt"),
|
|
43
|
-
"Media file path"
|
|
44
|
-
),
|
|
45
|
-
ValidationError
|
|
46
|
-
);
|
|
47
|
-
assert.throws(
|
|
48
|
-
() => resolvePathInsideDirectory(root, "../secret.txt", "Media file path"),
|
|
49
|
-
ValidationError
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("media file resolution is scoped to the active database directory", (t) => {
|
|
54
|
-
const root = createTempDirectory(t);
|
|
55
|
-
const mediaDirectory = path.join(root, "media");
|
|
56
|
-
const mediaPath = path.join(mediaDirectory, "photo.jpg");
|
|
57
|
-
|
|
58
|
-
fs.mkdirSync(mediaDirectory, { recursive: true });
|
|
59
|
-
fs.writeFileSync(mediaPath, "image");
|
|
60
|
-
|
|
61
|
-
const service = new MediaTaggingService({
|
|
62
|
-
connectionManager: {
|
|
63
|
-
getActiveConnection: () => ({
|
|
64
|
-
path: path.join(root, "database.sqlite"),
|
|
65
|
-
}),
|
|
66
|
-
},
|
|
67
|
-
appStateStore: {},
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
assert.equal(service.resolveMediaFilePath("media/photo.jpg"), mediaPath);
|
|
71
|
-
assert.equal(
|
|
72
|
-
service.resolveMediaFilePath(pathToFileURL(mediaPath).toString()),
|
|
73
|
-
mediaPath
|
|
74
|
-
);
|
|
75
|
-
assert.deepEqual(service.getMediaFileForRequest("media/photo.jpg"), {
|
|
76
|
-
directory: mediaDirectory,
|
|
77
|
-
fileName: "photo.jpg",
|
|
78
|
-
});
|
|
79
|
-
assert.throws(() => service.resolveMediaFilePath("../secret.jpg"), ValidationError);
|
|
80
|
-
assert.throws(
|
|
81
|
-
() => service.resolveMediaFilePath(path.join(path.dirname(root), "secret.jpg")),
|
|
82
|
-
ValidationError
|
|
83
|
-
);
|
|
84
|
-
});
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const express = require("express");
|
|
3
|
-
const fs = require("node:fs");
|
|
4
|
-
const os = require("node:os");
|
|
5
|
-
const path = require("node:path");
|
|
6
|
-
const test = require("node:test");
|
|
7
|
-
|
|
8
|
-
const { createSettingsRouter } = require("../server/routes/settings");
|
|
9
|
-
const { ApiTokenService } = require("../server/services/apiTokenService");
|
|
10
|
-
const { AppStateStore } = require("../server/services/storage/appStateStore");
|
|
11
|
-
const { errorMiddleware } = require("../server/utils/errors");
|
|
12
|
-
|
|
13
|
-
test("settings routes create and delete tokens for the active database", async (t) => {
|
|
14
|
-
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-hub-settings-token-"));
|
|
15
|
-
const store = new AppStateStore(path.join(directory, "state.db"));
|
|
16
|
-
const connection = {
|
|
17
|
-
id: "db-active",
|
|
18
|
-
label: "Active Database",
|
|
19
|
-
path: path.join(directory, "active.db"),
|
|
20
|
-
lastOpenedAt: new Date().toISOString(),
|
|
21
|
-
lastModifiedAt: new Date().toISOString(),
|
|
22
|
-
sizeBytes: 0,
|
|
23
|
-
readOnly: false,
|
|
24
|
-
logoPath: null,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
store.upsertRecentConnection(connection);
|
|
28
|
-
|
|
29
|
-
const tokenService = new ApiTokenService({ appStateStore: store });
|
|
30
|
-
const connectionManager = {
|
|
31
|
-
getActiveConnection: () => connection,
|
|
32
|
-
};
|
|
33
|
-
const app = express();
|
|
34
|
-
|
|
35
|
-
app.use(express.json());
|
|
36
|
-
app.use(
|
|
37
|
-
"/api/settings",
|
|
38
|
-
createSettingsRouter({
|
|
39
|
-
appStateStore: store,
|
|
40
|
-
connectionManager,
|
|
41
|
-
tokenService,
|
|
42
|
-
versionCheckService: async () => ({
|
|
43
|
-
packageName: "sqlite-hub",
|
|
44
|
-
currentVersion: "1.0.1",
|
|
45
|
-
latestVersion: "1.1.0",
|
|
46
|
-
updateAvailable: true,
|
|
47
|
-
checkedAt: "2026-06-19T20:00:00.000Z",
|
|
48
|
-
source: "npm",
|
|
49
|
-
releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.1.0",
|
|
50
|
-
}),
|
|
51
|
-
})
|
|
52
|
-
);
|
|
53
|
-
app.use(errorMiddleware);
|
|
54
|
-
|
|
55
|
-
const server = await new Promise((resolve) => {
|
|
56
|
-
const listener = app.listen(0, "127.0.0.1", () => resolve(listener));
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
t.after(async () => {
|
|
60
|
-
await new Promise((resolve, reject) => {
|
|
61
|
-
server.close((error) => (error ? reject(error) : resolve()));
|
|
62
|
-
});
|
|
63
|
-
store.db.close();
|
|
64
|
-
fs.rmSync(directory, { recursive: true, force: true });
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const baseUrl = `http://127.0.0.1:${server.address().port}/api/settings`;
|
|
68
|
-
const createResponse = await fetch(`${baseUrl}/api-tokens`, {
|
|
69
|
-
method: "POST",
|
|
70
|
-
headers: { "Content-Type": "application/json" },
|
|
71
|
-
body: JSON.stringify({ name: "Settings token" }),
|
|
72
|
-
});
|
|
73
|
-
const created = await createResponse.json();
|
|
74
|
-
|
|
75
|
-
assert.equal(createResponse.status, 201);
|
|
76
|
-
assert.equal(created.data.databaseKey, connection.id);
|
|
77
|
-
assert.match(created.data.token, /^shub_/);
|
|
78
|
-
assert.equal(created.metadata.apiTokens.length, 1);
|
|
79
|
-
assert.equal(created.metadata.apiTokens[0].token, undefined);
|
|
80
|
-
|
|
81
|
-
const deleteResponse = await fetch(`${baseUrl}/api-tokens/${created.data.id}`, {
|
|
82
|
-
method: "DELETE",
|
|
83
|
-
});
|
|
84
|
-
const deleted = await deleteResponse.json();
|
|
85
|
-
|
|
86
|
-
assert.equal(deleteResponse.status, 200);
|
|
87
|
-
assert.deepEqual(deleted.data, { id: created.data.id, deleted: true });
|
|
88
|
-
assert.equal(deleted.metadata.apiTokens.length, 0);
|
|
89
|
-
|
|
90
|
-
const versionResponse = await fetch(`${baseUrl}/version-check`);
|
|
91
|
-
const versionCheck = await versionResponse.json();
|
|
92
|
-
|
|
93
|
-
assert.equal(versionResponse.status, 200);
|
|
94
|
-
assert.equal(versionCheck.data.latestVersion, "1.1.0");
|
|
95
|
-
assert.equal(versionCheck.data.updateAvailable, true);
|
|
96
|
-
assert.equal(versionCheck.metadata.appVersion, require("../package.json").version);
|
|
97
|
-
});
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const test = require("node:test");
|
|
3
|
-
|
|
4
|
-
const { version } = require("../package.json");
|
|
5
|
-
const {
|
|
6
|
-
checkLatestAppVersion,
|
|
7
|
-
compareSemver,
|
|
8
|
-
isNewerVersion,
|
|
9
|
-
readSettingsMetadata,
|
|
10
|
-
readSqliteVersion,
|
|
11
|
-
} = require("../server/routes/settings");
|
|
12
|
-
const { buildAppInfo } = require("../server/services/appInfoService");
|
|
13
|
-
|
|
14
|
-
test("settings metadata exposes app and SQLite versions", () => {
|
|
15
|
-
const metadata = readSettingsMetadata();
|
|
16
|
-
|
|
17
|
-
assert.equal(metadata.appVersion, version);
|
|
18
|
-
assert.match(metadata.sqliteVersion, /^\d+\.\d+\.\d+$/);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("SQLite runtime version is readable", () => {
|
|
22
|
-
assert.match(readSqliteVersion(), /^\d+\.\d+\.\d+$/);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("settings version comparison detects newer releases", () => {
|
|
26
|
-
assert.equal(compareSemver("1.2.0", "1.1.9"), 1);
|
|
27
|
-
assert.equal(compareSemver("1.0.1", "1.0.1"), 0);
|
|
28
|
-
assert.equal(compareSemver("1.0.0", "1.0.1"), -1);
|
|
29
|
-
assert.equal(isNewerVersion("1.0.2", "1.0.1"), true);
|
|
30
|
-
assert.equal(isNewerVersion("1.0.1", "1.0.1"), false);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("settings version check reads the npm latest version", async () => {
|
|
34
|
-
const result = await checkLatestAppVersion({
|
|
35
|
-
currentVersion: "1.0.1",
|
|
36
|
-
packageName: "sqlite-hub",
|
|
37
|
-
fetchImpl: async (url, options) => {
|
|
38
|
-
assert.equal(url, "https://registry.npmjs.org/sqlite-hub/latest");
|
|
39
|
-
assert.equal(options.headers.Accept, "application/json");
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
ok: true,
|
|
43
|
-
json: async () => ({ version: "1.1.0" }),
|
|
44
|
-
};
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
assert.equal(result.packageName, "sqlite-hub");
|
|
49
|
-
assert.equal(result.currentVersion, "1.0.1");
|
|
50
|
-
assert.equal(result.latestVersion, "1.1.0");
|
|
51
|
-
assert.equal(result.updateAvailable, true);
|
|
52
|
-
assert.equal(result.source, "npm");
|
|
53
|
-
assert.equal(result.releaseUrl, "https://www.npmjs.com/package/sqlite-hub/v/1.1.0");
|
|
54
|
-
assert.match(result.checkedAt, /^\d{4}-\d{2}-\d{2}T/);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("app info includes update status for CLI and API consumers", async () => {
|
|
58
|
-
const info = await buildAppInfo({
|
|
59
|
-
port: 4173,
|
|
60
|
-
url: "http://127.0.0.1:4173",
|
|
61
|
-
currentVersion: "1.0.1",
|
|
62
|
-
sqliteVersion: "3.50.0",
|
|
63
|
-
versionCheckService: async () => ({
|
|
64
|
-
packageName: "sqlite-hub",
|
|
65
|
-
currentVersion: "1.0.1",
|
|
66
|
-
latestVersion: "1.1.0",
|
|
67
|
-
updateAvailable: true,
|
|
68
|
-
checkedAt: "2026-06-20T10:00:00.000Z",
|
|
69
|
-
source: "npm",
|
|
70
|
-
releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.1.0",
|
|
71
|
-
}),
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
assert.equal(info.packageName, "sqlite-hub");
|
|
75
|
-
assert.equal(info.appVersion, "1.0.1");
|
|
76
|
-
assert.equal(info.sqliteVersion, "3.50.0");
|
|
77
|
-
assert.equal(info.port, 4173);
|
|
78
|
-
assert.equal(info.url, "http://127.0.0.1:4173");
|
|
79
|
-
assert.equal(info.versionCheck.status, "update_available");
|
|
80
|
-
assert.equal(info.versionCheck.updateAvailable, true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("app info reports unknown version status when registry lookup fails", async () => {
|
|
84
|
-
const info = await buildAppInfo({
|
|
85
|
-
currentVersion: "1.0.1",
|
|
86
|
-
sqliteVersion: "3.50.0",
|
|
87
|
-
versionCheckService: async () => {
|
|
88
|
-
throw new Error("offline");
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
assert.equal(info.versionCheck.status, "unknown");
|
|
93
|
-
assert.equal(info.versionCheck.updateAvailable, null);
|
|
94
|
-
assert.equal(info.versionCheck.error.message, "offline");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("app info marks local builds newer than npm latest as ahead", async () => {
|
|
98
|
-
const info = await buildAppInfo({
|
|
99
|
-
currentVersion: "1.1.0",
|
|
100
|
-
sqliteVersion: "3.50.0",
|
|
101
|
-
versionCheckService: async () => ({
|
|
102
|
-
packageName: "sqlite-hub",
|
|
103
|
-
currentVersion: "1.1.0",
|
|
104
|
-
latestVersion: "1.0.0",
|
|
105
|
-
updateAvailable: false,
|
|
106
|
-
checkedAt: "2026-06-20T10:00:00.000Z",
|
|
107
|
-
source: "npm",
|
|
108
|
-
releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.0.0",
|
|
109
|
-
}),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
assert.equal(info.versionCheck.status, "ahead");
|
|
113
|
-
assert.equal(info.versionCheck.updateAvailable, false);
|
|
114
|
-
});
|
|
@@ -1,108 +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 settingsViewModulePromise = null;
|
|
7
|
-
|
|
8
|
-
function loadSettingsViewModule() {
|
|
9
|
-
if (!settingsViewModulePromise) {
|
|
10
|
-
settingsViewModulePromise = import(
|
|
11
|
-
pathToFileURL(path.resolve(__dirname, "../frontend/js/views/settings.js")).href
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return settingsViewModulePromise;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
test("settings view shows SQLite runtime version and custom port command", async () => {
|
|
19
|
-
const { renderSettingsView } = await loadSettingsViewModule();
|
|
20
|
-
const rendered = renderSettingsView({
|
|
21
|
-
settings: {
|
|
22
|
-
loading: false,
|
|
23
|
-
error: null,
|
|
24
|
-
appVersion: "1.2.3",
|
|
25
|
-
sqliteVersion: "3.53.1",
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
assert.match(rendered.main, /SQLite_Runtime/);
|
|
30
|
-
assert.match(rendered.main, /3\.53\.1/);
|
|
31
|
-
assert.match(rendered.main, /sqlite-hub --port:PORT/);
|
|
32
|
-
assert.match(rendered.main, /Version_Check/);
|
|
33
|
-
assert.match(rendered.main, /data-action="check-app-version"/);
|
|
34
|
-
assert.match(rendered.main, /Check Updates/);
|
|
35
|
-
assert.match(rendered.main, /No version check has been run yet\./);
|
|
36
|
-
assert.match(rendered.main, /data-section="information"/);
|
|
37
|
-
assert.match(rendered.main, /data-section="api-tokens"/);
|
|
38
|
-
assert.doesNotMatch(rendered.main, /data-form="create-api-token"/);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("settings view shows available app updates", async () => {
|
|
42
|
-
const { renderSettingsView } = await loadSettingsViewModule();
|
|
43
|
-
const rendered = renderSettingsView({
|
|
44
|
-
settings: {
|
|
45
|
-
loading: false,
|
|
46
|
-
error: null,
|
|
47
|
-
appVersion: "1.0.1",
|
|
48
|
-
sqliteVersion: "3.53.1",
|
|
49
|
-
versionCheckLoading: false,
|
|
50
|
-
versionCheckError: null,
|
|
51
|
-
versionCheck: {
|
|
52
|
-
currentVersion: "1.0.1",
|
|
53
|
-
latestVersion: "1.1.0",
|
|
54
|
-
updateAvailable: true,
|
|
55
|
-
releaseUrl: "https://www.npmjs.com/package/sqlite-hub/v/1.1.0",
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
assert.match(rendered.main, /New Version Available/);
|
|
61
|
-
assert.match(rendered.main, /Current v1\.0\.1 · Latest v1\.1\.0/);
|
|
62
|
-
assert.match(rendered.main, /Open Release/);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("settings view scopes API token controls to the active database", async () => {
|
|
66
|
-
const { renderSettingsView } = await loadSettingsViewModule();
|
|
67
|
-
const rendered = renderSettingsView({
|
|
68
|
-
settings: {
|
|
69
|
-
loading: false,
|
|
70
|
-
error: null,
|
|
71
|
-
appVersion: "1.2.3",
|
|
72
|
-
sqliteVersion: "3.53.1",
|
|
73
|
-
section: "api-tokens",
|
|
74
|
-
tokenDatabase: { id: "db-one", label: "Database One" },
|
|
75
|
-
apiTokens: [
|
|
76
|
-
{
|
|
77
|
-
id: "token-one",
|
|
78
|
-
name: "Automation",
|
|
79
|
-
tokenPrefix: "shub_example",
|
|
80
|
-
createdAt: "2026-06-15T10:00:00.000Z",
|
|
81
|
-
},
|
|
82
|
-
],
|
|
83
|
-
createdApiToken: null,
|
|
84
|
-
tokenSaving: false,
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
assert.match(rendered.main, /API Tokens/);
|
|
89
|
-
assert.match(rendered.main, /Security \/\/ Database API access/);
|
|
90
|
-
assert.match(rendered.main, /Database One/);
|
|
91
|
-
assert.match(rendered.main, /Database ID/);
|
|
92
|
-
assert.match(rendered.main, /data-database-id/);
|
|
93
|
-
assert.match(rendered.main, /data-action="copy-database-id"/);
|
|
94
|
-
assert.match(rendered.main, /Copy ID/);
|
|
95
|
-
assert.match(rendered.main, /sm:grid-cols-\[minmax\(0,1fr\)_9rem\]/);
|
|
96
|
-
assert.equal((rendered.main.match(/w-full self-center justify-center/g) ?? []).length, 2);
|
|
97
|
-
assert.match(rendered.main, /shub_example\.\.\./);
|
|
98
|
-
assert.match(rendered.main, /data-api-token-name/);
|
|
99
|
-
assert.match(rendered.main, /data-form="create-api-token"/);
|
|
100
|
-
assert.match(rendered.main, /name="name"/);
|
|
101
|
-
assert.match(rendered.main, /type="submit"/);
|
|
102
|
-
assert.match(rendered.main, /data-action="open-delete-api-token-modal"/);
|
|
103
|
-
assert.match(rendered.main, /<span class="material-symbols-outlined text-sm">delete<\/span>\s*Delete/);
|
|
104
|
-
assert.match(rendered.main, /bg-surface-container-lowest/);
|
|
105
|
-
assert.doesNotMatch(rendered.main, /sqlite-hub --port:PORT/);
|
|
106
|
-
assert.doesNotMatch(rendered.main, /Open Github/);
|
|
107
|
-
assert.doesNotMatch(rendered.main, /token_hash/);
|
|
108
|
-
});
|