@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/cli.js
CHANGED
|
@@ -1116,7 +1116,7 @@ var init_crdt = __esm(() => {
|
|
|
1116
1116
|
},
|
|
1117
1117
|
documents: {
|
|
1118
1118
|
table: "documents",
|
|
1119
|
-
cols: new Set(["title", "body", "database_id", "parent_id", "created_hlc", "__deleted"])
|
|
1119
|
+
cols: new Set(["title", "body", "database_id", "parent_id", "created_hlc", "order_key", "__deleted"])
|
|
1120
1120
|
},
|
|
1121
1121
|
doc_blocks: {
|
|
1122
1122
|
table: "doc_blocks",
|
|
@@ -1172,6 +1172,24 @@ function inferContentType(path) {
|
|
|
1172
1172
|
const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
|
|
1173
1173
|
return MIME[ext] ?? "application/octet-stream";
|
|
1174
1174
|
}
|
|
1175
|
+
function normalizeSiteName(raw) {
|
|
1176
|
+
const slug = raw.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1177
|
+
if (!slug)
|
|
1178
|
+
throw new Error(`invalid site name: ${JSON.stringify(raw)}`);
|
|
1179
|
+
return slug;
|
|
1180
|
+
}
|
|
1181
|
+
function normalizeSitePath(raw) {
|
|
1182
|
+
const segs = [];
|
|
1183
|
+
for (const seg of raw.split("/")) {
|
|
1184
|
+
if (seg === "" || seg === ".")
|
|
1185
|
+
continue;
|
|
1186
|
+
if (seg === "..")
|
|
1187
|
+
segs.pop();
|
|
1188
|
+
else
|
|
1189
|
+
segs.push(seg);
|
|
1190
|
+
}
|
|
1191
|
+
return segs.join("/");
|
|
1192
|
+
}
|
|
1175
1193
|
function isTextType(contentType) {
|
|
1176
1194
|
const ct = contentType.toLowerCase();
|
|
1177
1195
|
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");
|
|
@@ -1182,10 +1200,11 @@ function toBytes(data) {
|
|
|
1182
1200
|
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
1183
1201
|
}
|
|
1184
1202
|
function createSite(db, opts) {
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
const
|
|
1203
|
+
const name = normalizeSiteName(opts.name);
|
|
1204
|
+
if (getSiteByName(db, name))
|
|
1205
|
+
throw new Error(`site name already exists: ${name}`);
|
|
1206
|
+
const id = newId("site", name);
|
|
1207
|
+
const first = emit(db, "sites", id, "name", name);
|
|
1189
1208
|
emit(db, "sites", id, "created_hlc", first.hlc);
|
|
1190
1209
|
if (opts.title !== undefined)
|
|
1191
1210
|
emit(db, "sites", id, "title", opts.title);
|
|
@@ -1195,7 +1214,13 @@ function getSite(db, id) {
|
|
|
1195
1214
|
return db.query("SELECT id, name, title, created_hlc FROM sites WHERE id = ? AND __deleted = 0").get(id);
|
|
1196
1215
|
}
|
|
1197
1216
|
function getSiteByName(db, name) {
|
|
1198
|
-
|
|
1217
|
+
let slug;
|
|
1218
|
+
try {
|
|
1219
|
+
slug = normalizeSiteName(name);
|
|
1220
|
+
} catch {
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
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);
|
|
1199
1224
|
}
|
|
1200
1225
|
function listSites(db) {
|
|
1201
1226
|
return db.query("SELECT id, name, title, created_hlc FROM sites WHERE __deleted = 0 ORDER BY created_hlc").all();
|
|
@@ -1207,6 +1232,20 @@ function resolveSite(db, ref) {
|
|
|
1207
1232
|
throw new Error(`no such site: ${ref}`);
|
|
1208
1233
|
return site;
|
|
1209
1234
|
}
|
|
1235
|
+
function updateSite(db, id, opts) {
|
|
1236
|
+
if (!getSite(db, id))
|
|
1237
|
+
throw new Error(`no such site: ${id}`);
|
|
1238
|
+
if (opts.name !== undefined) {
|
|
1239
|
+
const name = normalizeSiteName(opts.name);
|
|
1240
|
+
const dup = getSiteByName(db, name);
|
|
1241
|
+
if (dup && dup.id !== id)
|
|
1242
|
+
throw new Error(`site name already exists: ${name}`);
|
|
1243
|
+
emit(db, "sites", id, "name", name);
|
|
1244
|
+
}
|
|
1245
|
+
if (opts.title !== undefined)
|
|
1246
|
+
emit(db, "sites", id, "title", opts.title);
|
|
1247
|
+
return getSite(db, id);
|
|
1248
|
+
}
|
|
1210
1249
|
function deleteSite(db, id) {
|
|
1211
1250
|
if (!getSite(db, id))
|
|
1212
1251
|
return false;
|
|
@@ -1221,7 +1260,10 @@ function fileIdFor(db, siteId, path) {
|
|
|
1221
1260
|
return row?.id ?? null;
|
|
1222
1261
|
}
|
|
1223
1262
|
async function putFile(db, siteId, path, opts) {
|
|
1224
|
-
const
|
|
1263
|
+
const cleanPath = normalizeSitePath(path);
|
|
1264
|
+
if (!cleanPath)
|
|
1265
|
+
throw new Error(`invalid file path: ${JSON.stringify(path)}`);
|
|
1266
|
+
const contentType = opts.contentType ?? inferContentType(cleanPath);
|
|
1225
1267
|
const bytes = toBytes(opts.data);
|
|
1226
1268
|
let encoding;
|
|
1227
1269
|
let content;
|
|
@@ -1235,11 +1277,11 @@ async function putFile(db, siteId, path, opts) {
|
|
|
1235
1277
|
encoding = "blob";
|
|
1236
1278
|
content = (await putBlob(bytes)).hash;
|
|
1237
1279
|
}
|
|
1238
|
-
const existing = fileIdFor(db, siteId,
|
|
1239
|
-
const id = existing ?? newId("sf",
|
|
1280
|
+
const existing = fileIdFor(db, siteId, cleanPath);
|
|
1281
|
+
const id = existing ?? newId("sf", cleanPath);
|
|
1240
1282
|
if (!existing) {
|
|
1241
1283
|
const first = emit(db, "site_files", id, "site_id", siteId);
|
|
1242
|
-
emit(db, "site_files", id, "path",
|
|
1284
|
+
emit(db, "site_files", id, "path", cleanPath);
|
|
1243
1285
|
emit(db, "site_files", id, "created_hlc", first.hlc);
|
|
1244
1286
|
}
|
|
1245
1287
|
emit(db, "site_files", id, "content_type", contentType);
|
|
@@ -1252,7 +1294,7 @@ function listFiles(db, siteId) {
|
|
|
1252
1294
|
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);
|
|
1253
1295
|
}
|
|
1254
1296
|
function deleteFile(db, siteId, path) {
|
|
1255
|
-
const id = fileIdFor(db, siteId, path);
|
|
1297
|
+
const id = fileIdFor(db, siteId, normalizeSitePath(path));
|
|
1256
1298
|
if (!id)
|
|
1257
1299
|
return false;
|
|
1258
1300
|
const live = db.query("SELECT __deleted AS d FROM site_files WHERE id = ?").get(id);
|
|
@@ -1262,7 +1304,10 @@ function deleteFile(db, siteId, path) {
|
|
|
1262
1304
|
return true;
|
|
1263
1305
|
}
|
|
1264
1306
|
async function getFileForServe(db, siteId, path) {
|
|
1265
|
-
const
|
|
1307
|
+
const withIndex = path === "" || path.endsWith("/") ? `${path}index.html` : path;
|
|
1308
|
+
const p = normalizeSitePath(withIndex);
|
|
1309
|
+
if (!p)
|
|
1310
|
+
return null;
|
|
1266
1311
|
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);
|
|
1267
1312
|
if (!row)
|
|
1268
1313
|
return null;
|
|
@@ -1516,7 +1561,7 @@ var HTML = `<!doctype html>
|
|
|
1516
1561
|
.sb-section-head .add { opacity:0; padding:3px; border-radius:5px; color:var(--muted); transition:opacity .12s; }
|
|
1517
1562
|
.sb-section:hover .add { opacity:1; }
|
|
1518
1563
|
.sb-section-head .add:hover { background:var(--hover-2); color:var(--fg); }
|
|
1519
|
-
.navitem { display:flex; align-items:center; gap:3px; width:100%; text-align:left; padding:5px 6px; border-radius:var(--radius-sm);
|
|
1564
|
+
.navitem { display:flex; align-items:center; gap:3px; width:100%; text-align:left; padding:5px 6px; margin-bottom:2px; border-radius:var(--radius-sm);
|
|
1520
1565
|
color:var(--fg-soft); font-size:13.5px; position:relative; white-space:nowrap; transition:background .1s; }
|
|
1521
1566
|
.navitem:hover { background:var(--hover-2); }
|
|
1522
1567
|
.navitem.active { background:var(--bg); color:var(--fg); font-weight:600; box-shadow:var(--shadow-sm); }
|
|
@@ -1625,6 +1670,9 @@ var HTML = `<!doctype html>
|
|
|
1625
1670
|
body.desktop-mac .sb-head .iconbtn { -webkit-app-region:no-drag; }
|
|
1626
1671
|
body.desktop-mac .topbar { -webkit-app-region:drag; }
|
|
1627
1672
|
body.desktop-mac .topbar .iconbtn, body.desktop-mac .topbar .btn { -webkit-app-region:no-drag; }
|
|
1673
|
+
/* peek drawer header overlaps the topbar drag region \u2192 clear its buttons so they stay clickable */
|
|
1674
|
+
body.desktop-mac .peek-head { -webkit-app-region:drag; }
|
|
1675
|
+
body.desktop-mac .peek-head .iconbtn, body.desktop-mac .peek-head .btn { -webkit-app-region:no-drag; }
|
|
1628
1676
|
/* sidebar collapsed \u2192 traffic lights sit over the main header; clear the hamburger */
|
|
1629
1677
|
body.desktop-mac .sidebar.collapsed ~ .main .topbar { padding-left:80px; }
|
|
1630
1678
|
|
|
@@ -1859,6 +1907,83 @@ var HTML = `<!doctype html>
|
|
|
1859
1907
|
.gridfoot { display:flex; gap:18px; color:var(--muted); font-size:12px; padding:8px 2px; }
|
|
1860
1908
|
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; }
|
|
1861
1909
|
|
|
1910
|
+
/* ---- sites (\u7AD9\u70B9\u7BA1\u7406) ---- */
|
|
1911
|
+
.sites-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(264px, 1fr)); gap:14px; margin-top:6px; }
|
|
1912
|
+
.site-card { display:flex; flex-direction:column; gap:13px; padding:16px 16px 14px; background:var(--bg); border:1px solid var(--line);
|
|
1913
|
+
border-radius:var(--radius); box-shadow:var(--shadow-sm); cursor:pointer; transition:box-shadow .16s, border-color .16s, transform .16s; }
|
|
1914
|
+
.site-card:hover { box-shadow:var(--shadow-md); border-color:var(--line-strong); transform:translateY(-1px); }
|
|
1915
|
+
.site-card-head { display:flex; align-items:center; gap:11px; min-width:0; }
|
|
1916
|
+
.site-card .si { width:40px; height:40px; border-radius:11px; background:var(--accent-soft); color:var(--accent); display:grid; place-items:center; flex:none; }
|
|
1917
|
+
.site-card .si svg { width:20px; height:20px; }
|
|
1918
|
+
.site-card-id { min-width:0; }
|
|
1919
|
+
.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; }
|
|
1920
|
+
.site-card .ttl { font-size:12px; color:var(--fg-soft); margin-top:1px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1921
|
+
.site-card .ttl.muted { color:var(--muted); font-style:italic; }
|
|
1922
|
+
.site-addr { display:flex; align-items:center; gap:6px; width:100%; text-align:left; background:var(--surface); border:1px solid var(--line);
|
|
1923
|
+
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; }
|
|
1924
|
+
.site-addr svg { flex:none; }
|
|
1925
|
+
.site-addr span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1926
|
+
.site-addr:hover { border-color:var(--accent); color:var(--accent); }
|
|
1927
|
+
.site-card-meta { display:flex; align-items:center; gap:7px; font-size:12px; color:var(--muted); }
|
|
1928
|
+
.site-card-meta b { color:var(--fg-soft); font-weight:600; font-variant-numeric:tabular-nums; }
|
|
1929
|
+
.site-card-meta .dot { color:var(--line-strong); }
|
|
1930
|
+
.site-card-foot { display:flex; align-items:center; gap:6px; padding-top:12px; margin-top:1px; border-top:1px solid var(--line); }
|
|
1931
|
+
.site-card-foot .btn-secondary { padding:6px 12px; justify-content:center; }
|
|
1932
|
+
|
|
1933
|
+
.site-empty { text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--radius); padding:48px 24px; margin-top:8px; }
|
|
1934
|
+
.site-empty .ei { font-size:30px; margin-bottom:10px; }
|
|
1935
|
+
.site-empty .et { font-size:14px; color:var(--fg-soft); margin-bottom:4px; font-weight:600; }
|
|
1936
|
+
.site-empty .ed { font-size:12.5px; margin-bottom:16px; }
|
|
1937
|
+
|
|
1938
|
+
/* site peek: access bar + file manager */
|
|
1939
|
+
.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; }
|
|
1940
|
+
.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; }
|
|
1941
|
+
.acc-link button { width:28px; height:28px; display:grid; place-items:center; border-radius:6px; color:var(--muted); flex:none; }
|
|
1942
|
+
.acc-link button:hover { background:var(--hover-2); color:var(--fg); }
|
|
1943
|
+
.acc-link button.accent { color:var(--accent); }
|
|
1944
|
+
.site-meta { display:flex; gap:18px; color:var(--muted); font-size:12px; margin-bottom:20px; }
|
|
1945
|
+
.site-meta b { color:var(--fg); font-weight:600; }
|
|
1946
|
+
.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; }
|
|
1947
|
+
.filerow { display:flex; align-items:center; gap:10px; padding:9px 8px; border-radius:var(--radius-sm); cursor:pointer; border-bottom:1px solid var(--line); }
|
|
1948
|
+
.filerow:last-child { border-bottom:0; }
|
|
1949
|
+
.filerow:hover { background:var(--hover); }
|
|
1950
|
+
.filerow .fi { color:var(--muted); display:grid; place-items:center; flex:none; }
|
|
1951
|
+
.filerow .fpath { flex:1; font-family:var(--mono); font-size:12.5px; color:var(--fg); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1952
|
+
.filerow .facts { display:flex; gap:1px; opacity:0; flex:none; }
|
|
1953
|
+
.filerow:hover .facts { opacity:1; }
|
|
1954
|
+
.filerow .facts button { width:27px; height:27px; display:grid; place-items:center; border-radius:6px; color:var(--muted); }
|
|
1955
|
+
.filerow .facts button:hover { background:var(--hover-2); color:var(--fg); }
|
|
1956
|
+
.filerow .facts button.del:hover { color:var(--danger); }
|
|
1957
|
+
.enc-badge { display:inline-flex; align-items:center; font-family:var(--mono); font-size:10.5px; font-weight:500; letter-spacing:.02em;
|
|
1958
|
+
padding:2px 6px; border-radius:5px; background:var(--surface-2); color:var(--fg-soft); border:1px solid var(--line); }
|
|
1959
|
+
.enc-badge.blob { color:var(--accent); background:var(--accent-soft); border-color:transparent; }
|
|
1960
|
+
|
|
1961
|
+
/* file preview (inside modal) */
|
|
1962
|
+
.preview-box { font-family:var(--mono); font-size:12.5px; background:var(--surface-2); border:1px solid var(--line); border-radius:var(--radius);
|
|
1963
|
+
padding:14px 16px; max-height:52vh; overflow:auto; white-space:pre-wrap; line-height:1.6; color:var(--fg); }
|
|
1964
|
+
.preview-bin { text-align:center; color:var(--muted); border:1px dashed var(--line-strong); border-radius:var(--radius); padding:40px 20px; }
|
|
1965
|
+
.preview-bin .pi { font-size:28px; margin-bottom:8px; }
|
|
1966
|
+
.preview-bin .pt { color:var(--fg-soft); font-size:13px; }
|
|
1967
|
+
|
|
1968
|
+
/* in-app site preview (browser-framed iframe) */
|
|
1969
|
+
.spv-scrim { position:fixed; inset:0; background:rgba(15,15,25,.5); z-index:85; display:grid; place-items:center; padding:28px;
|
|
1970
|
+
opacity:0; pointer-events:none; transition:opacity .16s; animation:spv-in .16s forwards; }
|
|
1971
|
+
@keyframes spv-in { to { opacity:1; pointer-events:auto; } }
|
|
1972
|
+
.spv-scrim.open { opacity:1; pointer-events:auto; }
|
|
1973
|
+
.spv { width:min(1100px, 92vw); height:88vh; background:var(--bg); border-radius:14px; box-shadow:var(--shadow-lg);
|
|
1974
|
+
display:flex; flex-direction:column; overflow:hidden; }
|
|
1975
|
+
.spv-chrome { display:flex; align-items:center; gap:10px; padding:9px 12px; border-bottom:1px solid var(--line); background:var(--surface); }
|
|
1976
|
+
.spv-dots { display:flex; gap:6px; flex:none; }
|
|
1977
|
+
.spv-dots i { width:11px; height:11px; border-radius:50%; background:var(--line-strong); }
|
|
1978
|
+
.spv-url { flex:1; display:flex; align-items:center; gap:6px; min-width:0; background:var(--bg); border:1px solid var(--line);
|
|
1979
|
+
border-radius:7px; padding:5px 10px; color:var(--fg-soft); font-family:var(--mono); font-size:12px; }
|
|
1980
|
+
.spv-url svg { color:var(--muted); flex:none; }
|
|
1981
|
+
.spv-url span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
1982
|
+
.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; }
|
|
1983
|
+
.spv-btn:hover { background:var(--hover-2); color:var(--fg); }
|
|
1984
|
+
.spv-btn.icon { padding:6px; }
|
|
1985
|
+
.spv-frame { flex:1; width:100%; border:0; background:#fff; }
|
|
1986
|
+
|
|
1862
1987
|
.selbar { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); z-index:65; display:flex; align-items:center; gap:4px;
|
|
1863
1988
|
background:var(--fg); color:var(--bg); padding:7px 8px 7px 14px; border-radius:11px; box-shadow:var(--shadow-lg); }
|
|
1864
1989
|
.selbar .cnt { font-size:13px; font-weight:600; margin-right:6px; }
|
|
@@ -3605,7 +3730,7 @@ async function runMain(cmd, opts = {}) {
|
|
|
3605
3730
|
// package.json
|
|
3606
3731
|
var package_default = {
|
|
3607
3732
|
name: "@tensorix/metahub",
|
|
3608
|
-
version: "0.1.
|
|
3733
|
+
version: "0.1.9",
|
|
3609
3734
|
description: "Metahub core library and CLI",
|
|
3610
3735
|
license: "AGPL-3.0-only",
|
|
3611
3736
|
type: "module",
|
|
@@ -3756,6 +3881,7 @@ CREATE TABLE IF NOT EXISTS documents (
|
|
|
3756
3881
|
database_id TEXT,
|
|
3757
3882
|
parent_id TEXT,
|
|
3758
3883
|
created_hlc TEXT,
|
|
3884
|
+
order_key TEXT, -- fractional index among siblings (same parent_id); ties broken by created_hlc,id
|
|
3759
3885
|
__deleted INTEGER NOT NULL DEFAULT 0
|
|
3760
3886
|
);
|
|
3761
3887
|
|
|
@@ -4398,6 +4524,291 @@ function deleteRecord(db, id) {
|
|
|
4398
4524
|
return true;
|
|
4399
4525
|
}
|
|
4400
4526
|
|
|
4527
|
+
// src/core/documents.ts
|
|
4528
|
+
init_ids();
|
|
4529
|
+
init_crdt();
|
|
4530
|
+
function liveBlocks(db, docId) {
|
|
4531
|
+
return db.query("SELECT id, text, order_key, blank_after FROM doc_blocks WHERE doc_id = ? AND __deleted = 0 ORDER BY order_key, id").all(docId);
|
|
4532
|
+
}
|
|
4533
|
+
function makeBlockId(text) {
|
|
4534
|
+
return newId("blk", text.split(`
|
|
4535
|
+
`, 1)[0] ?? "");
|
|
4536
|
+
}
|
|
4537
|
+
function emitBlock(db, docId, fields) {
|
|
4538
|
+
const id = makeBlockId(fields.text);
|
|
4539
|
+
emit(db, "doc_blocks", id, "doc_id", docId);
|
|
4540
|
+
emit(db, "doc_blocks", id, "order_key", fields.order_key);
|
|
4541
|
+
if (fields.blankAfter)
|
|
4542
|
+
emit(db, "doc_blocks", id, "blank_after", fields.blankAfter);
|
|
4543
|
+
emit(db, "doc_blocks", id, "text", fields.text);
|
|
4544
|
+
}
|
|
4545
|
+
function insertBlocks(db, docId, blocks, after, before) {
|
|
4546
|
+
const keys = keysBetween(after, before, blocks.length);
|
|
4547
|
+
blocks.forEach((b2, i2) => emitBlock(db, docId, { text: b2.text, order_key: keys[i2], blankAfter: b2.blankAfter }));
|
|
4548
|
+
}
|
|
4549
|
+
function ensureBlocks(db, docId) {
|
|
4550
|
+
if (db.query("SELECT 1 AS x FROM doc_blocks WHERE doc_id = ? LIMIT 1").get(docId))
|
|
4551
|
+
return;
|
|
4552
|
+
const row = db.query("SELECT body FROM documents WHERE id = ?").get(docId);
|
|
4553
|
+
const blocks = parseDocBlocks(row?.body ?? "");
|
|
4554
|
+
if (blocks.length)
|
|
4555
|
+
insertBlocks(db, docId, blocks, null, null);
|
|
4556
|
+
}
|
|
4557
|
+
function reconcileBody(db, docId, body) {
|
|
4558
|
+
const old = liveBlocks(db, docId);
|
|
4559
|
+
const next = parseDocBlocks(body);
|
|
4560
|
+
const plan = reconcile(old.map((b2) => b2.text), next.map((b2) => b2.text));
|
|
4561
|
+
for (const oi of plan.deleted)
|
|
4562
|
+
emit(db, "doc_blocks", old[oi].id, "__deleted", 1);
|
|
4563
|
+
const { items } = plan;
|
|
4564
|
+
let i2 = 0;
|
|
4565
|
+
while (i2 < items.length) {
|
|
4566
|
+
const it = items[i2];
|
|
4567
|
+
if ("keep" in it) {
|
|
4568
|
+
const oldB = old[it.keep];
|
|
4569
|
+
const want = next[i2].blankAfter;
|
|
4570
|
+
if ((oldB.blank_after ?? 0) !== want)
|
|
4571
|
+
emit(db, "doc_blocks", oldB.id, "blank_after", want);
|
|
4572
|
+
i2++;
|
|
4573
|
+
continue;
|
|
4574
|
+
}
|
|
4575
|
+
let j = i2;
|
|
4576
|
+
while (j < items.length && "insert" in items[j])
|
|
4577
|
+
j++;
|
|
4578
|
+
const left = i2 > 0 ? old[items[i2 - 1].keep].order_key : null;
|
|
4579
|
+
const right = j < items.length ? old[items[j].keep].order_key : null;
|
|
4580
|
+
insertBlocks(db, docId, next.slice(i2, j), left, right);
|
|
4581
|
+
i2 = j;
|
|
4582
|
+
}
|
|
4583
|
+
}
|
|
4584
|
+
function documentVersion(db, id) {
|
|
4585
|
+
const row = db.query(`SELECT MAX(hlc) AS h FROM crdt_changes
|
|
4586
|
+
WHERE (dataset = 'documents' AND row_id = ?)
|
|
4587
|
+
OR (dataset = 'doc_blocks' AND row_id IN (SELECT id FROM doc_blocks WHERE doc_id = ?))`).get(id, id);
|
|
4588
|
+
return row.h ?? "";
|
|
4589
|
+
}
|
|
4590
|
+
function createDocument(db, opts) {
|
|
4591
|
+
const id = newId("doc", opts.title);
|
|
4592
|
+
const first = emit(db, "documents", id, "title", opts.title);
|
|
4593
|
+
emit(db, "documents", id, "created_hlc", first.hlc);
|
|
4594
|
+
if (opts.database_id !== undefined)
|
|
4595
|
+
emit(db, "documents", id, "database_id", opts.database_id);
|
|
4596
|
+
if (opts.parent_id !== undefined)
|
|
4597
|
+
emit(db, "documents", id, "parent_id", opts.parent_id);
|
|
4598
|
+
placeInSiblings(db, id, opts.parent_id ?? null);
|
|
4599
|
+
if (opts.body !== undefined) {
|
|
4600
|
+
const blocks = parseDocBlocks(opts.body);
|
|
4601
|
+
if (blocks.length)
|
|
4602
|
+
insertBlocks(db, id, blocks, null, null);
|
|
4603
|
+
}
|
|
4604
|
+
return getDocument(db, id);
|
|
4605
|
+
}
|
|
4606
|
+
function getDocument(db, id) {
|
|
4607
|
+
return db.query("SELECT id, title, body, database_id, parent_id, created_hlc, order_key FROM documents WHERE id = ? AND __deleted = 0").get(id);
|
|
4608
|
+
}
|
|
4609
|
+
var ORDER_BY = "ORDER BY order_key IS NULL, order_key, created_hlc, id";
|
|
4610
|
+
function listDocuments(db, opts = {}) {
|
|
4611
|
+
const cols = "SELECT id, title, database_id, parent_id, created_hlc, order_key FROM documents WHERE __deleted = 0";
|
|
4612
|
+
if (opts.parent_id !== undefined)
|
|
4613
|
+
return db.query(`${cols} AND parent_id = ? ${ORDER_BY}`).all(opts.parent_id);
|
|
4614
|
+
if (opts.database_id)
|
|
4615
|
+
return db.query(`${cols} AND database_id = ? ${ORDER_BY}`).all(opts.database_id);
|
|
4616
|
+
return db.query(`${cols} ${ORDER_BY}`).all();
|
|
4617
|
+
}
|
|
4618
|
+
function siblingWhere(parentId) {
|
|
4619
|
+
return parentId === null ? { clause: "parent_id IS NULL", args: [] } : { clause: "parent_id = ?", args: [parentId] };
|
|
4620
|
+
}
|
|
4621
|
+
function canEmitOrderKeys2(db) {
|
|
4622
|
+
return tableExists2(db, "meta") && tableExists2(db, "crdt_changes");
|
|
4623
|
+
}
|
|
4624
|
+
function tableExists2(db, table) {
|
|
4625
|
+
return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
|
|
4626
|
+
}
|
|
4627
|
+
function lastSiblingOrderKey(db, parentId, excludeId) {
|
|
4628
|
+
const w2 = siblingWhere(parentId);
|
|
4629
|
+
const exclude = excludeId ? " AND id <> ?" : "";
|
|
4630
|
+
const row = db.query(`SELECT order_key FROM documents WHERE ${w2.clause}${exclude}
|
|
4631
|
+
AND __deleted = 0 AND order_key IS NOT NULL
|
|
4632
|
+
ORDER BY order_key DESC, id DESC LIMIT 1`).get(...w2.args, ...excludeId ? [excludeId] : []);
|
|
4633
|
+
return row?.order_key ?? null;
|
|
4634
|
+
}
|
|
4635
|
+
function orderedSiblings(db, parentId) {
|
|
4636
|
+
const w2 = siblingWhere(parentId);
|
|
4637
|
+
return db.query(`SELECT id, order_key FROM documents WHERE ${w2.clause} AND __deleted = 0 ${ORDER_BY}`).all(...w2.args);
|
|
4638
|
+
}
|
|
4639
|
+
function rebalanceSiblings(db, parentId) {
|
|
4640
|
+
const rows = orderedSiblings(db, parentId);
|
|
4641
|
+
const keys = keysBetween(null, null, rows.length);
|
|
4642
|
+
rows.forEach((row, i2) => {
|
|
4643
|
+
if (row.order_key !== keys[i2])
|
|
4644
|
+
emit(db, "documents", row.id, "order_key", keys[i2]);
|
|
4645
|
+
});
|
|
4646
|
+
}
|
|
4647
|
+
function backfillSiblings(db, parentId) {
|
|
4648
|
+
const emitChange = canEmitOrderKeys2(db);
|
|
4649
|
+
const w2 = siblingWhere(parentId);
|
|
4650
|
+
const missing = db.query(`SELECT id FROM documents WHERE ${w2.clause} AND __deleted = 0 AND order_key IS NULL
|
|
4651
|
+
ORDER BY created_hlc, id`).all(...w2.args);
|
|
4652
|
+
if (!missing.length)
|
|
4653
|
+
return;
|
|
4654
|
+
const keys = keysBetween(lastSiblingOrderKey(db, parentId), null, missing.length);
|
|
4655
|
+
missing.forEach((row, i2) => {
|
|
4656
|
+
if (emitChange)
|
|
4657
|
+
emit(db, "documents", row.id, "order_key", keys[i2]);
|
|
4658
|
+
else
|
|
4659
|
+
db.query("UPDATE documents SET order_key = ? WHERE id = ?").run(keys[i2], row.id);
|
|
4660
|
+
});
|
|
4661
|
+
}
|
|
4662
|
+
function backfillDocumentOrderKeys(db) {
|
|
4663
|
+
const parents = db.query("SELECT DISTINCT parent_id FROM documents WHERE __deleted = 0 AND order_key IS NULL").all();
|
|
4664
|
+
for (const p of parents)
|
|
4665
|
+
backfillSiblings(db, p.parent_id);
|
|
4666
|
+
}
|
|
4667
|
+
function placeInSiblings(db, id, parentId, anchor) {
|
|
4668
|
+
let cur = parentId;
|
|
4669
|
+
while (cur) {
|
|
4670
|
+
if (cur === id)
|
|
4671
|
+
throw new Error(`cannot set parent_id: would create a cycle (${id})`);
|
|
4672
|
+
cur = getDocument(db, cur)?.parent_id ?? null;
|
|
4673
|
+
}
|
|
4674
|
+
const current = getDocument(db, id);
|
|
4675
|
+
if (current && (current.parent_id ?? null) !== parentId)
|
|
4676
|
+
emit(db, "documents", id, "parent_id", parentId);
|
|
4677
|
+
let key;
|
|
4678
|
+
if (!anchor) {
|
|
4679
|
+
key = keyBetween(lastSiblingOrderKey(db, parentId, id), null);
|
|
4680
|
+
} else {
|
|
4681
|
+
backfillSiblings(db, parentId);
|
|
4682
|
+
const neighbors = () => {
|
|
4683
|
+
const rows = orderedSiblings(db, parentId).filter((r3) => r3.id !== id);
|
|
4684
|
+
const to = rows.findIndex((r3) => r3.id === anchor.targetId);
|
|
4685
|
+
if (to < 0)
|
|
4686
|
+
throw new Error(`no such target document: ${anchor.targetId}`);
|
|
4687
|
+
return anchor.where === "before" ? [rows[to - 1]?.order_key ?? null, rows[to].order_key] : [rows[to].order_key, rows[to + 1]?.order_key ?? null];
|
|
4688
|
+
};
|
|
4689
|
+
let [left, right] = neighbors();
|
|
4690
|
+
if (left !== null && right !== null && left >= right) {
|
|
4691
|
+
rebalanceSiblings(db, parentId);
|
|
4692
|
+
[left, right] = neighbors();
|
|
4693
|
+
}
|
|
4694
|
+
key = keyBetween(left, right);
|
|
4695
|
+
}
|
|
4696
|
+
emit(db, "documents", id, "order_key", key);
|
|
4697
|
+
}
|
|
4698
|
+
function moveDocument(db, id, targetId, where) {
|
|
4699
|
+
const src2 = getDocument(db, id);
|
|
4700
|
+
if (!src2)
|
|
4701
|
+
throw new Error(`no such document: ${id}`);
|
|
4702
|
+
if (id === targetId)
|
|
4703
|
+
return src2;
|
|
4704
|
+
const target = getDocument(db, targetId);
|
|
4705
|
+
if (!target)
|
|
4706
|
+
throw new Error(`no such target document: ${targetId}`);
|
|
4707
|
+
if (where === "into")
|
|
4708
|
+
placeInSiblings(db, id, targetId);
|
|
4709
|
+
else
|
|
4710
|
+
placeInSiblings(db, id, target.parent_id, { targetId, where });
|
|
4711
|
+
return getDocument(db, id);
|
|
4712
|
+
}
|
|
4713
|
+
function updateDocument(db, id, fields) {
|
|
4714
|
+
if (!getDocument(db, id))
|
|
4715
|
+
throw new Error(`no such document: ${id}`);
|
|
4716
|
+
if (fields.title !== undefined)
|
|
4717
|
+
emit(db, "documents", id, "title", fields.title);
|
|
4718
|
+
if (fields.database_id !== undefined)
|
|
4719
|
+
emit(db, "documents", id, "database_id", fields.database_id);
|
|
4720
|
+
if (fields.parent_id !== undefined) {
|
|
4721
|
+
const next = fields.parent_id ?? null;
|
|
4722
|
+
if ((getDocument(db, id).parent_id ?? null) !== next)
|
|
4723
|
+
placeInSiblings(db, id, next);
|
|
4724
|
+
}
|
|
4725
|
+
if (fields.body !== undefined) {
|
|
4726
|
+
ensureBlocks(db, id);
|
|
4727
|
+
reconcileBody(db, id, fields.body);
|
|
4728
|
+
}
|
|
4729
|
+
return getDocument(db, id);
|
|
4730
|
+
}
|
|
4731
|
+
function editDocument(db, id, opts) {
|
|
4732
|
+
if (!getDocument(db, id))
|
|
4733
|
+
throw new Error(`no such document: ${id}`);
|
|
4734
|
+
if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
|
|
4735
|
+
throw new Error(`stale: document changed since ${opts.ifMatch}; re-read first`);
|
|
4736
|
+
if (opts.old === "")
|
|
4737
|
+
throw new Error("--old must not be empty");
|
|
4738
|
+
ensureBlocks(db, id);
|
|
4739
|
+
const blocks = liveBlocks(db, id);
|
|
4740
|
+
const body = serializeDocBlocks(blocks.map((b2) => ({ text: b2.text, blankAfter: b2.blank_after })));
|
|
4741
|
+
const count = countOccurrences(body, opts.old);
|
|
4742
|
+
if (count === 0)
|
|
4743
|
+
throw new Error(`anchor not found: no match for --old`);
|
|
4744
|
+
if (count > 1 && !opts.replaceAll)
|
|
4745
|
+
throw new Error(`${count} matches for --old; add surrounding context or use --replace-all`);
|
|
4746
|
+
if (!opts.old.includes(`
|
|
4747
|
+
|
|
4748
|
+
`)) {
|
|
4749
|
+
const affected = blocks.filter((b2) => b2.text.includes(opts.old));
|
|
4750
|
+
const next = affected.map((b2) => opts.replaceAll ? b2.text.replaceAll(opts.old, opts.new) : b2.text.replace(opts.old, opts.new));
|
|
4751
|
+
if (next.every((t2) => t2.length > 0 && !t2.includes(`
|
|
4752
|
+
|
|
4753
|
+
`))) {
|
|
4754
|
+
const targets = opts.replaceAll ? affected : affected.slice(0, 1);
|
|
4755
|
+
targets.forEach((b2, i2) => emit(db, "doc_blocks", b2.id, "text", next[i2]));
|
|
4756
|
+
return { id, changed: true, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
|
|
4757
|
+
}
|
|
4758
|
+
}
|
|
4759
|
+
const newBody = opts.replaceAll ? body.replaceAll(opts.old, opts.new) : body.replace(opts.old, opts.new);
|
|
4760
|
+
reconcileBody(db, id, newBody);
|
|
4761
|
+
return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
|
|
4762
|
+
}
|
|
4763
|
+
function appendDocument(db, id, body) {
|
|
4764
|
+
if (!getDocument(db, id))
|
|
4765
|
+
throw new Error(`no such document: ${id}`);
|
|
4766
|
+
ensureBlocks(db, id);
|
|
4767
|
+
const next = parseDocBlocks(body);
|
|
4768
|
+
if (next.length) {
|
|
4769
|
+
const blocks = liveBlocks(db, id);
|
|
4770
|
+
const last = blocks.length ? blocks[blocks.length - 1].order_key : null;
|
|
4771
|
+
insertBlocks(db, id, next, last, null);
|
|
4772
|
+
}
|
|
4773
|
+
return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
|
|
4774
|
+
}
|
|
4775
|
+
function prependDocument(db, id, body) {
|
|
4776
|
+
if (!getDocument(db, id))
|
|
4777
|
+
throw new Error(`no such document: ${id}`);
|
|
4778
|
+
ensureBlocks(db, id);
|
|
4779
|
+
const next = parseDocBlocks(body);
|
|
4780
|
+
if (next.length) {
|
|
4781
|
+
const blocks = liveBlocks(db, id);
|
|
4782
|
+
const first = blocks.length ? blocks[0].order_key : null;
|
|
4783
|
+
insertBlocks(db, id, next, null, first);
|
|
4784
|
+
}
|
|
4785
|
+
return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
|
|
4786
|
+
}
|
|
4787
|
+
function deleteDocument(db, id) {
|
|
4788
|
+
if (!getDocument(db, id))
|
|
4789
|
+
return false;
|
|
4790
|
+
emit(db, "documents", id, "__deleted", 1);
|
|
4791
|
+
for (const b2 of liveChildIds(db, "doc_blocks", "doc_id", id))
|
|
4792
|
+
emit(db, "doc_blocks", b2, "__deleted", 1);
|
|
4793
|
+
for (const c3 of liveChildIds(db, "documents", "parent_id", id))
|
|
4794
|
+
emit(db, "documents", c3, "parent_id", null);
|
|
4795
|
+
return true;
|
|
4796
|
+
}
|
|
4797
|
+
function liveChildIds(db, table, col, parentId) {
|
|
4798
|
+
return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
|
|
4799
|
+
}
|
|
4800
|
+
function countOccurrences(haystack, needle) {
|
|
4801
|
+
if (needle === "")
|
|
4802
|
+
return 0;
|
|
4803
|
+
let n2 = 0;
|
|
4804
|
+
let i2 = haystack.indexOf(needle);
|
|
4805
|
+
while (i2 !== -1) {
|
|
4806
|
+
n2++;
|
|
4807
|
+
i2 = haystack.indexOf(needle, i2 + needle.length);
|
|
4808
|
+
}
|
|
4809
|
+
return n2;
|
|
4810
|
+
}
|
|
4811
|
+
|
|
4401
4812
|
// src/core/db.ts
|
|
4402
4813
|
function ensureDirs() {
|
|
4403
4814
|
mkdirSync(metahubHome(), { recursive: true });
|
|
@@ -4417,7 +4828,7 @@ function hasColumn(db, table, column) {
|
|
|
4417
4828
|
const cols = db.query(`PRAGMA table_info(${table})`).all();
|
|
4418
4829
|
return cols.some((c3) => c3.name === column);
|
|
4419
4830
|
}
|
|
4420
|
-
function
|
|
4831
|
+
function tableExists3(db, table) {
|
|
4421
4832
|
return db.query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) != null;
|
|
4422
4833
|
}
|
|
4423
4834
|
function migrateRecords(db) {
|
|
@@ -4425,7 +4836,7 @@ function migrateRecords(db) {
|
|
|
4425
4836
|
db.exec("ALTER TABLE records ADD COLUMN data TEXT NOT NULL DEFAULT '{}'");
|
|
4426
4837
|
if (!hasColumn(db, "records", "order_key"))
|
|
4427
4838
|
db.exec("ALTER TABLE records ADD COLUMN order_key TEXT");
|
|
4428
|
-
if (!
|
|
4839
|
+
if (!tableExists3(db, "record_values"))
|
|
4429
4840
|
return;
|
|
4430
4841
|
const tx = db.transaction(() => {
|
|
4431
4842
|
db.exec(`
|
|
@@ -4460,6 +4871,11 @@ function migrateDocBlocks(db) {
|
|
|
4460
4871
|
if (!hasColumn(db, "doc_blocks", "blank_after"))
|
|
4461
4872
|
db.exec("ALTER TABLE doc_blocks ADD COLUMN blank_after INTEGER NOT NULL DEFAULT 0");
|
|
4462
4873
|
}
|
|
4874
|
+
function migrateDocuments(db) {
|
|
4875
|
+
if (!hasColumn(db, "documents", "order_key"))
|
|
4876
|
+
db.exec("ALTER TABLE documents ADD COLUMN order_key TEXT");
|
|
4877
|
+
backfillDocumentOrderKeys(db);
|
|
4878
|
+
}
|
|
4463
4879
|
function openMetahub() {
|
|
4464
4880
|
ensureDirs();
|
|
4465
4881
|
const db = new Database(dbPath(), { create: true });
|
|
@@ -4468,6 +4884,7 @@ function openMetahub() {
|
|
|
4468
4884
|
migrateRecords(db);
|
|
4469
4885
|
migratePeers(db);
|
|
4470
4886
|
migrateDocBlocks(db);
|
|
4887
|
+
migrateDocuments(db);
|
|
4471
4888
|
return db;
|
|
4472
4889
|
}
|
|
4473
4890
|
|
|
@@ -4635,198 +5052,6 @@ var use_default = defineCommand({
|
|
|
4635
5052
|
})
|
|
4636
5053
|
});
|
|
4637
5054
|
|
|
4638
|
-
// src/core/documents.ts
|
|
4639
|
-
init_ids();
|
|
4640
|
-
init_crdt();
|
|
4641
|
-
function liveBlocks(db, docId) {
|
|
4642
|
-
return db.query("SELECT id, text, order_key, blank_after FROM doc_blocks WHERE doc_id = ? AND __deleted = 0 ORDER BY order_key, id").all(docId);
|
|
4643
|
-
}
|
|
4644
|
-
function makeBlockId(text) {
|
|
4645
|
-
return newId("blk", text.split(`
|
|
4646
|
-
`, 1)[0] ?? "");
|
|
4647
|
-
}
|
|
4648
|
-
function emitBlock(db, docId, fields) {
|
|
4649
|
-
const id = makeBlockId(fields.text);
|
|
4650
|
-
emit(db, "doc_blocks", id, "doc_id", docId);
|
|
4651
|
-
emit(db, "doc_blocks", id, "order_key", fields.order_key);
|
|
4652
|
-
if (fields.blankAfter)
|
|
4653
|
-
emit(db, "doc_blocks", id, "blank_after", fields.blankAfter);
|
|
4654
|
-
emit(db, "doc_blocks", id, "text", fields.text);
|
|
4655
|
-
}
|
|
4656
|
-
function insertBlocks(db, docId, blocks, after, before) {
|
|
4657
|
-
const keys = keysBetween(after, before, blocks.length);
|
|
4658
|
-
blocks.forEach((b2, i2) => emitBlock(db, docId, { text: b2.text, order_key: keys[i2], blankAfter: b2.blankAfter }));
|
|
4659
|
-
}
|
|
4660
|
-
function ensureBlocks(db, docId) {
|
|
4661
|
-
if (db.query("SELECT 1 AS x FROM doc_blocks WHERE doc_id = ? LIMIT 1").get(docId))
|
|
4662
|
-
return;
|
|
4663
|
-
const row = db.query("SELECT body FROM documents WHERE id = ?").get(docId);
|
|
4664
|
-
const blocks = parseDocBlocks(row?.body ?? "");
|
|
4665
|
-
if (blocks.length)
|
|
4666
|
-
insertBlocks(db, docId, blocks, null, null);
|
|
4667
|
-
}
|
|
4668
|
-
function reconcileBody(db, docId, body) {
|
|
4669
|
-
const old = liveBlocks(db, docId);
|
|
4670
|
-
const next = parseDocBlocks(body);
|
|
4671
|
-
const plan = reconcile(old.map((b2) => b2.text), next.map((b2) => b2.text));
|
|
4672
|
-
for (const oi of plan.deleted)
|
|
4673
|
-
emit(db, "doc_blocks", old[oi].id, "__deleted", 1);
|
|
4674
|
-
const { items } = plan;
|
|
4675
|
-
let i2 = 0;
|
|
4676
|
-
while (i2 < items.length) {
|
|
4677
|
-
const it = items[i2];
|
|
4678
|
-
if ("keep" in it) {
|
|
4679
|
-
const oldB = old[it.keep];
|
|
4680
|
-
const want = next[i2].blankAfter;
|
|
4681
|
-
if ((oldB.blank_after ?? 0) !== want)
|
|
4682
|
-
emit(db, "doc_blocks", oldB.id, "blank_after", want);
|
|
4683
|
-
i2++;
|
|
4684
|
-
continue;
|
|
4685
|
-
}
|
|
4686
|
-
let j = i2;
|
|
4687
|
-
while (j < items.length && "insert" in items[j])
|
|
4688
|
-
j++;
|
|
4689
|
-
const left = i2 > 0 ? old[items[i2 - 1].keep].order_key : null;
|
|
4690
|
-
const right = j < items.length ? old[items[j].keep].order_key : null;
|
|
4691
|
-
insertBlocks(db, docId, next.slice(i2, j), left, right);
|
|
4692
|
-
i2 = j;
|
|
4693
|
-
}
|
|
4694
|
-
}
|
|
4695
|
-
function documentVersion(db, id) {
|
|
4696
|
-
const row = db.query(`SELECT MAX(hlc) AS h FROM crdt_changes
|
|
4697
|
-
WHERE (dataset = 'documents' AND row_id = ?)
|
|
4698
|
-
OR (dataset = 'doc_blocks' AND row_id IN (SELECT id FROM doc_blocks WHERE doc_id = ?))`).get(id, id);
|
|
4699
|
-
return row.h ?? "";
|
|
4700
|
-
}
|
|
4701
|
-
function createDocument(db, opts) {
|
|
4702
|
-
const id = newId("doc", opts.title);
|
|
4703
|
-
const first = emit(db, "documents", id, "title", opts.title);
|
|
4704
|
-
emit(db, "documents", id, "created_hlc", first.hlc);
|
|
4705
|
-
if (opts.database_id !== undefined)
|
|
4706
|
-
emit(db, "documents", id, "database_id", opts.database_id);
|
|
4707
|
-
if (opts.parent_id !== undefined)
|
|
4708
|
-
emit(db, "documents", id, "parent_id", opts.parent_id);
|
|
4709
|
-
if (opts.body !== undefined) {
|
|
4710
|
-
const blocks = parseDocBlocks(opts.body);
|
|
4711
|
-
if (blocks.length)
|
|
4712
|
-
insertBlocks(db, id, blocks, null, null);
|
|
4713
|
-
}
|
|
4714
|
-
return getDocument(db, id);
|
|
4715
|
-
}
|
|
4716
|
-
function getDocument(db, id) {
|
|
4717
|
-
return db.query("SELECT id, title, body, database_id, parent_id, created_hlc FROM documents WHERE id = ? AND __deleted = 0").get(id);
|
|
4718
|
-
}
|
|
4719
|
-
function listDocuments(db, opts = {}) {
|
|
4720
|
-
const cols = "SELECT id, title, database_id, parent_id, created_hlc FROM documents WHERE __deleted = 0";
|
|
4721
|
-
if (opts.parent_id !== undefined)
|
|
4722
|
-
return db.query(`${cols} AND parent_id = ? ORDER BY created_hlc`).all(opts.parent_id);
|
|
4723
|
-
if (opts.database_id)
|
|
4724
|
-
return db.query(`${cols} AND database_id = ? ORDER BY created_hlc`).all(opts.database_id);
|
|
4725
|
-
return db.query(`${cols} ORDER BY created_hlc`).all();
|
|
4726
|
-
}
|
|
4727
|
-
function updateDocument(db, id, fields) {
|
|
4728
|
-
if (!getDocument(db, id))
|
|
4729
|
-
throw new Error(`no such document: ${id}`);
|
|
4730
|
-
if (fields.title !== undefined)
|
|
4731
|
-
emit(db, "documents", id, "title", fields.title);
|
|
4732
|
-
if (fields.database_id !== undefined)
|
|
4733
|
-
emit(db, "documents", id, "database_id", fields.database_id);
|
|
4734
|
-
if (fields.parent_id !== undefined) {
|
|
4735
|
-
let cur = fields.parent_id;
|
|
4736
|
-
while (cur) {
|
|
4737
|
-
if (cur === id)
|
|
4738
|
-
throw new Error(`cannot set parent_id: would create a cycle (${id})`);
|
|
4739
|
-
cur = getDocument(db, cur)?.parent_id ?? null;
|
|
4740
|
-
}
|
|
4741
|
-
emit(db, "documents", id, "parent_id", fields.parent_id);
|
|
4742
|
-
}
|
|
4743
|
-
if (fields.body !== undefined) {
|
|
4744
|
-
ensureBlocks(db, id);
|
|
4745
|
-
reconcileBody(db, id, fields.body);
|
|
4746
|
-
}
|
|
4747
|
-
return getDocument(db, id);
|
|
4748
|
-
}
|
|
4749
|
-
function editDocument(db, id, opts) {
|
|
4750
|
-
if (!getDocument(db, id))
|
|
4751
|
-
throw new Error(`no such document: ${id}`);
|
|
4752
|
-
if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
|
|
4753
|
-
throw new Error(`stale: document changed since ${opts.ifMatch}; re-read first`);
|
|
4754
|
-
if (opts.old === "")
|
|
4755
|
-
throw new Error("--old must not be empty");
|
|
4756
|
-
ensureBlocks(db, id);
|
|
4757
|
-
const blocks = liveBlocks(db, id);
|
|
4758
|
-
const body = serializeDocBlocks(blocks.map((b2) => ({ text: b2.text, blankAfter: b2.blank_after })));
|
|
4759
|
-
const count = countOccurrences(body, opts.old);
|
|
4760
|
-
if (count === 0)
|
|
4761
|
-
throw new Error(`anchor not found: no match for --old`);
|
|
4762
|
-
if (count > 1 && !opts.replaceAll)
|
|
4763
|
-
throw new Error(`${count} matches for --old; add surrounding context or use --replace-all`);
|
|
4764
|
-
if (!opts.old.includes(`
|
|
4765
|
-
|
|
4766
|
-
`)) {
|
|
4767
|
-
const affected = blocks.filter((b2) => b2.text.includes(opts.old));
|
|
4768
|
-
const next = affected.map((b2) => opts.replaceAll ? b2.text.replaceAll(opts.old, opts.new) : b2.text.replace(opts.old, opts.new));
|
|
4769
|
-
if (next.every((t2) => t2.length > 0 && !t2.includes(`
|
|
4770
|
-
|
|
4771
|
-
`))) {
|
|
4772
|
-
const targets = opts.replaceAll ? affected : affected.slice(0, 1);
|
|
4773
|
-
targets.forEach((b2, i2) => emit(db, "doc_blocks", b2.id, "text", next[i2]));
|
|
4774
|
-
return { id, changed: true, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
|
|
4775
|
-
}
|
|
4776
|
-
}
|
|
4777
|
-
const newBody = opts.replaceAll ? body.replaceAll(opts.old, opts.new) : body.replace(opts.old, opts.new);
|
|
4778
|
-
reconcileBody(db, id, newBody);
|
|
4779
|
-
return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
|
|
4780
|
-
}
|
|
4781
|
-
function appendDocument(db, id, body) {
|
|
4782
|
-
if (!getDocument(db, id))
|
|
4783
|
-
throw new Error(`no such document: ${id}`);
|
|
4784
|
-
ensureBlocks(db, id);
|
|
4785
|
-
const next = parseDocBlocks(body);
|
|
4786
|
-
if (next.length) {
|
|
4787
|
-
const blocks = liveBlocks(db, id);
|
|
4788
|
-
const last = blocks.length ? blocks[blocks.length - 1].order_key : null;
|
|
4789
|
-
insertBlocks(db, id, next, last, null);
|
|
4790
|
-
}
|
|
4791
|
-
return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
|
|
4792
|
-
}
|
|
4793
|
-
function prependDocument(db, id, body) {
|
|
4794
|
-
if (!getDocument(db, id))
|
|
4795
|
-
throw new Error(`no such document: ${id}`);
|
|
4796
|
-
ensureBlocks(db, id);
|
|
4797
|
-
const next = parseDocBlocks(body);
|
|
4798
|
-
if (next.length) {
|
|
4799
|
-
const blocks = liveBlocks(db, id);
|
|
4800
|
-
const first = blocks.length ? blocks[0].order_key : null;
|
|
4801
|
-
insertBlocks(db, id, next, null, first);
|
|
4802
|
-
}
|
|
4803
|
-
return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
|
|
4804
|
-
}
|
|
4805
|
-
function deleteDocument(db, id) {
|
|
4806
|
-
if (!getDocument(db, id))
|
|
4807
|
-
return false;
|
|
4808
|
-
emit(db, "documents", id, "__deleted", 1);
|
|
4809
|
-
for (const b2 of liveChildIds(db, "doc_blocks", "doc_id", id))
|
|
4810
|
-
emit(db, "doc_blocks", b2, "__deleted", 1);
|
|
4811
|
-
for (const c3 of liveChildIds(db, "documents", "parent_id", id))
|
|
4812
|
-
emit(db, "documents", c3, "parent_id", null);
|
|
4813
|
-
return true;
|
|
4814
|
-
}
|
|
4815
|
-
function liveChildIds(db, table2, col, parentId) {
|
|
4816
|
-
return db.query(`SELECT id FROM ${table2} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r3) => r3.id);
|
|
4817
|
-
}
|
|
4818
|
-
function countOccurrences(haystack, needle) {
|
|
4819
|
-
if (needle === "")
|
|
4820
|
-
return 0;
|
|
4821
|
-
let n2 = 0;
|
|
4822
|
-
let i2 = haystack.indexOf(needle);
|
|
4823
|
-
while (i2 !== -1) {
|
|
4824
|
-
n2++;
|
|
4825
|
-
i2 = haystack.indexOf(needle, i2 + needle.length);
|
|
4826
|
-
}
|
|
4827
|
-
return n2;
|
|
4828
|
-
}
|
|
4829
|
-
|
|
4830
5055
|
// src/cli/commands/get.ts
|
|
4831
5056
|
var get_default = defineCommand({
|
|
4832
5057
|
meta: { name: "get", description: "Look up any entity by ref (id/prefix/name), type auto-detected" },
|
|
@@ -22770,7 +22995,7 @@ var PropertySchema = exports_external.object({
|
|
|
22770
22995
|
var RecordSchema = exports_external.object({
|
|
22771
22996
|
id: exports_external.string(),
|
|
22772
22997
|
database_id: exports_external.string(),
|
|
22773
|
-
values: exports_external.record(exports_external.string(), exports_external.any())
|
|
22998
|
+
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.')
|
|
22774
22999
|
});
|
|
22775
23000
|
var MoveRecordReq = exports_external.object({
|
|
22776
23001
|
target: exports_external.string(),
|
|
@@ -22781,7 +23006,12 @@ var DocumentSummarySchema = exports_external.object({
|
|
|
22781
23006
|
title: exports_external.string(),
|
|
22782
23007
|
database_id: exports_external.string().nullable(),
|
|
22783
23008
|
parent_id: exports_external.string().nullable(),
|
|
22784
|
-
created_hlc: exports_external.string()
|
|
23009
|
+
created_hlc: exports_external.string(),
|
|
23010
|
+
order_key: exports_external.string().nullable()
|
|
23011
|
+
});
|
|
23012
|
+
var MoveDocumentReq = exports_external.object({
|
|
23013
|
+
target: exports_external.string(),
|
|
23014
|
+
where: exports_external.enum(["before", "after", "into"])
|
|
22785
23015
|
});
|
|
22786
23016
|
var DocumentSchema = DocumentSummarySchema.extend({ body: exports_external.string().nullable() });
|
|
22787
23017
|
var SearchHitSchema = exports_external.object({
|
|
@@ -22810,7 +23040,7 @@ var UpdatePropertyReq = exports_external.object({
|
|
|
22810
23040
|
position: exports_external.number().optional()
|
|
22811
23041
|
});
|
|
22812
23042
|
var SetWidthReq = exports_external.object({ width: exports_external.number() });
|
|
22813
|
-
var RecordValuesReq = exports_external.record(exports_external.string(), exports_external.any());
|
|
23043
|
+
var RecordValuesReq = exports_external.record(exports_external.string(), exports_external.any()).describe("Flat { column: value } cell patch (keyed by property name).");
|
|
22814
23044
|
var CreateDocumentReq = exports_external.object({
|
|
22815
23045
|
title: exports_external.string(),
|
|
22816
23046
|
body: exports_external.string().optional(),
|
|
@@ -23037,6 +23267,17 @@ var webuiRoutes = [
|
|
|
23037
23267
|
return updateDocument(db, need(req, "id"), body);
|
|
23038
23268
|
})
|
|
23039
23269
|
},
|
|
23270
|
+
{
|
|
23271
|
+
method: "PATCH",
|
|
23272
|
+
path: "/api/document/move",
|
|
23273
|
+
summary: "Move a document before/after another, or into it as a child. Query: ?id=<id>",
|
|
23274
|
+
request: MoveDocumentReq,
|
|
23275
|
+
response: DocumentSchema,
|
|
23276
|
+
handler: handle(async (req, { db }) => {
|
|
23277
|
+
const body = await req.json();
|
|
23278
|
+
return moveDocument(db, need(req, "id"), body.target, body.where);
|
|
23279
|
+
})
|
|
23280
|
+
},
|
|
23040
23281
|
{
|
|
23041
23282
|
method: "DELETE",
|
|
23042
23283
|
path: "/api/document",
|
|
@@ -23064,6 +23305,7 @@ var SiteSchema = exports_external.object({
|
|
|
23064
23305
|
title: exports_external.string().nullable(),
|
|
23065
23306
|
created_hlc: exports_external.string()
|
|
23066
23307
|
});
|
|
23308
|
+
var SiteWithCountSchema = SiteSchema.extend({ file_count: exports_external.number() });
|
|
23067
23309
|
var SiteFileSchema = exports_external.object({
|
|
23068
23310
|
id: exports_external.string(),
|
|
23069
23311
|
site_id: exports_external.string(),
|
|
@@ -23071,6 +23313,8 @@ var SiteFileSchema = exports_external.object({
|
|
|
23071
23313
|
content_type: exports_external.string(),
|
|
23072
23314
|
encoding: exports_external.string()
|
|
23073
23315
|
});
|
|
23316
|
+
var CreateSiteBody = exports_external.object({ name: exports_external.string(), title: exports_external.string().optional() });
|
|
23317
|
+
var UpdateSiteBody = exports_external.object({ name: exports_external.string().optional(), title: exports_external.string().optional() });
|
|
23074
23318
|
function need2(req, key) {
|
|
23075
23319
|
const v3 = new URL(req.url).searchParams.get(key);
|
|
23076
23320
|
if (!v3)
|
|
@@ -23090,9 +23334,9 @@ var sitesRoutes = [
|
|
|
23090
23334
|
{
|
|
23091
23335
|
method: "GET",
|
|
23092
23336
|
path: "/api/sites",
|
|
23093
|
-
summary: "List published sites",
|
|
23094
|
-
response: exports_external.array(
|
|
23095
|
-
handler: handle2((_req, { db }) => listSites(db))
|
|
23337
|
+
summary: "List published sites (with file counts)",
|
|
23338
|
+
response: exports_external.array(SiteWithCountSchema),
|
|
23339
|
+
handler: handle2((_req, { db }) => listSites(db).map((s2) => ({ ...s2, file_count: listFiles(db, s2.id).length })))
|
|
23096
23340
|
},
|
|
23097
23341
|
{
|
|
23098
23342
|
method: "GET",
|
|
@@ -23100,6 +23344,60 @@ var sitesRoutes = [
|
|
|
23100
23344
|
summary: "List a site's files (manifest). Query: ?site=<id|name>",
|
|
23101
23345
|
response: exports_external.array(SiteFileSchema),
|
|
23102
23346
|
handler: handle2((req, { db }) => listFiles(db, resolveSite(db, need2(req, "site")).id))
|
|
23347
|
+
},
|
|
23348
|
+
{
|
|
23349
|
+
method: "POST",
|
|
23350
|
+
path: "/api/sites",
|
|
23351
|
+
summary: "Create a site",
|
|
23352
|
+
request: CreateSiteBody,
|
|
23353
|
+
response: SiteSchema,
|
|
23354
|
+
handler: handle2(async (req, { db }) => {
|
|
23355
|
+
const body = CreateSiteBody.parse(await req.json());
|
|
23356
|
+
return createSite(db, body);
|
|
23357
|
+
})
|
|
23358
|
+
},
|
|
23359
|
+
{
|
|
23360
|
+
method: "PATCH",
|
|
23361
|
+
path: "/api/site",
|
|
23362
|
+
summary: "Rename a site or change its title. Query: ?id=<id>",
|
|
23363
|
+
request: UpdateSiteBody,
|
|
23364
|
+
response: SiteSchema,
|
|
23365
|
+
handler: handle2(async (req, { db }) => {
|
|
23366
|
+
const body = UpdateSiteBody.parse(await req.json());
|
|
23367
|
+
return updateSite(db, need2(req, "id"), body);
|
|
23368
|
+
})
|
|
23369
|
+
},
|
|
23370
|
+
{
|
|
23371
|
+
method: "DELETE",
|
|
23372
|
+
path: "/api/site",
|
|
23373
|
+
summary: "Delete a site and its files. Query: ?id=<id>",
|
|
23374
|
+
response: exports_external.object({ ok: exports_external.boolean() }),
|
|
23375
|
+
handler: handle2((req, { db }) => ({ ok: deleteSite(db, need2(req, "id")) }))
|
|
23376
|
+
},
|
|
23377
|
+
{
|
|
23378
|
+
method: "POST",
|
|
23379
|
+
path: "/api/site/file",
|
|
23380
|
+
summary: "Upload/replace a file. Query: ?site=<id|name>&path=<path>; body is raw bytes",
|
|
23381
|
+
response: SiteFileSchema,
|
|
23382
|
+
handler: handle2(async (req, { db }) => {
|
|
23383
|
+
const site = resolveSite(db, need2(req, "site"));
|
|
23384
|
+
const ct3 = req.headers.get("content-type");
|
|
23385
|
+
const file2 = await putFile(db, site.id, need2(req, "path"), {
|
|
23386
|
+
data: await req.arrayBuffer(),
|
|
23387
|
+
contentType: ct3 && ct3 !== "application/octet-stream" ? ct3 : undefined
|
|
23388
|
+
});
|
|
23389
|
+
const { content: _content, ...summary } = file2;
|
|
23390
|
+
return summary;
|
|
23391
|
+
})
|
|
23392
|
+
},
|
|
23393
|
+
{
|
|
23394
|
+
method: "DELETE",
|
|
23395
|
+
path: "/api/site/file",
|
|
23396
|
+
summary: "Delete a file. Query: ?site=<id|name>&path=<path>",
|
|
23397
|
+
response: exports_external.object({ ok: exports_external.boolean() }),
|
|
23398
|
+
handler: handle2((req, { db }) => ({
|
|
23399
|
+
ok: deleteFile(db, resolveSite(db, need2(req, "site")).id, need2(req, "path"))
|
|
23400
|
+
}))
|
|
23103
23401
|
}
|
|
23104
23402
|
];
|
|
23105
23403
|
|
|
@@ -23726,7 +24024,9 @@ USAGE
|
|
|
23726
24024
|
CONCEPTS
|
|
23727
24025
|
database (db) A typed table. Owns properties and records.
|
|
23728
24026
|
property (prop) A column with a type (text|number|...). Belongs to a db.
|
|
23729
|
-
record (rec) A row
|
|
24027
|
+
record (rec) A row of cells. Write a flat { column: value } map; reads
|
|
24028
|
+
wrap them as { id, database_id, values: { column: value } }
|
|
24029
|
+
\u2014 field values live under "values", keyed by property name.
|
|
23730
24030
|
document (doc) A markdown doc, stored as ordered blocks (block-level CRDT).
|
|
23731
24031
|
site (site) A named bucket of files (HTML/CSS/JS) served at /sites/<name>/.
|
|
23732
24032
|
Every id is "<kind>_<slug>-<rand>", e.g. db_tasks-k3f9c1, rec_fix-login-7j02an.
|
|
@@ -23823,6 +24123,13 @@ AI WORKFLOW (edit a doc without clobbering concurrent changes)
|
|
|
23823
24123
|
mh doc edit <ref> --old "old text" --new "new text" --if-match <version>
|
|
23824
24124
|
mh doc append <ref> --body "a new paragraph"
|
|
23825
24125
|
|
|
24126
|
+
SITE DATA (read a table from a hosted page \u2014 field values nest under "values")
|
|
24127
|
+
// a page served at /sites/<name>/ reads this library same-origin:
|
|
24128
|
+
const rs = await fetch('/api/records?db=tasks').then(r => r.json());
|
|
24129
|
+
// rs.records: [{ id, database_id, values: { Title, Status } }]
|
|
24130
|
+
rs.records.forEach(r => render(r.values.Title, r.values.Status));
|
|
24131
|
+
// NOTE: writes take a flat { column: value } map; reads wrap cells in .values
|
|
24132
|
+
|
|
23826
24133
|
Run 'mh <command> --help' for arguments and examples of a specific command.`;
|
|
23827
24134
|
var EXAMPLES = {
|
|
23828
24135
|
init: ["mh init", "METAHUB_HOME=/tmp/mh mh init # use an alternate home"],
|
|
@@ -24006,4 +24313,4 @@ if (argv2.includes("--server")) {
|
|
|
24006
24313
|
runMain(main, { showUsage: showUsage2 });
|
|
24007
24314
|
}
|
|
24008
24315
|
|
|
24009
|
-
//# debugId=
|
|
24316
|
+
//# debugId=740ECE016A65928164756E2164756E21
|