@tensorix/metahub 0.1.8 → 0.1.9
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/cli.js +523 -216
- package/dist/cli.js.map +12 -12
- package/dist/core/db.d.ts +7 -0
- package/dist/core/db.d.ts.map +1 -1
- package/dist/core/documents.d.ts +9 -0
- package/dist/core/documents.d.ts.map +1 -1
- package/dist/core/schema.d.ts +1 -1
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/sites.d.ts +21 -0
- package/dist/core/sites.d.ts.map +1 -1
- package/dist/core/sync/sites-routes.d.ts.map +1 -1
- package/dist/core/sync/webui-routes.d.ts.map +1 -1
- package/dist/core/sync/webui.d.ts.map +1 -1
- package/dist/index.js +435 -130
- package/dist/index.js.map +11 -11
- package/dist/webui.js +49 -49
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -342,7 +342,7 @@ var init_crdt = __esm(() => {
|
|
|
342
342
|
},
|
|
343
343
|
documents: {
|
|
344
344
|
table: "documents",
|
|
345
|
-
cols: new Set(["title", "body", "database_id", "parent_id", "created_hlc", "__deleted"])
|
|
345
|
+
cols: new Set(["title", "body", "database_id", "parent_id", "created_hlc", "order_key", "__deleted"])
|
|
346
346
|
},
|
|
347
347
|
doc_blocks: {
|
|
348
348
|
table: "doc_blocks",
|
|
@@ -398,6 +398,24 @@ function inferContentType(path) {
|
|
|
398
398
|
const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
|
|
399
399
|
return MIME[ext] ?? "application/octet-stream";
|
|
400
400
|
}
|
|
401
|
+
function normalizeSiteName(raw) {
|
|
402
|
+
const slug = raw.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
403
|
+
if (!slug)
|
|
404
|
+
throw new Error(`invalid site name: ${JSON.stringify(raw)}`);
|
|
405
|
+
return slug;
|
|
406
|
+
}
|
|
407
|
+
function normalizeSitePath(raw) {
|
|
408
|
+
const segs = [];
|
|
409
|
+
for (const seg of raw.split("/")) {
|
|
410
|
+
if (seg === "" || seg === ".")
|
|
411
|
+
continue;
|
|
412
|
+
if (seg === "..")
|
|
413
|
+
segs.pop();
|
|
414
|
+
else
|
|
415
|
+
segs.push(seg);
|
|
416
|
+
}
|
|
417
|
+
return segs.join("/");
|
|
418
|
+
}
|
|
401
419
|
function isTextType(contentType) {
|
|
402
420
|
const ct = contentType.toLowerCase();
|
|
403
421
|
return ct.startsWith("text/") || ct.startsWith("application/json") || ct.startsWith("application/manifest+json") || ct.startsWith("application/xml") || ct.startsWith("text/javascript") || ct.startsWith("application/javascript") || ct.startsWith("image/svg+xml");
|
|
@@ -408,10 +426,11 @@ function toBytes(data) {
|
|
|
408
426
|
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
409
427
|
}
|
|
410
428
|
function createSite(db, opts) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
429
|
+
const name = normalizeSiteName(opts.name);
|
|
430
|
+
if (getSiteByName(db, name))
|
|
431
|
+
throw new Error(`site name already exists: ${name}`);
|
|
432
|
+
const id = newId("site", name);
|
|
433
|
+
const first = emit(db, "sites", id, "name", name);
|
|
415
434
|
emit(db, "sites", id, "created_hlc", first.hlc);
|
|
416
435
|
if (opts.title !== undefined)
|
|
417
436
|
emit(db, "sites", id, "title", opts.title);
|
|
@@ -421,7 +440,13 @@ function getSite(db, id) {
|
|
|
421
440
|
return db.query("SELECT id, name, title, created_hlc FROM sites WHERE id = ? AND __deleted = 0").get(id);
|
|
422
441
|
}
|
|
423
442
|
function getSiteByName(db, name) {
|
|
424
|
-
|
|
443
|
+
let slug;
|
|
444
|
+
try {
|
|
445
|
+
slug = normalizeSiteName(name);
|
|
446
|
+
} catch {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
return db.query("SELECT id, name, title, created_hlc FROM sites WHERE name = ? AND __deleted = 0 ORDER BY created_hlc DESC LIMIT 1").get(slug);
|
|
425
450
|
}
|
|
426
451
|
function listSites(db) {
|
|
427
452
|
return db.query("SELECT id, name, title, created_hlc FROM sites WHERE __deleted = 0 ORDER BY created_hlc").all();
|
|
@@ -433,6 +458,20 @@ function resolveSite(db, ref) {
|
|
|
433
458
|
throw new Error(`no such site: ${ref}`);
|
|
434
459
|
return site;
|
|
435
460
|
}
|
|
461
|
+
function updateSite(db, id, opts) {
|
|
462
|
+
if (!getSite(db, id))
|
|
463
|
+
throw new Error(`no such site: ${id}`);
|
|
464
|
+
if (opts.name !== undefined) {
|
|
465
|
+
const name = normalizeSiteName(opts.name);
|
|
466
|
+
const dup = getSiteByName(db, name);
|
|
467
|
+
if (dup && dup.id !== id)
|
|
468
|
+
throw new Error(`site name already exists: ${name}`);
|
|
469
|
+
emit(db, "sites", id, "name", name);
|
|
470
|
+
}
|
|
471
|
+
if (opts.title !== undefined)
|
|
472
|
+
emit(db, "sites", id, "title", opts.title);
|
|
473
|
+
return getSite(db, id);
|
|
474
|
+
}
|
|
436
475
|
function deleteSite(db, id) {
|
|
437
476
|
if (!getSite(db, id))
|
|
438
477
|
return false;
|
|
@@ -447,7 +486,10 @@ function fileIdFor(db, siteId, path) {
|
|
|
447
486
|
return row?.id ?? null;
|
|
448
487
|
}
|
|
449
488
|
async function putFile(db, siteId, path, opts) {
|
|
450
|
-
const
|
|
489
|
+
const cleanPath = normalizeSitePath(path);
|
|
490
|
+
if (!cleanPath)
|
|
491
|
+
throw new Error(`invalid file path: ${JSON.stringify(path)}`);
|
|
492
|
+
const contentType = opts.contentType ?? inferContentType(cleanPath);
|
|
451
493
|
const bytes = toBytes(opts.data);
|
|
452
494
|
let encoding;
|
|
453
495
|
let content;
|
|
@@ -461,11 +503,11 @@ async function putFile(db, siteId, path, opts) {
|
|
|
461
503
|
encoding = "blob";
|
|
462
504
|
content = (await putBlob(bytes)).hash;
|
|
463
505
|
}
|
|
464
|
-
const existing = fileIdFor(db, siteId,
|
|
465
|
-
const id = existing ?? newId("sf",
|
|
506
|
+
const existing = fileIdFor(db, siteId, cleanPath);
|
|
507
|
+
const id = existing ?? newId("sf", cleanPath);
|
|
466
508
|
if (!existing) {
|
|
467
509
|
const first = emit(db, "site_files", id, "site_id", siteId);
|
|
468
|
-
emit(db, "site_files", id, "path",
|
|
510
|
+
emit(db, "site_files", id, "path", cleanPath);
|
|
469
511
|
emit(db, "site_files", id, "created_hlc", first.hlc);
|
|
470
512
|
}
|
|
471
513
|
emit(db, "site_files", id, "content_type", contentType);
|
|
@@ -478,7 +520,7 @@ function listFiles(db, siteId) {
|
|
|
478
520
|
return db.query("SELECT id, site_id, path, content_type, encoding FROM site_files WHERE site_id = ? AND __deleted = 0 ORDER BY path").all(siteId);
|
|
479
521
|
}
|
|
480
522
|
function deleteFile(db, siteId, path) {
|
|
481
|
-
const id = fileIdFor(db, siteId, path);
|
|
523
|
+
const id = fileIdFor(db, siteId, normalizeSitePath(path));
|
|
482
524
|
if (!id)
|
|
483
525
|
return false;
|
|
484
526
|
const live = db.query("SELECT __deleted AS d FROM site_files WHERE id = ?").get(id);
|
|
@@ -488,7 +530,10 @@ function deleteFile(db, siteId, path) {
|
|
|
488
530
|
return true;
|
|
489
531
|
}
|
|
490
532
|
async function getFileForServe(db, siteId, path) {
|
|
491
|
-
const
|
|
533
|
+
const withIndex = path === "" || path.endsWith("/") ? `${path}index.html` : path;
|
|
534
|
+
const p = normalizeSitePath(withIndex);
|
|
535
|
+
if (!p)
|
|
536
|
+
return null;
|
|
492
537
|
const row = db.query("SELECT content_type, encoding, content FROM site_files WHERE site_id = ? AND path = ? AND __deleted = 0 ORDER BY created_hlc LIMIT 1").get(siteId, p);
|
|
493
538
|
if (!row)
|
|
494
539
|
return null;
|
|
@@ -684,7 +729,7 @@ var HTML = `<!doctype html>
|
|
|
684
729
|
.sb-section-head .add { opacity:0; padding:3px; border-radius:5px; color:var(--muted); transition:opacity .12s; }
|
|
685
730
|
.sb-section:hover .add { opacity:1; }
|
|
686
731
|
.sb-section-head .add:hover { background:var(--hover-2); color:var(--fg); }
|
|
687
|
-
.navitem { display:flex; align-items:center; gap:3px; width:100%; text-align:left; padding:5px 6px; border-radius:var(--radius-sm);
|
|
732
|
+
.navitem { display:flex; align-items:center; gap:3px; width:100%; text-align:left; padding:5px 6px; margin-bottom:2px; border-radius:var(--radius-sm);
|
|
688
733
|
color:var(--fg-soft); font-size:13.5px; position:relative; white-space:nowrap; transition:background .1s; }
|
|
689
734
|
.navitem:hover { background:var(--hover-2); }
|
|
690
735
|
.navitem.active { background:var(--bg); color:var(--fg); font-weight:600; box-shadow:var(--shadow-sm); }
|
|
@@ -793,6 +838,9 @@ var HTML = `<!doctype html>
|
|
|
793
838
|
body.desktop-mac .sb-head .iconbtn { -webkit-app-region:no-drag; }
|
|
794
839
|
body.desktop-mac .topbar { -webkit-app-region:drag; }
|
|
795
840
|
body.desktop-mac .topbar .iconbtn, body.desktop-mac .topbar .btn { -webkit-app-region:no-drag; }
|
|
841
|
+
/* peek drawer header overlaps the topbar drag region \u2192 clear its buttons so they stay clickable */
|
|
842
|
+
body.desktop-mac .peek-head { -webkit-app-region:drag; }
|
|
843
|
+
body.desktop-mac .peek-head .iconbtn, body.desktop-mac .peek-head .btn { -webkit-app-region:no-drag; }
|
|
796
844
|
/* sidebar collapsed \u2192 traffic lights sit over the main header; clear the hamburger */
|
|
797
845
|
body.desktop-mac .sidebar.collapsed ~ .main .topbar { padding-left:80px; }
|
|
798
846
|
|
|
@@ -1027,6 +1075,83 @@ var HTML = `<!doctype html>
|
|
|
1027
1075
|
.gridfoot { display:flex; gap:18px; color:var(--muted); font-size:12px; padding:8px 2px; }
|
|
1028
1076
|
input.inlineedit { width:100%; border:0; outline:2px solid var(--accent); border-radius:4px; padding:7px 9px; background:var(--bg); color:var(--fg); font:inherit; }
|
|
1029
1077
|
|
|
1078
|
+
/* ---- sites (\u7AD9\u70B9\u7BA1\u7406) ---- */
|
|
1079
|
+
.sites-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(264px, 1fr)); gap:14px; margin-top:6px; }
|
|
1080
|
+
.site-card { display:flex; flex-direction:column; gap:13px; padding:16px 16px 14px; background:var(--bg); border:1px solid var(--line);
|
|
1081
|
+
border-radius:var(--radius); box-shadow:var(--shadow-sm); cursor:pointer; transition:box-shadow .16s, border-color .16s, transform .16s; }
|
|
1082
|
+
.site-card:hover { box-shadow:var(--shadow-md); border-color:var(--line-strong); transform:translateY(-1px); }
|
|
1083
|
+
.site-card-head { display:flex; align-items:center; gap:11px; min-width:0; }
|
|
1084
|
+
.site-card .si { width:40px; height:40px; border-radius:11px; background:var(--accent-soft); color:var(--accent); display:grid; place-items:center; flex:none; }
|
|
1085
|
+
.site-card .si svg { width:20px; height:20px; }
|
|
1086
|
+
.site-card-id { min-width:0; }
|
|
1087
|
+
.site-card .slug { font-family:var(--mono); font-weight:600; font-size:14.5px; color:var(--fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1088
|
+
.site-card .ttl { font-size:12px; color:var(--fg-soft); margin-top:1px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1089
|
+
.site-card .ttl.muted { color:var(--muted); font-style:italic; }
|
|
1090
|
+
.site-addr { display:flex; align-items:center; gap:6px; width:100%; text-align:left; background:var(--surface); border:1px solid var(--line);
|
|
1091
|
+
border-radius:var(--radius-sm); padding:6px 9px; color:var(--muted); font-family:var(--mono); font-size:11.5px; transition:border-color .12s, color .12s; }
|
|
1092
|
+
.site-addr svg { flex:none; }
|
|
1093
|
+
.site-addr span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1094
|
+
.site-addr:hover { border-color:var(--accent); color:var(--accent); }
|
|
1095
|
+
.site-card-meta { display:flex; align-items:center; gap:7px; font-size:12px; color:var(--muted); }
|
|
1096
|
+
.site-card-meta b { color:var(--fg-soft); font-weight:600; font-variant-numeric:tabular-nums; }
|
|
1097
|
+
.site-card-meta .dot { color:var(--line-strong); }
|
|
1098
|
+
.site-card-foot { display:flex; align-items:center; gap:6px; padding-top:12px; margin-top:1px; border-top:1px solid var(--line); }
|
|
1099
|
+
.site-card-foot .btn-secondary { padding:6px 12px; justify-content:center; }
|
|
1100
|
+
|
|
1101
|
+
.site-empty { text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--radius); padding:48px 24px; margin-top:8px; }
|
|
1102
|
+
.site-empty .ei { font-size:30px; margin-bottom:10px; }
|
|
1103
|
+
.site-empty .et { font-size:14px; color:var(--fg-soft); margin-bottom:4px; font-weight:600; }
|
|
1104
|
+
.site-empty .ed { font-size:12.5px; margin-bottom:16px; }
|
|
1105
|
+
|
|
1106
|
+
/* site peek: access bar + file manager */
|
|
1107
|
+
.acc-link { display:flex; align-items:center; gap:6px; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius-sm); padding:7px 8px 7px 11px; margin-bottom:16px; }
|
|
1108
|
+
.acc-link .url { flex:1; font-family:var(--mono); font-size:12.5px; color:var(--fg-soft); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1109
|
+
.acc-link button { width:28px; height:28px; display:grid; place-items:center; border-radius:6px; color:var(--muted); flex:none; }
|
|
1110
|
+
.acc-link button:hover { background:var(--hover-2); color:var(--fg); }
|
|
1111
|
+
.acc-link button.accent { color:var(--accent); }
|
|
1112
|
+
.site-meta { display:flex; gap:18px; color:var(--muted); font-size:12px; margin-bottom:20px; }
|
|
1113
|
+
.site-meta b { color:var(--fg); font-weight:600; }
|
|
1114
|
+
.files-head { display:flex; align-items:center; justify-content:space-between; font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); margin:0 0 4px; }
|
|
1115
|
+
.filerow { display:flex; align-items:center; gap:10px; padding:9px 8px; border-radius:var(--radius-sm); cursor:pointer; border-bottom:1px solid var(--line); }
|
|
1116
|
+
.filerow:last-child { border-bottom:0; }
|
|
1117
|
+
.filerow:hover { background:var(--hover); }
|
|
1118
|
+
.filerow .fi { color:var(--muted); display:grid; place-items:center; flex:none; }
|
|
1119
|
+
.filerow .fpath { flex:1; font-family:var(--mono); font-size:12.5px; color:var(--fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1120
|
+
.filerow .facts { display:flex; gap:1px; opacity:0; flex:none; }
|
|
1121
|
+
.filerow:hover .facts { opacity:1; }
|
|
1122
|
+
.filerow .facts button { width:27px; height:27px; display:grid; place-items:center; border-radius:6px; color:var(--muted); }
|
|
1123
|
+
.filerow .facts button:hover { background:var(--hover-2); color:var(--fg); }
|
|
1124
|
+
.filerow .facts button.del:hover { color:var(--danger); }
|
|
1125
|
+
.enc-badge { display:inline-flex; align-items:center; font-family:var(--mono); font-size:10.5px; font-weight:500; letter-spacing:.02em;
|
|
1126
|
+
padding:2px 6px; border-radius:5px; background:var(--surface-2); color:var(--fg-soft); border:1px solid var(--line); }
|
|
1127
|
+
.enc-badge.blob { color:var(--accent); background:var(--accent-soft); border-color:transparent; }
|
|
1128
|
+
|
|
1129
|
+
/* file preview (inside modal) */
|
|
1130
|
+
.preview-box { font-family:var(--mono); font-size:12.5px; background:var(--surface-2); border:1px solid var(--line); border-radius:var(--radius);
|
|
1131
|
+
padding:14px 16px; max-height:52vh; overflow:auto; white-space:pre-wrap; line-height:1.6; color:var(--fg); }
|
|
1132
|
+
.preview-bin { text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--radius); padding:40px 20px; }
|
|
1133
|
+
.preview-bin .pi { font-size:28px; margin-bottom:8px; }
|
|
1134
|
+
.preview-bin .pt { color:var(--fg-soft); font-size:13px; }
|
|
1135
|
+
|
|
1136
|
+
/* in-app site preview (browser-framed iframe) */
|
|
1137
|
+
.spv-scrim { position:fixed; inset:0; background:rgba(15,15,25,.5); z-index:85; display:grid; place-items:center; padding:28px;
|
|
1138
|
+
opacity:0; pointer-events:none; transition:opacity .16s; animation:spv-in .16s forwards; }
|
|
1139
|
+
@keyframes spv-in { to { opacity:1; pointer-events:auto; } }
|
|
1140
|
+
.spv-scrim.open { opacity:1; pointer-events:auto; }
|
|
1141
|
+
.spv { width:min(1100px, 92vw); height:88vh; background:var(--bg); border-radius:14px; box-shadow:var(--shadow-lg);
|
|
1142
|
+
display:flex; flex-direction:column; overflow:hidden; }
|
|
1143
|
+
.spv-chrome { display:flex; align-items:center; gap:10px; padding:9px 12px; border-bottom:1px solid var(--line); background:var(--surface); }
|
|
1144
|
+
.spv-dots { display:flex; gap:6px; flex:none; }
|
|
1145
|
+
.spv-dots i { width:11px; height:11px; border-radius:50%; background:var(--line-strong); }
|
|
1146
|
+
.spv-url { flex:1; display:flex; align-items:center; gap:6px; min-width:0; background:var(--bg); border:1px solid var(--line);
|
|
1147
|
+
border-radius:7px; padding:5px 10px; color:var(--fg-soft); font-family:var(--mono); font-size:12px; }
|
|
1148
|
+
.spv-url svg { color:var(--muted); flex:none; }
|
|
1149
|
+
.spv-url span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1150
|
+
.spv-btn { display:inline-flex; align-items:center; gap:5px; flex:none; padding:6px 10px; border-radius:7px; color:var(--fg-soft); font-size:12.5px; font-weight:500; }
|
|
1151
|
+
.spv-btn:hover { background:var(--hover-2); color:var(--fg); }
|
|
1152
|
+
.spv-btn.icon { padding:6px; }
|
|
1153
|
+
.spv-frame { flex:1; width:100%; border:0; background:#fff; }
|
|
1154
|
+
|
|
1030
1155
|
.selbar { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); z-index:65; display:flex; align-items:center; gap:4px;
|
|
1031
1156
|
background:var(--fg); color:var(--bg); padding:7px 8px 7px 14px; border-radius:11px; box-shadow:var(--shadow-lg); }
|
|
1032
1157
|
.selbar .cnt { font-size:13px; font-weight:600; margin-right:6px; }
|
|
@@ -1478,6 +1603,7 @@ CREATE TABLE IF NOT EXISTS documents (
|
|
|
1478
1603
|
database_id TEXT,
|
|
1479
1604
|
parent_id TEXT,
|
|
1480
1605
|
created_hlc TEXT,
|
|
1606
|
+
order_key TEXT, -- fractional index among siblings (same parent_id); ties broken by created_hlc,id
|
|
1481
1607
|
__deleted INTEGER NOT NULL DEFAULT 0
|
|
1482
1608
|
);
|
|
1483
1609
|
|
|
@@ -2120,103 +2246,6 @@ function deleteRecord(db, id) {
|
|
|
2120
2246
|
return true;
|
|
2121
2247
|
}
|
|
2122
2248
|
|
|
2123
|
-
// src/core/db.ts
|
|
2124
|
-
function ensureDirs() {
|
|
2125
|
-
mkdirSync(metahubHome(), { recursive: true });
|
|
2126
|
-
mkdirSync(cacheDir(), { recursive: true });
|
|
2127
|
-
}
|
|
2128
|
-
function runSchema(db) {
|
|
2129
|
-
db.exec(CORE_SCHEMA);
|
|
2130
|
-
try {
|
|
2131
|
-
db.exec(FTS_SCHEMA);
|
|
2132
|
-
} catch {}
|
|
2133
|
-
}
|
|
2134
|
-
function ftsAvailable(db) {
|
|
2135
|
-
const row = db.query("SELECT 1 AS ok FROM sqlite_master WHERE name = 'search_fts'").get();
|
|
2136
|
-
return row != null;
|
|
2137
|
-
}
|
|
2138
|
-
function hasColumn(db, table, column) {
|
|
2139
|
-
const cols = db.query(`PRAGMA table_info(${table})`).all();
|
|
2140
|
-
return cols.some((c) => c.name === column);
|
|
2141
|
-
}
|
|
2142
|
-
function tableExists2(db, table) {
|
|
2143
|
-
return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
|
|
2144
|
-
}
|
|
2145
|
-
function migrateRecords(db) {
|
|
2146
|
-
if (!hasColumn(db, "records", "data"))
|
|
2147
|
-
db.exec("ALTER TABLE records ADD COLUMN data TEXT NOT NULL DEFAULT '{}'");
|
|
2148
|
-
if (!hasColumn(db, "records", "order_key"))
|
|
2149
|
-
db.exec("ALTER TABLE records ADD COLUMN order_key TEXT");
|
|
2150
|
-
if (!tableExists2(db, "record_values"))
|
|
2151
|
-
return;
|
|
2152
|
-
const tx = db.transaction(() => {
|
|
2153
|
-
db.exec(`
|
|
2154
|
-
UPDATE records SET data = coalesce((
|
|
2155
|
-
SELECT json_group_object(rv.property_id, json(rv.value))
|
|
2156
|
-
FROM record_values rv WHERE rv.record_id = records.id
|
|
2157
|
-
), '{}')
|
|
2158
|
-
WHERE id IN (SELECT DISTINCT record_id FROM record_values);
|
|
2159
|
-
`);
|
|
2160
|
-
db.exec("DROP TABLE record_values");
|
|
2161
|
-
backfillRecordOrderKeys(db);
|
|
2162
|
-
});
|
|
2163
|
-
tx();
|
|
2164
|
-
backfillRecordOrderKeys(db);
|
|
2165
|
-
}
|
|
2166
|
-
function migratePeers(db) {
|
|
2167
|
-
const add = [
|
|
2168
|
-
["token", "TEXT"],
|
|
2169
|
-
["label", "TEXT"],
|
|
2170
|
-
["node_id", "TEXT"],
|
|
2171
|
-
["enabled", "INTEGER NOT NULL DEFAULT 1"],
|
|
2172
|
-
["last_sync_at", "INTEGER"],
|
|
2173
|
-
["last_status", "TEXT"],
|
|
2174
|
-
["last_error", "TEXT"]
|
|
2175
|
-
];
|
|
2176
|
-
for (const [col, decl] of add) {
|
|
2177
|
-
if (!hasColumn(db, "peers", col))
|
|
2178
|
-
db.exec(`ALTER TABLE peers ADD COLUMN ${col} ${decl}`);
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
|
-
function migrateDocBlocks(db) {
|
|
2182
|
-
if (!hasColumn(db, "doc_blocks", "blank_after"))
|
|
2183
|
-
db.exec("ALTER TABLE doc_blocks ADD COLUMN blank_after INTEGER NOT NULL DEFAULT 0");
|
|
2184
|
-
}
|
|
2185
|
-
function openMetahub() {
|
|
2186
|
-
ensureDirs();
|
|
2187
|
-
const db = new Database(dbPath(), { create: true });
|
|
2188
|
-
db.exec("PRAGMA journal_mode = WAL;");
|
|
2189
|
-
runSchema(db);
|
|
2190
|
-
migrateRecords(db);
|
|
2191
|
-
migratePeers(db);
|
|
2192
|
-
migrateDocBlocks(db);
|
|
2193
|
-
return db;
|
|
2194
|
-
}
|
|
2195
|
-
|
|
2196
|
-
// src/core/index.ts
|
|
2197
|
-
init_ids();
|
|
2198
|
-
init_crdt();
|
|
2199
|
-
init_node();
|
|
2200
|
-
|
|
2201
|
-
// src/core/context.ts
|
|
2202
|
-
var KEY = "current_db";
|
|
2203
|
-
function getCurrentDatabase(db) {
|
|
2204
|
-
const row = db.query("SELECT value FROM meta WHERE key = ?").get(KEY);
|
|
2205
|
-
if (!row)
|
|
2206
|
-
return null;
|
|
2207
|
-
const found = getDatabase(db, row.value);
|
|
2208
|
-
if (!found) {
|
|
2209
|
-
clearCurrentDatabase(db);
|
|
2210
|
-
return null;
|
|
2211
|
-
}
|
|
2212
|
-
return found;
|
|
2213
|
-
}
|
|
2214
|
-
function setCurrentDatabase(db, id) {
|
|
2215
|
-
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(KEY, id);
|
|
2216
|
-
}
|
|
2217
|
-
function clearCurrentDatabase(db) {
|
|
2218
|
-
db.query("DELETE FROM meta WHERE key = ?").run(KEY);
|
|
2219
|
-
}
|
|
2220
2249
|
// src/core/documents.ts
|
|
2221
2250
|
init_ids();
|
|
2222
2251
|
init_crdt();
|
|
@@ -2288,6 +2317,7 @@ function createDocument(db, opts) {
|
|
|
2288
2317
|
emit(db, "documents", id, "database_id", opts.database_id);
|
|
2289
2318
|
if (opts.parent_id !== undefined)
|
|
2290
2319
|
emit(db, "documents", id, "parent_id", opts.parent_id);
|
|
2320
|
+
placeInSiblings(db, id, opts.parent_id ?? null);
|
|
2291
2321
|
if (opts.body !== undefined) {
|
|
2292
2322
|
const blocks = parseDocBlocks(opts.body);
|
|
2293
2323
|
if (blocks.length)
|
|
@@ -2296,15 +2326,111 @@ function createDocument(db, opts) {
|
|
|
2296
2326
|
return getDocument(db, id);
|
|
2297
2327
|
}
|
|
2298
2328
|
function getDocument(db, id) {
|
|
2299
|
-
return db.query("SELECT id, title, body, database_id, parent_id, created_hlc FROM documents WHERE id = ? AND __deleted = 0").get(id);
|
|
2329
|
+
return db.query("SELECT id, title, body, database_id, parent_id, created_hlc, order_key FROM documents WHERE id = ? AND __deleted = 0").get(id);
|
|
2300
2330
|
}
|
|
2331
|
+
var ORDER_BY = "ORDER BY order_key IS NULL, order_key, created_hlc, id";
|
|
2301
2332
|
function listDocuments(db, opts = {}) {
|
|
2302
|
-
const cols = "SELECT id, title, database_id, parent_id, created_hlc FROM documents WHERE __deleted = 0";
|
|
2333
|
+
const cols = "SELECT id, title, database_id, parent_id, created_hlc, order_key FROM documents WHERE __deleted = 0";
|
|
2303
2334
|
if (opts.parent_id !== undefined)
|
|
2304
|
-
return db.query(`${cols} AND parent_id = ?
|
|
2335
|
+
return db.query(`${cols} AND parent_id = ? ${ORDER_BY}`).all(opts.parent_id);
|
|
2305
2336
|
if (opts.database_id)
|
|
2306
|
-
return db.query(`${cols} AND database_id = ?
|
|
2307
|
-
return db.query(`${cols}
|
|
2337
|
+
return db.query(`${cols} AND database_id = ? ${ORDER_BY}`).all(opts.database_id);
|
|
2338
|
+
return db.query(`${cols} ${ORDER_BY}`).all();
|
|
2339
|
+
}
|
|
2340
|
+
function siblingWhere(parentId) {
|
|
2341
|
+
return parentId === null ? { clause: "parent_id IS NULL", args: [] } : { clause: "parent_id = ?", args: [parentId] };
|
|
2342
|
+
}
|
|
2343
|
+
function canEmitOrderKeys2(db) {
|
|
2344
|
+
return tableExists2(db, "meta") && tableExists2(db, "crdt_changes");
|
|
2345
|
+
}
|
|
2346
|
+
function tableExists2(db, table) {
|
|
2347
|
+
return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
|
|
2348
|
+
}
|
|
2349
|
+
function lastSiblingOrderKey(db, parentId, excludeId) {
|
|
2350
|
+
const w = siblingWhere(parentId);
|
|
2351
|
+
const exclude = excludeId ? " AND id <> ?" : "";
|
|
2352
|
+
const row = db.query(`SELECT order_key FROM documents WHERE ${w.clause}${exclude}
|
|
2353
|
+
AND __deleted = 0 AND order_key IS NOT NULL
|
|
2354
|
+
ORDER BY order_key DESC, id DESC LIMIT 1`).get(...w.args, ...excludeId ? [excludeId] : []);
|
|
2355
|
+
return row?.order_key ?? null;
|
|
2356
|
+
}
|
|
2357
|
+
function orderedSiblings(db, parentId) {
|
|
2358
|
+
const w = siblingWhere(parentId);
|
|
2359
|
+
return db.query(`SELECT id, order_key FROM documents WHERE ${w.clause} AND __deleted = 0 ${ORDER_BY}`).all(...w.args);
|
|
2360
|
+
}
|
|
2361
|
+
function rebalanceSiblings(db, parentId) {
|
|
2362
|
+
const rows = orderedSiblings(db, parentId);
|
|
2363
|
+
const keys = keysBetween(null, null, rows.length);
|
|
2364
|
+
rows.forEach((row, i) => {
|
|
2365
|
+
if (row.order_key !== keys[i])
|
|
2366
|
+
emit(db, "documents", row.id, "order_key", keys[i]);
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
function backfillSiblings(db, parentId) {
|
|
2370
|
+
const emitChange = canEmitOrderKeys2(db);
|
|
2371
|
+
const w = siblingWhere(parentId);
|
|
2372
|
+
const missing = db.query(`SELECT id FROM documents WHERE ${w.clause} AND __deleted = 0 AND order_key IS NULL
|
|
2373
|
+
ORDER BY created_hlc, id`).all(...w.args);
|
|
2374
|
+
if (!missing.length)
|
|
2375
|
+
return;
|
|
2376
|
+
const keys = keysBetween(lastSiblingOrderKey(db, parentId), null, missing.length);
|
|
2377
|
+
missing.forEach((row, i) => {
|
|
2378
|
+
if (emitChange)
|
|
2379
|
+
emit(db, "documents", row.id, "order_key", keys[i]);
|
|
2380
|
+
else
|
|
2381
|
+
db.query("UPDATE documents SET order_key = ? WHERE id = ?").run(keys[i], row.id);
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
function backfillDocumentOrderKeys(db) {
|
|
2385
|
+
const parents = db.query("SELECT DISTINCT parent_id FROM documents WHERE __deleted = 0 AND order_key IS NULL").all();
|
|
2386
|
+
for (const p of parents)
|
|
2387
|
+
backfillSiblings(db, p.parent_id);
|
|
2388
|
+
}
|
|
2389
|
+
function placeInSiblings(db, id, parentId, anchor) {
|
|
2390
|
+
let cur = parentId;
|
|
2391
|
+
while (cur) {
|
|
2392
|
+
if (cur === id)
|
|
2393
|
+
throw new Error(`cannot set parent_id: would create a cycle (${id})`);
|
|
2394
|
+
cur = getDocument(db, cur)?.parent_id ?? null;
|
|
2395
|
+
}
|
|
2396
|
+
const current = getDocument(db, id);
|
|
2397
|
+
if (current && (current.parent_id ?? null) !== parentId)
|
|
2398
|
+
emit(db, "documents", id, "parent_id", parentId);
|
|
2399
|
+
let key;
|
|
2400
|
+
if (!anchor) {
|
|
2401
|
+
key = keyBetween(lastSiblingOrderKey(db, parentId, id), null);
|
|
2402
|
+
} else {
|
|
2403
|
+
backfillSiblings(db, parentId);
|
|
2404
|
+
const neighbors = () => {
|
|
2405
|
+
const rows = orderedSiblings(db, parentId).filter((r) => r.id !== id);
|
|
2406
|
+
const to = rows.findIndex((r) => r.id === anchor.targetId);
|
|
2407
|
+
if (to < 0)
|
|
2408
|
+
throw new Error(`no such target document: ${anchor.targetId}`);
|
|
2409
|
+
return anchor.where === "before" ? [rows[to - 1]?.order_key ?? null, rows[to].order_key] : [rows[to].order_key, rows[to + 1]?.order_key ?? null];
|
|
2410
|
+
};
|
|
2411
|
+
let [left, right] = neighbors();
|
|
2412
|
+
if (left !== null && right !== null && left >= right) {
|
|
2413
|
+
rebalanceSiblings(db, parentId);
|
|
2414
|
+
[left, right] = neighbors();
|
|
2415
|
+
}
|
|
2416
|
+
key = keyBetween(left, right);
|
|
2417
|
+
}
|
|
2418
|
+
emit(db, "documents", id, "order_key", key);
|
|
2419
|
+
}
|
|
2420
|
+
function moveDocument(db, id, targetId, where) {
|
|
2421
|
+
const src = getDocument(db, id);
|
|
2422
|
+
if (!src)
|
|
2423
|
+
throw new Error(`no such document: ${id}`);
|
|
2424
|
+
if (id === targetId)
|
|
2425
|
+
return src;
|
|
2426
|
+
const target = getDocument(db, targetId);
|
|
2427
|
+
if (!target)
|
|
2428
|
+
throw new Error(`no such target document: ${targetId}`);
|
|
2429
|
+
if (where === "into")
|
|
2430
|
+
placeInSiblings(db, id, targetId);
|
|
2431
|
+
else
|
|
2432
|
+
placeInSiblings(db, id, target.parent_id, { targetId, where });
|
|
2433
|
+
return getDocument(db, id);
|
|
2308
2434
|
}
|
|
2309
2435
|
function updateDocument(db, id, fields) {
|
|
2310
2436
|
if (!getDocument(db, id))
|
|
@@ -2314,13 +2440,9 @@ function updateDocument(db, id, fields) {
|
|
|
2314
2440
|
if (fields.database_id !== undefined)
|
|
2315
2441
|
emit(db, "documents", id, "database_id", fields.database_id);
|
|
2316
2442
|
if (fields.parent_id !== undefined) {
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
throw new Error(`cannot set parent_id: would create a cycle (${id})`);
|
|
2321
|
-
cur = getDocument(db, cur)?.parent_id ?? null;
|
|
2322
|
-
}
|
|
2323
|
-
emit(db, "documents", id, "parent_id", fields.parent_id);
|
|
2443
|
+
const next = fields.parent_id ?? null;
|
|
2444
|
+
if ((getDocument(db, id).parent_id ?? null) !== next)
|
|
2445
|
+
placeInSiblings(db, id, next);
|
|
2324
2446
|
}
|
|
2325
2447
|
if (fields.body !== undefined) {
|
|
2326
2448
|
ensureBlocks(db, id);
|
|
@@ -2408,6 +2530,110 @@ function countOccurrences(haystack, needle) {
|
|
|
2408
2530
|
}
|
|
2409
2531
|
return n;
|
|
2410
2532
|
}
|
|
2533
|
+
|
|
2534
|
+
// src/core/db.ts
|
|
2535
|
+
function ensureDirs() {
|
|
2536
|
+
mkdirSync(metahubHome(), { recursive: true });
|
|
2537
|
+
mkdirSync(cacheDir(), { recursive: true });
|
|
2538
|
+
}
|
|
2539
|
+
function runSchema(db) {
|
|
2540
|
+
db.exec(CORE_SCHEMA);
|
|
2541
|
+
try {
|
|
2542
|
+
db.exec(FTS_SCHEMA);
|
|
2543
|
+
} catch {}
|
|
2544
|
+
}
|
|
2545
|
+
function ftsAvailable(db) {
|
|
2546
|
+
const row = db.query("SELECT 1 AS ok FROM sqlite_master WHERE name = 'search_fts'").get();
|
|
2547
|
+
return row != null;
|
|
2548
|
+
}
|
|
2549
|
+
function hasColumn(db, table, column) {
|
|
2550
|
+
const cols = db.query(`PRAGMA table_info(${table})`).all();
|
|
2551
|
+
return cols.some((c) => c.name === column);
|
|
2552
|
+
}
|
|
2553
|
+
function tableExists3(db, table) {
|
|
2554
|
+
return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
|
|
2555
|
+
}
|
|
2556
|
+
function migrateRecords(db) {
|
|
2557
|
+
if (!hasColumn(db, "records", "data"))
|
|
2558
|
+
db.exec("ALTER TABLE records ADD COLUMN data TEXT NOT NULL DEFAULT '{}'");
|
|
2559
|
+
if (!hasColumn(db, "records", "order_key"))
|
|
2560
|
+
db.exec("ALTER TABLE records ADD COLUMN order_key TEXT");
|
|
2561
|
+
if (!tableExists3(db, "record_values"))
|
|
2562
|
+
return;
|
|
2563
|
+
const tx = db.transaction(() => {
|
|
2564
|
+
db.exec(`
|
|
2565
|
+
UPDATE records SET data = coalesce((
|
|
2566
|
+
SELECT json_group_object(rv.property_id, json(rv.value))
|
|
2567
|
+
FROM record_values rv WHERE rv.record_id = records.id
|
|
2568
|
+
), '{}')
|
|
2569
|
+
WHERE id IN (SELECT DISTINCT record_id FROM record_values);
|
|
2570
|
+
`);
|
|
2571
|
+
db.exec("DROP TABLE record_values");
|
|
2572
|
+
backfillRecordOrderKeys(db);
|
|
2573
|
+
});
|
|
2574
|
+
tx();
|
|
2575
|
+
backfillRecordOrderKeys(db);
|
|
2576
|
+
}
|
|
2577
|
+
function migratePeers(db) {
|
|
2578
|
+
const add = [
|
|
2579
|
+
["token", "TEXT"],
|
|
2580
|
+
["label", "TEXT"],
|
|
2581
|
+
["node_id", "TEXT"],
|
|
2582
|
+
["enabled", "INTEGER NOT NULL DEFAULT 1"],
|
|
2583
|
+
["last_sync_at", "INTEGER"],
|
|
2584
|
+
["last_status", "TEXT"],
|
|
2585
|
+
["last_error", "TEXT"]
|
|
2586
|
+
];
|
|
2587
|
+
for (const [col, decl] of add) {
|
|
2588
|
+
if (!hasColumn(db, "peers", col))
|
|
2589
|
+
db.exec(`ALTER TABLE peers ADD COLUMN ${col} ${decl}`);
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
function migrateDocBlocks(db) {
|
|
2593
|
+
if (!hasColumn(db, "doc_blocks", "blank_after"))
|
|
2594
|
+
db.exec("ALTER TABLE doc_blocks ADD COLUMN blank_after INTEGER NOT NULL DEFAULT 0");
|
|
2595
|
+
}
|
|
2596
|
+
function migrateDocuments(db) {
|
|
2597
|
+
if (!hasColumn(db, "documents", "order_key"))
|
|
2598
|
+
db.exec("ALTER TABLE documents ADD COLUMN order_key TEXT");
|
|
2599
|
+
backfillDocumentOrderKeys(db);
|
|
2600
|
+
}
|
|
2601
|
+
function openMetahub() {
|
|
2602
|
+
ensureDirs();
|
|
2603
|
+
const db = new Database(dbPath(), { create: true });
|
|
2604
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
2605
|
+
runSchema(db);
|
|
2606
|
+
migrateRecords(db);
|
|
2607
|
+
migratePeers(db);
|
|
2608
|
+
migrateDocBlocks(db);
|
|
2609
|
+
migrateDocuments(db);
|
|
2610
|
+
return db;
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// src/core/index.ts
|
|
2614
|
+
init_ids();
|
|
2615
|
+
init_crdt();
|
|
2616
|
+
init_node();
|
|
2617
|
+
|
|
2618
|
+
// src/core/context.ts
|
|
2619
|
+
var KEY = "current_db";
|
|
2620
|
+
function getCurrentDatabase(db) {
|
|
2621
|
+
const row = db.query("SELECT value FROM meta WHERE key = ?").get(KEY);
|
|
2622
|
+
if (!row)
|
|
2623
|
+
return null;
|
|
2624
|
+
const found = getDatabase(db, row.value);
|
|
2625
|
+
if (!found) {
|
|
2626
|
+
clearCurrentDatabase(db);
|
|
2627
|
+
return null;
|
|
2628
|
+
}
|
|
2629
|
+
return found;
|
|
2630
|
+
}
|
|
2631
|
+
function setCurrentDatabase(db, id) {
|
|
2632
|
+
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(KEY, id);
|
|
2633
|
+
}
|
|
2634
|
+
function clearCurrentDatabase(db) {
|
|
2635
|
+
db.query("DELETE FROM meta WHERE key = ?").run(KEY);
|
|
2636
|
+
}
|
|
2411
2637
|
// src/core/search.ts
|
|
2412
2638
|
init_crdt();
|
|
2413
2639
|
var TEXT_TYPES = "('text','url','select','multi_select','date')";
|
|
@@ -17028,7 +17254,7 @@ function getServerConfig(db) {
|
|
|
17028
17254
|
// package.json
|
|
17029
17255
|
var package_default = {
|
|
17030
17256
|
name: "@tensorix/metahub",
|
|
17031
|
-
version: "0.1.
|
|
17257
|
+
version: "0.1.9",
|
|
17032
17258
|
description: "Metahub core library and CLI",
|
|
17033
17259
|
license: "AGPL-3.0-only",
|
|
17034
17260
|
type: "module",
|
|
@@ -17097,7 +17323,7 @@ var PropertySchema = exports_external.object({
|
|
|
17097
17323
|
var RecordSchema = exports_external.object({
|
|
17098
17324
|
id: exports_external.string(),
|
|
17099
17325
|
database_id: exports_external.string(),
|
|
17100
|
-
values: exports_external.record(exports_external.string(), exports_external.any())
|
|
17326
|
+
values: exports_external.record(exports_external.string(), exports_external.any()).describe('Field values keyed by property name (e.g. {"Amount":35}). Reads nest cells here; writes take a flat {column: value} map.')
|
|
17101
17327
|
});
|
|
17102
17328
|
var MoveRecordReq = exports_external.object({
|
|
17103
17329
|
target: exports_external.string(),
|
|
@@ -17108,7 +17334,12 @@ var DocumentSummarySchema = exports_external.object({
|
|
|
17108
17334
|
title: exports_external.string(),
|
|
17109
17335
|
database_id: exports_external.string().nullable(),
|
|
17110
17336
|
parent_id: exports_external.string().nullable(),
|
|
17111
|
-
created_hlc: exports_external.string()
|
|
17337
|
+
created_hlc: exports_external.string(),
|
|
17338
|
+
order_key: exports_external.string().nullable()
|
|
17339
|
+
});
|
|
17340
|
+
var MoveDocumentReq = exports_external.object({
|
|
17341
|
+
target: exports_external.string(),
|
|
17342
|
+
where: exports_external.enum(["before", "after", "into"])
|
|
17112
17343
|
});
|
|
17113
17344
|
var DocumentSchema = DocumentSummarySchema.extend({ body: exports_external.string().nullable() });
|
|
17114
17345
|
var SearchHitSchema = exports_external.object({
|
|
@@ -17137,7 +17368,7 @@ var UpdatePropertyReq = exports_external.object({
|
|
|
17137
17368
|
position: exports_external.number().optional()
|
|
17138
17369
|
});
|
|
17139
17370
|
var SetWidthReq = exports_external.object({ width: exports_external.number() });
|
|
17140
|
-
var RecordValuesReq = exports_external.record(exports_external.string(), exports_external.any());
|
|
17371
|
+
var RecordValuesReq = exports_external.record(exports_external.string(), exports_external.any()).describe("Flat { column: value } cell patch (keyed by property name).");
|
|
17141
17372
|
var CreateDocumentReq = exports_external.object({
|
|
17142
17373
|
title: exports_external.string(),
|
|
17143
17374
|
body: exports_external.string().optional(),
|
|
@@ -17364,6 +17595,17 @@ var webuiRoutes = [
|
|
|
17364
17595
|
return updateDocument(db, need(req, "id"), body);
|
|
17365
17596
|
})
|
|
17366
17597
|
},
|
|
17598
|
+
{
|
|
17599
|
+
method: "PATCH",
|
|
17600
|
+
path: "/api/document/move",
|
|
17601
|
+
summary: "Move a document before/after another, or into it as a child. Query: ?id=<id>",
|
|
17602
|
+
request: MoveDocumentReq,
|
|
17603
|
+
response: DocumentSchema,
|
|
17604
|
+
handler: handle(async (req, { db }) => {
|
|
17605
|
+
const body = await req.json();
|
|
17606
|
+
return moveDocument(db, need(req, "id"), body.target, body.where);
|
|
17607
|
+
})
|
|
17608
|
+
},
|
|
17367
17609
|
{
|
|
17368
17610
|
method: "DELETE",
|
|
17369
17611
|
path: "/api/document",
|
|
@@ -17391,6 +17633,7 @@ var SiteSchema = exports_external.object({
|
|
|
17391
17633
|
title: exports_external.string().nullable(),
|
|
17392
17634
|
created_hlc: exports_external.string()
|
|
17393
17635
|
});
|
|
17636
|
+
var SiteWithCountSchema = SiteSchema.extend({ file_count: exports_external.number() });
|
|
17394
17637
|
var SiteFileSchema = exports_external.object({
|
|
17395
17638
|
id: exports_external.string(),
|
|
17396
17639
|
site_id: exports_external.string(),
|
|
@@ -17398,6 +17641,8 @@ var SiteFileSchema = exports_external.object({
|
|
|
17398
17641
|
content_type: exports_external.string(),
|
|
17399
17642
|
encoding: exports_external.string()
|
|
17400
17643
|
});
|
|
17644
|
+
var CreateSiteBody = exports_external.object({ name: exports_external.string(), title: exports_external.string().optional() });
|
|
17645
|
+
var UpdateSiteBody = exports_external.object({ name: exports_external.string().optional(), title: exports_external.string().optional() });
|
|
17401
17646
|
function need2(req, key) {
|
|
17402
17647
|
const v = new URL(req.url).searchParams.get(key);
|
|
17403
17648
|
if (!v)
|
|
@@ -17417,9 +17662,9 @@ var sitesRoutes = [
|
|
|
17417
17662
|
{
|
|
17418
17663
|
method: "GET",
|
|
17419
17664
|
path: "/api/sites",
|
|
17420
|
-
summary: "List published sites",
|
|
17421
|
-
response: exports_external.array(
|
|
17422
|
-
handler: handle2((_req, { db }) => listSites(db))
|
|
17665
|
+
summary: "List published sites (with file counts)",
|
|
17666
|
+
response: exports_external.array(SiteWithCountSchema),
|
|
17667
|
+
handler: handle2((_req, { db }) => listSites(db).map((s) => ({ ...s, file_count: listFiles(db, s.id).length })))
|
|
17423
17668
|
},
|
|
17424
17669
|
{
|
|
17425
17670
|
method: "GET",
|
|
@@ -17427,6 +17672,60 @@ var sitesRoutes = [
|
|
|
17427
17672
|
summary: "List a site's files (manifest). Query: ?site=<id|name>",
|
|
17428
17673
|
response: exports_external.array(SiteFileSchema),
|
|
17429
17674
|
handler: handle2((req, { db }) => listFiles(db, resolveSite(db, need2(req, "site")).id))
|
|
17675
|
+
},
|
|
17676
|
+
{
|
|
17677
|
+
method: "POST",
|
|
17678
|
+
path: "/api/sites",
|
|
17679
|
+
summary: "Create a site",
|
|
17680
|
+
request: CreateSiteBody,
|
|
17681
|
+
response: SiteSchema,
|
|
17682
|
+
handler: handle2(async (req, { db }) => {
|
|
17683
|
+
const body = CreateSiteBody.parse(await req.json());
|
|
17684
|
+
return createSite(db, body);
|
|
17685
|
+
})
|
|
17686
|
+
},
|
|
17687
|
+
{
|
|
17688
|
+
method: "PATCH",
|
|
17689
|
+
path: "/api/site",
|
|
17690
|
+
summary: "Rename a site or change its title. Query: ?id=<id>",
|
|
17691
|
+
request: UpdateSiteBody,
|
|
17692
|
+
response: SiteSchema,
|
|
17693
|
+
handler: handle2(async (req, { db }) => {
|
|
17694
|
+
const body = UpdateSiteBody.parse(await req.json());
|
|
17695
|
+
return updateSite(db, need2(req, "id"), body);
|
|
17696
|
+
})
|
|
17697
|
+
},
|
|
17698
|
+
{
|
|
17699
|
+
method: "DELETE",
|
|
17700
|
+
path: "/api/site",
|
|
17701
|
+
summary: "Delete a site and its files. Query: ?id=<id>",
|
|
17702
|
+
response: exports_external.object({ ok: exports_external.boolean() }),
|
|
17703
|
+
handler: handle2((req, { db }) => ({ ok: deleteSite(db, need2(req, "id")) }))
|
|
17704
|
+
},
|
|
17705
|
+
{
|
|
17706
|
+
method: "POST",
|
|
17707
|
+
path: "/api/site/file",
|
|
17708
|
+
summary: "Upload/replace a file. Query: ?site=<id|name>&path=<path>; body is raw bytes",
|
|
17709
|
+
response: SiteFileSchema,
|
|
17710
|
+
handler: handle2(async (req, { db }) => {
|
|
17711
|
+
const site = resolveSite(db, need2(req, "site"));
|
|
17712
|
+
const ct = req.headers.get("content-type");
|
|
17713
|
+
const file2 = await putFile(db, site.id, need2(req, "path"), {
|
|
17714
|
+
data: await req.arrayBuffer(),
|
|
17715
|
+
contentType: ct && ct !== "application/octet-stream" ? ct : undefined
|
|
17716
|
+
});
|
|
17717
|
+
const { content: _content, ...summary } = file2;
|
|
17718
|
+
return summary;
|
|
17719
|
+
})
|
|
17720
|
+
},
|
|
17721
|
+
{
|
|
17722
|
+
method: "DELETE",
|
|
17723
|
+
path: "/api/site/file",
|
|
17724
|
+
summary: "Delete a file. Query: ?site=<id|name>&path=<path>",
|
|
17725
|
+
response: exports_external.object({ ok: exports_external.boolean() }),
|
|
17726
|
+
handler: handle2((req, { db }) => ({
|
|
17727
|
+
ok: deleteFile(db, resolveSite(db, need2(req, "site")).id, need2(req, "path"))
|
|
17728
|
+
}))
|
|
17430
17729
|
}
|
|
17431
17730
|
];
|
|
17432
17731
|
|
|
@@ -18021,6 +18320,7 @@ function startServer(opts = {}) {
|
|
|
18021
18320
|
};
|
|
18022
18321
|
}
|
|
18023
18322
|
export {
|
|
18323
|
+
updateSite,
|
|
18024
18324
|
updateRecord,
|
|
18025
18325
|
updateProperty,
|
|
18026
18326
|
updateDocument,
|
|
@@ -18048,11 +18348,15 @@ export {
|
|
|
18048
18348
|
parseDocBlocks,
|
|
18049
18349
|
openMetahub,
|
|
18050
18350
|
observeHlc,
|
|
18351
|
+
normalizeSitePath,
|
|
18352
|
+
normalizeSiteName,
|
|
18051
18353
|
nextHlc,
|
|
18052
18354
|
newId,
|
|
18053
18355
|
moveRecord,
|
|
18356
|
+
moveDocument,
|
|
18054
18357
|
migrateRecords,
|
|
18055
18358
|
migratePeers,
|
|
18359
|
+
migrateDocuments,
|
|
18056
18360
|
migrateDocBlocks,
|
|
18057
18361
|
metahubHome,
|
|
18058
18362
|
maybeAutoIndex,
|
|
@@ -18103,6 +18407,7 @@ export {
|
|
|
18103
18407
|
cacheDir,
|
|
18104
18408
|
blobPath,
|
|
18105
18409
|
backfillRecordOrderKeys,
|
|
18410
|
+
backfillDocumentOrderKeys,
|
|
18106
18411
|
applyChange,
|
|
18107
18412
|
appendDocument,
|
|
18108
18413
|
addProperty,
|
|
@@ -18120,4 +18425,4 @@ export {
|
|
|
18120
18425
|
ChangeSchema
|
|
18121
18426
|
};
|
|
18122
18427
|
|
|
18123
|
-
//# debugId=
|
|
18428
|
+
//# debugId=F1DD5DA39D0D2ABD64756E2164756E21
|