graphddb 0.3.0 → 0.4.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 +1 -0
- package/dist/{chunk-6AIAHP3A.js → chunk-72VZYOSG.js} +850 -1
- package/dist/{chunk-QQNP43JL.js → chunk-GWFZVNAV.js} +347 -2
- package/dist/{chunk-QEOFIXTN.js → chunk-PC54CW5P.js} +231 -554
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +673 -5
- package/dist/index.js +658 -21
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types-xJRn5qkv.d.ts → types-CpCo8yHP.d.ts} +398 -66
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
attachModelClass,
|
|
11
11
|
buildConditionExpression,
|
|
12
12
|
buildDeleteInput,
|
|
13
|
+
buildMaintenanceGraph,
|
|
13
14
|
buildPutInput,
|
|
14
15
|
buildReadRuntime,
|
|
15
16
|
buildUpdateInput,
|
|
@@ -25,9 +26,13 @@ import {
|
|
|
25
26
|
executePut,
|
|
26
27
|
executeTransaction,
|
|
27
28
|
executeUpdate,
|
|
29
|
+
getEntityWrites,
|
|
28
30
|
isColumn,
|
|
31
|
+
isEntityWritesDefinition,
|
|
32
|
+
isLifecycleContract,
|
|
29
33
|
isParam,
|
|
30
34
|
isRawCondition,
|
|
35
|
+
lifecyclePhaseForIntent,
|
|
31
36
|
param,
|
|
32
37
|
pkTemplate,
|
|
33
38
|
resolveKey,
|
|
@@ -37,7 +42,7 @@ import {
|
|
|
37
42
|
serializeFieldValue,
|
|
38
43
|
serializeRawCondition,
|
|
39
44
|
skTemplate
|
|
40
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-72VZYOSG.js";
|
|
41
46
|
|
|
42
47
|
// src/spec/types.ts
|
|
43
48
|
var SPEC_VERSION = "1.0";
|
|
@@ -1756,137 +1761,6 @@ async function executeBatchWrite(requests) {
|
|
|
1756
1761
|
}
|
|
1757
1762
|
}
|
|
1758
1763
|
|
|
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
|
-
}
|
|
1800
|
-
var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
|
|
1801
|
-
"graphddb:lifecycleContract"
|
|
1802
|
-
);
|
|
1803
|
-
function isLifecycleContract(value) {
|
|
1804
|
-
return typeof value === "object" && value !== null && value[LIFECYCLE_CONTRACT_MARKER] === true;
|
|
1805
|
-
}
|
|
1806
|
-
var ENTITY_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:entityWrites");
|
|
1807
|
-
function isEntityWritesDefinition(value) {
|
|
1808
|
-
return typeof value === "object" && value !== null && value[ENTITY_WRITES_MARKER] === true;
|
|
1809
|
-
}
|
|
1810
|
-
function freezeEffects(effects) {
|
|
1811
|
-
const out = {};
|
|
1812
|
-
if (effects.requires !== void 0) out.requires = effects.requires;
|
|
1813
|
-
if (effects.unique !== void 0) out.unique = effects.unique;
|
|
1814
|
-
if (effects.edges !== void 0) out.edges = effects.edges;
|
|
1815
|
-
if (effects.derive !== void 0) out.derive = effects.derive;
|
|
1816
|
-
if (effects.emits !== void 0) out.emits = effects.emits;
|
|
1817
|
-
if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
|
|
1818
|
-
return out;
|
|
1819
|
-
}
|
|
1820
|
-
var recorder = {
|
|
1821
|
-
lifecycle(effects = {}) {
|
|
1822
|
-
return {
|
|
1823
|
-
[LIFECYCLE_CONTRACT_MARKER]: true,
|
|
1824
|
-
effects: freezeEffects(effects)
|
|
1825
|
-
};
|
|
1826
|
-
},
|
|
1827
|
-
exists(targetFactory, keys) {
|
|
1828
|
-
return { kind: "requires", targetFactory, keys };
|
|
1829
|
-
},
|
|
1830
|
-
unique(spec) {
|
|
1831
|
-
return { kind: "unique", name: spec.name, scope: spec.scope, fields: spec.fields };
|
|
1832
|
-
},
|
|
1833
|
-
putEdge(targetFactory, relationProperty) {
|
|
1834
|
-
return { kind: "putEdge", targetFactory, relationProperty };
|
|
1835
|
-
},
|
|
1836
|
-
deleteEdge(targetFactory, relationProperty) {
|
|
1837
|
-
return { kind: "deleteEdge", targetFactory, relationProperty };
|
|
1838
|
-
},
|
|
1839
|
-
increment(targetFactory, keys, attribute, amount) {
|
|
1840
|
-
return { kind: "derive", targetFactory, keys, attribute, amount };
|
|
1841
|
-
},
|
|
1842
|
-
transform(path, op, ...args) {
|
|
1843
|
-
return makeProjectionTransform(path, op, args);
|
|
1844
|
-
},
|
|
1845
|
-
event(name, payload) {
|
|
1846
|
-
return { kind: "event", name, payload };
|
|
1847
|
-
},
|
|
1848
|
-
idempotentBy(token) {
|
|
1849
|
-
return { kind: "idempotency", token };
|
|
1850
|
-
}
|
|
1851
|
-
};
|
|
1852
|
-
function entityWrites(builder) {
|
|
1853
|
-
const shape = builder(recorder);
|
|
1854
|
-
for (const phase of ["create", "update", "remove"]) {
|
|
1855
|
-
const contract = shape[phase];
|
|
1856
|
-
if (contract !== void 0 && !isLifecycleContract(contract)) {
|
|
1857
|
-
throw new Error(
|
|
1858
|
-
`entityWrites: the '${phase}' lifecycle must be built with \`w.lifecycle({...})\` (it carries the \xA72 effect arrays), but received ${JSON.stringify(contract)}.`
|
|
1859
|
-
);
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
|
|
1863
|
-
throw new Error(
|
|
1864
|
-
"entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`); an empty save contract declares nothing."
|
|
1865
|
-
);
|
|
1866
|
-
}
|
|
1867
|
-
return {
|
|
1868
|
-
[ENTITY_WRITES_MARKER]: true,
|
|
1869
|
-
...shape.create !== void 0 ? { create: shape.create } : {},
|
|
1870
|
-
...shape.update !== void 0 ? { update: shape.update } : {},
|
|
1871
|
-
...shape.remove !== void 0 ? { remove: shape.remove } : {}
|
|
1872
|
-
};
|
|
1873
|
-
}
|
|
1874
|
-
function getEntityWrites(modelClass) {
|
|
1875
|
-
for (const name of Object.getOwnPropertyNames(modelClass)) {
|
|
1876
|
-
let value;
|
|
1877
|
-
try {
|
|
1878
|
-
value = modelClass[name];
|
|
1879
|
-
} catch {
|
|
1880
|
-
continue;
|
|
1881
|
-
}
|
|
1882
|
-
if (isEntityWritesDefinition(value)) return value;
|
|
1883
|
-
}
|
|
1884
|
-
return void 0;
|
|
1885
|
-
}
|
|
1886
|
-
function lifecyclePhaseForIntent(intent) {
|
|
1887
|
-
return intent;
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
1764
|
// src/relation/edge-write.ts
|
|
1891
1765
|
var OLD_VALUE_NAMESPACE = "old";
|
|
1892
1766
|
var EDGE_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:edgeWrites");
|
|
@@ -1896,13 +1770,22 @@ function isEdgeWritesDefinition(value) {
|
|
|
1896
1770
|
function declaration(target, relationProperty) {
|
|
1897
1771
|
return { targetFactory: target, relationProperty };
|
|
1898
1772
|
}
|
|
1899
|
-
var
|
|
1773
|
+
var recorder = {
|
|
1900
1774
|
putEdge: declaration,
|
|
1901
1775
|
deleteEdge: declaration,
|
|
1902
|
-
updateKeyChangeEdge: declaration
|
|
1776
|
+
updateKeyChangeEdge: declaration,
|
|
1777
|
+
dualEdge: (forward, inverse) => ({
|
|
1778
|
+
targetFactory: forward.target,
|
|
1779
|
+
relationProperty: forward.relationProperty,
|
|
1780
|
+
inverse: {
|
|
1781
|
+
adjacencyFactory: inverse.adjacency,
|
|
1782
|
+
targetFactory: inverse.target,
|
|
1783
|
+
relationProperty: inverse.relationProperty
|
|
1784
|
+
}
|
|
1785
|
+
})
|
|
1903
1786
|
};
|
|
1904
1787
|
function edgeWrites(builder) {
|
|
1905
|
-
const edges = builder(
|
|
1788
|
+
const edges = builder(recorder);
|
|
1906
1789
|
if (edges.length === 0) {
|
|
1907
1790
|
throw new Error(
|
|
1908
1791
|
"edgeWrites(...) must declare at least one edge (w.putEdge / w.deleteEdge / w.updateKeyChangeEdge); an empty declaration writes nothing."
|
|
@@ -1944,6 +1827,13 @@ function assertRelationRoundTrips(entity, edgeFields, decl) {
|
|
|
1944
1827
|
}
|
|
1945
1828
|
return relation;
|
|
1946
1829
|
}
|
|
1830
|
+
function assertDualEdge(forwardAdjacency, inverseAdjacency, decl) {
|
|
1831
|
+
if (inverseAdjacency === forwardAdjacency) {
|
|
1832
|
+
throw new Error(
|
|
1833
|
+
`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.`
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1947
1837
|
function templateRecord(fields) {
|
|
1948
1838
|
const out = {};
|
|
1949
1839
|
for (const field of [...fields].sort()) {
|
|
@@ -1962,6 +1852,25 @@ function putItemTemplate(edgeFields) {
|
|
|
1962
1852
|
return templateRecord(edgeFields);
|
|
1963
1853
|
}
|
|
1964
1854
|
function deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle) {
|
|
1855
|
+
const forward = deriveOneEdgeRow(
|
|
1856
|
+
adjacencyClass,
|
|
1857
|
+
{ targetFactory: decl.targetFactory, relationProperty: decl.relationProperty },
|
|
1858
|
+
lifecycle
|
|
1859
|
+
);
|
|
1860
|
+
if (decl.inverse === void 0) return forward;
|
|
1861
|
+
const inverseAdjacency = decl.inverse.adjacencyFactory();
|
|
1862
|
+
assertDualEdge(adjacencyClass, inverseAdjacency, decl);
|
|
1863
|
+
const inverse = deriveOneEdgeRow(
|
|
1864
|
+
inverseAdjacency,
|
|
1865
|
+
{
|
|
1866
|
+
targetFactory: decl.inverse.targetFactory,
|
|
1867
|
+
relationProperty: decl.inverse.relationProperty
|
|
1868
|
+
},
|
|
1869
|
+
lifecycle
|
|
1870
|
+
);
|
|
1871
|
+
return [...forward, ...inverse];
|
|
1872
|
+
}
|
|
1873
|
+
function deriveOneEdgeRow(adjacencyClass, decl, lifecycle) {
|
|
1965
1874
|
const entity = MetadataRegistry.get(adjacencyClass);
|
|
1966
1875
|
if (!entity.primaryKey) {
|
|
1967
1876
|
throw new Error(
|
|
@@ -2029,387 +1938,6 @@ function deriveModelEdgeWriteItems(modelClass, lifecycle) {
|
|
|
2029
1938
|
return deriveEdgeWriteItems(modelClass, writes, lifecycle);
|
|
2030
1939
|
}
|
|
2031
1940
|
|
|
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
|
-
|
|
2413
1941
|
// src/runtime/command-runtime.ts
|
|
2414
1942
|
function resolveCommandMethod(contract, methodName) {
|
|
2415
1943
|
const method = contract.methods[methodName];
|
|
@@ -2658,6 +2186,13 @@ function renderOutboxEventItems(event, key, params, contextLabel) {
|
|
|
2658
2186
|
return { Put: put };
|
|
2659
2187
|
});
|
|
2660
2188
|
}
|
|
2189
|
+
function renderMaintainOutboxItems(outbox, key, params, contextLabel) {
|
|
2190
|
+
return outbox.items.map((item) => {
|
|
2191
|
+
const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} maintain-outbox Put`);
|
|
2192
|
+
const put = { TableName: TableMapping.resolve(item.tableName), Item };
|
|
2193
|
+
return { Put: put };
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2661
2196
|
function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
|
|
2662
2197
|
return guard.items.map((item) => {
|
|
2663
2198
|
const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} idempotency Put`);
|
|
@@ -2727,6 +2262,11 @@ function renderMaintainWriteItem(maintain, key, params, contextLabel, modelBySig
|
|
|
2727
2262
|
`Contract runtime: ${contextLabel} is a 'counter' maintenance write with no \`counter\` payload \u2014 the compiler (#141) should have set it.`
|
|
2728
2263
|
);
|
|
2729
2264
|
}
|
|
2265
|
+
if (counter.op !== "count") {
|
|
2266
|
+
throw new Error(
|
|
2267
|
+
`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.`
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2730
2270
|
updateInput = {
|
|
2731
2271
|
TableName,
|
|
2732
2272
|
Key,
|
|
@@ -2738,6 +2278,11 @@ function renderMaintainWriteItem(maintain, key, params, contextLabel, modelBySig
|
|
|
2738
2278
|
modelBySignature?.set(execItemKeySignature(out2), ownerClass.name);
|
|
2739
2279
|
return out2;
|
|
2740
2280
|
}
|
|
2281
|
+
if (maintain.kind === "membership") {
|
|
2282
|
+
throw new Error(
|
|
2283
|
+
`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.`
|
|
2284
|
+
);
|
|
2285
|
+
}
|
|
2741
2286
|
const projection = buildMaintainProjection(maintain, key, params, contextLabel);
|
|
2742
2287
|
if (maintain.kind === "collection") {
|
|
2743
2288
|
const field = maintain.collection?.field;
|
|
@@ -2783,7 +2328,7 @@ function renderMaintainWriteItem(maintain, key, params, contextLabel, modelBySig
|
|
|
2783
2328
|
modelBySignature?.set(execItemKeySignature(out), ownerClass.name);
|
|
2784
2329
|
return out;
|
|
2785
2330
|
}
|
|
2786
|
-
async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, maintainItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map(), retry, middleware) {
|
|
2331
|
+
async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, maintainItems, maintainOutboxItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map(), retry, middleware) {
|
|
2787
2332
|
const writeItems = writes.map((w) => {
|
|
2788
2333
|
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
2789
2334
|
let item;
|
|
@@ -2807,6 +2352,7 @@ async function executeReferentialTransaction(writes, edgeItems, updateItems, gua
|
|
|
2807
2352
|
...outboxItems,
|
|
2808
2353
|
...idempotencyItems,
|
|
2809
2354
|
...maintainItems,
|
|
2355
|
+
...maintainOutboxItems,
|
|
2810
2356
|
...checkItems
|
|
2811
2357
|
],
|
|
2812
2358
|
{
|
|
@@ -2874,7 +2420,7 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2874
2420
|
`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.`
|
|
2875
2421
|
);
|
|
2876
2422
|
}
|
|
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;
|
|
2423
|
+
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;
|
|
2878
2424
|
if (hasDerivedEffects2) {
|
|
2879
2425
|
throw new Error(
|
|
2880
2426
|
`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.`
|
|
@@ -2897,7 +2443,8 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2897
2443
|
const outboxEvents = method.outboxEvents ?? [];
|
|
2898
2444
|
const idempotencyGuard = method.idempotencyGuard;
|
|
2899
2445
|
const maintainWrites = method.maintainWrites ?? [];
|
|
2900
|
-
const
|
|
2446
|
+
const maintainOutbox = method.maintainOutbox ?? [];
|
|
2447
|
+
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;
|
|
2901
2448
|
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
2902
2449
|
if (isMultiFragment || hasDerivedEffects) {
|
|
2903
2450
|
const ops = method.ops ?? [method.op];
|
|
@@ -2945,6 +2492,9 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2945
2492
|
modelBySignature
|
|
2946
2493
|
)
|
|
2947
2494
|
);
|
|
2495
|
+
const maintainOutboxItems = maintainOutbox.flatMap(
|
|
2496
|
+
(mo, i) => renderMaintainOutboxItems(mo, key, params, `command method '${methodName}' (maintain-outbox #${i})`)
|
|
2497
|
+
);
|
|
2948
2498
|
const checks = conditionChecks.map((check, i) => ({
|
|
2949
2499
|
check: renderConditionCheck(
|
|
2950
2500
|
check,
|
|
@@ -2961,6 +2511,7 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2961
2511
|
outboxItems,
|
|
2962
2512
|
idempotencyItems,
|
|
2963
2513
|
maintainItems,
|
|
2514
|
+
maintainOutboxItems,
|
|
2964
2515
|
checks,
|
|
2965
2516
|
methodName,
|
|
2966
2517
|
modelBySignature
|
|
@@ -2990,6 +2541,7 @@ function renderCompiledOp(op, ctx) {
|
|
|
2990
2541
|
const outboxItems = [];
|
|
2991
2542
|
const idempotencyItems = [];
|
|
2992
2543
|
const maintainItems = [];
|
|
2544
|
+
const maintainOutboxItems = [];
|
|
2993
2545
|
const checkItems = [];
|
|
2994
2546
|
for (const edge of fragment.edgeWrites ?? []) {
|
|
2995
2547
|
edgeItems.push(...renderEdgeWriteItems(edge, op.params, op.params, `${ctx} edge`, modelBySignature));
|
|
@@ -3013,10 +2565,15 @@ function renderCompiledOp(op, ctx) {
|
|
|
3013
2565
|
renderMaintainWriteItem(m, op.params, op.params, `${ctx} maintain`, modelBySignature)
|
|
3014
2566
|
);
|
|
3015
2567
|
}
|
|
2568
|
+
for (const mo of fragment.maintainOutbox ?? []) {
|
|
2569
|
+
maintainOutboxItems.push(
|
|
2570
|
+
...renderMaintainOutboxItems(mo, op.params, op.params, `${ctx} maintain-outbox`)
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
3016
2573
|
for (const c of fragment.conditionChecks ?? []) {
|
|
3017
2574
|
checkItems.push({ check: renderConditionCheck(c, op.params, op.params, `${ctx} requires`) });
|
|
3018
2575
|
}
|
|
3019
|
-
const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || maintainItems.length > 0 || checkItems.length > 0;
|
|
2576
|
+
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;
|
|
3020
2577
|
return {
|
|
3021
2578
|
write,
|
|
3022
2579
|
edgeItems,
|
|
@@ -3025,6 +2582,7 @@ function renderCompiledOp(op, ctx) {
|
|
|
3025
2582
|
outboxItems,
|
|
3026
2583
|
idempotencyItems,
|
|
3027
2584
|
maintainItems,
|
|
2585
|
+
maintainOutboxItems,
|
|
3028
2586
|
checkItems,
|
|
3029
2587
|
modelBySignature,
|
|
3030
2588
|
hasDerivedEffects
|
|
@@ -3070,6 +2628,7 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
3070
2628
|
const outboxItems = [];
|
|
3071
2629
|
const idempotencyItems = [];
|
|
3072
2630
|
const maintainItems = [];
|
|
2631
|
+
const maintainOutboxItems = [];
|
|
3073
2632
|
const checkItems = [];
|
|
3074
2633
|
for (const r of renderedOps) {
|
|
3075
2634
|
for (const [sig, name] of r.modelBySignature) modelBySignature.set(sig, name);
|
|
@@ -3079,6 +2638,7 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
3079
2638
|
outboxItems.push(...r.outboxItems);
|
|
3080
2639
|
idempotencyItems.push(...r.idempotencyItems);
|
|
3081
2640
|
maintainItems.push(...r.maintainItems);
|
|
2641
|
+
maintainOutboxItems.push(...r.maintainOutboxItems);
|
|
3082
2642
|
checkItems.push(...r.checkItems);
|
|
3083
2643
|
}
|
|
3084
2644
|
const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
|
|
@@ -3098,6 +2658,7 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
3098
2658
|
outboxItems,
|
|
3099
2659
|
idempotencyItems,
|
|
3100
2660
|
maintainItems,
|
|
2661
|
+
maintainOutboxItems,
|
|
3101
2662
|
checkItems,
|
|
3102
2663
|
label,
|
|
3103
2664
|
modelBySignature,
|
|
@@ -3196,6 +2757,7 @@ function rewrittenCompiledOp(write) {
|
|
|
3196
2757
|
outboxItems: [],
|
|
3197
2758
|
idempotencyItems: [],
|
|
3198
2759
|
maintainItems: [],
|
|
2760
|
+
maintainOutboxItems: [],
|
|
3199
2761
|
checkItems: [],
|
|
3200
2762
|
modelBySignature: /* @__PURE__ */ new Map(),
|
|
3201
2763
|
hasDerivedEffects: false
|
|
@@ -4080,6 +3642,7 @@ function buildPlannedCommandMethod(name, planned) {
|
|
|
4080
3642
|
const idempotencyGuard = idempotencyGuards[0];
|
|
4081
3643
|
const maintainWrites = plan2.fragments.flatMap((f) => f.maintainWrites ?? []);
|
|
4082
3644
|
assertNoCrossFragmentMaintainCollision(name, maintainWrites);
|
|
3645
|
+
const maintainOutbox = plan2.fragments.flatMap((f) => f.maintainOutbox ?? []);
|
|
4083
3646
|
return {
|
|
4084
3647
|
__methodKind: "command",
|
|
4085
3648
|
op,
|
|
@@ -4091,6 +3654,7 @@ function buildPlannedCommandMethod(name, planned) {
|
|
|
4091
3654
|
...outboxEvents.length > 0 ? { outboxEvents } : {},
|
|
4092
3655
|
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
|
|
4093
3656
|
...maintainWrites.length > 0 ? { maintainWrites } : {},
|
|
3657
|
+
...maintainOutbox.length > 0 ? { maintainOutbox } : {},
|
|
4094
3658
|
inputArity: "either",
|
|
4095
3659
|
// A return projection means the method returns the read-back entity; otherwise
|
|
4096
3660
|
// it is a fire-and-forget write (`void`).
|
|
@@ -4453,11 +4017,7 @@ function assertNoCrossFragmentMaintainCollision(methodName, writes) {
|
|
|
4453
4017
|
}
|
|
4454
4018
|
function deriveOneMaintainWrite(fragment, item) {
|
|
4455
4019
|
const effect = item.effect;
|
|
4456
|
-
|
|
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
|
-
}
|
|
4020
|
+
const updateMode = effect.updateMode === "stream" ? "stream" : "mutation";
|
|
4461
4021
|
const ownerClass = effect.targetFactory();
|
|
4462
4022
|
const ownerMeta = MetadataRegistry.get(
|
|
4463
4023
|
ownerClass
|
|
@@ -4473,7 +4033,7 @@ function deriveOneMaintainWrite(fragment, item) {
|
|
|
4473
4033
|
);
|
|
4474
4034
|
}
|
|
4475
4035
|
if (effect.kind === "counter") {
|
|
4476
|
-
return deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding);
|
|
4036
|
+
return deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding, updateMode);
|
|
4477
4037
|
}
|
|
4478
4038
|
const projection = {};
|
|
4479
4039
|
for (const [attr, transform] of Object.entries(effect.project)) {
|
|
@@ -4494,9 +4054,32 @@ function deriveOneMaintainWrite(fragment, item) {
|
|
|
4494
4054
|
entity: { name: ownerName, modelClass: ownerClass },
|
|
4495
4055
|
relationProperty: item.relationProperty,
|
|
4496
4056
|
trigger: item.trigger,
|
|
4057
|
+
updateMode,
|
|
4497
4058
|
keyBinding,
|
|
4498
4059
|
projection
|
|
4499
4060
|
};
|
|
4061
|
+
if (effect.kind === "membership") {
|
|
4062
|
+
if (updateMode !== "stream") {
|
|
4063
|
+
throw new Error(
|
|
4064
|
+
`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).`
|
|
4065
|
+
);
|
|
4066
|
+
}
|
|
4067
|
+
const predInput = maintainPathToInputField(
|
|
4068
|
+
fragment,
|
|
4069
|
+
item,
|
|
4070
|
+
"membership predicate",
|
|
4071
|
+
effect.predicate.path
|
|
4072
|
+
);
|
|
4073
|
+
return {
|
|
4074
|
+
...base,
|
|
4075
|
+
kind: "membership",
|
|
4076
|
+
membership: {
|
|
4077
|
+
op: effect.predicate.op,
|
|
4078
|
+
inputField: predInput,
|
|
4079
|
+
...effect.predicate.value !== void 0 ? { value: effect.predicate.value } : {}
|
|
4080
|
+
}
|
|
4081
|
+
};
|
|
4082
|
+
}
|
|
4500
4083
|
if (effect.kind === "collection") {
|
|
4501
4084
|
const orderBy = effect.collection.orderBy !== void 0 ? maintainPathToInputField(fragment, item, "collection `orderBy`", effect.collection.orderBy) : void 0;
|
|
4502
4085
|
return {
|
|
@@ -4505,17 +4088,35 @@ function deriveOneMaintainWrite(fragment, item) {
|
|
|
4505
4088
|
collection: {
|
|
4506
4089
|
field: effect.collection.field,
|
|
4507
4090
|
...effect.collection.maxItems !== void 0 ? { maxItems: effect.collection.maxItems } : {},
|
|
4508
|
-
...orderBy !== void 0 ? { orderBy } : {}
|
|
4091
|
+
...orderBy !== void 0 ? { orderBy } : {},
|
|
4092
|
+
...orderBy !== void 0 && effect.collection.orderDir !== void 0 ? { orderDir: effect.collection.orderDir } : {}
|
|
4509
4093
|
}
|
|
4510
4094
|
};
|
|
4511
4095
|
}
|
|
4512
4096
|
return { ...base, kind: "snapshot" };
|
|
4513
4097
|
}
|
|
4514
|
-
function deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding) {
|
|
4098
|
+
function deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding, updateMode) {
|
|
4515
4099
|
if (effect.value.op === "max") {
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4100
|
+
if (updateMode !== "stream") {
|
|
4101
|
+
throw new Error(
|
|
4102
|
+
`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.`
|
|
4103
|
+
);
|
|
4104
|
+
}
|
|
4105
|
+
if (!ownerMeta.primaryKey) {
|
|
4106
|
+
throw new Error(
|
|
4107
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a max counter maintenance effect on '${ownerName}', which has no primary key.`
|
|
4108
|
+
);
|
|
4109
|
+
}
|
|
4110
|
+
return {
|
|
4111
|
+
entity: { name: ownerName, modelClass: ownerClass },
|
|
4112
|
+
relationProperty: item.relationProperty,
|
|
4113
|
+
trigger: item.trigger,
|
|
4114
|
+
updateMode,
|
|
4115
|
+
kind: "counter",
|
|
4116
|
+
keyBinding,
|
|
4117
|
+
projection: {},
|
|
4118
|
+
counter: { attribute: effect.attribute, op: "max", field: effect.value.field }
|
|
4119
|
+
};
|
|
4519
4120
|
}
|
|
4520
4121
|
if (effect.delta === void 0) {
|
|
4521
4122
|
throw new Error(
|
|
@@ -4537,10 +4138,11 @@ function deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ow
|
|
|
4537
4138
|
entity: { name: ownerName, modelClass: ownerClass },
|
|
4538
4139
|
relationProperty: item.relationProperty,
|
|
4539
4140
|
trigger: item.trigger,
|
|
4141
|
+
updateMode,
|
|
4540
4142
|
kind: "counter",
|
|
4541
4143
|
keyBinding,
|
|
4542
4144
|
projection: {},
|
|
4543
|
-
counter: { attribute: effect.attribute, delta: effect.delta }
|
|
4145
|
+
counter: { attribute: effect.attribute, op: "count", delta: effect.delta }
|
|
4544
4146
|
};
|
|
4545
4147
|
}
|
|
4546
4148
|
var UNIQUE_GUARD_PK_PREFIX = "UNIQUE#";
|
|
@@ -4722,6 +4324,67 @@ function deriveIdempotencyGuard(fragment, effect) {
|
|
|
4722
4324
|
]
|
|
4723
4325
|
};
|
|
4724
4326
|
}
|
|
4327
|
+
var MAINT_OUTBOX_PK_PREFIX = "OUTBOX#MAINT#";
|
|
4328
|
+
var MAINT_OUTBOX_SK_PREFIX = "MAINT#";
|
|
4329
|
+
var MAINT_OUTBOX_OWNER_ATTR = "__maintOwner__";
|
|
4330
|
+
var MAINT_OUTBOX_RELATION_ATTR = "__maintRelation__";
|
|
4331
|
+
var MAINT_OUTBOX_TRIGGER_ATTR = "__maintTrigger__";
|
|
4332
|
+
var MAINT_OUTBOX_EVENT_VALUE = "maint";
|
|
4333
|
+
function deriveMaintainOutbox(fragment, write) {
|
|
4334
|
+
const keyFields = primaryKeyFields(fragment);
|
|
4335
|
+
const discriminator = `${write.entity.name}#${write.relationProperty}`;
|
|
4336
|
+
const keySegments = keyFields.map((f) => `#{${f}}`).join("");
|
|
4337
|
+
const pk = MAINT_OUTBOX_PK_PREFIX + discriminator + keySegments;
|
|
4338
|
+
const sk = MAINT_OUTBOX_SK_PREFIX + keyFields.map((f) => `{${f}}`).join("#");
|
|
4339
|
+
const item = {
|
|
4340
|
+
PK: pk,
|
|
4341
|
+
SK: sk,
|
|
4342
|
+
[OUTBOX_EVENT_NAME_ATTR]: MAINT_OUTBOX_EVENT_VALUE,
|
|
4343
|
+
[MAINT_OUTBOX_OWNER_ATTR]: write.entity.name,
|
|
4344
|
+
[MAINT_OUTBOX_RELATION_ATTR]: write.relationProperty,
|
|
4345
|
+
[MAINT_OUTBOX_TRIGGER_ATTR]: write.trigger
|
|
4346
|
+
};
|
|
4347
|
+
const sourceFields = /* @__PURE__ */ new Set();
|
|
4348
|
+
for (const inputField of Object.values(write.keyBinding)) sourceFields.add(inputField);
|
|
4349
|
+
for (const transform of Object.values(write.projection)) sourceFields.add(transform.inputField);
|
|
4350
|
+
if (write.collection?.orderBy !== void 0) sourceFields.add(write.collection.orderBy);
|
|
4351
|
+
if (write.counter !== void 0 && write.counter.op === "max") {
|
|
4352
|
+
sourceFields.add(write.counter.field);
|
|
4353
|
+
}
|
|
4354
|
+
if (write.membership !== void 0) sourceFields.add(write.membership.inputField);
|
|
4355
|
+
for (const field of sourceFields) {
|
|
4356
|
+
item[field] = `{${field}}`;
|
|
4357
|
+
}
|
|
4358
|
+
return {
|
|
4359
|
+
ownerEntity: write.entity.name,
|
|
4360
|
+
relationProperty: write.relationProperty,
|
|
4361
|
+
trigger: write.trigger,
|
|
4362
|
+
items: [
|
|
4363
|
+
{
|
|
4364
|
+
type: "Put",
|
|
4365
|
+
tableName: tableNameOf(fragment),
|
|
4366
|
+
entity: MARKER_ROW_ENTITY,
|
|
4367
|
+
item,
|
|
4368
|
+
// No `attribute_not_exists`: the row keys off the source entity key, which the
|
|
4369
|
+
// source `Put`'s own create guard already makes unique — the maintenance intent
|
|
4370
|
+
// is recorded atomically with the source row it derives from.
|
|
4371
|
+
literalKey: true
|
|
4372
|
+
}
|
|
4373
|
+
]
|
|
4374
|
+
};
|
|
4375
|
+
}
|
|
4376
|
+
function splitMaintainByMode(fragment, writes) {
|
|
4377
|
+
const sync = [];
|
|
4378
|
+
const outbox = [];
|
|
4379
|
+
for (const write of writes) {
|
|
4380
|
+
if (write.updateMode === "stream") {
|
|
4381
|
+
outbox.push(deriveMaintainOutbox(fragment, write));
|
|
4382
|
+
} else {
|
|
4383
|
+
sync.push(write);
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
return { sync, outbox };
|
|
4387
|
+
}
|
|
4725
4388
|
function resolveLifecycle(fragment) {
|
|
4726
4389
|
const writes = fragment.use ?? getEntityWrites(fragment.entity.modelClass);
|
|
4727
4390
|
if (writes === void 0) return void 0;
|
|
@@ -4818,7 +4481,10 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenan
|
|
|
4818
4481
|
const outboxEvents = lifecycle?.effects.emits !== void 0 && lifecycle.effects.emits.length > 0 ? deriveOutboxEvents(fragment, lifecycle.effects.emits) : void 0;
|
|
4819
4482
|
const idempotencyGuard = lifecycle?.effects.idempotency !== void 0 ? deriveIdempotencyGuard(fragment, lifecycle.effects.idempotency) : void 0;
|
|
4820
4483
|
const maintainers = resolveMaintainers(fragment, maintenanceGraph);
|
|
4821
|
-
const
|
|
4484
|
+
const allMaintainWrites = maintainers.length > 0 ? deriveMaintainItems(fragment, maintainers) : void 0;
|
|
4485
|
+
const { sync: syncMaintainWrites, outbox: maintainOutboxAll } = allMaintainWrites !== void 0 ? splitMaintainByMode(fragment, allMaintainWrites) : { sync: [], outbox: [] };
|
|
4486
|
+
const maintainWrites = syncMaintainWrites.length > 0 ? syncMaintainWrites : void 0;
|
|
4487
|
+
const maintainOutbox = maintainOutboxAll.length > 0 ? maintainOutboxAll : void 0;
|
|
4822
4488
|
return {
|
|
4823
4489
|
op,
|
|
4824
4490
|
keyFields,
|
|
@@ -4829,7 +4495,8 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenan
|
|
|
4829
4495
|
...uniqueGuards !== void 0 ? { uniqueGuards } : {},
|
|
4830
4496
|
...outboxEvents !== void 0 ? { outboxEvents } : {},
|
|
4831
4497
|
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
|
|
4832
|
-
...maintainWrites !== void 0 ? { maintainWrites } : {}
|
|
4498
|
+
...maintainWrites !== void 0 ? { maintainWrites } : {},
|
|
4499
|
+
...maintainOutbox !== void 0 ? { maintainOutbox } : {}
|
|
4833
4500
|
};
|
|
4834
4501
|
}
|
|
4835
4502
|
function compileSingleFragmentPlan(plan2) {
|
|
@@ -5356,7 +5023,7 @@ function runBuildPass(params, build, scalarFields, arrayParams, pass, accessed,
|
|
|
5356
5023
|
const recordWrite = (instr) => {
|
|
5357
5024
|
currentBody.push(instr);
|
|
5358
5025
|
};
|
|
5359
|
-
const
|
|
5026
|
+
const recorder2 = {
|
|
5360
5027
|
put(model, item, options) {
|
|
5361
5028
|
recordWrite({
|
|
5362
5029
|
kind: "write",
|
|
@@ -5449,7 +5116,7 @@ function runBuildPass(params, build, scalarFields, arrayParams, pass, accessed,
|
|
|
5449
5116
|
currentBody.push(block);
|
|
5450
5117
|
}
|
|
5451
5118
|
};
|
|
5452
|
-
build(
|
|
5119
|
+
build(recorder2, pProxy);
|
|
5453
5120
|
return instructions;
|
|
5454
5121
|
}
|
|
5455
5122
|
function defineTransaction(params, build) {
|
|
@@ -6897,6 +6564,11 @@ function buildMaintainWriteItems(maintainWrites) {
|
|
|
6897
6564
|
);
|
|
6898
6565
|
const keyCondition = { PK: pk };
|
|
6899
6566
|
if (sk !== void 0) keyCondition.SK = sk;
|
|
6567
|
+
if (maintain.kind === "membership") {
|
|
6568
|
+
throw new Error(
|
|
6569
|
+
`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.`
|
|
6570
|
+
);
|
|
6571
|
+
}
|
|
6900
6572
|
const projection = {};
|
|
6901
6573
|
for (const [attr, transform] of Object.entries(maintain.projection)) {
|
|
6902
6574
|
projection[attr] = {
|
|
@@ -6914,13 +6586,14 @@ function buildMaintainWriteItems(maintainWrites) {
|
|
|
6914
6586
|
collection: {
|
|
6915
6587
|
field: maintain.collection.field,
|
|
6916
6588
|
...maintain.collection.maxItems !== void 0 ? { maxItems: maintain.collection.maxItems } : {},
|
|
6917
|
-
...maintain.collection.orderBy !== void 0 ? { orderBy: maintain.collection.orderBy } : {}
|
|
6589
|
+
...maintain.collection.orderBy !== void 0 ? { orderBy: maintain.collection.orderBy } : {},
|
|
6590
|
+
...maintain.collection.orderBy !== void 0 && maintain.collection.orderDir !== void 0 ? { orderDir: maintain.collection.orderDir } : {}
|
|
6918
6591
|
}
|
|
6919
6592
|
} : {},
|
|
6920
6593
|
// #141: a counter carries its scalar `ADD` (attribute + literal numeric delta);
|
|
6921
6594
|
// the runtimes render `ADD #attr :delta` exactly as the self-lifecycle derived
|
|
6922
6595
|
// counter (#85) does, so a same-row counter merge stays consistent across paths.
|
|
6923
|
-
...maintain.kind === "counter" && maintain.counter !== void 0 ? {
|
|
6596
|
+
...maintain.kind === "counter" && maintain.counter !== void 0 && maintain.counter.op === "count" ? {
|
|
6924
6597
|
counter: {
|
|
6925
6598
|
attribute: maintain.counter.attribute,
|
|
6926
6599
|
delta: String(maintain.counter.delta)
|
|
@@ -6961,6 +6634,17 @@ function buildOutboxEventItems(outboxEvents, entityMetadata) {
|
|
|
6961
6634
|
}
|
|
6962
6635
|
return { items, params };
|
|
6963
6636
|
}
|
|
6637
|
+
function buildMaintainOutboxItems(maintainOutbox, entityMetadata) {
|
|
6638
|
+
const items = [];
|
|
6639
|
+
const params = {};
|
|
6640
|
+
for (const outbox of maintainOutbox) {
|
|
6641
|
+
for (const item of outbox.items) {
|
|
6642
|
+
items.push({ ...item, tableName: TableMapping.resolve(item.tableName) });
|
|
6643
|
+
collectTemplateParams(item.item, entityMetadata, params);
|
|
6644
|
+
}
|
|
6645
|
+
}
|
|
6646
|
+
return { items, params };
|
|
6647
|
+
}
|
|
6964
6648
|
function buildIdempotencyGuardItems(guard, entityMetadata) {
|
|
6965
6649
|
const items = [];
|
|
6966
6650
|
const params = {};
|
|
@@ -6970,7 +6654,7 @@ function buildIdempotencyGuardItems(guard, entityMetadata) {
|
|
|
6970
6654
|
}
|
|
6971
6655
|
return { items, params };
|
|
6972
6656
|
}
|
|
6973
|
-
function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard, maintainWrites) {
|
|
6657
|
+
function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard, maintainWrites, maintainOutbox) {
|
|
6974
6658
|
const label = opRefName(contractName, methodName);
|
|
6975
6659
|
const base = composeFragmentTransaction(contractName, methodName, ops);
|
|
6976
6660
|
const { items: edgeItemsRaw, params: edgeParams } = buildEdgeWriteItems(edgeWrites2);
|
|
@@ -6989,6 +6673,7 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
6989
6673
|
);
|
|
6990
6674
|
const { items: idempotencyItems, params: idempotencyParams } = idempotencyGuard !== void 0 ? buildIdempotencyGuardItems(idempotencyGuard, primaryMetadata) : { items: [], params: {} };
|
|
6991
6675
|
const { items: maintainItems, params: maintainParams } = buildMaintainWriteItems(maintainWrites);
|
|
6676
|
+
const { items: maintainOutboxItems, params: maintainOutboxParams } = buildMaintainOutboxItems(maintainOutbox, primaryMetadata);
|
|
6992
6677
|
const baseKeys = new Set(base.items.map(itemKeySignature));
|
|
6993
6678
|
for (const update of updateItems) {
|
|
6994
6679
|
if (baseKeys.has(itemKeySignature(update))) {
|
|
@@ -7012,7 +6697,8 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
7012
6697
|
guardParams,
|
|
7013
6698
|
outboxParams,
|
|
7014
6699
|
idempotencyParams,
|
|
7015
|
-
maintainParams
|
|
6700
|
+
maintainParams,
|
|
6701
|
+
maintainOutboxParams
|
|
7016
6702
|
]) {
|
|
7017
6703
|
for (const [name, spec] of Object.entries(source)) params[name] = spec;
|
|
7018
6704
|
}
|
|
@@ -7024,6 +6710,7 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
7024
6710
|
...outboxItems,
|
|
7025
6711
|
...idempotencyItems,
|
|
7026
6712
|
...maintainItems,
|
|
6713
|
+
...maintainOutboxItems,
|
|
7027
6714
|
...checkItems
|
|
7028
6715
|
];
|
|
7029
6716
|
if (items.length > MAX_TRANSACT_ITEMS) {
|
|
@@ -7130,7 +6817,8 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
|
|
|
7130
6817
|
const outboxEvents = method.outboxEvents ?? [];
|
|
7131
6818
|
const idempotencyGuard = method.idempotencyGuard;
|
|
7132
6819
|
const maintainWrites = method.maintainWrites ?? [];
|
|
7133
|
-
const
|
|
6820
|
+
const maintainOutbox = method.maintainOutbox ?? [];
|
|
6821
|
+
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;
|
|
7134
6822
|
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
7135
6823
|
const promotedToTransaction = isMultiFragment || hasDerivedEffects;
|
|
7136
6824
|
if (promotedToTransaction) {
|
|
@@ -7149,7 +6837,8 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
|
|
|
7149
6837
|
uniqueGuards,
|
|
7150
6838
|
outboxEvents,
|
|
7151
6839
|
idempotencyGuard,
|
|
7152
|
-
maintainWrites
|
|
6840
|
+
maintainWrites,
|
|
6841
|
+
maintainOutbox
|
|
7153
6842
|
)
|
|
7154
6843
|
) : (
|
|
7155
6844
|
// #90: writes only.
|
|
@@ -7315,17 +7004,6 @@ export {
|
|
|
7315
7004
|
BatchGetResult,
|
|
7316
7005
|
executeBatchGet,
|
|
7317
7006
|
executeBatchWrite,
|
|
7318
|
-
maintainTrigger,
|
|
7319
|
-
isMaintainTrigger,
|
|
7320
|
-
preview,
|
|
7321
|
-
identity,
|
|
7322
|
-
LIFECYCLE_CONTRACT_MARKER,
|
|
7323
|
-
isLifecycleContract,
|
|
7324
|
-
ENTITY_WRITES_MARKER,
|
|
7325
|
-
isEntityWritesDefinition,
|
|
7326
|
-
entityWrites,
|
|
7327
|
-
getEntityWrites,
|
|
7328
|
-
lifecyclePhaseForIntent,
|
|
7329
7007
|
OLD_VALUE_NAMESPACE,
|
|
7330
7008
|
EDGE_WRITES_MARKER,
|
|
7331
7009
|
isEdgeWritesDefinition,
|
|
@@ -7334,7 +7012,6 @@ export {
|
|
|
7334
7012
|
deriveEdgeWriteItems,
|
|
7335
7013
|
getEdgeWrites,
|
|
7336
7014
|
deriveModelEdgeWriteItems,
|
|
7337
|
-
buildMaintenanceGraph,
|
|
7338
7015
|
resolveLifecycle,
|
|
7339
7016
|
compileFragment,
|
|
7340
7017
|
compileSingleFragmentPlan,
|