graphddb 0.2.5 → 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 +9 -1
- package/dist/{chunk-4TYK3AV6.js → chunk-6AIAHP3A.js} +471 -14
- package/dist/{chunk-SGYBE2OV.js → chunk-QEOFIXTN.js} +818 -13
- package/dist/{chunk-ITHQ2EDH.js → chunk-QQNP43JL.js} +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +113 -75
- package/dist/index.js +131 -7
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types--tdHK3vi.d.ts → types-xJRn5qkv.d.ts} +884 -16
- package/package.json +1 -1
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
serializeFieldValue,
|
|
38
38
|
serializeRawCondition,
|
|
39
39
|
skTemplate
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-6AIAHP3A.js";
|
|
41
41
|
|
|
42
42
|
// src/spec/types.ts
|
|
43
43
|
var SPEC_VERSION = "1.0";
|
|
@@ -1757,6 +1757,46 @@ async function executeBatchWrite(requests) {
|
|
|
1757
1757
|
}
|
|
1758
1758
|
|
|
1759
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
|
+
}
|
|
1760
1800
|
var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
|
|
1761
1801
|
"graphddb:lifecycleContract"
|
|
1762
1802
|
);
|
|
@@ -1799,6 +1839,9 @@ var recorder = {
|
|
|
1799
1839
|
increment(targetFactory, keys, attribute, amount) {
|
|
1800
1840
|
return { kind: "derive", targetFactory, keys, attribute, amount };
|
|
1801
1841
|
},
|
|
1842
|
+
transform(path, op, ...args) {
|
|
1843
|
+
return makeProjectionTransform(path, op, args);
|
|
1844
|
+
},
|
|
1802
1845
|
event(name, payload) {
|
|
1803
1846
|
return { kind: "event", name, payload };
|
|
1804
1847
|
},
|
|
@@ -1986,6 +2029,387 @@ function deriveModelEdgeWriteItems(modelClass, lifecycle) {
|
|
|
1986
2029
|
return deriveEdgeWriteItems(modelClass, writes, lifecycle);
|
|
1987
2030
|
}
|
|
1988
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
|
+
|
|
1989
2413
|
// src/runtime/command-runtime.ts
|
|
1990
2414
|
function resolveCommandMethod(contract, methodName) {
|
|
1991
2415
|
const method = contract.methods[methodName];
|
|
@@ -2245,7 +2669,121 @@ function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
|
|
|
2245
2669
|
return { Put: put };
|
|
2246
2670
|
});
|
|
2247
2671
|
}
|
|
2248
|
-
|
|
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) {
|
|
2249
2787
|
const writeItems = writes.map((w) => {
|
|
2250
2788
|
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
2251
2789
|
let item;
|
|
@@ -2268,6 +2806,7 @@ async function executeReferentialTransaction(writes, edgeItems, updateItems, gua
|
|
|
2268
2806
|
...guardItemsRaw,
|
|
2269
2807
|
...outboxItems,
|
|
2270
2808
|
...idempotencyItems,
|
|
2809
|
+
...maintainItems,
|
|
2271
2810
|
...checkItems
|
|
2272
2811
|
],
|
|
2273
2812
|
{
|
|
@@ -2275,7 +2814,7 @@ async function executeReferentialTransaction(writes, edgeItems, updateItems, gua
|
|
|
2275
2814
|
...retry !== void 0 ? { retry } : {},
|
|
2276
2815
|
...middleware !== void 0 ? { middleware } : {},
|
|
2277
2816
|
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.`
|
|
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.`
|
|
2279
2818
|
)
|
|
2280
2819
|
}
|
|
2281
2820
|
);
|
|
@@ -2335,10 +2874,10 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2335
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.`
|
|
2336
2875
|
);
|
|
2337
2876
|
}
|
|
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;
|
|
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;
|
|
2339
2878
|
if (hasDerivedEffects2) {
|
|
2340
2879
|
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.`
|
|
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.`
|
|
2342
2881
|
);
|
|
2343
2882
|
}
|
|
2344
2883
|
const writes = keys.map(
|
|
@@ -2357,7 +2896,8 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2357
2896
|
const uniqueGuards = method.uniqueGuards ?? [];
|
|
2358
2897
|
const outboxEvents = method.outboxEvents ?? [];
|
|
2359
2898
|
const idempotencyGuard = method.idempotencyGuard;
|
|
2360
|
-
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;
|
|
2361
2901
|
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
2362
2902
|
if (isMultiFragment || hasDerivedEffects) {
|
|
2363
2903
|
const ops = method.ops ?? [method.op];
|
|
@@ -2396,6 +2936,15 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2396
2936
|
params,
|
|
2397
2937
|
`command method '${methodName}' (idempotency)`
|
|
2398
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
|
+
);
|
|
2399
2948
|
const checks = conditionChecks.map((check, i) => ({
|
|
2400
2949
|
check: renderConditionCheck(
|
|
2401
2950
|
check,
|
|
@@ -2411,6 +2960,7 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2411
2960
|
guardItems,
|
|
2412
2961
|
outboxItems,
|
|
2413
2962
|
idempotencyItems,
|
|
2963
|
+
maintainItems,
|
|
2414
2964
|
checks,
|
|
2415
2965
|
methodName,
|
|
2416
2966
|
modelBySignature
|
|
@@ -2439,6 +2989,7 @@ function renderCompiledOp(op, ctx) {
|
|
|
2439
2989
|
const guardItems = [];
|
|
2440
2990
|
const outboxItems = [];
|
|
2441
2991
|
const idempotencyItems = [];
|
|
2992
|
+
const maintainItems = [];
|
|
2442
2993
|
const checkItems = [];
|
|
2443
2994
|
for (const edge of fragment.edgeWrites ?? []) {
|
|
2444
2995
|
edgeItems.push(...renderEdgeWriteItems(edge, op.params, op.params, `${ctx} edge`, modelBySignature));
|
|
@@ -2457,10 +3008,15 @@ function renderCompiledOp(op, ctx) {
|
|
|
2457
3008
|
...renderIdempotencyGuardItems(fragment.idempotencyGuard, op.params, op.params, `${ctx} idempotency`)
|
|
2458
3009
|
);
|
|
2459
3010
|
}
|
|
3011
|
+
for (const m of fragment.maintainWrites ?? []) {
|
|
3012
|
+
maintainItems.push(
|
|
3013
|
+
renderMaintainWriteItem(m, op.params, op.params, `${ctx} maintain`, modelBySignature)
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
2460
3016
|
for (const c of fragment.conditionChecks ?? []) {
|
|
2461
3017
|
checkItems.push({ check: renderConditionCheck(c, op.params, op.params, `${ctx} requires`) });
|
|
2462
3018
|
}
|
|
2463
|
-
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;
|
|
2464
3020
|
return {
|
|
2465
3021
|
write,
|
|
2466
3022
|
edgeItems,
|
|
@@ -2468,6 +3024,7 @@ function renderCompiledOp(op, ctx) {
|
|
|
2468
3024
|
guardItems,
|
|
2469
3025
|
outboxItems,
|
|
2470
3026
|
idempotencyItems,
|
|
3027
|
+
maintainItems,
|
|
2471
3028
|
checkItems,
|
|
2472
3029
|
modelBySignature,
|
|
2473
3030
|
hasDerivedEffects
|
|
@@ -2512,6 +3069,7 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
2512
3069
|
const guardItems = [];
|
|
2513
3070
|
const outboxItems = [];
|
|
2514
3071
|
const idempotencyItems = [];
|
|
3072
|
+
const maintainItems = [];
|
|
2515
3073
|
const checkItems = [];
|
|
2516
3074
|
for (const r of renderedOps) {
|
|
2517
3075
|
for (const [sig, name] of r.modelBySignature) modelBySignature.set(sig, name);
|
|
@@ -2520,6 +3078,7 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
2520
3078
|
guardItems.push(...r.guardItems);
|
|
2521
3079
|
outboxItems.push(...r.outboxItems);
|
|
2522
3080
|
idempotencyItems.push(...r.idempotencyItems);
|
|
3081
|
+
maintainItems.push(...r.maintainItems);
|
|
2523
3082
|
checkItems.push(...r.checkItems);
|
|
2524
3083
|
}
|
|
2525
3084
|
const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
|
|
@@ -2538,6 +3097,7 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
2538
3097
|
guardItems,
|
|
2539
3098
|
outboxItems,
|
|
2540
3099
|
idempotencyItems,
|
|
3100
|
+
maintainItems,
|
|
2541
3101
|
checkItems,
|
|
2542
3102
|
label,
|
|
2543
3103
|
modelBySignature,
|
|
@@ -2635,6 +3195,7 @@ function rewrittenCompiledOp(write) {
|
|
|
2635
3195
|
guardItems: [],
|
|
2636
3196
|
outboxItems: [],
|
|
2637
3197
|
idempotencyItems: [],
|
|
3198
|
+
maintainItems: [],
|
|
2638
3199
|
checkItems: [],
|
|
2639
3200
|
modelBySignature: /* @__PURE__ */ new Map(),
|
|
2640
3201
|
hasDerivedEffects: false
|
|
@@ -3517,6 +4078,8 @@ function buildPlannedCommandMethod(name, planned) {
|
|
|
3517
4078
|
);
|
|
3518
4079
|
}
|
|
3519
4080
|
const idempotencyGuard = idempotencyGuards[0];
|
|
4081
|
+
const maintainWrites = plan2.fragments.flatMap((f) => f.maintainWrites ?? []);
|
|
4082
|
+
assertNoCrossFragmentMaintainCollision(name, maintainWrites);
|
|
3520
4083
|
return {
|
|
3521
4084
|
__methodKind: "command",
|
|
3522
4085
|
op,
|
|
@@ -3527,6 +4090,7 @@ function buildPlannedCommandMethod(name, planned) {
|
|
|
3527
4090
|
...uniqueGuards.length > 0 ? { uniqueGuards } : {},
|
|
3528
4091
|
...outboxEvents.length > 0 ? { outboxEvents } : {},
|
|
3529
4092
|
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
|
|
4093
|
+
...maintainWrites.length > 0 ? { maintainWrites } : {},
|
|
3530
4094
|
inputArity: "either",
|
|
3531
4095
|
// A return projection means the method returns the read-back entity; otherwise
|
|
3532
4096
|
// it is a fire-and-forget write (`void`).
|
|
@@ -3814,6 +4378,171 @@ function deriveOneUpdate(fragment, effect) {
|
|
|
3814
4378
|
amount: effect.amount
|
|
3815
4379
|
};
|
|
3816
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
|
+
}
|
|
3817
4546
|
var UNIQUE_GUARD_PK_PREFIX = "UNIQUE#";
|
|
3818
4547
|
var UNIQUE_GUARD_SK_PREFIX = "VALUE#";
|
|
3819
4548
|
function deriveUniqueGuards(fragment, unique) {
|
|
@@ -4032,7 +4761,7 @@ function renderInputLeaf(leaf, isKeyFieldInPut, consumerIndex, consumerField, re
|
|
|
4032
4761
|
}
|
|
4033
4762
|
return leaf;
|
|
4034
4763
|
}
|
|
4035
|
-
function compileFragment(fragment, index = 0, resolveEntityRef = null) {
|
|
4764
|
+
function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenanceGraph) {
|
|
4036
4765
|
const keyFields = primaryKeyFields(fragment);
|
|
4037
4766
|
const keyFieldSet = new Set(keyFields);
|
|
4038
4767
|
const operation = INTENT_OPERATION[fragment.intent];
|
|
@@ -4088,6 +4817,8 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null) {
|
|
|
4088
4817
|
const uniqueGuards = lifecycle?.effects.unique !== void 0 && lifecycle.effects.unique.length > 0 ? deriveUniqueGuards(fragment, lifecycle.effects.unique) : void 0;
|
|
4089
4818
|
const outboxEvents = lifecycle?.effects.emits !== void 0 && lifecycle.effects.emits.length > 0 ? deriveOutboxEvents(fragment, lifecycle.effects.emits) : void 0;
|
|
4090
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;
|
|
4091
4822
|
return {
|
|
4092
4823
|
op,
|
|
4093
4824
|
keyFields,
|
|
@@ -4097,7 +4828,8 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null) {
|
|
|
4097
4828
|
...derivedUpdates !== void 0 ? { derivedUpdates } : {},
|
|
4098
4829
|
...uniqueGuards !== void 0 ? { uniqueGuards } : {},
|
|
4099
4830
|
...outboxEvents !== void 0 ? { outboxEvents } : {},
|
|
4100
|
-
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {}
|
|
4831
|
+
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
|
|
4832
|
+
...maintainWrites !== void 0 ? { maintainWrites } : {}
|
|
4101
4833
|
};
|
|
4102
4834
|
}
|
|
4103
4835
|
function compileSingleFragmentPlan(plan2) {
|
|
@@ -6143,6 +6875,69 @@ function buildDerivedUpdateItems(derivedUpdates) {
|
|
|
6143
6875
|
}
|
|
6144
6876
|
return { items, params };
|
|
6145
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
|
+
}
|
|
6146
6941
|
function buildUniqueGuardItems(uniqueGuards, entityMetadata) {
|
|
6147
6942
|
const items = [];
|
|
6148
6943
|
const params = {};
|
|
@@ -6175,7 +6970,7 @@ function buildIdempotencyGuardItems(guard, entityMetadata) {
|
|
|
6175
6970
|
}
|
|
6176
6971
|
return { items, params };
|
|
6177
6972
|
}
|
|
6178
|
-
function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard) {
|
|
6973
|
+
function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard, maintainWrites) {
|
|
6179
6974
|
const label = opRefName(contractName, methodName);
|
|
6180
6975
|
const base = composeFragmentTransaction(contractName, methodName, ops);
|
|
6181
6976
|
const { items: edgeItemsRaw, params: edgeParams } = buildEdgeWriteItems(edgeWrites2);
|
|
@@ -6193,6 +6988,7 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
6193
6988
|
primaryMetadata
|
|
6194
6989
|
);
|
|
6195
6990
|
const { items: idempotencyItems, params: idempotencyParams } = idempotencyGuard !== void 0 ? buildIdempotencyGuardItems(idempotencyGuard, primaryMetadata) : { items: [], params: {} };
|
|
6991
|
+
const { items: maintainItems, params: maintainParams } = buildMaintainWriteItems(maintainWrites);
|
|
6196
6992
|
const baseKeys = new Set(base.items.map(itemKeySignature));
|
|
6197
6993
|
for (const update of updateItems) {
|
|
6198
6994
|
if (baseKeys.has(itemKeySignature(update))) {
|
|
@@ -6215,7 +7011,8 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
6215
7011
|
checkParams,
|
|
6216
7012
|
guardParams,
|
|
6217
7013
|
outboxParams,
|
|
6218
|
-
idempotencyParams
|
|
7014
|
+
idempotencyParams,
|
|
7015
|
+
maintainParams
|
|
6219
7016
|
]) {
|
|
6220
7017
|
for (const [name, spec] of Object.entries(source)) params[name] = spec;
|
|
6221
7018
|
}
|
|
@@ -6226,6 +7023,7 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
6226
7023
|
...guardItems,
|
|
6227
7024
|
...outboxItems,
|
|
6228
7025
|
...idempotencyItems,
|
|
7026
|
+
...maintainItems,
|
|
6229
7027
|
...checkItems
|
|
6230
7028
|
];
|
|
6231
7029
|
if (items.length > MAX_TRANSACT_ITEMS) {
|
|
@@ -6331,7 +7129,8 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
|
|
|
6331
7129
|
const uniqueGuards = method.uniqueGuards ?? [];
|
|
6332
7130
|
const outboxEvents = method.outboxEvents ?? [];
|
|
6333
7131
|
const idempotencyGuard = method.idempotencyGuard;
|
|
6334
|
-
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;
|
|
6335
7134
|
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
6336
7135
|
const promotedToTransaction = isMultiFragment || hasDerivedEffects;
|
|
6337
7136
|
if (promotedToTransaction) {
|
|
@@ -6349,7 +7148,8 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
|
|
|
6349
7148
|
derivedUpdates,
|
|
6350
7149
|
uniqueGuards,
|
|
6351
7150
|
outboxEvents,
|
|
6352
|
-
idempotencyGuard
|
|
7151
|
+
idempotencyGuard,
|
|
7152
|
+
maintainWrites
|
|
6353
7153
|
)
|
|
6354
7154
|
) : (
|
|
6355
7155
|
// #90: writes only.
|
|
@@ -6515,6 +7315,10 @@ export {
|
|
|
6515
7315
|
BatchGetResult,
|
|
6516
7316
|
executeBatchGet,
|
|
6517
7317
|
executeBatchWrite,
|
|
7318
|
+
maintainTrigger,
|
|
7319
|
+
isMaintainTrigger,
|
|
7320
|
+
preview,
|
|
7321
|
+
identity,
|
|
6518
7322
|
LIFECYCLE_CONTRACT_MARKER,
|
|
6519
7323
|
isLifecycleContract,
|
|
6520
7324
|
ENTITY_WRITES_MARKER,
|
|
@@ -6530,6 +7334,7 @@ export {
|
|
|
6530
7334
|
deriveEdgeWriteItems,
|
|
6531
7335
|
getEdgeWrites,
|
|
6532
7336
|
deriveModelEdgeWriteItems,
|
|
7337
|
+
buildMaintenanceGraph,
|
|
6533
7338
|
resolveLifecycle,
|
|
6534
7339
|
compileFragment,
|
|
6535
7340
|
compileSingleFragmentPlan,
|