sqlite-hub 1.2.0 → 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 +118 -8
- 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/rowEditorPanel.js +13 -3
- package/frontend/js/router.js +28 -6
- package/frontend/js/store.js +422 -8
- package/frontend/js/views/backups.js +529 -37
- package/frontend/js/views/charts.js +9 -9
- package/frontend/js/views/connections.js +2 -2
- package/frontend/js/views/data.js +1 -1
- 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 +3 -3
- package/frontend/js/views/structure.js +6 -1
- package/frontend/styles/base.css +1 -40
- package/frontend/styles/components.css +109 -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 +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 -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,144 +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
|
-
function jsonResponse(payload) {
|
|
7
|
-
return new Response(JSON.stringify({ success: true, ...payload }), {
|
|
8
|
-
headers: { "content-type": "application/json" },
|
|
9
|
-
status: 200,
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function createChartsFetchMock() {
|
|
14
|
-
const activeConnection = {
|
|
15
|
-
id: "db-one",
|
|
16
|
-
label: "Database One",
|
|
17
|
-
readOnly: false,
|
|
18
|
-
};
|
|
19
|
-
const chartableQuery = {
|
|
20
|
-
id: 10,
|
|
21
|
-
displayTitle: "Revenue Chart",
|
|
22
|
-
previewSql: "select revenue from metrics",
|
|
23
|
-
rawSql: "select revenue from metrics",
|
|
24
|
-
queryType: "select",
|
|
25
|
-
isSaved: false,
|
|
26
|
-
isDestructive: false,
|
|
27
|
-
chartTypes: ["bar"],
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
return async function fetchMock(input, options = {}) {
|
|
31
|
-
const url = typeof input === "string" ? input : input.url;
|
|
32
|
-
const method = String(options.method ?? "GET").toUpperCase();
|
|
33
|
-
|
|
34
|
-
if (method === "GET" && url === "/api/connections/recent") {
|
|
35
|
-
return jsonResponse({ data: [activeConnection] });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (method === "GET" && url === "/api/connections/active") {
|
|
39
|
-
return jsonResponse({ data: activeConnection });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (method === "GET" && url === "/api/settings") {
|
|
43
|
-
return jsonResponse({
|
|
44
|
-
data: {},
|
|
45
|
-
metadata: {
|
|
46
|
-
activeDatabase: activeConnection,
|
|
47
|
-
apiTokens: [],
|
|
48
|
-
appVersion: "1.1.2",
|
|
49
|
-
sqliteVersion: "3.50.0",
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (method === "GET" && url.startsWith("/api/sql/history")) {
|
|
55
|
-
return jsonResponse({ data: { items: [], total: 0, hasMore: false } });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (method === "GET" && url === "/api/db/overview") {
|
|
59
|
-
return jsonResponse({ data: { tables: [] } });
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (method === "GET" && url === "/api/charts/query-history") {
|
|
63
|
-
return jsonResponse({ data: [chartableQuery] });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (method === "GET" && url === "/api/charts/query-history/10") {
|
|
67
|
-
return jsonResponse({
|
|
68
|
-
data: {
|
|
69
|
-
item: chartableQuery,
|
|
70
|
-
charts: [],
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (method === "POST" && url === "/api/charts/query-history/10/execute") {
|
|
76
|
-
return jsonResponse({
|
|
77
|
-
data: {
|
|
78
|
-
columns: ["revenue"],
|
|
79
|
-
rows: [{ revenue: 100 }],
|
|
80
|
-
sql: chartableQuery.rawSql,
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return new Response(
|
|
86
|
-
JSON.stringify({
|
|
87
|
-
success: false,
|
|
88
|
-
error: { code: "UNEXPECTED_REQUEST", message: `${method} ${url}` },
|
|
89
|
-
}),
|
|
90
|
-
{
|
|
91
|
-
headers: { "content-type": "application/json" },
|
|
92
|
-
status: 500,
|
|
93
|
-
},
|
|
94
|
-
);
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function importStoreWithFetch(fetchMock) {
|
|
99
|
-
const originalFetch = globalThis.fetch;
|
|
100
|
-
|
|
101
|
-
globalThis.fetch = fetchMock;
|
|
102
|
-
|
|
103
|
-
const moduleUrl = pathToFileURL(path.resolve(__dirname, "../frontend/js/store.js")).href;
|
|
104
|
-
const store = await import(`${moduleUrl}?charts-route-state=${Date.now()}`);
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
restore() {
|
|
108
|
-
globalThis.fetch = originalFetch;
|
|
109
|
-
},
|
|
110
|
-
store,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
test("charts route preserves the selected chart when returning from another menu", async () => {
|
|
115
|
-
const { restore, store } = await importStoreWithFetch(createChartsFetchMock());
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
await store.initializeApp();
|
|
119
|
-
await store.setRoute({
|
|
120
|
-
name: "charts",
|
|
121
|
-
params: { historyId: "10" },
|
|
122
|
-
path: "/charts/10",
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
assert.equal(store.getState().charts.selectedHistoryId, 10);
|
|
126
|
-
assert.equal(store.getState().charts.detail?.item?.id, 10);
|
|
127
|
-
|
|
128
|
-
await store.setRoute({
|
|
129
|
-
name: "overview",
|
|
130
|
-
params: {},
|
|
131
|
-
path: "/overview",
|
|
132
|
-
});
|
|
133
|
-
await store.setRoute({
|
|
134
|
-
name: "charts",
|
|
135
|
-
params: { historyId: null },
|
|
136
|
-
path: "/charts",
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
assert.equal(store.getState().charts.selectedHistoryId, 10);
|
|
140
|
-
assert.equal(store.getState().charts.detail?.item?.id, 10);
|
|
141
|
-
} finally {
|
|
142
|
-
restore();
|
|
143
|
-
}
|
|
144
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
const Database = require("better-sqlite3");
|
|
2
|
-
const assert = require("node:assert/strict");
|
|
3
|
-
const test = require("node:test");
|
|
4
|
-
const { getTableDetail } = require("../server/services/sqlite/introspection");
|
|
5
|
-
const { SqlExecutor } = require("../server/services/sqlite/sqlExecutor");
|
|
6
|
-
const { buildTableDesignerDraft } = require("../server/services/sqlite/tableDesigner/schemaMapping");
|
|
7
|
-
|
|
8
|
-
test("table detail exposes string options from simple CHECK IN constraints", () => {
|
|
9
|
-
const db = new Database(":memory:");
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
db.exec(`
|
|
13
|
-
CREATE TABLE stream_company_mentions (
|
|
14
|
-
id INTEGER PRIMARY KEY,
|
|
15
|
-
mention_type TEXT,
|
|
16
|
-
CHECK (
|
|
17
|
-
mention_type IS NULL
|
|
18
|
-
OR mention_type IN (
|
|
19
|
-
'company',
|
|
20
|
-
'stock_company',
|
|
21
|
-
'brand',
|
|
22
|
-
'product',
|
|
23
|
-
'person',
|
|
24
|
-
'organization',
|
|
25
|
-
'terrorists',
|
|
26
|
-
'events',
|
|
27
|
-
'collective_terms',
|
|
28
|
-
'countries',
|
|
29
|
-
'none',
|
|
30
|
-
'unknown'
|
|
31
|
-
)
|
|
32
|
-
)
|
|
33
|
-
);
|
|
34
|
-
INSERT INTO stream_company_mentions (mention_type) VALUES ('company');
|
|
35
|
-
`);
|
|
36
|
-
|
|
37
|
-
const tableDetail = getTableDetail(db, "stream_company_mentions");
|
|
38
|
-
const designerDraft = buildTableDesignerDraft(tableDetail);
|
|
39
|
-
const mentionTypeColumn = tableDetail.columns.find(
|
|
40
|
-
(column) => column.name === "mention_type"
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
assert.deepEqual(mentionTypeColumn.allowedValues, [
|
|
44
|
-
"company",
|
|
45
|
-
"stock_company",
|
|
46
|
-
"brand",
|
|
47
|
-
"product",
|
|
48
|
-
"person",
|
|
49
|
-
"organization",
|
|
50
|
-
"terrorists",
|
|
51
|
-
"events",
|
|
52
|
-
"collective_terms",
|
|
53
|
-
"countries",
|
|
54
|
-
"none",
|
|
55
|
-
"unknown",
|
|
56
|
-
]);
|
|
57
|
-
|
|
58
|
-
const executor = new SqlExecutor({
|
|
59
|
-
connectionManager: {
|
|
60
|
-
getActiveDatabase: () => db,
|
|
61
|
-
getActiveConnection: () => ({ id: "test" }),
|
|
62
|
-
},
|
|
63
|
-
appStateStore: {
|
|
64
|
-
recordQueryExecution: () => 1,
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
const result = executor.execute(
|
|
68
|
-
"SELECT id, mention_type FROM stream_company_mentions"
|
|
69
|
-
);
|
|
70
|
-
const editableColumn = result.editing.columns.find(
|
|
71
|
-
(column) => column.sourceColumn === "mention_type"
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
assert.deepEqual(editableColumn.allowedValues, mentionTypeColumn.allowedValues);
|
|
75
|
-
|
|
76
|
-
assert.equal(designerDraft.designerVersion, 2);
|
|
77
|
-
assert.equal(designerDraft.checkConstraints.length, 1);
|
|
78
|
-
assert.match(designerDraft.checkConstraints[0].expression, /CHECK/i);
|
|
79
|
-
assert.equal(designerDraft.checkConstraints[0].originalExpression, designerDraft.checkConstraints[0].expression);
|
|
80
|
-
assert.equal(designerDraft.checkConstraints[0].editable, true);
|
|
81
|
-
assert.deepEqual(designerDraft.checkConstraints[0].columns, [
|
|
82
|
-
{
|
|
83
|
-
name: "mention_type",
|
|
84
|
-
allowedValues: mentionTypeColumn.allowedValues,
|
|
85
|
-
},
|
|
86
|
-
]);
|
|
87
|
-
} finally {
|
|
88
|
-
db.close();
|
|
89
|
-
}
|
|
90
|
-
});
|
package/tests/cli-args.test.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const test = require("node:test");
|
|
3
|
-
|
|
4
|
-
const {
|
|
5
|
-
normalizeExportFormat,
|
|
6
|
-
parseCliArguments,
|
|
7
|
-
} = require("../bin/sqlite-hub");
|
|
8
|
-
|
|
9
|
-
test("parses database execute command", () => {
|
|
10
|
-
const options = parseCliArguments([
|
|
11
|
-
"--database:trump live interviews",
|
|
12
|
-
"--execute:Stock Winners",
|
|
13
|
-
]);
|
|
14
|
-
|
|
15
|
-
assert.equal(options.databaseName, "trump live interviews");
|
|
16
|
-
assert.equal(options.executeQuery, "Stock Winners");
|
|
17
|
-
assert.equal(options.queries, false);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test("parses database table list command", () => {
|
|
21
|
-
const options = parseCliArguments(["--database:trump live interviews", "--tables"]);
|
|
22
|
-
|
|
23
|
-
assert.equal(options.databaseName, "trump live interviews");
|
|
24
|
-
assert.equal(options.tables, true);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("parses database info commands with the new schema", () => {
|
|
28
|
-
assert.equal(parseCliArguments(["--database:db", "--path"]).pathInfo, true);
|
|
29
|
-
assert.equal(parseCliArguments(["--database:db", "---path"]).pathInfo, true);
|
|
30
|
-
assert.equal(parseCliArguments(["--database:db", "--size"]).sizeInfo, true);
|
|
31
|
-
assert.equal(parseCliArguments(["--database:db", "--lastopened"]).lastOpenedInfo, true);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("parses app info command and keeps config as a legacy alias", () => {
|
|
35
|
-
assert.equal(parseCliArguments(["--info"]).info, true);
|
|
36
|
-
assert.equal(parseCliArguments(["--config"]).info, true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("keeps old sqleditor aliases working", () => {
|
|
40
|
-
const listOptions = parseCliArguments(["--database:Unit-00", "--sqleditor"]);
|
|
41
|
-
const executeOptions = parseCliArguments(["--database:Unit-00", "--sqleditor:Saved Query"]);
|
|
42
|
-
|
|
43
|
-
assert.equal(listOptions.queries, true);
|
|
44
|
-
assert.equal(executeOptions.executeQuery, "Saved Query");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("keeps old database detail aliases working", () => {
|
|
48
|
-
const pathOptions = parseCliArguments(["--database-path:Billly", "--queries"]);
|
|
49
|
-
const tableOptions = parseCliArguments(["--database-tables:Billly"]);
|
|
50
|
-
|
|
51
|
-
assert.equal(pathOptions.databaseName, "Billly");
|
|
52
|
-
assert.equal(pathOptions.pathInfo, true);
|
|
53
|
-
assert.equal(pathOptions.queries, true);
|
|
54
|
-
assert.equal(tableOptions.databaseName, "Billly");
|
|
55
|
-
assert.equal(tableOptions.tables, true);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("parses raw query, store name, saved query display, and export commands", () => {
|
|
59
|
-
const rawOptions = parseCliArguments([
|
|
60
|
-
"--database:db",
|
|
61
|
-
"--query:SELECT 1",
|
|
62
|
-
"--store:Stored Select",
|
|
63
|
-
]);
|
|
64
|
-
const showOptions = parseCliArguments(["--database:db", "--saved-query:Stock Winners"]);
|
|
65
|
-
const notesOptions = parseCliArguments(["--database:db", "--notes:Stock Winners"]);
|
|
66
|
-
const exportOptions = parseCliArguments([
|
|
67
|
-
"--database:db",
|
|
68
|
-
"--export:Stock Winners",
|
|
69
|
-
"--format:md",
|
|
70
|
-
]);
|
|
71
|
-
|
|
72
|
-
assert.equal(rawOptions.rawQuery, "SELECT 1");
|
|
73
|
-
assert.equal(rawOptions.storeName, "Stored Select");
|
|
74
|
-
assert.equal(showOptions.showQuery, "Stock Winners");
|
|
75
|
-
assert.equal(notesOptions.showNotes, "Stock Winners");
|
|
76
|
-
assert.equal(exportOptions.exportTarget, "Stock Winners");
|
|
77
|
-
assert.equal(exportOptions.exportFormat, "md");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("parses row json export command", () => {
|
|
81
|
-
const options = parseCliArguments([
|
|
82
|
-
"--database:db",
|
|
83
|
-
"--table:companies",
|
|
84
|
-
"--export:0a754aba373d34972998792a0be4333c",
|
|
85
|
-
]);
|
|
86
|
-
|
|
87
|
-
assert.equal(options.tableName, "companies");
|
|
88
|
-
assert.equal(options.exportTarget, "0a754aba373d34972998792a0be4333c");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("parses document commands", () => {
|
|
92
|
-
const listOptions = parseCliArguments(["--database:db", "--documents"]);
|
|
93
|
-
const showOptions = parseCliArguments(["--database:db", "--documents:Research Note"]);
|
|
94
|
-
const exportOptions = parseCliArguments(["--database:db", "--documents:Research Note", "--export"]);
|
|
95
|
-
const compactExportOptions = parseCliArguments(["--database:db", "--documents:Research Note--export"]);
|
|
96
|
-
|
|
97
|
-
assert.equal(listOptions.documents, true);
|
|
98
|
-
assert.equal(listOptions.documentName, null);
|
|
99
|
-
assert.equal(showOptions.documents, true);
|
|
100
|
-
assert.equal(showOptions.documentName, "Research Note");
|
|
101
|
-
assert.equal(showOptions.documentExport, false);
|
|
102
|
-
assert.equal(exportOptions.documentName, "Research Note");
|
|
103
|
-
assert.equal(exportOptions.documentExport, true);
|
|
104
|
-
assert.equal(compactExportOptions.documentName, "Research Note");
|
|
105
|
-
assert.equal(compactExportOptions.documentExport, true);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("validates export formats", () => {
|
|
109
|
-
assert.equal(normalizeExportFormat("csv"), "csv");
|
|
110
|
-
assert.equal(normalizeExportFormat("TSV"), "tsv");
|
|
111
|
-
assert.equal(normalizeExportFormat("json"), "json");
|
|
112
|
-
assert.throws(() => normalizeExportFormat("xlsx"), /Unsupported export format/);
|
|
113
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const test = require("node:test");
|
|
3
|
-
|
|
4
|
-
const { main } = require("../bin/sqlite-hub");
|
|
5
|
-
|
|
6
|
-
test("CLI delegates database operations to the shared command service", async () => {
|
|
7
|
-
const calls = [];
|
|
8
|
-
const connection = {
|
|
9
|
-
id: "db-one",
|
|
10
|
-
label: "Database One",
|
|
11
|
-
};
|
|
12
|
-
const databaseService = {
|
|
13
|
-
getDatabase(reference) {
|
|
14
|
-
calls.push(["getDatabase", reference]);
|
|
15
|
-
return connection;
|
|
16
|
-
},
|
|
17
|
-
listDatabases() {
|
|
18
|
-
calls.push(["listDatabases"]);
|
|
19
|
-
return [connection];
|
|
20
|
-
},
|
|
21
|
-
listTables(reference) {
|
|
22
|
-
calls.push(["listTables", reference]);
|
|
23
|
-
return [{ name: "companies" }];
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
const output = [];
|
|
27
|
-
const originalLog = console.log;
|
|
28
|
-
|
|
29
|
-
console.log = (...values) => output.push(values.join(" "));
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
await main(["--database:Database One", "--tables"], { databaseService });
|
|
33
|
-
} finally {
|
|
34
|
-
console.log = originalLog;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
assert.deepEqual(calls, [
|
|
38
|
-
["listDatabases"],
|
|
39
|
-
["getDatabase", "Database One"],
|
|
40
|
-
["listTables", "db-one"],
|
|
41
|
-
]);
|
|
42
|
-
assert.match(output.join("\n"), /companies/);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("CLI marks raw query executions as cli", async () => {
|
|
46
|
-
const calls = [];
|
|
47
|
-
const connection = {
|
|
48
|
-
id: "db-one",
|
|
49
|
-
label: "Database One",
|
|
50
|
-
};
|
|
51
|
-
const databaseService = {
|
|
52
|
-
getDatabase(reference) {
|
|
53
|
-
calls.push(["getDatabase", reference]);
|
|
54
|
-
return connection;
|
|
55
|
-
},
|
|
56
|
-
listDatabases() {
|
|
57
|
-
calls.push(["listDatabases"]);
|
|
58
|
-
return [connection];
|
|
59
|
-
},
|
|
60
|
-
executeRawQuery(reference, sql, options = {}) {
|
|
61
|
-
calls.push(["executeRawQuery", reference, sql, options]);
|
|
62
|
-
return {
|
|
63
|
-
result: {
|
|
64
|
-
statementCount: 1,
|
|
65
|
-
timingMs: 1,
|
|
66
|
-
statements: [],
|
|
67
|
-
historyId: 7,
|
|
68
|
-
},
|
|
69
|
-
storedQuery: null,
|
|
70
|
-
};
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
const output = [];
|
|
74
|
-
const originalLog = console.log;
|
|
75
|
-
|
|
76
|
-
console.log = (...values) => output.push(values.join(" "));
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
await main(["--database:Database One", "--query:SELECT 1"], { databaseService });
|
|
80
|
-
} finally {
|
|
81
|
-
console.log = originalLog;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
assert.deepEqual(calls, [
|
|
85
|
-
["listDatabases"],
|
|
86
|
-
["getDatabase", "Database One"],
|
|
87
|
-
["executeRawQuery", "db-one", "SELECT 1", { storeName: null, executedBy: "cli" }],
|
|
88
|
-
]);
|
|
89
|
-
assert.match(output.join("\n"), /History ID: 7/);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test("CLI marks saved query executions as cli", async () => {
|
|
93
|
-
const calls = [];
|
|
94
|
-
const connection = {
|
|
95
|
-
id: "db-one",
|
|
96
|
-
label: "Database One",
|
|
97
|
-
};
|
|
98
|
-
const databaseService = {
|
|
99
|
-
getDatabase(reference) {
|
|
100
|
-
calls.push(["getDatabase", reference]);
|
|
101
|
-
return connection;
|
|
102
|
-
},
|
|
103
|
-
listDatabases() {
|
|
104
|
-
calls.push(["listDatabases"]);
|
|
105
|
-
return [connection];
|
|
106
|
-
},
|
|
107
|
-
executeSavedQuery(reference, queryName, options = {}) {
|
|
108
|
-
calls.push(["executeSavedQuery", reference, queryName, options]);
|
|
109
|
-
return {
|
|
110
|
-
query: {
|
|
111
|
-
title: queryName,
|
|
112
|
-
rawSql: "SELECT 1",
|
|
113
|
-
},
|
|
114
|
-
result: {
|
|
115
|
-
statementCount: 1,
|
|
116
|
-
timingMs: 1,
|
|
117
|
-
statements: [],
|
|
118
|
-
historyId: 8,
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
const output = [];
|
|
124
|
-
const originalLog = console.log;
|
|
125
|
-
|
|
126
|
-
console.log = (...values) => output.push(values.join(" "));
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
await main(["--database:Database One", "--execute:Hype-Reversal"], { databaseService });
|
|
130
|
-
} finally {
|
|
131
|
-
console.log = originalLog;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
assert.deepEqual(calls, [
|
|
135
|
-
["listDatabases"],
|
|
136
|
-
["getDatabase", "Database One"],
|
|
137
|
-
["executeSavedQuery", "db-one", "Hype-Reversal", { executedBy: "cli" }],
|
|
138
|
-
]);
|
|
139
|
-
assert.match(output.join("\n"), /Executing: Hype-Reversal/);
|
|
140
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const fs = require("node:fs");
|
|
3
|
-
const os = require("node:os");
|
|
4
|
-
const path = require("node:path");
|
|
5
|
-
const test = require("node:test");
|
|
6
|
-
|
|
7
|
-
const { ConnectionManager } = require("../server/services/sqlite/connectionManager");
|
|
8
|
-
const { AppStateStore } = require("../server/services/storage/appStateStore");
|
|
9
|
-
|
|
10
|
-
function createStore(t) {
|
|
11
|
-
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "sqlite-hub-connection-remove-"));
|
|
12
|
-
const store = new AppStateStore(path.join(directory, "state.db"));
|
|
13
|
-
|
|
14
|
-
t.after(() => {
|
|
15
|
-
store.db.close();
|
|
16
|
-
fs.rmSync(directory, { recursive: true, force: true });
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
return { directory, store };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
test("removing a recent connection does not require legacy sql_history", (t) => {
|
|
23
|
-
const { directory, store } = createStore(t);
|
|
24
|
-
const firstConnection = {
|
|
25
|
-
id: "db-one",
|
|
26
|
-
label: "One",
|
|
27
|
-
path: path.join(directory, "one.db"),
|
|
28
|
-
lastOpenedAt: "2026-06-21T10:00:00.000Z",
|
|
29
|
-
lastModifiedAt: null,
|
|
30
|
-
sizeBytes: 0,
|
|
31
|
-
readOnly: false,
|
|
32
|
-
logoPath: null,
|
|
33
|
-
};
|
|
34
|
-
const secondConnection = {
|
|
35
|
-
...firstConnection,
|
|
36
|
-
id: "db-two",
|
|
37
|
-
label: "Two",
|
|
38
|
-
path: path.join(directory, "two.db"),
|
|
39
|
-
};
|
|
40
|
-
const manager = new ConnectionManager({ appStateStore: store });
|
|
41
|
-
|
|
42
|
-
store.upsertRecentConnection(firstConnection);
|
|
43
|
-
store.upsertRecentConnection(secondConnection, { makeActive: false });
|
|
44
|
-
store.db.exec("DROP TABLE sql_history");
|
|
45
|
-
|
|
46
|
-
const remaining = manager.removeRecentConnection(firstConnection.id);
|
|
47
|
-
|
|
48
|
-
assert.deepEqual(
|
|
49
|
-
remaining.map((connection) => connection.id),
|
|
50
|
-
["db-two"]
|
|
51
|
-
);
|
|
52
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
const assert = require("node:assert/strict");
|
|
2
|
-
const express = require("express");
|
|
3
|
-
const test = require("node:test");
|
|
4
|
-
const { createConnectionsRouter } = require("../server/routes/connections");
|
|
5
|
-
const { errorMiddleware } = require("../server/utils/errors");
|
|
6
|
-
|
|
7
|
-
test("connections route returns the path selected by the native dialog", async () => {
|
|
8
|
-
const app = express();
|
|
9
|
-
app.use(express.json());
|
|
10
|
-
app.use(
|
|
11
|
-
"/api/connections",
|
|
12
|
-
createConnectionsRouter({
|
|
13
|
-
connectionManager: {},
|
|
14
|
-
importService: {},
|
|
15
|
-
backupService: {},
|
|
16
|
-
nativeFileDialogService: {
|
|
17
|
-
chooseCreateDatabasePath: async () => "/tmp/new-database.sqlite",
|
|
18
|
-
chooseOpenDatabasePath: async () => "/tmp/existing-database.db",
|
|
19
|
-
},
|
|
20
|
-
})
|
|
21
|
-
);
|
|
22
|
-
app.use(errorMiddleware);
|
|
23
|
-
|
|
24
|
-
const server = await new Promise((resolve) => {
|
|
25
|
-
const listener = app.listen(0, "127.0.0.1", () => resolve(listener));
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const address = server.address();
|
|
30
|
-
const response = await fetch(
|
|
31
|
-
`http://127.0.0.1:${address.port}/api/connections/choose-create-path`,
|
|
32
|
-
{ method: "POST" }
|
|
33
|
-
);
|
|
34
|
-
const payload = await response.json();
|
|
35
|
-
|
|
36
|
-
assert.equal(response.status, 200);
|
|
37
|
-
assert.equal(payload.success, true);
|
|
38
|
-
assert.deepEqual(payload.data, {
|
|
39
|
-
cancelled: false,
|
|
40
|
-
path: "/tmp/new-database.sqlite",
|
|
41
|
-
});
|
|
42
|
-
} finally {
|
|
43
|
-
await new Promise((resolve, reject) => {
|
|
44
|
-
server.close((error) => (error ? reject(error) : resolve()));
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("connections route returns the existing database selected by the native dialog", async () => {
|
|
50
|
-
const app = express();
|
|
51
|
-
app.use(express.json());
|
|
52
|
-
app.use(
|
|
53
|
-
"/api/connections",
|
|
54
|
-
createConnectionsRouter({
|
|
55
|
-
connectionManager: {},
|
|
56
|
-
importService: {},
|
|
57
|
-
backupService: {},
|
|
58
|
-
nativeFileDialogService: {
|
|
59
|
-
chooseCreateDatabasePath: async () => null,
|
|
60
|
-
chooseOpenDatabasePath: async () => "/tmp/existing-database.db",
|
|
61
|
-
},
|
|
62
|
-
})
|
|
63
|
-
);
|
|
64
|
-
app.use(errorMiddleware);
|
|
65
|
-
|
|
66
|
-
const server = await new Promise((resolve) => {
|
|
67
|
-
const listener = app.listen(0, "127.0.0.1", () => resolve(listener));
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
const address = server.address();
|
|
72
|
-
const response = await fetch(
|
|
73
|
-
`http://127.0.0.1:${address.port}/api/connections/choose-open-path`,
|
|
74
|
-
{ method: "POST" }
|
|
75
|
-
);
|
|
76
|
-
const payload = await response.json();
|
|
77
|
-
|
|
78
|
-
assert.equal(response.status, 200);
|
|
79
|
-
assert.equal(payload.success, true);
|
|
80
|
-
assert.deepEqual(payload.data, {
|
|
81
|
-
cancelled: false,
|
|
82
|
-
path: "/tmp/existing-database.db",
|
|
83
|
-
});
|
|
84
|
-
} finally {
|
|
85
|
-
await new Promise((resolve, reject) => {
|
|
86
|
-
server.close((error) => (error ? reject(error) : resolve()));
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
});
|