graphddb 0.7.9 → 0.8.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 +6 -6
- package/dist/cdc/index.d.ts +389 -4
- package/dist/cdc/index.js +4 -3
- package/dist/{chunk-ZPNRLOKA.js → chunk-GS4C5VGO.js} +4 -6
- package/dist/chunk-HNY2EJPV.js +1184 -0
- package/dist/{chunk-NYM7K2ST.js → chunk-I4LEJ4TF.js} +3812 -6724
- package/dist/{chunk-PFFPLD4B.js → chunk-L2NEDS7U.js} +725 -2112
- package/dist/chunk-L4QRCHRQ.js +278 -0
- package/dist/chunk-LGHSZIEE.js +187 -0
- package/dist/chunk-N4NWYNGZ.js +1987 -0
- package/dist/{chunk-PDUVTYC5.js → chunk-XTWXMOHD.js} +0 -1
- package/dist/cli.js +63 -252
- package/dist/index.d.ts +23 -1548
- package/dist/index.js +100 -1778
- package/dist/internal/index.d.ts +84 -0
- package/dist/internal/index.js +701 -0
- package/dist/{maintenance-view-adapter-BATUh_I8.d.ts → key-DR7_lpyk.d.ts} +538 -2975
- package/dist/linter/index.d.ts +39 -6
- package/dist/linter/index.js +22 -4
- package/dist/{registry-CXhP4TaE.d.ts → linter-C-vypgut.d.ts} +22 -22
- package/dist/prepared-artifact-BpPgkXEo.d.ts +281 -0
- package/dist/spec/index.d.ts +506 -4
- package/dist/spec/index.js +36 -17
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +4 -3
- package/dist/transform/index.d.ts +460 -1
- package/dist/transform/index.js +2085 -2
- package/dist/types-2PMXEn5x.d.ts +1205 -0
- package/dist/types-BXLzIcQD.d.ts +450 -0
- package/docs/cdc-projection.md +5 -5
- package/docs/class-hydration.md +1 -1
- package/docs/cqrs-contract.md +28 -20
- package/docs/design-patterns.md +5 -5
- package/docs/docs-generation.md +6 -6
- package/docs/middleware.md +15 -15
- package/docs/mutation-command-derivation.md +52 -42
- package/docs/prepared-statements.md +14 -14
- package/docs/python-bridge.md +113 -66
- package/docs/spec.md +153 -124
- package/docs/testing.md +9 -8
- package/package.json +20 -5
- package/dist/chunk-MMVHOUM4.js +0 -24
- package/dist/from-change-DanwjE5b.d.ts +0 -327
- package/dist/index-CtPJSMrc.d.ts +0 -934
- package/dist/relation-depth-Dg3yhl7S.d.ts +0 -36
|
@@ -0,0 +1,1987 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PREPARED_FORMAT_VERSION,
|
|
3
|
+
buildManifest,
|
|
4
|
+
entityFingerprint
|
|
5
|
+
} from "./chunk-LGHSZIEE.js";
|
|
6
|
+
import {
|
|
7
|
+
analyzeWriteRoute,
|
|
8
|
+
assertBundleSerializable,
|
|
9
|
+
assertSupportedCondition,
|
|
10
|
+
buildTransactions,
|
|
11
|
+
compileReadRoute,
|
|
12
|
+
compileWriteFragment,
|
|
13
|
+
conditionInputToSpec,
|
|
14
|
+
contractOfMethodSpec,
|
|
15
|
+
deriveCompositionPlan,
|
|
16
|
+
deriveExecutionPlan,
|
|
17
|
+
evaluateKey,
|
|
18
|
+
evaluatePreparedRoutes,
|
|
19
|
+
isCommandModelContract,
|
|
20
|
+
isContractKeyFieldRef,
|
|
21
|
+
isContractKeyRef,
|
|
22
|
+
isContractParamRef,
|
|
23
|
+
isQueryModelContract
|
|
24
|
+
} from "./chunk-I4LEJ4TF.js";
|
|
25
|
+
import {
|
|
26
|
+
detectRelationFields,
|
|
27
|
+
isInlineSnapshotSpec,
|
|
28
|
+
normalizeSelectSpec
|
|
29
|
+
} from "./chunk-HNY2EJPV.js";
|
|
30
|
+
import {
|
|
31
|
+
MARKER_ROW_ENTITY,
|
|
32
|
+
SPEC_VERSION,
|
|
33
|
+
operationsSpecVersion
|
|
34
|
+
} from "./chunk-L4QRCHRQ.js";
|
|
35
|
+
import {
|
|
36
|
+
BATCH_GET_MAX_KEYS,
|
|
37
|
+
CONDITION_OPERATOR_KEYS,
|
|
38
|
+
MAX_TRANSACT_ITEMS,
|
|
39
|
+
MetadataRegistry,
|
|
40
|
+
isParam,
|
|
41
|
+
isRawCondition,
|
|
42
|
+
param,
|
|
43
|
+
resolveModelClass
|
|
44
|
+
} from "./chunk-L2NEDS7U.js";
|
|
45
|
+
import {
|
|
46
|
+
TableMapping,
|
|
47
|
+
isColumn,
|
|
48
|
+
resolveKey
|
|
49
|
+
} from "./chunk-XTWXMOHD.js";
|
|
50
|
+
|
|
51
|
+
// src/spec/contract-n1-check.ts
|
|
52
|
+
function isQueryMethodSpec(spec) {
|
|
53
|
+
return typeof spec.resolution === "string" && spec.resolution !== void 0;
|
|
54
|
+
}
|
|
55
|
+
function checkArrayIntoRange(contract, method, spec) {
|
|
56
|
+
if (spec.resolution === "range" && spec.inputArity !== "single") {
|
|
57
|
+
return {
|
|
58
|
+
rule: "array-into-range",
|
|
59
|
+
contract,
|
|
60
|
+
method,
|
|
61
|
+
message: `Contract '${contract}.${method}': a \`range\` resolution accepts an array input (inputArity: '${spec.inputArity}'), but a partition \`Query\` cannot be coalesced across partition keys \u2014 feeding N keys would issue N \`Query\`s (an N+1 fan-out). A \`range\` method must be \`inputArity: 'single'\`. Resolve "just these few" with an application-side loop (\`Promise.all(ids.map((id) => contract.method(id)))\`), keeping the N visible at the call site; there is no bounded-fan-out opt-in.`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
function checkArrayIntoGsiPoint(contract, method, spec, isGsiPoint) {
|
|
67
|
+
if (spec.resolution === "point" && isGsiPoint && spec.inputArity !== "single") {
|
|
68
|
+
return {
|
|
69
|
+
rule: "array-into-gsi-point",
|
|
70
|
+
contract,
|
|
71
|
+
method,
|
|
72
|
+
message: `Contract '${contract}.${method}': a \`point\` read whose Key resolves via a unique GSI accepts an array input (inputArity: '${spec.inputArity}'), but a GSI point read is a per-key \`Query\` \u2014 \`BatchGetItem\` cannot read a GSI \u2014 so feeding N keys would issue N \`Query\`s (an N+1 fan-out). A unique-GSI \`point\` method must be \`inputArity: 'single'\` (only a base-table primary-key point coalesces into one \`BatchGetItem\`). Resolve "just these few" with an application-side loop (\`Promise.all(keys.map((k) => contract.method(k)))\`), keeping the N visible at the call site; there is no bounded-fan-out opt-in.`
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
function checkComposeChild(contract, method, parentCardinality, node) {
|
|
78
|
+
const childResolution = node.resolution;
|
|
79
|
+
if (childResolution !== "range") return void 0;
|
|
80
|
+
if (parentCardinality === "many") {
|
|
81
|
+
return {
|
|
82
|
+
rule: "list-under-list",
|
|
83
|
+
contract,
|
|
84
|
+
method,
|
|
85
|
+
message: `Contract '${contract}.${method}': composed child '${node.as}' is a \`range\` read nested under a parent step whose per-key cardinality is 'many' \u2014 a "list under a list". The parent yields N records and each drives its own partition \`Query\`, an N+1 fan-out, which is rejected at build time. A composed child under a many-yielding parent must be \`point\` (it coalesces to one \`BatchGetItem\`); resolve a true to-many with an application-side loop instead.`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
rule: "compose-range-under-many",
|
|
90
|
+
contract,
|
|
91
|
+
method,
|
|
92
|
+
message: `Contract '${contract}.${method}': composed child '${node.as}' is a \`range\` read (a partition \`Query\`). An External Query / composition child must be \`point\` so it coalesces to one \`BatchGetItem\` across all parent keys; a \`range\` child cannot be batched across the parent's keys and is an N+1 fan-out. Expose the to-many as a single-key \`range\` method and resolve it with an application-side loop, or make the child a \`point\` contract.`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function collectContractN1Violations(contractName, spec, isGsiPoint = () => false) {
|
|
96
|
+
const violations = [];
|
|
97
|
+
if (spec.kind !== "query") return violations;
|
|
98
|
+
for (const methodName of Object.keys(spec.methods)) {
|
|
99
|
+
const methodSpec = spec.methods[methodName];
|
|
100
|
+
if (!isQueryMethodSpec(methodSpec)) continue;
|
|
101
|
+
const arrayIntoRange = checkArrayIntoRange(contractName, methodName, methodSpec);
|
|
102
|
+
if (arrayIntoRange) violations.push(arrayIntoRange);
|
|
103
|
+
const arrayIntoGsiPoint = checkArrayIntoGsiPoint(
|
|
104
|
+
contractName,
|
|
105
|
+
methodName,
|
|
106
|
+
methodSpec,
|
|
107
|
+
isGsiPoint(methodName)
|
|
108
|
+
);
|
|
109
|
+
if (arrayIntoGsiPoint) violations.push(arrayIntoGsiPoint);
|
|
110
|
+
const parentCardinality = methodSpec.cardinality;
|
|
111
|
+
for (const child of methodSpec.compose ?? []) {
|
|
112
|
+
const violation = checkComposeChild(
|
|
113
|
+
contractName,
|
|
114
|
+
methodName,
|
|
115
|
+
parentCardinality,
|
|
116
|
+
child
|
|
117
|
+
);
|
|
118
|
+
if (violation) violations.push(violation);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return violations;
|
|
122
|
+
}
|
|
123
|
+
function assertContractN1Safe(contractName, spec, isGsiPoint) {
|
|
124
|
+
const violations = collectContractN1Violations(contractName, spec, isGsiPoint);
|
|
125
|
+
if (violations.length === 0) return;
|
|
126
|
+
const [first, ...rest] = violations;
|
|
127
|
+
const suffix = rest.length > 0 ? `
|
|
128
|
+
|
|
129
|
+
(${rest.length} further N+1 violation${rest.length === 1 ? "" : "s"} in this contract:
|
|
130
|
+
${rest.map((v) => ` - ${v.message}`).join("\n")})` : "";
|
|
131
|
+
throw new Error(first.message + suffix);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/spec/contracts.ts
|
|
135
|
+
function kindForDynamoType(dynamoType) {
|
|
136
|
+
return dynamoType === "N" ? "number" : "string";
|
|
137
|
+
}
|
|
138
|
+
function recoverKind(metadata, fieldName) {
|
|
139
|
+
const field = metadata.fields.find((f) => f.propertyName === fieldName);
|
|
140
|
+
if (field === void 0) return { kind: "string" };
|
|
141
|
+
if (field.literals !== void 0 && field.literals.length > 0) {
|
|
142
|
+
return { kind: "literal", literals: field.literals };
|
|
143
|
+
}
|
|
144
|
+
return { kind: kindForDynamoType(field.dynamoType) };
|
|
145
|
+
}
|
|
146
|
+
function opToDefinition(contractName, methodName, op) {
|
|
147
|
+
const metadata = MetadataRegistry.get(op.entity.modelClass);
|
|
148
|
+
const params = {};
|
|
149
|
+
const place = (paramName, bindField, kindOverride) => {
|
|
150
|
+
const recovered = kindOverride !== void 0 ? { kind: kindOverride } : recoverKind(metadata, bindField);
|
|
151
|
+
const description = op.paramDescriptions?.[paramName];
|
|
152
|
+
params[paramName] = {
|
|
153
|
+
kind: recovered.kind,
|
|
154
|
+
required: true,
|
|
155
|
+
...recovered.literals !== void 0 ? { literals: recovered.literals } : {},
|
|
156
|
+
...description !== void 0 ? { description } : {}
|
|
157
|
+
};
|
|
158
|
+
return recovered;
|
|
159
|
+
};
|
|
160
|
+
let key;
|
|
161
|
+
if (op.operation === "put") {
|
|
162
|
+
key = convertStructure(op.item ?? {}, place, `${contractName}.${methodName} item`);
|
|
163
|
+
} else if (isContractKeyRef(op.keys)) {
|
|
164
|
+
key = wholeKeyToParams(metadata, op, contractName, methodName, place);
|
|
165
|
+
} else {
|
|
166
|
+
key = convertKeyRecord(op.keys, place, `${contractName}.${methodName} key`);
|
|
167
|
+
}
|
|
168
|
+
const select = op.select !== void 0 ? assertBooleanProjection(op.select, `${contractName}.${methodName} select`) : void 0;
|
|
169
|
+
const changes = op.changes !== void 0 ? convertStructure(op.changes, place, `${contractName}.${methodName} changes`) : void 0;
|
|
170
|
+
const condition = op.condition !== void 0 ? convertCondition(op.condition, place, `${contractName}.${methodName} condition`) : void 0;
|
|
171
|
+
return {
|
|
172
|
+
__isOperationDefinition: true,
|
|
173
|
+
entity: op.entity,
|
|
174
|
+
operation: op.operation,
|
|
175
|
+
key,
|
|
176
|
+
select,
|
|
177
|
+
changes,
|
|
178
|
+
...condition !== void 0 ? { condition } : {},
|
|
179
|
+
// #154: the use-case description captured on the recorded op (from the
|
|
180
|
+
// descriptor's `description`) flows into the flattened `queries[op]` /
|
|
181
|
+
// `commands[op]` entry, so a documented contract read/write is self-describing
|
|
182
|
+
// in `operations.json` exactly as the removed define* op was.
|
|
183
|
+
...op.description !== void 0 ? { description: op.description } : {},
|
|
184
|
+
params
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function paramOfKind(recovered) {
|
|
188
|
+
if (recovered.kind === "number") return param.number();
|
|
189
|
+
if (recovered.kind === "literal" && recovered.literals !== void 0 && recovered.literals.length > 0) {
|
|
190
|
+
return param.literal(
|
|
191
|
+
...recovered.literals
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return param.string();
|
|
195
|
+
}
|
|
196
|
+
function wholeKeyToParams(metadata, op, contractName, methodName, place) {
|
|
197
|
+
const fields = wholeKeyFieldNames(metadata, op, contractName, methodName);
|
|
198
|
+
const key = {};
|
|
199
|
+
for (const field of fields) {
|
|
200
|
+
key[field] = paramOfKind(place(field, field));
|
|
201
|
+
}
|
|
202
|
+
return key;
|
|
203
|
+
}
|
|
204
|
+
function wholeKeyFieldNames(metadata, op, contractName, methodName) {
|
|
205
|
+
if (op.keyFields !== void 0 && op.keyFields.length > 0) return op.keyFields;
|
|
206
|
+
if (!metadata.primaryKey) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Contract '${contractName}.${methodName}': the '${op.operation}' op passes the whole \`keys\` into the key position, but model '${op.entity.name}' has no primary key to resolve the contract Key from. Supply an explicit Key-field list to the factory (e.g. \`publishQuery<Key>(['field'])\`).`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return metadata.primaryKey.inputFieldNames;
|
|
212
|
+
}
|
|
213
|
+
function convertKeyRecord(record, place, context) {
|
|
214
|
+
const out = {};
|
|
215
|
+
for (const [field, leaf] of Object.entries(record)) {
|
|
216
|
+
if (isContractKeyFieldRef(leaf)) {
|
|
217
|
+
out[field] = paramOfKind(place(leaf.field, field));
|
|
218
|
+
} else if (isConcreteLiteral(leaf)) {
|
|
219
|
+
out[field] = leaf;
|
|
220
|
+
} else {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`${context}: key field '${field}' is neither a \`keys.<field>\` reference nor a concrete literal \u2014 it cannot be serialized.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
function convertStructure(structure, place, context) {
|
|
229
|
+
const out = {};
|
|
230
|
+
for (const [field, leaf] of Object.entries(structure)) {
|
|
231
|
+
out[field] = convertLeaf(leaf, field, place, `${context} field '${field}'`);
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
function convertLeaf(leaf, bindField, place, context, kindOverride) {
|
|
236
|
+
if (isContractParamRef(leaf)) {
|
|
237
|
+
place(leaf.field, bindField, kindOverride);
|
|
238
|
+
return leaf;
|
|
239
|
+
}
|
|
240
|
+
if (isContractKeyFieldRef(leaf)) {
|
|
241
|
+
place(leaf.field, bindField, kindOverride);
|
|
242
|
+
return leaf;
|
|
243
|
+
}
|
|
244
|
+
if (isConcreteLiteral(leaf)) return leaf;
|
|
245
|
+
throw new Error(
|
|
246
|
+
`${context}: value is neither a param/key reference nor a concrete literal \u2014 it cannot be serialized as a declarative contract operation.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
function nearestColumnFields(condition) {
|
|
250
|
+
const parts = condition.parts;
|
|
251
|
+
const columnAt = (i) => isColumn(parts[i]) ? parts[i].name : void 0;
|
|
252
|
+
const out = [];
|
|
253
|
+
for (let i = 0; i < parts.length; i++) {
|
|
254
|
+
const part = parts[i];
|
|
255
|
+
if (!isContractParamRef(part) && !isContractKeyFieldRef(part)) continue;
|
|
256
|
+
let found;
|
|
257
|
+
for (let d = 1; d < parts.length && found === void 0; d++) {
|
|
258
|
+
found = columnAt(i - d) ?? columnAt(i + d);
|
|
259
|
+
}
|
|
260
|
+
out.push(found);
|
|
261
|
+
}
|
|
262
|
+
return out;
|
|
263
|
+
}
|
|
264
|
+
function convertCondition(condition, place, context) {
|
|
265
|
+
if (isRawCondition(condition)) {
|
|
266
|
+
const compareFieldForSlot = nearestColumnFields(condition);
|
|
267
|
+
let slotIndex = 0;
|
|
268
|
+
for (const part of condition.parts) {
|
|
269
|
+
if (isContractParamRef(part) || isContractKeyFieldRef(part)) {
|
|
270
|
+
const compareField = compareFieldForSlot[slotIndex];
|
|
271
|
+
if (compareField === void 0) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`${context}: a raw \`cond\` value slot ('${part.field}') has no adjacent column, so its param type cannot be inferred. A \`cond\` param slot must be compared against a \`Model.col.<field>\` reference (e.g. \`cond\`\${Model.col.x} < \${param.number()}\`\`) so the parameter inherits that column's type; refusing to emit an un-typed (silently 'string') param.`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
place(part.field, compareField);
|
|
277
|
+
slotIndex++;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return condition;
|
|
281
|
+
}
|
|
282
|
+
if (typeof condition === "object" && condition !== null) {
|
|
283
|
+
const obj2 = condition;
|
|
284
|
+
if (obj2.notExists === true) return { notExists: true };
|
|
285
|
+
if (typeof obj2.attributeExists === "string") {
|
|
286
|
+
return { attributeExists: obj2.attributeExists };
|
|
287
|
+
}
|
|
288
|
+
if (typeof obj2.attributeNotExists === "string") {
|
|
289
|
+
return { attributeNotExists: obj2.attributeNotExists };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const obj = condition;
|
|
293
|
+
if (usesConditionTree(obj)) {
|
|
294
|
+
return convertConditionTree(obj, place, context);
|
|
295
|
+
}
|
|
296
|
+
const out = {};
|
|
297
|
+
for (const [field, leaf] of Object.entries(obj)) {
|
|
298
|
+
out[field] = convertLeaf(leaf, field, place, `${context} field '${field}'`);
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
var CONDITION_LOGICAL_KEYS = /* @__PURE__ */ new Set(["and", "or", "not"]);
|
|
303
|
+
function isConditionOperatorObject(value) {
|
|
304
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || isContractParamRef(value) || isContractKeyFieldRef(value)) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
const keys = Object.keys(value);
|
|
308
|
+
if (keys.length === 0) return false;
|
|
309
|
+
return keys.every((k) => CONDITION_OPERATOR_KEYS.has(k));
|
|
310
|
+
}
|
|
311
|
+
function usesConditionTree(obj) {
|
|
312
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
313
|
+
if (CONDITION_LOGICAL_KEYS.has(key)) return true;
|
|
314
|
+
if (isConditionOperatorObject(value)) return true;
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
function convertConditionTree(obj, place, context) {
|
|
319
|
+
const out = {};
|
|
320
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
321
|
+
if (value === void 0) continue;
|
|
322
|
+
if (key === "and" || key === "or") {
|
|
323
|
+
out[key] = value.map(
|
|
324
|
+
(sub) => convertConditionTree(sub, place, `${context} ${key}`)
|
|
325
|
+
);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (key === "not") {
|
|
329
|
+
out[key] = convertConditionTree(value, place, `${context} not`);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (!isConditionOperatorObject(value)) {
|
|
333
|
+
out[key] = convertLeaf(value, key, place, `${context} field '${key}'`);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const ops = value;
|
|
337
|
+
const rendered = {};
|
|
338
|
+
for (const [op, opVal] of Object.entries(ops)) {
|
|
339
|
+
if (op === "between") {
|
|
340
|
+
const [lo, hi] = opVal;
|
|
341
|
+
rendered[op] = [
|
|
342
|
+
convertLeaf(lo, key, place, `${context} field '${key}' between[0]`),
|
|
343
|
+
convertLeaf(hi, key, place, `${context} field '${key}' between[1]`)
|
|
344
|
+
];
|
|
345
|
+
} else if (op === "in") {
|
|
346
|
+
rendered[op] = opVal.map(
|
|
347
|
+
(v, i) => convertLeaf(v, key, place, `${context} field '${key}' in[${i}]`)
|
|
348
|
+
);
|
|
349
|
+
} else if (op === "attributeExists" || op === "attributeType") {
|
|
350
|
+
rendered[op] = opVal;
|
|
351
|
+
} else if (op === "size") {
|
|
352
|
+
rendered[op] = convertLeaf(opVal, key, place, `${context} field '${key}' ${op}`, "number");
|
|
353
|
+
} else {
|
|
354
|
+
rendered[op] = convertLeaf(opVal, key, place, `${context} field '${key}' ${op}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
out[key] = rendered;
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
function assertBooleanProjection(select, context) {
|
|
362
|
+
const out = {};
|
|
363
|
+
for (const [field, leaf] of Object.entries(select)) {
|
|
364
|
+
if (typeof leaf === "boolean") {
|
|
365
|
+
out[field] = leaf;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (leaf !== null && typeof leaf === "object" && !isContractParamRef(leaf) && !isContractKeyFieldRef(leaf)) {
|
|
369
|
+
out[field] = leaf;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
throw new Error(
|
|
373
|
+
`${context}: projection field '${field}' must be a boolean flag (\`true\` / \`false\`), but a ${describeLeaf(leaf)} was found. A parameter reference in a projection slot is not a valid select \u2014 refusing to emit it.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
function describeLeaf(leaf) {
|
|
379
|
+
if (isContractParamRef(leaf)) return `param reference (${leaf.token})`;
|
|
380
|
+
if (isContractKeyFieldRef(leaf)) return `key-field reference (${leaf.token})`;
|
|
381
|
+
return `value of type '${typeof leaf}'`;
|
|
382
|
+
}
|
|
383
|
+
function isConcreteLiteral(value) {
|
|
384
|
+
const t = typeof value;
|
|
385
|
+
return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
|
|
386
|
+
}
|
|
387
|
+
function opRefName(contractName, methodName) {
|
|
388
|
+
return `${contractName}__${methodName}`;
|
|
389
|
+
}
|
|
390
|
+
function composeSpecOf(node, contractName, methodName, contracts) {
|
|
391
|
+
if (isRecordedCompose(node)) {
|
|
392
|
+
return recordedComposeSpec(node, contractName, methodName, contracts);
|
|
393
|
+
}
|
|
394
|
+
const c = node;
|
|
395
|
+
if (typeof c.as !== "string" || typeof c.contract !== "string" || typeof c.method !== "string") {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`Contract '${contractName}.${methodName}': a composition must declare string \`as\` / \`contract\` / \`method\`.`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
if (c.resolution !== "point" && c.resolution !== "range") {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`Contract '${contractName}.${methodName}': composed child '${c.as}' must declare a \`resolution\` of 'point' or 'range'.`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const bindIn = c.bind ?? {};
|
|
406
|
+
const bind = {};
|
|
407
|
+
for (const field of Object.keys(bindIn).sort()) {
|
|
408
|
+
const v = bindIn[field];
|
|
409
|
+
if (typeof v !== "string") {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`Contract '${contractName}.${methodName}': composition '${c.as}' binding '${field}' must be a \`from\` path string (e.g. "$.billingAccountId").`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
bind[field] = v;
|
|
415
|
+
}
|
|
416
|
+
const cardinality = c.cardinality === "many" ? "many" : "one";
|
|
417
|
+
return {
|
|
418
|
+
as: c.as,
|
|
419
|
+
contract: c.contract,
|
|
420
|
+
method: c.method,
|
|
421
|
+
...typeof c.context === "string" ? { context: c.context } : {},
|
|
422
|
+
bind,
|
|
423
|
+
resolution: c.resolution,
|
|
424
|
+
cardinality
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function isRecordedCompose(node) {
|
|
428
|
+
return typeof node === "object" && node !== null && typeof node.as === "string" && typeof node.method === "object" && node.method?.__methodKind === "query";
|
|
429
|
+
}
|
|
430
|
+
function recordedComposeSpec(node, contractName, methodName, contracts) {
|
|
431
|
+
const { contract: refName, method: refMethodName } = resolveComposeRef(
|
|
432
|
+
node,
|
|
433
|
+
contractName,
|
|
434
|
+
methodName,
|
|
435
|
+
contracts
|
|
436
|
+
);
|
|
437
|
+
const refMethod = node.method;
|
|
438
|
+
const bind = {};
|
|
439
|
+
for (const field of Object.keys(node.bind).sort()) {
|
|
440
|
+
bind[field] = node.bind[field].path;
|
|
441
|
+
}
|
|
442
|
+
const cardinality = refMethod.resolution === "point" ? "one" : "many";
|
|
443
|
+
return {
|
|
444
|
+
as: node.as,
|
|
445
|
+
contract: refName,
|
|
446
|
+
method: refMethodName,
|
|
447
|
+
bind,
|
|
448
|
+
resolution: refMethod.resolution,
|
|
449
|
+
cardinality
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function resolveComposeRef(node, contractName, methodName, contracts) {
|
|
453
|
+
const refMethodName = node.method.__methodName;
|
|
454
|
+
if (typeof refMethodName !== "string") {
|
|
455
|
+
throw new Error(
|
|
456
|
+
`Contract '${contractName}.${methodName}': composed child '${node.as}' references a query method that was not produced by publishQuery (no method name). Compose only the value of a publishQuery method (e.g. \`query(OtherContract.get, \u2026)\`).`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
const owner = contractOfMethodSpec(node.method);
|
|
460
|
+
if (owner !== void 0) {
|
|
461
|
+
for (const name of Object.keys(contracts)) {
|
|
462
|
+
if (contracts[name] === owner) return { contract: name, method: refMethodName };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
throw new Error(
|
|
466
|
+
`Contract '${contractName}.${methodName}': composed child '${node.as}' references a contract method that is not registered in this SSoT. An External Query must reference another contract passed to the generator's contract map (a same-SSoT, in-process reference); add the referenced contract to the \`contracts\` map.`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
function composeOf(op) {
|
|
470
|
+
const raw = op.compose;
|
|
471
|
+
return Array.isArray(raw) ? raw : [];
|
|
472
|
+
}
|
|
473
|
+
function compositionPlanOf(compose) {
|
|
474
|
+
if (compose.length === 0) return void 0;
|
|
475
|
+
const { stages, concurrency } = deriveCompositionPlan(compose.length);
|
|
476
|
+
return {
|
|
477
|
+
stages: stages.map((s) => [...s]),
|
|
478
|
+
concurrency,
|
|
479
|
+
batchChunkSize: BATCH_GET_MAX_KEYS
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function batchTxName(contractName, methodName) {
|
|
483
|
+
return `${opRefName(contractName, methodName)}__batch`;
|
|
484
|
+
}
|
|
485
|
+
function keyParamNames(op) {
|
|
486
|
+
return keyFieldsOf(op);
|
|
487
|
+
}
|
|
488
|
+
var TX_ITEM_TYPE = {
|
|
489
|
+
PutItem: "Put",
|
|
490
|
+
UpdateItem: "Update",
|
|
491
|
+
DeleteItem: "Delete"
|
|
492
|
+
};
|
|
493
|
+
function toElementTemplate(template, keyParams) {
|
|
494
|
+
return template.replace(
|
|
495
|
+
/\{([^}]+)\}/g,
|
|
496
|
+
(match, name) => keyParams.has(name) ? `{item.${name}}` : match
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
function toElementRecord(record, keyParams) {
|
|
500
|
+
const out = {};
|
|
501
|
+
for (const field of Object.keys(record)) {
|
|
502
|
+
out[field] = toElementTemplate(record[field], keyParams);
|
|
503
|
+
}
|
|
504
|
+
return out;
|
|
505
|
+
}
|
|
506
|
+
function toElementCondition(condition, keyParams) {
|
|
507
|
+
if (condition.kind === "equals") {
|
|
508
|
+
return { kind: "equals", fields: toElementRecord(condition.fields, keyParams) };
|
|
509
|
+
}
|
|
510
|
+
if (condition.kind === "expr") {
|
|
511
|
+
return { kind: "expr", declarative: toElementTree(condition.declarative, keyParams) };
|
|
512
|
+
}
|
|
513
|
+
if (condition.kind === "raw") {
|
|
514
|
+
return {
|
|
515
|
+
kind: "raw",
|
|
516
|
+
expression: condition.expression,
|
|
517
|
+
names: condition.names,
|
|
518
|
+
values: toElementTree(condition.values, keyParams)
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return condition;
|
|
522
|
+
}
|
|
523
|
+
function toElementTree(node, keyParams) {
|
|
524
|
+
if (Array.isArray(node)) return node.map((n) => toElementTree(n, keyParams));
|
|
525
|
+
if (node === null || typeof node !== "object") return node;
|
|
526
|
+
const obj = node;
|
|
527
|
+
if (typeof obj.$param === "string" && !obj.$param.startsWith("item.")) {
|
|
528
|
+
return keyParams.has(obj.$param) ? { $param: `item.${obj.$param}` } : obj;
|
|
529
|
+
}
|
|
530
|
+
const out = {};
|
|
531
|
+
for (const [k, v] of Object.entries(obj)) out[k] = toElementTree(v, keyParams);
|
|
532
|
+
return out;
|
|
533
|
+
}
|
|
534
|
+
function synthesizeBatchTransaction(commandSpec, op) {
|
|
535
|
+
const keyParams = new Set(keyParamNames(op));
|
|
536
|
+
const elementFields = {};
|
|
537
|
+
const sharedParams = {};
|
|
538
|
+
for (const name of Object.keys(commandSpec.params).sort()) {
|
|
539
|
+
if (keyParams.has(name)) elementFields[name] = commandSpec.params[name];
|
|
540
|
+
else sharedParams[name] = commandSpec.params[name];
|
|
541
|
+
}
|
|
542
|
+
const params = {
|
|
543
|
+
...sharedParams,
|
|
544
|
+
keys: { type: "array", required: true, element: elementFields }
|
|
545
|
+
};
|
|
546
|
+
const condition = commandSpec.condition !== void 0 ? toElementCondition(commandSpec.condition, keyParams) : void 0;
|
|
547
|
+
const item = {
|
|
548
|
+
type: TX_ITEM_TYPE[commandSpec.type],
|
|
549
|
+
tableName: commandSpec.tableName,
|
|
550
|
+
entity: commandSpec.entity,
|
|
551
|
+
...commandSpec.item !== void 0 ? { item: toElementRecord(commandSpec.item, keyParams) } : {},
|
|
552
|
+
...commandSpec.keyCondition !== void 0 ? { keyCondition: toElementRecord(commandSpec.keyCondition, keyParams) } : {},
|
|
553
|
+
...commandSpec.changes !== void 0 ? { changes: toElementRecord(commandSpec.changes, keyParams) } : {},
|
|
554
|
+
...condition !== void 0 ? { condition } : {},
|
|
555
|
+
forEach: { source: "keys" }
|
|
556
|
+
};
|
|
557
|
+
return { params, items: [item] };
|
|
558
|
+
}
|
|
559
|
+
function composeFragmentTransaction(contractName, methodName, ops) {
|
|
560
|
+
const items = [];
|
|
561
|
+
const params = {};
|
|
562
|
+
for (let i = 0; i < ops.length; i++) {
|
|
563
|
+
const op = ops[i];
|
|
564
|
+
const def = opToDefinition(contractName, `${methodName}__f${i}`, op);
|
|
565
|
+
const commandSpec = buildCommandSpec(def);
|
|
566
|
+
if (commandSpec.condition) {
|
|
567
|
+
assertSupportedCondition(`${opRefName(contractName, methodName)}#${i}`, commandSpec.condition);
|
|
568
|
+
}
|
|
569
|
+
for (const [name, spec] of Object.entries(commandSpec.params)) {
|
|
570
|
+
params[name] = spec;
|
|
571
|
+
}
|
|
572
|
+
const item = {
|
|
573
|
+
type: TX_ITEM_TYPE[commandSpec.type],
|
|
574
|
+
tableName: commandSpec.tableName,
|
|
575
|
+
entity: commandSpec.entity,
|
|
576
|
+
...commandSpec.item !== void 0 ? { item: commandSpec.item } : {},
|
|
577
|
+
...commandSpec.keyCondition !== void 0 ? { keyCondition: commandSpec.keyCondition } : {},
|
|
578
|
+
...commandSpec.changes !== void 0 ? { changes: commandSpec.changes } : {},
|
|
579
|
+
...commandSpec.condition !== void 0 ? { condition: commandSpec.condition } : {}
|
|
580
|
+
};
|
|
581
|
+
items.push(item);
|
|
582
|
+
}
|
|
583
|
+
return { params: sortedParams(params), items, maxItems: items.length };
|
|
584
|
+
}
|
|
585
|
+
function itemKeySignature(item) {
|
|
586
|
+
if (item.type === "Put") {
|
|
587
|
+
const metadata = MetadataRegistry.get(
|
|
588
|
+
// The entity name matches a manifest entity / a registered model class name.
|
|
589
|
+
resolveModelByName(item.entity)
|
|
590
|
+
);
|
|
591
|
+
if (!metadata.primaryKey) return `${item.entity}#<no-key>`;
|
|
592
|
+
const present = new Set(Object.keys(item.item ?? {}));
|
|
593
|
+
const { pk, sk } = evaluateKey(metadata.primaryKey.segmented, "param", present);
|
|
594
|
+
return `${item.entity}#${pk}#${sk ?? ""}`;
|
|
595
|
+
}
|
|
596
|
+
const kc = item.keyCondition ?? {};
|
|
597
|
+
return `${item.entity}#${kc.PK ?? ""}#${kc.SK ?? ""}`;
|
|
598
|
+
}
|
|
599
|
+
function resolveModelByName(name) {
|
|
600
|
+
for (const modelClass of MetadataRegistry.getAll().keys()) {
|
|
601
|
+
if (modelClass.name === name) {
|
|
602
|
+
return modelClass;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
throw new Error(
|
|
606
|
+
`Contract serializer: cannot resolve model '${name}' for the edge de-dup key signature \u2014 it is not a registered model.`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
function sortedParams(params) {
|
|
610
|
+
const out = {};
|
|
611
|
+
for (const name of Object.keys(params).sort()) out[name] = params[name];
|
|
612
|
+
return out;
|
|
613
|
+
}
|
|
614
|
+
function buildConditionCheckItems(checks, label) {
|
|
615
|
+
const items = [];
|
|
616
|
+
const params = {};
|
|
617
|
+
for (const check of checks) {
|
|
618
|
+
const metadata = MetadataRegistry.get(
|
|
619
|
+
check.entity.modelClass
|
|
620
|
+
);
|
|
621
|
+
if (!metadata.primaryKey) {
|
|
622
|
+
throw new Error(
|
|
623
|
+
`${label}: the referenced entity '${check.entity.name}' has no primary key, so a referential-integrity ConditionCheck cannot be keyed.`
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
627
|
+
const present = new Set(Object.keys(check.keyBinding));
|
|
628
|
+
const { pk, sk } = evaluateKey(
|
|
629
|
+
metadata.primaryKey.segmented,
|
|
630
|
+
"param",
|
|
631
|
+
present,
|
|
632
|
+
(field) => check.keyBinding[field] ?? field
|
|
633
|
+
);
|
|
634
|
+
const keyCondition = { PK: pk };
|
|
635
|
+
if (sk !== void 0) keyCondition.SK = sk;
|
|
636
|
+
const condition = { kind: "attributeExists", field: "PK" };
|
|
637
|
+
assertSupportedCondition(`${label} (${check.entity.name})`, condition);
|
|
638
|
+
items.push({
|
|
639
|
+
type: "ConditionCheck",
|
|
640
|
+
tableName,
|
|
641
|
+
entity: check.entity.name,
|
|
642
|
+
keyCondition,
|
|
643
|
+
condition
|
|
644
|
+
});
|
|
645
|
+
for (const [targetField, inputField] of Object.entries(check.keyBinding)) {
|
|
646
|
+
const recovered = recoverKind(metadata, targetField);
|
|
647
|
+
params[inputField] = recovered.literals === void 0 ? { type: recovered.kind, required: true } : { type: recovered.kind, required: true, literals: recovered.literals };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return { items, params };
|
|
651
|
+
}
|
|
652
|
+
function buildEdgeWriteItems(edgeWrites) {
|
|
653
|
+
const items = [];
|
|
654
|
+
const params = {};
|
|
655
|
+
for (const edge of edgeWrites) {
|
|
656
|
+
const metadata = MetadataRegistry.get(
|
|
657
|
+
edge.entity.modelClass
|
|
658
|
+
);
|
|
659
|
+
for (const item of edge.items) {
|
|
660
|
+
items.push(item);
|
|
661
|
+
collectTemplateParams(item.item, metadata, params);
|
|
662
|
+
collectTemplateParams(item.keyCondition, metadata, params);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return { items, params };
|
|
666
|
+
}
|
|
667
|
+
var TEMPLATE_TOKEN_RE = /\{([^{}]+)\}/g;
|
|
668
|
+
function collectTemplateParams(record, metadata, out) {
|
|
669
|
+
if (record === void 0) return;
|
|
670
|
+
for (const template of Object.values(record)) {
|
|
671
|
+
if (typeof template !== "string") continue;
|
|
672
|
+
for (const match of template.matchAll(TEMPLATE_TOKEN_RE)) {
|
|
673
|
+
const token = match[1];
|
|
674
|
+
const field = token.startsWith("old.") ? token.slice("old.".length) : token;
|
|
675
|
+
const recovered = recoverKind(metadata, field);
|
|
676
|
+
out[token] = recovered.literals === void 0 ? { type: recovered.kind, required: true } : { type: recovered.kind, required: true, literals: recovered.literals };
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function buildDerivedUpdateItems(derivedUpdates) {
|
|
681
|
+
const items = [];
|
|
682
|
+
const params = {};
|
|
683
|
+
for (const update of derivedUpdates) {
|
|
684
|
+
const metadata = MetadataRegistry.get(
|
|
685
|
+
update.entity.modelClass
|
|
686
|
+
);
|
|
687
|
+
if (!metadata.primaryKey) {
|
|
688
|
+
throw new Error(
|
|
689
|
+
`derived update: the target entity '${update.entity.name}' has no primary key, so a derived counter \`UpdateItem\` cannot be keyed.`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
693
|
+
const present = new Set(Object.keys(update.keyBinding));
|
|
694
|
+
const { pk, sk } = evaluateKey(
|
|
695
|
+
metadata.primaryKey.segmented,
|
|
696
|
+
"param",
|
|
697
|
+
present,
|
|
698
|
+
(field) => update.keyBinding[field] ?? field
|
|
699
|
+
);
|
|
700
|
+
const keyCondition = { PK: pk };
|
|
701
|
+
if (sk !== void 0) keyCondition.SK = sk;
|
|
702
|
+
items.push({
|
|
703
|
+
type: "Update",
|
|
704
|
+
tableName,
|
|
705
|
+
entity: update.entity.name,
|
|
706
|
+
keyCondition,
|
|
707
|
+
// The delta is a compile-time constant → a literal numeric template (no param).
|
|
708
|
+
add: { [update.attribute]: String(update.amount) }
|
|
709
|
+
});
|
|
710
|
+
collectTemplateParams(keyCondition, metadata, params);
|
|
711
|
+
}
|
|
712
|
+
return { items, params };
|
|
713
|
+
}
|
|
714
|
+
function buildMaintainWriteItems(maintainWrites) {
|
|
715
|
+
const items = [];
|
|
716
|
+
const params = {};
|
|
717
|
+
for (const maintain of maintainWrites) {
|
|
718
|
+
const metadata = MetadataRegistry.get(
|
|
719
|
+
maintain.entity.modelClass
|
|
720
|
+
);
|
|
721
|
+
if (!metadata.primaryKey) {
|
|
722
|
+
throw new Error(
|
|
723
|
+
`maintenance write: the maintained entity '${maintain.entity.name}' has no primary key, so a maintenance \`UpdateItem\` cannot be keyed.`
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
727
|
+
const present = new Set(Object.keys(maintain.keyBinding));
|
|
728
|
+
const { pk, sk } = evaluateKey(
|
|
729
|
+
metadata.primaryKey.segmented,
|
|
730
|
+
"param",
|
|
731
|
+
present,
|
|
732
|
+
(field) => maintain.keyBinding[field] ?? field
|
|
733
|
+
);
|
|
734
|
+
const keyCondition = { PK: pk };
|
|
735
|
+
if (sk !== void 0) keyCondition.SK = sk;
|
|
736
|
+
if (maintain.kind === "membership") {
|
|
737
|
+
throw new Error(
|
|
738
|
+
`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.`
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
const projection = {};
|
|
742
|
+
for (const [attr, transform] of Object.entries(maintain.projection)) {
|
|
743
|
+
projection[attr] = {
|
|
744
|
+
op: transform.op,
|
|
745
|
+
args: [...transform.args],
|
|
746
|
+
inputField: transform.inputField
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
const maintainSpec = {
|
|
750
|
+
kind: maintain.kind,
|
|
751
|
+
relationProperty: maintain.relationProperty,
|
|
752
|
+
trigger: maintain.trigger,
|
|
753
|
+
projection,
|
|
754
|
+
...maintain.kind === "collection" && maintain.collection !== void 0 ? {
|
|
755
|
+
collection: {
|
|
756
|
+
field: maintain.collection.field,
|
|
757
|
+
...maintain.collection.maxItems !== void 0 ? { maxItems: maintain.collection.maxItems } : {},
|
|
758
|
+
...maintain.collection.orderBy !== void 0 ? { orderBy: maintain.collection.orderBy } : {},
|
|
759
|
+
...maintain.collection.orderBy !== void 0 && maintain.collection.orderDir !== void 0 ? { orderDir: maintain.collection.orderDir } : {}
|
|
760
|
+
}
|
|
761
|
+
} : {},
|
|
762
|
+
// #141: a counter carries its scalar `ADD` (attribute + literal numeric delta);
|
|
763
|
+
// the runtimes render `ADD #attr :delta` exactly as the self-lifecycle derived
|
|
764
|
+
// counter (#85) does, so a same-row counter merge stays consistent across paths.
|
|
765
|
+
...maintain.kind === "counter" && maintain.counter !== void 0 && maintain.counter.op === "count" ? {
|
|
766
|
+
counter: {
|
|
767
|
+
attribute: maintain.counter.attribute,
|
|
768
|
+
delta: String(maintain.counter.delta)
|
|
769
|
+
}
|
|
770
|
+
} : {}
|
|
771
|
+
};
|
|
772
|
+
items.push({
|
|
773
|
+
type: "Update",
|
|
774
|
+
tableName,
|
|
775
|
+
entity: maintain.entity.name,
|
|
776
|
+
keyCondition,
|
|
777
|
+
maintain: maintainSpec
|
|
778
|
+
});
|
|
779
|
+
collectTemplateParams(keyCondition, metadata, params);
|
|
780
|
+
}
|
|
781
|
+
return { items, params };
|
|
782
|
+
}
|
|
783
|
+
function buildUniqueGuardItems(uniqueGuards, entityMetadata) {
|
|
784
|
+
const items = [];
|
|
785
|
+
const params = {};
|
|
786
|
+
for (const guard of uniqueGuards) {
|
|
787
|
+
for (const item of guard.items) {
|
|
788
|
+
items.push({ ...item, tableName: TableMapping.resolve(item.tableName) });
|
|
789
|
+
collectTemplateParams(item.item, entityMetadata, params);
|
|
790
|
+
collectTemplateParams(item.keyCondition, entityMetadata, params);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return { items, params };
|
|
794
|
+
}
|
|
795
|
+
function buildOutboxEventItems(outboxEvents, entityMetadata) {
|
|
796
|
+
const items = [];
|
|
797
|
+
const params = {};
|
|
798
|
+
for (const event of outboxEvents) {
|
|
799
|
+
for (const item of event.items) {
|
|
800
|
+
items.push({ ...item, tableName: TableMapping.resolve(item.tableName) });
|
|
801
|
+
collectTemplateParams(item.item, entityMetadata, params);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return { items, params };
|
|
805
|
+
}
|
|
806
|
+
function buildMaintainOutboxItems(maintainOutbox, entityMetadata) {
|
|
807
|
+
const items = [];
|
|
808
|
+
const params = {};
|
|
809
|
+
for (const outbox of maintainOutbox) {
|
|
810
|
+
for (const item of outbox.items) {
|
|
811
|
+
items.push({ ...item, tableName: TableMapping.resolve(item.tableName) });
|
|
812
|
+
collectTemplateParams(item.item, entityMetadata, params);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return { items, params };
|
|
816
|
+
}
|
|
817
|
+
function buildIdempotencyGuardItems(guard, entityMetadata) {
|
|
818
|
+
const items = [];
|
|
819
|
+
const params = {};
|
|
820
|
+
for (const item of guard.items) {
|
|
821
|
+
items.push({ ...item, tableName: TableMapping.resolve(item.tableName) });
|
|
822
|
+
collectTemplateParams(item.item, entityMetadata, params);
|
|
823
|
+
}
|
|
824
|
+
return { items, params };
|
|
825
|
+
}
|
|
826
|
+
function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard, maintainWrites, maintainOutbox) {
|
|
827
|
+
const label = opRefName(contractName, methodName);
|
|
828
|
+
const base = composeFragmentTransaction(contractName, methodName, ops);
|
|
829
|
+
const { items: edgeItemsRaw, params: edgeParams } = buildEdgeWriteItems(edgeWrites);
|
|
830
|
+
const { items: updateItems, params: updateParams } = buildDerivedUpdateItems(derivedUpdates);
|
|
831
|
+
const { items: checkItems, params: checkParams } = buildConditionCheckItems(checks, label);
|
|
832
|
+
const primaryMetadata = MetadataRegistry.get(
|
|
833
|
+
ops[0].entity.modelClass
|
|
834
|
+
);
|
|
835
|
+
const { items: guardItemsRaw, params: guardParams } = buildUniqueGuardItems(
|
|
836
|
+
uniqueGuards,
|
|
837
|
+
primaryMetadata
|
|
838
|
+
);
|
|
839
|
+
const { items: outboxItems, params: outboxParams } = buildOutboxEventItems(
|
|
840
|
+
outboxEvents,
|
|
841
|
+
primaryMetadata
|
|
842
|
+
);
|
|
843
|
+
const { items: idempotencyItems, params: idempotencyParams } = idempotencyGuard !== void 0 ? buildIdempotencyGuardItems(idempotencyGuard, primaryMetadata) : { items: [], params: {} };
|
|
844
|
+
const { items: maintainItems, params: maintainParams } = buildMaintainWriteItems(maintainWrites);
|
|
845
|
+
const { items: maintainOutboxItems, params: maintainOutboxParams } = buildMaintainOutboxItems(maintainOutbox, primaryMetadata);
|
|
846
|
+
const baseKeys = new Set(base.items.map(itemKeySignature));
|
|
847
|
+
for (const update of updateItems) {
|
|
848
|
+
if (baseKeys.has(itemKeySignature(update))) {
|
|
849
|
+
throw new Error(
|
|
850
|
+
`Contract '${contractName}.${methodName}': a derived counter (\`increment\`) targets the same row as the entity write (entity '${update.entity}', key ${JSON.stringify(update.keyCondition ?? {})}) \u2014 a Put+Update on one item is unsupported in a transaction (DynamoDB rejects touching one key twice). A \`w.increment(...)\` must target a DIFFERENT row (e.g. a parent aggregate's counter), not the fragment's own entity row; increment a field on the entity write itself instead of deriving a counter onto it.`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const edgeItems = edgeItemsRaw.filter((item) => {
|
|
855
|
+
const sig = itemKeySignature(item);
|
|
856
|
+
if (baseKeys.has(sig)) return false;
|
|
857
|
+
baseKeys.add(sig);
|
|
858
|
+
return true;
|
|
859
|
+
});
|
|
860
|
+
const guardItems = guardItemsRaw;
|
|
861
|
+
const params = { ...base.params };
|
|
862
|
+
for (const source of [
|
|
863
|
+
edgeParams,
|
|
864
|
+
updateParams,
|
|
865
|
+
checkParams,
|
|
866
|
+
guardParams,
|
|
867
|
+
outboxParams,
|
|
868
|
+
idempotencyParams,
|
|
869
|
+
maintainParams,
|
|
870
|
+
maintainOutboxParams
|
|
871
|
+
]) {
|
|
872
|
+
for (const [name, spec] of Object.entries(source)) params[name] = spec;
|
|
873
|
+
}
|
|
874
|
+
const items = [
|
|
875
|
+
...base.items,
|
|
876
|
+
...edgeItems,
|
|
877
|
+
...updateItems,
|
|
878
|
+
...guardItems,
|
|
879
|
+
...outboxItems,
|
|
880
|
+
...idempotencyItems,
|
|
881
|
+
...maintainItems,
|
|
882
|
+
...maintainOutboxItems,
|
|
883
|
+
...checkItems
|
|
884
|
+
];
|
|
885
|
+
const categories = [
|
|
886
|
+
...base.items.map(() => "base"),
|
|
887
|
+
...edgeItems.map(() => "edge"),
|
|
888
|
+
...updateItems.map(() => "derive"),
|
|
889
|
+
...guardItems.map(() => "unique"),
|
|
890
|
+
...outboxItems.map(() => "outbox"),
|
|
891
|
+
...idempotencyItems.map(() => "idempotency"),
|
|
892
|
+
...maintainItems.map(() => "maintain"),
|
|
893
|
+
...maintainOutboxItems.map(() => "maintainOutbox"),
|
|
894
|
+
...checkItems.map(() => "check")
|
|
895
|
+
];
|
|
896
|
+
if (items.length > MAX_TRANSACT_ITEMS) {
|
|
897
|
+
throw new Error(
|
|
898
|
+
`Contract '${contractName}.${methodName}': a mutation composes ${items.length} items (writes + edges + derived updates + uniqueness guards + outbox events + idempotency guard + ConditionChecks) into one TransactWriteItems, exceeding the DynamoDB limit of ${MAX_TRANSACT_ITEMS}. An atomic transaction cannot be split; reduce the fragment / effect count.`
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
spec: { params: sortedParams(params), items, maxItems: items.length },
|
|
903
|
+
categories
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function serializePreparedWriteFragment(label, fragment) {
|
|
907
|
+
const { spec, categories } = synthesizeMutationTransaction(
|
|
908
|
+
label,
|
|
909
|
+
"plan",
|
|
910
|
+
[fragment.op],
|
|
911
|
+
fragment.conditionChecks ?? [],
|
|
912
|
+
fragment.edgeWrites ?? [],
|
|
913
|
+
fragment.derivedUpdates ?? [],
|
|
914
|
+
fragment.uniqueGuards ?? [],
|
|
915
|
+
fragment.outboxEvents ?? [],
|
|
916
|
+
fragment.idempotencyGuard,
|
|
917
|
+
fragment.maintainWrites ?? [],
|
|
918
|
+
fragment.maintainOutbox ?? []
|
|
919
|
+
);
|
|
920
|
+
return { transaction: spec, categories };
|
|
921
|
+
}
|
|
922
|
+
function modeTargetFor(mode, op, commandSpec, contractName, methodName, opName, transactionSpecs) {
|
|
923
|
+
if (mode === "parallel") {
|
|
924
|
+
return { mode: "parallel", operation: opName };
|
|
925
|
+
}
|
|
926
|
+
const txName = batchTxName(contractName, methodName);
|
|
927
|
+
transactionSpecs[txName] = synthesizeBatchTransaction(commandSpec, op);
|
|
928
|
+
return { mode: "transaction", transaction: txName };
|
|
929
|
+
}
|
|
930
|
+
function buildContracts(contracts = {}) {
|
|
931
|
+
const contractSpecs = {};
|
|
932
|
+
const querySpecs = {};
|
|
933
|
+
const commandSpecs = {};
|
|
934
|
+
const transactionSpecs = {};
|
|
935
|
+
for (const contractName of Object.keys(contracts).sort()) {
|
|
936
|
+
const contract = contracts[contractName];
|
|
937
|
+
let spec;
|
|
938
|
+
if (isQueryModelContract(contract)) {
|
|
939
|
+
spec = serializeQueryContract(contractName, contract, querySpecs, contracts);
|
|
940
|
+
} else if (isCommandModelContract(contract)) {
|
|
941
|
+
spec = serializeCommandContract(
|
|
942
|
+
contractName,
|
|
943
|
+
contract,
|
|
944
|
+
commandSpecs,
|
|
945
|
+
transactionSpecs,
|
|
946
|
+
querySpecs
|
|
947
|
+
);
|
|
948
|
+
} else {
|
|
949
|
+
throw new Error(
|
|
950
|
+
`Contract '${contractName}' is not a publishQuery / publishCommand result. Pass the value returned by publishQuery(...) / publishCommand(...).`
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
assertContractN1Safe(
|
|
954
|
+
contractName,
|
|
955
|
+
spec,
|
|
956
|
+
(methodName) => isGsiPointOp(querySpecs[opRefName(contractName, methodName)])
|
|
957
|
+
);
|
|
958
|
+
contractSpecs[contractName] = spec;
|
|
959
|
+
}
|
|
960
|
+
return {
|
|
961
|
+
contracts: contractSpecs,
|
|
962
|
+
queries: querySpecs,
|
|
963
|
+
commands: commandSpecs,
|
|
964
|
+
transactions: transactionSpecs
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function serializeQueryContract(contractName, contract, querySpecs, contracts) {
|
|
968
|
+
const methods = {};
|
|
969
|
+
const keyFields = /* @__PURE__ */ new Set();
|
|
970
|
+
for (const methodName of Object.keys(contract.methods).sort()) {
|
|
971
|
+
const method = contract.methods[methodName];
|
|
972
|
+
const op = method.op;
|
|
973
|
+
const def = opToDefinition(contractName, methodName, op);
|
|
974
|
+
const querySpec = buildQuerySpec(def);
|
|
975
|
+
const opName = opRefName(contractName, methodName);
|
|
976
|
+
querySpecs[opName] = querySpec;
|
|
977
|
+
for (const f of keyFieldsOf(op)) keyFields.add(f);
|
|
978
|
+
const compose = composeOf(op).map(
|
|
979
|
+
(n) => composeSpecOf(n, contractName, methodName, contracts)
|
|
980
|
+
);
|
|
981
|
+
const compositionPlan = compositionPlanOf(compose);
|
|
982
|
+
methods[methodName] = {
|
|
983
|
+
resolution: method.resolution,
|
|
984
|
+
inputArity: method.inputArity,
|
|
985
|
+
// Per-key cardinality: a point read is at most one item; a range read is a
|
|
986
|
+
// connection. Derived from the #58 facts, not re-computed from storage.
|
|
987
|
+
cardinality: method.resolution === "point" ? "one" : "many",
|
|
988
|
+
operation: opName,
|
|
989
|
+
...compose.length > 0 ? { compose } : {},
|
|
990
|
+
...compositionPlan !== void 0 ? { compositionPlan } : {},
|
|
991
|
+
// Pure-documentation description (issue #154); absent → byte-identical spec.
|
|
992
|
+
...method.description !== void 0 ? { description: method.description } : {}
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
kind: "query",
|
|
997
|
+
key: { fields: sortedKeyFields(keyFields) },
|
|
998
|
+
methods
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function singleWriteTransactionToCommandSpec(contractName, methodName, tx, mode) {
|
|
1002
|
+
const ctx = `Contract '${contractName}.${methodName}'`;
|
|
1003
|
+
if (tx.items.length !== 1) {
|
|
1004
|
+
const cause = mode === "parallel" ? "non-atomic (mode:'parallel')" : "non-atomic (no mode)";
|
|
1005
|
+
throw new Error(
|
|
1006
|
+
`${ctx}: a ${cause} command body must be a single write (got ${tx.items.length} items). Declare mode:'transaction' for an atomic multi-write transaction.`
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
const item = tx.items[0];
|
|
1010
|
+
if (item.type === "ConditionCheck") {
|
|
1011
|
+
throw new Error(
|
|
1012
|
+
`${ctx}: a ConditionCheck only exists inside an atomic transaction; declare mode:'transaction'.`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
if (item.when !== void 0 || item.guard !== void 0) {
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
`${ctx}: a single-write body carrying a per-item guard has no plain non-atomic op form; declare mode:'transaction' (atomic) or 'parallel'.`
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
const stringTemplates = (rec, label) => {
|
|
1021
|
+
if (rec === void 0) return void 0;
|
|
1022
|
+
const out = {};
|
|
1023
|
+
for (const [field, leaf] of Object.entries(rec)) {
|
|
1024
|
+
if (typeof leaf !== "string") {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`${ctx}: the ${label} field '${field}' carries a typed ${typeof leaf} literal, which the non-atomic single-op form does not yet lower; declare an explicit mode (S6 folds this uniformly).`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
out[field] = leaf;
|
|
1030
|
+
}
|
|
1031
|
+
return out;
|
|
1032
|
+
};
|
|
1033
|
+
void mode;
|
|
1034
|
+
const commandType = item.type === "Put" ? "PutItem" : item.type === "Update" ? "UpdateItem" : "DeleteItem";
|
|
1035
|
+
return {
|
|
1036
|
+
type: commandType,
|
|
1037
|
+
tableName: item.tableName,
|
|
1038
|
+
entity: item.entity,
|
|
1039
|
+
params: tx.params,
|
|
1040
|
+
...item.keyCondition !== void 0 ? { keyCondition: item.keyCondition } : {},
|
|
1041
|
+
...stringTemplates(item.item, "item") !== void 0 ? { item: stringTemplates(item.item, "item") } : {},
|
|
1042
|
+
...stringTemplates(item.changes, "changes") !== void 0 ? { changes: stringTemplates(item.changes, "changes") } : {},
|
|
1043
|
+
...item.condition !== void 0 ? { condition: item.condition } : {}
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
function serializeCommandContract(contractName, contract, commandSpecs, transactionSpecs, querySpecs) {
|
|
1047
|
+
const methods = {};
|
|
1048
|
+
const keyFields = /* @__PURE__ */ new Set();
|
|
1049
|
+
for (const methodName of Object.keys(contract.methods).sort()) {
|
|
1050
|
+
const method = contract.methods[methodName];
|
|
1051
|
+
if (method.transaction !== void 0) {
|
|
1052
|
+
const opName2 = opRefName(contractName, methodName);
|
|
1053
|
+
if (method.mode === "transaction") {
|
|
1054
|
+
transactionSpecs[opName2] = method.transaction;
|
|
1055
|
+
methods[methodName] = {
|
|
1056
|
+
inputArity: method.inputArity,
|
|
1057
|
+
result: method.result,
|
|
1058
|
+
single: { mode: "transaction", transaction: opName2 },
|
|
1059
|
+
...method.description !== void 0 ? { description: method.description } : {}
|
|
1060
|
+
};
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
commandSpecs[opName2] = singleWriteTransactionToCommandSpec(
|
|
1064
|
+
contractName,
|
|
1065
|
+
methodName,
|
|
1066
|
+
method.transaction,
|
|
1067
|
+
method.mode
|
|
1068
|
+
);
|
|
1069
|
+
methods[methodName] = {
|
|
1070
|
+
inputArity: method.inputArity,
|
|
1071
|
+
result: method.result,
|
|
1072
|
+
single: { mode: "op", operation: opName2 },
|
|
1073
|
+
...method.description !== void 0 ? { description: method.description } : {}
|
|
1074
|
+
};
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
const op = method.op;
|
|
1078
|
+
if (op === void 0) {
|
|
1079
|
+
throw new Error(
|
|
1080
|
+
`Contract '${contractName}.${methodName}': a command method must carry either a resolved write op or a pre-built transaction (internal invariant).`
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
const def = opToDefinition(contractName, methodName, op);
|
|
1084
|
+
const commandSpec = buildCommandSpec(def);
|
|
1085
|
+
if (commandSpec.condition) assertSupportedCondition(opRefName(contractName, methodName), commandSpec.condition);
|
|
1086
|
+
const opName = opRefName(contractName, methodName);
|
|
1087
|
+
commandSpecs[opName] = commandSpec;
|
|
1088
|
+
const methodKeyFields = keyFieldsOf(op);
|
|
1089
|
+
for (const f of methodKeyFields) keyFields.add(f);
|
|
1090
|
+
const conditionChecks = method.conditionChecks ?? [];
|
|
1091
|
+
const edgeWrites = method.edgeWrites ?? [];
|
|
1092
|
+
const derivedUpdates = method.derivedUpdates ?? [];
|
|
1093
|
+
const uniqueGuards = method.uniqueGuards ?? [];
|
|
1094
|
+
const outboxEvents = method.outboxEvents ?? [];
|
|
1095
|
+
const idempotencyGuard = method.idempotencyGuard;
|
|
1096
|
+
const maintainWrites = method.maintainWrites ?? [];
|
|
1097
|
+
const maintainOutbox = method.maintainOutbox ?? [];
|
|
1098
|
+
const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0 || maintainWrites.length > 0 || maintainOutbox.length > 0;
|
|
1099
|
+
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
1100
|
+
const promotedToTransaction = isMultiFragment || hasDerivedEffects;
|
|
1101
|
+
if (promotedToTransaction) {
|
|
1102
|
+
const txName = opName;
|
|
1103
|
+
const ops = method.ops ?? [op];
|
|
1104
|
+
const tx = hasDerivedEffects ? (
|
|
1105
|
+
// #84/#85/#86/#87: writes + ConditionChecks + edge writes + derived updates +
|
|
1106
|
+
// uniqueness guards + outbox events + idempotency guard, one atomic tx.
|
|
1107
|
+
synthesizeMutationTransaction(
|
|
1108
|
+
contractName,
|
|
1109
|
+
methodName,
|
|
1110
|
+
ops,
|
|
1111
|
+
conditionChecks,
|
|
1112
|
+
edgeWrites,
|
|
1113
|
+
derivedUpdates,
|
|
1114
|
+
uniqueGuards,
|
|
1115
|
+
outboxEvents,
|
|
1116
|
+
idempotencyGuard,
|
|
1117
|
+
maintainWrites,
|
|
1118
|
+
maintainOutbox
|
|
1119
|
+
).spec
|
|
1120
|
+
) : (
|
|
1121
|
+
// #90: writes only.
|
|
1122
|
+
composeFragmentTransaction(contractName, methodName, ops)
|
|
1123
|
+
);
|
|
1124
|
+
if (tx.items.length > MAX_TRANSACT_ITEMS) {
|
|
1125
|
+
throw new Error(
|
|
1126
|
+
`Contract '${contractName}.${methodName}': a mutation composes ${tx.items.length} items (writes + ConditionChecks) into one TransactWriteItems, exceeding the DynamoDB limit of ${MAX_TRANSACT_ITEMS}. An atomic transaction cannot be split; reduce the fragment / requires count.`
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
transactionSpecs[txName] = tx;
|
|
1130
|
+
}
|
|
1131
|
+
const returnSelection = method.returnSelection;
|
|
1132
|
+
if (returnSelection !== void 0 && Object.keys(returnSelection).length > 0) {
|
|
1133
|
+
querySpecs[readbackQueryName(contractName, methodName)] = buildReadbackQuerySpec(
|
|
1134
|
+
contractName,
|
|
1135
|
+
methodName,
|
|
1136
|
+
op,
|
|
1137
|
+
methodKeyFields,
|
|
1138
|
+
returnSelection
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
const batch = method.mode !== void 0 && !promotedToTransaction ? modeTargetFor(
|
|
1142
|
+
method.mode,
|
|
1143
|
+
op,
|
|
1144
|
+
commandSpec,
|
|
1145
|
+
contractName,
|
|
1146
|
+
methodName,
|
|
1147
|
+
opName,
|
|
1148
|
+
transactionSpecs
|
|
1149
|
+
) : void 0;
|
|
1150
|
+
const single = promotedToTransaction ? { mode: "transaction", transaction: opName } : { mode: "op", operation: opName };
|
|
1151
|
+
methods[methodName] = {
|
|
1152
|
+
inputArity: method.inputArity,
|
|
1153
|
+
result: method.result,
|
|
1154
|
+
single,
|
|
1155
|
+
...batch !== void 0 ? { batch } : {},
|
|
1156
|
+
...returnSelection !== void 0 && Object.keys(returnSelection).length > 0 ? { returnSelection: sortedReturnSelection(returnSelection) } : {},
|
|
1157
|
+
// Pure-documentation description (issue #154); absent → byte-identical spec.
|
|
1158
|
+
...method.description !== void 0 ? { description: method.description } : {}
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
kind: "command",
|
|
1163
|
+
key: { fields: sortedKeyFields(keyFields) },
|
|
1164
|
+
methods
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function readbackQueryName(contractName, methodName) {
|
|
1168
|
+
return `${opRefName(contractName, methodName)}__readback`;
|
|
1169
|
+
}
|
|
1170
|
+
function sortedReturnSelection(selection) {
|
|
1171
|
+
const out = {};
|
|
1172
|
+
for (const field of Object.keys(selection).sort()) {
|
|
1173
|
+
if (selection[field] === true) out[field] = true;
|
|
1174
|
+
}
|
|
1175
|
+
return out;
|
|
1176
|
+
}
|
|
1177
|
+
function buildReadbackQuerySpec(contractName, methodName, op, keyFields, returnSelection) {
|
|
1178
|
+
if (keyFields.length === 0) {
|
|
1179
|
+
throw new Error(
|
|
1180
|
+
`Contract '${contractName}.${methodName}': a return projection needs the written entity's primary key to read it back, but no contract Key fields were resolved. The target model must declare a primary key.`
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
const metadata = MetadataRegistry.get(
|
|
1184
|
+
op.entity.modelClass
|
|
1185
|
+
);
|
|
1186
|
+
const params = {};
|
|
1187
|
+
const key = {};
|
|
1188
|
+
for (const field of keyFields) {
|
|
1189
|
+
const recovered = recoverKind(metadata, field);
|
|
1190
|
+
params[field] = recovered.literals === void 0 ? { kind: recovered.kind, required: true } : { kind: recovered.kind, literals: recovered.literals, required: true };
|
|
1191
|
+
key[field] = paramOfKind(recovered);
|
|
1192
|
+
}
|
|
1193
|
+
const select = {};
|
|
1194
|
+
for (const f of Object.keys(returnSelection).sort()) {
|
|
1195
|
+
if (returnSelection[f] === true) select[f] = true;
|
|
1196
|
+
}
|
|
1197
|
+
const def = {
|
|
1198
|
+
__isOperationDefinition: true,
|
|
1199
|
+
entity: op.entity,
|
|
1200
|
+
operation: "query",
|
|
1201
|
+
key,
|
|
1202
|
+
select,
|
|
1203
|
+
params
|
|
1204
|
+
};
|
|
1205
|
+
return buildQuerySpec(def);
|
|
1206
|
+
}
|
|
1207
|
+
function keyFieldsOf(op) {
|
|
1208
|
+
if (op.operation === "put") {
|
|
1209
|
+
const fields2 = [];
|
|
1210
|
+
for (const leaf of Object.values(op.item ?? {})) {
|
|
1211
|
+
if (isContractKeyFieldRef(leaf)) fields2.push(leaf.field);
|
|
1212
|
+
}
|
|
1213
|
+
return fields2;
|
|
1214
|
+
}
|
|
1215
|
+
if (isContractKeyRef(op.keys)) {
|
|
1216
|
+
if (op.keyFields !== void 0 && op.keyFields.length > 0) return op.keyFields;
|
|
1217
|
+
const metadata = MetadataRegistry.get(
|
|
1218
|
+
op.entity.modelClass
|
|
1219
|
+
);
|
|
1220
|
+
return metadata.primaryKey ? metadata.primaryKey.inputFieldNames : [];
|
|
1221
|
+
}
|
|
1222
|
+
const fields = [];
|
|
1223
|
+
for (const leaf of Object.values(op.keys)) {
|
|
1224
|
+
if (isContractKeyFieldRef(leaf)) fields.push(leaf.field);
|
|
1225
|
+
}
|
|
1226
|
+
return fields;
|
|
1227
|
+
}
|
|
1228
|
+
function sortedKeyFields(fields) {
|
|
1229
|
+
return [...fields].sort();
|
|
1230
|
+
}
|
|
1231
|
+
function isGsiPointOp(querySpec) {
|
|
1232
|
+
if (querySpec === void 0) return false;
|
|
1233
|
+
const root = querySpec.operations[0];
|
|
1234
|
+
return root !== void 0 && root.indexName !== void 0;
|
|
1235
|
+
}
|
|
1236
|
+
function buildContexts(contexts = {}) {
|
|
1237
|
+
const out = {};
|
|
1238
|
+
for (const name of Object.keys(contexts).sort()) {
|
|
1239
|
+
const c = contexts[name];
|
|
1240
|
+
out[name] = {
|
|
1241
|
+
models: [...c.models ?? []].sort(),
|
|
1242
|
+
contracts: [...c.contracts ?? []].sort()
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
return out;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/spec/contract-boundary-check.ts
|
|
1249
|
+
function buildOwnershipIndex(contexts) {
|
|
1250
|
+
const modelOwner = /* @__PURE__ */ new Map();
|
|
1251
|
+
const contractOwner = /* @__PURE__ */ new Map();
|
|
1252
|
+
const claim = (owner, kind, name, context) => {
|
|
1253
|
+
const existing = owner.get(name);
|
|
1254
|
+
if (existing !== void 0 && existing !== context) {
|
|
1255
|
+
throw new Error(
|
|
1256
|
+
`Context-ownership declaration is ambiguous: ${kind} '${name}' is claimed by both context '${existing}' and context '${context}'. A ${kind} belongs to exactly one bounded context \u2014 remove it from one of the \`${kind === "Model" ? "models" : "contracts"}\` lists.`
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
owner.set(name, context);
|
|
1260
|
+
};
|
|
1261
|
+
for (const context of Object.keys(contexts).sort()) {
|
|
1262
|
+
const membership = contexts[context];
|
|
1263
|
+
for (const model of membership.models ?? []) claim(modelOwner, "Model", model, context);
|
|
1264
|
+
for (const contract of membership.contracts ?? [])
|
|
1265
|
+
claim(contractOwner, "Contract", contract, context);
|
|
1266
|
+
}
|
|
1267
|
+
return { modelOwner, contractOwner };
|
|
1268
|
+
}
|
|
1269
|
+
function methodOpsOf(contract) {
|
|
1270
|
+
if (isQueryModelContract(contract) || isCommandModelContract(contract)) {
|
|
1271
|
+
return Object.keys(contract.methods).sort().flatMap((name) => {
|
|
1272
|
+
const op = contract.methods[name].op;
|
|
1273
|
+
return op !== void 0 ? [[name, op]] : [];
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
return [];
|
|
1277
|
+
}
|
|
1278
|
+
function collectContractBoundaryViolations(contracts, contexts) {
|
|
1279
|
+
const violations = [];
|
|
1280
|
+
const { modelOwner, contractOwner } = buildOwnershipIndex(contexts);
|
|
1281
|
+
for (const contractName of Object.keys(contracts).sort()) {
|
|
1282
|
+
const contractContext = contractOwner.get(contractName);
|
|
1283
|
+
if (contractContext === void 0) continue;
|
|
1284
|
+
const contract = contracts[contractName];
|
|
1285
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1286
|
+
for (const [method, op] of methodOpsOf(contract)) {
|
|
1287
|
+
const model = op.entity.name;
|
|
1288
|
+
const foreignContext = modelOwner.get(model);
|
|
1289
|
+
if (foreignContext === void 0) continue;
|
|
1290
|
+
if (foreignContext === contractContext) continue;
|
|
1291
|
+
const dedupeKey = `${method}\0${model}`;
|
|
1292
|
+
if (seen.has(dedupeKey)) continue;
|
|
1293
|
+
seen.add(dedupeKey);
|
|
1294
|
+
violations.push({
|
|
1295
|
+
contract: contractName,
|
|
1296
|
+
contractContext,
|
|
1297
|
+
method,
|
|
1298
|
+
foreignModel: model,
|
|
1299
|
+
foreignContext,
|
|
1300
|
+
message: `Contract '${contractName}.${method}' (context '${contractContext}') directly references Model '${model}', which is private to context '${foreignContext}'. A Model is the private implementation of its bounded context; crossing a context boundary is allowed only through the other context's published Contract (an External Query / Command), never its Models. Depend on context '${foreignContext}''s published Contract for '${model}' instead of reaching into the Model directly.`
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return violations;
|
|
1305
|
+
}
|
|
1306
|
+
function assertContractBoundaries(contracts, contexts) {
|
|
1307
|
+
const violations = collectContractBoundaryViolations(contracts, contexts);
|
|
1308
|
+
if (violations.length === 0) return;
|
|
1309
|
+
const [first, ...rest] = violations;
|
|
1310
|
+
const suffix = rest.length > 0 ? `
|
|
1311
|
+
|
|
1312
|
+
(${rest.length} further context-boundary violation${rest.length === 1 ? "" : "s"}:
|
|
1313
|
+
${rest.map((v) => ` - ${v.message}`).join("\n")})` : "";
|
|
1314
|
+
throw new Error(first.message + suffix);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// src/spec/operations.ts
|
|
1318
|
+
function paramSpecs(params) {
|
|
1319
|
+
const out = {};
|
|
1320
|
+
for (const name of Object.keys(params).sort()) {
|
|
1321
|
+
const d = params[name];
|
|
1322
|
+
out[name] = {
|
|
1323
|
+
type: d.kind,
|
|
1324
|
+
required: d.required,
|
|
1325
|
+
...d.literals !== void 0 ? { literals: d.literals } : {},
|
|
1326
|
+
// Pure-documentation description (issue #154); absent → byte-identical spec.
|
|
1327
|
+
...d.description !== void 0 ? { description: d.description } : {}
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
return out;
|
|
1331
|
+
}
|
|
1332
|
+
function metaFor(def) {
|
|
1333
|
+
return MetadataRegistry.get(def.entity.modelClass);
|
|
1334
|
+
}
|
|
1335
|
+
function templateLeaf(key, value) {
|
|
1336
|
+
if (isContractParamRef(value)) return value.token;
|
|
1337
|
+
if (isContractKeyFieldRef(value)) return `{${value.field}}`;
|
|
1338
|
+
if (isParam(value)) return `{${key}}`;
|
|
1339
|
+
if (value instanceof Date) return value.toISOString();
|
|
1340
|
+
return String(value);
|
|
1341
|
+
}
|
|
1342
|
+
function keyFieldNames(key) {
|
|
1343
|
+
if (key === null || typeof key !== "object") return [];
|
|
1344
|
+
return Object.keys(key);
|
|
1345
|
+
}
|
|
1346
|
+
function buildReadOperations(def) {
|
|
1347
|
+
const metadata = metaFor(def);
|
|
1348
|
+
const fields = keyFieldNames(def.key);
|
|
1349
|
+
const resolved = resolveKey(fields, metadata);
|
|
1350
|
+
const select = def.select ?? {};
|
|
1351
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
1352
|
+
const present = new Set(fields);
|
|
1353
|
+
let type;
|
|
1354
|
+
let indexName;
|
|
1355
|
+
let keyCondition = {};
|
|
1356
|
+
let rangeCondition;
|
|
1357
|
+
if (resolved.type === "pk") {
|
|
1358
|
+
if (!metadata.primaryKey) throw new Error("Primary key not defined");
|
|
1359
|
+
const { pk, sk } = evaluateKey(
|
|
1360
|
+
metadata.primaryKey.segmented,
|
|
1361
|
+
"param",
|
|
1362
|
+
present,
|
|
1363
|
+
void 0,
|
|
1364
|
+
resolved.partial
|
|
1365
|
+
);
|
|
1366
|
+
if (resolved.partial) {
|
|
1367
|
+
type = "Query";
|
|
1368
|
+
keyCondition = { PK: pk };
|
|
1369
|
+
if (sk !== void 0 && sk !== "") {
|
|
1370
|
+
rangeCondition = { operator: "begins_with", key: "SK", value: sk };
|
|
1371
|
+
}
|
|
1372
|
+
} else if (sk !== void 0) {
|
|
1373
|
+
type = "GetItem";
|
|
1374
|
+
keyCondition = { PK: pk, SK: sk };
|
|
1375
|
+
} else {
|
|
1376
|
+
type = "Query";
|
|
1377
|
+
keyCondition = { PK: pk };
|
|
1378
|
+
}
|
|
1379
|
+
} else {
|
|
1380
|
+
const gsi = metadata.gsiDefinitions.find(
|
|
1381
|
+
(g) => g.indexName === resolved.indexName
|
|
1382
|
+
);
|
|
1383
|
+
if (!gsi) throw new Error(`GSI '${resolved.indexName}' not found`);
|
|
1384
|
+
const { pk, sk } = evaluateKey(
|
|
1385
|
+
gsi.segmented,
|
|
1386
|
+
"param",
|
|
1387
|
+
present,
|
|
1388
|
+
void 0,
|
|
1389
|
+
resolved.partial
|
|
1390
|
+
);
|
|
1391
|
+
type = "Query";
|
|
1392
|
+
indexName = gsi.indexName;
|
|
1393
|
+
keyCondition = { [`${gsi.indexName}PK`]: pk };
|
|
1394
|
+
if (resolved.partial) {
|
|
1395
|
+
if (sk !== void 0 && sk !== "") {
|
|
1396
|
+
rangeCondition = {
|
|
1397
|
+
operator: "begins_with",
|
|
1398
|
+
key: `${gsi.indexName}SK`,
|
|
1399
|
+
value: sk
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
} else if (sk !== void 0 && sk !== "") {
|
|
1403
|
+
keyCondition[`${gsi.indexName}SK`] = sk;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const projection = projectionFields(select);
|
|
1407
|
+
const root = {
|
|
1408
|
+
type,
|
|
1409
|
+
tableName,
|
|
1410
|
+
...indexName ? { indexName } : {},
|
|
1411
|
+
keyCondition,
|
|
1412
|
+
...rangeCondition ? { rangeCondition } : {},
|
|
1413
|
+
projection,
|
|
1414
|
+
resultPath: "$"
|
|
1415
|
+
};
|
|
1416
|
+
const ops = [root];
|
|
1417
|
+
ops.push(...buildRelationOperations(metadata, select, "$"));
|
|
1418
|
+
return injectRefsSourceProjections(ops);
|
|
1419
|
+
}
|
|
1420
|
+
function projectionFields(select) {
|
|
1421
|
+
const out = [];
|
|
1422
|
+
for (const [field, value] of Object.entries(select)) {
|
|
1423
|
+
if (value === true || isInlineSnapshotSpec(value)) out.push(field);
|
|
1424
|
+
}
|
|
1425
|
+
return out.sort();
|
|
1426
|
+
}
|
|
1427
|
+
function buildRelationOperations(metadata, select, parentPath) {
|
|
1428
|
+
const ops = [];
|
|
1429
|
+
const relations = detectRelationFields(select, metadata);
|
|
1430
|
+
for (const rel of [...relations].sort(
|
|
1431
|
+
(a, b) => a.propertyName.localeCompare(b.propertyName)
|
|
1432
|
+
)) {
|
|
1433
|
+
const spec = normalizeSelectSpec(select[rel.propertyName]);
|
|
1434
|
+
const childSelect = spec.select ?? {};
|
|
1435
|
+
const targetClass = rel.targetFactory();
|
|
1436
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
1437
|
+
const childPath = `${parentPath}.${rel.propertyName}`;
|
|
1438
|
+
if (rel.type === "refs") {
|
|
1439
|
+
if (spec.filter !== void 0) {
|
|
1440
|
+
throw new Error(
|
|
1441
|
+
`Relation '${rel.propertyName}' (refs) declares a \`filter\`, which the static operations spec cannot apply (a BatchGetItem has no server-side FilterExpression and the spec runtimes run no client-side filter stage). Remove the filter from selects compiled to operations.json / generated Python, or filter in the consumer.`
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
ops.push(
|
|
1445
|
+
buildRefsOperation(rel, targetMeta, childSelect, `${childPath}.items`)
|
|
1446
|
+
);
|
|
1447
|
+
ops.push(...buildRelationOperations(targetMeta, childSelect, `${childPath}.items`));
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (rel.type === "hasMany") {
|
|
1451
|
+
const limit = spec.limit ?? rel.options?.limit?.default ?? 20;
|
|
1452
|
+
ops.push(
|
|
1453
|
+
buildHasManyOperation(rel, targetMeta, childSelect, limit, spec.filter, `${childPath}.items`)
|
|
1454
|
+
);
|
|
1455
|
+
ops.push(...buildRelationOperations(targetMeta, childSelect, `${childPath}.items`));
|
|
1456
|
+
} else {
|
|
1457
|
+
ops.push(
|
|
1458
|
+
buildBatchGetOperation(rel, targetMeta, childSelect, childPath)
|
|
1459
|
+
);
|
|
1460
|
+
ops.push(...buildRelationOperations(targetMeta, childSelect, childPath));
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return ops;
|
|
1464
|
+
}
|
|
1465
|
+
function singleSourceField(rel) {
|
|
1466
|
+
const entries = Object.entries(rel.keyBinding);
|
|
1467
|
+
if (entries.length !== 1) {
|
|
1468
|
+
throw new Error(
|
|
1469
|
+
`Relation '${rel.propertyName}' binds ${entries.length} fields; the static planner currently supports single-field relation key bindings only.`
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
const [targetField, sourceField] = entries[0];
|
|
1473
|
+
return { targetField, sourceField };
|
|
1474
|
+
}
|
|
1475
|
+
function buildHasManyOperation(rel, targetMeta, select, limit, filter, resultPath) {
|
|
1476
|
+
const { targetField, sourceField } = singleSourceField(rel);
|
|
1477
|
+
const resolved = resolveKey([targetField], targetMeta);
|
|
1478
|
+
const tableName = TableMapping.resolve(targetMeta.tableName);
|
|
1479
|
+
const nameOf = (f) => f === targetField ? sourceField : f;
|
|
1480
|
+
const present = /* @__PURE__ */ new Set([targetField]);
|
|
1481
|
+
const op = (() => {
|
|
1482
|
+
if (resolved.type === "pk") {
|
|
1483
|
+
if (!targetMeta.primaryKey) throw new Error("Primary key not defined");
|
|
1484
|
+
const { pk: pk2, sk: sk2 } = evaluateKey(
|
|
1485
|
+
targetMeta.primaryKey.segmented,
|
|
1486
|
+
"result",
|
|
1487
|
+
present,
|
|
1488
|
+
nameOf,
|
|
1489
|
+
resolved.partial
|
|
1490
|
+
);
|
|
1491
|
+
const keyCondition2 = {
|
|
1492
|
+
PK: pk2
|
|
1493
|
+
};
|
|
1494
|
+
const range2 = sk2 !== void 0 && sk2 !== "" ? { operator: "begins_with", key: "SK", value: sk2 } : void 0;
|
|
1495
|
+
return {
|
|
1496
|
+
type: "Query",
|
|
1497
|
+
tableName,
|
|
1498
|
+
keyCondition: keyCondition2,
|
|
1499
|
+
...range2 ? { rangeCondition: range2 } : {},
|
|
1500
|
+
projection: projectionFields(select),
|
|
1501
|
+
limit,
|
|
1502
|
+
resultPath,
|
|
1503
|
+
sourceField
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
const gsi = targetMeta.gsiDefinitions.find((g) => g.indexName === resolved.indexName);
|
|
1507
|
+
const { pk, sk } = evaluateKey(
|
|
1508
|
+
gsi.segmented,
|
|
1509
|
+
"result",
|
|
1510
|
+
present,
|
|
1511
|
+
nameOf,
|
|
1512
|
+
resolved.partial
|
|
1513
|
+
);
|
|
1514
|
+
const keyCondition = { [`${gsi.indexName}PK`]: pk };
|
|
1515
|
+
const range = sk !== void 0 && sk !== "" ? { operator: "begins_with", key: `${gsi.indexName}SK`, value: sk } : void 0;
|
|
1516
|
+
return {
|
|
1517
|
+
type: "Query",
|
|
1518
|
+
tableName,
|
|
1519
|
+
indexName: gsi.indexName,
|
|
1520
|
+
keyCondition,
|
|
1521
|
+
...range ? { rangeCondition: range } : {},
|
|
1522
|
+
projection: projectionFields(select),
|
|
1523
|
+
limit,
|
|
1524
|
+
resultPath,
|
|
1525
|
+
sourceField
|
|
1526
|
+
};
|
|
1527
|
+
})();
|
|
1528
|
+
return filter ? { ...op, filter: filterSpec(filter) } : op;
|
|
1529
|
+
}
|
|
1530
|
+
function buildBatchGetOperation(rel, targetMeta, select, resultPath) {
|
|
1531
|
+
const { targetField, sourceField } = singleSourceField(rel);
|
|
1532
|
+
const resolved = resolveKey([targetField], targetMeta);
|
|
1533
|
+
if (resolved.type !== "pk" || resolved.partial) {
|
|
1534
|
+
throw new Error(
|
|
1535
|
+
`Relation '${rel.propertyName}' (${rel.type}) must resolve to a full primary key (BatchGetItem); got ${resolved.type}${resolved.partial ? " (partial)" : ""}.`
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
if (!targetMeta.primaryKey) throw new Error("Primary key not defined");
|
|
1539
|
+
const nameOf = (f) => f === targetField ? sourceField : f;
|
|
1540
|
+
const { pk, sk } = evaluateKey(
|
|
1541
|
+
targetMeta.primaryKey.segmented,
|
|
1542
|
+
"result",
|
|
1543
|
+
/* @__PURE__ */ new Set([targetField]),
|
|
1544
|
+
nameOf
|
|
1545
|
+
);
|
|
1546
|
+
const keyCondition = {
|
|
1547
|
+
PK: pk
|
|
1548
|
+
};
|
|
1549
|
+
if (sk !== void 0) keyCondition.SK = sk;
|
|
1550
|
+
return {
|
|
1551
|
+
type: "BatchGetItem",
|
|
1552
|
+
tableName: TableMapping.resolve(targetMeta.tableName),
|
|
1553
|
+
keyCondition,
|
|
1554
|
+
projection: projectionFields(select),
|
|
1555
|
+
resultPath,
|
|
1556
|
+
sourceField
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
function buildRefsOperation(rel, targetMeta, select, resultPath) {
|
|
1560
|
+
const refs = rel.refs;
|
|
1561
|
+
if (!refs) {
|
|
1562
|
+
throw new Error(
|
|
1563
|
+
`Relation '${rel.propertyName}' is type 'refs' but carries no refs binding.`
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
const { targetField, sourceField } = singleSourceField(rel);
|
|
1567
|
+
const resolved = resolveKey([targetField], targetMeta);
|
|
1568
|
+
if (resolved.type !== "pk" || resolved.partial) {
|
|
1569
|
+
throw new Error(
|
|
1570
|
+
`Relation '${rel.propertyName}' (refs) must resolve to a full primary key (BatchGetItem); got ${resolved.type}${resolved.partial ? " (partial)" : ""}.`
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
if (!targetMeta.primaryKey) throw new Error("Primary key not defined");
|
|
1574
|
+
const nameOf = (f) => f === targetField ? sourceField : f;
|
|
1575
|
+
const { pk, sk } = evaluateKey(
|
|
1576
|
+
targetMeta.primaryKey.segmented,
|
|
1577
|
+
"result",
|
|
1578
|
+
/* @__PURE__ */ new Set([targetField]),
|
|
1579
|
+
nameOf
|
|
1580
|
+
);
|
|
1581
|
+
const keyCondition = { PK: pk };
|
|
1582
|
+
if (sk !== void 0) keyCondition.SK = sk;
|
|
1583
|
+
return {
|
|
1584
|
+
type: "BatchGetItem",
|
|
1585
|
+
tableName: TableMapping.resolve(targetMeta.tableName),
|
|
1586
|
+
keyCondition,
|
|
1587
|
+
projection: projectionFields(select),
|
|
1588
|
+
resultPath,
|
|
1589
|
+
sourceField,
|
|
1590
|
+
sourceList: { from: refs.from, key: refs.key }
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
function injectRefsSourceProjections(ops) {
|
|
1594
|
+
for (let i = 0; i < ops.length; i++) {
|
|
1595
|
+
const op = ops[i];
|
|
1596
|
+
if (op.sourceList === void 0) continue;
|
|
1597
|
+
const tokens = op.resultPath.split(".");
|
|
1598
|
+
const parentPath = tokens.slice(0, -2).join(".") || "$";
|
|
1599
|
+
const parentIndex = ops.findIndex((p) => p.resultPath === parentPath);
|
|
1600
|
+
if (parentIndex === -1) {
|
|
1601
|
+
throw new Error(
|
|
1602
|
+
`refs fan-out at '${op.resultPath}': no parent operation at '${parentPath}'.`
|
|
1603
|
+
);
|
|
1604
|
+
}
|
|
1605
|
+
const parent = ops[parentIndex];
|
|
1606
|
+
const from = op.sourceList.from;
|
|
1607
|
+
if (parent.projection.includes(from)) {
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
ops[parentIndex] = {
|
|
1611
|
+
...parent,
|
|
1612
|
+
projection: [...parent.projection, from].sort()
|
|
1613
|
+
};
|
|
1614
|
+
ops[i] = { ...op, sourceList: { ...op.sourceList, implicit: true } };
|
|
1615
|
+
}
|
|
1616
|
+
return ops;
|
|
1617
|
+
}
|
|
1618
|
+
function filterSpec(filter) {
|
|
1619
|
+
return { declarative: filter };
|
|
1620
|
+
}
|
|
1621
|
+
function buildCommandSpec(def) {
|
|
1622
|
+
const metadata = metaFor(def);
|
|
1623
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
1624
|
+
const params = paramSpecs(def.params);
|
|
1625
|
+
const entity = def.entity.name;
|
|
1626
|
+
const condition = extractCondition(def);
|
|
1627
|
+
const description = def.description !== void 0 ? { description: def.description } : {};
|
|
1628
|
+
if (def.operation === "put") {
|
|
1629
|
+
const item = templateRecord(def.key);
|
|
1630
|
+
return {
|
|
1631
|
+
type: "PutItem",
|
|
1632
|
+
tableName,
|
|
1633
|
+
entity,
|
|
1634
|
+
params,
|
|
1635
|
+
item,
|
|
1636
|
+
...condition ? { condition } : {},
|
|
1637
|
+
...description
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
if (def.operation === "delete") {
|
|
1641
|
+
return {
|
|
1642
|
+
type: "DeleteItem",
|
|
1643
|
+
tableName,
|
|
1644
|
+
entity,
|
|
1645
|
+
params,
|
|
1646
|
+
keyCondition: writeKeyCondition(metadata, def.key, entity),
|
|
1647
|
+
...condition ? { condition } : {},
|
|
1648
|
+
...description
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
return {
|
|
1652
|
+
type: "UpdateItem",
|
|
1653
|
+
tableName,
|
|
1654
|
+
entity,
|
|
1655
|
+
params,
|
|
1656
|
+
keyCondition: writeKeyCondition(metadata, def.key, entity),
|
|
1657
|
+
changes: templateRecord(def.changes),
|
|
1658
|
+
...condition ? { condition } : {},
|
|
1659
|
+
...description
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
function writeKeyCondition(metadata, key, entity) {
|
|
1663
|
+
if (!metadata.primaryKey) throw new Error("Primary key not defined");
|
|
1664
|
+
const fields = Object.keys(key);
|
|
1665
|
+
const resolved = resolveKey(fields, metadata);
|
|
1666
|
+
if (resolved.type === "gsi") {
|
|
1667
|
+
throw new Error(
|
|
1668
|
+
`Command on '${entity}': the contract Key fields ${JSON.stringify(fields)} resolve to the GSI '${resolved.indexName}', but a DynamoDB write (\`UpdateItem\` / \`DeleteItem\`) can target an item only by its base-table primary key \u2014 there is no GSI-keyed write. A command keyed by a secondary index is not expressible. Key the command by the model's primary key (e.g. read the item by its GSI first, then write by the resolved primary key), or model the write against the base-table PK Key fields.`
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
const present = new Set(fields);
|
|
1672
|
+
const { pk, sk } = evaluateKey(
|
|
1673
|
+
metadata.primaryKey.segmented,
|
|
1674
|
+
"param",
|
|
1675
|
+
present
|
|
1676
|
+
);
|
|
1677
|
+
const out = { PK: pk };
|
|
1678
|
+
if (sk !== void 0) out.SK = sk;
|
|
1679
|
+
return out;
|
|
1680
|
+
}
|
|
1681
|
+
function templateRecord(structure) {
|
|
1682
|
+
const out = {};
|
|
1683
|
+
for (const key of Object.keys(structure).sort()) {
|
|
1684
|
+
out[key] = templateLeaf(key, structure[key]);
|
|
1685
|
+
}
|
|
1686
|
+
return out;
|
|
1687
|
+
}
|
|
1688
|
+
function extractCondition(def) {
|
|
1689
|
+
return conditionInputToSpec(
|
|
1690
|
+
def.condition,
|
|
1691
|
+
`Command on '${def.entity.name}'`,
|
|
1692
|
+
(field, value) => templateLeaf(field, value),
|
|
1693
|
+
(value, name) => conditionTreeLeaf(value, name),
|
|
1694
|
+
// A raw `cond` value slot (issue #114-B): a contract param / key ref carries
|
|
1695
|
+
// its own name; a plain `param.*` falls through to the positional default.
|
|
1696
|
+
(part) => {
|
|
1697
|
+
if (isContractParamRef(part)) return { $param: tokenName(part.token) };
|
|
1698
|
+
if (isContractKeyFieldRef(part)) return { $param: part.field };
|
|
1699
|
+
return void 0;
|
|
1700
|
+
}
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
function conditionTreeLeaf(value, name) {
|
|
1704
|
+
if (isContractParamRef(value)) return { $param: tokenName(value.token) };
|
|
1705
|
+
if (isContractKeyFieldRef(value)) return { $param: value.field };
|
|
1706
|
+
if (isParam(value)) return { $param: name };
|
|
1707
|
+
if (value instanceof Date) return value.toISOString();
|
|
1708
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1709
|
+
return value;
|
|
1710
|
+
}
|
|
1711
|
+
throw new Error(
|
|
1712
|
+
`A declarative write-condition operand must be a concrete literal or a param reference; got ${typeof value}.`
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
function tokenName(token) {
|
|
1716
|
+
return token.startsWith("{") && token.endsWith("}") ? token.slice(1, -1) : token;
|
|
1717
|
+
}
|
|
1718
|
+
function buildQuerySpec(def) {
|
|
1719
|
+
const operations = buildReadOperations(def);
|
|
1720
|
+
const executionPlan = deriveExecutionPlan(operations.map((op) => op.resultPath));
|
|
1721
|
+
return {
|
|
1722
|
+
params: paramSpecs(def.params),
|
|
1723
|
+
operations,
|
|
1724
|
+
// operation 'query' → a single entity object; 'list' → a connection.
|
|
1725
|
+
cardinality: def.operation === "query" ? "one" : "many",
|
|
1726
|
+
...executionPlan !== void 0 ? { executionPlan } : {},
|
|
1727
|
+
// Pure-documentation description (issue #154); absent → byte-identical spec.
|
|
1728
|
+
...def.description !== void 0 ? { description: def.description } : {}
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
function mergeSpecs(base, synthesized, kind) {
|
|
1732
|
+
for (const name of Object.keys(synthesized)) {
|
|
1733
|
+
if (Object.prototype.hasOwnProperty.call(base, name)) {
|
|
1734
|
+
throw new Error(
|
|
1735
|
+
`Contract serialization produced ${kind} '${name}', which collides with a hand-written definition of the same name. Contract method ops are named '<Contract>__<method>'; rename the conflicting definition.`
|
|
1736
|
+
);
|
|
1737
|
+
}
|
|
1738
|
+
base[name] = synthesized[name];
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
function buildOperations(queries = {}, commands = {}, transactions = {}, contractInputs = {}) {
|
|
1742
|
+
const querySpecs = {};
|
|
1743
|
+
for (const name of Object.keys(queries).sort()) {
|
|
1744
|
+
querySpecs[name] = buildQuerySpec(queries[name]);
|
|
1745
|
+
}
|
|
1746
|
+
const commandSpecs = {};
|
|
1747
|
+
for (const name of Object.keys(commands).sort()) {
|
|
1748
|
+
const spec = buildCommandSpec(commands[name]);
|
|
1749
|
+
if (spec.condition) assertSupportedCondition(name, spec.condition);
|
|
1750
|
+
commandSpecs[name] = spec;
|
|
1751
|
+
}
|
|
1752
|
+
const transactionSpecs = buildTransactions(transactions);
|
|
1753
|
+
const contractInputMap = contractInputs.contracts ?? {};
|
|
1754
|
+
const contextInputMap = contractInputs.contexts ?? {};
|
|
1755
|
+
assertContractBoundaries(contractInputMap, contextInputMap);
|
|
1756
|
+
const built = buildContracts(contractInputMap);
|
|
1757
|
+
mergeSpecs(querySpecs, built.queries, "query");
|
|
1758
|
+
mergeSpecs(commandSpecs, built.commands, "command");
|
|
1759
|
+
mergeSpecs(transactionSpecs, built.transactions, "transaction");
|
|
1760
|
+
const contractSpecs = built.contracts;
|
|
1761
|
+
const contextSpecs = buildContexts(contextInputMap);
|
|
1762
|
+
const content = {
|
|
1763
|
+
queries: querySpecs,
|
|
1764
|
+
commands: commandSpecs,
|
|
1765
|
+
...Object.keys(transactionSpecs).length > 0 ? { transactions: transactionSpecs } : {},
|
|
1766
|
+
...Object.keys(contractSpecs).length > 0 ? { contracts: contractSpecs } : {},
|
|
1767
|
+
...Object.keys(contextSpecs).length > 0 ? { contexts: contextSpecs } : {}
|
|
1768
|
+
};
|
|
1769
|
+
return {
|
|
1770
|
+
version: operationsSpecVersion(content),
|
|
1771
|
+
...content
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// src/spec/prepared.ts
|
|
1776
|
+
function bindOfSlot(slot, where) {
|
|
1777
|
+
if (slot.kind === "param") return { param: slot.name };
|
|
1778
|
+
const v = slot.value;
|
|
1779
|
+
if (v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
1780
|
+
return { literal: v };
|
|
1781
|
+
}
|
|
1782
|
+
if (v instanceof Date) return { literalDate: v.toISOString() };
|
|
1783
|
+
if (typeof v === "bigint") return { literalBigInt: v.toString() };
|
|
1784
|
+
throw new Error(
|
|
1785
|
+
`prepared AOT: ${where} carries a non-serializable literal (${typeof v}).`
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
function bindMapOf(slots, where) {
|
|
1789
|
+
const out = {};
|
|
1790
|
+
for (const [field, slot] of Object.entries(slots)) {
|
|
1791
|
+
out[field] = bindOfSlot(slot, `${where} field '${field}'`);
|
|
1792
|
+
}
|
|
1793
|
+
return out;
|
|
1794
|
+
}
|
|
1795
|
+
function paramKindForField(metadata, field) {
|
|
1796
|
+
const f = metadata.fields.find((x) => x.propertyName === field);
|
|
1797
|
+
return f?.dynamoType === "N" ? "number" : "string";
|
|
1798
|
+
}
|
|
1799
|
+
function addParam(params, name, kind) {
|
|
1800
|
+
if (!(name in params)) params[name] = { type: kind, required: true };
|
|
1801
|
+
}
|
|
1802
|
+
function readRouteQuerySpec(route, metadata) {
|
|
1803
|
+
const params = {};
|
|
1804
|
+
const key = {};
|
|
1805
|
+
for (const field of Object.keys(route.keySlots)) {
|
|
1806
|
+
const kind = paramKindForField(metadata, field);
|
|
1807
|
+
params[field] = { kind, required: true };
|
|
1808
|
+
key[field] = kind === "number" ? param.number() : param.string();
|
|
1809
|
+
}
|
|
1810
|
+
const def = {
|
|
1811
|
+
__isOperationDefinition: true,
|
|
1812
|
+
entity: { name: route.modelClass.name, modelClass: route.modelClass },
|
|
1813
|
+
operation: route.kind === "query" ? "query" : "list",
|
|
1814
|
+
key,
|
|
1815
|
+
select: route.select,
|
|
1816
|
+
params
|
|
1817
|
+
};
|
|
1818
|
+
return buildQuerySpec(def);
|
|
1819
|
+
}
|
|
1820
|
+
function compileReadPlan(routes) {
|
|
1821
|
+
const specs = [];
|
|
1822
|
+
const params = {};
|
|
1823
|
+
const entities = {};
|
|
1824
|
+
for (const { alias, route } of routes) {
|
|
1825
|
+
const compiled = compileReadRoute(alias, route);
|
|
1826
|
+
const metadata = MetadataRegistry.get(compiled.modelClass);
|
|
1827
|
+
const entityName = compiled.modelClass.name;
|
|
1828
|
+
entities[entityName] = entityFingerprint(metadata);
|
|
1829
|
+
for (const [field, slot] of Object.entries(compiled.keySlots)) {
|
|
1830
|
+
if (slot.kind === "param") {
|
|
1831
|
+
addParam(params, slot.name, paramKindForField(metadata, field));
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
const dynamicParam = (slot, kind) => {
|
|
1835
|
+
if (slot?.kind === "param") addParam(params, slot.name, kind);
|
|
1836
|
+
};
|
|
1837
|
+
dynamicParam(compiled.limitSlot, "number");
|
|
1838
|
+
dynamicParam(compiled.afterSlot, "string");
|
|
1839
|
+
dynamicParam(compiled.consistentReadSlot, "string");
|
|
1840
|
+
specs.push({
|
|
1841
|
+
alias,
|
|
1842
|
+
op: compiled.kind,
|
|
1843
|
+
entity: entityName,
|
|
1844
|
+
select: compiled.select,
|
|
1845
|
+
key: bindMapOf(compiled.keySlots, `${alias} key`),
|
|
1846
|
+
...compiled.maxDepth !== void 0 ? { maxDepth: compiled.maxDepth } : {},
|
|
1847
|
+
...compiled.order !== void 0 ? { order: compiled.order } : {},
|
|
1848
|
+
...compiled.filter !== void 0 ? { filter: compiled.filter } : {},
|
|
1849
|
+
...compiled.consistentReadSlot !== void 0 ? { consistentRead: bindOfSlot(compiled.consistentReadSlot, `${alias} consistentRead`) } : {},
|
|
1850
|
+
...compiled.limitSlot !== void 0 ? { limit: bindOfSlot(compiled.limitSlot, `${alias} limit`) } : {},
|
|
1851
|
+
...compiled.afterSlot !== void 0 ? { after: bindOfSlot(compiled.afterSlot, `${alias} after`) } : {},
|
|
1852
|
+
query: readRouteQuerySpec(compiled, metadata)
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
return { kind: "read", params, entities, reads: specs };
|
|
1856
|
+
}
|
|
1857
|
+
function compileWritePlan(routes, planLabel) {
|
|
1858
|
+
const specs = [];
|
|
1859
|
+
const params = {};
|
|
1860
|
+
const entities = {};
|
|
1861
|
+
for (const { alias, route } of routes) {
|
|
1862
|
+
const analyzed = analyzeWriteRoute(alias, route);
|
|
1863
|
+
if (analyzed.intent === "create" && analyzed.conditionSlots !== void 0) {
|
|
1864
|
+
throw new Error(
|
|
1865
|
+
`prepared AOT (${planLabel} '${alias}'): a \`condition\` on a 'create' is not supported \u2014 a create already guards with attribute_not_exists(PK). Use an 'update' with a condition, or drop the condition.`
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
const modelClass = resolveModelClass(analyzed.model);
|
|
1869
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1870
|
+
const entityName = modelClass.name;
|
|
1871
|
+
entities[entityName] = entityFingerprint(metadata);
|
|
1872
|
+
const fragment = compileWriteFragment({
|
|
1873
|
+
alias,
|
|
1874
|
+
intent: analyzed.intent,
|
|
1875
|
+
model: analyzed.model,
|
|
1876
|
+
keyFieldNames: analyzed.keyFields,
|
|
1877
|
+
inputFieldNames: analyzed.inputFields
|
|
1878
|
+
});
|
|
1879
|
+
const { transaction, categories } = serializePreparedWriteFragment(
|
|
1880
|
+
`${planLabel}.${alias}`,
|
|
1881
|
+
fragment
|
|
1882
|
+
);
|
|
1883
|
+
for (const item of transaction.items) {
|
|
1884
|
+
if (item.literalKey === true || item.entity === MARKER_ROW_ENTITY) continue;
|
|
1885
|
+
if (item.entity in entities) continue;
|
|
1886
|
+
const cls = resolveRegisteredModel(item.entity, `${planLabel}.${alias}`);
|
|
1887
|
+
entities[item.entity] = entityFingerprint(MetadataRegistry.get(cls));
|
|
1888
|
+
}
|
|
1889
|
+
for (const [field, slot] of Object.entries({
|
|
1890
|
+
...analyzed.inputSlots,
|
|
1891
|
+
...analyzed.keySlots,
|
|
1892
|
+
...analyzed.conditionSlots ?? {}
|
|
1893
|
+
})) {
|
|
1894
|
+
if (slot.kind === "param") {
|
|
1895
|
+
addParam(params, slot.name, paramKindForField(metadata, field));
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
let readback;
|
|
1899
|
+
const resultSelect = analyzed.result?.select;
|
|
1900
|
+
if (resultSelect !== void 0 && Object.keys(resultSelect).length > 0) {
|
|
1901
|
+
const kf = {};
|
|
1902
|
+
const kp = {};
|
|
1903
|
+
for (const field of fragment.keyFields) {
|
|
1904
|
+
const kind = paramKindForField(metadata, field);
|
|
1905
|
+
kp[field] = { kind, required: true };
|
|
1906
|
+
kf[field] = kind === "number" ? param.number() : param.string();
|
|
1907
|
+
}
|
|
1908
|
+
readback = buildQuerySpec({
|
|
1909
|
+
__isOperationDefinition: true,
|
|
1910
|
+
entity: { name: entityName, modelClass },
|
|
1911
|
+
operation: "query",
|
|
1912
|
+
key: kf,
|
|
1913
|
+
select: resultSelect,
|
|
1914
|
+
params: kp
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
specs.push({
|
|
1918
|
+
alias,
|
|
1919
|
+
entity: entityName,
|
|
1920
|
+
intent: analyzed.intent,
|
|
1921
|
+
keyFields: fragment.keyFields,
|
|
1922
|
+
key: bindMapOf(analyzed.keySlots, `${alias} key`),
|
|
1923
|
+
input: bindMapOf(analyzed.inputSlots, `${alias} input`),
|
|
1924
|
+
...analyzed.conditionSlots !== void 0 ? { condition: bindMapOf(analyzed.conditionSlots, `${alias} condition`) } : {},
|
|
1925
|
+
...analyzed.result !== void 0 ? { result: analyzed.result } : {},
|
|
1926
|
+
...readback !== void 0 ? { readback } : {},
|
|
1927
|
+
transaction,
|
|
1928
|
+
categories
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
return { kind: "write", params, entities, writes: specs };
|
|
1932
|
+
}
|
|
1933
|
+
function resolveRegisteredModel(name, label) {
|
|
1934
|
+
for (const cls of MetadataRegistry.getAll().keys()) {
|
|
1935
|
+
if (cls.name === name) {
|
|
1936
|
+
return cls;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
throw new Error(
|
|
1940
|
+
`prepared AOT (${label}): a derived effect targets entity '${name}', which is not a registered model.`
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
function compilePreparedPlan(body, planLabel = "prepared") {
|
|
1944
|
+
const { kind, routes } = evaluatePreparedRoutes(body);
|
|
1945
|
+
if (kind === "read") {
|
|
1946
|
+
return compileReadPlan(
|
|
1947
|
+
routes
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
return compileWritePlan(
|
|
1951
|
+
routes,
|
|
1952
|
+
planLabel
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
function buildPreparedPlanDocument(plans) {
|
|
1956
|
+
const sorted = {};
|
|
1957
|
+
for (const id of Object.keys(plans).sort()) sorted[id] = plans[id];
|
|
1958
|
+
return {
|
|
1959
|
+
formatVersion: PREPARED_FORMAT_VERSION,
|
|
1960
|
+
specVersion: SPEC_VERSION,
|
|
1961
|
+
plans: sorted
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// src/spec/index.ts
|
|
1966
|
+
function buildBridgeBundle(queries = {}, commands = {}, registry = MetadataRegistry, transactions = {}, contractInputs = {}) {
|
|
1967
|
+
const bundle = {
|
|
1968
|
+
manifest: buildManifest(registry),
|
|
1969
|
+
operations: buildOperations(queries, commands, transactions, contractInputs)
|
|
1970
|
+
};
|
|
1971
|
+
assertBundleSerializable(bundle);
|
|
1972
|
+
return bundle;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
export {
|
|
1976
|
+
collectContractN1Violations,
|
|
1977
|
+
assertContractN1Safe,
|
|
1978
|
+
buildContracts,
|
|
1979
|
+
buildContexts,
|
|
1980
|
+
collectContractBoundaryViolations,
|
|
1981
|
+
assertContractBoundaries,
|
|
1982
|
+
buildQuerySpec,
|
|
1983
|
+
buildOperations,
|
|
1984
|
+
compilePreparedPlan,
|
|
1985
|
+
buildPreparedPlanDocument,
|
|
1986
|
+
buildBridgeBundle
|
|
1987
|
+
};
|