@tensorix/metahub 0.1.10 → 0.1.11
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 +19 -6
- package/README.zh-CN.md +7 -2
- package/dist/cli.js +1601 -595
- package/dist/cli.js.map +43 -38
- package/dist/core/compact.d.ts +42 -0
- package/dist/core/compact.d.ts.map +1 -0
- package/dist/core/config.d.ts +3 -3
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/context.d.ts +4 -4
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/crdt.d.ts +30 -7
- package/dist/core/crdt.d.ts.map +1 -1
- package/dist/core/databases.d.ts +22 -8
- package/dist/core/databases.d.ts.map +1 -1
- package/dist/core/db.d.ts +1 -27
- package/dist/core/db.d.ts.map +1 -1
- package/dist/core/documents.d.ts +28 -15
- package/dist/core/documents.d.ts.map +1 -1
- package/dist/core/driver.d.ts +25 -0
- package/dist/core/driver.d.ts.map +1 -0
- package/dist/core/history.d.ts +99 -11
- package/dist/core/history.d.ts.map +1 -1
- package/dist/core/hlc.d.ts +3 -3
- package/dist/core/hlc.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/indexing.d.ts +4 -4
- package/dist/core/indexing.d.ts.map +1 -1
- package/dist/core/integrity.d.ts +1 -1
- package/dist/core/integrity.d.ts.map +1 -1
- package/dist/core/node.d.ts +2 -2
- package/dist/core/node.d.ts.map +1 -1
- package/dist/core/properties.d.ts +10 -9
- package/dist/core/properties.d.ts.map +1 -1
- package/dist/core/records.d.ts +13 -8
- package/dist/core/records.d.ts.map +1 -1
- package/dist/core/resolve.d.ts +4 -4
- package/dist/core/resolve.d.ts.map +1 -1
- package/dist/core/schema-init.d.ts +37 -0
- package/dist/core/schema-init.d.ts.map +1 -0
- package/dist/core/schema.d.ts +1 -1
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/search.d.ts +3 -3
- package/dist/core/search.d.ts.map +1 -1
- package/dist/core/sites-core.d.ts +60 -0
- package/dist/core/sites-core.d.ts.map +1 -0
- package/dist/core/sites.d.ts +11 -52
- package/dist/core/sites.d.ts.map +1 -1
- package/dist/core/sqlcols.d.ts +4 -0
- package/dist/core/sqlcols.d.ts.map +1 -0
- package/dist/core/sync/auth.d.ts +5 -3
- package/dist/core/sync/auth.d.ts.map +1 -1
- package/dist/core/sync/client.d.ts +11 -3
- package/dist/core/sync/client.d.ts.map +1 -1
- package/dist/core/sync/peers.d.ts +10 -10
- package/dist/core/sync/peers.d.ts.map +1 -1
- package/dist/core/sync/protocol.d.ts +5 -0
- package/dist/core/sync/protocol.d.ts.map +1 -1
- package/dist/core/sync/routes.d.ts.map +1 -1
- package/dist/core/sync/server.d.ts +10 -0
- package/dist/core/sync/server.d.ts.map +1 -1
- package/dist/db-worker.js +268 -0
- package/dist/index.js +1061 -340
- package/dist/index.js.map +31 -28
- package/dist/metahub-sdk.js +1 -0
- package/dist/mh-runtime.js +1 -0
- package/dist/sqlite3.wasm +0 -0
- package/dist/sw.js +1 -0
- package/dist/webui/server/assets.d.ts +8 -1
- package/dist/webui/server/assets.d.ts.map +1 -1
- package/dist/webui/server/icons.d.ts +4 -0
- package/dist/webui/server/icons.d.ts.map +1 -0
- package/dist/webui/server/routes.d.ts.map +1 -1
- package/dist/webui.js +57 -57
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -242,6 +242,19 @@ function lcs(a, b) {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
// src/core/crdt.ts
|
|
245
|
+
function withChangeGroup(label, fn) {
|
|
246
|
+
if (currentTxn !== null)
|
|
247
|
+
return fn();
|
|
248
|
+
currentTxn = (label ? label + ":" : "") + randomSuffix(8);
|
|
249
|
+
try {
|
|
250
|
+
return fn();
|
|
251
|
+
} finally {
|
|
252
|
+
currentTxn = null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function grouped(fn, label = null) {
|
|
256
|
+
return (...args) => withChangeGroup(label, () => fn(...args));
|
|
257
|
+
}
|
|
245
258
|
function encodeScalar(val) {
|
|
246
259
|
if (val === null || val === undefined)
|
|
247
260
|
return null;
|
|
@@ -295,7 +308,7 @@ function materialize(db, dataset, rowId, col, valueJson) {
|
|
|
295
308
|
}
|
|
296
309
|
}
|
|
297
310
|
function applyChange(db, c) {
|
|
298
|
-
db.query("INSERT OR IGNORE INTO crdt_changes (hlc, node_id, dataset, row_id, col, value) VALUES (?, ?, ?, ?, ?, ?)").run(c.hlc, c.node_id, c.dataset, c.row_id, c.col, c.value);
|
|
311
|
+
db.query("INSERT OR IGNORE INTO crdt_changes (hlc, node_id, dataset, row_id, col, value, txn) VALUES (?, ?, ?, ?, ?, ?, ?)").run(c.hlc, c.node_id, c.dataset, c.row_id, c.col, c.value, c.txn ?? null);
|
|
299
312
|
const cur = db.query("SELECT MAX(hlc) AS h FROM crdt_changes WHERE dataset = ? AND row_id = ? AND col = ?").get(c.dataset, c.row_id, c.col);
|
|
300
313
|
if (cur.h !== c.hlc)
|
|
301
314
|
return false;
|
|
@@ -310,7 +323,8 @@ function emit(db, dataset, rowId, col, value) {
|
|
|
310
323
|
dataset,
|
|
311
324
|
row_id: rowId,
|
|
312
325
|
col,
|
|
313
|
-
value: value === undefined ? null : JSON.stringify(value)
|
|
326
|
+
value: value === undefined ? null : JSON.stringify(value),
|
|
327
|
+
txn: currentTxn
|
|
314
328
|
};
|
|
315
329
|
applyChange(db, change);
|
|
316
330
|
return change;
|
|
@@ -336,17 +350,29 @@ function ingest(db, changes) {
|
|
|
336
350
|
return applied;
|
|
337
351
|
}
|
|
338
352
|
function changesSince(db, since) {
|
|
339
|
-
return db.query(
|
|
340
|
-
}
|
|
341
|
-
function changesAfterSeq(db, seq) {
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
const
|
|
353
|
+
return db.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes WHERE hlc > ? ORDER BY hlc`).all(since);
|
|
354
|
+
}
|
|
355
|
+
function changesAfterSeq(db, seq, opts = {}) {
|
|
356
|
+
const exclude = opts.excludeDatasets ?? [];
|
|
357
|
+
const where = exclude.length ? `rowid > ? AND dataset NOT IN (${exclude.map(() => "?").join(", ")})` : "rowid > ?";
|
|
358
|
+
const limitSql = opts.limit != null && opts.limit > 0 ? ` LIMIT ${Math.floor(opts.limit)}` : "";
|
|
359
|
+
const rows = db.query(`SELECT rowid AS seq, ${CHANGE_SELECT} FROM crdt_changes WHERE ${where} ORDER BY rowid${limitSql}`).all(seq, ...exclude);
|
|
360
|
+
const exhausted = opts.limit == null || opts.limit <= 0 || rows.length < opts.limit;
|
|
361
|
+
let cursor;
|
|
362
|
+
if (exhausted) {
|
|
363
|
+
const top = db.query("SELECT MAX(rowid) AS m FROM crdt_changes").get();
|
|
364
|
+
cursor = Math.max(seq, top.m ?? seq);
|
|
365
|
+
} else {
|
|
366
|
+
cursor = rows[rows.length - 1].seq;
|
|
367
|
+
}
|
|
345
368
|
return { changes: rows.map(({ seq: _seq, ...c }) => c), cursor };
|
|
346
369
|
}
|
|
347
|
-
var DOMAIN, RECORD_META;
|
|
370
|
+
var CHANGE_COLS, CHANGE_SELECT, currentTxn = null, DOMAIN, RECORD_META;
|
|
348
371
|
var init_crdt = __esm(() => {
|
|
349
372
|
init_node();
|
|
373
|
+
init_ids();
|
|
374
|
+
CHANGE_COLS = ["hlc", "node_id", "dataset", "row_id", "col", "value", "txn"];
|
|
375
|
+
CHANGE_SELECT = CHANGE_COLS.join(", ");
|
|
350
376
|
DOMAIN = {
|
|
351
377
|
databases: {
|
|
352
378
|
table: "databases",
|
|
@@ -409,7 +435,7 @@ var init_cache = __esm(() => {
|
|
|
409
435
|
init_paths();
|
|
410
436
|
});
|
|
411
437
|
|
|
412
|
-
// src/core/sites.ts
|
|
438
|
+
// src/core/sites-core.ts
|
|
413
439
|
function inferContentType(path) {
|
|
414
440
|
const ext = path.slice(path.lastIndexOf(".") + 1).toLowerCase();
|
|
415
441
|
return MIME[ext] ?? "application/octet-stream";
|
|
@@ -432,28 +458,8 @@ function normalizeSitePath(raw) {
|
|
|
432
458
|
}
|
|
433
459
|
return segs.join("/");
|
|
434
460
|
}
|
|
435
|
-
function isTextType(contentType) {
|
|
436
|
-
const ct = contentType.toLowerCase();
|
|
437
|
-
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");
|
|
438
|
-
}
|
|
439
|
-
function toBytes(data) {
|
|
440
|
-
if (typeof data === "string")
|
|
441
|
-
return new TextEncoder().encode(data);
|
|
442
|
-
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
443
|
-
}
|
|
444
|
-
function createSite(db, opts) {
|
|
445
|
-
const name = normalizeSiteName(opts.name);
|
|
446
|
-
if (getSiteByName(db, name))
|
|
447
|
-
throw new MhError("conflict", `site name already exists: ${name}`);
|
|
448
|
-
const id = newId("site", name);
|
|
449
|
-
const first = emit(db, "sites", id, "name", name);
|
|
450
|
-
emit(db, "sites", id, "created_hlc", first.hlc);
|
|
451
|
-
if (opts.title !== undefined)
|
|
452
|
-
emit(db, "sites", id, "title", opts.title);
|
|
453
|
-
return getSite(db, id);
|
|
454
|
-
}
|
|
455
461
|
function getSite(db, id) {
|
|
456
|
-
return db.query(
|
|
462
|
+
return db.query(`SELECT ${SITE_SELECT} FROM sites WHERE id = ? AND __deleted = 0`).get(id);
|
|
457
463
|
}
|
|
458
464
|
function getSiteByName(db, name) {
|
|
459
465
|
let slug;
|
|
@@ -462,10 +468,10 @@ function getSiteByName(db, name) {
|
|
|
462
468
|
} catch {
|
|
463
469
|
return null;
|
|
464
470
|
}
|
|
465
|
-
return db.query(
|
|
471
|
+
return db.query(`SELECT ${SITE_SELECT} FROM sites WHERE name = ? AND __deleted = 0 ORDER BY created_hlc DESC LIMIT 1`).get(slug);
|
|
466
472
|
}
|
|
467
473
|
function listSites(db) {
|
|
468
|
-
return db.query(
|
|
474
|
+
return db.query(`SELECT ${SITE_SELECT} FROM sites WHERE __deleted = 0 ORDER BY created_hlc`).all();
|
|
469
475
|
}
|
|
470
476
|
function resolveSite(db, ref) {
|
|
471
477
|
const byId = ref.startsWith("site_") ? getSite(db, ref) : null;
|
|
@@ -474,28 +480,67 @@ function resolveSite(db, ref) {
|
|
|
474
480
|
throw new MhError("not_found", `no such site: ${ref}`);
|
|
475
481
|
return site;
|
|
476
482
|
}
|
|
477
|
-
function
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (dup && dup.id !== id)
|
|
484
|
-
throw new MhError("conflict", `site name already exists: ${name}`);
|
|
485
|
-
emit(db, "sites", id, "name", name);
|
|
486
|
-
}
|
|
487
|
-
if (opts.title !== undefined)
|
|
488
|
-
emit(db, "sites", id, "title", opts.title);
|
|
489
|
-
return getSite(db, id);
|
|
483
|
+
function getFileRow(db, siteId, path) {
|
|
484
|
+
const withIndex = path === "" || path.endsWith("/") ? `${path}index.html` : path;
|
|
485
|
+
const p = normalizeSitePath(withIndex);
|
|
486
|
+
if (!p)
|
|
487
|
+
return null;
|
|
488
|
+
return 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);
|
|
490
489
|
}
|
|
491
|
-
function
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
490
|
+
function listFiles(db, siteId) {
|
|
491
|
+
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);
|
|
492
|
+
}
|
|
493
|
+
var SITE_COLS, SITE_SELECT, SITE_FILE_COLS, SITE_FILE_SELECT, MIME;
|
|
494
|
+
var init_sites_core = __esm(() => {
|
|
495
|
+
init_errors();
|
|
496
|
+
SITE_COLS = ["id", "name", "title", "created_hlc"];
|
|
497
|
+
SITE_SELECT = SITE_COLS.join(", ");
|
|
498
|
+
SITE_FILE_COLS = [
|
|
499
|
+
"id",
|
|
500
|
+
"site_id",
|
|
501
|
+
"path",
|
|
502
|
+
"content_type",
|
|
503
|
+
"encoding",
|
|
504
|
+
"content"
|
|
505
|
+
];
|
|
506
|
+
SITE_FILE_SELECT = SITE_FILE_COLS.join(", ");
|
|
507
|
+
MIME = {
|
|
508
|
+
html: "text/html; charset=utf-8",
|
|
509
|
+
htm: "text/html; charset=utf-8",
|
|
510
|
+
css: "text/css; charset=utf-8",
|
|
511
|
+
js: "text/javascript; charset=utf-8",
|
|
512
|
+
mjs: "text/javascript; charset=utf-8",
|
|
513
|
+
json: "application/json; charset=utf-8",
|
|
514
|
+
svg: "image/svg+xml",
|
|
515
|
+
txt: "text/plain; charset=utf-8",
|
|
516
|
+
md: "text/markdown; charset=utf-8",
|
|
517
|
+
xml: "application/xml; charset=utf-8",
|
|
518
|
+
webmanifest: "application/manifest+json",
|
|
519
|
+
ico: "image/x-icon",
|
|
520
|
+
png: "image/png",
|
|
521
|
+
jpg: "image/jpeg",
|
|
522
|
+
jpeg: "image/jpeg",
|
|
523
|
+
gif: "image/gif",
|
|
524
|
+
webp: "image/webp",
|
|
525
|
+
avif: "image/avif",
|
|
526
|
+
woff: "font/woff",
|
|
527
|
+
woff2: "font/woff2",
|
|
528
|
+
ttf: "font/ttf",
|
|
529
|
+
otf: "font/otf",
|
|
530
|
+
pdf: "application/pdf",
|
|
531
|
+
wasm: "application/wasm"
|
|
532
|
+
};
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// src/core/sites.ts
|
|
536
|
+
function isTextType(contentType) {
|
|
537
|
+
const ct = contentType.toLowerCase();
|
|
538
|
+
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");
|
|
539
|
+
}
|
|
540
|
+
function toBytes(data) {
|
|
541
|
+
if (typeof data === "string")
|
|
542
|
+
return new TextEncoder().encode(data);
|
|
543
|
+
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
499
544
|
}
|
|
500
545
|
function fileIdFor(db, siteId, path) {
|
|
501
546
|
const row = db.query("SELECT id FROM site_files WHERE site_id = ? AND path = ? ORDER BY created_hlc LIMIT 1").get(siteId, path);
|
|
@@ -521,36 +566,21 @@ async function putFile(db, siteId, path, opts) {
|
|
|
521
566
|
}
|
|
522
567
|
const existing = fileIdFor(db, siteId, cleanPath);
|
|
523
568
|
const id = existing ?? newId("sf", cleanPath);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
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);
|
|
537
|
-
}
|
|
538
|
-
function deleteFile(db, siteId, path) {
|
|
539
|
-
const id = fileIdFor(db, siteId, normalizeSitePath(path));
|
|
540
|
-
if (!id)
|
|
541
|
-
return false;
|
|
542
|
-
const live = db.query("SELECT __deleted AS d FROM site_files WHERE id = ?").get(id);
|
|
543
|
-
if (!live || live.d)
|
|
544
|
-
return false;
|
|
545
|
-
emit(db, "site_files", id, "__deleted", 1);
|
|
546
|
-
return true;
|
|
569
|
+
withChangeGroup(null, () => {
|
|
570
|
+
if (!existing) {
|
|
571
|
+
const first = emit(db, "site_files", id, "site_id", siteId);
|
|
572
|
+
emit(db, "site_files", id, "path", cleanPath);
|
|
573
|
+
emit(db, "site_files", id, "created_hlc", first.hlc);
|
|
574
|
+
}
|
|
575
|
+
emit(db, "site_files", id, "content_type", contentType);
|
|
576
|
+
emit(db, "site_files", id, "encoding", encoding);
|
|
577
|
+
emit(db, "site_files", id, "content", content);
|
|
578
|
+
emit(db, "site_files", id, "__deleted", 0);
|
|
579
|
+
});
|
|
580
|
+
return db.query(`SELECT ${SITE_FILE_SELECT} FROM site_files WHERE id = ?`).get(id);
|
|
547
581
|
}
|
|
548
582
|
async function getFileForServe(db, siteId, path) {
|
|
549
|
-
const
|
|
550
|
-
const p = normalizeSitePath(withIndex);
|
|
551
|
-
if (!p)
|
|
552
|
-
return null;
|
|
553
|
-
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);
|
|
583
|
+
const row = getFileRow(db, siteId, path);
|
|
554
584
|
if (!row)
|
|
555
585
|
return null;
|
|
556
586
|
const content = row.content ?? "";
|
|
@@ -565,39 +595,59 @@ async function getFileForServe(db, siteId, path) {
|
|
|
565
595
|
return null;
|
|
566
596
|
return { contentType: row.content_type, bytes };
|
|
567
597
|
}
|
|
568
|
-
var INLINE_LIMIT,
|
|
598
|
+
var INLINE_LIMIT, createSite, updateSite, deleteSite, deleteFile;
|
|
569
599
|
var init_sites = __esm(() => {
|
|
570
600
|
init_ids();
|
|
571
601
|
init_crdt();
|
|
572
602
|
init_cache();
|
|
573
603
|
init_errors();
|
|
604
|
+
init_sites_core();
|
|
605
|
+
init_sites_core();
|
|
574
606
|
INLINE_LIMIT = 256 * 1024;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
607
|
+
createSite = grouped(function createSite2(db, opts) {
|
|
608
|
+
const name = normalizeSiteName(opts.name);
|
|
609
|
+
if (getSiteByName(db, name))
|
|
610
|
+
throw new MhError("conflict", `site name already exists: ${name}`);
|
|
611
|
+
const id = newId("site", name);
|
|
612
|
+
const first = emit(db, "sites", id, "name", name);
|
|
613
|
+
emit(db, "sites", id, "created_hlc", first.hlc);
|
|
614
|
+
if (opts.title !== undefined)
|
|
615
|
+
emit(db, "sites", id, "title", opts.title);
|
|
616
|
+
return getSite(db, id);
|
|
617
|
+
});
|
|
618
|
+
updateSite = grouped(function updateSite2(db, id, opts) {
|
|
619
|
+
if (!getSite(db, id))
|
|
620
|
+
throw new MhError("not_found", `no such site: ${id}`);
|
|
621
|
+
if (opts.name !== undefined) {
|
|
622
|
+
const name = normalizeSiteName(opts.name);
|
|
623
|
+
const dup = getSiteByName(db, name);
|
|
624
|
+
if (dup && dup.id !== id)
|
|
625
|
+
throw new MhError("conflict", `site name already exists: ${name}`);
|
|
626
|
+
emit(db, "sites", id, "name", name);
|
|
627
|
+
}
|
|
628
|
+
if (opts.title !== undefined)
|
|
629
|
+
emit(db, "sites", id, "title", opts.title);
|
|
630
|
+
return getSite(db, id);
|
|
631
|
+
});
|
|
632
|
+
deleteSite = grouped(function deleteSite2(db, id) {
|
|
633
|
+
if (!getSite(db, id))
|
|
634
|
+
return false;
|
|
635
|
+
const files = db.query("SELECT id FROM site_files WHERE site_id = ? AND __deleted = 0").all(id);
|
|
636
|
+
for (const f of files)
|
|
637
|
+
emit(db, "site_files", f.id, "__deleted", 1);
|
|
638
|
+
emit(db, "sites", id, "__deleted", 1);
|
|
639
|
+
return true;
|
|
640
|
+
});
|
|
641
|
+
deleteFile = grouped(function deleteFile2(db, siteId, path) {
|
|
642
|
+
const id = fileIdFor(db, siteId, normalizeSitePath(path));
|
|
643
|
+
if (!id)
|
|
644
|
+
return false;
|
|
645
|
+
const live = db.query("SELECT __deleted AS d FROM site_files WHERE id = ?").get(id);
|
|
646
|
+
if (!live || live.d)
|
|
647
|
+
return false;
|
|
648
|
+
emit(db, "site_files", id, "__deleted", 1);
|
|
649
|
+
return true;
|
|
650
|
+
});
|
|
601
651
|
});
|
|
602
652
|
|
|
603
653
|
// src/core/sync/sites-serve.ts
|
|
@@ -647,6 +697,8 @@ CREATE TABLE IF NOT EXISTS meta (
|
|
|
647
697
|
);
|
|
648
698
|
|
|
649
699
|
-- CRDT oplog: one row per field assignment. Source of truth for sync.
|
|
700
|
+
-- txn groups the changes of one logical mutation for history rendering
|
|
701
|
+
-- (nullable: legacy rows and changes from pre-txn peers have none).
|
|
650
702
|
CREATE TABLE IF NOT EXISTS crdt_changes (
|
|
651
703
|
hlc TEXT NOT NULL,
|
|
652
704
|
node_id TEXT NOT NULL,
|
|
@@ -654,9 +706,14 @@ CREATE TABLE IF NOT EXISTS crdt_changes (
|
|
|
654
706
|
row_id TEXT NOT NULL,
|
|
655
707
|
col TEXT NOT NULL,
|
|
656
708
|
value TEXT,
|
|
709
|
+
txn TEXT,
|
|
657
710
|
PRIMARY KEY (dataset, row_id, col, hlc)
|
|
658
711
|
);
|
|
659
712
|
CREATE INDEX IF NOT EXISTS idx_changes_hlc ON crdt_changes(hlc);
|
|
713
|
+
-- Serves history's "every block ever attached to this doc" lookup. Partial so
|
|
714
|
+
-- it never indexes the large values other datasets carry (e.g. site file bodies).
|
|
715
|
+
CREATE INDEX IF NOT EXISTS idx_changes_docref ON crdt_changes(value)
|
|
716
|
+
WHERE dataset = 'doc_blocks' AND col = 'doc_id';
|
|
660
717
|
|
|
661
718
|
-- Per-peer replication cursors (SQLite rowids: monotonic in insertion order,
|
|
662
719
|
-- so no change is ever skipped regardless of HLC/clock skew). Outbound side of
|
|
@@ -796,47 +853,6 @@ init_crdt();
|
|
|
796
853
|
init_ids();
|
|
797
854
|
init_crdt();
|
|
798
855
|
init_errors();
|
|
799
|
-
function createDatabase(db, opts) {
|
|
800
|
-
const id = newId("db", opts.name);
|
|
801
|
-
const first = emit(db, "databases", id, "name", opts.name);
|
|
802
|
-
emit(db, "databases", id, "created_hlc", first.hlc);
|
|
803
|
-
if (opts.icon !== undefined)
|
|
804
|
-
emit(db, "databases", id, "icon", opts.icon);
|
|
805
|
-
return getDatabase(db, id);
|
|
806
|
-
}
|
|
807
|
-
function updateDatabase(db, id, fields) {
|
|
808
|
-
if (!getDatabase(db, id))
|
|
809
|
-
throw new MhError("not_found", `no such database: ${id}`);
|
|
810
|
-
if (fields.name !== undefined)
|
|
811
|
-
emit(db, "databases", id, "name", fields.name);
|
|
812
|
-
if (fields.icon !== undefined)
|
|
813
|
-
emit(db, "databases", id, "icon", fields.icon);
|
|
814
|
-
return getDatabase(db, id);
|
|
815
|
-
}
|
|
816
|
-
function getDatabase(db, id) {
|
|
817
|
-
return db.query("SELECT id, name, icon, created_hlc FROM databases WHERE id = ? AND __deleted = 0").get(id);
|
|
818
|
-
}
|
|
819
|
-
function listDatabases(db) {
|
|
820
|
-
return db.query("SELECT id, name, icon, created_hlc FROM databases WHERE __deleted = 0 ORDER BY created_hlc").all();
|
|
821
|
-
}
|
|
822
|
-
function deleteDatabase(db, id) {
|
|
823
|
-
if (!getDatabase(db, id))
|
|
824
|
-
return false;
|
|
825
|
-
emit(db, "databases", id, "__deleted", 1);
|
|
826
|
-
const tombstone = (table) => {
|
|
827
|
-
for (const r of liveChildren(db, table, "database_id", id))
|
|
828
|
-
emit(db, table, r, "__deleted", 1);
|
|
829
|
-
};
|
|
830
|
-
tombstone("properties");
|
|
831
|
-
tombstone("records");
|
|
832
|
-
for (const d of liveChildren(db, "documents", "database_id", id))
|
|
833
|
-
emit(db, "documents", d, "database_id", null);
|
|
834
|
-
db.query("DELETE FROM meta WHERE key = 'current_db' AND value = ?").run(id);
|
|
835
|
-
return true;
|
|
836
|
-
}
|
|
837
|
-
function liveChildren(db, table, col, parentId) {
|
|
838
|
-
return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r) => r.id);
|
|
839
|
-
}
|
|
840
856
|
|
|
841
857
|
// src/core/properties.ts
|
|
842
858
|
init_ids();
|
|
@@ -884,6 +900,8 @@ var PROP_TYPES = new Set([
|
|
|
884
900
|
"relation",
|
|
885
901
|
"url"
|
|
886
902
|
]);
|
|
903
|
+
var PROPERTY_COLS = ["id", "database_id", "name", "type", "config", "position"];
|
|
904
|
+
var PROPERTY_SELECT = PROPERTY_COLS.join(", ");
|
|
887
905
|
function validateConfig(type, config) {
|
|
888
906
|
if (type === "select" || type === "multi_select") {
|
|
889
907
|
const opts = config?.options;
|
|
@@ -899,7 +917,7 @@ function nextPosition(db, databaseId) {
|
|
|
899
917
|
const row = db.query("SELECT MAX(position) AS m FROM properties WHERE database_id = ? AND __deleted = 0").get(databaseId);
|
|
900
918
|
return (row.m ?? 0) + 1;
|
|
901
919
|
}
|
|
902
|
-
|
|
920
|
+
var addProperty = grouped(function addProperty2(db, databaseId, opts) {
|
|
903
921
|
if (!getDatabase(db, databaseId))
|
|
904
922
|
throw new MhError("not_found", `no such database: ${databaseId}`);
|
|
905
923
|
if (!PROP_TYPES.has(opts.type))
|
|
@@ -915,21 +933,21 @@ function addProperty(db, databaseId, opts) {
|
|
|
915
933
|
if (opts.type === "relation" || opts.indexed === true)
|
|
916
934
|
ensurePropIndex(db, databaseId, id);
|
|
917
935
|
return getProperty(db, id);
|
|
918
|
-
}
|
|
936
|
+
});
|
|
919
937
|
function getProperty(db, id) {
|
|
920
|
-
const row = db.query(
|
|
938
|
+
const row = db.query(`SELECT ${PROPERTY_SELECT} FROM properties WHERE id = ? AND __deleted = 0`).get(id);
|
|
921
939
|
if (!row)
|
|
922
940
|
return null;
|
|
923
941
|
return { ...row, config: row.config ? JSON.parse(row.config) : null };
|
|
924
942
|
}
|
|
925
943
|
function listProperties(db, databaseId) {
|
|
926
|
-
const rows = db.query(
|
|
944
|
+
const rows = db.query(`SELECT ${PROPERTY_SELECT} FROM properties WHERE database_id = ? AND __deleted = 0 ORDER BY position`).all(databaseId);
|
|
927
945
|
return rows.map((r) => ({
|
|
928
946
|
...r,
|
|
929
947
|
config: r.config ? JSON.parse(r.config) : null
|
|
930
948
|
}));
|
|
931
949
|
}
|
|
932
|
-
|
|
950
|
+
var updateProperty = grouped(function updateProperty2(db, id, fields) {
|
|
933
951
|
const cur = getProperty(db, id);
|
|
934
952
|
if (!cur)
|
|
935
953
|
throw new MhError("not_found", `no such property: ${id}`);
|
|
@@ -955,7 +973,7 @@ function updateProperty(db, id, fields) {
|
|
|
955
973
|
emit(db, "records", r.id, id, null);
|
|
956
974
|
}
|
|
957
975
|
return getProperty(db, id);
|
|
958
|
-
}
|
|
976
|
+
});
|
|
959
977
|
function setPropertyWidth(db, id, width) {
|
|
960
978
|
const cur = getProperty(db, id);
|
|
961
979
|
if (!cur)
|
|
@@ -965,7 +983,7 @@ function setPropertyWidth(db, id, width) {
|
|
|
965
983
|
const w = Math.max(80, Math.min(2000, Math.round(width)));
|
|
966
984
|
return updateProperty(db, id, { config: { ...cur.config ?? {}, width: w } });
|
|
967
985
|
}
|
|
968
|
-
|
|
986
|
+
var removeProperty = grouped(function removeProperty2(db, id) {
|
|
969
987
|
const prop = getProperty(db, id);
|
|
970
988
|
if (!prop)
|
|
971
989
|
return false;
|
|
@@ -974,6 +992,76 @@ function removeProperty(db, id) {
|
|
|
974
992
|
for (const r of rows)
|
|
975
993
|
emit(db, "records", r.id, id, undefined);
|
|
976
994
|
return true;
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// src/core/databases.ts
|
|
998
|
+
var DATABASE_COLS = ["id", "name", "icon", "created_hlc"];
|
|
999
|
+
var DATABASE_SELECT = DATABASE_COLS.join(", ");
|
|
1000
|
+
var createDatabase = grouped(function createDatabase2(db, opts) {
|
|
1001
|
+
const id = newId("db", opts.name);
|
|
1002
|
+
const first = emit(db, "databases", id, "name", opts.name);
|
|
1003
|
+
emit(db, "databases", id, "created_hlc", first.hlc);
|
|
1004
|
+
if (opts.icon !== undefined)
|
|
1005
|
+
emit(db, "databases", id, "icon", opts.icon);
|
|
1006
|
+
return getDatabase(db, id);
|
|
1007
|
+
});
|
|
1008
|
+
var updateDatabase = grouped(function updateDatabase2(db, id, fields) {
|
|
1009
|
+
if (!getDatabase(db, id))
|
|
1010
|
+
throw new MhError("not_found", `no such database: ${id}`);
|
|
1011
|
+
if (fields.name !== undefined)
|
|
1012
|
+
emit(db, "databases", id, "name", fields.name);
|
|
1013
|
+
if (fields.icon !== undefined)
|
|
1014
|
+
emit(db, "databases", id, "icon", fields.icon);
|
|
1015
|
+
return getDatabase(db, id);
|
|
1016
|
+
});
|
|
1017
|
+
var duplicateDatabase = grouped(function duplicateDatabase2(db, id, opts = {}) {
|
|
1018
|
+
const src = getDatabase(db, id);
|
|
1019
|
+
if (!src)
|
|
1020
|
+
throw new MhError("not_found", `no such database: ${id}`);
|
|
1021
|
+
const dup = createDatabase(db, {
|
|
1022
|
+
name: opts.name ?? src.name,
|
|
1023
|
+
icon: opts.icon ?? src.icon ?? undefined
|
|
1024
|
+
});
|
|
1025
|
+
const propIdMap = new Map;
|
|
1026
|
+
for (const p of listProperties(db, id)) {
|
|
1027
|
+
const config = p.config?.database === id ? { ...p.config, database: dup.id } : p.config ?? undefined;
|
|
1028
|
+
const np = addProperty(db, dup.id, { name: p.name, type: p.type, config, position: p.position });
|
|
1029
|
+
propIdMap.set(p.id, np.id);
|
|
1030
|
+
}
|
|
1031
|
+
for (const r of listRecords(db, id)) {
|
|
1032
|
+
const data = {};
|
|
1033
|
+
for (const [pid, v] of Object.entries(r.cells)) {
|
|
1034
|
+
const nid = propIdMap.get(pid);
|
|
1035
|
+
if (nid)
|
|
1036
|
+
data[nid] = v;
|
|
1037
|
+
}
|
|
1038
|
+
createRecord(db, dup.id, data);
|
|
1039
|
+
}
|
|
1040
|
+
return getDatabase(db, dup.id);
|
|
1041
|
+
});
|
|
1042
|
+
function getDatabase(db, id) {
|
|
1043
|
+
return db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE id = ? AND __deleted = 0`).get(id);
|
|
1044
|
+
}
|
|
1045
|
+
function listDatabases(db) {
|
|
1046
|
+
return db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE __deleted = 0 ORDER BY created_hlc`).all();
|
|
1047
|
+
}
|
|
1048
|
+
var deleteDatabase = grouped(function deleteDatabase2(db, id) {
|
|
1049
|
+
if (!getDatabase(db, id))
|
|
1050
|
+
return false;
|
|
1051
|
+
emit(db, "databases", id, "__deleted", 1);
|
|
1052
|
+
const tombstone = (table) => {
|
|
1053
|
+
for (const r of liveChildren(db, table, "database_id", id))
|
|
1054
|
+
emit(db, table, r, "__deleted", 1);
|
|
1055
|
+
};
|
|
1056
|
+
tombstone("properties");
|
|
1057
|
+
tombstone("records");
|
|
1058
|
+
for (const d of liveChildren(db, "documents", "database_id", id))
|
|
1059
|
+
emit(db, "documents", d, "database_id", null);
|
|
1060
|
+
db.query("DELETE FROM meta WHERE key = 'current_db' AND value = ?").run(id);
|
|
1061
|
+
return true;
|
|
1062
|
+
});
|
|
1063
|
+
function liveChildren(db, table, col, parentId) {
|
|
1064
|
+
return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r) => r.id);
|
|
977
1065
|
}
|
|
978
1066
|
|
|
979
1067
|
// src/core/resolve.ts
|
|
@@ -1191,12 +1279,30 @@ function coerce(db, prop, value) {
|
|
|
1191
1279
|
}
|
|
1192
1280
|
}
|
|
1193
1281
|
}
|
|
1282
|
+
function propsByName(props) {
|
|
1283
|
+
const byName = new Map;
|
|
1284
|
+
for (const p of props) {
|
|
1285
|
+
const k = p.name.toLowerCase();
|
|
1286
|
+
const list = byName.get(k);
|
|
1287
|
+
if (list)
|
|
1288
|
+
list.push(p);
|
|
1289
|
+
else
|
|
1290
|
+
byName.set(k, [p]);
|
|
1291
|
+
}
|
|
1292
|
+
return byName;
|
|
1293
|
+
}
|
|
1194
1294
|
function resolveData(props, data) {
|
|
1195
1295
|
const byId = new Map(props.map((p) => [p.id, p]));
|
|
1196
|
-
const byName =
|
|
1296
|
+
const byName = propsByName(props);
|
|
1197
1297
|
const out = [];
|
|
1198
1298
|
for (const [key, value] of Object.entries(data)) {
|
|
1199
|
-
|
|
1299
|
+
let prop = byId.get(key);
|
|
1300
|
+
if (!prop) {
|
|
1301
|
+
const matches = byName.get(key.toLowerCase()) ?? [];
|
|
1302
|
+
if (matches.length > 1)
|
|
1303
|
+
throw new MhError("ambiguous", `property name "${key}" matches ${matches.length} properties; use a property id`);
|
|
1304
|
+
prop = matches[0];
|
|
1305
|
+
}
|
|
1200
1306
|
if (!prop)
|
|
1201
1307
|
throw new MhError("not_found", `unknown property: ${key}`);
|
|
1202
1308
|
out.push({ prop, value });
|
|
@@ -1207,12 +1313,15 @@ function rowToRecord(row, props) {
|
|
|
1207
1313
|
const raw = JSON.parse(row.data || "{}");
|
|
1208
1314
|
const byId = new Map(props.map((p) => [p.id, p]));
|
|
1209
1315
|
const values = {};
|
|
1316
|
+
const cells = {};
|
|
1210
1317
|
for (const [propId, v] of Object.entries(raw)) {
|
|
1211
1318
|
const p = byId.get(propId);
|
|
1212
|
-
if (p)
|
|
1213
|
-
|
|
1319
|
+
if (!p)
|
|
1320
|
+
continue;
|
|
1321
|
+
values[p.name] = v;
|
|
1322
|
+
cells[p.id] = v;
|
|
1214
1323
|
}
|
|
1215
|
-
return { id: row.id, database_id: row.database_id, values };
|
|
1324
|
+
return { id: row.id, database_id: row.database_id, values, cells };
|
|
1216
1325
|
}
|
|
1217
1326
|
function deriveTitle(props, resolved, fallbackBase) {
|
|
1218
1327
|
const text = resolved.find((r) => r.prop.type === "text" && typeof r.value === "string" && r.value);
|
|
@@ -1249,7 +1358,7 @@ function rebalanceRecordOrderKeys(db, databaseId) {
|
|
|
1249
1358
|
emit(db, "records", row.id, "order_key", keys[i]);
|
|
1250
1359
|
});
|
|
1251
1360
|
}
|
|
1252
|
-
|
|
1361
|
+
var backfillRecordOrderKeys = grouped(function backfillRecordOrderKeys2(db, databaseId) {
|
|
1253
1362
|
const emitChange = canEmitOrderKeys(db);
|
|
1254
1363
|
const dbRows = databaseId ? [{ database_id: databaseId }] : db.query(`SELECT DISTINCT database_id FROM records
|
|
1255
1364
|
WHERE database_id IS NOT NULL AND __deleted = 0 AND order_key IS NULL`).all();
|
|
@@ -1263,8 +1372,8 @@ function backfillRecordOrderKeys(db, databaseId) {
|
|
|
1263
1372
|
const keys = keysBetween(start, null, missing.length);
|
|
1264
1373
|
missing.forEach((row, i) => writeRecordOrderKey(db, row.id, keys[i], emitChange));
|
|
1265
1374
|
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1375
|
+
});
|
|
1376
|
+
var createRecord = grouped(function createRecord2(db, databaseId, data) {
|
|
1268
1377
|
const dbRow = getDatabase(db, databaseId);
|
|
1269
1378
|
if (!dbRow)
|
|
1270
1379
|
throw new MhError("not_found", `no such database: ${databaseId}`);
|
|
@@ -1278,7 +1387,7 @@ function createRecord(db, databaseId, data) {
|
|
|
1278
1387
|
for (const { prop, value } of resolved)
|
|
1279
1388
|
emit(db, "records", id, prop.id, coerce(db, prop, value));
|
|
1280
1389
|
return getRecord(db, id);
|
|
1281
|
-
}
|
|
1390
|
+
});
|
|
1282
1391
|
function getRecord(db, id) {
|
|
1283
1392
|
const row = db.query("SELECT id, database_id, data FROM records WHERE id = ? AND __deleted = 0").get(id);
|
|
1284
1393
|
if (!row)
|
|
@@ -1305,7 +1414,13 @@ function listRecords(db, databaseId, opts = {}) {
|
|
|
1305
1414
|
key = key.slice(1);
|
|
1306
1415
|
}
|
|
1307
1416
|
if (key !== "created" && key !== "created_hlc") {
|
|
1308
|
-
|
|
1417
|
+
let p = props.find((p2) => p2.id === key);
|
|
1418
|
+
if (!p) {
|
|
1419
|
+
const matches = props.filter((q) => q.name.toLowerCase() === key.toLowerCase());
|
|
1420
|
+
if (matches.length > 1)
|
|
1421
|
+
throw new MhError("ambiguous", `sort field "${key}" matches ${matches.length} properties; use a property id`);
|
|
1422
|
+
p = matches[0];
|
|
1423
|
+
}
|
|
1309
1424
|
if (!p)
|
|
1310
1425
|
throw new MhError("not_found", `unknown sort field: ${key}`);
|
|
1311
1426
|
sortProp = p;
|
|
@@ -1331,13 +1446,13 @@ function listRecords(db, databaseId, opts = {}) {
|
|
|
1331
1446
|
}
|
|
1332
1447
|
let rows = db.query(sql).all(...args).map((r) => rowToRecord(r, props));
|
|
1333
1448
|
if (jsFilters.length) {
|
|
1334
|
-
rows = rows.filter((r) => jsFilters.every((f) => JSON.stringify(r.
|
|
1449
|
+
rows = rows.filter((r) => jsFilters.every((f) => JSON.stringify(r.cells[f.prop.id] ?? null) === JSON.stringify(f.value)));
|
|
1335
1450
|
if (opts.limit != null)
|
|
1336
1451
|
rows = rows.slice(0, opts.limit);
|
|
1337
1452
|
}
|
|
1338
1453
|
return rows;
|
|
1339
1454
|
}
|
|
1340
|
-
|
|
1455
|
+
var moveRecord = grouped(function moveRecord2(db, id, targetId, where) {
|
|
1341
1456
|
if (where !== "before" && where !== "after")
|
|
1342
1457
|
throw new MhError("invalid_input", `unknown move position: ${where}`);
|
|
1343
1458
|
const src = db.query("SELECT id, database_id FROM records WHERE id = ? AND __deleted = 0").get(id);
|
|
@@ -1366,8 +1481,8 @@ function moveRecord(db, id, targetId, where) {
|
|
|
1366
1481
|
}
|
|
1367
1482
|
emit(db, "records", id, "order_key", keyBetween(left, right));
|
|
1368
1483
|
return getRecord(db, id);
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1484
|
+
});
|
|
1485
|
+
var updateRecord = grouped(function updateRecord2(db, id, data) {
|
|
1371
1486
|
const rec = db.query("SELECT id, database_id FROM records WHERE id = ? AND __deleted = 0").get(id);
|
|
1372
1487
|
if (!rec)
|
|
1373
1488
|
throw new MhError("not_found", `no such record: ${id}`);
|
|
@@ -1375,19 +1490,38 @@ function updateRecord(db, id, data) {
|
|
|
1375
1490
|
for (const { prop, value } of resolved)
|
|
1376
1491
|
emit(db, "records", id, prop.id, coerce(db, prop, value));
|
|
1377
1492
|
return getRecord(db, id);
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1493
|
+
});
|
|
1494
|
+
var deleteRecord = grouped(function deleteRecord2(db, id) {
|
|
1380
1495
|
const rec = db.query("SELECT id FROM records WHERE id = ? AND __deleted = 0").get(id);
|
|
1381
1496
|
if (!rec)
|
|
1382
1497
|
return false;
|
|
1383
1498
|
emit(db, "records", id, "__deleted", 1);
|
|
1384
1499
|
return true;
|
|
1385
|
-
}
|
|
1500
|
+
});
|
|
1386
1501
|
|
|
1387
1502
|
// src/core/documents.ts
|
|
1388
1503
|
init_ids();
|
|
1389
1504
|
init_crdt();
|
|
1390
1505
|
init_errors();
|
|
1506
|
+
var DOCUMENT_COLS = [
|
|
1507
|
+
"id",
|
|
1508
|
+
"title",
|
|
1509
|
+
"body",
|
|
1510
|
+
"database_id",
|
|
1511
|
+
"parent_id",
|
|
1512
|
+
"created_hlc",
|
|
1513
|
+
"order_key"
|
|
1514
|
+
];
|
|
1515
|
+
var DOCUMENT_SELECT = DOCUMENT_COLS.join(", ");
|
|
1516
|
+
var DOC_SUMMARY_COLS = [
|
|
1517
|
+
"id",
|
|
1518
|
+
"title",
|
|
1519
|
+
"database_id",
|
|
1520
|
+
"parent_id",
|
|
1521
|
+
"created_hlc",
|
|
1522
|
+
"order_key"
|
|
1523
|
+
];
|
|
1524
|
+
var DOC_SUMMARY_SELECT = DOC_SUMMARY_COLS.join(", ");
|
|
1391
1525
|
function liveBlocks(db, docId) {
|
|
1392
1526
|
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);
|
|
1393
1527
|
}
|
|
@@ -1448,7 +1582,7 @@ function documentVersion(db, id) {
|
|
|
1448
1582
|
OR (dataset = 'doc_blocks' AND row_id IN (SELECT id FROM doc_blocks WHERE doc_id = ?))`).get(id, id);
|
|
1449
1583
|
return row.h ?? "";
|
|
1450
1584
|
}
|
|
1451
|
-
|
|
1585
|
+
var createDocument = grouped(function createDocument2(db, opts) {
|
|
1452
1586
|
const id = newId("doc", opts.title);
|
|
1453
1587
|
const first = emit(db, "documents", id, "title", opts.title);
|
|
1454
1588
|
emit(db, "documents", id, "created_hlc", first.hlc);
|
|
@@ -1463,13 +1597,13 @@ function createDocument(db, opts) {
|
|
|
1463
1597
|
insertBlocks(db, id, blocks, null, null);
|
|
1464
1598
|
}
|
|
1465
1599
|
return getDocument(db, id);
|
|
1466
|
-
}
|
|
1600
|
+
});
|
|
1467
1601
|
function getDocument(db, id) {
|
|
1468
|
-
return db.query(
|
|
1602
|
+
return db.query(`SELECT ${DOCUMENT_SELECT} FROM documents WHERE id = ? AND __deleted = 0`).get(id);
|
|
1469
1603
|
}
|
|
1470
1604
|
var ORDER_BY = "ORDER BY order_key IS NULL, order_key, created_hlc, id";
|
|
1471
1605
|
function listDocuments(db, opts = {}) {
|
|
1472
|
-
const cols =
|
|
1606
|
+
const cols = `SELECT ${DOC_SUMMARY_SELECT} FROM documents WHERE __deleted = 0`;
|
|
1473
1607
|
if (opts.parent_id !== undefined)
|
|
1474
1608
|
return db.query(`${cols} AND parent_id = ? ${ORDER_BY}`).all(opts.parent_id);
|
|
1475
1609
|
if (opts.database_id)
|
|
@@ -1520,11 +1654,11 @@ function backfillSiblings(db, parentId) {
|
|
|
1520
1654
|
db.query("UPDATE documents SET order_key = ? WHERE id = ?").run(keys[i], row.id);
|
|
1521
1655
|
});
|
|
1522
1656
|
}
|
|
1523
|
-
|
|
1657
|
+
var backfillDocumentOrderKeys = grouped(function backfillDocumentOrderKeys2(db) {
|
|
1524
1658
|
const parents = db.query("SELECT DISTINCT parent_id FROM documents WHERE __deleted = 0 AND order_key IS NULL").all();
|
|
1525
1659
|
for (const p of parents)
|
|
1526
1660
|
backfillSiblings(db, p.parent_id);
|
|
1527
|
-
}
|
|
1661
|
+
});
|
|
1528
1662
|
function placeInSiblings(db, id, parentId, anchor) {
|
|
1529
1663
|
let cur = parentId;
|
|
1530
1664
|
while (cur) {
|
|
@@ -1556,7 +1690,7 @@ function placeInSiblings(db, id, parentId, anchor) {
|
|
|
1556
1690
|
}
|
|
1557
1691
|
emit(db, "documents", id, "order_key", key);
|
|
1558
1692
|
}
|
|
1559
|
-
|
|
1693
|
+
var moveDocument = grouped(function moveDocument2(db, id, targetId, where) {
|
|
1560
1694
|
const src = getDocument(db, id);
|
|
1561
1695
|
if (!src)
|
|
1562
1696
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
@@ -1570,8 +1704,26 @@ function moveDocument(db, id, targetId, where) {
|
|
|
1570
1704
|
else
|
|
1571
1705
|
placeInSiblings(db, id, target.parent_id, { targetId, where });
|
|
1572
1706
|
return getDocument(db, id);
|
|
1573
|
-
}
|
|
1574
|
-
function
|
|
1707
|
+
});
|
|
1708
|
+
var duplicateDocument = grouped(function duplicateDocument2(db, id, opts = {}) {
|
|
1709
|
+
const src = getDocument(db, id);
|
|
1710
|
+
if (!src)
|
|
1711
|
+
throw new MhError("not_found", `no such document: ${id}`);
|
|
1712
|
+
ensureBlocks(db, id);
|
|
1713
|
+
const blocks = liveBlocks(db, id);
|
|
1714
|
+
const parentId = opts.parentId !== undefined ? opts.parentId : src.parent_id;
|
|
1715
|
+
const dup = createDocument(db, {
|
|
1716
|
+
title: opts.title ?? src.title,
|
|
1717
|
+
database_id: src.database_id ?? undefined,
|
|
1718
|
+
parent_id: parentId ?? undefined
|
|
1719
|
+
});
|
|
1720
|
+
if (blocks.length)
|
|
1721
|
+
insertBlocks(db, dup.id, blocks.map((b) => ({ text: b.text, blankAfter: b.blank_after })), null, null);
|
|
1722
|
+
if (parentId === src.parent_id)
|
|
1723
|
+
moveDocument(db, dup.id, src.id, "after");
|
|
1724
|
+
return getDocument(db, dup.id);
|
|
1725
|
+
});
|
|
1726
|
+
var updateDocument = grouped(function updateDocument2(db, id, fields, opts = {}) {
|
|
1575
1727
|
if (!getDocument(db, id))
|
|
1576
1728
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
1577
1729
|
if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
|
|
@@ -1590,8 +1742,8 @@ function updateDocument(db, id, fields, opts = {}) {
|
|
|
1590
1742
|
reconcileBody(db, id, fields.body);
|
|
1591
1743
|
}
|
|
1592
1744
|
return getDocument(db, id);
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1745
|
+
});
|
|
1746
|
+
var editDocument = grouped(function editDocument2(db, id, opts) {
|
|
1595
1747
|
if (!getDocument(db, id))
|
|
1596
1748
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
1597
1749
|
if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
|
|
@@ -1622,8 +1774,8 @@ function editDocument(db, id, opts) {
|
|
|
1622
1774
|
const newBody = opts.replaceAll ? body.replaceAll(opts.old, opts.new) : body.replace(opts.old, opts.new);
|
|
1623
1775
|
reconcileBody(db, id, newBody);
|
|
1624
1776
|
return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1777
|
+
});
|
|
1778
|
+
var appendDocument = grouped(function appendDocument2(db, id, body) {
|
|
1627
1779
|
if (!getDocument(db, id))
|
|
1628
1780
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
1629
1781
|
ensureBlocks(db, id);
|
|
@@ -1634,8 +1786,8 @@ function appendDocument(db, id, body) {
|
|
|
1634
1786
|
insertBlocks(db, id, next, last, null);
|
|
1635
1787
|
}
|
|
1636
1788
|
return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1789
|
+
});
|
|
1790
|
+
var prependDocument = grouped(function prependDocument2(db, id, body) {
|
|
1639
1791
|
if (!getDocument(db, id))
|
|
1640
1792
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
1641
1793
|
ensureBlocks(db, id);
|
|
@@ -1646,8 +1798,8 @@ function prependDocument(db, id, body) {
|
|
|
1646
1798
|
insertBlocks(db, id, next, null, first);
|
|
1647
1799
|
}
|
|
1648
1800
|
return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1801
|
+
});
|
|
1802
|
+
var deleteDocument = grouped(function deleteDocument2(db, id) {
|
|
1651
1803
|
if (!getDocument(db, id))
|
|
1652
1804
|
return false;
|
|
1653
1805
|
emit(db, "documents", id, "__deleted", 1);
|
|
@@ -1656,7 +1808,7 @@ function deleteDocument(db, id) {
|
|
|
1656
1808
|
for (const c of liveChildIds(db, "documents", "parent_id", id))
|
|
1657
1809
|
emit(db, "documents", c, "parent_id", null);
|
|
1658
1810
|
return true;
|
|
1659
|
-
}
|
|
1811
|
+
});
|
|
1660
1812
|
function liveChildIds(db, table, col, parentId) {
|
|
1661
1813
|
return db.query(`SELECT id FROM ${table} WHERE ${col} = ? AND __deleted = 0`).all(parentId).map((r) => r.id);
|
|
1662
1814
|
}
|
|
@@ -1672,11 +1824,7 @@ function countOccurrences(haystack, needle) {
|
|
|
1672
1824
|
return n;
|
|
1673
1825
|
}
|
|
1674
1826
|
|
|
1675
|
-
// src/core/
|
|
1676
|
-
function ensureDirs() {
|
|
1677
|
-
mkdirSync(metahubHome(), { recursive: true });
|
|
1678
|
-
mkdirSync(cacheDir(), { recursive: true });
|
|
1679
|
-
}
|
|
1827
|
+
// src/core/schema-init.ts
|
|
1680
1828
|
function runSchema(db) {
|
|
1681
1829
|
db.exec(CORE_SCHEMA);
|
|
1682
1830
|
try {
|
|
@@ -1739,15 +1887,29 @@ function migrateDocuments(db) {
|
|
|
1739
1887
|
db.exec("ALTER TABLE documents ADD COLUMN order_key TEXT");
|
|
1740
1888
|
backfillDocumentOrderKeys(db);
|
|
1741
1889
|
}
|
|
1742
|
-
function
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1890
|
+
function migrateOplog(db) {
|
|
1891
|
+
if (!hasColumn(db, "crdt_changes", "txn"))
|
|
1892
|
+
db.exec("ALTER TABLE crdt_changes ADD COLUMN txn TEXT");
|
|
1893
|
+
}
|
|
1894
|
+
function initSchema(db) {
|
|
1746
1895
|
runSchema(db);
|
|
1896
|
+
migrateOplog(db);
|
|
1747
1897
|
migrateRecords(db);
|
|
1748
1898
|
migratePeers(db);
|
|
1749
1899
|
migrateDocBlocks(db);
|
|
1750
1900
|
migrateDocuments(db);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// src/core/db.ts
|
|
1904
|
+
function ensureDirs() {
|
|
1905
|
+
mkdirSync(metahubHome(), { recursive: true });
|
|
1906
|
+
mkdirSync(cacheDir(), { recursive: true });
|
|
1907
|
+
}
|
|
1908
|
+
function openMetahub() {
|
|
1909
|
+
ensureDirs();
|
|
1910
|
+
const db = new Database(dbPath(), { create: true });
|
|
1911
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
1912
|
+
initSchema(db);
|
|
1751
1913
|
return db;
|
|
1752
1914
|
}
|
|
1753
1915
|
|
|
@@ -16231,12 +16393,15 @@ var ChangeSchema = exports_external.object({
|
|
|
16231
16393
|
dataset: exports_external.string().describe("CRDT dataset, e.g. databases / records / documents"),
|
|
16232
16394
|
row_id: exports_external.string().describe("Row identifier within the dataset"),
|
|
16233
16395
|
col: exports_external.string().describe("Column / property identifier"),
|
|
16234
|
-
value: exports_external.string().nullable().describe("JSON-encoded field value, or null")
|
|
16396
|
+
value: exports_external.string().nullable().describe("JSON-encoded field value, or null"),
|
|
16397
|
+
txn: exports_external.string().nullable().optional().describe("Change-group id (one logical mutation)")
|
|
16235
16398
|
});
|
|
16236
16399
|
var SyncRequestSchema = exports_external.object({
|
|
16237
16400
|
node_id: exports_external.string().describe("Calling client's node id"),
|
|
16238
16401
|
since: exports_external.number().describe("Server cursor the client last pulled"),
|
|
16239
|
-
changes: exports_external.array(ChangeSchema).describe("Client's changes to push")
|
|
16402
|
+
changes: exports_external.array(ChangeSchema).describe("Client's changes to push"),
|
|
16403
|
+
limit: exports_external.number().optional().describe("Max changes to return (pagination for hydration)"),
|
|
16404
|
+
exclude_datasets: exports_external.array(exports_external.string()).optional().describe("Datasets to omit from the pull (partial replica)")
|
|
16240
16405
|
});
|
|
16241
16406
|
var SyncResponseSchema = exports_external.object({
|
|
16242
16407
|
node_id: exports_external.string().describe("Server's node id"),
|
|
@@ -16272,10 +16437,11 @@ function getPeer(db, url2) {
|
|
|
16272
16437
|
function setPeer(db, url2, c) {
|
|
16273
16438
|
db.query("INSERT INTO peers (url, pull_cursor, push_cursor) VALUES (?, ?, ?) ON CONFLICT(url) DO UPDATE SET pull_cursor = excluded.pull_cursor, push_cursor = excluded.push_cursor").run(url2, c.pull_cursor, c.push_cursor);
|
|
16274
16439
|
}
|
|
16275
|
-
async function syncWithPeer(db, url2,
|
|
16440
|
+
async function syncWithPeer(db, url2, opts = {}) {
|
|
16441
|
+
const o = typeof opts === "string" ? { token: opts } : opts;
|
|
16276
16442
|
const node = getNodeId(db);
|
|
16277
16443
|
const peer = getPeer(db, url2);
|
|
16278
|
-
const bearer = token ?? peer.token ?? undefined;
|
|
16444
|
+
const bearer = o.token ?? peer.token ?? undefined;
|
|
16279
16445
|
const toPush = changesAfterSeq(db, peer.push_cursor);
|
|
16280
16446
|
const headers = { "content-type": "application/json" };
|
|
16281
16447
|
if (bearer)
|
|
@@ -16283,7 +16449,13 @@ async function syncWithPeer(db, url2, token) {
|
|
|
16283
16449
|
const res = await fetch(new URL(SYNC_PATH, url2), {
|
|
16284
16450
|
method: "POST",
|
|
16285
16451
|
headers,
|
|
16286
|
-
body: JSON.stringify({
|
|
16452
|
+
body: JSON.stringify({
|
|
16453
|
+
node_id: node,
|
|
16454
|
+
since: peer.pull_cursor,
|
|
16455
|
+
changes: toPush.changes,
|
|
16456
|
+
...o.pullLimit != null ? { limit: o.pullLimit } : {},
|
|
16457
|
+
...o.excludeDatasets?.length ? { exclude_datasets: o.excludeDatasets } : {}
|
|
16458
|
+
})
|
|
16287
16459
|
});
|
|
16288
16460
|
if (!res.ok)
|
|
16289
16461
|
throw new MhError(res.status === 401 || res.status === 403 ? "auth" : "network", `sync failed: ${res.status} ${await res.text()}`);
|
|
@@ -16397,7 +16569,7 @@ function getServerConfig(db) {
|
|
|
16397
16569
|
// package.json
|
|
16398
16570
|
var package_default = {
|
|
16399
16571
|
name: "@tensorix/metahub",
|
|
16400
|
-
version: "0.1.
|
|
16572
|
+
version: "0.1.11",
|
|
16401
16573
|
description: "Metahub core library and CLI",
|
|
16402
16574
|
license: "AGPL-3.0-only",
|
|
16403
16575
|
type: "module",
|
|
@@ -16431,6 +16603,7 @@ var package_default = {
|
|
|
16431
16603
|
},
|
|
16432
16604
|
dependencies: {
|
|
16433
16605
|
"@clack/prompts": "^1.5.0",
|
|
16606
|
+
"@sqlite.org/sqlite-wasm": "^3.53.0-build1",
|
|
16434
16607
|
citty: "^0.1.6",
|
|
16435
16608
|
"highlight.js": "^11.11.1",
|
|
16436
16609
|
preact: "^10.29.2",
|
|
@@ -16439,6 +16612,7 @@ var package_default = {
|
|
|
16439
16612
|
zod: "^4.4.3"
|
|
16440
16613
|
},
|
|
16441
16614
|
devDependencies: {
|
|
16615
|
+
"@happy-dom/global-registrator": "^20.10.2",
|
|
16442
16616
|
"@types/bun": "latest",
|
|
16443
16617
|
"@types/turndown": "^5.0.6",
|
|
16444
16618
|
typescript: "^5"
|
|
@@ -16830,7 +17004,10 @@ var syncRoutes = [
|
|
|
16830
17004
|
async handler(req, { db, node }) {
|
|
16831
17005
|
const body = await req.json();
|
|
16832
17006
|
ingest(db, body.changes ?? []);
|
|
16833
|
-
const batch = changesAfterSeq(db, body.since ?? 0
|
|
17007
|
+
const batch = changesAfterSeq(db, body.since ?? 0, {
|
|
17008
|
+
limit: body.limit,
|
|
17009
|
+
excludeDatasets: body.exclude_datasets
|
|
17010
|
+
});
|
|
16834
17011
|
return Response.json({ node_id: node, changes: batch.changes, cursor: batch.cursor });
|
|
16835
17012
|
}
|
|
16836
17013
|
},
|
|
@@ -16995,50 +17172,14 @@ function unlockPage() {
|
|
|
16995
17172
|
</script>
|
|
16996
17173
|
</body></html>`;
|
|
16997
17174
|
}
|
|
16998
|
-
var
|
|
16999
|
-
var KEY = "mh_token";
|
|
17000
|
-
function save(t){
|
|
17001
|
-
try { localStorage.setItem(KEY, t); } catch (e) {}
|
|
17002
|
-
document.cookie = KEY + "=" + encodeURIComponent(t) + "; path=/; SameSite=Strict; Max-Age=31536000";
|
|
17003
|
-
}
|
|
17004
|
-
var orig = window.fetch.bind(window);
|
|
17005
|
-
window.fetch = function(input, init){
|
|
17006
|
-
init = init || {};
|
|
17007
|
-
var t = null; try { t = localStorage.getItem(KEY); } catch (e) {}
|
|
17008
|
-
var url;
|
|
17009
|
-
try { url = new URL((typeof input === "string" ? input : input.url), location.href); } catch (e) { url = null; }
|
|
17010
|
-
var same = url && url.origin === location.origin;
|
|
17011
|
-
if (same && t) {
|
|
17012
|
-
var h = new Headers(init.headers || (typeof input !== "string" && input.headers) || {});
|
|
17013
|
-
if (!h.has("authorization")) h.set("authorization", "Bearer " + t);
|
|
17014
|
-
init.headers = h;
|
|
17015
|
-
}
|
|
17016
|
-
return orig(input, init).then(function(res){
|
|
17017
|
-
if (res.status !== 401 || !same || !t || init.__mhRetried) return res;
|
|
17018
|
-
return orig("${RENEW_PATH}", { headers: { authorization: "Bearer " + t } }).then(function(r){
|
|
17019
|
-
if (!r.ok) return res;
|
|
17020
|
-
return r.json().then(function(d){
|
|
17021
|
-
if (!d || !d.token) return res;
|
|
17022
|
-
save(d.token);
|
|
17023
|
-
var h2 = new Headers(init.headers || {});
|
|
17024
|
-
h2.set("authorization", "Bearer " + d.token);
|
|
17025
|
-
init.headers = h2;
|
|
17026
|
-
init.__mhRetried = true;
|
|
17027
|
-
return orig(input, init);
|
|
17028
|
-
});
|
|
17029
|
-
}).catch(function(){ return res; });
|
|
17030
|
-
});
|
|
17031
|
-
};
|
|
17032
|
-
})();</script>`;
|
|
17175
|
+
var RUNTIME_TAG = `<script src="/mh-runtime.js"></script>`;
|
|
17033
17176
|
function injectShim(html) {
|
|
17034
17177
|
const i = html.toLowerCase().indexOf("<head>");
|
|
17035
17178
|
if (i >= 0)
|
|
17036
|
-
return html.slice(0, i + 6) +
|
|
17037
|
-
return
|
|
17179
|
+
return html.slice(0, i + 6) + RUNTIME_TAG + html.slice(i + 6);
|
|
17180
|
+
return RUNTIME_TAG + html;
|
|
17038
17181
|
}
|
|
17039
|
-
async function withShim(res,
|
|
17040
|
-
if (!authActive(cfg))
|
|
17041
|
-
return res;
|
|
17182
|
+
async function withShim(res, _cfg) {
|
|
17042
17183
|
const ct = res.headers.get("content-type") ?? "";
|
|
17043
17184
|
if (!ct.includes("text/html"))
|
|
17044
17185
|
return res;
|
|
@@ -17091,15 +17232,16 @@ function startServer(opts = {}) {
|
|
|
17091
17232
|
server = Bun.serve({
|
|
17092
17233
|
port,
|
|
17093
17234
|
hostname: host,
|
|
17235
|
+
tls: opts.tls ? { cert: Bun.file(opts.tls.certPath), key: Bun.file(opts.tls.keyPath) } : undefined,
|
|
17094
17236
|
async fetch(req) {
|
|
17095
17237
|
const url2 = new URL(req.url);
|
|
17096
17238
|
if (url2.pathname === SYNC_PATH) {
|
|
17097
17239
|
if (!acceptsSyncToken(req, url2, auth, db))
|
|
17098
17240
|
return unauthorized();
|
|
17099
17241
|
} else {
|
|
17100
|
-
const exempt = url2.pathname === HEALTH_PATH || url2.pathname === RENEW_PATH || url2.pathname === PAIR_PATH;
|
|
17242
|
+
const exempt = url2.pathname === HEALTH_PATH || url2.pathname === RENEW_PATH || url2.pathname === PAIR_PATH || url2.pathname === "/manifest.webmanifest" || url2.pathname === "/sw.js" || url2.pathname.startsWith("/icons/");
|
|
17101
17243
|
if (!exempt && !hasValidToken(req, url2, auth)) {
|
|
17102
|
-
return wantsHtml(req) ? new Response(unlockPage(), { headers: HTML_HEADERS }) : unauthorized();
|
|
17244
|
+
return wantsHtml(req) ? new Response(unlockPage(), { headers: { ...HTML_HEADERS, "x-mh-unlock": "1" } }) : unauthorized();
|
|
17103
17245
|
}
|
|
17104
17246
|
}
|
|
17105
17247
|
if (url2.pathname === RENEW_PATH) {
|
|
@@ -17156,6 +17298,7 @@ function startServer(opts = {}) {
|
|
|
17156
17298
|
token: active?.token ?? null,
|
|
17157
17299
|
exp: active?.exp ?? null,
|
|
17158
17300
|
authMode,
|
|
17301
|
+
tls: Boolean(opts.tls),
|
|
17159
17302
|
autoSync: Boolean(autoSync && syncIntervalMs > 0),
|
|
17160
17303
|
syncIntervalMs,
|
|
17161
17304
|
stop() {
|
|
@@ -17168,7 +17311,7 @@ function startServer(opts = {}) {
|
|
|
17168
17311
|
// src/webui/server/assets.ts
|
|
17169
17312
|
import { fileURLToPath } from "url";
|
|
17170
17313
|
import { readdirSync, statSync } from "fs";
|
|
17171
|
-
import { join as join3 } from "path";
|
|
17314
|
+
import { join as join3, dirname } from "path";
|
|
17172
17315
|
|
|
17173
17316
|
// src/webui/styles.css
|
|
17174
17317
|
var styles_default = ` :root {
|
|
@@ -17253,7 +17396,26 @@ var styles_default = ` :root {
|
|
|
17253
17396
|
/* sidebar */
|
|
17254
17397
|
.sidebar { width:268px; flex:none; background:var(--sidebar); border-right:1px solid var(--line);
|
|
17255
17398
|
display:flex; flex-direction:column; position:relative; transition:margin-left .22s cubic-bezier(.4,0,.2,1); }
|
|
17256
|
-
|
|
17399
|
+
/* right:-4px (not -3): the 7px handle then straddles the 1px border line
|
|
17400
|
+
symmetrically (3.5px each side of the border's center), so the centered
|
|
17401
|
+
::before grip line lands exactly on the border instead of 1px left of it. */
|
|
17402
|
+
.sb-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:5; }
|
|
17403
|
+
/* Unified resize-handle visual (sidebar + datasheet/doc-table column resizers):
|
|
17404
|
+
the handle stays an invisible hit area; a centered 3px round-capped line
|
|
17405
|
+
fades/expands in on hover and solidifies with a soft halo while dragging.
|
|
17406
|
+
--rz-guide-h (written by startColumnResize) stretches the dragging line to
|
|
17407
|
+
the full table height so the new column edge is visible across all rows. */
|
|
17408
|
+
.sb-resizer::before, .col-resizer::before, .doc-col-resizer::before {
|
|
17409
|
+
content:""; position:absolute; left:50%; top:0; height:100%; width:3px;
|
|
17410
|
+
transform:translateX(-50%) scaleX(.4); border-radius:999px;
|
|
17411
|
+
background:var(--accent); opacity:0; pointer-events:none;
|
|
17412
|
+
transition:opacity .15s cubic-bezier(.4,0,.2,1), transform .15s cubic-bezier(.4,0,.2,1), box-shadow .15s; }
|
|
17413
|
+
.sb-resizer:hover::before, .col-resizer:hover::before, .doc-col-resizer:hover::before {
|
|
17414
|
+
opacity:.6; transform:translateX(-50%) scaleX(1); }
|
|
17415
|
+
.sb-resizer.dragging::before, .col-resizer.dragging::before, .doc-col-resizer.dragging::before {
|
|
17416
|
+
opacity:1; transform:translateX(-50%) scaleX(1);
|
|
17417
|
+
box-shadow:0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
|
|
17418
|
+
height:var(--rz-guide-h, 100%); }
|
|
17257
17419
|
.sb-head { display:flex; align-items:center; gap:8px; padding:14px 14px 10px; }
|
|
17258
17420
|
.brand { display:flex; align-items:center; gap:8px; flex:1; font-weight:700; font-size:15px; letter-spacing:-.01em; }
|
|
17259
17421
|
.brand .mark { width:24px; height:24px; border-radius:7px;
|
|
@@ -17385,18 +17547,19 @@ var styles_default = ` :root {
|
|
|
17385
17547
|
.btn-primary:hover { filter:brightness(1.07); }
|
|
17386
17548
|
.btn-danger { background:var(--danger); color:#fff; } .btn-danger:hover { filter:brightness(1.05); }
|
|
17387
17549
|
|
|
17388
|
-
/* desktop
|
|
17389
|
-
strip
|
|
17390
|
-
|
|
17550
|
+
/* macOS desktop (hiddenInset frameless): drop the sidebar logo and reserve a
|
|
17551
|
+
draggable strip so the inset traffic lights never overlap our controls.
|
|
17552
|
+
Windows/Linux keep the native frame, so the logo stays. */
|
|
17553
|
+
body.desktop-mac .brand { display:none; }
|
|
17391
17554
|
/* logo gone \u2192 keep the collapse button on the right (where .brand's flex used to push it) */
|
|
17392
|
-
body.desktop .sb-head .iconbtn { margin-left:auto; }
|
|
17555
|
+
body.desktop-mac .sb-head .iconbtn { margin-left:auto; }
|
|
17393
17556
|
body.desktop-mac .sb-head { -webkit-app-region:drag; }
|
|
17394
17557
|
body.desktop-mac .sb-head .iconbtn { -webkit-app-region:no-drag; }
|
|
17395
17558
|
body.desktop-mac .topbar { -webkit-app-region:drag; }
|
|
17396
17559
|
body.desktop-mac .topbar .iconbtn, body.desktop-mac .topbar .btn { -webkit-app-region:no-drag; }
|
|
17397
17560
|
/* peek drawer header overlaps the topbar drag region \u2192 clear its buttons so they stay clickable */
|
|
17398
17561
|
body.desktop-mac .peek-head { -webkit-app-region:drag; }
|
|
17399
|
-
body.desktop-mac .peek-head .iconbtn, body.desktop-mac .peek-head .btn { -webkit-app-region:no-drag; }
|
|
17562
|
+
body.desktop-mac .peek-head .iconbtn, body.desktop-mac .peek-head .btn, body.desktop-mac .peek-head .hist-toggle { -webkit-app-region:no-drag; }
|
|
17400
17563
|
/* sidebar collapsed \u2192 traffic lights sit over the main header; clear the hamburger */
|
|
17401
17564
|
body.desktop-mac .sidebar.collapsed ~ .main .topbar { padding-left:80px; }
|
|
17402
17565
|
|
|
@@ -17561,8 +17724,7 @@ var styles_default = ` :root {
|
|
|
17561
17724
|
color:var(--muted); border-radius:4px; opacity:0; transition:opacity .12s,background .12s; }
|
|
17562
17725
|
.doc-th:hover .doc-col-menu { opacity:1; }
|
|
17563
17726
|
.doc-col-menu:hover { background:var(--hover-2); color:var(--fg); }
|
|
17564
|
-
.doc-col-resizer { position:absolute; top:0; right:-
|
|
17565
|
-
.doc-col-resizer:hover, .doc-col-resizer.dragging { background:var(--accent); opacity:.35; }
|
|
17727
|
+
.doc-col-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:3; touch-action:none; }
|
|
17566
17728
|
/* per-row delete: small handle floating at the first cell on row hover */
|
|
17567
17729
|
.doc-row-del { position:absolute; top:3px; left:2px; width:18px; height:18px; display:grid; place-items:center;
|
|
17568
17730
|
color:var(--muted); background:var(--bg); border:1px solid var(--line); border-radius:5px; box-shadow:var(--shadow-sm);
|
|
@@ -17607,15 +17769,15 @@ var styles_default = ` :root {
|
|
|
17607
17769
|
.colhead:hover { background:var(--hover); }
|
|
17608
17770
|
.colhead .ti { color:var(--muted); } .colhead .ti svg { width:14px; height:14px; }
|
|
17609
17771
|
.colhead .nm { flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
17610
|
-
.col-resizer { position:absolute; top:0; right:-
|
|
17611
|
-
.col-resizer:hover, .col-resizer.dragging { background:var(--accent); opacity:.35; }
|
|
17772
|
+
.col-resizer { position:absolute; top:0; right:-4px; width:7px; height:100%; cursor:col-resize; z-index:2; touch-action:none; }
|
|
17612
17773
|
.addcol { min-width:46px; } .addcol .colhead { justify-content:flex-start; color:var(--muted); }
|
|
17613
17774
|
/* trailing filler column: empty canvas to the right of the data, not a real cell */
|
|
17614
17775
|
table.grid td.filler { border-right:0; border-bottom:0; background:transparent; }
|
|
17615
17776
|
body.col-resizing { user-select:none; cursor:col-resize; }
|
|
17616
17777
|
table.grid .selcell { width:38px; min-width:38px; max-width:38px; text-align:center; }
|
|
17617
17778
|
table.grid .gripcol, table.grid .rowgrip { width:26px; min-width:26px; max-width:26px; text-align:center; }
|
|
17618
|
-
|
|
17779
|
+
/* relative: anchors the .celledit overlay editor to the cell */
|
|
17780
|
+
td.cell-td { position:relative; vertical-align:top; }
|
|
17619
17781
|
.cell { padding:8px 11px; min-height:37px; cursor:text; word-break:break-word; display:flex; flex-wrap:wrap; gap:3px; align-items:center; }
|
|
17620
17782
|
/* hover/active tint goes on the <td> (always full row height) not the inner
|
|
17621
17783
|
.cell (only as tall as its content), so a taller sibling row never leaves a
|
|
@@ -17667,6 +17829,14 @@ var styles_default = ` :root {
|
|
|
17667
17829
|
.addrow:hover { background:var(--hover); color:var(--fg-soft); }
|
|
17668
17830
|
.gridfoot { display:flex; gap:18px; color:var(--muted); font-size:12px; padding:8px 2px; }
|
|
17669
17831
|
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; }
|
|
17832
|
+
/* Overlay cell editor: floats over the td; the display content underneath
|
|
17833
|
+
stays in flow so the row height never changes while editing. Padding on the
|
|
17834
|
+
input matches .cell (8px 11px) so the text doesn't shift on entering edit. */
|
|
17835
|
+
.celledit { position:absolute; top:0; left:0; width:100%; min-height:100%; z-index:10;
|
|
17836
|
+
display:flex; align-items:stretch; background:var(--bg); border-radius:var(--radius-sm);
|
|
17837
|
+
box-shadow:0 0 0 2px var(--accent), var(--shadow-md); }
|
|
17838
|
+
.celledit input.inlineedit { width:100%; min-width:0; border:0; outline:0; border-radius:inherit;
|
|
17839
|
+
padding:8px 11px; background:transparent; }
|
|
17670
17840
|
|
|
17671
17841
|
/* ---- sites (\u7AD9\u70B9\u7BA1\u7406) ---- */
|
|
17672
17842
|
.sites-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(264px, 1fr)); gap:14px; margin-top:6px; }
|
|
@@ -17770,6 +17940,53 @@ var styles_default = ` :root {
|
|
|
17770
17940
|
.proprow .v:hover { background:var(--hover); }
|
|
17771
17941
|
.peek-divider { height:1px; background:var(--line); margin:22px 0 18px; }
|
|
17772
17942
|
|
|
17943
|
+
/* version history (doc drawer + record peek view) */
|
|
17944
|
+
.hist-peek { width:760px; }
|
|
17945
|
+
.hist-title { display:flex; align-items:center; gap:6px; font-weight:600; font-size:var(--fs-ui); }
|
|
17946
|
+
.hist-toggle { display:flex; align-items:center; gap:5px; font-size:var(--fs-sm); color:var(--muted); cursor:pointer; user-select:none; white-space:nowrap; }
|
|
17947
|
+
.hist-toggle input { accent-color:var(--accent); }
|
|
17948
|
+
.hist-split { display:flex; flex:1; min-height:0; }
|
|
17949
|
+
.hist-list { width:252px; flex:none; overflow:auto; border-right:1px solid var(--line); padding:8px; }
|
|
17950
|
+
.hist-item { padding:8px 10px; border-radius:var(--radius-sm); cursor:pointer; }
|
|
17951
|
+
.hist-item:hover { background:var(--hover); }
|
|
17952
|
+
.hist-item.sel { background:var(--accent-soft); }
|
|
17953
|
+
.hist-item.static { cursor:default; border-bottom:1px solid var(--line); border-radius:0; padding:10px 4px; }
|
|
17954
|
+
.hist-item.static:hover { background:none; }
|
|
17955
|
+
.hist-item .row1 { display:flex; align-items:center; gap:7px; }
|
|
17956
|
+
.hist-item .row1 .when { font-weight:600; font-size:var(--fs-ui); }
|
|
17957
|
+
.hist-item .row2 { display:flex; gap:8px; margin-top:2px; font-size:var(--fs-sm); color:var(--muted); min-width:0; }
|
|
17958
|
+
.hist-item .row2 .what { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
17959
|
+
.hist-kind { font-size:var(--fs-xs); padding:1px 6px; border-radius:99px; background:var(--surface-2); color:var(--muted); }
|
|
17960
|
+
.hist-kind.revert { background:var(--accent-soft); color:var(--accent); }
|
|
17961
|
+
.hist-kind.repair { background:var(--danger-soft); color:var(--danger); }
|
|
17962
|
+
.hist-now { font-size:var(--fs-xs); padding:1px 6px; border-radius:99px; background:var(--accent); color:var(--accent-fg); }
|
|
17963
|
+
.hist-preview { flex:1; overflow:auto; padding:20px 24px; }
|
|
17964
|
+
.hist-doc-title { font-size:21px; font-weight:700; letter-spacing:-.02em; margin-bottom:14px; }
|
|
17965
|
+
.hist-block { white-space:pre-wrap; font-size:var(--fs-body); line-height:1.65; padding:2px 6px; border-radius:4px; margin:0 -6px 12px; }
|
|
17966
|
+
/* line-level diff rows (git-like): pale full-line tint, darker tint on the
|
|
17967
|
+
intra-line changed segment; strikethrough only on the deleted segment. */
|
|
17968
|
+
.hist-line { white-space:pre-wrap; font-size:var(--fs-body); line-height:1.6; padding:1px 6px; margin:0 -6px; border-radius:3px; }
|
|
17969
|
+
.hist-line.add { background:color-mix(in srgb, var(--success) 13%, transparent); }
|
|
17970
|
+
.hist-line.del { background:var(--danger-soft); }
|
|
17971
|
+
.hist-line.add .seg { background:color-mix(in srgb, var(--success) 32%, transparent); border-radius:2px; }
|
|
17972
|
+
.hist-line.del .seg { background:color-mix(in srgb, var(--danger) 28%, transparent); border-radius:2px; text-decoration:line-through; }
|
|
17973
|
+
.hist-foot { display:flex; justify-content:flex-end; padding:12px 16px; border-top:1px solid var(--line); }
|
|
17974
|
+
.pad { padding:12px; }
|
|
17975
|
+
.hist-rec-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
|
|
17976
|
+
.hist-feed { padding:12px 24px; }
|
|
17977
|
+
.hist-recname { font-weight:600; font-size:var(--fs-sm); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
17978
|
+
.hist-status { font-size:var(--fs-xs); color:var(--muted); flex:none; }
|
|
17979
|
+
.hist-field .old, .hist-field .new { max-width:200px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
17980
|
+
.hist-more { border:0; background:none; color:var(--muted); font-size:var(--fs-sm); cursor:pointer; padding:2px 0; text-align:left; }
|
|
17981
|
+
.hist-more:hover { color:var(--fg); }
|
|
17982
|
+
.hist-fields { padding:8px 4px 4px; display:flex; flex-direction:column; gap:6px; }
|
|
17983
|
+
.hist-field { display:flex; gap:8px; font-size:var(--fs-sm); align-items:baseline; flex-wrap:wrap; }
|
|
17984
|
+
.hist-field .fname { color:var(--muted); width:120px; flex:none; }
|
|
17985
|
+
.hist-field .old { text-decoration:line-through; color:var(--muted); }
|
|
17986
|
+
.hist-field .arr { color:var(--muted); }
|
|
17987
|
+
.hist-field .new { font-weight:500; }
|
|
17988
|
+
.hist-restore { margin-top:8px; align-self:flex-start; }
|
|
17989
|
+
|
|
17773
17990
|
/* menu / popover */
|
|
17774
17991
|
.menu-layer { position:fixed; inset:0; z-index:90; }
|
|
17775
17992
|
.pop { position:fixed; background:var(--bg); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow-lg);
|
|
@@ -17898,12 +18115,51 @@ var styles_default = ` :root {
|
|
|
17898
18115
|
.kg-particle { opacity:0 !important; }
|
|
17899
18116
|
}
|
|
17900
18117
|
.error-bar { margin:12px 24px; padding:10px 14px; background:var(--danger-soft); color:var(--danger); border-radius:var(--radius-sm); font-size:13px; cursor:pointer; }
|
|
18118
|
+
.offline-bar { margin:12px 24px 0; padding:8px 14px; background:var(--surface-2); color:var(--fg-soft); border-radius:var(--radius-sm); font-size:12.5px; }
|
|
17901
18119
|
.search-hit { margin:0 0 8px; padding:10px 14px; background:var(--surface); border-radius:var(--radius-sm); font-size:13px; cursor:pointer; }
|
|
17902
18120
|
.search-hit:hover { background:var(--surface-2); }
|
|
17903
18121
|
.search-hit mark { background:transparent; color:var(--accent); font-weight:600; }
|
|
17904
18122
|
.view-placeholder { padding:40px; text-align:center; border:1px dashed var(--line-strong); border-radius:var(--radius); color:var(--muted); margin-top:8px; }
|
|
17905
18123
|
.ico.flip { transform:scaleX(-1); }
|
|
17906
18124
|
|
|
18125
|
+
/* ---- find in document (Ctrl-F) ---- */
|
|
18126
|
+
/* CSS Custom Highlight overlays \u2014 no DOM mutation, safe over contentEditable. */
|
|
18127
|
+
::highlight(mh-find-all) { background:color-mix(in srgb, var(--accent) 26%, transparent); border-radius:2px; }
|
|
18128
|
+
::highlight(mh-find-current) { background:var(--accent); color:var(--accent-fg); }
|
|
18129
|
+
.find-bar {
|
|
18130
|
+
position:fixed; top:58px; right:18px; z-index:40; /* below the ~50px topbar */
|
|
18131
|
+
display:flex; align-items:center; gap:4px;
|
|
18132
|
+
padding:5px 6px; background:var(--bg); border:1px solid var(--line-strong);
|
|
18133
|
+
border-radius:var(--radius); box-shadow:var(--shadow-lg);
|
|
18134
|
+
}
|
|
18135
|
+
.find-bar .find-ico { color:var(--muted); margin:0 2px; flex:none; }
|
|
18136
|
+
.find-bar input {
|
|
18137
|
+
width:200px; max-width:42vw; border:0; outline:0; background:transparent;
|
|
18138
|
+
color:var(--fg); font-size:13px; padding:3px 2px;
|
|
18139
|
+
}
|
|
18140
|
+
.find-bar input::placeholder { color:var(--muted); }
|
|
18141
|
+
.find-count { font-size:12px; color:var(--muted); min-width:48px; text-align:right; padding:0 2px; font-variant-numeric:tabular-nums; }
|
|
18142
|
+
.find-opt {
|
|
18143
|
+
border:1px solid var(--line-strong); background:transparent; color:var(--fg-soft);
|
|
18144
|
+
border-radius:var(--radius-sm); font-size:12px; line-height:1; padding:4px 6px; cursor:pointer;
|
|
18145
|
+
transition:background .12s, color .12s, border-color .12s;
|
|
18146
|
+
}
|
|
18147
|
+
.find-opt:hover { background:var(--surface-2); }
|
|
18148
|
+
.find-opt.on { background:var(--accent-soft); color:var(--accent); border-color:color-mix(in srgb, var(--accent) 45%, transparent); }
|
|
18149
|
+
.find-nav {
|
|
18150
|
+
display:grid; place-items:center; width:26px; height:26px; flex:none;
|
|
18151
|
+
border:0; background:transparent; color:var(--fg-soft); border-radius:var(--radius-sm); cursor:pointer;
|
|
18152
|
+
transition:background .12s, color .12s;
|
|
18153
|
+
}
|
|
18154
|
+
.find-nav:hover:not(:disabled) { background:var(--surface-2); color:var(--fg); }
|
|
18155
|
+
.find-nav:disabled { opacity:.4; cursor:default; }
|
|
18156
|
+
.find-nav .find-prev { transform:rotate(180deg); }
|
|
18157
|
+
.find-close { color:var(--muted); }
|
|
18158
|
+
@media (max-width:560px) {
|
|
18159
|
+
.find-bar { left:12px; right:12px; }
|
|
18160
|
+
.find-bar input { width:auto; flex:1; max-width:none; }
|
|
18161
|
+
}
|
|
18162
|
+
|
|
17907
18163
|
/* ---- board (kanban) view ---- */
|
|
17908
18164
|
.board-wrap { margin-top:4px; }
|
|
17909
18165
|
.board-bar { display:flex; align-items:center; gap:10px; padding:2px 0 12px; }
|
|
@@ -17979,9 +18235,16 @@ var styles_default = ` :root {
|
|
|
17979
18235
|
display:flex; align-items:center; gap:2px; padding:0 4px; cursor:grab; touch-action:none; box-shadow:var(--shadow-sm); overflow:hidden; }
|
|
17980
18236
|
.tl-item .tl-label { flex:1; font-size:12px; font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; pointer-events:none; }
|
|
17981
18237
|
.tl-row:hover .tl-item { box-shadow:0 0 0 1px var(--accent), var(--shadow-sm); }
|
|
17982
|
-
|
|
17983
|
-
|
|
17984
|
-
|
|
18238
|
+
/* Edge handles: a small centered round-capped grip inside the colored bar.
|
|
18239
|
+
--accent-fg (not white) keeps contrast right in both themes \u2014 dark grip on
|
|
18240
|
+
the light-purple dark-mode bar, white grip on the blue light-mode bar. */
|
|
18241
|
+
.tl-item .tl-h { position:relative; width:9px; align-self:stretch; flex:none; cursor:col-resize; opacity:0; transition:opacity .15s; touch-action:none; }
|
|
18242
|
+
.tl-item .tl-h::before { content:""; position:absolute; left:50%; top:50%; width:3px; height:10px;
|
|
18243
|
+
transform:translate(-50%,-50%); border-radius:999px;
|
|
18244
|
+
background:color-mix(in srgb, var(--accent-fg) 70%, transparent);
|
|
18245
|
+
transition:height .15s cubic-bezier(.4,0,.2,1), background .15s; }
|
|
18246
|
+
.tl-item:hover .tl-h, .tl-item .tl-h.dragging { opacity:1; }
|
|
18247
|
+
.tl-item .tl-h:hover::before, .tl-item .tl-h.dragging::before { height:14px; background:var(--accent-fg); }
|
|
17985
18248
|
.tl-milestone { position:absolute; top:9px; width:18px; height:18px; transform:rotate(45deg); background:var(--accent); border-radius:3px; cursor:grab; touch-action:none; box-shadow:var(--shadow-sm); margin-left:-9px; }
|
|
17986
18249
|
.tl-milestone:hover { box-shadow:0 0 0 2px var(--accent-soft), var(--shadow-sm); }
|
|
17987
18250
|
.tl-track-empty { position:absolute; top:0; right:0; height:100%; cursor:copy; }
|
|
@@ -18075,8 +18338,13 @@ var styles_default = ` :root {
|
|
|
18075
18338
|
/* Anchor/TOC jumps: clear the mobile topbar (49px min + notch inset);
|
|
18076
18339
|
the desktop 60px estimate leaves blocks half-hidden behind it. */
|
|
18077
18340
|
.block { scroll-margin-top:calc(49px + env(safe-area-inset-top) + 8px); }
|
|
18341
|
+
/* Find bar clears the taller mobile topbar (49px min + notch inset). */
|
|
18342
|
+
.find-bar { top:calc(49px + env(safe-area-inset-top) + 8px); }
|
|
18078
18343
|
.db { padding:16px 14px 90px; }
|
|
18079
18344
|
.peek { width:100%; }
|
|
18345
|
+
/* history drawer: the side-by-side list/preview doesn't fit a phone \u2014 stack. */
|
|
18346
|
+
.hist-split { flex-direction:column; }
|
|
18347
|
+
.hist-list { width:auto; max-height:38%; border-right:0; border-bottom:1px solid var(--line); }
|
|
18080
18348
|
|
|
18081
18349
|
/* Safe areas (viewport-fit=cover): keep chrome clear of the notch / home bar. */
|
|
18082
18350
|
.sb-head { padding-top:calc(14px + env(safe-area-inset-top)); }
|
|
@@ -18126,7 +18394,7 @@ var styles_default = ` :root {
|
|
|
18126
18394
|
table.doc-table .doc-row-del { opacity:1; }
|
|
18127
18395
|
.selcell input { opacity:1; }
|
|
18128
18396
|
.rowgrip { color:var(--muted); }
|
|
18129
|
-
.tl-item .tl-h { opacity:1;
|
|
18397
|
+
.tl-item .tl-h { opacity:1; }
|
|
18130
18398
|
/* Neutralize the stickiest tap-residue tints (a tap "hovers" until the
|
|
18131
18399
|
next tap elsewhere); transient menus/buttons are left alone. */
|
|
18132
18400
|
.navitem:hover { background:none; }
|
|
@@ -18142,15 +18410,20 @@ var styles_default = ` :root {
|
|
|
18142
18410
|
macOS vibrancy blur show through; a translucent panel keeps text readable.
|
|
18143
18411
|
The top bar is the window drag region; its buttons opt back out. */
|
|
18144
18412
|
body.quicknote { background:transparent; }
|
|
18413
|
+
/* html carries an opaque bg for the iOS 26 Safari toolbar tint; the vibrancy
|
|
18414
|
+
window needs the root transparent again or the blur is walled off. */
|
|
18415
|
+
html:has(body.quicknote) { background:transparent; }
|
|
18145
18416
|
/* The main app lays #app out as a flex row (sidebar + main); the quick-note
|
|
18146
18417
|
route has a single child, so override to a full-bleed block or the panel
|
|
18147
18418
|
shrinks to its content width and the window's vibrancy shows through. */
|
|
18148
18419
|
body.quicknote #app { display:block; width:100vw; height:100vh; height:100dvh; }
|
|
18149
18420
|
.qn { display:flex; flex-direction:column; width:100%; height:100vh; height:100dvh; overflow:hidden;
|
|
18150
18421
|
background:color-mix(in srgb, var(--bg) 20%, transparent); }
|
|
18151
|
-
.qn-bar { display:flex; align-items:center; gap:6px; padding:4px 10px
|
|
18422
|
+
.qn-bar { display:flex; align-items:center; gap:6px; padding:4px 10px;
|
|
18152
18423
|
user-select:none; -webkit-app-region:drag;
|
|
18153
18424
|
background:color-mix(in srgb, var(--surface) 30%, transparent); }
|
|
18425
|
+
/* macOS only: clear the inset traffic lights; Win/Linux frameless has none */
|
|
18426
|
+
body.desktop-mac .qn-bar { padding-left:76px; }
|
|
18154
18427
|
.qn-brand { flex:1; min-width:0; font-size:12px; font-weight:600; color:var(--fg-soft);
|
|
18155
18428
|
white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
18156
18429
|
.qn-actions { display:flex; gap:2px; -webkit-app-region:no-drag; }
|
|
@@ -18160,6 +18433,10 @@ var styles_default = ` :root {
|
|
|
18160
18433
|
.qn-body .doc { max-width:none; margin:0; padding:14px 22px 30vh; }
|
|
18161
18434
|
.qn-body .doc-title { font-size:24px; margin-bottom:2px; }
|
|
18162
18435
|
.qn-body .doc-meta { display:none; }
|
|
18436
|
+
/* The narrow-width gutter hide is now gated on (pointer:coarse), which this
|
|
18437
|
+
desktop window never matches \u2014 re-hide explicitly: quick notes offer no
|
|
18438
|
+
block insert/drag handles (Enter / slash commands cover insertion). */
|
|
18439
|
+
body.quicknote .block .gutter { display:none; }
|
|
18163
18440
|
/* A native checkbox paints an opaque white box when unchecked, which breaks
|
|
18164
18441
|
the window's vibrancy blur. Draw a custom box so the unchecked background
|
|
18165
18442
|
stays transparent; checked falls back to the accent fill + checkmark. */
|
|
@@ -18193,6 +18470,11 @@ var styles_default = ` :root {
|
|
|
18193
18470
|
.qn-shortcut.capturing { outline:2px solid var(--accent); color:var(--accent); }
|
|
18194
18471
|
`;
|
|
18195
18472
|
|
|
18473
|
+
// src/webui/server/icons.ts
|
|
18474
|
+
var ICON_192 = "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";
|
|
18475
|
+
var ICON_512 = "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";
|
|
18476
|
+
var ICON_180 = "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";
|
|
18477
|
+
|
|
18196
18478
|
// src/webui/server/assets.ts
|
|
18197
18479
|
var RUNNING_FROM_SOURCE = import.meta.url.includes("/src/webui/");
|
|
18198
18480
|
var HTML = `<!doctype html>
|
|
@@ -18220,6 +18502,8 @@ var HTML = `<!doctype html>
|
|
|
18220
18502
|
alone, so the dark palette exists exactly once \u2014 see styles.css. Dark also
|
|
18221
18503
|
pre-tints the status bar (hex mirrors --bg) so the first frame isn't white. -->
|
|
18222
18504
|
<script>try{var t=localStorage.getItem('mh-theme'),d=t==='dark'||(t!=='light'&&matchMedia('(prefers-color-scheme: dark)').matches);document.documentElement.dataset.resolved=d?'dark':'light';if(d)document.getElementById('theme-color-meta').content='#1a1a1c'}catch(e){}</script>
|
|
18505
|
+
<link rel="manifest" href="/manifest.webmanifest">
|
|
18506
|
+
<link rel="apple-touch-icon" href="/icons/icon-180.png">
|
|
18223
18507
|
<link rel="stylesheet" href="/webui.css">
|
|
18224
18508
|
</head>
|
|
18225
18509
|
<body>
|
|
@@ -18227,12 +18511,23 @@ var HTML = `<!doctype html>
|
|
|
18227
18511
|
<script type="module" src="/webui.js"></script>
|
|
18228
18512
|
</body>
|
|
18229
18513
|
</html>`;
|
|
18230
|
-
var
|
|
18231
|
-
|
|
18232
|
-
|
|
18514
|
+
var injected = {
|
|
18515
|
+
js: null,
|
|
18516
|
+
sw: null,
|
|
18517
|
+
dbWorker: null,
|
|
18518
|
+
runtime: null,
|
|
18519
|
+
sdk: null
|
|
18520
|
+
};
|
|
18521
|
+
var injectedWasm = null;
|
|
18522
|
+
function setWebuiBundle(bundle) {
|
|
18523
|
+
injected.js = bundle.js;
|
|
18524
|
+
injected.sw = bundle.sw;
|
|
18525
|
+
injected.dbWorker = bundle.dbWorker;
|
|
18526
|
+
injected.runtime = bundle.runtime;
|
|
18527
|
+
injected.sdk = bundle.sdk;
|
|
18528
|
+
const w = bundle.wasm;
|
|
18529
|
+
injectedWasm = w.byteOffset === 0 && w.byteLength === w.buffer.byteLength ? w.buffer : w.slice().buffer;
|
|
18233
18530
|
}
|
|
18234
|
-
var cachedJs = null;
|
|
18235
|
-
var cachedJsMtime = 0;
|
|
18236
18531
|
function newestSourceMtime(dir) {
|
|
18237
18532
|
let newest = 0;
|
|
18238
18533
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -18246,37 +18541,83 @@ function newestSourceMtime(dir) {
|
|
|
18246
18541
|
}
|
|
18247
18542
|
return newest;
|
|
18248
18543
|
}
|
|
18249
|
-
|
|
18250
|
-
|
|
18251
|
-
|
|
18252
|
-
|
|
18253
|
-
const
|
|
18254
|
-
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
const
|
|
18258
|
-
|
|
18259
|
-
|
|
18260
|
-
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
|
|
18267
|
-
|
|
18268
|
-
|
|
18269
|
-
|
|
18270
|
-
|
|
18271
|
-
|
|
18272
|
-
|
|
18273
|
-
}
|
|
18544
|
+
function bundleGetter(opts) {
|
|
18545
|
+
let cached2 = null;
|
|
18546
|
+
let cachedMtime = 0;
|
|
18547
|
+
return async () => {
|
|
18548
|
+
const fromBinary = injected[opts.key];
|
|
18549
|
+
if (fromBinary != null)
|
|
18550
|
+
return fromBinary;
|
|
18551
|
+
if (RUNNING_FROM_SOURCE) {
|
|
18552
|
+
const srcDir = fileURLToPath(new URL("..", import.meta.url));
|
|
18553
|
+
let newest = newestSourceMtime(srcDir);
|
|
18554
|
+
for (const extra of opts.extraDirs ?? []) {
|
|
18555
|
+
newest = Math.max(newest, newestSourceMtime(fileURLToPath(new URL(extra, import.meta.url))));
|
|
18556
|
+
}
|
|
18557
|
+
if (cached2 == null || newest > cachedMtime) {
|
|
18558
|
+
const entry = fileURLToPath(new URL(opts.entry, import.meta.url));
|
|
18559
|
+
const res = await Bun.build({ entrypoints: [entry], target: "browser" });
|
|
18560
|
+
if (!res.success)
|
|
18561
|
+
throw new AggregateError(res.logs, `${opts.dist} build failed`);
|
|
18562
|
+
cached2 = await res.outputs[0].text();
|
|
18563
|
+
cachedMtime = newest;
|
|
18564
|
+
}
|
|
18565
|
+
return cached2;
|
|
18566
|
+
}
|
|
18567
|
+
if (cached2 == null) {
|
|
18568
|
+
const prebuilt = Bun.file(fileURLToPath(new URL(`./${opts.dist}`, import.meta.url)));
|
|
18569
|
+
if (!await prebuilt.exists()) {
|
|
18570
|
+
throw new Error(`${opts.dist} missing: run \`bun run build\``);
|
|
18571
|
+
}
|
|
18572
|
+
cached2 = await prebuilt.text();
|
|
18573
|
+
}
|
|
18574
|
+
return cached2;
|
|
18575
|
+
};
|
|
18576
|
+
}
|
|
18577
|
+
var getJs = bundleGetter({ key: "js", entry: "../app.tsx", dist: "webui.js" });
|
|
18578
|
+
var getSwRaw = bundleGetter({ key: "sw", entry: "../sw.ts", dist: "sw.js" });
|
|
18579
|
+
var getDbWorker = bundleGetter({ key: "dbWorker", entry: "../data/db-worker.ts", dist: "db-worker.js" });
|
|
18580
|
+
var getRuntime = bundleGetter({ key: "runtime", entry: "../runtime.ts", dist: "mh-runtime.js" });
|
|
18581
|
+
var getSdk = bundleGetter({ key: "sdk", entry: "../../sdk/client.ts", dist: "metahub-sdk.js", extraDirs: ["../../sdk"] });
|
|
18274
18582
|
async function getCss() {
|
|
18275
18583
|
if (RUNNING_FROM_SOURCE) {
|
|
18276
18584
|
return Bun.file(fileURLToPath(new URL("../styles.css", import.meta.url))).text();
|
|
18277
18585
|
}
|
|
18278
18586
|
return styles_default;
|
|
18279
18587
|
}
|
|
18588
|
+
async function getSw() {
|
|
18589
|
+
const [raw, js, css] = await Promise.all([getSwRaw(), getJs(), getCss()]);
|
|
18590
|
+
const version2 = new Bun.CryptoHasher("sha256").update(js).update(css).digest("hex").slice(0, 16);
|
|
18591
|
+
return raw.replaceAll("__MH_SW_VERSION__", version2);
|
|
18592
|
+
}
|
|
18593
|
+
async function getWasm() {
|
|
18594
|
+
if (injectedWasm != null)
|
|
18595
|
+
return injectedWasm;
|
|
18596
|
+
const path = RUNNING_FROM_SOURCE ? join3(dirname(Bun.resolveSync("@sqlite.org/sqlite-wasm", fileURLToPath(new URL(".", import.meta.url)))), "sqlite3.wasm") : fileURLToPath(new URL("./sqlite3.wasm", import.meta.url));
|
|
18597
|
+
const f = Bun.file(path);
|
|
18598
|
+
if (!await f.exists()) {
|
|
18599
|
+
throw new Error(`sqlite3.wasm missing at ${path} \u2014 run \`bun install\` / \`bun run build\``);
|
|
18600
|
+
}
|
|
18601
|
+
return f.arrayBuffer();
|
|
18602
|
+
}
|
|
18603
|
+
var MANIFEST = JSON.stringify({
|
|
18604
|
+
name: "Metahub",
|
|
18605
|
+
short_name: "Metahub",
|
|
18606
|
+
start_url: "/",
|
|
18607
|
+
scope: "/",
|
|
18608
|
+
display: "standalone",
|
|
18609
|
+
background_color: "#ffffff",
|
|
18610
|
+
theme_color: "#ffffff",
|
|
18611
|
+
icons: [
|
|
18612
|
+
{ src: "/icons/icon-192.png", sizes: "192x192", type: "image/png" },
|
|
18613
|
+
{ src: "/icons/icon-512.png", sizes: "512x512", type: "image/png" }
|
|
18614
|
+
]
|
|
18615
|
+
});
|
|
18616
|
+
var ICONS = {
|
|
18617
|
+
"/icons/icon-192.png": ICON_192,
|
|
18618
|
+
"/icons/icon-512.png": ICON_512,
|
|
18619
|
+
"/icons/icon-180.png": ICON_180
|
|
18620
|
+
};
|
|
18280
18621
|
async function warmWebui() {
|
|
18281
18622
|
try {
|
|
18282
18623
|
await getJs();
|
|
@@ -18295,11 +18636,37 @@ async function serveWebui(req) {
|
|
|
18295
18636
|
return asset(HTML, "text/html; charset=utf-8");
|
|
18296
18637
|
if (pathname === "/webui.css")
|
|
18297
18638
|
return asset(await getCss(), "text/css; charset=utf-8");
|
|
18298
|
-
if (pathname === "/
|
|
18639
|
+
if (pathname === "/manifest.webmanifest")
|
|
18640
|
+
return asset(MANIFEST, "application/manifest+json");
|
|
18641
|
+
const icon = ICONS[pathname];
|
|
18642
|
+
if (icon) {
|
|
18643
|
+
return new Response(Buffer.from(icon, "base64"), {
|
|
18644
|
+
headers: { "content-type": "image/png", "cache-control": "public, max-age=86400" }
|
|
18645
|
+
});
|
|
18646
|
+
}
|
|
18647
|
+
if (pathname === "/sqlite3.wasm") {
|
|
18299
18648
|
try {
|
|
18300
|
-
return
|
|
18649
|
+
return new Response(await getWasm(), {
|
|
18650
|
+
headers: { "content-type": "application/wasm", "cache-control": "public, max-age=86400" }
|
|
18651
|
+
});
|
|
18301
18652
|
} catch (e) {
|
|
18302
|
-
console.error("[webui] failed to serve /
|
|
18653
|
+
console.error("[webui] failed to serve /sqlite3.wasm \u2014", e);
|
|
18654
|
+
return new Response(`sqlite3.wasm unavailable: ${e}`, { status: 500 });
|
|
18655
|
+
}
|
|
18656
|
+
}
|
|
18657
|
+
const script = {
|
|
18658
|
+
"/webui.js": getJs,
|
|
18659
|
+
"/sw.js": getSw,
|
|
18660
|
+
"/db-worker.js": getDbWorker,
|
|
18661
|
+
"/mh-runtime.js": getRuntime,
|
|
18662
|
+
"/metahub-sdk.js": getSdk
|
|
18663
|
+
};
|
|
18664
|
+
const getter = script[pathname];
|
|
18665
|
+
if (getter) {
|
|
18666
|
+
try {
|
|
18667
|
+
return asset(await getter(), "text/javascript; charset=utf-8");
|
|
18668
|
+
} catch (e) {
|
|
18669
|
+
console.error(`[webui] failed to serve ${pathname} \u2014`, e);
|
|
18303
18670
|
return new Response(`webui build failed: ${e}`, { status: 500 });
|
|
18304
18671
|
}
|
|
18305
18672
|
}
|
|
@@ -18318,12 +18685,20 @@ function hlcMillis(hlc2) {
|
|
|
18318
18685
|
function hlcIso(hlc2) {
|
|
18319
18686
|
return new Date(hlcMillis(hlc2)).toISOString();
|
|
18320
18687
|
}
|
|
18688
|
+
function revisionKind(txn) {
|
|
18689
|
+
if (txn?.startsWith("repair:"))
|
|
18690
|
+
return "repair";
|
|
18691
|
+
if (txn?.startsWith("revert:"))
|
|
18692
|
+
return "revert";
|
|
18693
|
+
return "user";
|
|
18694
|
+
}
|
|
18321
18695
|
function clusterRevisions(changes) {
|
|
18322
18696
|
const groups = [];
|
|
18323
18697
|
let cur = [];
|
|
18324
18698
|
for (const c of changes) {
|
|
18325
18699
|
const prev = cur[cur.length - 1];
|
|
18326
|
-
|
|
18700
|
+
const boundary = prev && (prev.txn !== null && c.txn !== null ? prev.txn !== c.txn : prev.node_id !== c.node_id || hlcMillis(c.hlc) - hlcMillis(prev.hlc) > REVISION_GAP_MS);
|
|
18701
|
+
if (boundary) {
|
|
18327
18702
|
groups.push(cur);
|
|
18328
18703
|
cur = [];
|
|
18329
18704
|
}
|
|
@@ -18348,7 +18723,7 @@ function registersAt(changes, at) {
|
|
|
18348
18723
|
return out;
|
|
18349
18724
|
}
|
|
18350
18725
|
function rowChanges(db2, dataset, rowId) {
|
|
18351
|
-
return db2.query(
|
|
18726
|
+
return db2.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes WHERE dataset = ? AND row_id = ? ORDER BY hlc`).all(dataset, rowId);
|
|
18352
18727
|
}
|
|
18353
18728
|
function everBlockIds(db2, docId) {
|
|
18354
18729
|
return db2.query("SELECT DISTINCT row_id FROM crdt_changes WHERE dataset = 'doc_blocks' AND col = 'doc_id' AND value = ?").all(JSON.stringify(docId)).map((r) => r.row_id);
|
|
@@ -18357,7 +18732,7 @@ function docChanges(db2, docId) {
|
|
|
18357
18732
|
const blocks2 = everBlockIds(db2, docId);
|
|
18358
18733
|
const placeholders = blocks2.map(() => "?").join(",");
|
|
18359
18734
|
const blockClause = blocks2.length ? ` OR (dataset = 'doc_blocks' AND row_id IN (${placeholders}))` : "";
|
|
18360
|
-
return db2.query(`SELECT
|
|
18735
|
+
return db2.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes
|
|
18361
18736
|
WHERE (dataset = 'documents' AND row_id = ?)${blockClause}
|
|
18362
18737
|
ORDER BY hlc`).all(docId, ...blocks2);
|
|
18363
18738
|
}
|
|
@@ -18390,6 +18765,7 @@ function listDocumentRevisions(db2, id) {
|
|
|
18390
18765
|
version: last.hlc,
|
|
18391
18766
|
at: hlcIso(last.hlc),
|
|
18392
18767
|
node_id: last.node_id,
|
|
18768
|
+
kind: revisionKind(last.txn),
|
|
18393
18769
|
changes: group.length,
|
|
18394
18770
|
created,
|
|
18395
18771
|
deleted,
|
|
@@ -18456,15 +18832,20 @@ function documentAtVersion(db2, id, at) {
|
|
|
18456
18832
|
version: changes[changes.length - 1].hlc
|
|
18457
18833
|
};
|
|
18458
18834
|
}
|
|
18459
|
-
|
|
18460
|
-
|
|
18461
|
-
if (!cur)
|
|
18835
|
+
var revertDocument = grouped(function revertDocument2(db2, id, to, opts = {}) {
|
|
18836
|
+
let cur = getDocument(db2, id);
|
|
18837
|
+
if (!cur && !db2.query("SELECT 1 AS x FROM documents WHERE id = ?").get(id))
|
|
18462
18838
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
18463
18839
|
if (opts.ifMatch !== undefined && documentVersion(db2, id) !== opts.ifMatch)
|
|
18464
18840
|
throw new MhError("stale", `stale: document changed since ${opts.ifMatch}; re-read first`);
|
|
18465
18841
|
const past = documentAtVersion(db2, id, to);
|
|
18466
18842
|
if (past.deleted)
|
|
18467
18843
|
throw new MhError("invalid_input", "target version is a deleted state; use doc delete instead");
|
|
18844
|
+
const undeleted = !cur;
|
|
18845
|
+
if (!cur) {
|
|
18846
|
+
emit(db2, "documents", id, "__deleted", 0);
|
|
18847
|
+
cur = getDocument(db2, id);
|
|
18848
|
+
}
|
|
18468
18849
|
const fields = {};
|
|
18469
18850
|
if (past.title !== cur.title)
|
|
18470
18851
|
fields.title = past.title;
|
|
@@ -18474,43 +18855,98 @@ function revertDocument(db2, id, to, opts = {}) {
|
|
|
18474
18855
|
updateDocument(db2, id, fields);
|
|
18475
18856
|
return {
|
|
18476
18857
|
id,
|
|
18477
|
-
changed: Object.keys(fields).length > 0,
|
|
18858
|
+
changed: Object.keys(fields).length > 0 || undeleted,
|
|
18478
18859
|
restored: past.version,
|
|
18479
|
-
version: documentVersion(db2, id)
|
|
18860
|
+
version: documentVersion(db2, id),
|
|
18861
|
+
undeleted
|
|
18480
18862
|
};
|
|
18481
|
-
}
|
|
18863
|
+
}, "revert");
|
|
18482
18864
|
var RECORD_META2 = new Set(["database_id", "created_hlc", "order_key", "__deleted"]);
|
|
18865
|
+
function recordRevisionOf(group) {
|
|
18866
|
+
const fields = new Set;
|
|
18867
|
+
let created = false;
|
|
18868
|
+
let deleted = false;
|
|
18869
|
+
let moved = false;
|
|
18870
|
+
for (const c of group) {
|
|
18871
|
+
if (c.col === "created_hlc")
|
|
18872
|
+
created = true;
|
|
18873
|
+
else if (c.col === "__deleted")
|
|
18874
|
+
deleted = flagSet(c.value);
|
|
18875
|
+
else if (c.col === "order_key")
|
|
18876
|
+
moved = true;
|
|
18877
|
+
else if (!RECORD_META2.has(c.col))
|
|
18878
|
+
fields.add(c.col);
|
|
18879
|
+
}
|
|
18880
|
+
const last = group[group.length - 1];
|
|
18881
|
+
return {
|
|
18882
|
+
version: last.hlc,
|
|
18883
|
+
at: hlcIso(last.hlc),
|
|
18884
|
+
node_id: last.node_id,
|
|
18885
|
+
kind: revisionKind(last.txn),
|
|
18886
|
+
changes: group.length,
|
|
18887
|
+
created,
|
|
18888
|
+
deleted,
|
|
18889
|
+
fields: [...fields],
|
|
18890
|
+
moved
|
|
18891
|
+
};
|
|
18892
|
+
}
|
|
18483
18893
|
function listRecordRevisions(db2, id) {
|
|
18484
18894
|
const changes = rowChanges(db2, "records", id);
|
|
18485
18895
|
if (!changes.length)
|
|
18486
18896
|
throw new MhError("not_found", `no such record: ${id}`);
|
|
18487
|
-
return clusterRevisions(changes).map((
|
|
18488
|
-
|
|
18489
|
-
|
|
18490
|
-
|
|
18491
|
-
|
|
18492
|
-
|
|
18493
|
-
|
|
18494
|
-
|
|
18495
|
-
|
|
18496
|
-
|
|
18497
|
-
|
|
18498
|
-
|
|
18499
|
-
|
|
18500
|
-
|
|
18897
|
+
return clusterRevisions(changes).map(recordRevisionOf).reverse();
|
|
18898
|
+
}
|
|
18899
|
+
function listDatabaseActivity(db2, databaseId, opts = {}) {
|
|
18900
|
+
if (!db2.query("SELECT 1 AS x FROM databases WHERE id = ?").get(databaseId))
|
|
18901
|
+
throw new MhError("not_found", `no such database: ${databaseId}`);
|
|
18902
|
+
const titleProp = db2.query("SELECT id FROM properties WHERE database_id = ? AND type = 'text' AND __deleted = 0 ORDER BY position LIMIT 1").get(databaseId)?.id ?? null;
|
|
18903
|
+
const changes = db2.query(`SELECT ${CHANGE_SELECT} FROM crdt_changes
|
|
18904
|
+
WHERE dataset = 'records' AND row_id IN (SELECT id FROM records WHERE database_id = ?)
|
|
18905
|
+
ORDER BY row_id, hlc`).all(databaseId);
|
|
18906
|
+
const out = [];
|
|
18907
|
+
let start = 0;
|
|
18908
|
+
for (let i = 1;i <= changes.length; i++) {
|
|
18909
|
+
if (i !== changes.length && changes[i].row_id === changes[start].row_id)
|
|
18910
|
+
continue;
|
|
18911
|
+
const rowId = changes[start].row_id;
|
|
18912
|
+
const state = new Map;
|
|
18913
|
+
for (const g of clusterRevisions(changes.slice(start, i))) {
|
|
18914
|
+
const touched = new Set;
|
|
18915
|
+
for (const c of g)
|
|
18916
|
+
if (!RECORD_META2.has(c.col))
|
|
18917
|
+
touched.add(c.col);
|
|
18918
|
+
const before = new Map;
|
|
18919
|
+
for (const col of touched)
|
|
18920
|
+
if (state.has(col))
|
|
18921
|
+
before.set(col, state.get(col));
|
|
18922
|
+
for (const c of g) {
|
|
18923
|
+
if (RECORD_META2.has(c.col))
|
|
18924
|
+
continue;
|
|
18925
|
+
if (c.value === null)
|
|
18926
|
+
state.delete(c.col);
|
|
18927
|
+
else
|
|
18928
|
+
state.set(c.col, JSON.parse(c.value));
|
|
18929
|
+
}
|
|
18930
|
+
const diffs = [];
|
|
18931
|
+
for (const col of touched) {
|
|
18932
|
+
const fc = { prop: col };
|
|
18933
|
+
if (before.has(col))
|
|
18934
|
+
fc.before = before.get(col);
|
|
18935
|
+
if (state.has(col))
|
|
18936
|
+
fc.after = state.get(col);
|
|
18937
|
+
diffs.push(fc);
|
|
18938
|
+
}
|
|
18939
|
+
out.push({
|
|
18940
|
+
...recordRevisionOf(g),
|
|
18941
|
+
record_id: rowId,
|
|
18942
|
+
record_title: titleProp && state.has(titleProp) ? String(state.get(titleProp) ?? "") : null,
|
|
18943
|
+
diffs
|
|
18944
|
+
});
|
|
18501
18945
|
}
|
|
18502
|
-
|
|
18503
|
-
|
|
18504
|
-
|
|
18505
|
-
|
|
18506
|
-
node_id: last.node_id,
|
|
18507
|
-
changes: group.length,
|
|
18508
|
-
created,
|
|
18509
|
-
deleted,
|
|
18510
|
-
fields: [...fields],
|
|
18511
|
-
moved
|
|
18512
|
-
};
|
|
18513
|
-
}).reverse();
|
|
18946
|
+
start = i;
|
|
18947
|
+
}
|
|
18948
|
+
out.sort((a, b) => a.version < b.version ? 1 : -1);
|
|
18949
|
+
return out.slice(0, opts.limit ?? 100);
|
|
18514
18950
|
}
|
|
18515
18951
|
function recordAtVersion(db2, id, at) {
|
|
18516
18952
|
if (!at)
|
|
@@ -18536,7 +18972,7 @@ function recordAtVersion(db2, id, at) {
|
|
|
18536
18972
|
version: changes[changes.length - 1].hlc
|
|
18537
18973
|
};
|
|
18538
18974
|
}
|
|
18539
|
-
|
|
18975
|
+
var revertRecord = grouped(function revertRecord2(db2, id, to) {
|
|
18540
18976
|
const cur = db2.query("SELECT database_id, data, __deleted FROM records WHERE id = ?").get(id);
|
|
18541
18977
|
if (!cur)
|
|
18542
18978
|
throw new MhError("not_found", `no such record: ${id}`);
|
|
@@ -18561,9 +18997,140 @@ function revertRecord(db2, id, to) {
|
|
|
18561
18997
|
if (undeleted)
|
|
18562
18998
|
emit(db2, "records", id, "__deleted", 0);
|
|
18563
18999
|
return { id, changed: fields.length > 0 || undeleted, fields, undeleted, restored: past.version };
|
|
19000
|
+
}, "revert");
|
|
19001
|
+
var PROP_DEF_COLS = ["name", "type", "config", "position"];
|
|
19002
|
+
function listPropertyRevisions(db2, id) {
|
|
19003
|
+
const changes = rowChanges(db2, "properties", id);
|
|
19004
|
+
if (!changes.length)
|
|
19005
|
+
throw new MhError("not_found", `no such property: ${id}`);
|
|
19006
|
+
const clearedByTxn = new Map(db2.query("SELECT txn, COUNT(*) AS n FROM crdt_changes WHERE dataset = 'records' AND col = ? AND txn IS NOT NULL GROUP BY txn").all(id).map((r) => [r.txn, r.n]));
|
|
19007
|
+
return clusterRevisions(changes).map((group) => {
|
|
19008
|
+
const fields = new Set;
|
|
19009
|
+
let created = false;
|
|
19010
|
+
let deleted = false;
|
|
19011
|
+
for (const c of group) {
|
|
19012
|
+
if (c.col === "database_id")
|
|
19013
|
+
created = true;
|
|
19014
|
+
else if (c.col === "__deleted")
|
|
19015
|
+
deleted = flagSet(c.value);
|
|
19016
|
+
else if (PROP_DEF_COLS.includes(c.col))
|
|
19017
|
+
fields.add(c.col);
|
|
19018
|
+
}
|
|
19019
|
+
const last = group[group.length - 1];
|
|
19020
|
+
return {
|
|
19021
|
+
version: last.hlc,
|
|
19022
|
+
at: hlcIso(last.hlc),
|
|
19023
|
+
node_id: last.node_id,
|
|
19024
|
+
kind: revisionKind(last.txn),
|
|
19025
|
+
changes: group.length,
|
|
19026
|
+
created,
|
|
19027
|
+
deleted,
|
|
19028
|
+
fields: [...fields],
|
|
19029
|
+
cells_cleared: last.txn ? clearedByTxn.get(last.txn) ?? 0 : 0
|
|
19030
|
+
};
|
|
19031
|
+
}).reverse();
|
|
19032
|
+
}
|
|
19033
|
+
function propertyAtVersion(db2, id, at) {
|
|
19034
|
+
if (!at)
|
|
19035
|
+
throw new MhError("invalid_input", "missing version cutoff");
|
|
19036
|
+
const changes = rowChanges(db2, "properties", id).filter((c) => c.hlc <= at);
|
|
19037
|
+
if (!changes.length)
|
|
19038
|
+
throw new MhError("not_found", `no version of property ${id} at or before ${at}`);
|
|
19039
|
+
const regs = registersAt(changes, at);
|
|
19040
|
+
const reg = (col) => {
|
|
19041
|
+
const r = regs.get(col);
|
|
19042
|
+
return r?.value == null ? undefined : JSON.parse(r.value);
|
|
19043
|
+
};
|
|
19044
|
+
return {
|
|
19045
|
+
id,
|
|
19046
|
+
database_id: reg("database_id") ?? null,
|
|
19047
|
+
name: reg("name") ?? "",
|
|
19048
|
+
type: reg("type") ?? "text",
|
|
19049
|
+
config: reg("config") ?? null,
|
|
19050
|
+
position: reg("position") ?? null,
|
|
19051
|
+
deleted: regs.get("__deleted") ? flagSet(regs.get("__deleted").value) : false,
|
|
19052
|
+
version: changes[changes.length - 1].hlc
|
|
19053
|
+
};
|
|
19054
|
+
}
|
|
19055
|
+
var revertProperty = grouped(function revertProperty2(db2, id, to) {
|
|
19056
|
+
const cur = db2.query("SELECT database_id, name, type, config, position, __deleted FROM properties WHERE id = ?").get(id);
|
|
19057
|
+
if (!cur)
|
|
19058
|
+
throw new MhError("not_found", `no such property: ${id}`);
|
|
19059
|
+
const past = propertyAtVersion(db2, id, to);
|
|
19060
|
+
if (past.deleted)
|
|
19061
|
+
throw new MhError("invalid_input", "target version is a deleted state; use prop remove instead");
|
|
19062
|
+
const curDef = {
|
|
19063
|
+
name: cur.name,
|
|
19064
|
+
type: cur.type,
|
|
19065
|
+
config: cur.config ? JSON.parse(cur.config) : null,
|
|
19066
|
+
position: cur.position
|
|
19067
|
+
};
|
|
19068
|
+
const pastDef = {
|
|
19069
|
+
name: past.name,
|
|
19070
|
+
type: past.type,
|
|
19071
|
+
config: past.config,
|
|
19072
|
+
position: past.position
|
|
19073
|
+
};
|
|
19074
|
+
const fields = [];
|
|
19075
|
+
for (const col of PROP_DEF_COLS) {
|
|
19076
|
+
if (JSON.stringify(pastDef[col] ?? null) !== JSON.stringify(curDef[col] ?? null)) {
|
|
19077
|
+
emit(db2, "properties", id, col, pastDef[col]);
|
|
19078
|
+
fields.push(col);
|
|
19079
|
+
}
|
|
19080
|
+
}
|
|
19081
|
+
const undeleted = cur.__deleted !== 0;
|
|
19082
|
+
if (undeleted)
|
|
19083
|
+
emit(db2, "properties", id, "__deleted", 0);
|
|
19084
|
+
const schemaTxns = new Set(db2.query("SELECT DISTINCT txn FROM crdt_changes WHERE dataset = 'properties' AND row_id = ? AND hlc > ? AND txn IS NOT NULL").all(id, to).map((r) => r.txn));
|
|
19085
|
+
const cellChanges = db2.query("SELECT row_id, hlc, value, txn FROM crdt_changes WHERE dataset = 'records' AND col = ? ORDER BY row_id, hlc").all(id);
|
|
19086
|
+
const isCascade = (txn) => txn !== null && (schemaTxns.has(txn) || txn.startsWith("repair:"));
|
|
19087
|
+
let restored = 0;
|
|
19088
|
+
let skipped = 0;
|
|
19089
|
+
let i = 0;
|
|
19090
|
+
while (i < cellChanges.length) {
|
|
19091
|
+
const rowId = cellChanges[i].row_id;
|
|
19092
|
+
let winner = cellChanges[i];
|
|
19093
|
+
let lastUser = null;
|
|
19094
|
+
for (;i < cellChanges.length && cellChanges[i].row_id === rowId; i++) {
|
|
19095
|
+
winner = cellChanges[i];
|
|
19096
|
+
if (!isCascade(winner.txn))
|
|
19097
|
+
lastUser = winner;
|
|
19098
|
+
}
|
|
19099
|
+
if (winner.hlc <= to)
|
|
19100
|
+
continue;
|
|
19101
|
+
if (!isCascade(winner.txn)) {
|
|
19102
|
+
skipped++;
|
|
19103
|
+
continue;
|
|
19104
|
+
}
|
|
19105
|
+
const target = lastUser ? lastUser.value : null;
|
|
19106
|
+
if (target === winner.value)
|
|
19107
|
+
continue;
|
|
19108
|
+
emit(db2, "records", rowId, id, target === null ? undefined : JSON.parse(target));
|
|
19109
|
+
restored++;
|
|
19110
|
+
}
|
|
19111
|
+
return {
|
|
19112
|
+
id,
|
|
19113
|
+
changed: fields.length > 0 || undeleted || restored > 0,
|
|
19114
|
+
fields,
|
|
19115
|
+
restored_cells: restored,
|
|
19116
|
+
skipped_cells: skipped,
|
|
19117
|
+
undeleted,
|
|
19118
|
+
restored: past.version
|
|
19119
|
+
};
|
|
19120
|
+
}, "revert");
|
|
19121
|
+
function recordFieldHistory(db2, id, propId) {
|
|
19122
|
+
const rows = db2.query("SELECT hlc, node_id, value FROM crdt_changes WHERE dataset = 'records' AND row_id = ? AND col = ? ORDER BY hlc DESC").all(id, propId);
|
|
19123
|
+
return rows.map((r) => ({
|
|
19124
|
+
version: r.hlc,
|
|
19125
|
+
at: hlcIso(r.hlc),
|
|
19126
|
+
node_id: r.node_id,
|
|
19127
|
+
value: r.value === null ? undefined : JSON.parse(r.value),
|
|
19128
|
+
cleared: r.value === null
|
|
19129
|
+
}));
|
|
18564
19130
|
}
|
|
18565
19131
|
|
|
18566
19132
|
// src/webui/server/routes.ts
|
|
19133
|
+
init_node();
|
|
18567
19134
|
var DatabaseSchema = exports_external.object({
|
|
18568
19135
|
id: exports_external.string(),
|
|
18569
19136
|
name: exports_external.string(),
|
|
@@ -18581,7 +19148,8 @@ var PropertySchema = exports_external.object({
|
|
|
18581
19148
|
var RecordSchema = exports_external.object({
|
|
18582
19149
|
id: exports_external.string(),
|
|
18583
19150
|
database_id: exports_external.string(),
|
|
18584
|
-
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.')
|
|
19151
|
+
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. Lossy when two properties share a name \u2014 prefer `cells` then.'),
|
|
19152
|
+
cells: exports_external.record(exports_external.string(), exports_external.any()).describe("Field values keyed by property id \u2014 lossless under duplicate property names. Writes accept property ids as keys too.")
|
|
18585
19153
|
});
|
|
18586
19154
|
var MoveRecordReq = exports_external.object({
|
|
18587
19155
|
target: exports_external.string(),
|
|
@@ -18616,6 +19184,10 @@ var UpdateDatabaseReq = exports_external.object({
|
|
|
18616
19184
|
name: exports_external.string().optional(),
|
|
18617
19185
|
icon: exports_external.string().nullable().optional()
|
|
18618
19186
|
});
|
|
19187
|
+
var DuplicateDatabaseReq = exports_external.object({
|
|
19188
|
+
name: exports_external.string().optional().describe("Name for the copy (defaults to the source name)"),
|
|
19189
|
+
icon: exports_external.string().optional()
|
|
19190
|
+
});
|
|
18619
19191
|
var CreatePropertyReq = exports_external.object({
|
|
18620
19192
|
db: exports_external.string(),
|
|
18621
19193
|
name: exports_external.string(),
|
|
@@ -18642,10 +19214,15 @@ var UpdateDocumentReq = exports_external.object({
|
|
|
18642
19214
|
parent_id: exports_external.string().nullable().optional(),
|
|
18643
19215
|
if_match: exports_external.string().optional()
|
|
18644
19216
|
});
|
|
19217
|
+
var DuplicateDocumentReq = exports_external.object({
|
|
19218
|
+
title: exports_external.string().optional().describe("Title for the copy (defaults to the source title)"),
|
|
19219
|
+
parent_id: exports_external.string().nullable().optional()
|
|
19220
|
+
});
|
|
18645
19221
|
var RevisionBase = exports_external.object({
|
|
18646
19222
|
version: exports_external.string(),
|
|
18647
19223
|
at: exports_external.string().describe("Wall-clock time of the revision (ISO 8601)"),
|
|
18648
19224
|
node_id: exports_external.string(),
|
|
19225
|
+
kind: exports_external.enum(["user", "repair", "revert"]).describe("Source of the revision"),
|
|
18649
19226
|
changes: exports_external.number(),
|
|
18650
19227
|
created: exports_external.boolean(),
|
|
18651
19228
|
deleted: exports_external.boolean()
|
|
@@ -18659,6 +19236,15 @@ var RecordRevisionSchema = RevisionBase.extend({
|
|
|
18659
19236
|
fields: exports_external.array(exports_external.string()).describe("Property ids of the cells written"),
|
|
18660
19237
|
moved: exports_external.boolean()
|
|
18661
19238
|
});
|
|
19239
|
+
var DatabaseActivityEntrySchema = RecordRevisionSchema.extend({
|
|
19240
|
+
record_id: exports_external.string(),
|
|
19241
|
+
record_title: exports_external.string().nullable().describe("Title-property value as of this revision (deleted records keep their last title)"),
|
|
19242
|
+
diffs: exports_external.array(exports_external.object({
|
|
19243
|
+
prop: exports_external.string(),
|
|
19244
|
+
before: exports_external.any().optional(),
|
|
19245
|
+
after: exports_external.any().optional()
|
|
19246
|
+
})).describe("Value-level cell changes; a missing side means the cell did not exist")
|
|
19247
|
+
});
|
|
18662
19248
|
var DocumentVersionStateSchema = exports_external.object({
|
|
18663
19249
|
id: exports_external.string(),
|
|
18664
19250
|
title: exports_external.string(),
|
|
@@ -18677,7 +19263,8 @@ var RevertDocResultSchema = exports_external.object({
|
|
|
18677
19263
|
id: exports_external.string(),
|
|
18678
19264
|
changed: exports_external.boolean(),
|
|
18679
19265
|
restored: exports_external.string(),
|
|
18680
|
-
version: exports_external.string()
|
|
19266
|
+
version: exports_external.string(),
|
|
19267
|
+
undeleted: exports_external.boolean()
|
|
18681
19268
|
});
|
|
18682
19269
|
var RevertRecordResultSchema = exports_external.object({
|
|
18683
19270
|
id: exports_external.string(),
|
|
@@ -18686,6 +19273,41 @@ var RevertRecordResultSchema = exports_external.object({
|
|
|
18686
19273
|
undeleted: exports_external.boolean(),
|
|
18687
19274
|
restored: exports_external.string()
|
|
18688
19275
|
});
|
|
19276
|
+
var PropertyRevisionSchema = RevisionBase.extend({
|
|
19277
|
+
fields: exports_external.array(exports_external.string()).describe("Definition columns touched (name/type/config/position)"),
|
|
19278
|
+
cells_cleared: exports_external.number()
|
|
19279
|
+
});
|
|
19280
|
+
var RevertPropertyReq = exports_external.object({
|
|
19281
|
+
to: exports_external.string().describe("Version token from /api/property/history")
|
|
19282
|
+
});
|
|
19283
|
+
var RevertPropertyResultSchema = exports_external.object({
|
|
19284
|
+
id: exports_external.string(),
|
|
19285
|
+
changed: exports_external.boolean(),
|
|
19286
|
+
fields: exports_external.array(exports_external.string()),
|
|
19287
|
+
restored_cells: exports_external.number(),
|
|
19288
|
+
skipped_cells: exports_external.number(),
|
|
19289
|
+
undeleted: exports_external.boolean(),
|
|
19290
|
+
restored: exports_external.string()
|
|
19291
|
+
});
|
|
19292
|
+
var RecordVersionStateSchema = exports_external.object({
|
|
19293
|
+
id: exports_external.string(),
|
|
19294
|
+
database_id: exports_external.string().nullable(),
|
|
19295
|
+
deleted: exports_external.boolean(),
|
|
19296
|
+
data: exports_external.record(exports_external.string(), exports_external.any()).describe("Cells at that version, keyed by property id"),
|
|
19297
|
+
version: exports_external.string()
|
|
19298
|
+
});
|
|
19299
|
+
var FieldHistoryEntrySchema = exports_external.object({
|
|
19300
|
+
version: exports_external.string(),
|
|
19301
|
+
at: exports_external.string(),
|
|
19302
|
+
node_id: exports_external.string(),
|
|
19303
|
+
value: exports_external.any().optional(),
|
|
19304
|
+
cleared: exports_external.boolean()
|
|
19305
|
+
});
|
|
19306
|
+
var NodeInfoSchema = exports_external.object({
|
|
19307
|
+
node_id: exports_external.string(),
|
|
19308
|
+
label: exports_external.string().nullable().describe("Peer label from pairing; null when unnamed"),
|
|
19309
|
+
self: exports_external.boolean().describe("True for the node serving this request")
|
|
19310
|
+
});
|
|
18689
19311
|
function need3(req, key) {
|
|
18690
19312
|
const v = new URL(req.url).searchParams.get(key);
|
|
18691
19313
|
if (!v)
|
|
@@ -18745,6 +19367,17 @@ var webuiRoutes = [
|
|
|
18745
19367
|
return updateDatabase(db2, need3(req, "id"), body);
|
|
18746
19368
|
})
|
|
18747
19369
|
},
|
|
19370
|
+
{
|
|
19371
|
+
method: "POST",
|
|
19372
|
+
path: "/api/database/duplicate",
|
|
19373
|
+
summary: "Copy a database whole \u2014 schema and all records. Query: ?id=<id>",
|
|
19374
|
+
request: DuplicateDatabaseReq,
|
|
19375
|
+
response: DatabaseSchema,
|
|
19376
|
+
handler: handle3(async (req, { db: db2 }) => {
|
|
19377
|
+
const body = await req.json().catch(() => ({}));
|
|
19378
|
+
return duplicateDatabase(db2, need3(req, "id"), body);
|
|
19379
|
+
})
|
|
19380
|
+
},
|
|
18748
19381
|
{
|
|
18749
19382
|
method: "DELETE",
|
|
18750
19383
|
path: "/api/database",
|
|
@@ -18752,6 +19385,18 @@ var webuiRoutes = [
|
|
|
18752
19385
|
response: OkSchema2,
|
|
18753
19386
|
handler: handle3((req, { db: db2 }) => ({ ok: deleteDatabase(db2, need3(req, "id")) }))
|
|
18754
19387
|
},
|
|
19388
|
+
{
|
|
19389
|
+
method: "GET",
|
|
19390
|
+
path: "/api/database/activity",
|
|
19391
|
+
summary: "Recent changes across all records of a database, newest first. Query: ?db=<id>&limit=<n>",
|
|
19392
|
+
response: exports_external.array(DatabaseActivityEntrySchema),
|
|
19393
|
+
handler: handle3((req, { db: db2 }) => {
|
|
19394
|
+
const limit = opt(req, "limit");
|
|
19395
|
+
return listDatabaseActivity(db2, need3(req, "db"), {
|
|
19396
|
+
limit: limit ? Number(limit) : undefined
|
|
19397
|
+
});
|
|
19398
|
+
})
|
|
19399
|
+
},
|
|
18755
19400
|
{
|
|
18756
19401
|
method: "GET",
|
|
18757
19402
|
path: "/api/properties",
|
|
@@ -18799,6 +19444,24 @@ var webuiRoutes = [
|
|
|
18799
19444
|
response: OkSchema2,
|
|
18800
19445
|
handler: handle3((req, { db: db2 }) => ({ ok: removeProperty(db2, need3(req, "id")) }))
|
|
18801
19446
|
},
|
|
19447
|
+
{
|
|
19448
|
+
method: "GET",
|
|
19449
|
+
path: "/api/property/history",
|
|
19450
|
+
summary: "List a property's definition revisions, newest first. Query: ?id=<id>",
|
|
19451
|
+
response: exports_external.array(PropertyRevisionSchema),
|
|
19452
|
+
handler: handle3((req, { db: db2 }) => listPropertyRevisions(db2, need3(req, "id")))
|
|
19453
|
+
},
|
|
19454
|
+
{
|
|
19455
|
+
method: "POST",
|
|
19456
|
+
path: "/api/property/revert",
|
|
19457
|
+
summary: "Schema rollback: restore a property's definition and the cells its type-change/removal cleared (user edits since are kept). Query: ?id=<id>",
|
|
19458
|
+
request: RevertPropertyReq,
|
|
19459
|
+
response: RevertPropertyResultSchema,
|
|
19460
|
+
handler: handle3(async (req, { db: db2 }) => {
|
|
19461
|
+
const body = await req.json();
|
|
19462
|
+
return revertProperty(db2, need3(req, "id"), body.to);
|
|
19463
|
+
})
|
|
19464
|
+
},
|
|
18802
19465
|
{
|
|
18803
19466
|
method: "GET",
|
|
18804
19467
|
path: "/api/records",
|
|
@@ -18865,6 +19528,20 @@ var webuiRoutes = [
|
|
|
18865
19528
|
response: exports_external.array(RecordRevisionSchema),
|
|
18866
19529
|
handler: handle3((req, { db: db2 }) => listRecordRevisions(db2, need3(req, "id")))
|
|
18867
19530
|
},
|
|
19531
|
+
{
|
|
19532
|
+
method: "GET",
|
|
19533
|
+
path: "/api/record/at",
|
|
19534
|
+
summary: "A record's cells as of a past version. Query: ?id=<id>&version=<token>",
|
|
19535
|
+
response: RecordVersionStateSchema,
|
|
19536
|
+
handler: handle3((req, { db: db2 }) => recordAtVersion(db2, need3(req, "id"), need3(req, "version")))
|
|
19537
|
+
},
|
|
19538
|
+
{
|
|
19539
|
+
method: "GET",
|
|
19540
|
+
path: "/api/record/field-history",
|
|
19541
|
+
summary: "The write trail of one record cell, newest first. Query: ?id=<id>&prop=<propertyId>",
|
|
19542
|
+
response: exports_external.array(FieldHistoryEntrySchema),
|
|
19543
|
+
handler: handle3((req, { db: db2 }) => recordFieldHistory(db2, need3(req, "id"), need3(req, "prop")))
|
|
19544
|
+
},
|
|
18868
19545
|
{
|
|
18869
19546
|
method: "POST",
|
|
18870
19547
|
path: "/api/record/revert",
|
|
@@ -18927,6 +19604,21 @@ var webuiRoutes = [
|
|
|
18927
19604
|
return { ...doc2, version: documentVersion(db2, doc2.id) };
|
|
18928
19605
|
})
|
|
18929
19606
|
},
|
|
19607
|
+
{
|
|
19608
|
+
method: "POST",
|
|
19609
|
+
path: "/api/document/duplicate",
|
|
19610
|
+
summary: "Copy a document \u2014 title and every block \u2014 next to the source. Query: ?id=<id>",
|
|
19611
|
+
request: DuplicateDocumentReq,
|
|
19612
|
+
response: DocumentSchema,
|
|
19613
|
+
handler: handle3(async (req, { db: db2 }) => {
|
|
19614
|
+
const body = await req.json().catch(() => ({}));
|
|
19615
|
+
const doc2 = duplicateDocument(db2, need3(req, "id"), {
|
|
19616
|
+
title: body.title,
|
|
19617
|
+
parentId: body.parent_id
|
|
19618
|
+
});
|
|
19619
|
+
return { ...doc2, version: documentVersion(db2, doc2.id) };
|
|
19620
|
+
})
|
|
19621
|
+
},
|
|
18930
19622
|
{
|
|
18931
19623
|
method: "PATCH",
|
|
18932
19624
|
path: "/api/document/move",
|
|
@@ -18970,6 +19662,20 @@ var webuiRoutes = [
|
|
|
18970
19662
|
response: OkSchema2,
|
|
18971
19663
|
handler: handle3((req, { db: db2 }) => ({ ok: deleteDocument(db2, need3(req, "id")) }))
|
|
18972
19664
|
},
|
|
19665
|
+
{
|
|
19666
|
+
method: "GET",
|
|
19667
|
+
path: "/api/nodes",
|
|
19668
|
+
summary: "Known nodes for display: this device plus paired peers with their labels",
|
|
19669
|
+
response: exports_external.array(NodeInfoSchema),
|
|
19670
|
+
handler: handle3((_req, { db: db2 }) => {
|
|
19671
|
+
const self = getNodeId(db2);
|
|
19672
|
+
const peers = db2.query("SELECT node_id, label FROM peers WHERE node_id IS NOT NULL AND node_id <> '' GROUP BY node_id").all();
|
|
19673
|
+
return [
|
|
19674
|
+
{ node_id: self, label: null, self: true },
|
|
19675
|
+
...peers.filter((p) => p.node_id !== self).map((p) => ({ ...p, self: false }))
|
|
19676
|
+
];
|
|
19677
|
+
})
|
|
19678
|
+
},
|
|
18973
19679
|
{
|
|
18974
19680
|
method: "GET",
|
|
18975
19681
|
path: "/api/search",
|
|
@@ -18982,6 +19688,7 @@ var webuiRoutes = [
|
|
|
18982
19688
|
}
|
|
18983
19689
|
];
|
|
18984
19690
|
export {
|
|
19691
|
+
withChangeGroup,
|
|
18985
19692
|
webuiRoutes,
|
|
18986
19693
|
warmWebui,
|
|
18987
19694
|
updateSite,
|
|
@@ -19022,6 +19729,7 @@ export {
|
|
|
19022
19729
|
moveDocument,
|
|
19023
19730
|
migrateRecords,
|
|
19024
19731
|
migratePeers,
|
|
19732
|
+
migrateOplog,
|
|
19025
19733
|
migrateDocuments,
|
|
19026
19734
|
migrateDocBlocks,
|
|
19027
19735
|
metahubHome,
|
|
@@ -19035,15 +19743,18 @@ export {
|
|
|
19035
19743
|
listDatabases,
|
|
19036
19744
|
keysBetween,
|
|
19037
19745
|
keyBetween,
|
|
19746
|
+
initSchema,
|
|
19038
19747
|
ingest,
|
|
19039
19748
|
inferContentType,
|
|
19040
19749
|
idKind,
|
|
19041
19750
|
hasIndex,
|
|
19751
|
+
grouped,
|
|
19042
19752
|
getSiteByName,
|
|
19043
19753
|
getSite,
|
|
19044
19754
|
getRecord,
|
|
19045
19755
|
getProperty,
|
|
19046
19756
|
getNodeId,
|
|
19757
|
+
getFileRow,
|
|
19047
19758
|
getFileForServe,
|
|
19048
19759
|
getDocument,
|
|
19049
19760
|
getDatabase,
|
|
@@ -19057,6 +19768,8 @@ export {
|
|
|
19057
19768
|
emitFields,
|
|
19058
19769
|
emit,
|
|
19059
19770
|
editDocument,
|
|
19771
|
+
duplicateDocument,
|
|
19772
|
+
duplicateDatabase,
|
|
19060
19773
|
documentVersion,
|
|
19061
19774
|
deleteSite,
|
|
19062
19775
|
deleteRecord,
|
|
@@ -19081,16 +19794,24 @@ export {
|
|
|
19081
19794
|
SyncResponseSchema,
|
|
19082
19795
|
SyncRequestSchema,
|
|
19083
19796
|
SYNC_PATH,
|
|
19797
|
+
SITE_FILE_SELECT,
|
|
19798
|
+
SITE_FILE_COLS,
|
|
19799
|
+
SITE_COLS,
|
|
19084
19800
|
RENEW_PATH,
|
|
19085
19801
|
PortInUseError,
|
|
19086
19802
|
PairResponseSchema,
|
|
19087
19803
|
PairRequestSchema,
|
|
19088
19804
|
PROP_TYPES,
|
|
19805
|
+
PROPERTY_COLS,
|
|
19089
19806
|
PAIR_PATH,
|
|
19090
19807
|
MhError,
|
|
19091
19808
|
HealthResponseSchema,
|
|
19092
19809
|
HEALTH_PATH,
|
|
19093
|
-
|
|
19810
|
+
DOCUMENT_COLS,
|
|
19811
|
+
DATABASE_COLS,
|
|
19812
|
+
ChangeSchema,
|
|
19813
|
+
CHANGE_SELECT,
|
|
19814
|
+
CHANGE_COLS
|
|
19094
19815
|
};
|
|
19095
19816
|
|
|
19096
|
-
//# debugId=
|
|
19817
|
+
//# debugId=7F98FB2AC8CA296264756E2164756E21
|