graphddb 0.2.5 → 0.3.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/README.md +10 -1
- package/dist/chunk-PNIZS37E.js +916 -0
- package/dist/{chunk-SGYBE2OV.js → chunk-VST3WOK3.js} +597 -113
- package/dist/{chunk-4TYK3AV6.js → chunk-YIXXTGZ6.js} +1369 -15
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +587 -75
- package/dist/index.js +535 -16
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types--tdHK3vi.d.ts → types-DuJ08bgc.d.ts} +1542 -63
- package/package.json +1 -1
- package/dist/chunk-ITHQ2EDH.js +0 -461
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
MetadataRegistry,
|
|
7
7
|
NO_MIDDLEWARE,
|
|
8
8
|
TableMapping,
|
|
9
|
+
ViewRegistry,
|
|
9
10
|
attachHiddenKey,
|
|
10
11
|
attachModelClass,
|
|
11
12
|
buildConditionExpression,
|
|
12
13
|
buildDeleteInput,
|
|
14
|
+
buildMaintenanceGraph,
|
|
13
15
|
buildPutInput,
|
|
14
16
|
buildReadRuntime,
|
|
15
17
|
buildUpdateInput,
|
|
@@ -25,9 +27,13 @@ import {
|
|
|
25
27
|
executePut,
|
|
26
28
|
executeTransaction,
|
|
27
29
|
executeUpdate,
|
|
30
|
+
getEntityWrites,
|
|
28
31
|
isColumn,
|
|
32
|
+
isEntityWritesDefinition,
|
|
33
|
+
isLifecycleContract,
|
|
29
34
|
isParam,
|
|
30
35
|
isRawCondition,
|
|
36
|
+
lifecyclePhaseForIntent,
|
|
31
37
|
param,
|
|
32
38
|
pkTemplate,
|
|
33
39
|
resolveKey,
|
|
@@ -37,7 +43,7 @@ import {
|
|
|
37
43
|
serializeFieldValue,
|
|
38
44
|
serializeRawCondition,
|
|
39
45
|
skTemplate
|
|
40
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-YIXXTGZ6.js";
|
|
41
47
|
|
|
42
48
|
// src/spec/types.ts
|
|
43
49
|
var SPEC_VERSION = "1.0";
|
|
@@ -1756,94 +1762,6 @@ async function executeBatchWrite(requests) {
|
|
|
1756
1762
|
}
|
|
1757
1763
|
}
|
|
1758
1764
|
|
|
1759
|
-
// src/define/entity-writes.ts
|
|
1760
|
-
var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
|
|
1761
|
-
"graphddb:lifecycleContract"
|
|
1762
|
-
);
|
|
1763
|
-
function isLifecycleContract(value) {
|
|
1764
|
-
return typeof value === "object" && value !== null && value[LIFECYCLE_CONTRACT_MARKER] === true;
|
|
1765
|
-
}
|
|
1766
|
-
var ENTITY_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:entityWrites");
|
|
1767
|
-
function isEntityWritesDefinition(value) {
|
|
1768
|
-
return typeof value === "object" && value !== null && value[ENTITY_WRITES_MARKER] === true;
|
|
1769
|
-
}
|
|
1770
|
-
function freezeEffects(effects) {
|
|
1771
|
-
const out = {};
|
|
1772
|
-
if (effects.requires !== void 0) out.requires = effects.requires;
|
|
1773
|
-
if (effects.unique !== void 0) out.unique = effects.unique;
|
|
1774
|
-
if (effects.edges !== void 0) out.edges = effects.edges;
|
|
1775
|
-
if (effects.derive !== void 0) out.derive = effects.derive;
|
|
1776
|
-
if (effects.emits !== void 0) out.emits = effects.emits;
|
|
1777
|
-
if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
|
|
1778
|
-
return out;
|
|
1779
|
-
}
|
|
1780
|
-
var recorder = {
|
|
1781
|
-
lifecycle(effects = {}) {
|
|
1782
|
-
return {
|
|
1783
|
-
[LIFECYCLE_CONTRACT_MARKER]: true,
|
|
1784
|
-
effects: freezeEffects(effects)
|
|
1785
|
-
};
|
|
1786
|
-
},
|
|
1787
|
-
exists(targetFactory, keys) {
|
|
1788
|
-
return { kind: "requires", targetFactory, keys };
|
|
1789
|
-
},
|
|
1790
|
-
unique(spec) {
|
|
1791
|
-
return { kind: "unique", name: spec.name, scope: spec.scope, fields: spec.fields };
|
|
1792
|
-
},
|
|
1793
|
-
putEdge(targetFactory, relationProperty) {
|
|
1794
|
-
return { kind: "putEdge", targetFactory, relationProperty };
|
|
1795
|
-
},
|
|
1796
|
-
deleteEdge(targetFactory, relationProperty) {
|
|
1797
|
-
return { kind: "deleteEdge", targetFactory, relationProperty };
|
|
1798
|
-
},
|
|
1799
|
-
increment(targetFactory, keys, attribute, amount) {
|
|
1800
|
-
return { kind: "derive", targetFactory, keys, attribute, amount };
|
|
1801
|
-
},
|
|
1802
|
-
event(name, payload) {
|
|
1803
|
-
return { kind: "event", name, payload };
|
|
1804
|
-
},
|
|
1805
|
-
idempotentBy(token) {
|
|
1806
|
-
return { kind: "idempotency", token };
|
|
1807
|
-
}
|
|
1808
|
-
};
|
|
1809
|
-
function entityWrites(builder) {
|
|
1810
|
-
const shape = builder(recorder);
|
|
1811
|
-
for (const phase of ["create", "update", "remove"]) {
|
|
1812
|
-
const contract = shape[phase];
|
|
1813
|
-
if (contract !== void 0 && !isLifecycleContract(contract)) {
|
|
1814
|
-
throw new Error(
|
|
1815
|
-
`entityWrites: the '${phase}' lifecycle must be built with \`w.lifecycle({...})\` (it carries the \xA72 effect arrays), but received ${JSON.stringify(contract)}.`
|
|
1816
|
-
);
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
|
|
1820
|
-
throw new Error(
|
|
1821
|
-
"entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`); an empty save contract declares nothing."
|
|
1822
|
-
);
|
|
1823
|
-
}
|
|
1824
|
-
return {
|
|
1825
|
-
[ENTITY_WRITES_MARKER]: true,
|
|
1826
|
-
...shape.create !== void 0 ? { create: shape.create } : {},
|
|
1827
|
-
...shape.update !== void 0 ? { update: shape.update } : {},
|
|
1828
|
-
...shape.remove !== void 0 ? { remove: shape.remove } : {}
|
|
1829
|
-
};
|
|
1830
|
-
}
|
|
1831
|
-
function getEntityWrites(modelClass) {
|
|
1832
|
-
for (const name of Object.getOwnPropertyNames(modelClass)) {
|
|
1833
|
-
let value;
|
|
1834
|
-
try {
|
|
1835
|
-
value = modelClass[name];
|
|
1836
|
-
} catch {
|
|
1837
|
-
continue;
|
|
1838
|
-
}
|
|
1839
|
-
if (isEntityWritesDefinition(value)) return value;
|
|
1840
|
-
}
|
|
1841
|
-
return void 0;
|
|
1842
|
-
}
|
|
1843
|
-
function lifecyclePhaseForIntent(intent) {
|
|
1844
|
-
return intent;
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
1765
|
// src/relation/edge-write.ts
|
|
1848
1766
|
var OLD_VALUE_NAMESPACE = "old";
|
|
1849
1767
|
var EDGE_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:edgeWrites");
|
|
@@ -1853,13 +1771,22 @@ function isEdgeWritesDefinition(value) {
|
|
|
1853
1771
|
function declaration(target, relationProperty) {
|
|
1854
1772
|
return { targetFactory: target, relationProperty };
|
|
1855
1773
|
}
|
|
1856
|
-
var
|
|
1774
|
+
var recorder = {
|
|
1857
1775
|
putEdge: declaration,
|
|
1858
1776
|
deleteEdge: declaration,
|
|
1859
|
-
updateKeyChangeEdge: declaration
|
|
1777
|
+
updateKeyChangeEdge: declaration,
|
|
1778
|
+
dualEdge: (forward, inverse) => ({
|
|
1779
|
+
targetFactory: forward.target,
|
|
1780
|
+
relationProperty: forward.relationProperty,
|
|
1781
|
+
inverse: {
|
|
1782
|
+
adjacencyFactory: inverse.adjacency,
|
|
1783
|
+
targetFactory: inverse.target,
|
|
1784
|
+
relationProperty: inverse.relationProperty
|
|
1785
|
+
}
|
|
1786
|
+
})
|
|
1860
1787
|
};
|
|
1861
1788
|
function edgeWrites(builder) {
|
|
1862
|
-
const edges = builder(
|
|
1789
|
+
const edges = builder(recorder);
|
|
1863
1790
|
if (edges.length === 0) {
|
|
1864
1791
|
throw new Error(
|
|
1865
1792
|
"edgeWrites(...) must declare at least one edge (w.putEdge / w.deleteEdge / w.updateKeyChangeEdge); an empty declaration writes nothing."
|
|
@@ -1901,6 +1828,13 @@ function assertRelationRoundTrips(entity, edgeFields, decl) {
|
|
|
1901
1828
|
}
|
|
1902
1829
|
return relation;
|
|
1903
1830
|
}
|
|
1831
|
+
function assertDualEdge(forwardAdjacency, inverseAdjacency, decl) {
|
|
1832
|
+
if (inverseAdjacency === forwardAdjacency) {
|
|
1833
|
+
throw new Error(
|
|
1834
|
+
`Dual-edge declaration for forward relation '${decl.relationProperty}' on adjacency entity '${forwardAdjacency.name}' points its inverse edge at the SAME adjacency model. A dual edge is TWO physical rows (forward + inverse); both directions resolving to one model would write the same key twice in a transaction (cf. #96). Declare the inverse on a DISTINCT adjacency model, or drop \`inverse\` for the single-row (base PK + inverse GSI) bidirectional edge.`
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1904
1838
|
function templateRecord(fields) {
|
|
1905
1839
|
const out = {};
|
|
1906
1840
|
for (const field of [...fields].sort()) {
|
|
@@ -1919,6 +1853,25 @@ function putItemTemplate(edgeFields) {
|
|
|
1919
1853
|
return templateRecord(edgeFields);
|
|
1920
1854
|
}
|
|
1921
1855
|
function deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle) {
|
|
1856
|
+
const forward = deriveOneEdgeRow(
|
|
1857
|
+
adjacencyClass,
|
|
1858
|
+
{ targetFactory: decl.targetFactory, relationProperty: decl.relationProperty },
|
|
1859
|
+
lifecycle
|
|
1860
|
+
);
|
|
1861
|
+
if (decl.inverse === void 0) return forward;
|
|
1862
|
+
const inverseAdjacency = decl.inverse.adjacencyFactory();
|
|
1863
|
+
assertDualEdge(adjacencyClass, inverseAdjacency, decl);
|
|
1864
|
+
const inverse = deriveOneEdgeRow(
|
|
1865
|
+
inverseAdjacency,
|
|
1866
|
+
{
|
|
1867
|
+
targetFactory: decl.inverse.targetFactory,
|
|
1868
|
+
relationProperty: decl.inverse.relationProperty
|
|
1869
|
+
},
|
|
1870
|
+
lifecycle
|
|
1871
|
+
);
|
|
1872
|
+
return [...forward, ...inverse];
|
|
1873
|
+
}
|
|
1874
|
+
function deriveOneEdgeRow(adjacencyClass, decl, lifecycle) {
|
|
1922
1875
|
const entity = MetadataRegistry.get(adjacencyClass);
|
|
1923
1876
|
if (!entity.primaryKey) {
|
|
1924
1877
|
throw new Error(
|
|
@@ -2234,6 +2187,13 @@ function renderOutboxEventItems(event, key, params, contextLabel) {
|
|
|
2234
2187
|
return { Put: put };
|
|
2235
2188
|
});
|
|
2236
2189
|
}
|
|
2190
|
+
function renderMaintainOutboxItems(outbox, key, params, contextLabel) {
|
|
2191
|
+
return outbox.items.map((item) => {
|
|
2192
|
+
const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} maintain-outbox Put`);
|
|
2193
|
+
const put = { TableName: TableMapping.resolve(item.tableName), Item };
|
|
2194
|
+
return { Put: put };
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2237
2197
|
function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
|
|
2238
2198
|
return guard.items.map((item) => {
|
|
2239
2199
|
const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} idempotency Put`);
|
|
@@ -2245,7 +2205,131 @@ function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
|
|
|
2245
2205
|
return { Put: put };
|
|
2246
2206
|
});
|
|
2247
2207
|
}
|
|
2248
|
-
|
|
2208
|
+
function applyProjectionTransform(op, args, value, contextLabel) {
|
|
2209
|
+
if (op === "identity") return value;
|
|
2210
|
+
if (op === "preview") {
|
|
2211
|
+
const n = args[0];
|
|
2212
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
|
|
2213
|
+
throw new Error(
|
|
2214
|
+
`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.`
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
if (value === null || value === void 0) return value;
|
|
2218
|
+
return String(value).slice(0, n);
|
|
2219
|
+
}
|
|
2220
|
+
throw new Error(
|
|
2221
|
+
`Contract runtime: ${contextLabel} declares an unknown projection transform op '${String(op)}' (expected 'identity' or 'preview').`
|
|
2222
|
+
);
|
|
2223
|
+
}
|
|
2224
|
+
function buildMaintainProjection(maintain, key, params, contextLabel) {
|
|
2225
|
+
const out = {};
|
|
2226
|
+
for (const [attr, transform] of Object.entries(maintain.projection)) {
|
|
2227
|
+
const value = renderTemplate(
|
|
2228
|
+
`{${transform.inputField}}`,
|
|
2229
|
+
key,
|
|
2230
|
+
params,
|
|
2231
|
+
`${contextLabel} projection '${attr}'`
|
|
2232
|
+
);
|
|
2233
|
+
out[attr] = applyProjectionTransform(transform.op, transform.args, value, `${contextLabel} '${attr}'`);
|
|
2234
|
+
}
|
|
2235
|
+
return out;
|
|
2236
|
+
}
|
|
2237
|
+
function renderMaintainWriteItem(maintain, key, params, contextLabel, modelBySignature) {
|
|
2238
|
+
const ownerClass = maintain.entity.modelClass;
|
|
2239
|
+
const metadata = MetadataRegistry.get(ownerClass);
|
|
2240
|
+
if (!metadata.primaryKey) {
|
|
2241
|
+
throw new Error(
|
|
2242
|
+
`Contract runtime: ${contextLabel} maintains a snapshot / collection on '${maintain.entity.name}', which has no primary key.`
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
const keyInput = {};
|
|
2246
|
+
for (const [ownerField, inputField] of Object.entries(maintain.keyBinding)) {
|
|
2247
|
+
keyInput[ownerField] = renderTemplate(
|
|
2248
|
+
`{${inputField}}`,
|
|
2249
|
+
key,
|
|
2250
|
+
params,
|
|
2251
|
+
`${contextLabel} maintain key`
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
2254
|
+
const { TableName, Key } = buildDeleteInput(
|
|
2255
|
+
ownerClass,
|
|
2256
|
+
keyInput
|
|
2257
|
+
);
|
|
2258
|
+
let updateInput;
|
|
2259
|
+
if (maintain.kind === "counter") {
|
|
2260
|
+
const counter = maintain.counter;
|
|
2261
|
+
if (counter === void 0) {
|
|
2262
|
+
throw new Error(
|
|
2263
|
+
`Contract runtime: ${contextLabel} is a 'counter' maintenance write with no \`counter\` payload \u2014 the compiler (#141) should have set it.`
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
if (counter.op !== "count") {
|
|
2267
|
+
throw new Error(
|
|
2268
|
+
`Contract runtime: ${contextLabel} is a 'counter' maintenance write with op '${counter.op}', which is the asynchronous stream path (#130) and must not be composed into a synchronous TransactWriteItems \u2014 the compiler should divert it to a maintenance-outbox row.`
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
updateInput = {
|
|
2272
|
+
TableName,
|
|
2273
|
+
Key,
|
|
2274
|
+
UpdateExpression: "ADD #a0 :a0",
|
|
2275
|
+
ExpressionAttributeNames: { "#a0": counter.attribute },
|
|
2276
|
+
ExpressionAttributeValues: { ":a0": counter.delta }
|
|
2277
|
+
};
|
|
2278
|
+
const out2 = { Update: updateInput };
|
|
2279
|
+
modelBySignature?.set(execItemKeySignature(out2), ownerClass.name);
|
|
2280
|
+
return out2;
|
|
2281
|
+
}
|
|
2282
|
+
if (maintain.kind === "membership") {
|
|
2283
|
+
throw new Error(
|
|
2284
|
+
`Contract runtime: ${contextLabel} is a 'membership' (sparse-view) maintenance write, which is stream-only (#133) and must not be composed into a synchronous TransactWriteItems \u2014 the compiler should divert it to a maintenance-outbox row.`
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
const projection = buildMaintainProjection(maintain, key, params, contextLabel);
|
|
2288
|
+
if (maintain.kind === "collection") {
|
|
2289
|
+
const field = maintain.collection?.field;
|
|
2290
|
+
if (field === void 0) {
|
|
2291
|
+
throw new Error(
|
|
2292
|
+
`Contract runtime: ${contextLabel} is a 'collection' maintenance write with no \`collection.field\` \u2014 the compiler (#125) should have set it.`
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
updateInput = {
|
|
2296
|
+
TableName,
|
|
2297
|
+
Key,
|
|
2298
|
+
UpdateExpression: "SET #c = list_append(if_not_exists(#c, :empty), :item)",
|
|
2299
|
+
ExpressionAttributeNames: { "#c": field },
|
|
2300
|
+
ExpressionAttributeValues: { ":empty": [], ":item": [projection] }
|
|
2301
|
+
};
|
|
2302
|
+
} else {
|
|
2303
|
+
const setClauses = [];
|
|
2304
|
+
const names = {};
|
|
2305
|
+
const values = {};
|
|
2306
|
+
let i = 0;
|
|
2307
|
+
for (const [attr, value] of Object.entries(projection)) {
|
|
2308
|
+
const nameTok = `#m${i}`;
|
|
2309
|
+
const valTok = `:m${i}`;
|
|
2310
|
+
setClauses.push(`${nameTok} = ${valTok}`);
|
|
2311
|
+
names[nameTok] = attr;
|
|
2312
|
+
values[valTok] = value;
|
|
2313
|
+
i += 1;
|
|
2314
|
+
}
|
|
2315
|
+
if (setClauses.length === 0) {
|
|
2316
|
+
throw new Error(
|
|
2317
|
+
`Contract runtime: ${contextLabel} is a 'snapshot' maintenance write with an empty projection \u2014 nothing to mirror onto '${maintain.entity.name}'.`
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
updateInput = {
|
|
2321
|
+
TableName,
|
|
2322
|
+
Key,
|
|
2323
|
+
UpdateExpression: `SET ${setClauses.join(", ")}`,
|
|
2324
|
+
ExpressionAttributeNames: names,
|
|
2325
|
+
ExpressionAttributeValues: values
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
const out = { Update: updateInput };
|
|
2329
|
+
modelBySignature?.set(execItemKeySignature(out), ownerClass.name);
|
|
2330
|
+
return out;
|
|
2331
|
+
}
|
|
2332
|
+
async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, maintainItems, maintainOutboxItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map(), retry, middleware) {
|
|
2249
2333
|
const writeItems = writes.map((w) => {
|
|
2250
2334
|
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
2251
2335
|
let item;
|
|
@@ -2268,6 +2352,8 @@ async function executeReferentialTransaction(writes, edgeItems, updateItems, gua
|
|
|
2268
2352
|
...guardItemsRaw,
|
|
2269
2353
|
...outboxItems,
|
|
2270
2354
|
...idempotencyItems,
|
|
2355
|
+
...maintainItems,
|
|
2356
|
+
...maintainOutboxItems,
|
|
2271
2357
|
...checkItems
|
|
2272
2358
|
],
|
|
2273
2359
|
{
|
|
@@ -2275,7 +2361,7 @@ async function executeReferentialTransaction(writes, edgeItems, updateItems, gua
|
|
|
2275
2361
|
...retry !== void 0 ? { retry } : {},
|
|
2276
2362
|
...middleware !== void 0 ? { middleware } : {},
|
|
2277
2363
|
limitError: (count) => new Error(
|
|
2278
|
-
`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.`
|
|
2364
|
+
`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.`
|
|
2279
2365
|
)
|
|
2280
2366
|
}
|
|
2281
2367
|
);
|
|
@@ -2335,10 +2421,10 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2335
2421
|
`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.`
|
|
2336
2422
|
);
|
|
2337
2423
|
}
|
|
2338
|
-
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;
|
|
2424
|
+
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.maintainOutbox !== void 0 && method.maintainOutbox.length > 0 || method.idempotencyGuard !== void 0;
|
|
2339
2425
|
if (hasDerivedEffects2) {
|
|
2340
2426
|
throw new Error(
|
|
2341
|
-
`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.`
|
|
2427
|
+
`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.`
|
|
2342
2428
|
);
|
|
2343
2429
|
}
|
|
2344
2430
|
const writes = keys.map(
|
|
@@ -2357,7 +2443,9 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2357
2443
|
const uniqueGuards = method.uniqueGuards ?? [];
|
|
2358
2444
|
const outboxEvents = method.outboxEvents ?? [];
|
|
2359
2445
|
const idempotencyGuard = method.idempotencyGuard;
|
|
2360
|
-
const
|
|
2446
|
+
const maintainWrites = method.maintainWrites ?? [];
|
|
2447
|
+
const maintainOutbox = method.maintainOutbox ?? [];
|
|
2448
|
+
const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || maintainWrites.length > 0 || maintainOutbox.length > 0 || idempotencyGuard !== void 0;
|
|
2361
2449
|
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
2362
2450
|
if (isMultiFragment || hasDerivedEffects) {
|
|
2363
2451
|
const ops = method.ops ?? [method.op];
|
|
@@ -2396,6 +2484,18 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2396
2484
|
params,
|
|
2397
2485
|
`command method '${methodName}' (idempotency)`
|
|
2398
2486
|
) : [];
|
|
2487
|
+
const maintainItems = maintainWrites.map(
|
|
2488
|
+
(m, i) => renderMaintainWriteItem(
|
|
2489
|
+
m,
|
|
2490
|
+
key,
|
|
2491
|
+
params,
|
|
2492
|
+
`command method '${methodName}' (maintain #${i})`,
|
|
2493
|
+
modelBySignature
|
|
2494
|
+
)
|
|
2495
|
+
);
|
|
2496
|
+
const maintainOutboxItems = maintainOutbox.flatMap(
|
|
2497
|
+
(mo, i) => renderMaintainOutboxItems(mo, key, params, `command method '${methodName}' (maintain-outbox #${i})`)
|
|
2498
|
+
);
|
|
2399
2499
|
const checks = conditionChecks.map((check, i) => ({
|
|
2400
2500
|
check: renderConditionCheck(
|
|
2401
2501
|
check,
|
|
@@ -2411,6 +2511,8 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2411
2511
|
guardItems,
|
|
2412
2512
|
outboxItems,
|
|
2413
2513
|
idempotencyItems,
|
|
2514
|
+
maintainItems,
|
|
2515
|
+
maintainOutboxItems,
|
|
2414
2516
|
checks,
|
|
2415
2517
|
methodName,
|
|
2416
2518
|
modelBySignature
|
|
@@ -2439,6 +2541,8 @@ function renderCompiledOp(op, ctx) {
|
|
|
2439
2541
|
const guardItems = [];
|
|
2440
2542
|
const outboxItems = [];
|
|
2441
2543
|
const idempotencyItems = [];
|
|
2544
|
+
const maintainItems = [];
|
|
2545
|
+
const maintainOutboxItems = [];
|
|
2442
2546
|
const checkItems = [];
|
|
2443
2547
|
for (const edge of fragment.edgeWrites ?? []) {
|
|
2444
2548
|
edgeItems.push(...renderEdgeWriteItems(edge, op.params, op.params, `${ctx} edge`, modelBySignature));
|
|
@@ -2457,10 +2561,20 @@ function renderCompiledOp(op, ctx) {
|
|
|
2457
2561
|
...renderIdempotencyGuardItems(fragment.idempotencyGuard, op.params, op.params, `${ctx} idempotency`)
|
|
2458
2562
|
);
|
|
2459
2563
|
}
|
|
2564
|
+
for (const m of fragment.maintainWrites ?? []) {
|
|
2565
|
+
maintainItems.push(
|
|
2566
|
+
renderMaintainWriteItem(m, op.params, op.params, `${ctx} maintain`, modelBySignature)
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
for (const mo of fragment.maintainOutbox ?? []) {
|
|
2570
|
+
maintainOutboxItems.push(
|
|
2571
|
+
...renderMaintainOutboxItems(mo, op.params, op.params, `${ctx} maintain-outbox`)
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
2460
2574
|
for (const c of fragment.conditionChecks ?? []) {
|
|
2461
2575
|
checkItems.push({ check: renderConditionCheck(c, op.params, op.params, `${ctx} requires`) });
|
|
2462
2576
|
}
|
|
2463
|
-
const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || checkItems.length > 0;
|
|
2577
|
+
const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || maintainItems.length > 0 || maintainOutboxItems.length > 0 || checkItems.length > 0;
|
|
2464
2578
|
return {
|
|
2465
2579
|
write,
|
|
2466
2580
|
edgeItems,
|
|
@@ -2468,6 +2582,8 @@ function renderCompiledOp(op, ctx) {
|
|
|
2468
2582
|
guardItems,
|
|
2469
2583
|
outboxItems,
|
|
2470
2584
|
idempotencyItems,
|
|
2585
|
+
maintainItems,
|
|
2586
|
+
maintainOutboxItems,
|
|
2471
2587
|
checkItems,
|
|
2472
2588
|
modelBySignature,
|
|
2473
2589
|
hasDerivedEffects
|
|
@@ -2512,6 +2628,8 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
2512
2628
|
const guardItems = [];
|
|
2513
2629
|
const outboxItems = [];
|
|
2514
2630
|
const idempotencyItems = [];
|
|
2631
|
+
const maintainItems = [];
|
|
2632
|
+
const maintainOutboxItems = [];
|
|
2515
2633
|
const checkItems = [];
|
|
2516
2634
|
for (const r of renderedOps) {
|
|
2517
2635
|
for (const [sig, name] of r.modelBySignature) modelBySignature.set(sig, name);
|
|
@@ -2520,6 +2638,8 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
2520
2638
|
guardItems.push(...r.guardItems);
|
|
2521
2639
|
outboxItems.push(...r.outboxItems);
|
|
2522
2640
|
idempotencyItems.push(...r.idempotencyItems);
|
|
2641
|
+
maintainItems.push(...r.maintainItems);
|
|
2642
|
+
maintainOutboxItems.push(...r.maintainOutboxItems);
|
|
2523
2643
|
checkItems.push(...r.checkItems);
|
|
2524
2644
|
}
|
|
2525
2645
|
const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
|
|
@@ -2538,6 +2658,8 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
2538
2658
|
guardItems,
|
|
2539
2659
|
outboxItems,
|
|
2540
2660
|
idempotencyItems,
|
|
2661
|
+
maintainItems,
|
|
2662
|
+
maintainOutboxItems,
|
|
2541
2663
|
checkItems,
|
|
2542
2664
|
label,
|
|
2543
2665
|
modelBySignature,
|
|
@@ -2635,6 +2757,8 @@ function rewrittenCompiledOp(write) {
|
|
|
2635
2757
|
guardItems: [],
|
|
2636
2758
|
outboxItems: [],
|
|
2637
2759
|
idempotencyItems: [],
|
|
2760
|
+
maintainItems: [],
|
|
2761
|
+
maintainOutboxItems: [],
|
|
2638
2762
|
checkItems: [],
|
|
2639
2763
|
modelBySignature: /* @__PURE__ */ new Map(),
|
|
2640
2764
|
hasDerivedEffects: false
|
|
@@ -3517,6 +3641,9 @@ function buildPlannedCommandMethod(name, planned) {
|
|
|
3517
3641
|
);
|
|
3518
3642
|
}
|
|
3519
3643
|
const idempotencyGuard = idempotencyGuards[0];
|
|
3644
|
+
const maintainWrites = plan2.fragments.flatMap((f) => f.maintainWrites ?? []);
|
|
3645
|
+
assertNoCrossFragmentMaintainCollision(name, maintainWrites);
|
|
3646
|
+
const maintainOutbox = plan2.fragments.flatMap((f) => f.maintainOutbox ?? []);
|
|
3520
3647
|
return {
|
|
3521
3648
|
__methodKind: "command",
|
|
3522
3649
|
op,
|
|
@@ -3527,6 +3654,8 @@ function buildPlannedCommandMethod(name, planned) {
|
|
|
3527
3654
|
...uniqueGuards.length > 0 ? { uniqueGuards } : {},
|
|
3528
3655
|
...outboxEvents.length > 0 ? { outboxEvents } : {},
|
|
3529
3656
|
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
|
|
3657
|
+
...maintainWrites.length > 0 ? { maintainWrites } : {},
|
|
3658
|
+
...maintainOutbox.length > 0 ? { maintainOutbox } : {},
|
|
3530
3659
|
inputArity: "either",
|
|
3531
3660
|
// A return projection means the method returns the read-back entity; otherwise
|
|
3532
3661
|
// it is a fire-and-forget write (`void`).
|
|
@@ -3814,6 +3943,210 @@ function deriveOneUpdate(fragment, effect) {
|
|
|
3814
3943
|
amount: effect.amount
|
|
3815
3944
|
};
|
|
3816
3945
|
}
|
|
3946
|
+
var INTENT_MAINTAIN_EVENT = {
|
|
3947
|
+
create: "created",
|
|
3948
|
+
update: "updated",
|
|
3949
|
+
remove: "removed"
|
|
3950
|
+
};
|
|
3951
|
+
var MAINTAIN_ENTITY_PATH_RE = /^\$\.entity\.([A-Za-z_$][\w$]*)$/;
|
|
3952
|
+
var cachedMaintenanceGraph;
|
|
3953
|
+
function globalMaintenanceGraph() {
|
|
3954
|
+
const generation = MetadataRegistry.generation;
|
|
3955
|
+
const viewGeneration = ViewRegistry.generation;
|
|
3956
|
+
if (cachedMaintenanceGraph?.generation !== generation || cachedMaintenanceGraph?.viewGeneration !== viewGeneration) {
|
|
3957
|
+
cachedMaintenanceGraph = { graph: buildMaintenanceGraph(), generation, viewGeneration };
|
|
3958
|
+
}
|
|
3959
|
+
return cachedMaintenanceGraph.graph;
|
|
3960
|
+
}
|
|
3961
|
+
function logicalEntityName(fragment) {
|
|
3962
|
+
const meta = MetadataRegistry.get(
|
|
3963
|
+
fragment.entity.modelClass
|
|
3964
|
+
);
|
|
3965
|
+
return meta.prefix.endsWith("#") ? meta.prefix.slice(0, -1) : meta.prefix;
|
|
3966
|
+
}
|
|
3967
|
+
function resolveMaintainers(fragment, graph) {
|
|
3968
|
+
const g = graph ?? globalMaintenanceGraph();
|
|
3969
|
+
const event = INTENT_MAINTAIN_EVENT[fragment.intent];
|
|
3970
|
+
const trigger = `${logicalEntityName(fragment)}.${event}`;
|
|
3971
|
+
return g.effectsFor(trigger);
|
|
3972
|
+
}
|
|
3973
|
+
function maintainPathToInputField(fragment, item, role, path) {
|
|
3974
|
+
const entityMatch = MAINTAIN_ENTITY_PATH_RE.exec(path);
|
|
3975
|
+
if (entityMatch !== null) return entityMatch[1];
|
|
3976
|
+
const inputMatch = INPUT_PATH_RE.exec(path);
|
|
3977
|
+
if (inputMatch !== null) return inputMatch[1];
|
|
3978
|
+
throw new Error(
|
|
3979
|
+
`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.`
|
|
3980
|
+
);
|
|
3981
|
+
}
|
|
3982
|
+
function deriveMaintainItems(fragment, items) {
|
|
3983
|
+
const byRow = /* @__PURE__ */ new Map();
|
|
3984
|
+
for (const item of items) {
|
|
3985
|
+
const prior = byRow.get(item.destinationRowKey);
|
|
3986
|
+
if (prior !== void 0) {
|
|
3987
|
+
const bothCounter = item.effect.kind === "counter" && prior.effect.kind === "counter";
|
|
3988
|
+
if (!bothCounter) {
|
|
3989
|
+
throw new Error(
|
|
3990
|
+
`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.)`
|
|
3991
|
+
);
|
|
3992
|
+
}
|
|
3993
|
+
} else {
|
|
3994
|
+
byRow.set(item.destinationRowKey, item);
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
return items.map((item) => deriveOneMaintainWrite(fragment, item));
|
|
3998
|
+
}
|
|
3999
|
+
function maintainWriteRowKey(write) {
|
|
4000
|
+
const parts = Object.entries(write.keyBinding).map(([field, inputField]) => `${field}=${inputField}`).sort();
|
|
4001
|
+
return `${write.entity.name}#${parts.join("&")}`;
|
|
4002
|
+
}
|
|
4003
|
+
function assertNoCrossFragmentMaintainCollision(methodName, writes) {
|
|
4004
|
+
const byRow = /* @__PURE__ */ new Map();
|
|
4005
|
+
for (const write of writes) {
|
|
4006
|
+
const rowKey = maintainWriteRowKey(write);
|
|
4007
|
+
const prior = byRow.get(rowKey);
|
|
4008
|
+
if (prior !== void 0) {
|
|
4009
|
+
const bothCounter = write.kind === "counter" && prior.kind === "counter";
|
|
4010
|
+
if (!bothCounter) {
|
|
4011
|
+
throw new Error(
|
|
4012
|
+
`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.)`
|
|
4013
|
+
);
|
|
4014
|
+
}
|
|
4015
|
+
} else {
|
|
4016
|
+
byRow.set(rowKey, write);
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
function deriveOneMaintainWrite(fragment, item) {
|
|
4021
|
+
const effect = item.effect;
|
|
4022
|
+
const updateMode = effect.updateMode === "stream" ? "stream" : "mutation";
|
|
4023
|
+
const ownerClass = effect.targetFactory();
|
|
4024
|
+
const ownerMeta = MetadataRegistry.get(
|
|
4025
|
+
ownerClass
|
|
4026
|
+
);
|
|
4027
|
+
const ownerName = ownerClass.name;
|
|
4028
|
+
const keyBinding = {};
|
|
4029
|
+
for (const [ownerField, source] of Object.entries(effect.keys)) {
|
|
4030
|
+
keyBinding[ownerField] = maintainPathToInputField(
|
|
4031
|
+
fragment,
|
|
4032
|
+
item,
|
|
4033
|
+
`destination-key field '${ownerField}'`,
|
|
4034
|
+
source
|
|
4035
|
+
);
|
|
4036
|
+
}
|
|
4037
|
+
if (effect.kind === "counter") {
|
|
4038
|
+
return deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding, updateMode);
|
|
4039
|
+
}
|
|
4040
|
+
const projection = {};
|
|
4041
|
+
for (const [attr, transform] of Object.entries(effect.project)) {
|
|
4042
|
+
const inputField = maintainPathToInputField(
|
|
4043
|
+
fragment,
|
|
4044
|
+
item,
|
|
4045
|
+
`projection attribute '${attr}'`,
|
|
4046
|
+
transform.path
|
|
4047
|
+
);
|
|
4048
|
+
projection[attr] = { op: transform.op, args: transform.args, inputField };
|
|
4049
|
+
}
|
|
4050
|
+
if (!ownerMeta.primaryKey) {
|
|
4051
|
+
throw new Error(
|
|
4052
|
+
`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.`
|
|
4053
|
+
);
|
|
4054
|
+
}
|
|
4055
|
+
const base = {
|
|
4056
|
+
entity: { name: ownerName, modelClass: ownerClass },
|
|
4057
|
+
relationProperty: item.relationProperty,
|
|
4058
|
+
trigger: item.trigger,
|
|
4059
|
+
updateMode,
|
|
4060
|
+
keyBinding,
|
|
4061
|
+
projection
|
|
4062
|
+
};
|
|
4063
|
+
if (effect.kind === "membership") {
|
|
4064
|
+
if (updateMode !== "stream") {
|
|
4065
|
+
throw new Error(
|
|
4066
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a sparse-view membership effect (view '${item.ownerEntity}', relation '${item.relationProperty}') on the SYNCHRONOUS \`mutation\` path. A membership row is PUT when its predicate holds and DELETED when it flips false; a single atomic transaction cannot branch an item between Put and Delete on a runtime predicate. Declare \`write: { updateMode: 'stream' }\` so the host-side drain applies the put/delete (#133 sparse view is the asynchronous stream path).`
|
|
4067
|
+
);
|
|
4068
|
+
}
|
|
4069
|
+
const predInput = maintainPathToInputField(
|
|
4070
|
+
fragment,
|
|
4071
|
+
item,
|
|
4072
|
+
"membership predicate",
|
|
4073
|
+
effect.predicate.path
|
|
4074
|
+
);
|
|
4075
|
+
return {
|
|
4076
|
+
...base,
|
|
4077
|
+
kind: "membership",
|
|
4078
|
+
membership: {
|
|
4079
|
+
op: effect.predicate.op,
|
|
4080
|
+
inputField: predInput,
|
|
4081
|
+
...effect.predicate.value !== void 0 ? { value: effect.predicate.value } : {}
|
|
4082
|
+
}
|
|
4083
|
+
};
|
|
4084
|
+
}
|
|
4085
|
+
if (effect.kind === "collection") {
|
|
4086
|
+
const orderBy = effect.collection.orderBy !== void 0 ? maintainPathToInputField(fragment, item, "collection `orderBy`", effect.collection.orderBy) : void 0;
|
|
4087
|
+
return {
|
|
4088
|
+
...base,
|
|
4089
|
+
kind: "collection",
|
|
4090
|
+
collection: {
|
|
4091
|
+
field: effect.collection.field,
|
|
4092
|
+
...effect.collection.maxItems !== void 0 ? { maxItems: effect.collection.maxItems } : {},
|
|
4093
|
+
...orderBy !== void 0 ? { orderBy } : {},
|
|
4094
|
+
...orderBy !== void 0 && effect.collection.orderDir !== void 0 ? { orderDir: effect.collection.orderDir } : {}
|
|
4095
|
+
}
|
|
4096
|
+
};
|
|
4097
|
+
}
|
|
4098
|
+
return { ...base, kind: "snapshot" };
|
|
4099
|
+
}
|
|
4100
|
+
function deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding, updateMode) {
|
|
4101
|
+
if (effect.value.op === "max") {
|
|
4102
|
+
if (updateMode !== "stream") {
|
|
4103
|
+
throw new Error(
|
|
4104
|
+
`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}')\` on the SYNCHRONOUS \`mutation\` path. 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). Use \`value: count()\` for a synchronous counter, or declare \`updateMode: 'stream'\` on the \`@aggregate\` to realize the running max.`
|
|
4105
|
+
);
|
|
4106
|
+
}
|
|
4107
|
+
if (!ownerMeta.primaryKey) {
|
|
4108
|
+
throw new Error(
|
|
4109
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a max counter maintenance effect on '${ownerName}', which has no primary key.`
|
|
4110
|
+
);
|
|
4111
|
+
}
|
|
4112
|
+
return {
|
|
4113
|
+
entity: { name: ownerName, modelClass: ownerClass },
|
|
4114
|
+
relationProperty: item.relationProperty,
|
|
4115
|
+
trigger: item.trigger,
|
|
4116
|
+
updateMode,
|
|
4117
|
+
kind: "counter",
|
|
4118
|
+
keyBinding,
|
|
4119
|
+
projection: {},
|
|
4120
|
+
counter: { attribute: effect.attribute, op: "max", field: effect.value.field }
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
if (effect.delta === void 0) {
|
|
4124
|
+
throw new Error(
|
|
4125
|
+
`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.`
|
|
4126
|
+
);
|
|
4127
|
+
}
|
|
4128
|
+
if (!ownerMeta.primaryKey) {
|
|
4129
|
+
throw new Error(
|
|
4130
|
+
`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.`
|
|
4131
|
+
);
|
|
4132
|
+
}
|
|
4133
|
+
const isField = ownerMeta.fields.some((f) => f.propertyName === effect.attribute) || ownerMeta.aggregates.some((a) => a.propertyName === effect.attribute);
|
|
4134
|
+
if (!isField) {
|
|
4135
|
+
throw new Error(
|
|
4136
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' derives a counter on '${ownerName}.${effect.attribute}', which is not a field of '${ownerName}'.`
|
|
4137
|
+
);
|
|
4138
|
+
}
|
|
4139
|
+
return {
|
|
4140
|
+
entity: { name: ownerName, modelClass: ownerClass },
|
|
4141
|
+
relationProperty: item.relationProperty,
|
|
4142
|
+
trigger: item.trigger,
|
|
4143
|
+
updateMode,
|
|
4144
|
+
kind: "counter",
|
|
4145
|
+
keyBinding,
|
|
4146
|
+
projection: {},
|
|
4147
|
+
counter: { attribute: effect.attribute, op: "count", delta: effect.delta }
|
|
4148
|
+
};
|
|
4149
|
+
}
|
|
3817
4150
|
var UNIQUE_GUARD_PK_PREFIX = "UNIQUE#";
|
|
3818
4151
|
var UNIQUE_GUARD_SK_PREFIX = "VALUE#";
|
|
3819
4152
|
function deriveUniqueGuards(fragment, unique) {
|
|
@@ -3993,6 +4326,67 @@ function deriveIdempotencyGuard(fragment, effect) {
|
|
|
3993
4326
|
]
|
|
3994
4327
|
};
|
|
3995
4328
|
}
|
|
4329
|
+
var MAINT_OUTBOX_PK_PREFIX = "OUTBOX#MAINT#";
|
|
4330
|
+
var MAINT_OUTBOX_SK_PREFIX = "MAINT#";
|
|
4331
|
+
var MAINT_OUTBOX_OWNER_ATTR = "__maintOwner__";
|
|
4332
|
+
var MAINT_OUTBOX_RELATION_ATTR = "__maintRelation__";
|
|
4333
|
+
var MAINT_OUTBOX_TRIGGER_ATTR = "__maintTrigger__";
|
|
4334
|
+
var MAINT_OUTBOX_EVENT_VALUE = "maint";
|
|
4335
|
+
function deriveMaintainOutbox(fragment, write) {
|
|
4336
|
+
const keyFields = primaryKeyFields(fragment);
|
|
4337
|
+
const discriminator = `${write.entity.name}#${write.relationProperty}`;
|
|
4338
|
+
const keySegments = keyFields.map((f) => `#{${f}}`).join("");
|
|
4339
|
+
const pk = MAINT_OUTBOX_PK_PREFIX + discriminator + keySegments;
|
|
4340
|
+
const sk = MAINT_OUTBOX_SK_PREFIX + keyFields.map((f) => `{${f}}`).join("#");
|
|
4341
|
+
const item = {
|
|
4342
|
+
PK: pk,
|
|
4343
|
+
SK: sk,
|
|
4344
|
+
[OUTBOX_EVENT_NAME_ATTR]: MAINT_OUTBOX_EVENT_VALUE,
|
|
4345
|
+
[MAINT_OUTBOX_OWNER_ATTR]: write.entity.name,
|
|
4346
|
+
[MAINT_OUTBOX_RELATION_ATTR]: write.relationProperty,
|
|
4347
|
+
[MAINT_OUTBOX_TRIGGER_ATTR]: write.trigger
|
|
4348
|
+
};
|
|
4349
|
+
const sourceFields = /* @__PURE__ */ new Set();
|
|
4350
|
+
for (const inputField of Object.values(write.keyBinding)) sourceFields.add(inputField);
|
|
4351
|
+
for (const transform of Object.values(write.projection)) sourceFields.add(transform.inputField);
|
|
4352
|
+
if (write.collection?.orderBy !== void 0) sourceFields.add(write.collection.orderBy);
|
|
4353
|
+
if (write.counter !== void 0 && write.counter.op === "max") {
|
|
4354
|
+
sourceFields.add(write.counter.field);
|
|
4355
|
+
}
|
|
4356
|
+
if (write.membership !== void 0) sourceFields.add(write.membership.inputField);
|
|
4357
|
+
for (const field of sourceFields) {
|
|
4358
|
+
item[field] = `{${field}}`;
|
|
4359
|
+
}
|
|
4360
|
+
return {
|
|
4361
|
+
ownerEntity: write.entity.name,
|
|
4362
|
+
relationProperty: write.relationProperty,
|
|
4363
|
+
trigger: write.trigger,
|
|
4364
|
+
items: [
|
|
4365
|
+
{
|
|
4366
|
+
type: "Put",
|
|
4367
|
+
tableName: tableNameOf(fragment),
|
|
4368
|
+
entity: MARKER_ROW_ENTITY,
|
|
4369
|
+
item,
|
|
4370
|
+
// No `attribute_not_exists`: the row keys off the source entity key, which the
|
|
4371
|
+
// source `Put`'s own create guard already makes unique — the maintenance intent
|
|
4372
|
+
// is recorded atomically with the source row it derives from.
|
|
4373
|
+
literalKey: true
|
|
4374
|
+
}
|
|
4375
|
+
]
|
|
4376
|
+
};
|
|
4377
|
+
}
|
|
4378
|
+
function splitMaintainByMode(fragment, writes) {
|
|
4379
|
+
const sync = [];
|
|
4380
|
+
const outbox = [];
|
|
4381
|
+
for (const write of writes) {
|
|
4382
|
+
if (write.updateMode === "stream") {
|
|
4383
|
+
outbox.push(deriveMaintainOutbox(fragment, write));
|
|
4384
|
+
} else {
|
|
4385
|
+
sync.push(write);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
return { sync, outbox };
|
|
4389
|
+
}
|
|
3996
4390
|
function resolveLifecycle(fragment) {
|
|
3997
4391
|
const writes = fragment.use ?? getEntityWrites(fragment.entity.modelClass);
|
|
3998
4392
|
if (writes === void 0) return void 0;
|
|
@@ -4032,7 +4426,7 @@ function renderInputLeaf(leaf, isKeyFieldInPut, consumerIndex, consumerField, re
|
|
|
4032
4426
|
}
|
|
4033
4427
|
return leaf;
|
|
4034
4428
|
}
|
|
4035
|
-
function compileFragment(fragment, index = 0, resolveEntityRef = null) {
|
|
4429
|
+
function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenanceGraph) {
|
|
4036
4430
|
const keyFields = primaryKeyFields(fragment);
|
|
4037
4431
|
const keyFieldSet = new Set(keyFields);
|
|
4038
4432
|
const operation = INTENT_OPERATION[fragment.intent];
|
|
@@ -4088,6 +4482,11 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null) {
|
|
|
4088
4482
|
const uniqueGuards = lifecycle?.effects.unique !== void 0 && lifecycle.effects.unique.length > 0 ? deriveUniqueGuards(fragment, lifecycle.effects.unique) : void 0;
|
|
4089
4483
|
const outboxEvents = lifecycle?.effects.emits !== void 0 && lifecycle.effects.emits.length > 0 ? deriveOutboxEvents(fragment, lifecycle.effects.emits) : void 0;
|
|
4090
4484
|
const idempotencyGuard = lifecycle?.effects.idempotency !== void 0 ? deriveIdempotencyGuard(fragment, lifecycle.effects.idempotency) : void 0;
|
|
4485
|
+
const maintainers = resolveMaintainers(fragment, maintenanceGraph);
|
|
4486
|
+
const allMaintainWrites = maintainers.length > 0 ? deriveMaintainItems(fragment, maintainers) : void 0;
|
|
4487
|
+
const { sync: syncMaintainWrites, outbox: maintainOutboxAll } = allMaintainWrites !== void 0 ? splitMaintainByMode(fragment, allMaintainWrites) : { sync: [], outbox: [] };
|
|
4488
|
+
const maintainWrites = syncMaintainWrites.length > 0 ? syncMaintainWrites : void 0;
|
|
4489
|
+
const maintainOutbox = maintainOutboxAll.length > 0 ? maintainOutboxAll : void 0;
|
|
4091
4490
|
return {
|
|
4092
4491
|
op,
|
|
4093
4492
|
keyFields,
|
|
@@ -4097,7 +4496,9 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null) {
|
|
|
4097
4496
|
...derivedUpdates !== void 0 ? { derivedUpdates } : {},
|
|
4098
4497
|
...uniqueGuards !== void 0 ? { uniqueGuards } : {},
|
|
4099
4498
|
...outboxEvents !== void 0 ? { outboxEvents } : {},
|
|
4100
|
-
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {}
|
|
4499
|
+
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
|
|
4500
|
+
...maintainWrites !== void 0 ? { maintainWrites } : {},
|
|
4501
|
+
...maintainOutbox !== void 0 ? { maintainOutbox } : {}
|
|
4101
4502
|
};
|
|
4102
4503
|
}
|
|
4103
4504
|
function compileSingleFragmentPlan(plan2) {
|
|
@@ -4624,7 +5025,7 @@ function runBuildPass(params, build, scalarFields, arrayParams, pass, accessed,
|
|
|
4624
5025
|
const recordWrite = (instr) => {
|
|
4625
5026
|
currentBody.push(instr);
|
|
4626
5027
|
};
|
|
4627
|
-
const
|
|
5028
|
+
const recorder2 = {
|
|
4628
5029
|
put(model, item, options) {
|
|
4629
5030
|
recordWrite({
|
|
4630
5031
|
kind: "write",
|
|
@@ -4717,7 +5118,7 @@ function runBuildPass(params, build, scalarFields, arrayParams, pass, accessed,
|
|
|
4717
5118
|
currentBody.push(block);
|
|
4718
5119
|
}
|
|
4719
5120
|
};
|
|
4720
|
-
build(
|
|
5121
|
+
build(recorder2, pProxy);
|
|
4721
5122
|
return instructions;
|
|
4722
5123
|
}
|
|
4723
5124
|
function defineTransaction(params, build) {
|
|
@@ -6143,6 +6544,75 @@ function buildDerivedUpdateItems(derivedUpdates) {
|
|
|
6143
6544
|
}
|
|
6144
6545
|
return { items, params };
|
|
6145
6546
|
}
|
|
6547
|
+
function buildMaintainWriteItems(maintainWrites) {
|
|
6548
|
+
const items = [];
|
|
6549
|
+
const params = {};
|
|
6550
|
+
for (const maintain of maintainWrites) {
|
|
6551
|
+
const metadata = MetadataRegistry.get(
|
|
6552
|
+
maintain.entity.modelClass
|
|
6553
|
+
);
|
|
6554
|
+
if (!metadata.primaryKey) {
|
|
6555
|
+
throw new Error(
|
|
6556
|
+
`maintenance write: the maintained entity '${maintain.entity.name}' has no primary key, so a maintenance \`UpdateItem\` cannot be keyed.`
|
|
6557
|
+
);
|
|
6558
|
+
}
|
|
6559
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
6560
|
+
const present = new Set(Object.keys(maintain.keyBinding));
|
|
6561
|
+
const { pk, sk } = evaluateKey(
|
|
6562
|
+
metadata.primaryKey.segmented,
|
|
6563
|
+
"param",
|
|
6564
|
+
present,
|
|
6565
|
+
(field) => maintain.keyBinding[field] ?? field
|
|
6566
|
+
);
|
|
6567
|
+
const keyCondition = { PK: pk };
|
|
6568
|
+
if (sk !== void 0) keyCondition.SK = sk;
|
|
6569
|
+
if (maintain.kind === "membership") {
|
|
6570
|
+
throw new Error(
|
|
6571
|
+
`maintenance write: a sparse-view membership maintainer ('${maintain.relationProperty}' on '${maintain.entity.name}') reached the synchronous transaction spec builder. Membership is stream-only (#133) \u2014 it should have been diverted to a maintenance-outbox row. This is a compiler defect.`
|
|
6572
|
+
);
|
|
6573
|
+
}
|
|
6574
|
+
const projection = {};
|
|
6575
|
+
for (const [attr, transform] of Object.entries(maintain.projection)) {
|
|
6576
|
+
projection[attr] = {
|
|
6577
|
+
op: transform.op,
|
|
6578
|
+
args: [...transform.args],
|
|
6579
|
+
inputField: transform.inputField
|
|
6580
|
+
};
|
|
6581
|
+
}
|
|
6582
|
+
const maintainSpec = {
|
|
6583
|
+
kind: maintain.kind,
|
|
6584
|
+
relationProperty: maintain.relationProperty,
|
|
6585
|
+
trigger: maintain.trigger,
|
|
6586
|
+
projection,
|
|
6587
|
+
...maintain.kind === "collection" && maintain.collection !== void 0 ? {
|
|
6588
|
+
collection: {
|
|
6589
|
+
field: maintain.collection.field,
|
|
6590
|
+
...maintain.collection.maxItems !== void 0 ? { maxItems: maintain.collection.maxItems } : {},
|
|
6591
|
+
...maintain.collection.orderBy !== void 0 ? { orderBy: maintain.collection.orderBy } : {},
|
|
6592
|
+
...maintain.collection.orderBy !== void 0 && maintain.collection.orderDir !== void 0 ? { orderDir: maintain.collection.orderDir } : {}
|
|
6593
|
+
}
|
|
6594
|
+
} : {},
|
|
6595
|
+
// #141: a counter carries its scalar `ADD` (attribute + literal numeric delta);
|
|
6596
|
+
// the runtimes render `ADD #attr :delta` exactly as the self-lifecycle derived
|
|
6597
|
+
// counter (#85) does, so a same-row counter merge stays consistent across paths.
|
|
6598
|
+
...maintain.kind === "counter" && maintain.counter !== void 0 && maintain.counter.op === "count" ? {
|
|
6599
|
+
counter: {
|
|
6600
|
+
attribute: maintain.counter.attribute,
|
|
6601
|
+
delta: String(maintain.counter.delta)
|
|
6602
|
+
}
|
|
6603
|
+
} : {}
|
|
6604
|
+
};
|
|
6605
|
+
items.push({
|
|
6606
|
+
type: "Update",
|
|
6607
|
+
tableName,
|
|
6608
|
+
entity: maintain.entity.name,
|
|
6609
|
+
keyCondition,
|
|
6610
|
+
maintain: maintainSpec
|
|
6611
|
+
});
|
|
6612
|
+
collectTemplateParams(keyCondition, metadata, params);
|
|
6613
|
+
}
|
|
6614
|
+
return { items, params };
|
|
6615
|
+
}
|
|
6146
6616
|
function buildUniqueGuardItems(uniqueGuards, entityMetadata) {
|
|
6147
6617
|
const items = [];
|
|
6148
6618
|
const params = {};
|
|
@@ -6166,6 +6636,17 @@ function buildOutboxEventItems(outboxEvents, entityMetadata) {
|
|
|
6166
6636
|
}
|
|
6167
6637
|
return { items, params };
|
|
6168
6638
|
}
|
|
6639
|
+
function buildMaintainOutboxItems(maintainOutbox, entityMetadata) {
|
|
6640
|
+
const items = [];
|
|
6641
|
+
const params = {};
|
|
6642
|
+
for (const outbox of maintainOutbox) {
|
|
6643
|
+
for (const item of outbox.items) {
|
|
6644
|
+
items.push({ ...item, tableName: TableMapping.resolve(item.tableName) });
|
|
6645
|
+
collectTemplateParams(item.item, entityMetadata, params);
|
|
6646
|
+
}
|
|
6647
|
+
}
|
|
6648
|
+
return { items, params };
|
|
6649
|
+
}
|
|
6169
6650
|
function buildIdempotencyGuardItems(guard, entityMetadata) {
|
|
6170
6651
|
const items = [];
|
|
6171
6652
|
const params = {};
|
|
@@ -6175,7 +6656,7 @@ function buildIdempotencyGuardItems(guard, entityMetadata) {
|
|
|
6175
6656
|
}
|
|
6176
6657
|
return { items, params };
|
|
6177
6658
|
}
|
|
6178
|
-
function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard) {
|
|
6659
|
+
function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard, maintainWrites, maintainOutbox) {
|
|
6179
6660
|
const label = opRefName(contractName, methodName);
|
|
6180
6661
|
const base = composeFragmentTransaction(contractName, methodName, ops);
|
|
6181
6662
|
const { items: edgeItemsRaw, params: edgeParams } = buildEdgeWriteItems(edgeWrites2);
|
|
@@ -6193,6 +6674,8 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
6193
6674
|
primaryMetadata
|
|
6194
6675
|
);
|
|
6195
6676
|
const { items: idempotencyItems, params: idempotencyParams } = idempotencyGuard !== void 0 ? buildIdempotencyGuardItems(idempotencyGuard, primaryMetadata) : { items: [], params: {} };
|
|
6677
|
+
const { items: maintainItems, params: maintainParams } = buildMaintainWriteItems(maintainWrites);
|
|
6678
|
+
const { items: maintainOutboxItems, params: maintainOutboxParams } = buildMaintainOutboxItems(maintainOutbox, primaryMetadata);
|
|
6196
6679
|
const baseKeys = new Set(base.items.map(itemKeySignature));
|
|
6197
6680
|
for (const update of updateItems) {
|
|
6198
6681
|
if (baseKeys.has(itemKeySignature(update))) {
|
|
@@ -6215,7 +6698,9 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
6215
6698
|
checkParams,
|
|
6216
6699
|
guardParams,
|
|
6217
6700
|
outboxParams,
|
|
6218
|
-
idempotencyParams
|
|
6701
|
+
idempotencyParams,
|
|
6702
|
+
maintainParams,
|
|
6703
|
+
maintainOutboxParams
|
|
6219
6704
|
]) {
|
|
6220
6705
|
for (const [name, spec] of Object.entries(source)) params[name] = spec;
|
|
6221
6706
|
}
|
|
@@ -6226,6 +6711,8 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
6226
6711
|
...guardItems,
|
|
6227
6712
|
...outboxItems,
|
|
6228
6713
|
...idempotencyItems,
|
|
6714
|
+
...maintainItems,
|
|
6715
|
+
...maintainOutboxItems,
|
|
6229
6716
|
...checkItems
|
|
6230
6717
|
];
|
|
6231
6718
|
if (items.length > MAX_TRANSACT_ITEMS) {
|
|
@@ -6331,7 +6818,9 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
|
|
|
6331
6818
|
const uniqueGuards = method.uniqueGuards ?? [];
|
|
6332
6819
|
const outboxEvents = method.outboxEvents ?? [];
|
|
6333
6820
|
const idempotencyGuard = method.idempotencyGuard;
|
|
6334
|
-
const
|
|
6821
|
+
const maintainWrites = method.maintainWrites ?? [];
|
|
6822
|
+
const maintainOutbox = method.maintainOutbox ?? [];
|
|
6823
|
+
const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0 || maintainWrites.length > 0 || maintainOutbox.length > 0;
|
|
6335
6824
|
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
6336
6825
|
const promotedToTransaction = isMultiFragment || hasDerivedEffects;
|
|
6337
6826
|
if (promotedToTransaction) {
|
|
@@ -6349,7 +6838,9 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
|
|
|
6349
6838
|
derivedUpdates,
|
|
6350
6839
|
uniqueGuards,
|
|
6351
6840
|
outboxEvents,
|
|
6352
|
-
idempotencyGuard
|
|
6841
|
+
idempotencyGuard,
|
|
6842
|
+
maintainWrites,
|
|
6843
|
+
maintainOutbox
|
|
6353
6844
|
)
|
|
6354
6845
|
) : (
|
|
6355
6846
|
// #90: writes only.
|
|
@@ -6515,13 +7006,6 @@ export {
|
|
|
6515
7006
|
BatchGetResult,
|
|
6516
7007
|
executeBatchGet,
|
|
6517
7008
|
executeBatchWrite,
|
|
6518
|
-
LIFECYCLE_CONTRACT_MARKER,
|
|
6519
|
-
isLifecycleContract,
|
|
6520
|
-
ENTITY_WRITES_MARKER,
|
|
6521
|
-
isEntityWritesDefinition,
|
|
6522
|
-
entityWrites,
|
|
6523
|
-
getEntityWrites,
|
|
6524
|
-
lifecyclePhaseForIntent,
|
|
6525
7009
|
OLD_VALUE_NAMESPACE,
|
|
6526
7010
|
EDGE_WRITES_MARKER,
|
|
6527
7011
|
isEdgeWritesDefinition,
|