graphddb 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/chunk-PNIZS37E.js +916 -0
- package/dist/{chunk-QEOFIXTN.js → chunk-VST3WOK3.js} +235 -556
- package/dist/{chunk-6AIAHP3A.js → chunk-YIXXTGZ6.js} +898 -1
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +477 -3
- package/dist/index.js +412 -17
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types-xJRn5qkv.d.ts → types-DuJ08bgc.d.ts} +675 -64
- package/package.json +1 -1
- package/dist/chunk-QQNP43JL.js +0 -461
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
MetadataRegistry,
|
|
7
7
|
NO_MIDDLEWARE,
|
|
8
8
|
TableMapping,
|
|
9
|
+
ViewRegistry,
|
|
9
10
|
attachHiddenKey,
|
|
10
11
|
attachModelClass,
|
|
11
12
|
buildConditionExpression,
|
|
12
13
|
buildDeleteInput,
|
|
14
|
+
buildMaintenanceGraph,
|
|
13
15
|
buildPutInput,
|
|
14
16
|
buildReadRuntime,
|
|
15
17
|
buildUpdateInput,
|
|
@@ -25,9 +27,13 @@ import {
|
|
|
25
27
|
executePut,
|
|
26
28
|
executeTransaction,
|
|
27
29
|
executeUpdate,
|
|
30
|
+
getEntityWrites,
|
|
28
31
|
isColumn,
|
|
32
|
+
isEntityWritesDefinition,
|
|
33
|
+
isLifecycleContract,
|
|
29
34
|
isParam,
|
|
30
35
|
isRawCondition,
|
|
36
|
+
lifecyclePhaseForIntent,
|
|
31
37
|
param,
|
|
32
38
|
pkTemplate,
|
|
33
39
|
resolveKey,
|
|
@@ -37,7 +43,7 @@ import {
|
|
|
37
43
|
serializeFieldValue,
|
|
38
44
|
serializeRawCondition,
|
|
39
45
|
skTemplate
|
|
40
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-YIXXTGZ6.js";
|
|
41
47
|
|
|
42
48
|
// src/spec/types.ts
|
|
43
49
|
var SPEC_VERSION = "1.0";
|
|
@@ -1756,137 +1762,6 @@ async function executeBatchWrite(requests) {
|
|
|
1756
1762
|
}
|
|
1757
1763
|
}
|
|
1758
1764
|
|
|
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
1765
|
// src/relation/edge-write.ts
|
|
1891
1766
|
var OLD_VALUE_NAMESPACE = "old";
|
|
1892
1767
|
var EDGE_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:edgeWrites");
|
|
@@ -1896,13 +1771,22 @@ function isEdgeWritesDefinition(value) {
|
|
|
1896
1771
|
function declaration(target, relationProperty) {
|
|
1897
1772
|
return { targetFactory: target, relationProperty };
|
|
1898
1773
|
}
|
|
1899
|
-
var
|
|
1774
|
+
var recorder = {
|
|
1900
1775
|
putEdge: declaration,
|
|
1901
1776
|
deleteEdge: declaration,
|
|
1902
|
-
updateKeyChangeEdge: declaration
|
|
1777
|
+
updateKeyChangeEdge: declaration,
|
|
1778
|
+
dualEdge: (forward, inverse) => ({
|
|
1779
|
+
targetFactory: forward.target,
|
|
1780
|
+
relationProperty: forward.relationProperty,
|
|
1781
|
+
inverse: {
|
|
1782
|
+
adjacencyFactory: inverse.adjacency,
|
|
1783
|
+
targetFactory: inverse.target,
|
|
1784
|
+
relationProperty: inverse.relationProperty
|
|
1785
|
+
}
|
|
1786
|
+
})
|
|
1903
1787
|
};
|
|
1904
1788
|
function edgeWrites(builder) {
|
|
1905
|
-
const edges = builder(
|
|
1789
|
+
const edges = builder(recorder);
|
|
1906
1790
|
if (edges.length === 0) {
|
|
1907
1791
|
throw new Error(
|
|
1908
1792
|
"edgeWrites(...) must declare at least one edge (w.putEdge / w.deleteEdge / w.updateKeyChangeEdge); an empty declaration writes nothing."
|
|
@@ -1944,6 +1828,13 @@ function assertRelationRoundTrips(entity, edgeFields, decl) {
|
|
|
1944
1828
|
}
|
|
1945
1829
|
return relation;
|
|
1946
1830
|
}
|
|
1831
|
+
function assertDualEdge(forwardAdjacency, inverseAdjacency, decl) {
|
|
1832
|
+
if (inverseAdjacency === forwardAdjacency) {
|
|
1833
|
+
throw new Error(
|
|
1834
|
+
`Dual-edge declaration for forward relation '${decl.relationProperty}' on adjacency entity '${forwardAdjacency.name}' points its inverse edge at the SAME adjacency model. A dual edge is TWO physical rows (forward + inverse); both directions resolving to one model would write the same key twice in a transaction (cf. #96). Declare the inverse on a DISTINCT adjacency model, or drop \`inverse\` for the single-row (base PK + inverse GSI) bidirectional edge.`
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1947
1838
|
function templateRecord(fields) {
|
|
1948
1839
|
const out = {};
|
|
1949
1840
|
for (const field of [...fields].sort()) {
|
|
@@ -1962,6 +1853,25 @@ function putItemTemplate(edgeFields) {
|
|
|
1962
1853
|
return templateRecord(edgeFields);
|
|
1963
1854
|
}
|
|
1964
1855
|
function deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle) {
|
|
1856
|
+
const forward = deriveOneEdgeRow(
|
|
1857
|
+
adjacencyClass,
|
|
1858
|
+
{ targetFactory: decl.targetFactory, relationProperty: decl.relationProperty },
|
|
1859
|
+
lifecycle
|
|
1860
|
+
);
|
|
1861
|
+
if (decl.inverse === void 0) return forward;
|
|
1862
|
+
const inverseAdjacency = decl.inverse.adjacencyFactory();
|
|
1863
|
+
assertDualEdge(adjacencyClass, inverseAdjacency, decl);
|
|
1864
|
+
const inverse = deriveOneEdgeRow(
|
|
1865
|
+
inverseAdjacency,
|
|
1866
|
+
{
|
|
1867
|
+
targetFactory: decl.inverse.targetFactory,
|
|
1868
|
+
relationProperty: decl.inverse.relationProperty
|
|
1869
|
+
},
|
|
1870
|
+
lifecycle
|
|
1871
|
+
);
|
|
1872
|
+
return [...forward, ...inverse];
|
|
1873
|
+
}
|
|
1874
|
+
function deriveOneEdgeRow(adjacencyClass, decl, lifecycle) {
|
|
1965
1875
|
const entity = MetadataRegistry.get(adjacencyClass);
|
|
1966
1876
|
if (!entity.primaryKey) {
|
|
1967
1877
|
throw new Error(
|
|
@@ -2029,387 +1939,6 @@ function deriveModelEdgeWriteItems(modelClass, lifecycle) {
|
|
|
2029
1939
|
return deriveEdgeWriteItems(modelClass, writes, lifecycle);
|
|
2030
1940
|
}
|
|
2031
1941
|
|
|
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
1942
|
// src/runtime/command-runtime.ts
|
|
2414
1943
|
function resolveCommandMethod(contract, methodName) {
|
|
2415
1944
|
const method = contract.methods[methodName];
|
|
@@ -2658,6 +2187,13 @@ function renderOutboxEventItems(event, key, params, contextLabel) {
|
|
|
2658
2187
|
return { Put: put };
|
|
2659
2188
|
});
|
|
2660
2189
|
}
|
|
2190
|
+
function renderMaintainOutboxItems(outbox, key, params, contextLabel) {
|
|
2191
|
+
return outbox.items.map((item) => {
|
|
2192
|
+
const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} maintain-outbox Put`);
|
|
2193
|
+
const put = { TableName: TableMapping.resolve(item.tableName), Item };
|
|
2194
|
+
return { Put: put };
|
|
2195
|
+
});
|
|
2196
|
+
}
|
|
2661
2197
|
function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
|
|
2662
2198
|
return guard.items.map((item) => {
|
|
2663
2199
|
const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} idempotency Put`);
|
|
@@ -2727,6 +2263,11 @@ function renderMaintainWriteItem(maintain, key, params, contextLabel, modelBySig
|
|
|
2727
2263
|
`Contract runtime: ${contextLabel} is a 'counter' maintenance write with no \`counter\` payload \u2014 the compiler (#141) should have set it.`
|
|
2728
2264
|
);
|
|
2729
2265
|
}
|
|
2266
|
+
if (counter.op !== "count") {
|
|
2267
|
+
throw new Error(
|
|
2268
|
+
`Contract runtime: ${contextLabel} is a 'counter' maintenance write with op '${counter.op}', which is the asynchronous stream path (#130) and must not be composed into a synchronous TransactWriteItems \u2014 the compiler should divert it to a maintenance-outbox row.`
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2730
2271
|
updateInput = {
|
|
2731
2272
|
TableName,
|
|
2732
2273
|
Key,
|
|
@@ -2738,6 +2279,11 @@ function renderMaintainWriteItem(maintain, key, params, contextLabel, modelBySig
|
|
|
2738
2279
|
modelBySignature?.set(execItemKeySignature(out2), ownerClass.name);
|
|
2739
2280
|
return out2;
|
|
2740
2281
|
}
|
|
2282
|
+
if (maintain.kind === "membership") {
|
|
2283
|
+
throw new Error(
|
|
2284
|
+
`Contract runtime: ${contextLabel} is a 'membership' (sparse-view) maintenance write, which is stream-only (#133) and must not be composed into a synchronous TransactWriteItems \u2014 the compiler should divert it to a maintenance-outbox row.`
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2741
2287
|
const projection = buildMaintainProjection(maintain, key, params, contextLabel);
|
|
2742
2288
|
if (maintain.kind === "collection") {
|
|
2743
2289
|
const field = maintain.collection?.field;
|
|
@@ -2783,7 +2329,7 @@ function renderMaintainWriteItem(maintain, key, params, contextLabel, modelBySig
|
|
|
2783
2329
|
modelBySignature?.set(execItemKeySignature(out), ownerClass.name);
|
|
2784
2330
|
return out;
|
|
2785
2331
|
}
|
|
2786
|
-
async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, maintainItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map(), retry, middleware) {
|
|
2332
|
+
async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, maintainItems, maintainOutboxItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map(), retry, middleware) {
|
|
2787
2333
|
const writeItems = writes.map((w) => {
|
|
2788
2334
|
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
2789
2335
|
let item;
|
|
@@ -2807,6 +2353,7 @@ async function executeReferentialTransaction(writes, edgeItems, updateItems, gua
|
|
|
2807
2353
|
...outboxItems,
|
|
2808
2354
|
...idempotencyItems,
|
|
2809
2355
|
...maintainItems,
|
|
2356
|
+
...maintainOutboxItems,
|
|
2810
2357
|
...checkItems
|
|
2811
2358
|
],
|
|
2812
2359
|
{
|
|
@@ -2874,7 +2421,7 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2874
2421
|
`Contract runtime: command method '${methodName}' is a multi-fragment mutation (it composes ${method.ops.length} fragments into one atomic TransactWriteItems) and accepts a single key only. Call it once per target, not with a key array.`
|
|
2875
2422
|
);
|
|
2876
2423
|
}
|
|
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;
|
|
2424
|
+
const hasDerivedEffects2 = method.conditionChecks !== void 0 && method.conditionChecks.length > 0 || method.edgeWrites !== void 0 && method.edgeWrites.length > 0 || method.derivedUpdates !== void 0 && method.derivedUpdates.length > 0 || method.uniqueGuards !== void 0 && method.uniqueGuards.length > 0 || method.outboxEvents !== void 0 && method.outboxEvents.length > 0 || method.maintainWrites !== void 0 && method.maintainWrites.length > 0 || method.maintainOutbox !== void 0 && method.maintainOutbox.length > 0 || method.idempotencyGuard !== void 0;
|
|
2878
2425
|
if (hasDerivedEffects2) {
|
|
2879
2426
|
throw new Error(
|
|
2880
2427
|
`Contract runtime: command method '${methodName}' carries derived effects (referential integrity / edge writes / derived counters / uniqueness guards / outbox events / idempotency guard / maintenance writes \u2014 it composes its write + those items into one atomic TransactWriteItems) and accepts a single key only. Call it once per target, not with a key array.`
|
|
@@ -2897,7 +2444,8 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2897
2444
|
const outboxEvents = method.outboxEvents ?? [];
|
|
2898
2445
|
const idempotencyGuard = method.idempotencyGuard;
|
|
2899
2446
|
const maintainWrites = method.maintainWrites ?? [];
|
|
2900
|
-
const
|
|
2447
|
+
const maintainOutbox = method.maintainOutbox ?? [];
|
|
2448
|
+
const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || maintainWrites.length > 0 || maintainOutbox.length > 0 || idempotencyGuard !== void 0;
|
|
2901
2449
|
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
2902
2450
|
if (isMultiFragment || hasDerivedEffects) {
|
|
2903
2451
|
const ops = method.ops ?? [method.op];
|
|
@@ -2945,6 +2493,9 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2945
2493
|
modelBySignature
|
|
2946
2494
|
)
|
|
2947
2495
|
);
|
|
2496
|
+
const maintainOutboxItems = maintainOutbox.flatMap(
|
|
2497
|
+
(mo, i) => renderMaintainOutboxItems(mo, key, params, `command method '${methodName}' (maintain-outbox #${i})`)
|
|
2498
|
+
);
|
|
2948
2499
|
const checks = conditionChecks.map((check, i) => ({
|
|
2949
2500
|
check: renderConditionCheck(
|
|
2950
2501
|
check,
|
|
@@ -2961,6 +2512,7 @@ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}
|
|
|
2961
2512
|
outboxItems,
|
|
2962
2513
|
idempotencyItems,
|
|
2963
2514
|
maintainItems,
|
|
2515
|
+
maintainOutboxItems,
|
|
2964
2516
|
checks,
|
|
2965
2517
|
methodName,
|
|
2966
2518
|
modelBySignature
|
|
@@ -2990,6 +2542,7 @@ function renderCompiledOp(op, ctx) {
|
|
|
2990
2542
|
const outboxItems = [];
|
|
2991
2543
|
const idempotencyItems = [];
|
|
2992
2544
|
const maintainItems = [];
|
|
2545
|
+
const maintainOutboxItems = [];
|
|
2993
2546
|
const checkItems = [];
|
|
2994
2547
|
for (const edge of fragment.edgeWrites ?? []) {
|
|
2995
2548
|
edgeItems.push(...renderEdgeWriteItems(edge, op.params, op.params, `${ctx} edge`, modelBySignature));
|
|
@@ -3013,10 +2566,15 @@ function renderCompiledOp(op, ctx) {
|
|
|
3013
2566
|
renderMaintainWriteItem(m, op.params, op.params, `${ctx} maintain`, modelBySignature)
|
|
3014
2567
|
);
|
|
3015
2568
|
}
|
|
2569
|
+
for (const mo of fragment.maintainOutbox ?? []) {
|
|
2570
|
+
maintainOutboxItems.push(
|
|
2571
|
+
...renderMaintainOutboxItems(mo, op.params, op.params, `${ctx} maintain-outbox`)
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
3016
2574
|
for (const c of fragment.conditionChecks ?? []) {
|
|
3017
2575
|
checkItems.push({ check: renderConditionCheck(c, op.params, op.params, `${ctx} requires`) });
|
|
3018
2576
|
}
|
|
3019
|
-
const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || maintainItems.length > 0 || checkItems.length > 0;
|
|
2577
|
+
const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || maintainItems.length > 0 || maintainOutboxItems.length > 0 || checkItems.length > 0;
|
|
3020
2578
|
return {
|
|
3021
2579
|
write,
|
|
3022
2580
|
edgeItems,
|
|
@@ -3025,6 +2583,7 @@ function renderCompiledOp(op, ctx) {
|
|
|
3025
2583
|
outboxItems,
|
|
3026
2584
|
idempotencyItems,
|
|
3027
2585
|
maintainItems,
|
|
2586
|
+
maintainOutboxItems,
|
|
3028
2587
|
checkItems,
|
|
3029
2588
|
modelBySignature,
|
|
3030
2589
|
hasDerivedEffects
|
|
@@ -3070,6 +2629,7 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
3070
2629
|
const outboxItems = [];
|
|
3071
2630
|
const idempotencyItems = [];
|
|
3072
2631
|
const maintainItems = [];
|
|
2632
|
+
const maintainOutboxItems = [];
|
|
3073
2633
|
const checkItems = [];
|
|
3074
2634
|
for (const r of renderedOps) {
|
|
3075
2635
|
for (const [sig, name] of r.modelBySignature) modelBySignature.set(sig, name);
|
|
@@ -3079,6 +2639,7 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
3079
2639
|
outboxItems.push(...r.outboxItems);
|
|
3080
2640
|
idempotencyItems.push(...r.idempotencyItems);
|
|
3081
2641
|
maintainItems.push(...r.maintainItems);
|
|
2642
|
+
maintainOutboxItems.push(...r.maintainOutboxItems);
|
|
3082
2643
|
checkItems.push(...r.checkItems);
|
|
3083
2644
|
}
|
|
3084
2645
|
const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
|
|
@@ -3098,6 +2659,7 @@ async function executeCompiledWriteOp(ops, options) {
|
|
|
3098
2659
|
outboxItems,
|
|
3099
2660
|
idempotencyItems,
|
|
3100
2661
|
maintainItems,
|
|
2662
|
+
maintainOutboxItems,
|
|
3101
2663
|
checkItems,
|
|
3102
2664
|
label,
|
|
3103
2665
|
modelBySignature,
|
|
@@ -3196,6 +2758,7 @@ function rewrittenCompiledOp(write) {
|
|
|
3196
2758
|
outboxItems: [],
|
|
3197
2759
|
idempotencyItems: [],
|
|
3198
2760
|
maintainItems: [],
|
|
2761
|
+
maintainOutboxItems: [],
|
|
3199
2762
|
checkItems: [],
|
|
3200
2763
|
modelBySignature: /* @__PURE__ */ new Map(),
|
|
3201
2764
|
hasDerivedEffects: false
|
|
@@ -4080,6 +3643,7 @@ function buildPlannedCommandMethod(name, planned) {
|
|
|
4080
3643
|
const idempotencyGuard = idempotencyGuards[0];
|
|
4081
3644
|
const maintainWrites = plan2.fragments.flatMap((f) => f.maintainWrites ?? []);
|
|
4082
3645
|
assertNoCrossFragmentMaintainCollision(name, maintainWrites);
|
|
3646
|
+
const maintainOutbox = plan2.fragments.flatMap((f) => f.maintainOutbox ?? []);
|
|
4083
3647
|
return {
|
|
4084
3648
|
__methodKind: "command",
|
|
4085
3649
|
op,
|
|
@@ -4091,6 +3655,7 @@ function buildPlannedCommandMethod(name, planned) {
|
|
|
4091
3655
|
...outboxEvents.length > 0 ? { outboxEvents } : {},
|
|
4092
3656
|
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
|
|
4093
3657
|
...maintainWrites.length > 0 ? { maintainWrites } : {},
|
|
3658
|
+
...maintainOutbox.length > 0 ? { maintainOutbox } : {},
|
|
4094
3659
|
inputArity: "either",
|
|
4095
3660
|
// A return projection means the method returns the read-back entity; otherwise
|
|
4096
3661
|
// it is a fire-and-forget write (`void`).
|
|
@@ -4387,8 +3952,9 @@ var MAINTAIN_ENTITY_PATH_RE = /^\$\.entity\.([A-Za-z_$][\w$]*)$/;
|
|
|
4387
3952
|
var cachedMaintenanceGraph;
|
|
4388
3953
|
function globalMaintenanceGraph() {
|
|
4389
3954
|
const generation = MetadataRegistry.generation;
|
|
4390
|
-
|
|
4391
|
-
|
|
3955
|
+
const viewGeneration = ViewRegistry.generation;
|
|
3956
|
+
if (cachedMaintenanceGraph?.generation !== generation || cachedMaintenanceGraph?.viewGeneration !== viewGeneration) {
|
|
3957
|
+
cachedMaintenanceGraph = { graph: buildMaintenanceGraph(), generation, viewGeneration };
|
|
4392
3958
|
}
|
|
4393
3959
|
return cachedMaintenanceGraph.graph;
|
|
4394
3960
|
}
|
|
@@ -4453,11 +4019,7 @@ function assertNoCrossFragmentMaintainCollision(methodName, writes) {
|
|
|
4453
4019
|
}
|
|
4454
4020
|
function deriveOneMaintainWrite(fragment, item) {
|
|
4455
4021
|
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
|
-
}
|
|
4022
|
+
const updateMode = effect.updateMode === "stream" ? "stream" : "mutation";
|
|
4461
4023
|
const ownerClass = effect.targetFactory();
|
|
4462
4024
|
const ownerMeta = MetadataRegistry.get(
|
|
4463
4025
|
ownerClass
|
|
@@ -4473,7 +4035,7 @@ function deriveOneMaintainWrite(fragment, item) {
|
|
|
4473
4035
|
);
|
|
4474
4036
|
}
|
|
4475
4037
|
if (effect.kind === "counter") {
|
|
4476
|
-
return deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding);
|
|
4038
|
+
return deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding, updateMode);
|
|
4477
4039
|
}
|
|
4478
4040
|
const projection = {};
|
|
4479
4041
|
for (const [attr, transform] of Object.entries(effect.project)) {
|
|
@@ -4494,9 +4056,32 @@ function deriveOneMaintainWrite(fragment, item) {
|
|
|
4494
4056
|
entity: { name: ownerName, modelClass: ownerClass },
|
|
4495
4057
|
relationProperty: item.relationProperty,
|
|
4496
4058
|
trigger: item.trigger,
|
|
4059
|
+
updateMode,
|
|
4497
4060
|
keyBinding,
|
|
4498
4061
|
projection
|
|
4499
4062
|
};
|
|
4063
|
+
if (effect.kind === "membership") {
|
|
4064
|
+
if (updateMode !== "stream") {
|
|
4065
|
+
throw new Error(
|
|
4066
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a sparse-view membership effect (view '${item.ownerEntity}', relation '${item.relationProperty}') on the SYNCHRONOUS \`mutation\` path. A membership row is PUT when its predicate holds and DELETED when it flips false; a single atomic transaction cannot branch an item between Put and Delete on a runtime predicate. Declare \`write: { updateMode: 'stream' }\` so the host-side drain applies the put/delete (#133 sparse view is the asynchronous stream path).`
|
|
4067
|
+
);
|
|
4068
|
+
}
|
|
4069
|
+
const predInput = maintainPathToInputField(
|
|
4070
|
+
fragment,
|
|
4071
|
+
item,
|
|
4072
|
+
"membership predicate",
|
|
4073
|
+
effect.predicate.path
|
|
4074
|
+
);
|
|
4075
|
+
return {
|
|
4076
|
+
...base,
|
|
4077
|
+
kind: "membership",
|
|
4078
|
+
membership: {
|
|
4079
|
+
op: effect.predicate.op,
|
|
4080
|
+
inputField: predInput,
|
|
4081
|
+
...effect.predicate.value !== void 0 ? { value: effect.predicate.value } : {}
|
|
4082
|
+
}
|
|
4083
|
+
};
|
|
4084
|
+
}
|
|
4500
4085
|
if (effect.kind === "collection") {
|
|
4501
4086
|
const orderBy = effect.collection.orderBy !== void 0 ? maintainPathToInputField(fragment, item, "collection `orderBy`", effect.collection.orderBy) : void 0;
|
|
4502
4087
|
return {
|
|
@@ -4505,17 +4090,35 @@ function deriveOneMaintainWrite(fragment, item) {
|
|
|
4505
4090
|
collection: {
|
|
4506
4091
|
field: effect.collection.field,
|
|
4507
4092
|
...effect.collection.maxItems !== void 0 ? { maxItems: effect.collection.maxItems } : {},
|
|
4508
|
-
...orderBy !== void 0 ? { orderBy } : {}
|
|
4093
|
+
...orderBy !== void 0 ? { orderBy } : {},
|
|
4094
|
+
...orderBy !== void 0 && effect.collection.orderDir !== void 0 ? { orderDir: effect.collection.orderDir } : {}
|
|
4509
4095
|
}
|
|
4510
4096
|
};
|
|
4511
4097
|
}
|
|
4512
4098
|
return { ...base, kind: "snapshot" };
|
|
4513
4099
|
}
|
|
4514
|
-
function deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding) {
|
|
4100
|
+
function deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ownerName, keyBinding, updateMode) {
|
|
4515
4101
|
if (effect.value.op === "max") {
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4102
|
+
if (updateMode !== "stream") {
|
|
4103
|
+
throw new Error(
|
|
4104
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a counter maintenance effect (\`@aggregate\` field '${item.relationProperty}' on '${item.ownerEntity}') declared \`value: max('${effect.value.field}')\` on the SYNCHRONOUS \`mutation\` path. A \`max\` needs a conditional \`SET\` whose failed guard would roll back the SOURCE write that legitimately happened, so a running max is the asynchronous stream path (#130). Use \`value: count()\` for a synchronous counter, or declare \`updateMode: 'stream'\` on the \`@aggregate\` to realize the running max.`
|
|
4105
|
+
);
|
|
4106
|
+
}
|
|
4107
|
+
if (!ownerMeta.primaryKey) {
|
|
4108
|
+
throw new Error(
|
|
4109
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' fires a max counter maintenance effect on '${ownerName}', which has no primary key.`
|
|
4110
|
+
);
|
|
4111
|
+
}
|
|
4112
|
+
return {
|
|
4113
|
+
entity: { name: ownerName, modelClass: ownerClass },
|
|
4114
|
+
relationProperty: item.relationProperty,
|
|
4115
|
+
trigger: item.trigger,
|
|
4116
|
+
updateMode,
|
|
4117
|
+
kind: "counter",
|
|
4118
|
+
keyBinding,
|
|
4119
|
+
projection: {},
|
|
4120
|
+
counter: { attribute: effect.attribute, op: "max", field: effect.value.field }
|
|
4121
|
+
};
|
|
4519
4122
|
}
|
|
4520
4123
|
if (effect.delta === void 0) {
|
|
4521
4124
|
throw new Error(
|
|
@@ -4537,10 +4140,11 @@ function deriveOneCounterWrite(fragment, item, effect, ownerClass, ownerMeta, ow
|
|
|
4537
4140
|
entity: { name: ownerName, modelClass: ownerClass },
|
|
4538
4141
|
relationProperty: item.relationProperty,
|
|
4539
4142
|
trigger: item.trigger,
|
|
4143
|
+
updateMode,
|
|
4540
4144
|
kind: "counter",
|
|
4541
4145
|
keyBinding,
|
|
4542
4146
|
projection: {},
|
|
4543
|
-
counter: { attribute: effect.attribute, delta: effect.delta }
|
|
4147
|
+
counter: { attribute: effect.attribute, op: "count", delta: effect.delta }
|
|
4544
4148
|
};
|
|
4545
4149
|
}
|
|
4546
4150
|
var UNIQUE_GUARD_PK_PREFIX = "UNIQUE#";
|
|
@@ -4722,6 +4326,67 @@ function deriveIdempotencyGuard(fragment, effect) {
|
|
|
4722
4326
|
]
|
|
4723
4327
|
};
|
|
4724
4328
|
}
|
|
4329
|
+
var MAINT_OUTBOX_PK_PREFIX = "OUTBOX#MAINT#";
|
|
4330
|
+
var MAINT_OUTBOX_SK_PREFIX = "MAINT#";
|
|
4331
|
+
var MAINT_OUTBOX_OWNER_ATTR = "__maintOwner__";
|
|
4332
|
+
var MAINT_OUTBOX_RELATION_ATTR = "__maintRelation__";
|
|
4333
|
+
var MAINT_OUTBOX_TRIGGER_ATTR = "__maintTrigger__";
|
|
4334
|
+
var MAINT_OUTBOX_EVENT_VALUE = "maint";
|
|
4335
|
+
function deriveMaintainOutbox(fragment, write) {
|
|
4336
|
+
const keyFields = primaryKeyFields(fragment);
|
|
4337
|
+
const discriminator = `${write.entity.name}#${write.relationProperty}`;
|
|
4338
|
+
const keySegments = keyFields.map((f) => `#{${f}}`).join("");
|
|
4339
|
+
const pk = MAINT_OUTBOX_PK_PREFIX + discriminator + keySegments;
|
|
4340
|
+
const sk = MAINT_OUTBOX_SK_PREFIX + keyFields.map((f) => `{${f}}`).join("#");
|
|
4341
|
+
const item = {
|
|
4342
|
+
PK: pk,
|
|
4343
|
+
SK: sk,
|
|
4344
|
+
[OUTBOX_EVENT_NAME_ATTR]: MAINT_OUTBOX_EVENT_VALUE,
|
|
4345
|
+
[MAINT_OUTBOX_OWNER_ATTR]: write.entity.name,
|
|
4346
|
+
[MAINT_OUTBOX_RELATION_ATTR]: write.relationProperty,
|
|
4347
|
+
[MAINT_OUTBOX_TRIGGER_ATTR]: write.trigger
|
|
4348
|
+
};
|
|
4349
|
+
const sourceFields = /* @__PURE__ */ new Set();
|
|
4350
|
+
for (const inputField of Object.values(write.keyBinding)) sourceFields.add(inputField);
|
|
4351
|
+
for (const transform of Object.values(write.projection)) sourceFields.add(transform.inputField);
|
|
4352
|
+
if (write.collection?.orderBy !== void 0) sourceFields.add(write.collection.orderBy);
|
|
4353
|
+
if (write.counter !== void 0 && write.counter.op === "max") {
|
|
4354
|
+
sourceFields.add(write.counter.field);
|
|
4355
|
+
}
|
|
4356
|
+
if (write.membership !== void 0) sourceFields.add(write.membership.inputField);
|
|
4357
|
+
for (const field of sourceFields) {
|
|
4358
|
+
item[field] = `{${field}}`;
|
|
4359
|
+
}
|
|
4360
|
+
return {
|
|
4361
|
+
ownerEntity: write.entity.name,
|
|
4362
|
+
relationProperty: write.relationProperty,
|
|
4363
|
+
trigger: write.trigger,
|
|
4364
|
+
items: [
|
|
4365
|
+
{
|
|
4366
|
+
type: "Put",
|
|
4367
|
+
tableName: tableNameOf(fragment),
|
|
4368
|
+
entity: MARKER_ROW_ENTITY,
|
|
4369
|
+
item,
|
|
4370
|
+
// No `attribute_not_exists`: the row keys off the source entity key, which the
|
|
4371
|
+
// source `Put`'s own create guard already makes unique — the maintenance intent
|
|
4372
|
+
// is recorded atomically with the source row it derives from.
|
|
4373
|
+
literalKey: true
|
|
4374
|
+
}
|
|
4375
|
+
]
|
|
4376
|
+
};
|
|
4377
|
+
}
|
|
4378
|
+
function splitMaintainByMode(fragment, writes) {
|
|
4379
|
+
const sync = [];
|
|
4380
|
+
const outbox = [];
|
|
4381
|
+
for (const write of writes) {
|
|
4382
|
+
if (write.updateMode === "stream") {
|
|
4383
|
+
outbox.push(deriveMaintainOutbox(fragment, write));
|
|
4384
|
+
} else {
|
|
4385
|
+
sync.push(write);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
return { sync, outbox };
|
|
4389
|
+
}
|
|
4725
4390
|
function resolveLifecycle(fragment) {
|
|
4726
4391
|
const writes = fragment.use ?? getEntityWrites(fragment.entity.modelClass);
|
|
4727
4392
|
if (writes === void 0) return void 0;
|
|
@@ -4818,7 +4483,10 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenan
|
|
|
4818
4483
|
const outboxEvents = lifecycle?.effects.emits !== void 0 && lifecycle.effects.emits.length > 0 ? deriveOutboxEvents(fragment, lifecycle.effects.emits) : void 0;
|
|
4819
4484
|
const idempotencyGuard = lifecycle?.effects.idempotency !== void 0 ? deriveIdempotencyGuard(fragment, lifecycle.effects.idempotency) : void 0;
|
|
4820
4485
|
const maintainers = resolveMaintainers(fragment, maintenanceGraph);
|
|
4821
|
-
const
|
|
4486
|
+
const allMaintainWrites = maintainers.length > 0 ? deriveMaintainItems(fragment, maintainers) : void 0;
|
|
4487
|
+
const { sync: syncMaintainWrites, outbox: maintainOutboxAll } = allMaintainWrites !== void 0 ? splitMaintainByMode(fragment, allMaintainWrites) : { sync: [], outbox: [] };
|
|
4488
|
+
const maintainWrites = syncMaintainWrites.length > 0 ? syncMaintainWrites : void 0;
|
|
4489
|
+
const maintainOutbox = maintainOutboxAll.length > 0 ? maintainOutboxAll : void 0;
|
|
4822
4490
|
return {
|
|
4823
4491
|
op,
|
|
4824
4492
|
keyFields,
|
|
@@ -4829,7 +4497,8 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenan
|
|
|
4829
4497
|
...uniqueGuards !== void 0 ? { uniqueGuards } : {},
|
|
4830
4498
|
...outboxEvents !== void 0 ? { outboxEvents } : {},
|
|
4831
4499
|
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
|
|
4832
|
-
...maintainWrites !== void 0 ? { maintainWrites } : {}
|
|
4500
|
+
...maintainWrites !== void 0 ? { maintainWrites } : {},
|
|
4501
|
+
...maintainOutbox !== void 0 ? { maintainOutbox } : {}
|
|
4833
4502
|
};
|
|
4834
4503
|
}
|
|
4835
4504
|
function compileSingleFragmentPlan(plan2) {
|
|
@@ -5356,7 +5025,7 @@ function runBuildPass(params, build, scalarFields, arrayParams, pass, accessed,
|
|
|
5356
5025
|
const recordWrite = (instr) => {
|
|
5357
5026
|
currentBody.push(instr);
|
|
5358
5027
|
};
|
|
5359
|
-
const
|
|
5028
|
+
const recorder2 = {
|
|
5360
5029
|
put(model, item, options) {
|
|
5361
5030
|
recordWrite({
|
|
5362
5031
|
kind: "write",
|
|
@@ -5449,7 +5118,7 @@ function runBuildPass(params, build, scalarFields, arrayParams, pass, accessed,
|
|
|
5449
5118
|
currentBody.push(block);
|
|
5450
5119
|
}
|
|
5451
5120
|
};
|
|
5452
|
-
build(
|
|
5121
|
+
build(recorder2, pProxy);
|
|
5453
5122
|
return instructions;
|
|
5454
5123
|
}
|
|
5455
5124
|
function defineTransaction(params, build) {
|
|
@@ -6897,6 +6566,11 @@ function buildMaintainWriteItems(maintainWrites) {
|
|
|
6897
6566
|
);
|
|
6898
6567
|
const keyCondition = { PK: pk };
|
|
6899
6568
|
if (sk !== void 0) keyCondition.SK = sk;
|
|
6569
|
+
if (maintain.kind === "membership") {
|
|
6570
|
+
throw new Error(
|
|
6571
|
+
`maintenance write: a sparse-view membership maintainer ('${maintain.relationProperty}' on '${maintain.entity.name}') reached the synchronous transaction spec builder. Membership is stream-only (#133) \u2014 it should have been diverted to a maintenance-outbox row. This is a compiler defect.`
|
|
6572
|
+
);
|
|
6573
|
+
}
|
|
6900
6574
|
const projection = {};
|
|
6901
6575
|
for (const [attr, transform] of Object.entries(maintain.projection)) {
|
|
6902
6576
|
projection[attr] = {
|
|
@@ -6914,13 +6588,14 @@ function buildMaintainWriteItems(maintainWrites) {
|
|
|
6914
6588
|
collection: {
|
|
6915
6589
|
field: maintain.collection.field,
|
|
6916
6590
|
...maintain.collection.maxItems !== void 0 ? { maxItems: maintain.collection.maxItems } : {},
|
|
6917
|
-
...maintain.collection.orderBy !== void 0 ? { orderBy: maintain.collection.orderBy } : {}
|
|
6591
|
+
...maintain.collection.orderBy !== void 0 ? { orderBy: maintain.collection.orderBy } : {},
|
|
6592
|
+
...maintain.collection.orderBy !== void 0 && maintain.collection.orderDir !== void 0 ? { orderDir: maintain.collection.orderDir } : {}
|
|
6918
6593
|
}
|
|
6919
6594
|
} : {},
|
|
6920
6595
|
// #141: a counter carries its scalar `ADD` (attribute + literal numeric delta);
|
|
6921
6596
|
// the runtimes render `ADD #attr :delta` exactly as the self-lifecycle derived
|
|
6922
6597
|
// counter (#85) does, so a same-row counter merge stays consistent across paths.
|
|
6923
|
-
...maintain.kind === "counter" && maintain.counter !== void 0 ? {
|
|
6598
|
+
...maintain.kind === "counter" && maintain.counter !== void 0 && maintain.counter.op === "count" ? {
|
|
6924
6599
|
counter: {
|
|
6925
6600
|
attribute: maintain.counter.attribute,
|
|
6926
6601
|
delta: String(maintain.counter.delta)
|
|
@@ -6961,6 +6636,17 @@ function buildOutboxEventItems(outboxEvents, entityMetadata) {
|
|
|
6961
6636
|
}
|
|
6962
6637
|
return { items, params };
|
|
6963
6638
|
}
|
|
6639
|
+
function buildMaintainOutboxItems(maintainOutbox, entityMetadata) {
|
|
6640
|
+
const items = [];
|
|
6641
|
+
const params = {};
|
|
6642
|
+
for (const outbox of maintainOutbox) {
|
|
6643
|
+
for (const item of outbox.items) {
|
|
6644
|
+
items.push({ ...item, tableName: TableMapping.resolve(item.tableName) });
|
|
6645
|
+
collectTemplateParams(item.item, entityMetadata, params);
|
|
6646
|
+
}
|
|
6647
|
+
}
|
|
6648
|
+
return { items, params };
|
|
6649
|
+
}
|
|
6964
6650
|
function buildIdempotencyGuardItems(guard, entityMetadata) {
|
|
6965
6651
|
const items = [];
|
|
6966
6652
|
const params = {};
|
|
@@ -6970,7 +6656,7 @@ function buildIdempotencyGuardItems(guard, entityMetadata) {
|
|
|
6970
6656
|
}
|
|
6971
6657
|
return { items, params };
|
|
6972
6658
|
}
|
|
6973
|
-
function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard, maintainWrites) {
|
|
6659
|
+
function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard, maintainWrites, maintainOutbox) {
|
|
6974
6660
|
const label = opRefName(contractName, methodName);
|
|
6975
6661
|
const base = composeFragmentTransaction(contractName, methodName, ops);
|
|
6976
6662
|
const { items: edgeItemsRaw, params: edgeParams } = buildEdgeWriteItems(edgeWrites2);
|
|
@@ -6989,6 +6675,7 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
6989
6675
|
);
|
|
6990
6676
|
const { items: idempotencyItems, params: idempotencyParams } = idempotencyGuard !== void 0 ? buildIdempotencyGuardItems(idempotencyGuard, primaryMetadata) : { items: [], params: {} };
|
|
6991
6677
|
const { items: maintainItems, params: maintainParams } = buildMaintainWriteItems(maintainWrites);
|
|
6678
|
+
const { items: maintainOutboxItems, params: maintainOutboxParams } = buildMaintainOutboxItems(maintainOutbox, primaryMetadata);
|
|
6992
6679
|
const baseKeys = new Set(base.items.map(itemKeySignature));
|
|
6993
6680
|
for (const update of updateItems) {
|
|
6994
6681
|
if (baseKeys.has(itemKeySignature(update))) {
|
|
@@ -7012,7 +6699,8 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
7012
6699
|
guardParams,
|
|
7013
6700
|
outboxParams,
|
|
7014
6701
|
idempotencyParams,
|
|
7015
|
-
maintainParams
|
|
6702
|
+
maintainParams,
|
|
6703
|
+
maintainOutboxParams
|
|
7016
6704
|
]) {
|
|
7017
6705
|
for (const [name, spec] of Object.entries(source)) params[name] = spec;
|
|
7018
6706
|
}
|
|
@@ -7024,6 +6712,7 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
7024
6712
|
...outboxItems,
|
|
7025
6713
|
...idempotencyItems,
|
|
7026
6714
|
...maintainItems,
|
|
6715
|
+
...maintainOutboxItems,
|
|
7027
6716
|
...checkItems
|
|
7028
6717
|
];
|
|
7029
6718
|
if (items.length > MAX_TRANSACT_ITEMS) {
|
|
@@ -7130,7 +6819,8 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
|
|
|
7130
6819
|
const outboxEvents = method.outboxEvents ?? [];
|
|
7131
6820
|
const idempotencyGuard = method.idempotencyGuard;
|
|
7132
6821
|
const maintainWrites = method.maintainWrites ?? [];
|
|
7133
|
-
const
|
|
6822
|
+
const maintainOutbox = method.maintainOutbox ?? [];
|
|
6823
|
+
const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0 || maintainWrites.length > 0 || maintainOutbox.length > 0;
|
|
7134
6824
|
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
7135
6825
|
const promotedToTransaction = isMultiFragment || hasDerivedEffects;
|
|
7136
6826
|
if (promotedToTransaction) {
|
|
@@ -7149,7 +6839,8 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
|
|
|
7149
6839
|
uniqueGuards,
|
|
7150
6840
|
outboxEvents,
|
|
7151
6841
|
idempotencyGuard,
|
|
7152
|
-
maintainWrites
|
|
6842
|
+
maintainWrites,
|
|
6843
|
+
maintainOutbox
|
|
7153
6844
|
)
|
|
7154
6845
|
) : (
|
|
7155
6846
|
// #90: writes only.
|
|
@@ -7315,17 +7006,6 @@ export {
|
|
|
7315
7006
|
BatchGetResult,
|
|
7316
7007
|
executeBatchGet,
|
|
7317
7008
|
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
7009
|
OLD_VALUE_NAMESPACE,
|
|
7330
7010
|
EDGE_WRITES_MARKER,
|
|
7331
7011
|
isEdgeWritesDefinition,
|
|
@@ -7334,7 +7014,6 @@ export {
|
|
|
7334
7014
|
deriveEdgeWriteItems,
|
|
7335
7015
|
getEdgeWrites,
|
|
7336
7016
|
deriveModelEdgeWriteItems,
|
|
7337
|
-
buildMaintenanceGraph,
|
|
7338
7017
|
resolveLifecycle,
|
|
7339
7018
|
compileFragment,
|
|
7340
7019
|
compileSingleFragmentPlan,
|