sqlite-hub 0.12.0 → 0.17.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 +118 -23
- package/bin/sqlite-hub.js +1041 -224
- package/frontend/index.html +1 -0
- package/frontend/js/api.js +32 -2
- package/frontend/js/app.js +989 -13
- package/frontend/js/components/modal.js +644 -15
- package/frontend/js/components/pageHeader.js +14 -16
- package/frontend/js/components/queryEditor.js +9 -1
- package/frontend/js/components/queryHistoryPanel.js +126 -131
- package/frontend/js/components/queryResults.js +79 -17
- package/frontend/js/components/rowEditorPanel.js +204 -10
- package/frontend/js/components/sidebar.js +102 -18
- package/frontend/js/components/tableDesignerEditor.js +69 -11
- package/frontend/js/router.js +8 -0
- package/frontend/js/store.js +868 -9
- package/frontend/js/utils/copyColumnExport.js +117 -0
- package/frontend/js/utils/exportFilenames.js +32 -0
- package/frontend/js/utils/filePathPreview.js +315 -0
- package/frontend/js/utils/format.js +1 -1
- package/frontend/js/utils/markdownDocuments.js +248 -0
- package/frontend/js/utils/rowEditorJson.js +65 -0
- package/frontend/js/utils/sqlFormatter.js +691 -0
- package/frontend/js/utils/tableDesigner.js +178 -6
- package/frontend/js/utils/textCellStats.js +20 -0
- package/frontend/js/utils/timestampPreview.js +264 -0
- package/frontend/js/views/charts.js +3 -1
- package/frontend/js/views/data.js +34 -2
- package/frontend/js/views/documents.js +300 -0
- package/frontend/js/views/editor.js +48 -1
- package/frontend/js/views/settings.js +39 -6
- package/frontend/js/views/structure.js +154 -212
- package/frontend/styles/base.css +6 -0
- package/frontend/styles/components.css +476 -2
- package/frontend/styles/structure-graph.css +0 -3
- package/frontend/styles/tailwind.generated.css +1 -1
- package/frontend/styles/tokens.css +1 -1
- package/frontend/styles/views.css +422 -0
- package/package.json +3 -3
- package/server/routes/documents.js +163 -0
- package/server/routes/settings.js +22 -6
- package/server/server.js +6 -0
- package/server/services/sqlite/introspection.js +10 -0
- package/server/services/sqlite/sqlExecutor.js +29 -0
- package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
- package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
- package/server/services/sqlite/tableDesigner/validation.js +60 -2
- package/server/services/sqlite/tableDesignerService.js +1 -1
- package/server/services/storage/appStateStore.js +313 -0
- package/tests/check-constraint-options.test.js +14 -0
- package/tests/cli-args.test.js +100 -0
- package/tests/copy-column-modal.test.js +83 -0
- package/tests/database-documents.test.js +85 -0
- package/tests/export-filenames.test.js +34 -0
- package/tests/file-path-preview.test.js +165 -0
- package/tests/markdown-documents.test.js +79 -0
- package/tests/row-editor-json.test.js +82 -0
- package/tests/row-editor-timestamp-preview.test.js +192 -0
- package/tests/settings-metadata.test.js +16 -0
- package/tests/settings-view.test.js +32 -0
- package/tests/sql-formatter.test.js +173 -0
- package/tests/sql-highlight.test.js +38 -0
- package/tests/table-designer-v2-unique-constraints.test.js +78 -0
- package/tests/text-cell-stats.test.js +38 -0
- package/fill.js +0 -526
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const { readFileSync } = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const test = require("node:test");
|
|
5
|
+
|
|
6
|
+
let rowEditorJsonModulePromise = null;
|
|
7
|
+
|
|
8
|
+
function loadRowEditorJsonModule() {
|
|
9
|
+
if (!rowEditorJsonModulePromise) {
|
|
10
|
+
const source = readFileSync(
|
|
11
|
+
path.resolve(__dirname, "../frontend/js/utils/rowEditorJson.js"),
|
|
12
|
+
"utf8"
|
|
13
|
+
);
|
|
14
|
+
const url = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
|
|
15
|
+
|
|
16
|
+
rowEditorJsonModulePromise = import(url);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return rowEditorJsonModulePromise;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test("row editor JSON for data rows preserves column order and omits identity metadata", async () => {
|
|
23
|
+
const { buildDataRowEditorJsonObject, stringifyRowEditorJson } = await loadRowEditorJsonModule();
|
|
24
|
+
const row = {
|
|
25
|
+
id: 1,
|
|
26
|
+
title: "Event",
|
|
27
|
+
created_at: 1717682400,
|
|
28
|
+
payload: { __type: "blob", sizeBytes: 4, hexPreview: "ffff" },
|
|
29
|
+
__identity: { kind: "primaryKey", values: { id: 1 } },
|
|
30
|
+
};
|
|
31
|
+
const rowObject = buildDataRowEditorJsonObject({
|
|
32
|
+
row,
|
|
33
|
+
columns: ["id", "title", "created_at", "payload"],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
assert.deepEqual(Object.keys(rowObject), ["id", "title", "created_at", "payload"]);
|
|
37
|
+
assert.deepEqual(rowObject, {
|
|
38
|
+
id: 1,
|
|
39
|
+
title: "Event",
|
|
40
|
+
created_at: 1717682400,
|
|
41
|
+
payload: { __type: "blob", sizeBytes: 4, hexPreview: "ffff" },
|
|
42
|
+
});
|
|
43
|
+
assert.equal(
|
|
44
|
+
stringifyRowEditorJson(rowObject),
|
|
45
|
+
[
|
|
46
|
+
"{",
|
|
47
|
+
' "id": 1,',
|
|
48
|
+
' "title": "Event",',
|
|
49
|
+
' "created_at": 1717682400,',
|
|
50
|
+
' "payload": {',
|
|
51
|
+
' "__type": "blob",',
|
|
52
|
+
' "sizeBytes": 4,',
|
|
53
|
+
' "hexPreview": "ffff"',
|
|
54
|
+
" }",
|
|
55
|
+
"}",
|
|
56
|
+
].join("\n")
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("row editor JSON for SQL result rows maps visible source columns once", async () => {
|
|
61
|
+
const { buildEditorRowEditorJsonObject } = await loadRowEditorJsonModule();
|
|
62
|
+
const rowObject = buildEditorRowEditorJsonObject({
|
|
63
|
+
row: {
|
|
64
|
+
id: 1,
|
|
65
|
+
title_alias: "Event",
|
|
66
|
+
duplicate_title: "Ignored",
|
|
67
|
+
hidden_note: "Hidden",
|
|
68
|
+
__identity: { kind: "primaryKey", values: { id: 1 } },
|
|
69
|
+
},
|
|
70
|
+
editingColumns: [
|
|
71
|
+
{ resultName: "id", sourceColumn: "id", visible: true },
|
|
72
|
+
{ resultName: "title_alias", sourceColumn: "title", visible: true },
|
|
73
|
+
{ resultName: "duplicate_title", sourceColumn: "title", visible: true },
|
|
74
|
+
{ resultName: "hidden_note", sourceColumn: "note", visible: false },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
assert.deepEqual(rowObject, {
|
|
79
|
+
id: 1,
|
|
80
|
+
title: "Event",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
|
|
4
|
+
const { version } = require("../package.json");
|
|
5
|
+
const { readSettingsMetadata, readSqliteVersion } = require("../server/routes/settings");
|
|
6
|
+
|
|
7
|
+
test("settings metadata exposes app and SQLite versions", () => {
|
|
8
|
+
const metadata = readSettingsMetadata();
|
|
9
|
+
|
|
10
|
+
assert.equal(metadata.appVersion, version);
|
|
11
|
+
assert.match(metadata.sqliteVersion, /^\d+\.\d+\.\d+$/);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("SQLite runtime version is readable", () => {
|
|
15
|
+
assert.match(readSqliteVersion(), /^\d+\.\d+\.\d+$/);
|
|
16
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
});
|