@tensorix/metahub 0.4.1 → 0.5.1
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 +6151 -15068
- package/dist/cli.js.map +26 -77
- 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 +1 -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 +96 -98
- package/dist/edge-worker.js +135 -87
- package/dist/index.js +11712 -20459
- package/dist/index.js.map +18 -71
- 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 +182 -178
- package/package.json +1 -1
package/dist/edge-worker.js
CHANGED
|
@@ -279,9 +279,9 @@ function lcs(a, b) {
|
|
|
279
279
|
const m = a.length;
|
|
280
280
|
const n = b.length;
|
|
281
281
|
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
282
|
-
for (let
|
|
283
|
-
for (let
|
|
284
|
-
dp[
|
|
282
|
+
for (let i = m - 1;i >= 0; i--)
|
|
283
|
+
for (let j = n - 1;j >= 0; j--)
|
|
284
|
+
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
285
285
|
const pairs = [];
|
|
286
286
|
let i = 0;
|
|
287
287
|
let j = 0;
|
|
@@ -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 {
|
|
@@ -462,8 +463,8 @@ function materialize(db, dataset, rowId, col, valueJson) {
|
|
|
462
463
|
if (dataset === "records") {
|
|
463
464
|
if (RECORD_META.has(col)) {
|
|
464
465
|
ensureRow(db, "records", rowId);
|
|
465
|
-
const
|
|
466
|
-
db.query(`UPDATE records SET "${col}" = ? WHERE id = ?`).run(encodeScalar(
|
|
466
|
+
const v = decodeMaterializeValue("records", col, valueJson);
|
|
467
|
+
db.query(`UPDATE records SET "${col}" = ? WHERE id = ?`).run(encodeScalar(v), rowId);
|
|
467
468
|
} else {
|
|
468
469
|
ensureRow(db, "records", rowId);
|
|
469
470
|
if (valueJson === null) {
|
|
@@ -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"];
|
|
@@ -601,7 +603,7 @@ function nextPosition(db, databaseId) {
|
|
|
601
603
|
const row = db.query("SELECT MAX(position) AS m FROM properties WHERE database_id = ? AND __deleted = 0").get(databaseId);
|
|
602
604
|
return (row.m ?? 0) + 1;
|
|
603
605
|
}
|
|
604
|
-
var addProperty = grouped(function
|
|
606
|
+
var addProperty = grouped(function addProperty(db, databaseId, opts) {
|
|
605
607
|
if (!getDatabase(db, databaseId))
|
|
606
608
|
throw new MhError("not_found", `no such database: ${databaseId}`);
|
|
607
609
|
if (!PROP_TYPES.has(opts.type))
|
|
@@ -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
|
});
|
|
@@ -631,7 +633,7 @@ function listProperties(db, databaseId) {
|
|
|
631
633
|
config: r.config ? JSON.parse(r.config) : null
|
|
632
634
|
}));
|
|
633
635
|
}
|
|
634
|
-
var updateProperty = grouped(function
|
|
636
|
+
var updateProperty = grouped(function updateProperty(db, id, fields) {
|
|
635
637
|
const cur = getProperty(db, id);
|
|
636
638
|
if (!cur)
|
|
637
639
|
throw new MhError("not_found", `no such property: ${id}`);
|
|
@@ -675,7 +677,7 @@ function getSelectProperty(db, id) {
|
|
|
675
677
|
throw new MhError("invalid_input", `${prop.type} property has no options`);
|
|
676
678
|
return { prop, options: prop.config?.options ?? [] };
|
|
677
679
|
}
|
|
678
|
-
var renameSelectOption = grouped(function
|
|
680
|
+
var renameSelectOption = grouped(function renameSelectOption(db, id, from, to) {
|
|
679
681
|
const { prop, options } = getSelectProperty(db, id);
|
|
680
682
|
to = to.trim();
|
|
681
683
|
if (!to)
|
|
@@ -708,7 +710,7 @@ var renameSelectOption = grouped(function renameSelectOption2(db, id, from, to)
|
|
|
708
710
|
}
|
|
709
711
|
return { property: getProperty(db, id), renamed };
|
|
710
712
|
});
|
|
711
|
-
var removeSelectOption = grouped(function
|
|
713
|
+
var removeSelectOption = grouped(function removeSelectOption(db, id, name) {
|
|
712
714
|
const { prop, options } = getSelectProperty(db, id);
|
|
713
715
|
if (!options.includes(name))
|
|
714
716
|
throw new MhError("not_found", `no such option: ${name}`);
|
|
@@ -736,7 +738,7 @@ var removeSelectOption = grouped(function removeSelectOption2(db, id, name) {
|
|
|
736
738
|
}
|
|
737
739
|
return { property: getProperty(db, id), cleared };
|
|
738
740
|
});
|
|
739
|
-
var removeProperty = grouped(function
|
|
741
|
+
var removeProperty = grouped(function removeProperty(db, id) {
|
|
740
742
|
const prop = getProperty(db, id);
|
|
741
743
|
if (!prop)
|
|
742
744
|
return false;
|
|
@@ -886,6 +888,20 @@ function resolveRelation(db, prop, value) {
|
|
|
886
888
|
}
|
|
887
889
|
throw new MhError("ambiguous", `${prop.name}: ambiguous relation "${value}" (${cands.length} matches); use a full id`);
|
|
888
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
|
+
}
|
|
889
905
|
function coerce(db, prop, value) {
|
|
890
906
|
switch (prop.type) {
|
|
891
907
|
case "text":
|
|
@@ -934,6 +950,10 @@ function coerce(db, prop, value) {
|
|
|
934
950
|
const arr = value === null ? [] : Array.isArray(value) ? value : [value];
|
|
935
951
|
return arr.map((v) => resolveRelation(db, prop, String(v)));
|
|
936
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
|
+
}
|
|
937
957
|
}
|
|
938
958
|
}
|
|
939
959
|
function propsByName(props) {
|
|
@@ -1021,7 +1041,7 @@ function rebalanceRecordOrderKeys(db, databaseId) {
|
|
|
1021
1041
|
emit(db, "records", row.id, "order_key", keys[i]);
|
|
1022
1042
|
});
|
|
1023
1043
|
}
|
|
1024
|
-
var backfillRecordOrderKeys = grouped(function
|
|
1044
|
+
var backfillRecordOrderKeys = grouped(function backfillRecordOrderKeys(db, databaseId) {
|
|
1025
1045
|
const emitChange = canEmitOrderKeys(db);
|
|
1026
1046
|
const dbRows = databaseId ? [{ database_id: databaseId }] : db.query(`SELECT DISTINCT database_id FROM records
|
|
1027
1047
|
WHERE database_id IS NOT NULL AND __deleted = 0 AND order_key IS NULL`).all();
|
|
@@ -1036,7 +1056,7 @@ var backfillRecordOrderKeys = grouped(function backfillRecordOrderKeys2(db, data
|
|
|
1036
1056
|
missing.forEach((row, i) => writeRecordOrderKey(db, row.id, keys[i], emitChange));
|
|
1037
1057
|
}
|
|
1038
1058
|
});
|
|
1039
|
-
var createRecordPrepared = grouped(function
|
|
1059
|
+
var createRecordPrepared = grouped(function createRecordPrepared(db, database, cells) {
|
|
1040
1060
|
const id = deriveTitle(cells, slugify(database.name, "rec"));
|
|
1041
1061
|
const orderKey = keyBetween(lastRecordOrderKey(db, database.id), null);
|
|
1042
1062
|
const first = emit(db, "records", id, "database_id", database.id);
|
|
@@ -1046,7 +1066,7 @@ var createRecordPrepared = grouped(function createRecordPrepared2(db, database,
|
|
|
1046
1066
|
emit(db, "records", id, prop.id, value);
|
|
1047
1067
|
return getRecord(db, id);
|
|
1048
1068
|
});
|
|
1049
|
-
var createRecord = grouped(function
|
|
1069
|
+
var createRecord = grouped(function createRecord(db, databaseId, data) {
|
|
1050
1070
|
const database = getDatabase(db, databaseId);
|
|
1051
1071
|
if (!database)
|
|
1052
1072
|
throw new MhError("not_found", `no such database: ${databaseId}`);
|
|
@@ -1078,7 +1098,7 @@ function listRecords(db, databaseId, opts = {}) {
|
|
|
1078
1098
|
key = key.slice(1);
|
|
1079
1099
|
}
|
|
1080
1100
|
if (key !== "created" && key !== "created_hlc") {
|
|
1081
|
-
let p = props.find((
|
|
1101
|
+
let p = props.find((p) => p.id === key);
|
|
1082
1102
|
if (!p) {
|
|
1083
1103
|
const matches = props.filter((q) => q.name.toLowerCase() === key.toLowerCase());
|
|
1084
1104
|
if (matches.length > 1)
|
|
@@ -1116,7 +1136,7 @@ function listRecords(db, databaseId, opts = {}) {
|
|
|
1116
1136
|
}
|
|
1117
1137
|
return rows;
|
|
1118
1138
|
}
|
|
1119
|
-
var moveRecord = grouped(function
|
|
1139
|
+
var moveRecord = grouped(function moveRecord(db, id, targetId, where) {
|
|
1120
1140
|
if (where !== "before" && where !== "after")
|
|
1121
1141
|
throw new MhError("invalid_input", `unknown move position: ${where}`);
|
|
1122
1142
|
const src = db.query("SELECT id, database_id FROM records WHERE id = ? AND __deleted = 0").get(id);
|
|
@@ -1146,21 +1166,26 @@ var moveRecord = grouped(function moveRecord2(db, id, targetId, where) {
|
|
|
1146
1166
|
emit(db, "records", id, "order_key", keyBetween(left, right));
|
|
1147
1167
|
return getRecord(db, id);
|
|
1148
1168
|
});
|
|
1149
|
-
var updateRecordPrepared = grouped(function
|
|
1150
|
-
const rec = db.query("SELECT id, database_id FROM records WHERE id = ? AND __deleted = 0").get(id);
|
|
1169
|
+
var updateRecordPrepared = grouped(function updateRecordPrepared(db, id, cells) {
|
|
1170
|
+
const rec = db.query("SELECT id, database_id, data FROM records WHERE id = ? AND __deleted = 0").get(id);
|
|
1151
1171
|
if (!rec)
|
|
1152
1172
|
throw new MhError("not_found", `no such record: ${id}`);
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
+
}
|
|
1155
1180
|
return getRecord(db, id);
|
|
1156
1181
|
});
|
|
1157
|
-
var updateRecord = grouped(function
|
|
1182
|
+
var updateRecord = grouped(function updateRecord(db, id, data) {
|
|
1158
1183
|
const rec = db.query("SELECT id, database_id FROM records WHERE id = ? AND __deleted = 0").get(id);
|
|
1159
1184
|
if (!rec)
|
|
1160
1185
|
throw new MhError("not_found", `no such record: ${id}`);
|
|
1161
1186
|
return updateRecordPrepared(db, id, prepareRecordCells(db, rec.database_id, data));
|
|
1162
1187
|
});
|
|
1163
|
-
var deleteRecord = grouped(function
|
|
1188
|
+
var deleteRecord = grouped(function deleteRecord(db, id) {
|
|
1164
1189
|
const rec = db.query("SELECT id FROM records WHERE id = ? AND __deleted = 0").get(id);
|
|
1165
1190
|
if (!rec)
|
|
1166
1191
|
return false;
|
|
@@ -1176,7 +1201,7 @@ function rowOut(r) {
|
|
|
1176
1201
|
return null;
|
|
1177
1202
|
return { ...r, meta: r.meta === null ? null : JSON.parse(r.meta) };
|
|
1178
1203
|
}
|
|
1179
|
-
var createDatabase = grouped(function
|
|
1204
|
+
var createDatabase = grouped(function createDatabase(db, opts) {
|
|
1180
1205
|
const id = newId("db", opts.name);
|
|
1181
1206
|
const first = emit(db, "databases", id, "name", opts.name);
|
|
1182
1207
|
emit(db, "databases", id, "created_hlc", first.hlc);
|
|
@@ -1184,7 +1209,7 @@ var createDatabase = grouped(function createDatabase2(db, opts) {
|
|
|
1184
1209
|
emit(db, "databases", id, "icon", opts.icon);
|
|
1185
1210
|
return getDatabase(db, id);
|
|
1186
1211
|
});
|
|
1187
|
-
var updateDatabase = grouped(function
|
|
1212
|
+
var updateDatabase = grouped(function updateDatabase(db, id, fields) {
|
|
1188
1213
|
if (!getDatabase(db, id))
|
|
1189
1214
|
throw new MhError("not_found", `no such database: ${id}`);
|
|
1190
1215
|
if (fields.name !== undefined)
|
|
@@ -1198,7 +1223,7 @@ var updateDatabase = grouped(function updateDatabase2(db, id, fields) {
|
|
|
1198
1223
|
}
|
|
1199
1224
|
return getDatabase(db, id);
|
|
1200
1225
|
});
|
|
1201
|
-
var duplicateDatabase = grouped(function
|
|
1226
|
+
var duplicateDatabase = grouped(function duplicateDatabase(db, id, opts = {}) {
|
|
1202
1227
|
const src = getDatabase(db, id);
|
|
1203
1228
|
if (!src)
|
|
1204
1229
|
throw new MhError("not_found", `no such database: ${id}`);
|
|
@@ -1212,26 +1237,45 @@ var duplicateDatabase = grouped(function duplicateDatabase2(db, id, opts = {}) {
|
|
|
1212
1237
|
emit(db, "databases", dup.id, "meta", meta);
|
|
1213
1238
|
}
|
|
1214
1239
|
const propIdMap = new Map;
|
|
1240
|
+
const selfRelNew = new Set;
|
|
1215
1241
|
for (const p of listProperties(db, id)) {
|
|
1216
|
-
const
|
|
1242
|
+
const self = p.type === "relation" && p.config?.database === id;
|
|
1243
|
+
const config = self ? { ...p.config, database: dup.id } : p.config ?? undefined;
|
|
1217
1244
|
const np = addProperty(db, dup.id, { name: p.name, type: p.type, config, position: p.position });
|
|
1218
1245
|
propIdMap.set(p.id, np.id);
|
|
1246
|
+
if (self)
|
|
1247
|
+
selfRelNew.add(np.id);
|
|
1219
1248
|
}
|
|
1220
|
-
|
|
1249
|
+
const srcRecords = listRecords(db, id);
|
|
1250
|
+
const recIdMap = new Map;
|
|
1251
|
+
for (const r of srcRecords) {
|
|
1221
1252
|
const data = {};
|
|
1222
1253
|
for (const [pid, v] of Object.entries(r.cells)) {
|
|
1223
1254
|
const nid = propIdMap.get(pid);
|
|
1224
|
-
if (nid)
|
|
1225
|
-
|
|
1255
|
+
if (!nid)
|
|
1256
|
+
continue;
|
|
1257
|
+
if (selfRelNew.has(nid) && Array.isArray(v) && v.length > 0)
|
|
1258
|
+
continue;
|
|
1259
|
+
data[nid] = v;
|
|
1260
|
+
}
|
|
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);
|
|
1226
1269
|
}
|
|
1227
|
-
|
|
1270
|
+
if (Object.keys(patch).length)
|
|
1271
|
+
updateRecord(db, recIdMap.get(r.id), patch);
|
|
1228
1272
|
}
|
|
1229
1273
|
return getDatabase(db, dup.id);
|
|
1230
1274
|
});
|
|
1231
1275
|
function getDatabase(db, id) {
|
|
1232
1276
|
return rowOut(db.query(`SELECT ${DATABASE_SELECT} FROM databases WHERE id = ? AND __deleted = 0`).get(id));
|
|
1233
1277
|
}
|
|
1234
|
-
var deleteDatabase = grouped(function
|
|
1278
|
+
var deleteDatabase = grouped(function deleteDatabase(db, id) {
|
|
1235
1279
|
if (!getDatabase(db, id))
|
|
1236
1280
|
return false;
|
|
1237
1281
|
emit(db, "databases", id, "__deleted", 1);
|
|
@@ -1363,29 +1407,33 @@ function liveRowCount(db, databaseId) {
|
|
|
1363
1407
|
}
|
|
1364
1408
|
function assertRelationAllowed(set, principalKind, prop) {
|
|
1365
1409
|
if (principalKind === "public")
|
|
1366
|
-
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)`);
|
|
1367
1413
|
const target = prop.config?.database;
|
|
1368
1414
|
if (!target || !grantFor(set, target))
|
|
1369
1415
|
throw new MhError("invalid_input", `${prop.name}: relation target database is not in this share's grants`);
|
|
1370
1416
|
}
|
|
1417
|
+
function isLiveRefValue(prop, value) {
|
|
1418
|
+
return (prop.type === "relation" || prop.type === "doc") && value != null && !(Array.isArray(value) && value.length === 0);
|
|
1419
|
+
}
|
|
1371
1420
|
function assertGuestPayload(db, set, principal, database, values, opts = {}) {
|
|
1372
1421
|
const limits = opts.limits ?? GUEST_LIMITS;
|
|
1373
1422
|
const entries = Object.entries(values);
|
|
1374
1423
|
if (entries.length > limits.maxCells)
|
|
1375
1424
|
throw new MhError("invalid_input", `too many cells (max ${limits.maxCells})`);
|
|
1376
1425
|
const props = listProperties(db, database.id);
|
|
1377
|
-
const resolved = resolveData(props, values).map(({ prop, value }) =>
|
|
1378
|
-
prop,
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
+
});
|
|
1381
1431
|
let total = 0;
|
|
1382
1432
|
for (const { prop, value } of resolved) {
|
|
1383
1433
|
const size = utf8Len(JSON.stringify(value ?? null));
|
|
1384
1434
|
if (size > limits.maxValueBytes)
|
|
1385
1435
|
throw new MhError("invalid_input", `${prop.name}: value too large (max ${limits.maxValueBytes} bytes)`);
|
|
1386
1436
|
total += size;
|
|
1387
|
-
if (prop.type === "relation" && value != null && !(Array.isArray(value) && value.length === 0))
|
|
1388
|
-
assertRelationAllowed(set, principal.kind, prop);
|
|
1389
1437
|
}
|
|
1390
1438
|
if (total > limits.maxBodyBytes)
|
|
1391
1439
|
throw new MhError("invalid_input", `payload too large (max ${limits.maxBodyBytes} bytes)`);
|
|
@@ -1551,14 +1599,14 @@ function prepareIntent(db, policy, principal, intent, clock) {
|
|
|
1551
1599
|
if (intent.action === "createRecord") {
|
|
1552
1600
|
if (!intent.table || typeof intent.table !== "string")
|
|
1553
1601
|
throw new MhError("invalid_input", "createRecord intent needs a table");
|
|
1554
|
-
const
|
|
1555
|
-
const
|
|
1602
|
+
const database = authorizeDbRef(db, policy.grants, intent.table, "create");
|
|
1603
|
+
const cells = assertGuestPayload(db, policy.grants, principal, database, intent.payload, { limits: policy.limits });
|
|
1556
1604
|
return {
|
|
1557
1605
|
action: intent.action,
|
|
1558
|
-
database
|
|
1559
|
-
cells
|
|
1606
|
+
database,
|
|
1607
|
+
cells,
|
|
1560
1608
|
targetLive: true,
|
|
1561
|
-
fingerprint: fingerprintIntent(intent.action,
|
|
1609
|
+
fingerprint: fingerprintIntent(intent.action, database.id, clock === "submitted" ? intent.recordId ?? null : null, cells)
|
|
1562
1610
|
};
|
|
1563
1611
|
}
|
|
1564
1612
|
if (intent.action !== "updateRecord")
|
|
@@ -1704,10 +1752,10 @@ function isRecordRow(value) {
|
|
|
1704
1752
|
return typeof row.id === "string" && typeof row.database_id === "string" && !!row.values && typeof row.values === "object" && !Array.isArray(row.values) && !!row.cells && typeof row.cells === "object" && !Array.isArray(row.cells);
|
|
1705
1753
|
}
|
|
1706
1754
|
function clampMillis(submittedAt, now) {
|
|
1707
|
-
const
|
|
1708
|
-
const ceil =
|
|
1755
|
+
const safeNow = Math.max(0, Math.trunc(Number.isFinite(now) ? now : Date.now()));
|
|
1756
|
+
const ceil = safeNow + HLC_SKEW_MS;
|
|
1709
1757
|
if (!Number.isFinite(submittedAt))
|
|
1710
|
-
return
|
|
1758
|
+
return safeNow;
|
|
1711
1759
|
return Math.max(0, Math.min(Math.trunc(submittedAt), ceil));
|
|
1712
1760
|
}
|
|
1713
1761
|
function seedCounter(db, node, millis) {
|
|
@@ -1747,12 +1795,12 @@ function stableStringify(value) {
|
|
|
1747
1795
|
|
|
1748
1796
|
// src/core/sync/anti-abuse.ts
|
|
1749
1797
|
function timingSafeEq(a, b) {
|
|
1750
|
-
const
|
|
1798
|
+
const ab = new TextEncoder().encode(a);
|
|
1751
1799
|
const bb = new TextEncoder().encode(b);
|
|
1752
|
-
let diff =
|
|
1753
|
-
const n = Math.max(
|
|
1800
|
+
let diff = ab.length ^ bb.length;
|
|
1801
|
+
const n = Math.max(ab.length, bb.length);
|
|
1754
1802
|
for (let i = 0;i < n; i++)
|
|
1755
|
-
diff |= (
|
|
1803
|
+
diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
|
|
1756
1804
|
return diff === 0;
|
|
1757
1805
|
}
|
|
1758
1806
|
var siteverify = async (secret, token, ip) => {
|
|
@@ -2033,7 +2081,7 @@ function assertVisibility(v) {
|
|
|
2033
2081
|
if (v !== "public" && v !== "private")
|
|
2034
2082
|
throw new MhError("invalid_input", `visibility must be "public" or "private", got ${JSON.stringify(v)}`);
|
|
2035
2083
|
}
|
|
2036
|
-
var createSite = grouped(function
|
|
2084
|
+
var createSite = grouped(function createSite(db, opts) {
|
|
2037
2085
|
const name = normalizeSiteName(opts.name);
|
|
2038
2086
|
if (getSiteByName(db, name))
|
|
2039
2087
|
throw new MhError("conflict", `site name already exists: ${name}`);
|
|
@@ -2048,7 +2096,7 @@ var createSite = grouped(function createSite2(db, opts) {
|
|
|
2048
2096
|
emit(db, "sites", id, "visibility", opts.visibility);
|
|
2049
2097
|
return getSite(db, id);
|
|
2050
2098
|
});
|
|
2051
|
-
var updateSite = grouped(function
|
|
2099
|
+
var updateSite = grouped(function updateSite(db, id, opts) {
|
|
2052
2100
|
if (!getSite(db, id))
|
|
2053
2101
|
throw new MhError("not_found", `no such site: ${id}`);
|
|
2054
2102
|
if (opts.visibility !== undefined)
|
|
@@ -2068,14 +2116,14 @@ var updateSite = grouped(function updateSite2(db, id, opts) {
|
|
|
2068
2116
|
emit(db, "sites", id, "spa", opts.spa ? 1 : 0);
|
|
2069
2117
|
return getSite(db, id);
|
|
2070
2118
|
});
|
|
2071
|
-
var setSitePublicGrants = grouped(function
|
|
2119
|
+
var setSitePublicGrants = grouped(function setSitePublicGrants(db, siteId, grants) {
|
|
2072
2120
|
if (!getSite(db, siteId))
|
|
2073
2121
|
throw new MhError("not_found", `no such site: ${siteId}`);
|
|
2074
2122
|
const value = grants == null ? null : serializeGrantSet(validateGrantSetInput(grants));
|
|
2075
2123
|
emit(db, "sites", siteId, "public_grants", value);
|
|
2076
2124
|
return getSite(db, siteId);
|
|
2077
2125
|
});
|
|
2078
|
-
var deleteSite = grouped(function
|
|
2126
|
+
var deleteSite = grouped(function deleteSite(db, id) {
|
|
2079
2127
|
if (!getSite(db, id))
|
|
2080
2128
|
return false;
|
|
2081
2129
|
const files = db.query("SELECT id FROM site_files WHERE site_id = ? AND __deleted = 0").all(id);
|
|
@@ -2088,7 +2136,7 @@ function fileIdFor(db, siteId, path) {
|
|
|
2088
2136
|
const row = db.query("SELECT id FROM site_files WHERE site_id = ? AND path = ? ORDER BY created_hlc LIMIT 1").get(siteId, path);
|
|
2089
2137
|
return row?.id ?? null;
|
|
2090
2138
|
}
|
|
2091
|
-
var deleteFile = grouped(function
|
|
2139
|
+
var deleteFile = grouped(function deleteFile(db, siteId, path) {
|
|
2092
2140
|
const id = fileIdFor(db, siteId, normalizeSitePath(path));
|
|
2093
2141
|
if (!id)
|
|
2094
2142
|
return false;
|
|
@@ -2552,7 +2600,7 @@ function documentVersion(db, id) {
|
|
|
2552
2600
|
OR (dataset = 'doc_blocks' AND row_id IN (SELECT id FROM doc_blocks WHERE doc_id = ?))`).get(id, id);
|
|
2553
2601
|
return row.h ?? "";
|
|
2554
2602
|
}
|
|
2555
|
-
var createDocument = grouped(function
|
|
2603
|
+
var createDocument = grouped(function createDocument(db, opts) {
|
|
2556
2604
|
const id = newId("doc", opts.title);
|
|
2557
2605
|
const first = emit(db, "documents", id, "title", opts.title);
|
|
2558
2606
|
emit(db, "documents", id, "created_hlc", first.hlc);
|
|
@@ -2616,7 +2664,7 @@ function backfillSiblings(db, parentId) {
|
|
|
2616
2664
|
db.query("UPDATE documents SET order_key = ? WHERE id = ?").run(keys[i], row.id);
|
|
2617
2665
|
});
|
|
2618
2666
|
}
|
|
2619
|
-
var backfillDocumentOrderKeys = grouped(function
|
|
2667
|
+
var backfillDocumentOrderKeys = grouped(function backfillDocumentOrderKeys(db) {
|
|
2620
2668
|
const parents = db.query("SELECT DISTINCT parent_id FROM documents WHERE __deleted = 0 AND order_key IS NULL").all();
|
|
2621
2669
|
for (const p of parents)
|
|
2622
2670
|
backfillSiblings(db, p.parent_id);
|
|
@@ -2652,7 +2700,7 @@ function placeInSiblings(db, id, parentId, anchor) {
|
|
|
2652
2700
|
}
|
|
2653
2701
|
emit(db, "documents", id, "order_key", key);
|
|
2654
2702
|
}
|
|
2655
|
-
var moveDocument = grouped(function
|
|
2703
|
+
var moveDocument = grouped(function moveDocument(db, id, targetId, where) {
|
|
2656
2704
|
const src = getDocument(db, id);
|
|
2657
2705
|
if (!src)
|
|
2658
2706
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
@@ -2667,7 +2715,7 @@ var moveDocument = grouped(function moveDocument2(db, id, targetId, where) {
|
|
|
2667
2715
|
placeInSiblings(db, id, target.parent_id, { targetId, where });
|
|
2668
2716
|
return getDocument(db, id);
|
|
2669
2717
|
});
|
|
2670
|
-
var duplicateDocument = grouped(function
|
|
2718
|
+
var duplicateDocument = grouped(function duplicateDocument(db, id, opts = {}) {
|
|
2671
2719
|
const src = getDocument(db, id);
|
|
2672
2720
|
if (!src)
|
|
2673
2721
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
@@ -2685,7 +2733,7 @@ var duplicateDocument = grouped(function duplicateDocument2(db, id, opts = {}) {
|
|
|
2685
2733
|
moveDocument(db, dup.id, src.id, "after");
|
|
2686
2734
|
return getDocument(db, dup.id);
|
|
2687
2735
|
});
|
|
2688
|
-
var updateDocument = grouped(function
|
|
2736
|
+
var updateDocument = grouped(function updateDocument(db, id, fields, opts = {}) {
|
|
2689
2737
|
const doc = getDocument(db, id);
|
|
2690
2738
|
if (!doc)
|
|
2691
2739
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
@@ -2706,7 +2754,7 @@ var updateDocument = grouped(function updateDocument2(db, id, fields, opts = {})
|
|
|
2706
2754
|
}
|
|
2707
2755
|
return getDocument(db, id);
|
|
2708
2756
|
});
|
|
2709
|
-
var editDocument = grouped(function
|
|
2757
|
+
var editDocument = grouped(function editDocument(db, id, opts) {
|
|
2710
2758
|
if (!getDocument(db, id))
|
|
2711
2759
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
2712
2760
|
if (opts.ifMatch !== undefined && documentVersion(db, id) !== opts.ifMatch)
|
|
@@ -2744,7 +2792,7 @@ var editDocument = grouped(function editDocument2(db, id, opts) {
|
|
|
2744
2792
|
reconcileBody(db, id, newBody);
|
|
2745
2793
|
return { id, changed: newBody !== body, replaced: opts.replaceAll ? count : 1, version: documentVersion(db, id) };
|
|
2746
2794
|
});
|
|
2747
|
-
var editDocumentBatch = grouped(function
|
|
2795
|
+
var editDocumentBatch = grouped(function editDocumentBatch(db, id, opts) {
|
|
2748
2796
|
if (!getDocument(db, id))
|
|
2749
2797
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
2750
2798
|
if (opts.edits.length === 0)
|
|
@@ -2771,7 +2819,7 @@ var editDocumentBatch = grouped(function editDocumentBatch2(db, id, opts) {
|
|
|
2771
2819
|
reconcileBody(db, id, body);
|
|
2772
2820
|
return { id, changed: body !== orig, replaced: total, version: documentVersion(db, id) };
|
|
2773
2821
|
});
|
|
2774
|
-
var appendDocument = grouped(function
|
|
2822
|
+
var appendDocument = grouped(function appendDocument(db, id, body) {
|
|
2775
2823
|
if (!getDocument(db, id))
|
|
2776
2824
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
2777
2825
|
ensureBlocks(db, id);
|
|
@@ -2783,7 +2831,7 @@ var appendDocument = grouped(function appendDocument2(db, id, body) {
|
|
|
2783
2831
|
}
|
|
2784
2832
|
return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
|
|
2785
2833
|
});
|
|
2786
|
-
var prependDocument = grouped(function
|
|
2834
|
+
var prependDocument = grouped(function prependDocument(db, id, body) {
|
|
2787
2835
|
if (!getDocument(db, id))
|
|
2788
2836
|
throw new MhError("not_found", `no such document: ${id}`);
|
|
2789
2837
|
ensureBlocks(db, id);
|
|
@@ -2795,7 +2843,7 @@ var prependDocument = grouped(function prependDocument2(db, id, body) {
|
|
|
2795
2843
|
}
|
|
2796
2844
|
return { id, changed: next.length > 0, replaced: next.length, version: documentVersion(db, id) };
|
|
2797
2845
|
});
|
|
2798
|
-
var deleteDocument = grouped(function
|
|
2846
|
+
var deleteDocument = grouped(function deleteDocument(db, id) {
|
|
2799
2847
|
if (!getDocument(db, id))
|
|
2800
2848
|
return false;
|
|
2801
2849
|
emit(db, "documents", id, "__deleted", 1);
|
|
@@ -3495,7 +3543,7 @@ function createRoomFetch(deps) {
|
|
|
3495
3543
|
if (!body || typeof body.guestBase !== "string" || body.guestBase === "")
|
|
3496
3544
|
return json({ error: "provision needs a guestBase", code: "invalid_input" }, 400);
|
|
3497
3545
|
const str = (v) => typeof v === "string" && v !== "" ? v : null;
|
|
3498
|
-
const
|
|
3546
|
+
const cfg = initRoomDb(db, {
|
|
3499
3547
|
slug: typeof body.slug === "string" && body.slug !== "" ? body.slug : pathSlug,
|
|
3500
3548
|
guestBase: body.guestBase,
|
|
3501
3549
|
grants: str(body.grants) ?? "",
|
|
@@ -3503,11 +3551,11 @@ function createRoomFetch(deps) {
|
|
|
3503
3551
|
pwSalt: str(body.pwSalt),
|
|
3504
3552
|
expiresAt: typeof body.expiresAt === "number" ? body.expiresAt : null
|
|
3505
3553
|
});
|
|
3506
|
-
await deps.setAlarm?.(
|
|
3507
|
-
return json({ slug:
|
|
3554
|
+
await deps.setAlarm?.(cfg.expiresAt ?? null);
|
|
3555
|
+
return json({ slug: cfg.slug, room_node: cfg.roomNode, provisioned: true });
|
|
3508
3556
|
}
|
|
3509
|
-
const
|
|
3510
|
-
if (!
|
|
3557
|
+
const cfg = tryConfig(db);
|
|
3558
|
+
if (!cfg)
|
|
3511
3559
|
return json({ error: "room is not provisioned", code: "not_found" }, 404);
|
|
3512
3560
|
if (sub === "owner/sync") {
|
|
3513
3561
|
const body = await req.json().catch(() => null);
|
|
@@ -3515,7 +3563,7 @@ function createRoomFetch(deps) {
|
|
|
3515
3563
|
return json({ error: "malformed sync request", code: "invalid_input" }, 400);
|
|
3516
3564
|
await hlcHeadroom(db, deps);
|
|
3517
3565
|
try {
|
|
3518
|
-
const resp = handleOwnerSync(db, body,
|
|
3566
|
+
const resp = handleOwnerSync(db, body, cfg);
|
|
3519
3567
|
if (resp.share_state === "active" && body.changes.length > 0)
|
|
3520
3568
|
deps.poke?.(maxSeq(db));
|
|
3521
3569
|
return json(resp);
|
|
@@ -3614,10 +3662,10 @@ function serveRoomFile(db, cfg, path) {
|
|
|
3614
3662
|
if (row.encoding === "utf8")
|
|
3615
3663
|
return new Response(row.content ?? "", { status, headers });
|
|
3616
3664
|
if (row.encoding === "base64") {
|
|
3617
|
-
const
|
|
3618
|
-
const
|
|
3619
|
-
|
|
3620
|
-
return new Response(
|
|
3665
|
+
const bytes = base64ToBytes(row.content ?? "");
|
|
3666
|
+
const body = new Uint8Array(bytes.byteLength);
|
|
3667
|
+
body.set(bytes);
|
|
3668
|
+
return new Response(body.buffer, { status, headers });
|
|
3621
3669
|
}
|
|
3622
3670
|
const bytes = row.content ? roomBlobBytes(db, row.content) : null;
|
|
3623
3671
|
if (!bytes)
|
|
@@ -4018,21 +4066,21 @@ var edge_worker_default = {
|
|
|
4018
4066
|
if (slug === null)
|
|
4019
4067
|
return new Response("bad request", { status: 400 });
|
|
4020
4068
|
const id = env.ROOM.idFromName(slug);
|
|
4021
|
-
const
|
|
4022
|
-
return cors ? corsResponse(
|
|
4069
|
+
const res = await env.ROOM.get(id).fetch(req);
|
|
4070
|
+
return cors ? corsResponse(res) : res;
|
|
4023
4071
|
}
|
|
4024
4072
|
const res = await createInboxFetch({ sql: d1Sql(env.DB), ownerToken: env.OWNER_TOKEN })(req);
|
|
4025
4073
|
return cors ? corsResponse(res) : res;
|
|
4026
4074
|
}
|
|
4027
4075
|
};
|
|
4028
4076
|
export {
|
|
4029
|
-
|
|
4030
|
-
createInboxFetch,
|
|
4031
|
-
MhRoom,
|
|
4032
|
-
EDGE_WORKER_VERSION,
|
|
4033
|
-
EDGE_WORKER_MARKER,
|
|
4034
|
-
EDGE_SCHEMA_SQL,
|
|
4035
|
-
DROP_ENVELOPE_LIMIT_BYTES,
|
|
4077
|
+
DROP_DEFAULT_MAX_BYTES,
|
|
4036
4078
|
DROP_DEFAULT_MAX_ENVELOPES,
|
|
4037
|
-
|
|
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
|
|
4038
4086
|
};
|