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.
Files changed (45) hide show
  1. package/README.md +6 -6
  2. package/dist/cdc/index.d.ts +389 -4
  3. package/dist/cdc/index.js +4 -3
  4. package/dist/{chunk-ZPNRLOKA.js → chunk-GS4C5VGO.js} +4 -6
  5. package/dist/chunk-HNY2EJPV.js +1184 -0
  6. package/dist/{chunk-NYM7K2ST.js → chunk-I4LEJ4TF.js} +3812 -6724
  7. package/dist/{chunk-PFFPLD4B.js → chunk-L2NEDS7U.js} +725 -2112
  8. package/dist/chunk-L4QRCHRQ.js +278 -0
  9. package/dist/chunk-LGHSZIEE.js +187 -0
  10. package/dist/chunk-N4NWYNGZ.js +1987 -0
  11. package/dist/{chunk-PDUVTYC5.js → chunk-XTWXMOHD.js} +0 -1
  12. package/dist/cli.js +63 -252
  13. package/dist/index.d.ts +23 -1548
  14. package/dist/index.js +100 -1778
  15. package/dist/internal/index.d.ts +84 -0
  16. package/dist/internal/index.js +701 -0
  17. package/dist/{maintenance-view-adapter-BATUh_I8.d.ts → key-DR7_lpyk.d.ts} +538 -2975
  18. package/dist/linter/index.d.ts +39 -6
  19. package/dist/linter/index.js +22 -4
  20. package/dist/{registry-CXhP4TaE.d.ts → linter-C-vypgut.d.ts} +22 -22
  21. package/dist/prepared-artifact-BpPgkXEo.d.ts +281 -0
  22. package/dist/spec/index.d.ts +506 -4
  23. package/dist/spec/index.js +36 -17
  24. package/dist/testing/index.d.ts +2 -2
  25. package/dist/testing/index.js +4 -3
  26. package/dist/transform/index.d.ts +460 -1
  27. package/dist/transform/index.js +2085 -2
  28. package/dist/types-2PMXEn5x.d.ts +1205 -0
  29. package/dist/types-BXLzIcQD.d.ts +450 -0
  30. package/docs/cdc-projection.md +5 -5
  31. package/docs/class-hydration.md +1 -1
  32. package/docs/cqrs-contract.md +28 -20
  33. package/docs/design-patterns.md +5 -5
  34. package/docs/docs-generation.md +6 -6
  35. package/docs/middleware.md +15 -15
  36. package/docs/mutation-command-derivation.md +52 -42
  37. package/docs/prepared-statements.md +14 -14
  38. package/docs/python-bridge.md +113 -66
  39. package/docs/spec.md +153 -124
  40. package/docs/testing.md +9 -8
  41. package/package.json +20 -5
  42. package/dist/chunk-MMVHOUM4.js +0 -24
  43. package/dist/from-change-DanwjE5b.d.ts +0 -327
  44. package/dist/index-CtPJSMrc.d.ts +0 -934
  45. 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
+ };