graphddb 0.2.4 → 0.3.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/README.md +322 -305
- package/dist/{chunk-GCPEUPAA.js → chunk-6AIAHP3A.js} +1129 -156
- package/dist/{chunk-IQEOJVHI.js → chunk-QEOFIXTN.js} +1194 -60
- package/dist/{chunk-LNESVUTO.js → chunk-QQNP43JL.js} +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +275 -73
- package/dist/index.js +131 -7
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types-BEeQ_nSd.d.ts → types-xJRn5qkv.d.ts} +1567 -237
- package/package.json +1 -1
|
@@ -4,17 +4,22 @@ import {
|
|
|
4
4
|
ClientManager,
|
|
5
5
|
MAX_TRANSACT_ITEMS,
|
|
6
6
|
MetadataRegistry,
|
|
7
|
+
NO_MIDDLEWARE,
|
|
7
8
|
TableMapping,
|
|
8
9
|
attachHiddenKey,
|
|
9
10
|
attachModelClass,
|
|
10
11
|
buildConditionExpression,
|
|
11
12
|
buildDeleteInput,
|
|
12
13
|
buildPutInput,
|
|
14
|
+
buildReadRuntime,
|
|
13
15
|
buildUpdateInput,
|
|
16
|
+
buildWriteRuntime,
|
|
14
17
|
captureWrite,
|
|
18
|
+
chunkArray,
|
|
15
19
|
commitTransaction,
|
|
16
20
|
compileRawCondition,
|
|
17
21
|
createColumnMap,
|
|
22
|
+
ctxModelFor,
|
|
18
23
|
execItemKeySignature,
|
|
19
24
|
executeDelete,
|
|
20
25
|
executePut,
|
|
@@ -32,7 +37,7 @@ import {
|
|
|
32
37
|
serializeFieldValue,
|
|
33
38
|
serializeRawCondition,
|
|
34
39
|
skTemplate
|
|
35
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-6AIAHP3A.js";
|
|
36
41
|
|
|
37
42
|
// src/spec/types.ts
|
|
38
43
|
var SPEC_VERSION = "1.0";
|
|
@@ -810,23 +815,71 @@ async function executeListInternal(modelClass, key, selectSpec, options = {}) {
|
|
|
810
815
|
updatable: options.updatable ?? false
|
|
811
816
|
});
|
|
812
817
|
const operation = executionPlan.operations[0];
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
818
|
+
const mw = options.mw ?? NO_MIDDLEWARE;
|
|
819
|
+
const sendList = (op) => execute(op, options.retry !== void 0 ? { retry: options.retry } : void 0).then(
|
|
820
|
+
(r) => r
|
|
816
821
|
);
|
|
817
|
-
|
|
822
|
+
let lastEvaluatedKey;
|
|
823
|
+
let rawItems;
|
|
824
|
+
if (mw.active) {
|
|
825
|
+
let captured;
|
|
826
|
+
const opModel = ctxModelFor(modelClass);
|
|
827
|
+
rawItems = await mw.runOp(
|
|
828
|
+
operation,
|
|
829
|
+
options.relationPath ?? [],
|
|
830
|
+
opModel,
|
|
831
|
+
async (op) => {
|
|
832
|
+
captured = await sendList(op);
|
|
833
|
+
return captured.items;
|
|
834
|
+
}
|
|
835
|
+
);
|
|
836
|
+
lastEvaluatedKey = captured?.lastEvaluatedKey;
|
|
837
|
+
} else {
|
|
838
|
+
const result = await sendList(operation);
|
|
839
|
+
rawItems = result.items;
|
|
840
|
+
lastEvaluatedKey = result.lastEvaluatedKey;
|
|
841
|
+
}
|
|
818
842
|
const hydrated = hydrate(rawItems, select, metadata, options.updatable ?? false);
|
|
819
|
-
const cursor =
|
|
843
|
+
const cursor = lastEvaluatedKey ? encodeCursor(lastEvaluatedKey) : null;
|
|
820
844
|
return { items: hydrated, rawItems, cursor };
|
|
821
845
|
}
|
|
822
846
|
async function executeList(modelClass, key, selectSpec, options = {}) {
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
847
|
+
const mw = buildReadRuntime(options.context);
|
|
848
|
+
if (!mw.active) {
|
|
849
|
+
const { items, cursor } = await executeListInternal(
|
|
850
|
+
modelClass,
|
|
851
|
+
key,
|
|
852
|
+
selectSpec,
|
|
853
|
+
options
|
|
854
|
+
);
|
|
855
|
+
return { items, cursor };
|
|
856
|
+
}
|
|
857
|
+
const ctxModel = ctxModelFor(modelClass);
|
|
858
|
+
const opts = { ...options };
|
|
859
|
+
const params = {
|
|
860
|
+
key: { ...key },
|
|
861
|
+
select: selectSpec && typeof selectSpec === "object" ? selectSpec : {},
|
|
862
|
+
options: opts
|
|
863
|
+
};
|
|
864
|
+
let reqCtx;
|
|
865
|
+
try {
|
|
866
|
+
reqCtx = await mw.runRequestBefore("list", ctxModel, params);
|
|
867
|
+
const { items, cursor } = await executeListInternal(
|
|
868
|
+
modelClass,
|
|
869
|
+
params.key,
|
|
870
|
+
params.select,
|
|
871
|
+
// Thread the SAME runtime + an empty (root) relation path so R2/R3 fire on
|
|
872
|
+
// the partition Query; carry the (possibly R1-mutated) per-call options.
|
|
873
|
+
{ ...params.options, mw, relationPath: [] }
|
|
874
|
+
);
|
|
875
|
+
const result = { items, cursor };
|
|
876
|
+
return await mw.runRequestAfter(reqCtx, result);
|
|
877
|
+
} catch (err) {
|
|
878
|
+
return mw.runRequestError(
|
|
879
|
+
reqCtx ?? mw.requestCtx("list", ctxModel, params),
|
|
880
|
+
err
|
|
881
|
+
);
|
|
882
|
+
}
|
|
830
883
|
}
|
|
831
884
|
|
|
832
885
|
// src/planner/relation-planner.ts
|
|
@@ -1174,12 +1227,23 @@ async function fetchBelongsToHasOneBatch(rel, rawParentItems, selectSpec, target
|
|
|
1174
1227
|
);
|
|
1175
1228
|
const executor = ClientManager.getExecutor();
|
|
1176
1229
|
const queryKeyToRaw = /* @__PURE__ */ new Map();
|
|
1230
|
+
const mw = options.mw ?? NO_MIDDLEWARE;
|
|
1231
|
+
const opPath = [...options.relationPath ?? [], rel.propertyName];
|
|
1232
|
+
const opModel = mw.active ? ctxModelFor(rel.targetFactory()) : void 0;
|
|
1177
1233
|
for (const operation of batchOps) {
|
|
1178
|
-
const
|
|
1234
|
+
const items = mw.active ? await mw.runOp(
|
|
1235
|
+
operation,
|
|
1236
|
+
opPath,
|
|
1237
|
+
opModel,
|
|
1238
|
+
(op) => executor.batchGet(
|
|
1239
|
+
op,
|
|
1240
|
+
options.retry !== void 0 ? { retry: options.retry } : void 0
|
|
1241
|
+
).then((r) => r.items)
|
|
1242
|
+
) : (await executor.batchGet(
|
|
1179
1243
|
operation,
|
|
1180
1244
|
options.retry !== void 0 ? { retry: options.retry } : void 0
|
|
1181
|
-
);
|
|
1182
|
-
for (const item of
|
|
1245
|
+
)).items;
|
|
1246
|
+
for (const item of items) {
|
|
1183
1247
|
for (const queryKey of queryKeys) {
|
|
1184
1248
|
if (!matchesQueryKey(item, queryKey)) {
|
|
1185
1249
|
continue;
|
|
@@ -1265,9 +1329,11 @@ async function resolveRelations(parentItem, rawParentItem, select, metadata, opt
|
|
|
1265
1329
|
const relations = detectRelationFields(select, metadata);
|
|
1266
1330
|
const plan2 = options.plan ?? (currentPath === "$" ? buildRelationExecutionPlan(select, metadata) : void 0);
|
|
1267
1331
|
const opts = plan2 === options.plan ? options : { ...options, plan: plan2 };
|
|
1332
|
+
const basePath = opts.relationPath ?? [];
|
|
1268
1333
|
await runStaged(relations, currentPath, opts.plan, async (rel) => {
|
|
1269
1334
|
const selectSpec = relationSpec(select[rel.propertyName]);
|
|
1270
1335
|
const ownPath = relationResultPath(currentPath, rel);
|
|
1336
|
+
const relPath = [...basePath, rel.propertyName];
|
|
1271
1337
|
if (rel.type === "hasMany") {
|
|
1272
1338
|
const targetClass2 = rel.targetFactory();
|
|
1273
1339
|
const queryKey = buildRelationQueryKey(rel, rawParentItem);
|
|
@@ -1279,7 +1345,9 @@ async function resolveRelations(parentItem, rawParentItem, select, metadata, opt
|
|
|
1279
1345
|
order,
|
|
1280
1346
|
filter: selectSpec.filter,
|
|
1281
1347
|
updatable: opts.updatable,
|
|
1282
|
-
retry: opts.retry
|
|
1348
|
+
retry: opts.retry,
|
|
1349
|
+
mw: opts.mw,
|
|
1350
|
+
relationPath: relPath
|
|
1283
1351
|
});
|
|
1284
1352
|
let items = listResult.items;
|
|
1285
1353
|
const rawItems = listResult.rawItems;
|
|
@@ -1291,7 +1359,7 @@ async function resolveRelations(parentItem, rawParentItem, select, metadata, opt
|
|
|
1291
1359
|
rawItems,
|
|
1292
1360
|
selectSpec.select,
|
|
1293
1361
|
targetMeta2,
|
|
1294
|
-
opts,
|
|
1362
|
+
{ ...opts, relationPath: relPath },
|
|
1295
1363
|
currentDepth + 1,
|
|
1296
1364
|
ownPath
|
|
1297
1365
|
);
|
|
@@ -1320,6 +1388,36 @@ async function resolveRelations(parentItem, rawParentItem, select, metadata, opt
|
|
|
1320
1388
|
|
|
1321
1389
|
// src/operations/query.ts
|
|
1322
1390
|
async function executeQuery(modelClass, key, selectSpec, options) {
|
|
1391
|
+
const mw = buildReadRuntime(options?.context);
|
|
1392
|
+
if (!mw.active) {
|
|
1393
|
+
return executeQueryInner(modelClass, key, selectSpec, options, mw);
|
|
1394
|
+
}
|
|
1395
|
+
const ctxModel = ctxModelFor(modelClass);
|
|
1396
|
+
const opts = { ...options ?? {} };
|
|
1397
|
+
const params = {
|
|
1398
|
+
key: { ...key },
|
|
1399
|
+
select: selectSpec && typeof selectSpec === "object" ? selectSpec : {},
|
|
1400
|
+
options: opts
|
|
1401
|
+
};
|
|
1402
|
+
let reqCtx;
|
|
1403
|
+
try {
|
|
1404
|
+
reqCtx = await mw.runRequestBefore("query", ctxModel, params);
|
|
1405
|
+
const result = await executeQueryInner(
|
|
1406
|
+
modelClass,
|
|
1407
|
+
params.key,
|
|
1408
|
+
params.select,
|
|
1409
|
+
params.options,
|
|
1410
|
+
mw
|
|
1411
|
+
);
|
|
1412
|
+
return await mw.runRequestAfter(reqCtx, result);
|
|
1413
|
+
} catch (err) {
|
|
1414
|
+
return mw.runRequestError(
|
|
1415
|
+
reqCtx ?? mw.requestCtx("query", ctxModel, params),
|
|
1416
|
+
err
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
async function executeQueryInner(modelClass, key, selectSpec, options, mw) {
|
|
1323
1421
|
const metadata = MetadataRegistry.get(modelClass);
|
|
1324
1422
|
const queryFields = Object.keys(key);
|
|
1325
1423
|
const resolved = resolveKey(queryFields, metadata);
|
|
@@ -1353,11 +1451,25 @@ async function executeQuery(modelClass, key, selectSpec, options) {
|
|
|
1353
1451
|
)
|
|
1354
1452
|
);
|
|
1355
1453
|
const retry = options?.retry;
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1454
|
+
const sendRoot = () => execute(operation, retry !== void 0 ? { retry } : void 0);
|
|
1455
|
+
const parentPromise = mw.active ? (() => {
|
|
1456
|
+
const opModel = ctxModelFor(modelClass);
|
|
1457
|
+
return mw.runOp(operation, [], opModel, async (op) => {
|
|
1458
|
+
const r = await execute(
|
|
1459
|
+
op,
|
|
1460
|
+
retry !== void 0 ? { retry } : void 0
|
|
1461
|
+
);
|
|
1462
|
+
return r.items;
|
|
1463
|
+
}).then((items) => ({ items }));
|
|
1464
|
+
})() : sendRoot();
|
|
1465
|
+
const relationPromise = canParallelizeRelations ? resolveRelations(
|
|
1466
|
+
{},
|
|
1467
|
+
key,
|
|
1468
|
+
select,
|
|
1469
|
+
metadata,
|
|
1470
|
+
{ maxDepth, updatable, retry, mw, relationPath: [] },
|
|
1471
|
+
1
|
|
1472
|
+
) : null;
|
|
1361
1473
|
let result;
|
|
1362
1474
|
try {
|
|
1363
1475
|
result = await parentPromise;
|
|
@@ -1384,7 +1496,7 @@ async function executeQuery(modelClass, key, selectSpec, options) {
|
|
|
1384
1496
|
rawItem,
|
|
1385
1497
|
select,
|
|
1386
1498
|
metadata,
|
|
1387
|
-
{ maxDepth, updatable, retry },
|
|
1499
|
+
{ maxDepth, updatable, retry, mw, relationPath: [] },
|
|
1388
1500
|
1
|
|
1389
1501
|
);
|
|
1390
1502
|
if (hydratedItem && updatable) {
|
|
@@ -1465,16 +1577,37 @@ function hydrateBatchItems(rawItems, metadata) {
|
|
|
1465
1577
|
return merged;
|
|
1466
1578
|
});
|
|
1467
1579
|
}
|
|
1468
|
-
async function executeBatchGetForTable(tableName, entries) {
|
|
1580
|
+
async function executeBatchGetForTable(tableName, entries, mw, opModel) {
|
|
1469
1581
|
const groups = /* @__PURE__ */ new Map();
|
|
1470
1582
|
if (entries.length === 0) {
|
|
1471
1583
|
return groups;
|
|
1472
1584
|
}
|
|
1473
1585
|
const keys = entries.map((entry) => entry.dynamoKey);
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1586
|
+
const sendChunk = (op) => execute(op).then((r) => r.items);
|
|
1587
|
+
let rawItems;
|
|
1588
|
+
if (mw.active) {
|
|
1589
|
+
rawItems = [];
|
|
1590
|
+
for (const chunk of chunkArray(keys, BATCH_GET_MAX_KEYS)) {
|
|
1591
|
+
const operation = {
|
|
1592
|
+
type: "BatchGetItem",
|
|
1593
|
+
tableName,
|
|
1594
|
+
keys: chunk
|
|
1595
|
+
};
|
|
1596
|
+
const chunkItems = await mw.runOp(
|
|
1597
|
+
operation,
|
|
1598
|
+
[],
|
|
1599
|
+
opModel,
|
|
1600
|
+
(op) => sendChunk(op)
|
|
1601
|
+
);
|
|
1602
|
+
rawItems.push(...chunkItems);
|
|
1603
|
+
}
|
|
1604
|
+
} else {
|
|
1605
|
+
const { items } = await ClientManager.getExecutor().batchGet({
|
|
1606
|
+
tableName,
|
|
1607
|
+
keys
|
|
1608
|
+
});
|
|
1609
|
+
rawItems = items;
|
|
1610
|
+
}
|
|
1478
1611
|
const entriesByModel = /* @__PURE__ */ new Map();
|
|
1479
1612
|
for (const entry of entries) {
|
|
1480
1613
|
const existing = entriesByModel.get(entry.modelClass) ?? [];
|
|
@@ -1495,12 +1628,42 @@ async function executeBatchGetForTable(tableName, entries) {
|
|
|
1495
1628
|
}
|
|
1496
1629
|
return groups;
|
|
1497
1630
|
}
|
|
1498
|
-
async function executeBatchGet(requests) {
|
|
1631
|
+
async function executeBatchGet(requests, options) {
|
|
1632
|
+
const mw = buildReadRuntime(options?.context);
|
|
1633
|
+
if (!mw.active || requests.length === 0) {
|
|
1634
|
+
return executeBatchGetInner(requests, mw);
|
|
1635
|
+
}
|
|
1636
|
+
const firstModel = ctxModelFor(resolveModelClass(requests[0].model));
|
|
1637
|
+
const reqList = [...requests];
|
|
1638
|
+
const params = {
|
|
1639
|
+
key: {},
|
|
1640
|
+
select: {},
|
|
1641
|
+
options: {},
|
|
1642
|
+
requests: reqList
|
|
1643
|
+
};
|
|
1644
|
+
let reqCtx;
|
|
1645
|
+
try {
|
|
1646
|
+
reqCtx = await mw.runRequestBefore("batchGet", firstModel, params);
|
|
1647
|
+
const effectiveRequests = params.requests;
|
|
1648
|
+
const result = await executeBatchGetInner(effectiveRequests, mw);
|
|
1649
|
+
return await mw.runRequestAfter(reqCtx, result);
|
|
1650
|
+
} catch (err) {
|
|
1651
|
+
return mw.runRequestError(
|
|
1652
|
+
reqCtx ?? mw.requestCtx("batchGet", firstModel, params),
|
|
1653
|
+
err
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
async function executeBatchGetInner(requests, mw) {
|
|
1499
1658
|
const entriesByTable = /* @__PURE__ */ new Map();
|
|
1659
|
+
const firstModelClassByTable = /* @__PURE__ */ new Map();
|
|
1500
1660
|
for (const request of requests) {
|
|
1501
1661
|
const modelClass = resolveModelClass(request.model);
|
|
1502
1662
|
const metadata = MetadataRegistry.get(modelClass);
|
|
1503
1663
|
const tableName = TableMapping.resolve(metadata.tableName);
|
|
1664
|
+
if (!firstModelClassByTable.has(tableName)) {
|
|
1665
|
+
firstModelClassByTable.set(tableName, modelClass);
|
|
1666
|
+
}
|
|
1504
1667
|
for (const keyObj of request.keys) {
|
|
1505
1668
|
const entry = {
|
|
1506
1669
|
modelClass,
|
|
@@ -1515,7 +1678,13 @@ async function executeBatchGet(requests) {
|
|
|
1515
1678
|
const mergedGroups = /* @__PURE__ */ new Map();
|
|
1516
1679
|
for (const [, entries] of entriesByTable) {
|
|
1517
1680
|
const tableName = TableMapping.resolve(entries[0].metadata.tableName);
|
|
1518
|
-
const
|
|
1681
|
+
const opModel = mw.active ? ctxModelFor(firstModelClassByTable.get(tableName) ?? entries[0].modelClass) : void 0;
|
|
1682
|
+
const tableGroups = await executeBatchGetForTable(
|
|
1683
|
+
tableName,
|
|
1684
|
+
entries,
|
|
1685
|
+
mw,
|
|
1686
|
+
opModel
|
|
1687
|
+
);
|
|
1519
1688
|
for (const [modelClass, items] of tableGroups) {
|
|
1520
1689
|
const existing = mergedGroups.get(modelClass) ?? [];
|
|
1521
1690
|
mergedGroups.set(modelClass, existing.concat(items));
|
|
@@ -1588,6 +1757,46 @@ async function executeBatchWrite(requests) {
|
|
|
1588
1757
|
}
|
|
1589
1758
|
|
|
1590
1759
|
// src/define/entity-writes.ts
|
|
1760
|
+
var MAINTAIN_TRIGGER_RE = /^[^.\s$[\]*]+\.(?:created|updated|removed)$/;
|
|
1761
|
+
function maintainTrigger(value) {
|
|
1762
|
+
if (typeof value !== "string" || !MAINTAIN_TRIGGER_RE.test(value)) {
|
|
1763
|
+
throw new Error(
|
|
1764
|
+
`maintainTrigger: a maintenance trigger must be an \`Entity.event\` string where event is one of \`created\` / \`updated\` / \`removed\` (e.g. \`"Post.created"\`), but received ${JSON.stringify(value)}.`
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
return value;
|
|
1768
|
+
}
|
|
1769
|
+
function isMaintainTrigger(value) {
|
|
1770
|
+
return typeof value === "string" && MAINTAIN_TRIGGER_RE.test(value);
|
|
1771
|
+
}
|
|
1772
|
+
function makeProjectionTransform(path, op, args) {
|
|
1773
|
+
if (op === "preview") {
|
|
1774
|
+
const [n] = args;
|
|
1775
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
|
|
1776
|
+
throw new Error(
|
|
1777
|
+
`transform: the \`preview\` op requires a positive integer length bound (e.g. \`preview('$.body', 200)\`), but received ${JSON.stringify(n)}.`
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
return { kind: "transform", path, op, args: [n] };
|
|
1781
|
+
}
|
|
1782
|
+
if (op === "identity") {
|
|
1783
|
+
if (args.length > 0) {
|
|
1784
|
+
throw new Error(
|
|
1785
|
+
`transform: the \`identity\` op takes no arguments, but received ${JSON.stringify(args)}.`
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
return { kind: "transform", path, op, args: [] };
|
|
1789
|
+
}
|
|
1790
|
+
throw new Error(
|
|
1791
|
+
`transform: unknown projection op ${JSON.stringify(op)} (expected \`identity\` or \`preview\`).`
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
function preview(path, n) {
|
|
1795
|
+
return makeProjectionTransform(path, "preview", [n]);
|
|
1796
|
+
}
|
|
1797
|
+
function identity(path) {
|
|
1798
|
+
return makeProjectionTransform(path, "identity", []);
|
|
1799
|
+
}
|
|
1591
1800
|
var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
|
|
1592
1801
|
"graphddb:lifecycleContract"
|
|
1593
1802
|
);
|
|
@@ -1630,6 +1839,9 @@ var recorder = {
|
|
|
1630
1839
|
increment(targetFactory, keys, attribute, amount) {
|
|
1631
1840
|
return { kind: "derive", targetFactory, keys, attribute, amount };
|
|
1632
1841
|
},
|
|
1842
|
+
transform(path, op, ...args) {
|
|
1843
|
+
return makeProjectionTransform(path, op, args);
|
|
1844
|
+
},
|
|
1633
1845
|
event(name, payload) {
|
|
1634
1846
|
return { kind: "event", name, payload };
|
|
1635
1847
|
},
|
|
@@ -1817,6 +2029,387 @@ function deriveModelEdgeWriteItems(modelClass, lifecycle) {
|
|
|
1817
2029
|
return deriveEdgeWriteItems(modelClass, writes, lifecycle);
|
|
1818
2030
|
}
|
|
1819
2031
|
|
|
2032
|
+
// src/relation/maintenance-graph.ts
|
|
2033
|
+
var COUNT_DELTA_FOR_EVENT = {
|
|
2034
|
+
created: 1,
|
|
2035
|
+
removed: -1,
|
|
2036
|
+
updated: 0
|
|
2037
|
+
};
|
|
2038
|
+
var MAINTAIN_EVENTS = ["created", "updated", "removed"];
|
|
2039
|
+
function isPathRooted(value) {
|
|
2040
|
+
return value.startsWith("$.input.") || value.startsWith("$.entity.");
|
|
2041
|
+
}
|
|
2042
|
+
function sourceAttributeOf(path) {
|
|
2043
|
+
const stripped = path.replace(/^\$\.(input|entity)\./, "");
|
|
2044
|
+
return stripped.split(".")[0] ?? stripped;
|
|
2045
|
+
}
|
|
2046
|
+
function normalizeProjectionEntry(value) {
|
|
2047
|
+
if (typeof value === "string") {
|
|
2048
|
+
const path = isPathRooted(value) ? value : `$.entity.${value}`;
|
|
2049
|
+
return identity(path);
|
|
2050
|
+
}
|
|
2051
|
+
return value;
|
|
2052
|
+
}
|
|
2053
|
+
function buildProjectionMap(projection, ownerEntity, relationProperty) {
|
|
2054
|
+
const entries = projection ? Object.entries(projection) : [];
|
|
2055
|
+
if (entries.length === 0) {
|
|
2056
|
+
throw new Error(
|
|
2057
|
+
`Relation '${relationProperty}' on '${ownerEntity}' declares maintenance (\`write.maintainedOn\`) but no \`projection\`: a maintained snapshot / collection with no captured attributes projects nothing. Declare a \`projection\` map (e.g. \`{ postId: 'postId', textPreview: preview('body', 120) }\`).`
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
const out = {};
|
|
2061
|
+
for (const [attr, value] of entries) {
|
|
2062
|
+
out[attr] = normalizeProjectionEntry(value);
|
|
2063
|
+
}
|
|
2064
|
+
return out;
|
|
2065
|
+
}
|
|
2066
|
+
function buildDestinationKeys(keyBinding) {
|
|
2067
|
+
const keys = {};
|
|
2068
|
+
const sourceFields = [];
|
|
2069
|
+
for (const [sourceField, ownerField] of Object.entries(keyBinding)) {
|
|
2070
|
+
keys[ownerField] = `$.entity.${sourceField}`;
|
|
2071
|
+
sourceFields.push(sourceField);
|
|
2072
|
+
}
|
|
2073
|
+
return { keys, sourceFields };
|
|
2074
|
+
}
|
|
2075
|
+
function sourcePayloadAttributes(meta) {
|
|
2076
|
+
const attrs = /* @__PURE__ */ new Set();
|
|
2077
|
+
for (const f of meta.fields) attrs.add(f.propertyName);
|
|
2078
|
+
if (meta.primaryKey) {
|
|
2079
|
+
for (const f of segmentFieldNames(meta.primaryKey.segmented.pkSegments)) attrs.add(f);
|
|
2080
|
+
for (const f of segmentFieldNames(meta.primaryKey.segmented.skSegments)) attrs.add(f);
|
|
2081
|
+
}
|
|
2082
|
+
for (const gsi of meta.gsiDefinitions) {
|
|
2083
|
+
for (const f of segmentFieldNames(gsi.segmented.pkSegments)) attrs.add(f);
|
|
2084
|
+
for (const f of segmentFieldNames(gsi.segmented.skSegments)) attrs.add(f);
|
|
2085
|
+
}
|
|
2086
|
+
return attrs;
|
|
2087
|
+
}
|
|
2088
|
+
function assertMaintenanceRoundTrips(args) {
|
|
2089
|
+
const {
|
|
2090
|
+
ownerEntity,
|
|
2091
|
+
relationProperty,
|
|
2092
|
+
ownerMeta,
|
|
2093
|
+
sourceEntity,
|
|
2094
|
+
sourceMeta,
|
|
2095
|
+
destinationKeyFields,
|
|
2096
|
+
keyBindingSourceFields,
|
|
2097
|
+
projection
|
|
2098
|
+
} = args;
|
|
2099
|
+
const sourceAttrs = sourcePayloadAttributes(sourceMeta);
|
|
2100
|
+
try {
|
|
2101
|
+
resolveKey([...destinationKeyFields], ownerMeta);
|
|
2102
|
+
} catch (cause) {
|
|
2103
|
+
throw new Error(
|
|
2104
|
+
`Maintenance declaration for relation '${relationProperty}' on '${ownerEntity}' binds destination-key fields [${destinationKeyFields.map((f) => `'${f}'`).join(", ")}], which cover no access-pattern partition of the maintained entity '${ownerEntity}'. The maintained snapshot / collection row could not be resolved, so the snapshot key would not point at a real target row \u2014 bind the partition-key field(s) of the index the maintained row lives on. (${cause.message})`
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
for (const field of keyBindingSourceFields) {
|
|
2108
|
+
if (!sourceAttrs.has(field)) {
|
|
2109
|
+
throw new Error(
|
|
2110
|
+
`Maintenance key binding for relation '${relationProperty}' on '${ownerEntity}' reads source field '${field}' to resolve the destination row, but the source entity '${sourceEntity}' carries no attribute '${field}' on its payload (available: ${[...sourceAttrs].sort().map((f) => `'${f}'`).join(", ")}). The destination key could not be built from the source payload \u2014 reject.`
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
for (const [attr, transform] of Object.entries(projection)) {
|
|
2115
|
+
if (!isPathRooted(transform.path)) {
|
|
2116
|
+
throw new Error(
|
|
2117
|
+
`Maintenance projection for relation '${relationProperty}' on '${ownerEntity}' captures attribute '${attr}' from '${transform.path}', which is not a payload-rooted source path (\`$.input.*\` / \`$.entity.*\`). The maintenance graph projects only from the source payload (payload \u540C\u68B1) \u2014 there is no fetch / re-projection.`
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
const srcAttr = sourceAttributeOf(transform.path);
|
|
2121
|
+
if (!sourceAttrs.has(srcAttr)) {
|
|
2122
|
+
throw new Error(
|
|
2123
|
+
`Maintenance projection for relation '${relationProperty}' on '${ownerEntity}' captures '${attr}' from '${transform.path}', but the source entity '${sourceEntity}' carries no attribute '${srcAttr}' on its payload (available: ${[...sourceAttrs].sort().map((f) => `'${f}'`).join(", ")}). Projection is payload \u540C\u68B1 only (no fetch / re-projection): an attribute the source row does not carry cannot be projected \u2014 reject.`
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
function assertCounterRoundTrips(args) {
|
|
2129
|
+
const {
|
|
2130
|
+
ownerEntity,
|
|
2131
|
+
aggregateField,
|
|
2132
|
+
ownerMeta,
|
|
2133
|
+
sourceEntity,
|
|
2134
|
+
sourceMeta,
|
|
2135
|
+
destinationKeyFields,
|
|
2136
|
+
keyBindingSourceFields,
|
|
2137
|
+
valueSourceFields
|
|
2138
|
+
} = args;
|
|
2139
|
+
const sourceAttrs = sourcePayloadAttributes(sourceMeta);
|
|
2140
|
+
try {
|
|
2141
|
+
resolveKey([...destinationKeyFields], ownerMeta);
|
|
2142
|
+
} catch (cause) {
|
|
2143
|
+
throw new Error(
|
|
2144
|
+
`Counter aggregate '${aggregateField}' on '${ownerEntity}' binds destination-key fields [${destinationKeyFields.map((f) => `'${f}'`).join(", ")}], which cover no access-pattern partition of the maintained entity '${ownerEntity}'. The counter row could not be resolved \u2014 bind the partition-key field(s) of the index the counter row lives on. (${cause.message})`
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
for (const field of keyBindingSourceFields) {
|
|
2148
|
+
if (!sourceAttrs.has(field)) {
|
|
2149
|
+
throw new Error(
|
|
2150
|
+
`Counter aggregate '${aggregateField}' on '${ownerEntity}' reads source field '${field}' to resolve the counter row, but the source entity '${sourceEntity}' carries no attribute '${field}' on its payload (available: ${[...sourceAttrs].sort().map((f) => `'${f}'`).join(", ")}). The destination key could not be built from the source payload \u2014 reject.`
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
for (const field of valueSourceFields) {
|
|
2155
|
+
if (!sourceAttrs.has(field)) {
|
|
2156
|
+
throw new Error(
|
|
2157
|
+
`Counter aggregate '${aggregateField}' on '${ownerEntity}' tracks \`max('${field}')\`, but the source entity '${sourceEntity}' carries no attribute '${field}' on its payload (available: ${[...sourceAttrs].sort().map((f) => `'${f}'`).join(", ")}). A counter aggregates only the source payload (payload \u540C\u68B1) \u2014 reject.`
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
function logicalNameOf(meta) {
|
|
2163
|
+
return meta.prefix.endsWith("#") ? meta.prefix.slice(0, -1) : meta.prefix;
|
|
2164
|
+
}
|
|
2165
|
+
function resolveEntityByName(name, registry) {
|
|
2166
|
+
for (const [klass, meta] of registry) {
|
|
2167
|
+
if (logicalNameOf(meta) === name) return klass;
|
|
2168
|
+
}
|
|
2169
|
+
return void 0;
|
|
2170
|
+
}
|
|
2171
|
+
function parseTrigger(raw) {
|
|
2172
|
+
const idx = raw.lastIndexOf(".");
|
|
2173
|
+
return { entity: raw.slice(0, idx), event: raw.slice(idx + 1) };
|
|
2174
|
+
}
|
|
2175
|
+
function buildEffect(relation, ownerFactory, trigger, keys, project) {
|
|
2176
|
+
const write = relation.options?.write;
|
|
2177
|
+
const updateMode = write?.updateMode;
|
|
2178
|
+
const consistency = write?.consistency;
|
|
2179
|
+
const base = {
|
|
2180
|
+
trigger,
|
|
2181
|
+
targetFactory: ownerFactory,
|
|
2182
|
+
keys,
|
|
2183
|
+
project,
|
|
2184
|
+
...updateMode !== void 0 ? { updateMode } : {},
|
|
2185
|
+
...consistency !== void 0 ? { consistency } : {}
|
|
2186
|
+
};
|
|
2187
|
+
if (relation.type === "hasMany") {
|
|
2188
|
+
const read = relation.options?.read;
|
|
2189
|
+
const collection = {
|
|
2190
|
+
field: relation.propertyName,
|
|
2191
|
+
...read?.maxItems !== void 0 ? { maxItems: read.maxItems } : {}
|
|
2192
|
+
};
|
|
2193
|
+
return { kind: "collection", ...base, collection };
|
|
2194
|
+
}
|
|
2195
|
+
return { kind: "snapshot", ...base };
|
|
2196
|
+
}
|
|
2197
|
+
function buildCounterEffect(aggregate, ownerFactory, trigger, event, keys) {
|
|
2198
|
+
const write = aggregate.options.write;
|
|
2199
|
+
const updateMode = write?.updateMode;
|
|
2200
|
+
const consistency = write?.consistency;
|
|
2201
|
+
const value = aggregate.options.value.op === "count" ? { op: "count" } : { op: "max", field: aggregate.options.value.field };
|
|
2202
|
+
return {
|
|
2203
|
+
kind: "counter",
|
|
2204
|
+
trigger,
|
|
2205
|
+
targetFactory: ownerFactory,
|
|
2206
|
+
keys,
|
|
2207
|
+
attribute: aggregate.propertyName,
|
|
2208
|
+
value,
|
|
2209
|
+
...value.op === "count" ? { delta: COUNT_DELTA_FOR_EVENT[event] } : {},
|
|
2210
|
+
...updateMode !== void 0 ? { updateMode } : {},
|
|
2211
|
+
...consistency !== void 0 ? { consistency } : {}
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
function destinationRowKeyOf(ownerEntity, keys) {
|
|
2215
|
+
const parts = Object.entries(keys).map(([field, path]) => `${field}=${path}`).sort();
|
|
2216
|
+
return `${ownerEntity}#${parts.join("&")}`;
|
|
2217
|
+
}
|
|
2218
|
+
function assertNoCycle(items) {
|
|
2219
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
2220
|
+
for (const item of items) {
|
|
2221
|
+
if (!adjacency.has(item.sourceEntity)) adjacency.set(item.sourceEntity, /* @__PURE__ */ new Set());
|
|
2222
|
+
adjacency.get(item.sourceEntity).add(item.ownerEntity);
|
|
2223
|
+
}
|
|
2224
|
+
const WHITE = 0;
|
|
2225
|
+
const GRAY = 1;
|
|
2226
|
+
const BLACK = 2;
|
|
2227
|
+
const color = /* @__PURE__ */ new Map();
|
|
2228
|
+
for (const node of adjacency.keys()) color.set(node, WHITE);
|
|
2229
|
+
for (const start of adjacency.keys()) {
|
|
2230
|
+
if (color.get(start) !== WHITE) continue;
|
|
2231
|
+
const stack = [];
|
|
2232
|
+
const path = [];
|
|
2233
|
+
color.set(start, GRAY);
|
|
2234
|
+
path.push(start);
|
|
2235
|
+
stack.push({ node: start, iter: (adjacency.get(start) ?? /* @__PURE__ */ new Set()).values() });
|
|
2236
|
+
while (stack.length > 0) {
|
|
2237
|
+
const top = stack[stack.length - 1];
|
|
2238
|
+
const next = top.iter.next();
|
|
2239
|
+
if (next.done) {
|
|
2240
|
+
color.set(top.node, BLACK);
|
|
2241
|
+
stack.pop();
|
|
2242
|
+
path.pop();
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
const child = next.value;
|
|
2246
|
+
const childColor = color.get(child) ?? WHITE;
|
|
2247
|
+
if (childColor === GRAY) {
|
|
2248
|
+
const from2 = path.indexOf(child);
|
|
2249
|
+
const cycle = [...path.slice(from2), child].join(" \u2192 ");
|
|
2250
|
+
throw new Error(
|
|
2251
|
+
`Maintenance dependency cycle detected: ${cycle}. A maintenance trigger chain that loops back on itself is an unbounded cascade \u2014 break the cycle (an entity's maintained shape must not, transitively, be triggered by its own maintenance).`
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
if (childColor === WHITE) {
|
|
2255
|
+
color.set(child, GRAY);
|
|
2256
|
+
path.push(child);
|
|
2257
|
+
stack.push({ node: child, iter: (adjacency.get(child) ?? /* @__PURE__ */ new Set()).values() });
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
function buildMaintenanceGraph(registry = MetadataRegistry.getAll()) {
|
|
2263
|
+
const items = [];
|
|
2264
|
+
const owners = [...registry.entries()].map(([klass, meta]) => ({ klass, meta })).sort((a, b) => a.klass.name < b.klass.name ? -1 : a.klass.name > b.klass.name ? 1 : 0);
|
|
2265
|
+
for (const { klass: ownerClass, meta: ownerMeta } of owners) {
|
|
2266
|
+
const ownerEntity = logicalNameOf(ownerMeta);
|
|
2267
|
+
const relations = [...ownerMeta.relations].sort(
|
|
2268
|
+
(a, b) => a.propertyName < b.propertyName ? -1 : a.propertyName > b.propertyName ? 1 : 0
|
|
2269
|
+
);
|
|
2270
|
+
for (const relation of relations) {
|
|
2271
|
+
const maintainedOn = relation.options?.write?.maintainedOn;
|
|
2272
|
+
if (!maintainedOn || maintainedOn.length === 0) continue;
|
|
2273
|
+
const sourceClass = relation.targetFactory();
|
|
2274
|
+
const sourceMeta = MetadataRegistry.get(sourceClass);
|
|
2275
|
+
const relationTargetEntity = logicalNameOf(sourceMeta);
|
|
2276
|
+
const project = buildProjectionMap(
|
|
2277
|
+
relation.options?.projection,
|
|
2278
|
+
ownerEntity,
|
|
2279
|
+
relation.propertyName
|
|
2280
|
+
);
|
|
2281
|
+
const { keys, sourceFields } = buildDestinationKeys(relation.keyBinding);
|
|
2282
|
+
const destinationKeyFields = Object.keys(keys);
|
|
2283
|
+
for (const raw of maintainedOn) {
|
|
2284
|
+
if (!isMaintainTrigger(raw)) {
|
|
2285
|
+
const { event } = parseTrigger(raw);
|
|
2286
|
+
throw new Error(
|
|
2287
|
+
`Maintenance trigger ${JSON.stringify(raw)} on relation '${relation.propertyName}' of '${ownerEntity}' is not a well-formed \`Entity.event\` trigger (event must be one of ${MAINTAIN_EVENTS.map((e) => `'${e}'`).join(" / ")}; got '${event}'). Fix the \`write.maintainedOn\` entry.`
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
const trigger = raw;
|
|
2291
|
+
const { entity: triggerEntity } = parseTrigger(trigger);
|
|
2292
|
+
if (!resolveEntityByName(triggerEntity, registry)) {
|
|
2293
|
+
throw new Error(
|
|
2294
|
+
`Maintenance trigger '${trigger}' on relation '${relation.propertyName}' of '${ownerEntity}' names source entity '${triggerEntity}', which is not a registered model. Declare a model named '${triggerEntity}' or fix the trigger's entity segment.`
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
if (triggerEntity !== relationTargetEntity) {
|
|
2298
|
+
throw new Error(
|
|
2299
|
+
`Maintenance trigger '${trigger}' on relation '${relation.propertyName}' of '${ownerEntity}' names source entity '${triggerEntity}', but the relation's target (the projected source) is '${relationTargetEntity}'. #124 projects only from the relation target's payload (payload \u540C\u68B1); a trigger on a different entity needs a write-time context fetch, which is Phase 2 \u2014 restrict \`maintainedOn\` to '${relationTargetEntity}.*' triggers.`
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
assertMaintenanceRoundTrips({
|
|
2303
|
+
ownerEntity,
|
|
2304
|
+
relationProperty: relation.propertyName,
|
|
2305
|
+
ownerMeta,
|
|
2306
|
+
sourceEntity: triggerEntity,
|
|
2307
|
+
sourceMeta,
|
|
2308
|
+
destinationKeyFields,
|
|
2309
|
+
keyBindingSourceFields: sourceFields,
|
|
2310
|
+
projection: project
|
|
2311
|
+
});
|
|
2312
|
+
const effect = buildEffect(relation, () => ownerClass, trigger, keys, project);
|
|
2313
|
+
items.push({
|
|
2314
|
+
trigger,
|
|
2315
|
+
sourceEntity: triggerEntity,
|
|
2316
|
+
sourceClass,
|
|
2317
|
+
ownerEntity,
|
|
2318
|
+
ownerClass,
|
|
2319
|
+
relationProperty: relation.propertyName,
|
|
2320
|
+
destinationRowKey: destinationRowKeyOf(ownerEntity, keys),
|
|
2321
|
+
effect
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
const aggregates = [...ownerMeta.aggregates].sort(
|
|
2326
|
+
(a, b) => a.propertyName < b.propertyName ? -1 : a.propertyName > b.propertyName ? 1 : 0
|
|
2327
|
+
);
|
|
2328
|
+
for (const agg of aggregates) {
|
|
2329
|
+
const maintainedOn = agg.options.write?.maintainedOn;
|
|
2330
|
+
if (!maintainedOn || maintainedOn.length === 0) continue;
|
|
2331
|
+
const sourceClass = agg.targetFactory();
|
|
2332
|
+
const sourceMeta = MetadataRegistry.get(sourceClass);
|
|
2333
|
+
const aggSourceEntity = logicalNameOf(sourceMeta);
|
|
2334
|
+
const { keys, sourceFields } = buildDestinationKeys(agg.keyBinding);
|
|
2335
|
+
const destinationKeyFields = Object.keys(keys);
|
|
2336
|
+
const valueSourceFields = agg.options.value.op === "max" ? [agg.options.value.field] : [];
|
|
2337
|
+
for (const raw of maintainedOn) {
|
|
2338
|
+
if (!isMaintainTrigger(raw)) {
|
|
2339
|
+
const { event: event2 } = parseTrigger(raw);
|
|
2340
|
+
throw new Error(
|
|
2341
|
+
`Maintenance trigger ${JSON.stringify(raw)} on \`@aggregate\` field '${agg.propertyName}' of '${ownerEntity}' is not a well-formed \`Entity.event\` trigger (event must be one of ${MAINTAIN_EVENTS.map((e) => `'${e}'`).join(" / ")}; got '${event2}'). Fix the \`write.maintainedOn\` entry.`
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
const trigger = raw;
|
|
2345
|
+
const parsed = parseTrigger(trigger);
|
|
2346
|
+
const triggerEntity = parsed.entity;
|
|
2347
|
+
const event = parsed.event;
|
|
2348
|
+
if (!resolveEntityByName(triggerEntity, registry)) {
|
|
2349
|
+
throw new Error(
|
|
2350
|
+
`Maintenance trigger '${trigger}' on \`@aggregate\` field '${agg.propertyName}' of '${ownerEntity}' names source entity '${triggerEntity}', which is not a registered model. Declare a model named '${triggerEntity}' or fix the trigger's entity segment.`
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
if (triggerEntity !== aggSourceEntity) {
|
|
2354
|
+
throw new Error(
|
|
2355
|
+
`Maintenance trigger '${trigger}' on \`@aggregate\` field '${agg.propertyName}' of '${ownerEntity}' names source entity '${triggerEntity}', but the aggregate's source (the entity being counted) is '${aggSourceEntity}'. A counter aggregates only the rows of its own source (payload \u540C\u68B1); a trigger on a different entity is Phase 2 \u2014 restrict \`maintainedOn\` to '${aggSourceEntity}.*' triggers.`
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
if (agg.options.value.op === "count" && COUNT_DELTA_FOR_EVENT[event] === 0) {
|
|
2359
|
+
throw new Error(
|
|
2360
|
+
`\`@aggregate\` field '${agg.propertyName}' of '${ownerEntity}' is a \`count()\` counter maintained on '${trigger}', but a 'updated' event does not change a row count (it adds 0). A \`count()\` counter is maintained on 'created' (+1) and 'removed' (-1); drop the 'updated' trigger.`
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
assertCounterRoundTrips({
|
|
2364
|
+
ownerEntity,
|
|
2365
|
+
aggregateField: agg.propertyName,
|
|
2366
|
+
ownerMeta,
|
|
2367
|
+
sourceEntity: triggerEntity,
|
|
2368
|
+
sourceMeta,
|
|
2369
|
+
destinationKeyFields,
|
|
2370
|
+
keyBindingSourceFields: sourceFields,
|
|
2371
|
+
valueSourceFields
|
|
2372
|
+
});
|
|
2373
|
+
const effect = buildCounterEffect(agg, () => ownerClass, trigger, event, keys);
|
|
2374
|
+
items.push({
|
|
2375
|
+
trigger,
|
|
2376
|
+
sourceEntity: triggerEntity,
|
|
2377
|
+
sourceClass,
|
|
2378
|
+
ownerEntity,
|
|
2379
|
+
ownerClass,
|
|
2380
|
+
relationProperty: agg.propertyName,
|
|
2381
|
+
destinationRowKey: destinationRowKeyOf(ownerEntity, keys),
|
|
2382
|
+
effect
|
|
2383
|
+
});
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
assertNoCycle(items);
|
|
2388
|
+
const byTrigger = /* @__PURE__ */ new Map();
|
|
2389
|
+
const sameRow = /* @__PURE__ */ new Map();
|
|
2390
|
+
for (const item of items) {
|
|
2391
|
+
const triggerBucket = byTrigger.get(item.trigger);
|
|
2392
|
+
if (triggerBucket) triggerBucket.push(item);
|
|
2393
|
+
else byTrigger.set(item.trigger, [item]);
|
|
2394
|
+
const rowKey = `${item.trigger}\0${item.destinationRowKey}`;
|
|
2395
|
+
const rowBucket = sameRow.get(rowKey);
|
|
2396
|
+
if (rowBucket) rowBucket.push(item);
|
|
2397
|
+
else sameRow.set(rowKey, [item]);
|
|
2398
|
+
}
|
|
2399
|
+
const multiMaintainerTargets = /* @__PURE__ */ new Map();
|
|
2400
|
+
for (const [rowKey, bucket] of sameRow) {
|
|
2401
|
+
if (bucket.length > 1) multiMaintainerTargets.set(rowKey, bucket);
|
|
2402
|
+
}
|
|
2403
|
+
return {
|
|
2404
|
+
items,
|
|
2405
|
+
byTrigger,
|
|
2406
|
+
effectsFor(trigger) {
|
|
2407
|
+
return byTrigger.get(trigger) ?? [];
|
|
2408
|
+
},
|
|
2409
|
+
multiMaintainerTargets
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
|
|
1820
2413
|
// src/runtime/command-runtime.ts
|
|
1821
2414
|
function resolveCommandMethod(contract, methodName) {
|
|
1822
2415
|
const method = contract.methods[methodName];
|
|
@@ -2076,7 +2669,121 @@ function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
|
|
|
2076
2669
|
return { Put: put };
|
|
2077
2670
|
});
|
|
2078
2671
|
}
|
|
2079
|
-
|
|
2672
|
+
function applyProjectionTransform(op, args, value, contextLabel) {
|
|
2673
|
+
if (op === "identity") return value;
|
|
2674
|
+
if (op === "preview") {
|
|
2675
|
+
const n = args[0];
|
|
2676
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
|
|
2677
|
+
throw new Error(
|
|
2678
|
+
`Contract runtime: ${contextLabel} declares a \`preview\` projection with a non-positive-integer length bound (${JSON.stringify(n)}). A preview length must be a positive integer.`
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
2681
|
+
if (value === null || value === void 0) return value;
|
|
2682
|
+
return String(value).slice(0, n);
|
|
2683
|
+
}
|
|
2684
|
+
throw new Error(
|
|
2685
|
+
`Contract runtime: ${contextLabel} declares an unknown projection transform op '${String(op)}' (expected 'identity' or 'preview').`
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2688
|
+
function buildMaintainProjection(maintain, key, params, contextLabel) {
|
|
2689
|
+
const out = {};
|
|
2690
|
+
for (const [attr, transform] of Object.entries(maintain.projection)) {
|
|
2691
|
+
const value = renderTemplate(
|
|
2692
|
+
`{${transform.inputField}}`,
|
|
2693
|
+
key,
|
|
2694
|
+
params,
|
|
2695
|
+
`${contextLabel} projection '${attr}'`
|
|
2696
|
+
);
|
|
2697
|
+
out[attr] = applyProjectionTransform(transform.op, transform.args, value, `${contextLabel} '${attr}'`);
|
|
2698
|
+
}
|
|
2699
|
+
return out;
|
|
2700
|
+
}
|
|
2701
|
+
function renderMaintainWriteItem(maintain, key, params, contextLabel, modelBySignature) {
|
|
2702
|
+
const ownerClass = maintain.entity.modelClass;
|
|
2703
|
+
const metadata = MetadataRegistry.get(ownerClass);
|
|
2704
|
+
if (!metadata.primaryKey) {
|
|
2705
|
+
throw new Error(
|
|
2706
|
+
`Contract runtime: ${contextLabel} maintains a snapshot / collection on '${maintain.entity.name}', which has no primary key.`
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
const keyInput = {};
|
|
2710
|
+
for (const [ownerField, inputField] of Object.entries(maintain.keyBinding)) {
|
|
2711
|
+
keyInput[ownerField] = renderTemplate(
|
|
2712
|
+
`{${inputField}}`,
|
|
2713
|
+
key,
|
|
2714
|
+
params,
|
|
2715
|
+
`${contextLabel} maintain key`
|
|
2716
|
+
);
|
|
2717
|
+
}
|
|
2718
|
+
const { TableName, Key } = buildDeleteInput(
|
|
2719
|
+
ownerClass,
|
|
2720
|
+
keyInput
|
|
2721
|
+
);
|
|
2722
|
+
let updateInput;
|
|
2723
|
+
if (maintain.kind === "counter") {
|
|
2724
|
+
const counter = maintain.counter;
|
|
2725
|
+
if (counter === void 0) {
|
|
2726
|
+
throw new Error(
|
|
2727
|
+
`Contract runtime: ${contextLabel} is a 'counter' maintenance write with no \`counter\` payload \u2014 the compiler (#141) should have set it.`
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
updateInput = {
|
|
2731
|
+
TableName,
|
|
2732
|
+
Key,
|
|
2733
|
+
UpdateExpression: "ADD #a0 :a0",
|
|
2734
|
+
ExpressionAttributeNames: { "#a0": counter.attribute },
|
|
2735
|
+
ExpressionAttributeValues: { ":a0": counter.delta }
|
|
2736
|
+
};
|
|
2737
|
+
const out2 = { Update: updateInput };
|
|
2738
|
+
modelBySignature?.set(execItemKeySignature(out2), ownerClass.name);
|
|
2739
|
+
return out2;
|
|
2740
|
+
}
|
|
2741
|
+
const projection = buildMaintainProjection(maintain, key, params, contextLabel);
|
|
2742
|
+
if (maintain.kind === "collection") {
|
|
2743
|
+
const field = maintain.collection?.field;
|
|
2744
|
+
if (field === void 0) {
|
|
2745
|
+
throw new Error(
|
|
2746
|
+
`Contract runtime: ${contextLabel} is a 'collection' maintenance write with no \`collection.field\` \u2014 the compiler (#125) should have set it.`
|
|
2747
|
+
);
|
|
2748
|
+
}
|
|
2749
|
+
updateInput = {
|
|
2750
|
+
TableName,
|
|
2751
|
+
Key,
|
|
2752
|
+
UpdateExpression: "SET #c = list_append(if_not_exists(#c, :empty), :item)",
|
|
2753
|
+
ExpressionAttributeNames: { "#c": field },
|
|
2754
|
+
ExpressionAttributeValues: { ":empty": [], ":item": [projection] }
|
|
2755
|
+
};
|
|
2756
|
+
} else {
|
|
2757
|
+
const setClauses = [];
|
|
2758
|
+
const names = {};
|
|
2759
|
+
const values = {};
|
|
2760
|
+
let i = 0;
|
|
2761
|
+
for (const [attr, value] of Object.entries(projection)) {
|
|
2762
|
+
const nameTok = `#m${i}`;
|
|
2763
|
+
const valTok = `:m${i}`;
|
|
2764
|
+
setClauses.push(`${nameTok} = ${valTok}`);
|
|
2765
|
+
names[nameTok] = attr;
|
|
2766
|
+
values[valTok] = value;
|
|
2767
|
+
i += 1;
|
|
2768
|
+
}
|
|
2769
|
+
if (setClauses.length === 0) {
|
|
2770
|
+
throw new Error(
|
|
2771
|
+
`Contract runtime: ${contextLabel} is a 'snapshot' maintenance write with an empty projection \u2014 nothing to mirror onto '${maintain.entity.name}'.`
|
|
2772
|
+
);
|
|
2773
|
+
}
|
|
2774
|
+
updateInput = {
|
|
2775
|
+
TableName,
|
|
2776
|
+
Key,
|
|
2777
|
+
UpdateExpression: `SET ${setClauses.join(", ")}`,
|
|
2778
|
+
ExpressionAttributeNames: names,
|
|
2779
|
+
ExpressionAttributeValues: values
|
|
2780
|
+
};
|
|
2781
|
+
}
|
|
2782
|
+
const out = { Update: updateInput };
|
|
2783
|
+
modelBySignature?.set(execItemKeySignature(out), ownerClass.name);
|
|
2784
|
+
return out;
|
|
2785
|
+
}
|
|
2786
|
+
async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, maintainItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map(), retry, middleware) {
|
|
2080
2787
|
const writeItems = writes.map((w) => {
|
|
2081
2788
|
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
2082
2789
|
let item;
|
|
@@ -2099,13 +2806,15 @@ async function executeReferentialTransaction(writes, edgeItems, updateItems, gua
|
|
|
2099
2806
|
...guardItemsRaw,
|
|
2100
2807
|
...outboxItems,
|
|
2101
2808
|
...idempotencyItems,
|
|
2809
|
+
...maintainItems,
|
|
2102
2810
|
...checkItems
|
|
2103
2811
|
],
|
|
2104
2812
|
{
|
|
2105
2813
|
modelBySignature,
|
|
2106
2814
|
...retry !== void 0 ? { retry } : {},
|
|
2815
|
+
...middleware !== void 0 ? { middleware } : {},
|
|
2107
2816
|
limitError: (count) => new Error(
|
|
2108
|
-
`Contract runtime: command method '${methodName}' composes ${count} items (writes + edges + derived updates + uniqueness guards + outbox events + idempotency guard + ConditionChecks) into one TransactWriteItems, but DynamoDB caps a transaction at ${MAX_TRANSACT_ITEMS} items and a transaction is atomic \u2014 it cannot be split. Reduce the fragment / effect count.`
|
|
2817
|
+
`Contract runtime: command method '${methodName}' composes ${count} items (writes + edges + derived updates + uniqueness guards + outbox events + idempotency guard + maintenance writes + ConditionChecks) into one TransactWriteItems, but DynamoDB caps a transaction at ${MAX_TRANSACT_ITEMS} items and a transaction is atomic \u2014 it cannot be split. Reduce the fragment / effect count.`
|
|
2109
2818
|
)
|
|
2110
2819
|
}
|
|
2111
2820
|
);
|
|
@@ -2165,10 +2874,10 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2165
2874
|
`Contract runtime: command method '${methodName}' is a multi-fragment mutation (it composes ${method.ops.length} fragments into one atomic TransactWriteItems) and accepts a single key only. Call it once per target, not with a key array.`
|
|
2166
2875
|
);
|
|
2167
2876
|
}
|
|
2168
|
-
const hasDerivedEffects2 = method.conditionChecks !== void 0 && method.conditionChecks.length > 0 || method.edgeWrites !== void 0 && method.edgeWrites.length > 0 || method.derivedUpdates !== void 0 && method.derivedUpdates.length > 0 || method.uniqueGuards !== void 0 && method.uniqueGuards.length > 0 || method.outboxEvents !== void 0 && method.outboxEvents.length > 0 || method.idempotencyGuard !== void 0;
|
|
2877
|
+
const hasDerivedEffects2 = method.conditionChecks !== void 0 && method.conditionChecks.length > 0 || method.edgeWrites !== void 0 && method.edgeWrites.length > 0 || method.derivedUpdates !== void 0 && method.derivedUpdates.length > 0 || method.uniqueGuards !== void 0 && method.uniqueGuards.length > 0 || method.outboxEvents !== void 0 && method.outboxEvents.length > 0 || method.maintainWrites !== void 0 && method.maintainWrites.length > 0 || method.idempotencyGuard !== void 0;
|
|
2169
2878
|
if (hasDerivedEffects2) {
|
|
2170
2879
|
throw new Error(
|
|
2171
|
-
`Contract runtime: command method '${methodName}' carries derived effects (referential integrity / edge writes / derived counters / uniqueness guards / outbox events / idempotency guard \u2014 it composes its write + those items into one atomic TransactWriteItems) and accepts a single key only. Call it once per target, not with a key array.`
|
|
2880
|
+
`Contract runtime: command method '${methodName}' carries derived effects (referential integrity / edge writes / derived counters / uniqueness guards / outbox events / idempotency guard / maintenance writes \u2014 it composes its write + those items into one atomic TransactWriteItems) and accepts a single key only. Call it once per target, not with a key array.`
|
|
2172
2881
|
);
|
|
2173
2882
|
}
|
|
2174
2883
|
const writes = keys.map(
|
|
@@ -2187,7 +2896,8 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2187
2896
|
const uniqueGuards = method.uniqueGuards ?? [];
|
|
2188
2897
|
const outboxEvents = method.outboxEvents ?? [];
|
|
2189
2898
|
const idempotencyGuard = method.idempotencyGuard;
|
|
2190
|
-
const
|
|
2899
|
+
const maintainWrites = method.maintainWrites ?? [];
|
|
2900
|
+
const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || maintainWrites.length > 0 || idempotencyGuard !== void 0;
|
|
2191
2901
|
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
2192
2902
|
if (isMultiFragment || hasDerivedEffects) {
|
|
2193
2903
|
const ops = method.ops ?? [method.op];
|
|
@@ -2226,6 +2936,15 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2226
2936
|
params,
|
|
2227
2937
|
`command method '${methodName}' (idempotency)`
|
|
2228
2938
|
) : [];
|
|
2939
|
+
const maintainItems = maintainWrites.map(
|
|
2940
|
+
(m, i) => renderMaintainWriteItem(
|
|
2941
|
+
m,
|
|
2942
|
+
key,
|
|
2943
|
+
params,
|
|
2944
|
+
`command method '${methodName}' (maintain #${i})`,
|
|
2945
|
+
modelBySignature
|
|
2946
|
+
)
|
|
2947
|
+
);
|
|
2229
2948
|
const checks = conditionChecks.map((check, i) => ({
|
|
2230
2949
|
check: renderConditionCheck(
|
|
2231
2950
|
check,
|
|
@@ -2241,6 +2960,7 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2241
2960
|
guardItems,
|
|
2242
2961
|
outboxItems,
|
|
2243
2962
|
idempotencyItems,
|
|
2963
|
+
maintainItems,
|
|
2244
2964
|
checks,
|
|
2245
2965
|
methodName,
|
|
2246
2966
|
modelBySignature
|
|
@@ -2269,6 +2989,7 @@ function renderCompiledOp(op, ctx) {
|
|
|
2269
2989
|
const guardItems = [];
|
|
2270
2990
|
const outboxItems = [];
|
|
2271
2991
|
const idempotencyItems = [];
|
|
2992
|
+
const maintainItems = [];
|
|
2272
2993
|
const checkItems = [];
|
|
2273
2994
|
for (const edge of fragment.edgeWrites ?? []) {
|
|
2274
2995
|
edgeItems.push(...renderEdgeWriteItems(edge, op.params, op.params, `${ctx} edge`, modelBySignature));
|
|
@@ -2287,10 +3008,15 @@ function renderCompiledOp(op, ctx) {
|
|
|
2287
3008
|
...renderIdempotencyGuardItems(fragment.idempotencyGuard, op.params, op.params, `${ctx} idempotency`)
|
|
2288
3009
|
);
|
|
2289
3010
|
}
|
|
3011
|
+
for (const m of fragment.maintainWrites ?? []) {
|
|
3012
|
+
maintainItems.push(
|
|
3013
|
+
renderMaintainWriteItem(m, op.params, op.params, `${ctx} maintain`, modelBySignature)
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
2290
3016
|
for (const c of fragment.conditionChecks ?? []) {
|
|
2291
3017
|
checkItems.push({ check: renderConditionCheck(c, op.params, op.params, `${ctx} requires`) });
|
|
2292
3018
|
}
|
|
2293
|
-
const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || checkItems.length > 0;
|
|
3019
|
+
const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || maintainItems.length > 0 || checkItems.length > 0;
|
|
2294
3020
|
return {
|
|
2295
3021
|
write,
|
|
2296
3022
|
edgeItems,
|
|
@@ -2298,6 +3024,7 @@ function renderCompiledOp(op, ctx) {
|
|
|
2298
3024
|
guardItems,
|
|
2299
3025
|
outboxItems,
|
|
2300
3026
|
idempotencyItems,
|
|
3027
|
+
maintainItems,
|
|
2301
3028
|
checkItems,
|
|
2302
3029
|
modelBySignature,
|
|
2303
3030
|
hasDerivedEffects
|
|
@@ -2317,7 +3044,24 @@ function readBackForCompiledOp(op, write) {
|
|
|
2317
3044
|
}
|
|
2318
3045
|
async function executeCompiledWriteOp(ops, options) {
|
|
2319
3046
|
const label = options.label;
|
|
2320
|
-
const
|
|
3047
|
+
const writeMw = buildWriteRuntime(options.context);
|
|
3048
|
+
const writeCtxs = [];
|
|
3049
|
+
const txId = { id: /* @__PURE__ */ Symbol("graphddb.mutate") };
|
|
3050
|
+
let rewrites = [];
|
|
3051
|
+
if (writeMw.active) {
|
|
3052
|
+
const w1ed = [];
|
|
3053
|
+
for (const op of ops) {
|
|
3054
|
+
const r = await runEnvelopeW1(op, writeMw, txId, writeCtxs);
|
|
3055
|
+
w1ed.push(r.op);
|
|
3056
|
+
rewrites.push(r.rewritten ?? null);
|
|
3057
|
+
}
|
|
3058
|
+
ops = w1ed;
|
|
3059
|
+
} else {
|
|
3060
|
+
rewrites = ops.map(() => null);
|
|
3061
|
+
}
|
|
3062
|
+
const renderedOps = ops.map(
|
|
3063
|
+
(op, i) => rewrites[i] !== null ? rewrittenCompiledOp(rewrites[i]) : renderCompiledOp(op, `${label} (op #${i})`)
|
|
3064
|
+
);
|
|
2321
3065
|
const rendered = renderedOps.map((r) => r.write);
|
|
2322
3066
|
const modelBySignature = /* @__PURE__ */ new Map();
|
|
2323
3067
|
const edgeItems = [];
|
|
@@ -2325,6 +3069,7 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
2325
3069
|
const guardItems = [];
|
|
2326
3070
|
const outboxItems = [];
|
|
2327
3071
|
const idempotencyItems = [];
|
|
3072
|
+
const maintainItems = [];
|
|
2328
3073
|
const checkItems = [];
|
|
2329
3074
|
for (const r of renderedOps) {
|
|
2330
3075
|
for (const [sig, name] of r.modelBySignature) modelBySignature.set(sig, name);
|
|
@@ -2333,11 +3078,17 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
2333
3078
|
guardItems.push(...r.guardItems);
|
|
2334
3079
|
outboxItems.push(...r.outboxItems);
|
|
2335
3080
|
idempotencyItems.push(...r.idempotencyItems);
|
|
3081
|
+
maintainItems.push(...r.maintainItems);
|
|
2336
3082
|
checkItems.push(...r.checkItems);
|
|
2337
3083
|
}
|
|
2338
3084
|
const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
|
|
3085
|
+
const origins = writeMw.active ? rendered.map((w) => ({ model: ctxModelFor(w.modelClass), kind: w.operation })) : [];
|
|
2339
3086
|
if (rendered.length === 1 && !hasDerivedEffects) {
|
|
2340
|
-
|
|
3087
|
+
if (writeMw.active) {
|
|
3088
|
+
await persistSingleWriteWithHooks(rendered[0], writeMw, origins, txId, options.retry);
|
|
3089
|
+
} else {
|
|
3090
|
+
await executeSingleWrite(rendered[0], options.retry);
|
|
3091
|
+
}
|
|
2341
3092
|
} else {
|
|
2342
3093
|
await executeReferentialTransaction(
|
|
2343
3094
|
rendered,
|
|
@@ -2346,26 +3097,120 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
2346
3097
|
guardItems,
|
|
2347
3098
|
outboxItems,
|
|
2348
3099
|
idempotencyItems,
|
|
3100
|
+
maintainItems,
|
|
2349
3101
|
checkItems,
|
|
2350
3102
|
label,
|
|
2351
3103
|
modelBySignature,
|
|
2352
|
-
options.retry
|
|
3104
|
+
options.retry,
|
|
3105
|
+
writeMw.active ? { runtime: writeMw, origins, transaction: txId } : void 0
|
|
2353
3106
|
);
|
|
2354
3107
|
}
|
|
3108
|
+
if (writeMw.active) {
|
|
3109
|
+
for (let i = writeCtxs.length - 1; i >= 0; i--) {
|
|
3110
|
+
await writeMw.runWriteAfter(writeCtxs[i], {});
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
2355
3113
|
const readBacks = await Promise.all(
|
|
2356
3114
|
ops.map((op, i) => readBackForCompiledOp(op, rendered[i]))
|
|
2357
3115
|
);
|
|
2358
3116
|
return { readBacks };
|
|
2359
3117
|
}
|
|
3118
|
+
async function persistSingleWriteWithHooks(w, runtime, origins, transaction, retry) {
|
|
3119
|
+
const options = w.condition !== void 0 || retry !== void 0 ? {
|
|
3120
|
+
...w.condition !== void 0 ? { condition: w.condition } : {},
|
|
3121
|
+
...retry !== void 0 ? { retry } : {}
|
|
3122
|
+
} : void 0;
|
|
3123
|
+
let item;
|
|
3124
|
+
if (w.operation === "put") {
|
|
3125
|
+
item = { Put: buildPutInput(w.modelClass, w.item ?? {}, options) };
|
|
3126
|
+
} else if (w.operation === "update") {
|
|
3127
|
+
item = { Update: buildUpdateInput(w.modelClass, w.key, w.changes ?? {}, options) };
|
|
3128
|
+
} else {
|
|
3129
|
+
item = { Delete: buildDeleteInput(w.modelClass, w.key, options) };
|
|
3130
|
+
}
|
|
3131
|
+
const persistCtx = runtime.persistCtx([item], origins, transaction);
|
|
3132
|
+
await runtime.runPersist(persistCtx, async (items) => {
|
|
3133
|
+
const sent = items[0];
|
|
3134
|
+
const sendOpts = retry !== void 0 ? { retry } : void 0;
|
|
3135
|
+
const exec = ClientManager.getExecutor();
|
|
3136
|
+
if ("Put" in sent) return exec.put(sent.Put, sendOpts);
|
|
3137
|
+
if ("Update" in sent) return exec.update(sent.Update, sendOpts);
|
|
3138
|
+
if ("Delete" in sent) return exec.delete(sent.Delete, sendOpts);
|
|
3139
|
+
throw new Error("graphddb middleware: a single-op persist received a ConditionCheck.");
|
|
3140
|
+
});
|
|
3141
|
+
}
|
|
3142
|
+
async function runEnvelopeW1(op, runtime, transaction, writeCtxs) {
|
|
3143
|
+
const kind = op.fragment.op.operation;
|
|
3144
|
+
const keyFields = new Set(op.fragment.keyFields);
|
|
3145
|
+
const input = {};
|
|
3146
|
+
if (kind === "put") {
|
|
3147
|
+
input.item = { ...op.params };
|
|
3148
|
+
} else {
|
|
3149
|
+
const key = {};
|
|
3150
|
+
const changes = {};
|
|
3151
|
+
for (const [k, v] of Object.entries(op.params)) {
|
|
3152
|
+
if (keyFields.has(k)) key[k] = v;
|
|
3153
|
+
else changes[k] = v;
|
|
3154
|
+
}
|
|
3155
|
+
input.key = key;
|
|
3156
|
+
input.changes = changes;
|
|
3157
|
+
}
|
|
3158
|
+
const modelClass = op.fragment.op.entity.modelClass;
|
|
3159
|
+
const ctx = runtime.writeCtx(kind, ctxModelFor(modelClass), input, transaction);
|
|
3160
|
+
await runtime.runWriteBefore(ctx);
|
|
3161
|
+
writeCtxs.push(ctx);
|
|
3162
|
+
if (ctx.kind !== kind) {
|
|
3163
|
+
return { op, rewritten: renderRewrittenWrite(modelClass, ctx) };
|
|
3164
|
+
}
|
|
3165
|
+
const merged = ctx.kind === "put" ? { ...ctx.input.item ?? {} } : { ...ctx.input.key ?? {}, ...ctx.input.changes ?? {} };
|
|
3166
|
+
return { op: { ...op, params: merged } };
|
|
3167
|
+
}
|
|
3168
|
+
function renderRewrittenWrite(modelClass, ctx) {
|
|
3169
|
+
const modelStatic = modelClass.asModel();
|
|
3170
|
+
const condition = ctx.input.options?.condition;
|
|
3171
|
+
const base = {
|
|
3172
|
+
modelClass,
|
|
3173
|
+
modelStatic,
|
|
3174
|
+
...condition !== void 0 ? { condition } : {}
|
|
3175
|
+
};
|
|
3176
|
+
if (ctx.kind === "put") {
|
|
3177
|
+
const item = ctx.input.item ?? {};
|
|
3178
|
+
return { ...base, operation: "put", key: item, item };
|
|
3179
|
+
}
|
|
3180
|
+
if (ctx.kind === "update") {
|
|
3181
|
+
return {
|
|
3182
|
+
...base,
|
|
3183
|
+
operation: "update",
|
|
3184
|
+
key: ctx.input.key ?? {},
|
|
3185
|
+
changes: ctx.input.changes ?? {}
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
return { ...base, operation: "delete", key: ctx.input.key ?? {} };
|
|
3189
|
+
}
|
|
3190
|
+
function rewrittenCompiledOp(write) {
|
|
3191
|
+
return {
|
|
3192
|
+
write,
|
|
3193
|
+
edgeItems: [],
|
|
3194
|
+
updateItems: [],
|
|
3195
|
+
guardItems: [],
|
|
3196
|
+
outboxItems: [],
|
|
3197
|
+
idempotencyItems: [],
|
|
3198
|
+
maintainItems: [],
|
|
3199
|
+
checkItems: [],
|
|
3200
|
+
modelBySignature: /* @__PURE__ */ new Map(),
|
|
3201
|
+
hasDerivedEffects: false
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
2360
3204
|
async function executeCompiledParallelWrites(ops, options) {
|
|
2361
3205
|
const label = options.label;
|
|
2362
3206
|
const results = new Array(ops.length);
|
|
2363
3207
|
const renderedOps = ops.map((op, i) => renderCompiledOp(op, `${label} (op #${i})`));
|
|
2364
3208
|
const coalescibleIdx = [];
|
|
2365
3209
|
const individualIdx = [];
|
|
3210
|
+
const hooksActive = buildWriteRuntime(options.context).active;
|
|
2366
3211
|
renderedOps.forEach((r, i) => {
|
|
2367
3212
|
const w = r.write;
|
|
2368
|
-
if (!r.hasDerivedEffects && w.condition === void 0 && (w.operation === "put" || w.operation === "delete")) {
|
|
3213
|
+
if (!hooksActive && !r.hasDerivedEffects && w.condition === void 0 && (w.operation === "put" || w.operation === "delete")) {
|
|
2369
3214
|
coalescibleIdx.push(i);
|
|
2370
3215
|
} else {
|
|
2371
3216
|
individualIdx.push(i);
|
|
@@ -2397,7 +3242,8 @@ async function executeCompiledParallelWrites(ops, options) {
|
|
|
2397
3242
|
try {
|
|
2398
3243
|
const { readBacks } = await executeCompiledWriteOp([ops[i]], {
|
|
2399
3244
|
label: `${label} (op #${i})`,
|
|
2400
|
-
...options.retry !== void 0 ? { retry: options.retry } : {}
|
|
3245
|
+
...options.retry !== void 0 ? { retry: options.retry } : {},
|
|
3246
|
+
...options.context !== void 0 ? { context: options.context } : {}
|
|
2401
3247
|
});
|
|
2402
3248
|
results[i] = { ok: true, value: readBacks[0] };
|
|
2403
3249
|
} catch (e) {
|
|
@@ -2455,14 +3301,16 @@ async function executeReadRoute(alias, route) {
|
|
|
2455
3301
|
if (isQuery) {
|
|
2456
3302
|
return executeQuery(modelClass, route.key, route.select, {
|
|
2457
3303
|
consistentRead: opt.consistentRead,
|
|
2458
|
-
maxDepth: opt.maxDepth
|
|
3304
|
+
maxDepth: opt.maxDepth,
|
|
3305
|
+
context: opt.context
|
|
2459
3306
|
});
|
|
2460
3307
|
}
|
|
2461
3308
|
return executeList(modelClass, route.key, route.select, {
|
|
2462
3309
|
limit: opt.limit,
|
|
2463
3310
|
after: opt.after,
|
|
2464
3311
|
order: opt.order,
|
|
2465
|
-
filter: opt.filter
|
|
3312
|
+
filter: opt.filter,
|
|
3313
|
+
context: opt.context
|
|
2466
3314
|
});
|
|
2467
3315
|
}
|
|
2468
3316
|
var INTENT_KEYS = ["create", "update", "remove"];
|
|
@@ -2476,9 +3324,9 @@ async function executeWriteEnvelope(envelope, options = {}) {
|
|
|
2476
3324
|
}
|
|
2477
3325
|
const ops = aliases.flatMap((alias) => normalizeWriteOp(alias, envelope[alias]));
|
|
2478
3326
|
if (mode === "transaction") {
|
|
2479
|
-
return executeTransactionMode(ops, options.retry);
|
|
3327
|
+
return executeTransactionMode(ops, options.retry, options.context);
|
|
2480
3328
|
}
|
|
2481
|
-
return executeParallelMode(ops, options.retry);
|
|
3329
|
+
return executeParallelMode(ops, options.retry, options.context);
|
|
2482
3330
|
}
|
|
2483
3331
|
function normalizeWriteOp(alias, d) {
|
|
2484
3332
|
if (d === null || typeof d !== "object") {
|
|
@@ -2551,11 +3399,12 @@ function mapToRefs($, fields) {
|
|
|
2551
3399
|
for (const f of fields) out[f] = $[f];
|
|
2552
3400
|
return out;
|
|
2553
3401
|
}
|
|
2554
|
-
async function executeTransactionMode(ops, retry) {
|
|
3402
|
+
async function executeTransactionMode(ops, retry, context) {
|
|
2555
3403
|
const compiled = ops.map(compileWriteOp);
|
|
2556
3404
|
const { readBacks } = await executeCompiledWriteOp(compiled, {
|
|
2557
3405
|
label: "DDBModel.mutate",
|
|
2558
|
-
...retry !== void 0 ? { retry } : {}
|
|
3406
|
+
...retry !== void 0 ? { retry } : {},
|
|
3407
|
+
...context !== void 0 ? { context } : {}
|
|
2559
3408
|
});
|
|
2560
3409
|
const out = {};
|
|
2561
3410
|
ops.forEach((op, i) => {
|
|
@@ -2570,11 +3419,12 @@ async function executeTransactionMode(ops, retry) {
|
|
|
2570
3419
|
});
|
|
2571
3420
|
return out;
|
|
2572
3421
|
}
|
|
2573
|
-
async function executeParallelMode(ops, retry) {
|
|
3422
|
+
async function executeParallelMode(ops, retry, context) {
|
|
2574
3423
|
const compiled = ops.map(compileWriteOp);
|
|
2575
3424
|
const settled = await executeCompiledParallelWrites(compiled, {
|
|
2576
3425
|
label: "DDBModel.mutate",
|
|
2577
|
-
...retry !== void 0 ? { retry } : {}
|
|
3426
|
+
...retry !== void 0 ? { retry } : {},
|
|
3427
|
+
...context !== void 0 ? { context } : {}
|
|
2578
3428
|
});
|
|
2579
3429
|
const out = {};
|
|
2580
3430
|
ops.forEach((op, i) => {
|
|
@@ -2634,8 +3484,36 @@ var DDBModel = class {
|
|
|
2634
3484
|
static setRetryPolicy(policy) {
|
|
2635
3485
|
ClientManager.setRetryPolicy(policy);
|
|
2636
3486
|
}
|
|
2637
|
-
|
|
2638
|
-
|
|
3487
|
+
/**
|
|
3488
|
+
* Register a {@link Middleware} (issue #50; read path #138, write path #139).
|
|
3489
|
+
* One registered object may carry read and/or write hooks.
|
|
3490
|
+
*
|
|
3491
|
+
* - **Read** R1–R5 — request entry (`read.before`), each physical op before send
|
|
3492
|
+
* / after raw fetch (`read.op.before` / `read.op.afterFetch`, INCLUDING every
|
|
3493
|
+
* relation fan-out fetch), the final hydrated result (`read.afterFetch`), and on
|
|
3494
|
+
* error (`read.op.onError` / `read.onError`).
|
|
3495
|
+
* - **Write** W1–W5 — the logical write before effect derivation (`write.before`,
|
|
3496
|
+
* may mutate `ctx.input` / `ctx.kind` incl. a delete→update rewrite), after commit
|
|
3497
|
+
* (`write.after`), the physical persist on the fully composed batch
|
|
3498
|
+
* (`write.persist.before` / `write.persist.after`, fires ONCE per atomic
|
|
3499
|
+
* transaction), and on error (`write.persist.onError` / `write.onError`).
|
|
3500
|
+
*
|
|
3501
|
+
* Hooks may observe, mutate `ctx`, `throw` to cancel, or (on `onError`) recover by
|
|
3502
|
+
* returning a value; the library imposes no semantics. Ordering: `before*` runs
|
|
3503
|
+
* first-registered-first (FIFO); `after*` / `onError` run last-registered-first
|
|
3504
|
+
* (LIFO). Mirrors {@link setRetryPolicy} — a host-runtime global stored on
|
|
3505
|
+
* {@link ClientManager}; never serialized (the bridge #48 is unaffected). Per-call
|
|
3506
|
+
* host context flows in via the read's / write's `{ context }` option.
|
|
3507
|
+
*/
|
|
3508
|
+
static use(mw) {
|
|
3509
|
+
ClientManager.use(mw);
|
|
3510
|
+
}
|
|
3511
|
+
/** Remove every registered middleware (read AND write; teardown / tests). See {@link use}. */
|
|
3512
|
+
static clearMiddleware() {
|
|
3513
|
+
ClientManager.clearMiddleware();
|
|
3514
|
+
}
|
|
3515
|
+
static async transaction(fn, options) {
|
|
3516
|
+
await executeTransaction(fn, options);
|
|
2639
3517
|
}
|
|
2640
3518
|
/**
|
|
2641
3519
|
* In-process multi-route READ (issue #101 — the unified envelope). Each alias
|
|
@@ -2650,8 +3528,20 @@ var DDBModel = class {
|
|
|
2650
3528
|
static mutate(envelope, options) {
|
|
2651
3529
|
return executeWriteEnvelope(envelope, options);
|
|
2652
3530
|
}
|
|
2653
|
-
|
|
2654
|
-
|
|
3531
|
+
/**
|
|
3532
|
+
* Multi-key, multi-model batch read. Each {@link BatchGetRequest} contributes its
|
|
3533
|
+
* keys to a chunked `BatchGetItem` per physical table; results are re-grouped by
|
|
3534
|
+
* model on the returned {@link BatchGetResult}.
|
|
3535
|
+
*
|
|
3536
|
+
* Fires the read hooks (issue #142): request-level R1/R4/R5 with kind
|
|
3537
|
+
* `'batchGet'`, and op-level R2/R3/R5 for each underlying physical `BatchGetItem`
|
|
3538
|
+
* op. The optional `{ context }` is exposed to every hook as `ctx.context` and
|
|
3539
|
+
* threaded UNCHANGED to every op (absent ⇒ `{}`), so a global hook (e.g. a
|
|
3540
|
+
* tenant-scope R2 FilterExpression) applies across the whole batch. With no
|
|
3541
|
+
* middleware registered the read path is unchanged (zero-overhead fast path).
|
|
3542
|
+
*/
|
|
3543
|
+
static async batchGet(requests, options) {
|
|
3544
|
+
return executeBatchGet(requests, options);
|
|
2655
3545
|
}
|
|
2656
3546
|
static async batchWrite(requests) {
|
|
2657
3547
|
return executeBatchWrite(requests);
|
|
@@ -3188,6 +4078,8 @@ function buildPlannedCommandMethod(name, planned) {
|
|
|
3188
4078
|
);
|
|
3189
4079
|
}
|
|
3190
4080
|
const idempotencyGuard = idempotencyGuards[0];
|
|
4081
|
+
const maintainWrites = plan2.fragments.flatMap((f) => f.maintainWrites ?? []);
|
|
4082
|
+
assertNoCrossFragmentMaintainCollision(name, maintainWrites);
|
|
3191
4083
|
return {
|
|
3192
4084
|
__methodKind: "command",
|
|
3193
4085
|
op,
|
|
@@ -3198,6 +4090,7 @@ function buildPlannedCommandMethod(name, planned) {
|
|
|
3198
4090
|
...uniqueGuards.length > 0 ? { uniqueGuards } : {},
|
|
3199
4091
|
...outboxEvents.length > 0 ? { outboxEvents } : {},
|
|
3200
4092
|
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
|
|
4093
|
+
...maintainWrites.length > 0 ? { maintainWrites } : {},
|
|
3201
4094
|
inputArity: "either",
|
|
3202
4095
|
// A return projection means the method returns the read-back entity; otherwise
|
|
3203
4096
|
// it is a fire-and-forget write (`void`).
|
|
@@ -3485,6 +4378,171 @@ function deriveOneUpdate(fragment, effect) {
|
|
|
3485
4378
|
amount: effect.amount
|
|
3486
4379
|
};
|
|
3487
4380
|
}
|
|
4381
|
+
var INTENT_MAINTAIN_EVENT = {
|
|
4382
|
+
create: "created",
|
|
4383
|
+
update: "updated",
|
|
4384
|
+
remove: "removed"
|
|
4385
|
+
};
|
|
4386
|
+
var MAINTAIN_ENTITY_PATH_RE = /^\$\.entity\.([A-Za-z_$][\w$]*)$/;
|
|
4387
|
+
var cachedMaintenanceGraph;
|
|
4388
|
+
function globalMaintenanceGraph() {
|
|
4389
|
+
const generation = MetadataRegistry.generation;
|
|
4390
|
+
if (cachedMaintenanceGraph?.generation !== generation) {
|
|
4391
|
+
cachedMaintenanceGraph = { graph: buildMaintenanceGraph(), generation };
|
|
4392
|
+
}
|
|
4393
|
+
return cachedMaintenanceGraph.graph;
|
|
4394
|
+
}
|
|
4395
|
+
function logicalEntityName(fragment) {
|
|
4396
|
+
const meta = MetadataRegistry.get(
|
|
4397
|
+
fragment.entity.modelClass
|
|
4398
|
+
);
|
|
4399
|
+
return meta.prefix.endsWith("#") ? meta.prefix.slice(0, -1) : meta.prefix;
|
|
4400
|
+
}
|
|
4401
|
+
function resolveMaintainers(fragment, graph) {
|
|
4402
|
+
const g = graph ?? globalMaintenanceGraph();
|
|
4403
|
+
const event = INTENT_MAINTAIN_EVENT[fragment.intent];
|
|
4404
|
+
const trigger = `${logicalEntityName(fragment)}.${event}`;
|
|
4405
|
+
return g.effectsFor(trigger);
|
|
4406
|
+
}
|
|
4407
|
+
function maintainPathToInputField(fragment, item, role, path) {
|
|
4408
|
+
const entityMatch = MAINTAIN_ENTITY_PATH_RE.exec(path);
|
|
4409
|
+
if (entityMatch !== null) return entityMatch[1];
|
|
4410
|
+
const inputMatch = INPUT_PATH_RE.exec(path);
|
|
4411
|
+
if (inputMatch !== null) return inputMatch[1];
|
|
4412
|
+
throw new Error(
|
|
4413
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a maintenance effect (relation '${item.relationProperty}' on '${item.ownerEntity}') whose ${role} binds to ${JSON.stringify(path)}, which is not a payload-rooted \`$.entity.<field>\` / \`$.input.<field>\` source. A maintenance write projects only from the written source row (payload \u540C\u68B1) \u2014 fix the relation's projection / key binding.`
|
|
4414
|
+
);
|
|
4415
|
+
}
|
|
4416
|
+
function deriveMaintainItems(fragment, items) {
|
|
4417
|
+
const byRow = /* @__PURE__ */ new Map();
|
|
4418
|
+
for (const item of items) {
|
|
4419
|
+
const prior = byRow.get(item.destinationRowKey);
|
|
4420
|
+
if (prior !== void 0) {
|
|
4421
|
+
const bothCounter = item.effect.kind === "counter" && prior.effect.kind === "counter";
|
|
4422
|
+
if (!bothCounter) {
|
|
4423
|
+
throw new Error(
|
|
4424
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires TWO maintenance effects that target the SAME row (relation '${prior.relationProperty}' and '${item.relationProperty}' on '${item.ownerEntity}', destination '${item.destinationRowKey}'). Phase 1 constrains a mutation to ONE non-counter maintenance effect per target row (\u8AD6\u70B92 = b) \u2014 a \`TransactWriteItems\` may not touch one key twice with a snapshot/collection write, and merging those is a separate future issue. (Counter ADDs are exempt \u2014 they merge. Consistent with the #96 same-row reject.)`
|
|
4425
|
+
);
|
|
4426
|
+
}
|
|
4427
|
+
} else {
|
|
4428
|
+
byRow.set(item.destinationRowKey, item);
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
return items.map((item) => deriveOneMaintainWrite(fragment, item));
|
|
4432
|
+
}
|
|
4433
|
+
function maintainWriteRowKey(write) {
|
|
4434
|
+
const parts = Object.entries(write.keyBinding).map(([field, inputField]) => `${field}=${inputField}`).sort();
|
|
4435
|
+
return `${write.entity.name}#${parts.join("&")}`;
|
|
4436
|
+
}
|
|
4437
|
+
function assertNoCrossFragmentMaintainCollision(methodName, writes) {
|
|
4438
|
+
const byRow = /* @__PURE__ */ new Map();
|
|
4439
|
+
for (const write of writes) {
|
|
4440
|
+
const rowKey = maintainWriteRowKey(write);
|
|
4441
|
+
const prior = byRow.get(rowKey);
|
|
4442
|
+
if (prior !== void 0) {
|
|
4443
|
+
const bothCounter = write.kind === "counter" && prior.kind === "counter";
|
|
4444
|
+
if (!bothCounter) {
|
|
4445
|
+
throw new Error(
|
|
4446
|
+
`publicCommandModel: planned method '${methodName}' aggregates TWO maintenance effects that resolve to the SAME owner row (relation '${prior.relationProperty}' and '${write.relationProperty}' on '${write.entity.name}', destination '${rowKey}'), across different mutation fragments. Phase 1 constrains a mutation to ONE non-counter maintenance effect per target row (\u8AD6\u70B92 = b) \u2014 this holds for the WHOLE mutation, not just within a single fragment. Multiple snapshot/collection maintain effects resolving to one owner row is the true cause here (NOT a derived-counter merge failure); a \`TransactWriteItems\` may not touch one key twice with a snapshot/collection write, and merging those is a separate future issue. (Counter ADDs are exempt \u2014 they merge. Consistent with the intra-fragment reject and the #96 same-row reject.)`
|
|
4447
|
+
);
|
|
4448
|
+
}
|
|
4449
|
+
} else {
|
|
4450
|
+
byRow.set(rowKey, write);
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
function deriveOneMaintainWrite(fragment, item) {
|
|
4455
|
+
const effect = item.effect;
|
|
4456
|
+
if (effect.updateMode === "stream") {
|
|
4457
|
+
throw new Error(
|
|
4458
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a maintenance effect (relation '${item.relationProperty}' on '${item.ownerEntity}') declared \`updateMode: 'stream'\` (asynchronous). Phase 1 (#125) lowers only the SYNCHRONOUS \`mutation\` path (the same TransactWriteItems as the source write); the stream path (outbox \u2192 CDC drain) is #130 (Phase 2). Use \`updateMode: 'mutation'\` (the default) for a synchronous maintenance, or wait for #130.`
|
|
4459
|
+
);
|
|
4460
|
+
}
|
|
4461
|
+
const ownerClass = effect.targetFactory();
|
|
4462
|
+
const ownerMeta = MetadataRegistry.get(
|
|
4463
|
+
ownerClass
|
|
4464
|
+
);
|
|
4465
|
+
const ownerName = ownerClass.name;
|
|
4466
|
+
const keyBinding = {};
|
|
4467
|
+
for (const [ownerField, source] of Object.entries(effect.keys)) {
|
|
4468
|
+
keyBinding[ownerField] = maintainPathToInputField(
|
|
4469
|
+
fragment,
|
|
4470
|
+
item,
|
|
4471
|
+
`destination-key field '${ownerField}'`,
|
|
4472
|
+
source
|
|
4473
|
+
);
|
|
4474
|
+
}
|
|
4475
|
+
if (effect.kind === "counter") {
|
|
4476
|
+
return deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding);
|
|
4477
|
+
}
|
|
4478
|
+
const projection = {};
|
|
4479
|
+
for (const [attr, transform] of Object.entries(effect.project)) {
|
|
4480
|
+
const inputField = maintainPathToInputField(
|
|
4481
|
+
fragment,
|
|
4482
|
+
item,
|
|
4483
|
+
`projection attribute '${attr}'`,
|
|
4484
|
+
transform.path
|
|
4485
|
+
);
|
|
4486
|
+
projection[attr] = { op: transform.op, args: transform.args, inputField };
|
|
4487
|
+
}
|
|
4488
|
+
if (!ownerMeta.primaryKey) {
|
|
4489
|
+
throw new Error(
|
|
4490
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a maintenance effect on '${ownerName}', which has no primary key \u2014 a maintenance write keys a single target row, so the maintained model must declare one.`
|
|
4491
|
+
);
|
|
4492
|
+
}
|
|
4493
|
+
const base = {
|
|
4494
|
+
entity: { name: ownerName, modelClass: ownerClass },
|
|
4495
|
+
relationProperty: item.relationProperty,
|
|
4496
|
+
trigger: item.trigger,
|
|
4497
|
+
keyBinding,
|
|
4498
|
+
projection
|
|
4499
|
+
};
|
|
4500
|
+
if (effect.kind === "collection") {
|
|
4501
|
+
const orderBy = effect.collection.orderBy !== void 0 ? maintainPathToInputField(fragment, item, "collection `orderBy`", effect.collection.orderBy) : void 0;
|
|
4502
|
+
return {
|
|
4503
|
+
...base,
|
|
4504
|
+
kind: "collection",
|
|
4505
|
+
collection: {
|
|
4506
|
+
field: effect.collection.field,
|
|
4507
|
+
...effect.collection.maxItems !== void 0 ? { maxItems: effect.collection.maxItems } : {},
|
|
4508
|
+
...orderBy !== void 0 ? { orderBy } : {}
|
|
4509
|
+
}
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4512
|
+
return { ...base, kind: "snapshot" };
|
|
4513
|
+
}
|
|
4514
|
+
function deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding) {
|
|
4515
|
+
if (effect.value.op === "max") {
|
|
4516
|
+
throw new Error(
|
|
4517
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a counter maintenance effect (\`@aggregate\` field '${item.relationProperty}' on '${item.ownerEntity}') declared \`value: max('${effect.value.field}')\`. Phase 1 (#141) realizes only \`count()\` synchronously: a \`max\` needs a conditional \`SET\` whose failed guard would roll back the SOURCE write that legitimately happened, so a running max is the asynchronous stream path (#130, Phase 2). Use \`value: count()\` for a synchronous counter, or wait for #130.`
|
|
4518
|
+
);
|
|
4519
|
+
}
|
|
4520
|
+
if (effect.delta === void 0) {
|
|
4521
|
+
throw new Error(
|
|
4522
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a counter maintenance effect (\`@aggregate\` field '${item.relationProperty}' on '${item.ownerEntity}') with no \`delta\` \u2014 the maintenance graph (#141) should set it from the trigger event.`
|
|
4523
|
+
);
|
|
4524
|
+
}
|
|
4525
|
+
if (!ownerMeta.primaryKey) {
|
|
4526
|
+
throw new Error(
|
|
4527
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a counter maintenance effect on '${ownerName}', which has no primary key \u2014 a counter \`UpdateItem\` keys a single row, so the maintained model must declare one.`
|
|
4528
|
+
);
|
|
4529
|
+
}
|
|
4530
|
+
const isField = ownerMeta.fields.some((f) => f.propertyName === effect.attribute) || ownerMeta.aggregates.some((a) => a.propertyName === effect.attribute);
|
|
4531
|
+
if (!isField) {
|
|
4532
|
+
throw new Error(
|
|
4533
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' derives a counter on '${ownerName}.${effect.attribute}', which is not a field of '${ownerName}'.`
|
|
4534
|
+
);
|
|
4535
|
+
}
|
|
4536
|
+
return {
|
|
4537
|
+
entity: { name: ownerName, modelClass: ownerClass },
|
|
4538
|
+
relationProperty: item.relationProperty,
|
|
4539
|
+
trigger: item.trigger,
|
|
4540
|
+
kind: "counter",
|
|
4541
|
+
keyBinding,
|
|
4542
|
+
projection: {},
|
|
4543
|
+
counter: { attribute: effect.attribute, delta: effect.delta }
|
|
4544
|
+
};
|
|
4545
|
+
}
|
|
3488
4546
|
var UNIQUE_GUARD_PK_PREFIX = "UNIQUE#";
|
|
3489
4547
|
var UNIQUE_GUARD_SK_PREFIX = "VALUE#";
|
|
3490
4548
|
function deriveUniqueGuards(fragment, unique) {
|
|
@@ -3703,7 +4761,7 @@ function renderInputLeaf(leaf, isKeyFieldInPut, consumerIndex, consumerField, re
|
|
|
3703
4761
|
}
|
|
3704
4762
|
return leaf;
|
|
3705
4763
|
}
|
|
3706
|
-
function compileFragment(fragment, index = 0, resolveEntityRef = null) {
|
|
4764
|
+
function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenanceGraph) {
|
|
3707
4765
|
const keyFields = primaryKeyFields(fragment);
|
|
3708
4766
|
const keyFieldSet = new Set(keyFields);
|
|
3709
4767
|
const operation = INTENT_OPERATION[fragment.intent];
|
|
@@ -3759,6 +4817,8 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null) {
|
|
|
3759
4817
|
const uniqueGuards = lifecycle?.effects.unique !== void 0 && lifecycle.effects.unique.length > 0 ? deriveUniqueGuards(fragment, lifecycle.effects.unique) : void 0;
|
|
3760
4818
|
const outboxEvents = lifecycle?.effects.emits !== void 0 && lifecycle.effects.emits.length > 0 ? deriveOutboxEvents(fragment, lifecycle.effects.emits) : void 0;
|
|
3761
4819
|
const idempotencyGuard = lifecycle?.effects.idempotency !== void 0 ? deriveIdempotencyGuard(fragment, lifecycle.effects.idempotency) : void 0;
|
|
4820
|
+
const maintainers = resolveMaintainers(fragment, maintenanceGraph);
|
|
4821
|
+
const maintainWrites = maintainers.length > 0 ? deriveMaintainItems(fragment, maintainers) : void 0;
|
|
3762
4822
|
return {
|
|
3763
4823
|
op,
|
|
3764
4824
|
keyFields,
|
|
@@ -3768,7 +4828,8 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null) {
|
|
|
3768
4828
|
...derivedUpdates !== void 0 ? { derivedUpdates } : {},
|
|
3769
4829
|
...uniqueGuards !== void 0 ? { uniqueGuards } : {},
|
|
3770
4830
|
...outboxEvents !== void 0 ? { outboxEvents } : {},
|
|
3771
|
-
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {}
|
|
4831
|
+
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
|
|
4832
|
+
...maintainWrites !== void 0 ? { maintainWrites } : {}
|
|
3772
4833
|
};
|
|
3773
4834
|
}
|
|
3774
4835
|
function compileSingleFragmentPlan(plan2) {
|
|
@@ -5814,6 +6875,69 @@ function buildDerivedUpdateItems(derivedUpdates) {
|
|
|
5814
6875
|
}
|
|
5815
6876
|
return { items, params };
|
|
5816
6877
|
}
|
|
6878
|
+
function buildMaintainWriteItems(maintainWrites) {
|
|
6879
|
+
const items = [];
|
|
6880
|
+
const params = {};
|
|
6881
|
+
for (const maintain of maintainWrites) {
|
|
6882
|
+
const metadata = MetadataRegistry.get(
|
|
6883
|
+
maintain.entity.modelClass
|
|
6884
|
+
);
|
|
6885
|
+
if (!metadata.primaryKey) {
|
|
6886
|
+
throw new Error(
|
|
6887
|
+
`maintenance write: the maintained entity '${maintain.entity.name}' has no primary key, so a maintenance \`UpdateItem\` cannot be keyed.`
|
|
6888
|
+
);
|
|
6889
|
+
}
|
|
6890
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
6891
|
+
const present = new Set(Object.keys(maintain.keyBinding));
|
|
6892
|
+
const { pk, sk } = evaluateKey(
|
|
6893
|
+
metadata.primaryKey.segmented,
|
|
6894
|
+
"param",
|
|
6895
|
+
present,
|
|
6896
|
+
(field) => maintain.keyBinding[field] ?? field
|
|
6897
|
+
);
|
|
6898
|
+
const keyCondition = { PK: pk };
|
|
6899
|
+
if (sk !== void 0) keyCondition.SK = sk;
|
|
6900
|
+
const projection = {};
|
|
6901
|
+
for (const [attr, transform] of Object.entries(maintain.projection)) {
|
|
6902
|
+
projection[attr] = {
|
|
6903
|
+
op: transform.op,
|
|
6904
|
+
args: [...transform.args],
|
|
6905
|
+
inputField: transform.inputField
|
|
6906
|
+
};
|
|
6907
|
+
}
|
|
6908
|
+
const maintainSpec = {
|
|
6909
|
+
kind: maintain.kind,
|
|
6910
|
+
relationProperty: maintain.relationProperty,
|
|
6911
|
+
trigger: maintain.trigger,
|
|
6912
|
+
projection,
|
|
6913
|
+
...maintain.kind === "collection" && maintain.collection !== void 0 ? {
|
|
6914
|
+
collection: {
|
|
6915
|
+
field: maintain.collection.field,
|
|
6916
|
+
...maintain.collection.maxItems !== void 0 ? { maxItems: maintain.collection.maxItems } : {},
|
|
6917
|
+
...maintain.collection.orderBy !== void 0 ? { orderBy: maintain.collection.orderBy } : {}
|
|
6918
|
+
}
|
|
6919
|
+
} : {},
|
|
6920
|
+
// #141: a counter carries its scalar `ADD` (attribute + literal numeric delta);
|
|
6921
|
+
// the runtimes render `ADD #attr :delta` exactly as the self-lifecycle derived
|
|
6922
|
+
// counter (#85) does, so a same-row counter merge stays consistent across paths.
|
|
6923
|
+
...maintain.kind === "counter" && maintain.counter !== void 0 ? {
|
|
6924
|
+
counter: {
|
|
6925
|
+
attribute: maintain.counter.attribute,
|
|
6926
|
+
delta: String(maintain.counter.delta)
|
|
6927
|
+
}
|
|
6928
|
+
} : {}
|
|
6929
|
+
};
|
|
6930
|
+
items.push({
|
|
6931
|
+
type: "Update",
|
|
6932
|
+
tableName,
|
|
6933
|
+
entity: maintain.entity.name,
|
|
6934
|
+
keyCondition,
|
|
6935
|
+
maintain: maintainSpec
|
|
6936
|
+
});
|
|
6937
|
+
collectTemplateParams(keyCondition, metadata, params);
|
|
6938
|
+
}
|
|
6939
|
+
return { items, params };
|
|
6940
|
+
}
|
|
5817
6941
|
function buildUniqueGuardItems(uniqueGuards, entityMetadata) {
|
|
5818
6942
|
const items = [];
|
|
5819
6943
|
const params = {};
|
|
@@ -5846,7 +6970,7 @@ function buildIdempotencyGuardItems(guard, entityMetadata) {
|
|
|
5846
6970
|
}
|
|
5847
6971
|
return { items, params };
|
|
5848
6972
|
}
|
|
5849
|
-
function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard) {
|
|
6973
|
+
function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard, maintainWrites) {
|
|
5850
6974
|
const label = opRefName(contractName, methodName);
|
|
5851
6975
|
const base = composeFragmentTransaction(contractName, methodName, ops);
|
|
5852
6976
|
const { items: edgeItemsRaw, params: edgeParams } = buildEdgeWriteItems(edgeWrites2);
|
|
@@ -5864,6 +6988,7 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
5864
6988
|
primaryMetadata
|
|
5865
6989
|
);
|
|
5866
6990
|
const { items: idempotencyItems, params: idempotencyParams } = idempotencyGuard !== void 0 ? buildIdempotencyGuardItems(idempotencyGuard, primaryMetadata) : { items: [], params: {} };
|
|
6991
|
+
const { items: maintainItems, params: maintainParams } = buildMaintainWriteItems(maintainWrites);
|
|
5867
6992
|
const baseKeys = new Set(base.items.map(itemKeySignature));
|
|
5868
6993
|
for (const update of updateItems) {
|
|
5869
6994
|
if (baseKeys.has(itemKeySignature(update))) {
|
|
@@ -5886,7 +7011,8 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
5886
7011
|
checkParams,
|
|
5887
7012
|
guardParams,
|
|
5888
7013
|
outboxParams,
|
|
5889
|
-
idempotencyParams
|
|
7014
|
+
idempotencyParams,
|
|
7015
|
+
maintainParams
|
|
5890
7016
|
]) {
|
|
5891
7017
|
for (const [name, spec] of Object.entries(source)) params[name] = spec;
|
|
5892
7018
|
}
|
|
@@ -5897,6 +7023,7 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
5897
7023
|
...guardItems,
|
|
5898
7024
|
...outboxItems,
|
|
5899
7025
|
...idempotencyItems,
|
|
7026
|
+
...maintainItems,
|
|
5900
7027
|
...checkItems
|
|
5901
7028
|
];
|
|
5902
7029
|
if (items.length > MAX_TRANSACT_ITEMS) {
|
|
@@ -6002,7 +7129,8 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
|
|
|
6002
7129
|
const uniqueGuards = method.uniqueGuards ?? [];
|
|
6003
7130
|
const outboxEvents = method.outboxEvents ?? [];
|
|
6004
7131
|
const idempotencyGuard = method.idempotencyGuard;
|
|
6005
|
-
const
|
|
7132
|
+
const maintainWrites = method.maintainWrites ?? [];
|
|
7133
|
+
const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0 || maintainWrites.length > 0;
|
|
6006
7134
|
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
6007
7135
|
const promotedToTransaction = isMultiFragment || hasDerivedEffects;
|
|
6008
7136
|
if (promotedToTransaction) {
|
|
@@ -6020,7 +7148,8 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
|
|
|
6020
7148
|
derivedUpdates,
|
|
6021
7149
|
uniqueGuards,
|
|
6022
7150
|
outboxEvents,
|
|
6023
|
-
idempotencyGuard
|
|
7151
|
+
idempotencyGuard,
|
|
7152
|
+
maintainWrites
|
|
6024
7153
|
)
|
|
6025
7154
|
) : (
|
|
6026
7155
|
// #90: writes only.
|
|
@@ -6186,6 +7315,10 @@ export {
|
|
|
6186
7315
|
BatchGetResult,
|
|
6187
7316
|
executeBatchGet,
|
|
6188
7317
|
executeBatchWrite,
|
|
7318
|
+
maintainTrigger,
|
|
7319
|
+
isMaintainTrigger,
|
|
7320
|
+
preview,
|
|
7321
|
+
identity,
|
|
6189
7322
|
LIFECYCLE_CONTRACT_MARKER,
|
|
6190
7323
|
isLifecycleContract,
|
|
6191
7324
|
ENTITY_WRITES_MARKER,
|
|
@@ -6201,6 +7334,7 @@ export {
|
|
|
6201
7334
|
deriveEdgeWriteItems,
|
|
6202
7335
|
getEdgeWrites,
|
|
6203
7336
|
deriveModelEdgeWriteItems,
|
|
7337
|
+
buildMaintenanceGraph,
|
|
6204
7338
|
resolveLifecycle,
|
|
6205
7339
|
compileFragment,
|
|
6206
7340
|
compileSingleFragmentPlan,
|