@tensorix/metahub 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/cli.js +2447 -1619
  2. package/dist/cli.js.map +26 -22
  3. package/dist/core/audit.d.ts +97 -0
  4. package/dist/core/audit.d.ts.map +1 -0
  5. package/dist/core/crdt.d.ts +13 -1
  6. package/dist/core/crdt.d.ts.map +1 -1
  7. package/dist/core/databases.d.ts.map +1 -1
  8. package/dist/core/documents.d.ts +6 -0
  9. package/dist/core/documents.d.ts.map +1 -1
  10. package/dist/core/grants-core.d.ts.map +1 -1
  11. package/dist/core/history.d.ts +31 -0
  12. package/dist/core/history.d.ts.map +1 -1
  13. package/dist/core/indexing.d.ts +1 -1
  14. package/dist/core/integrity.d.ts +1 -1
  15. package/dist/core/integrity.d.ts.map +1 -1
  16. package/dist/core/properties.d.ts +9 -1
  17. package/dist/core/properties.d.ts.map +1 -1
  18. package/dist/core/records.d.ts +5 -0
  19. package/dist/core/records.d.ts.map +1 -1
  20. package/dist/core/relation-cells.d.ts +23 -0
  21. package/dist/core/relation-cells.d.ts.map +1 -0
  22. package/dist/core/resolve.d.ts +4 -0
  23. package/dist/core/resolve.d.ts.map +1 -1
  24. package/dist/core/sync/files.d.ts.map +1 -1
  25. package/dist/core/sync/server.d.ts.map +1 -1
  26. package/dist/core/sync/share-serve.d.ts.map +1 -1
  27. package/dist/db-worker.js +100 -98
  28. package/dist/edge-worker.js +158 -32
  29. package/dist/index.js +2372 -1382
  30. package/dist/index.js.map +18 -16
  31. package/dist/metahub-sdk.js +7 -7
  32. package/dist/mh-runtime.js +1 -1
  33. package/dist/sw.js +7 -7
  34. package/dist/webui/server/assets.d.ts +3 -1
  35. package/dist/webui/server/assets.d.ts.map +1 -1
  36. package/dist/webui/server/changes-route.d.ts +7 -0
  37. package/dist/webui/server/changes-route.d.ts.map +1 -0
  38. package/dist/webui/server/routes.d.ts.map +1 -1
  39. package/dist/webui-fmt-clang.js +1 -1
  40. package/dist/webui-fmt-gofmt.js +1 -1
  41. package/dist/webui-fmt-lua.js +2 -2
  42. package/dist/webui-fmt-ruff.js +2 -2
  43. package/dist/webui-fmt-sh.js +8 -8
  44. package/dist/webui-fmt-taplo.js +2 -2
  45. package/dist/webui-fmt.js +346 -346
  46. package/dist/webui.js +186 -176
  47. package/package.json +2 -2
@@ -327,6 +327,7 @@ function intentSubmissionExpired(submittedAt, now = Date.now()) {
327
327
  var CHANGE_COLS = ["hlc", "node_id", "dataset", "row_id", "col", "value", "txn"];
328
328
  var CHANGE_SELECT = CHANGE_COLS.join(", ");
329
329
  var currentTxn = null;
330
+ var currentActor = null;
330
331
  var currentNodeOverride = null;
331
332
  function withNodeId(node, fn) {
332
333
  const prev = currentNodeOverride;
@@ -340,7 +341,7 @@ function withNodeId(node, fn) {
340
341
  function withChangeGroup(label, fn) {
341
342
  if (currentTxn !== null)
342
343
  return fn();
343
- currentTxn = (label ? label + ":" : "") + randomSuffix(8);
344
+ currentTxn = (currentActor ? currentActor + "/" : "") + (label ? label + ":" : "") + randomSuffix(8);
344
345
  try {
345
346
  return fn();
346
347
  } finally {
@@ -565,7 +566,7 @@ function ensurePropIndex(db, databaseId, propId) {
565
566
  function maybeAutoIndex(db, databaseId, prop) {
566
567
  if (hasIndex(db, databaseId, prop.id))
567
568
  return;
568
- if (prop.type !== "relation") {
569
+ if (prop.type !== "relation" && prop.type !== "doc") {
569
570
  const row = db.query("SELECT COUNT(*) AS c FROM records WHERE database_id = ? AND __deleted = 0").get(databaseId);
570
571
  if (row.c < AUTO_INDEX_ROW_THRESHOLD)
571
572
  return;
@@ -582,6 +583,7 @@ var PROP_TYPES = new Set([
582
583
  "multi_select",
583
584
  "date",
584
585
  "relation",
586
+ "doc",
585
587
  "url"
586
588
  ]);
587
589
  var PROPERTY_COLS = ["id", "database_id", "name", "type", "config", "position"];
@@ -614,7 +616,7 @@ var addProperty = grouped(function addProperty2(db, databaseId, opts) {
614
616
  emit(db, "properties", id, "type", opts.type);
615
617
  emit(db, "properties", id, "config", config);
616
618
  emit(db, "properties", id, "position", opts.position ?? nextPosition(db, databaseId));
617
- if (opts.type === "relation" || opts.indexed === true)
619
+ if (opts.type === "relation" || opts.type === "doc" || opts.indexed === true)
618
620
  ensurePropIndex(db, databaseId, id);
619
621
  return getProperty(db, id);
620
622
  });
@@ -639,16 +641,25 @@ var updateProperty = grouped(function updateProperty2(db, id, fields) {
639
641
  if (fields.type !== undefined && !PROP_TYPES.has(fields.type))
640
642
  throw new MhError("invalid_input", `unknown property type: ${fields.type}`);
641
643
  const effectiveType = fields.type ?? cur.type;
642
- if (fields.config !== undefined)
643
- validateConfig(effectiveType, fields.config);
644
- else if (typeChanged)
645
- validateConfig(fields.type, undefined);
644
+ let mergedConfig;
645
+ if (fields.config !== undefined) {
646
+ const merged = { ...cur.config ?? {} };
647
+ for (const [k, v] of Object.entries(fields.config)) {
648
+ if (v === null)
649
+ delete merged[k];
650
+ else
651
+ merged[k] = v;
652
+ }
653
+ mergedConfig = merged;
654
+ validateConfig(effectiveType, mergedConfig);
655
+ } else if (typeChanged)
656
+ validateConfig(fields.type, cur.config ?? undefined);
646
657
  if (fields.name !== undefined)
647
658
  emit(db, "properties", id, "name", fields.name);
648
659
  if (fields.type !== undefined)
649
660
  emit(db, "properties", id, "type", fields.type);
650
- if (fields.config !== undefined)
651
- emit(db, "properties", id, "config", fields.config);
661
+ if (mergedConfig !== undefined)
662
+ emit(db, "properties", id, "config", mergedConfig);
652
663
  if (fields.position !== undefined)
653
664
  emit(db, "properties", id, "position", fields.position);
654
665
  if (typeChanged) {
@@ -658,6 +669,75 @@ var updateProperty = grouped(function updateProperty2(db, id, fields) {
658
669
  }
659
670
  return getProperty(db, id);
660
671
  });
672
+ function getSelectProperty(db, id) {
673
+ const prop = getProperty(db, id);
674
+ if (!prop)
675
+ throw new MhError("not_found", `no such property: ${id}`);
676
+ if (prop.type !== "select" && prop.type !== "multi_select")
677
+ throw new MhError("invalid_input", `${prop.type} property has no options`);
678
+ return { prop, options: prop.config?.options ?? [] };
679
+ }
680
+ var renameSelectOption = grouped(function renameSelectOption2(db, id, from, to) {
681
+ const { prop, options } = getSelectProperty(db, id);
682
+ to = to.trim();
683
+ if (!to)
684
+ throw new MhError("invalid_input", "option name must not be empty");
685
+ if (!options.includes(from))
686
+ throw new MhError("not_found", `no such option: ${from}`);
687
+ if (to === from)
688
+ return { property: prop, renamed: 0 };
689
+ if (options.includes(to))
690
+ throw new MhError("conflict", `option already exists: ${to}`);
691
+ emit(db, "properties", id, "config", {
692
+ ...prop.config ?? {},
693
+ options: options.map((o) => o === from ? to : o)
694
+ });
695
+ let renamed = 0;
696
+ if (prop.type === "select") {
697
+ const rows = db.query("SELECT id FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? = ?").all(prop.database_id, id, from);
698
+ for (const r of rows)
699
+ emit(db, "records", r.id, id, to);
700
+ renamed = rows.length;
701
+ } else {
702
+ const rows = db.query("SELECT id, data ->> ? AS v FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? IS NOT NULL").all(id, prop.database_id, id);
703
+ for (const r of rows) {
704
+ const vals = JSON.parse(r.v);
705
+ if (!Array.isArray(vals) || !vals.includes(from))
706
+ continue;
707
+ emit(db, "records", r.id, id, [...new Set(vals.map((v) => v === from ? to : v))]);
708
+ renamed++;
709
+ }
710
+ }
711
+ return { property: getProperty(db, id), renamed };
712
+ });
713
+ var removeSelectOption = grouped(function removeSelectOption2(db, id, name) {
714
+ const { prop, options } = getSelectProperty(db, id);
715
+ if (!options.includes(name))
716
+ throw new MhError("not_found", `no such option: ${name}`);
717
+ if (options.length === 1)
718
+ throw new MhError("invalid_input", "cannot remove the last option; delete the property instead");
719
+ emit(db, "properties", id, "config", {
720
+ ...prop.config ?? {},
721
+ options: options.filter((o) => o !== name)
722
+ });
723
+ let cleared = 0;
724
+ if (prop.type === "select") {
725
+ const rows = db.query("SELECT id FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? = ?").all(prop.database_id, id, name);
726
+ for (const r of rows)
727
+ emit(db, "records", r.id, id, null);
728
+ cleared = rows.length;
729
+ } else {
730
+ const rows = db.query("SELECT id, data ->> ? AS v FROM records WHERE database_id = ? AND __deleted = 0 AND data ->> ? IS NOT NULL").all(id, prop.database_id, id);
731
+ for (const r of rows) {
732
+ const vals = JSON.parse(r.v);
733
+ if (!Array.isArray(vals) || !vals.includes(name))
734
+ continue;
735
+ emit(db, "records", r.id, id, vals.filter((v) => v !== name));
736
+ cleared++;
737
+ }
738
+ }
739
+ return { property: getProperty(db, id), cleared };
740
+ });
661
741
  var removeProperty = grouped(function removeProperty2(db, id) {
662
742
  const prop = getProperty(db, id);
663
743
  if (!prop)
@@ -808,6 +888,20 @@ function resolveRelation(db, prop, value) {
808
888
  }
809
889
  throw new MhError("ambiguous", `${prop.name}: ambiguous relation "${value}" (${cands.length} matches); use a full id`);
810
890
  }
891
+ function resolveDoc(db, prop, value) {
892
+ const cands = resolveCandidates(db, value, { kind: "doc" });
893
+ const exact = cands.find((c) => c.id === value);
894
+ if (exact)
895
+ return exact.id;
896
+ if (cands.length === 1)
897
+ return cands[0].id;
898
+ if (cands.length === 0) {
899
+ if (idKind(value) === "doc")
900
+ return value;
901
+ throw new MhError("not_found", `${prop.name}: no such document: ${value}`);
902
+ }
903
+ throw new MhError("ambiguous", `${prop.name}: ambiguous document "${value}" (${cands.length} matches); use a full id`);
904
+ }
811
905
  function coerce(db, prop, value) {
812
906
  switch (prop.type) {
813
907
  case "text":
@@ -856,6 +950,10 @@ function coerce(db, prop, value) {
856
950
  const arr = value === null ? [] : Array.isArray(value) ? value : [value];
857
951
  return arr.map((v) => resolveRelation(db, prop, String(v)));
858
952
  }
953
+ case "doc": {
954
+ const arr = value === null ? [] : Array.isArray(value) ? value : [value];
955
+ return arr.map((v) => resolveDoc(db, prop, String(v)));
956
+ }
859
957
  }
860
958
  }
861
959
  function propsByName(props) {
@@ -1069,11 +1167,16 @@ var moveRecord = grouped(function moveRecord2(db, id, targetId, where) {
1069
1167
  return getRecord(db, id);
1070
1168
  });
1071
1169
  var updateRecordPrepared = grouped(function updateRecordPrepared2(db, id, cells) {
1072
- const rec = db.query("SELECT id, database_id FROM records WHERE id = ? AND __deleted = 0").get(id);
1170
+ const rec = db.query("SELECT id, database_id, data FROM records WHERE id = ? AND __deleted = 0").get(id);
1073
1171
  if (!rec)
1074
1172
  throw new MhError("not_found", `no such record: ${id}`);
1075
- for (const { prop, value } of cells)
1076
- emit(db, "records", id, prop.id, value);
1173
+ const cur = JSON.parse(rec.data || "{}");
1174
+ for (const { prop, value } of cells) {
1175
+ const exists = Object.hasOwn(cur, prop.id);
1176
+ const same = value === undefined ? !exists : exists && JSON.stringify(value) === JSON.stringify(cur[prop.id]);
1177
+ if (!same)
1178
+ emit(db, "records", id, prop.id, value);
1179
+ }
1077
1180
  return getRecord(db, id);
1078
1181
  });
1079
1182
  var updateRecord = grouped(function updateRecord2(db, id, data) {
@@ -1134,19 +1237,38 @@ var duplicateDatabase = grouped(function duplicateDatabase2(db, id, opts = {}) {
1134
1237
  emit(db, "databases", dup.id, "meta", meta);
1135
1238
  }
1136
1239
  const propIdMap = new Map;
1240
+ const selfRelNew = new Set;
1137
1241
  for (const p of listProperties(db, id)) {
1138
- const config = p.config?.database === id ? { ...p.config, database: dup.id } : p.config ?? undefined;
1242
+ const self = p.type === "relation" && p.config?.database === id;
1243
+ const config = self ? { ...p.config, database: dup.id } : p.config ?? undefined;
1139
1244
  const np = addProperty(db, dup.id, { name: p.name, type: p.type, config, position: p.position });
1140
1245
  propIdMap.set(p.id, np.id);
1246
+ if (self)
1247
+ selfRelNew.add(np.id);
1141
1248
  }
1142
- for (const r of listRecords(db, id)) {
1249
+ const srcRecords = listRecords(db, id);
1250
+ const recIdMap = new Map;
1251
+ for (const r of srcRecords) {
1143
1252
  const data = {};
1144
1253
  for (const [pid, v] of Object.entries(r.cells)) {
1145
1254
  const nid = propIdMap.get(pid);
1146
- if (nid)
1147
- data[nid] = v;
1255
+ if (!nid)
1256
+ continue;
1257
+ if (selfRelNew.has(nid) && Array.isArray(v) && v.length > 0)
1258
+ continue;
1259
+ data[nid] = v;
1148
1260
  }
1149
- createRecord(db, dup.id, data);
1261
+ recIdMap.set(r.id, createRecord(db, dup.id, data).id);
1262
+ }
1263
+ for (const r of srcRecords) {
1264
+ const patch = {};
1265
+ for (const [pid, v] of Object.entries(r.cells)) {
1266
+ const nid = propIdMap.get(pid);
1267
+ if (nid && selfRelNew.has(nid) && Array.isArray(v) && v.length > 0)
1268
+ patch[nid] = v.map((x) => recIdMap.get(String(x)) ?? x);
1269
+ }
1270
+ if (Object.keys(patch).length)
1271
+ updateRecord(db, recIdMap.get(r.id), patch);
1150
1272
  }
1151
1273
  return getDatabase(db, dup.id);
1152
1274
  });
@@ -1285,29 +1407,33 @@ function liveRowCount(db, databaseId) {
1285
1407
  }
1286
1408
  function assertRelationAllowed(set, principalKind, prop) {
1287
1409
  if (principalKind === "public")
1288
- throw new MhError("invalid_input", `${prop.name}: relation properties are not writable anonymously`);
1410
+ throw new MhError("invalid_input", `${prop.name}: ${prop.type} properties are not writable anonymously`);
1411
+ if (prop.type === "doc")
1412
+ throw new MhError("invalid_input", `${prop.name}: doc properties are not writable via shares (grants cannot scope documents)`);
1289
1413
  const target = prop.config?.database;
1290
1414
  if (!target || !grantFor(set, target))
1291
1415
  throw new MhError("invalid_input", `${prop.name}: relation target database is not in this share's grants`);
1292
1416
  }
1417
+ function isLiveRefValue(prop, value) {
1418
+ return (prop.type === "relation" || prop.type === "doc") && value != null && !(Array.isArray(value) && value.length === 0);
1419
+ }
1293
1420
  function assertGuestPayload(db, set, principal, database, values, opts = {}) {
1294
1421
  const limits = opts.limits ?? GUEST_LIMITS;
1295
1422
  const entries = Object.entries(values);
1296
1423
  if (entries.length > limits.maxCells)
1297
1424
  throw new MhError("invalid_input", `too many cells (max ${limits.maxCells})`);
1298
1425
  const props = listProperties(db, database.id);
1299
- const resolved = resolveData(props, values).map(({ prop, value }) => ({
1300
- prop,
1301
- value: coerce(db, prop, value)
1302
- }));
1426
+ const resolved = resolveData(props, values).map(({ prop, value }) => {
1427
+ if (isLiveRefValue(prop, value))
1428
+ assertRelationAllowed(set, principal.kind, prop);
1429
+ return { prop, value: coerce(db, prop, value) };
1430
+ });
1303
1431
  let total = 0;
1304
1432
  for (const { prop, value } of resolved) {
1305
1433
  const size = utf8Len(JSON.stringify(value ?? null));
1306
1434
  if (size > limits.maxValueBytes)
1307
1435
  throw new MhError("invalid_input", `${prop.name}: value too large (max ${limits.maxValueBytes} bytes)`);
1308
1436
  total += size;
1309
- if (prop.type === "relation" && value != null && !(Array.isArray(value) && value.length === 0))
1310
- assertRelationAllowed(set, principal.kind, prop);
1311
1437
  }
1312
1438
  if (total > limits.maxBodyBytes)
1313
1439
  throw new MhError("invalid_input", `payload too large (max ${limits.maxBodyBytes} bytes)`);
@@ -3948,13 +4074,13 @@ var edge_worker_default = {
3948
4074
  }
3949
4075
  };
3950
4076
  export {
3951
- edge_worker_default as default,
3952
- createInboxFetch,
3953
- MhRoom,
3954
- EDGE_WORKER_VERSION,
3955
- EDGE_WORKER_MARKER,
3956
- EDGE_SCHEMA_SQL,
3957
- DROP_ENVELOPE_LIMIT_BYTES,
4077
+ DROP_DEFAULT_MAX_BYTES,
3958
4078
  DROP_DEFAULT_MAX_ENVELOPES,
3959
- DROP_DEFAULT_MAX_BYTES
4079
+ DROP_ENVELOPE_LIMIT_BYTES,
4080
+ EDGE_SCHEMA_SQL,
4081
+ EDGE_WORKER_MARKER,
4082
+ EDGE_WORKER_VERSION,
4083
+ MhRoom,
4084
+ createInboxFetch,
4085
+ edge_worker_default as default
3960
4086
  };