@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.
- package/dist/cli.js +2447 -1619
- package/dist/cli.js.map +26 -22
- package/dist/core/audit.d.ts +97 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/crdt.d.ts +13 -1
- package/dist/core/crdt.d.ts.map +1 -1
- package/dist/core/databases.d.ts.map +1 -1
- package/dist/core/documents.d.ts +6 -0
- package/dist/core/documents.d.ts.map +1 -1
- package/dist/core/grants-core.d.ts.map +1 -1
- package/dist/core/history.d.ts +31 -0
- package/dist/core/history.d.ts.map +1 -1
- package/dist/core/indexing.d.ts +1 -1
- package/dist/core/integrity.d.ts +1 -1
- package/dist/core/integrity.d.ts.map +1 -1
- package/dist/core/properties.d.ts +9 -1
- package/dist/core/properties.d.ts.map +1 -1
- package/dist/core/records.d.ts +5 -0
- package/dist/core/records.d.ts.map +1 -1
- package/dist/core/relation-cells.d.ts +23 -0
- package/dist/core/relation-cells.d.ts.map +1 -0
- package/dist/core/resolve.d.ts +4 -0
- package/dist/core/resolve.d.ts.map +1 -1
- package/dist/core/sync/files.d.ts.map +1 -1
- package/dist/core/sync/server.d.ts.map +1 -1
- package/dist/core/sync/share-serve.d.ts.map +1 -1
- package/dist/db-worker.js +100 -98
- package/dist/edge-worker.js +158 -32
- package/dist/index.js +2372 -1382
- package/dist/index.js.map +18 -16
- package/dist/metahub-sdk.js +7 -7
- package/dist/mh-runtime.js +1 -1
- package/dist/sw.js +7 -7
- package/dist/webui/server/assets.d.ts +3 -1
- package/dist/webui/server/assets.d.ts.map +1 -1
- package/dist/webui/server/changes-route.d.ts +7 -0
- package/dist/webui/server/changes-route.d.ts.map +1 -0
- package/dist/webui/server/routes.d.ts.map +1 -1
- package/dist/webui-fmt-clang.js +1 -1
- package/dist/webui-fmt-gofmt.js +1 -1
- package/dist/webui-fmt-lua.js +2 -2
- package/dist/webui-fmt-ruff.js +2 -2
- package/dist/webui-fmt-sh.js +8 -8
- package/dist/webui-fmt-taplo.js +2 -2
- package/dist/webui-fmt.js +346 -346
- package/dist/webui.js +186 -176
- package/package.json +2 -2
package/dist/edge-worker.js
CHANGED
|
@@ -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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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 (
|
|
651
|
-
emit(db, "properties", id, "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
|
-
|
|
1076
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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}:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|