@tensorix/metahub 0.1.7 → 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 +538 -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 +450 -130
- package/dist/index.js.map +11 -11
- package/dist/webui.js +114 -33
- package/package.json +4 -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); }
|
|
@@ -784,6 +829,21 @@ var HTML = `<!doctype html>
|
|
|
784
829
|
.btn-primary:hover { filter:brightness(1.07); }
|
|
785
830
|
.btn-danger { background:var(--danger); color:#fff; } .btn-danger:hover { filter:brightness(1.05); }
|
|
786
831
|
|
|
832
|
+
/* desktop app (frameless): drop the sidebar logo; on macOS reserve a draggable
|
|
833
|
+
strip for the inset traffic lights so they never overlap our controls */
|
|
834
|
+
body.desktop .brand { display:none; }
|
|
835
|
+
/* logo gone \u2192 keep the collapse button on the right (where .brand's flex used to push it) */
|
|
836
|
+
body.desktop .sb-head .iconbtn { margin-left:auto; }
|
|
837
|
+
body.desktop-mac .sb-head { -webkit-app-region:drag; }
|
|
838
|
+
body.desktop-mac .sb-head .iconbtn { -webkit-app-region:no-drag; }
|
|
839
|
+
body.desktop-mac .topbar { -webkit-app-region:drag; }
|
|
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; }
|
|
844
|
+
/* sidebar collapsed \u2192 traffic lights sit over the main header; clear the hamburger */
|
|
845
|
+
body.desktop-mac .sidebar.collapsed ~ .main .topbar { padding-left:80px; }
|
|
846
|
+
|
|
787
847
|
/* document editor */
|
|
788
848
|
.doc { max-width:740px; margin:0 auto; padding:60px 60px 36vh; }
|
|
789
849
|
.doc-title { font-size:38px; font-weight:700; letter-spacing:-.02em; outline:none; line-height:1.15; margin-bottom:4px; }
|
|
@@ -1015,6 +1075,83 @@ var HTML = `<!doctype html>
|
|
|
1015
1075
|
.gridfoot { display:flex; gap:18px; color:var(--muted); font-size:12px; padding:8px 2px; }
|
|
1016
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; }
|
|
1017
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
|
+
|
|
1018
1155
|
.selbar { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); z-index:65; display:flex; align-items:center; gap:4px;
|
|
1019
1156
|
background:var(--fg); color:var(--bg); padding:7px 8px 7px 14px; border-radius:11px; box-shadow:var(--shadow-lg); }
|
|
1020
1157
|
.selbar .cnt { font-size:13px; font-weight:600; margin-right:6px; }
|
|
@@ -1466,6 +1603,7 @@ CREATE TABLE IF NOT EXISTS documents (
|
|
|
1466
1603
|
database_id TEXT,
|
|
1467
1604
|
parent_id TEXT,
|
|
1468
1605
|
created_hlc TEXT,
|
|
1606
|
+
order_key TEXT, -- fractional index among siblings (same parent_id); ties broken by created_hlc,id
|
|
1469
1607
|
__deleted INTEGER NOT NULL DEFAULT 0
|
|
1470
1608
|
);
|
|
1471
1609
|
|
|
@@ -2108,103 +2246,6 @@ function deleteRecord(db, id) {
|
|
|
2108
2246
|
return true;
|
|
2109
2247
|
}
|
|
2110
2248
|
|
|
2111
|
-
// src/core/db.ts
|
|
2112
|
-
function ensureDirs() {
|
|
2113
|
-
mkdirSync(metahubHome(), { recursive: true });
|
|
2114
|
-
mkdirSync(cacheDir(), { recursive: true });
|
|
2115
|
-
}
|
|
2116
|
-
function runSchema(db) {
|
|
2117
|
-
db.exec(CORE_SCHEMA);
|
|
2118
|
-
try {
|
|
2119
|
-
db.exec(FTS_SCHEMA);
|
|
2120
|
-
} catch {}
|
|
2121
|
-
}
|
|
2122
|
-
function ftsAvailable(db) {
|
|
2123
|
-
const row = db.query("SELECT 1 AS ok FROM sqlite_master WHERE name = 'search_fts'").get();
|
|
2124
|
-
return row != null;
|
|
2125
|
-
}
|
|
2126
|
-
function hasColumn(db, table, column) {
|
|
2127
|
-
const cols = db.query(`PRAGMA table_info(${table})`).all();
|
|
2128
|
-
return cols.some((c) => c.name === column);
|
|
2129
|
-
}
|
|
2130
|
-
function tableExists2(db, table) {
|
|
2131
|
-
return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
|
|
2132
|
-
}
|
|
2133
|
-
function migrateRecords(db) {
|
|
2134
|
-
if (!hasColumn(db, "records", "data"))
|
|
2135
|
-
db.exec("ALTER TABLE records ADD COLUMN data TEXT NOT NULL DEFAULT '{}'");
|
|
2136
|
-
if (!hasColumn(db, "records", "order_key"))
|
|
2137
|
-
db.exec("ALTER TABLE records ADD COLUMN order_key TEXT");
|
|
2138
|
-
if (!tableExists2(db, "record_values"))
|
|
2139
|
-
return;
|
|
2140
|
-
const tx = db.transaction(() => {
|
|
2141
|
-
db.exec(`
|
|
2142
|
-
UPDATE records SET data = coalesce((
|
|
2143
|
-
SELECT json_group_object(rv.property_id, json(rv.value))
|
|
2144
|
-
FROM record_values rv WHERE rv.record_id = records.id
|
|
2145
|
-
), '{}')
|
|
2146
|
-
WHERE id IN (SELECT DISTINCT record_id FROM record_values);
|
|
2147
|
-
`);
|
|
2148
|
-
db.exec("DROP TABLE record_values");
|
|
2149
|
-
backfillRecordOrderKeys(db);
|
|
2150
|
-
});
|
|
2151
|
-
tx();
|
|
2152
|
-
backfillRecordOrderKeys(db);
|
|
2153
|
-
}
|
|
2154
|
-
function migratePeers(db) {
|
|
2155
|
-
const add = [
|
|
2156
|
-
["token", "TEXT"],
|
|
2157
|
-
["label", "TEXT"],
|
|
2158
|
-
["node_id", "TEXT"],
|
|
2159
|
-
["enabled", "INTEGER NOT NULL DEFAULT 1"],
|
|
2160
|
-
["last_sync_at", "INTEGER"],
|
|
2161
|
-
["last_status", "TEXT"],
|
|
2162
|
-
["last_error", "TEXT"]
|
|
2163
|
-
];
|
|
2164
|
-
for (const [col, decl] of add) {
|
|
2165
|
-
if (!hasColumn(db, "peers", col))
|
|
2166
|
-
db.exec(`ALTER TABLE peers ADD COLUMN ${col} ${decl}`);
|
|
2167
|
-
}
|
|
2168
|
-
}
|
|
2169
|
-
function migrateDocBlocks(db) {
|
|
2170
|
-
if (!hasColumn(db, "doc_blocks", "blank_after"))
|
|
2171
|
-
db.exec("ALTER TABLE doc_blocks ADD COLUMN blank_after INTEGER NOT NULL DEFAULT 0");
|
|
2172
|
-
}
|
|
2173
|
-
function openMetahub() {
|
|
2174
|
-
ensureDirs();
|
|
2175
|
-
const db = new Database(dbPath(), { create: true });
|
|
2176
|
-
db.exec("PRAGMA journal_mode = WAL;");
|
|
2177
|
-
runSchema(db);
|
|
2178
|
-
migrateRecords(db);
|
|
2179
|
-
migratePeers(db);
|
|
2180
|
-
migrateDocBlocks(db);
|
|
2181
|
-
return db;
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
// src/core/index.ts
|
|
2185
|
-
init_ids();
|
|
2186
|
-
init_crdt();
|
|
2187
|
-
init_node();
|
|
2188
|
-
|
|
2189
|
-
// src/core/context.ts
|
|
2190
|
-
var KEY = "current_db";
|
|
2191
|
-
function getCurrentDatabase(db) {
|
|
2192
|
-
const row = db.query("SELECT value FROM meta WHERE key = ?").get(KEY);
|
|
2193
|
-
if (!row)
|
|
2194
|
-
return null;
|
|
2195
|
-
const found = getDatabase(db, row.value);
|
|
2196
|
-
if (!found) {
|
|
2197
|
-
clearCurrentDatabase(db);
|
|
2198
|
-
return null;
|
|
2199
|
-
}
|
|
2200
|
-
return found;
|
|
2201
|
-
}
|
|
2202
|
-
function setCurrentDatabase(db, id) {
|
|
2203
|
-
db.query("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(KEY, id);
|
|
2204
|
-
}
|
|
2205
|
-
function clearCurrentDatabase(db) {
|
|
2206
|
-
db.query("DELETE FROM meta WHERE key = ?").run(KEY);
|
|
2207
|
-
}
|
|
2208
2249
|
// src/core/documents.ts
|
|
2209
2250
|
init_ids();
|
|
2210
2251
|
init_crdt();
|
|
@@ -2276,6 +2317,7 @@ function createDocument(db, opts) {
|
|
|
2276
2317
|
emit(db, "documents", id, "database_id", opts.database_id);
|
|
2277
2318
|
if (opts.parent_id !== undefined)
|
|
2278
2319
|
emit(db, "documents", id, "parent_id", opts.parent_id);
|
|
2320
|
+
placeInSiblings(db, id, opts.parent_id ?? null);
|
|
2279
2321
|
if (opts.body !== undefined) {
|
|
2280
2322
|
const blocks = parseDocBlocks(opts.body);
|
|
2281
2323
|
if (blocks.length)
|
|
@@ -2284,15 +2326,111 @@ function createDocument(db, opts) {
|
|
|
2284
2326
|
return getDocument(db, id);
|
|
2285
2327
|
}
|
|
2286
2328
|
function getDocument(db, id) {
|
|
2287
|
-
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);
|
|
2288
2330
|
}
|
|
2331
|
+
var ORDER_BY = "ORDER BY order_key IS NULL, order_key, created_hlc, id";
|
|
2289
2332
|
function listDocuments(db, opts = {}) {
|
|
2290
|
-
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";
|
|
2291
2334
|
if (opts.parent_id !== undefined)
|
|
2292
|
-
return db.query(`${cols} AND parent_id = ?
|
|
2335
|
+
return db.query(`${cols} AND parent_id = ? ${ORDER_BY}`).all(opts.parent_id);
|
|
2293
2336
|
if (opts.database_id)
|
|
2294
|
-
return db.query(`${cols} AND database_id = ?
|
|
2295
|
-
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);
|
|
2296
2434
|
}
|
|
2297
2435
|
function updateDocument(db, id, fields) {
|
|
2298
2436
|
if (!getDocument(db, id))
|
|
@@ -2302,13 +2440,9 @@ function updateDocument(db, id, fields) {
|
|
|
2302
2440
|
if (fields.database_id !== undefined)
|
|
2303
2441
|
emit(db, "documents", id, "database_id", fields.database_id);
|
|
2304
2442
|
if (fields.parent_id !== undefined) {
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
throw new Error(`cannot set parent_id: would create a cycle (${id})`);
|
|
2309
|
-
cur = getDocument(db, cur)?.parent_id ?? null;
|
|
2310
|
-
}
|
|
2311
|
-
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);
|
|
2312
2446
|
}
|
|
2313
2447
|
if (fields.body !== undefined) {
|
|
2314
2448
|
ensureBlocks(db, id);
|
|
@@ -2396,6 +2530,110 @@ function countOccurrences(haystack, needle) {
|
|
|
2396
2530
|
}
|
|
2397
2531
|
return n;
|
|
2398
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
|
+
}
|
|
2399
2637
|
// src/core/search.ts
|
|
2400
2638
|
init_crdt();
|
|
2401
2639
|
var TEXT_TYPES = "('text','url','select','multi_select','date')";
|
|
@@ -17016,7 +17254,7 @@ function getServerConfig(db) {
|
|
|
17016
17254
|
// package.json
|
|
17017
17255
|
var package_default = {
|
|
17018
17256
|
name: "@tensorix/metahub",
|
|
17019
|
-
version: "0.1.
|
|
17257
|
+
version: "0.1.9",
|
|
17020
17258
|
description: "Metahub core library and CLI",
|
|
17021
17259
|
license: "AGPL-3.0-only",
|
|
17022
17260
|
type: "module",
|
|
@@ -17053,10 +17291,13 @@ var package_default = {
|
|
|
17053
17291
|
citty: "^0.1.6",
|
|
17054
17292
|
"highlight.js": "^11.11.1",
|
|
17055
17293
|
preact: "^10.29.2",
|
|
17294
|
+
turndown: "^7.2.4",
|
|
17295
|
+
"turndown-plugin-gfm": "^1.0.2",
|
|
17056
17296
|
zod: "^4.4.3"
|
|
17057
17297
|
},
|
|
17058
17298
|
devDependencies: {
|
|
17059
17299
|
"@types/bun": "latest",
|
|
17300
|
+
"@types/turndown": "^5.0.6",
|
|
17060
17301
|
typescript: "^5"
|
|
17061
17302
|
}
|
|
17062
17303
|
};
|
|
@@ -17082,7 +17323,7 @@ var PropertySchema = exports_external.object({
|
|
|
17082
17323
|
var RecordSchema = exports_external.object({
|
|
17083
17324
|
id: exports_external.string(),
|
|
17084
17325
|
database_id: exports_external.string(),
|
|
17085
|
-
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.')
|
|
17086
17327
|
});
|
|
17087
17328
|
var MoveRecordReq = exports_external.object({
|
|
17088
17329
|
target: exports_external.string(),
|
|
@@ -17093,7 +17334,12 @@ var DocumentSummarySchema = exports_external.object({
|
|
|
17093
17334
|
title: exports_external.string(),
|
|
17094
17335
|
database_id: exports_external.string().nullable(),
|
|
17095
17336
|
parent_id: exports_external.string().nullable(),
|
|
17096
|
-
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"])
|
|
17097
17343
|
});
|
|
17098
17344
|
var DocumentSchema = DocumentSummarySchema.extend({ body: exports_external.string().nullable() });
|
|
17099
17345
|
var SearchHitSchema = exports_external.object({
|
|
@@ -17122,7 +17368,7 @@ var UpdatePropertyReq = exports_external.object({
|
|
|
17122
17368
|
position: exports_external.number().optional()
|
|
17123
17369
|
});
|
|
17124
17370
|
var SetWidthReq = exports_external.object({ width: exports_external.number() });
|
|
17125
|
-
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).");
|
|
17126
17372
|
var CreateDocumentReq = exports_external.object({
|
|
17127
17373
|
title: exports_external.string(),
|
|
17128
17374
|
body: exports_external.string().optional(),
|
|
@@ -17349,6 +17595,17 @@ var webuiRoutes = [
|
|
|
17349
17595
|
return updateDocument(db, need(req, "id"), body);
|
|
17350
17596
|
})
|
|
17351
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
|
+
},
|
|
17352
17609
|
{
|
|
17353
17610
|
method: "DELETE",
|
|
17354
17611
|
path: "/api/document",
|
|
@@ -17376,6 +17633,7 @@ var SiteSchema = exports_external.object({
|
|
|
17376
17633
|
title: exports_external.string().nullable(),
|
|
17377
17634
|
created_hlc: exports_external.string()
|
|
17378
17635
|
});
|
|
17636
|
+
var SiteWithCountSchema = SiteSchema.extend({ file_count: exports_external.number() });
|
|
17379
17637
|
var SiteFileSchema = exports_external.object({
|
|
17380
17638
|
id: exports_external.string(),
|
|
17381
17639
|
site_id: exports_external.string(),
|
|
@@ -17383,6 +17641,8 @@ var SiteFileSchema = exports_external.object({
|
|
|
17383
17641
|
content_type: exports_external.string(),
|
|
17384
17642
|
encoding: exports_external.string()
|
|
17385
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() });
|
|
17386
17646
|
function need2(req, key) {
|
|
17387
17647
|
const v = new URL(req.url).searchParams.get(key);
|
|
17388
17648
|
if (!v)
|
|
@@ -17402,9 +17662,9 @@ var sitesRoutes = [
|
|
|
17402
17662
|
{
|
|
17403
17663
|
method: "GET",
|
|
17404
17664
|
path: "/api/sites",
|
|
17405
|
-
summary: "List published sites",
|
|
17406
|
-
response: exports_external.array(
|
|
17407
|
-
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 })))
|
|
17408
17668
|
},
|
|
17409
17669
|
{
|
|
17410
17670
|
method: "GET",
|
|
@@ -17412,6 +17672,60 @@ var sitesRoutes = [
|
|
|
17412
17672
|
summary: "List a site's files (manifest). Query: ?site=<id|name>",
|
|
17413
17673
|
response: exports_external.array(SiteFileSchema),
|
|
17414
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
|
+
}))
|
|
17415
17729
|
}
|
|
17416
17730
|
];
|
|
17417
17731
|
|
|
@@ -18006,6 +18320,7 @@ function startServer(opts = {}) {
|
|
|
18006
18320
|
};
|
|
18007
18321
|
}
|
|
18008
18322
|
export {
|
|
18323
|
+
updateSite,
|
|
18009
18324
|
updateRecord,
|
|
18010
18325
|
updateProperty,
|
|
18011
18326
|
updateDocument,
|
|
@@ -18033,11 +18348,15 @@ export {
|
|
|
18033
18348
|
parseDocBlocks,
|
|
18034
18349
|
openMetahub,
|
|
18035
18350
|
observeHlc,
|
|
18351
|
+
normalizeSitePath,
|
|
18352
|
+
normalizeSiteName,
|
|
18036
18353
|
nextHlc,
|
|
18037
18354
|
newId,
|
|
18038
18355
|
moveRecord,
|
|
18356
|
+
moveDocument,
|
|
18039
18357
|
migrateRecords,
|
|
18040
18358
|
migratePeers,
|
|
18359
|
+
migrateDocuments,
|
|
18041
18360
|
migrateDocBlocks,
|
|
18042
18361
|
metahubHome,
|
|
18043
18362
|
maybeAutoIndex,
|
|
@@ -18088,6 +18407,7 @@ export {
|
|
|
18088
18407
|
cacheDir,
|
|
18089
18408
|
blobPath,
|
|
18090
18409
|
backfillRecordOrderKeys,
|
|
18410
|
+
backfillDocumentOrderKeys,
|
|
18091
18411
|
applyChange,
|
|
18092
18412
|
appendDocument,
|
|
18093
18413
|
addProperty,
|
|
@@ -18105,4 +18425,4 @@ export {
|
|
|
18105
18425
|
ChangeSchema
|
|
18106
18426
|
};
|
|
18107
18427
|
|
|
18108
|
-
//# debugId=
|
|
18428
|
+
//# debugId=F1DD5DA39D0D2ABD64756E2164756E21
|