graphddb 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,38 @@
1
1
  import {
2
2
  BATCH_GET_MAX_KEYS,
3
+ ChangeCaptureRegistry,
4
+ ClientManager,
3
5
  MAX_TRANSACT_ITEMS,
4
6
  MetadataRegistry,
5
7
  TableMapping,
8
+ attachHiddenKey,
9
+ attachModelClass,
10
+ buildConditionExpression,
11
+ buildDeleteInput,
12
+ buildPutInput,
13
+ buildUpdateInput,
14
+ captureWrite,
15
+ commitTransaction,
16
+ compileRawCondition,
17
+ createColumnMap,
18
+ execItemKeySignature,
19
+ executeDelete,
20
+ executePut,
21
+ executeTransaction,
22
+ executeUpdate,
23
+ isColumn,
24
+ isParam,
25
+ isRawCondition,
26
+ param,
6
27
  pkTemplate,
7
28
  resolveKey,
8
29
  resolveModelClass,
30
+ resolveSegmentedKey,
9
31
  segmentFieldNames,
32
+ serializeFieldValue,
33
+ serializeRawCondition,
10
34
  skTemplate
11
- } from "./chunk-347U24SB.js";
35
+ } from "./chunk-PWV7JDMR.js";
12
36
 
13
37
  // src/spec/types.ts
14
38
  var SPEC_VERSION = "1.0";
@@ -217,305 +241,1451 @@ function getImplicitKeyFields(select, metadata) {
217
241
  return [...needed];
218
242
  }
219
243
 
220
- // src/define/param.ts
221
- function makeParam(kind, literals) {
222
- return literals === void 0 ? { kind } : { kind, literals };
223
- }
224
- var param = {
225
- /** A placeholder for a `string` value. */
226
- string() {
227
- return makeParam("string");
228
- },
229
- /** A placeholder for a `number` value. */
230
- number() {
231
- return makeParam("number");
232
- },
233
- /**
234
- * A placeholder constrained to one of the given literal values. The value
235
- * type of the resulting {@link Param} is the **union of the literals**, so it
236
- * is preserved precisely in the IR and the inferred definition types.
237
- *
238
- * @example
239
- * ```ts
240
- * param.literal('active', 'disabled'); // Param<'active' | 'disabled'>
241
- * ```
242
- */
243
- literal(...values) {
244
- return makeParam("literal", values);
245
- },
246
- /**
247
- * A placeholder for an **array** parameter whose elements have the given field
248
- * shape. Used by `defineTransaction`'s `tx.forEach(p.<arrayParam>, …)` to bind
249
- * each element's fields to `{item.<field>}` templates.
250
- *
251
- * @example
252
- * ```ts
253
- * param.array({ userId: param.string(), role: param.string() });
254
- * // Param<{ userId: string; role: string }[]>
255
- * ```
256
- */
257
- array(element) {
258
- return { kind: "array", element };
244
+ // src/planner/projection.ts
245
+ function buildProjection(select, additionalFields) {
246
+ const paths = [];
247
+ const names = {};
248
+ let nameCounter = 0;
249
+ function addPath(segments) {
250
+ const aliased = segments.map((seg) => {
251
+ const alias = `#p${nameCounter++}`;
252
+ names[alias] = seg;
253
+ return alias;
254
+ });
255
+ paths.push(aliased.join("."));
256
+ }
257
+ function traverse(selectObj, parentSegments) {
258
+ for (const [fieldName, value] of Object.entries(selectObj)) {
259
+ if (value === true) {
260
+ addPath([...parentSegments, fieldName]);
261
+ } else if (typeof value === "object" && value !== null) {
262
+ if ("select" in value || isSelectBuilder(value)) {
263
+ continue;
264
+ }
265
+ traverse(value, [...parentSegments, fieldName]);
266
+ }
267
+ }
259
268
  }
260
- };
261
- function isParam(value) {
262
- return typeof value === "object" && value !== null && "kind" in value && (value.kind === "string" || value.kind === "number" || value.kind === "literal" || value.kind === "array");
269
+ traverse(select, []);
270
+ if (additionalFields) {
271
+ for (const fieldName of additionalFields) {
272
+ addPath([fieldName]);
273
+ }
274
+ }
275
+ if (paths.length === 0) {
276
+ return void 0;
277
+ }
278
+ return {
279
+ projectionExpression: paths.join(", "),
280
+ expressionAttributeNames: names
281
+ };
263
282
  }
264
283
 
265
- // src/define/entity-writes.ts
266
- var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
267
- "graphddb:lifecycleContract"
268
- );
269
- function isLifecycleContract(value) {
270
- return typeof value === "object" && value !== null && value[LIFECYCLE_CONTRACT_MARKER] === true;
284
+ // src/expression/filter-expression.ts
285
+ var LOGICAL_KEYS = /* @__PURE__ */ new Set(["and", "or", "not"]);
286
+ var OPERATOR_KEYS = /* @__PURE__ */ new Set([
287
+ "eq",
288
+ "ne",
289
+ "gt",
290
+ "ge",
291
+ "lt",
292
+ "le",
293
+ "between",
294
+ "in",
295
+ "beginsWith",
296
+ "contains",
297
+ "notContains",
298
+ "attributeExists",
299
+ "attributeType",
300
+ "size"
301
+ ]);
302
+ function nameAlias(ctx, column) {
303
+ for (const [alias2, col] of Object.entries(ctx.names)) {
304
+ if (col === column) return alias2;
305
+ }
306
+ const alias = `#f${ctx.nameCounter.n++}`;
307
+ ctx.names[alias] = column;
308
+ return alias;
309
+ }
310
+ function valueAlias(ctx, field, raw) {
311
+ const alias = `:vf${ctx.valueCounter.n++}`;
312
+ const fieldMeta = ctx.fieldMap.get(field);
313
+ ctx.values[alias] = fieldMeta ? serializeFieldValue(raw, fieldMeta) : raw;
314
+ return alias;
315
+ }
316
+ function compileField(ctx, field, condition) {
317
+ const nAlias = nameAlias(ctx, field);
318
+ if (!isOperatorObject(condition)) {
319
+ const vAlias = valueAlias(ctx, field, condition);
320
+ return `${nAlias} = ${vAlias}`;
321
+ }
322
+ const ops = condition;
323
+ const clauses = [];
324
+ for (const [op, value] of Object.entries(ops)) {
325
+ switch (op) {
326
+ case "eq":
327
+ clauses.push(`${nAlias} = ${valueAlias(ctx, field, value)}`);
328
+ break;
329
+ case "ne":
330
+ clauses.push(`${nAlias} <> ${valueAlias(ctx, field, value)}`);
331
+ break;
332
+ case "gt":
333
+ clauses.push(`${nAlias} > ${valueAlias(ctx, field, value)}`);
334
+ break;
335
+ case "ge":
336
+ clauses.push(`${nAlias} >= ${valueAlias(ctx, field, value)}`);
337
+ break;
338
+ case "lt":
339
+ clauses.push(`${nAlias} < ${valueAlias(ctx, field, value)}`);
340
+ break;
341
+ case "le":
342
+ clauses.push(`${nAlias} <= ${valueAlias(ctx, field, value)}`);
343
+ break;
344
+ case "between": {
345
+ const [lo, hi] = value;
346
+ clauses.push(
347
+ `${nAlias} BETWEEN ${valueAlias(ctx, field, lo)} AND ${valueAlias(ctx, field, hi)}`
348
+ );
349
+ break;
350
+ }
351
+ case "in": {
352
+ const arr = value;
353
+ const aliases = arr.map((v) => valueAlias(ctx, field, v));
354
+ clauses.push(`${nAlias} IN (${aliases.join(", ")})`);
355
+ break;
356
+ }
357
+ case "beginsWith":
358
+ clauses.push(`begins_with(${nAlias}, ${valueAlias(ctx, field, value)})`);
359
+ break;
360
+ case "contains":
361
+ clauses.push(`contains(${nAlias}, ${valueAlias(ctx, field, value)})`);
362
+ break;
363
+ case "notContains":
364
+ clauses.push(
365
+ `NOT contains(${nAlias}, ${valueAlias(ctx, field, value)})`
366
+ );
367
+ break;
368
+ case "attributeExists":
369
+ clauses.push(
370
+ value === false ? `attribute_not_exists(${nAlias})` : `attribute_exists(${nAlias})`
371
+ );
372
+ break;
373
+ case "attributeType":
374
+ clauses.push(
375
+ `attribute_type(${nAlias}, ${valueAlias(ctx, field, value)})`
376
+ );
377
+ break;
378
+ case "size":
379
+ clauses.push(`size(${nAlias}) = ${valueAlias(ctx, field, value)}`);
380
+ break;
381
+ default:
382
+ throw new Error(`Unknown filter operator '${op}' on field '${field}'`);
383
+ }
384
+ }
385
+ return joinAnd(clauses);
271
386
  }
272
- var ENTITY_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:entityWrites");
273
- function isEntityWritesDefinition(value) {
274
- return typeof value === "object" && value !== null && value[ENTITY_WRITES_MARKER] === true;
387
+ function isOperatorObject(value) {
388
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || value instanceof Uint8Array || value instanceof Set) {
389
+ return false;
390
+ }
391
+ const keys = Object.keys(value);
392
+ if (keys.length === 0) return false;
393
+ return keys.every((k) => OPERATOR_KEYS.has(k));
275
394
  }
276
- function freezeEffects(effects) {
277
- const out = {};
278
- if (effects.requires !== void 0) out.requires = effects.requires;
279
- if (effects.unique !== void 0) out.unique = effects.unique;
280
- if (effects.edges !== void 0) out.edges = effects.edges;
281
- if (effects.derive !== void 0) out.derive = effects.derive;
282
- if (effects.emits !== void 0) out.emits = effects.emits;
283
- if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
284
- return out;
395
+ function joinAnd(clauses) {
396
+ if (clauses.length === 1) return clauses[0];
397
+ return clauses.map((c) => wrap(c)).join(" AND ");
285
398
  }
286
- var recorder = {
287
- lifecycle(effects = {}) {
288
- return {
289
- [LIFECYCLE_CONTRACT_MARKER]: true,
290
- effects: freezeEffects(effects)
291
- };
292
- },
293
- exists(targetFactory, keys) {
294
- return { kind: "requires", targetFactory, keys };
295
- },
296
- unique(spec) {
297
- return { kind: "unique", name: spec.name, scope: spec.scope, fields: spec.fields };
298
- },
299
- putEdge(targetFactory, relationProperty) {
300
- return { kind: "putEdge", targetFactory, relationProperty };
301
- },
302
- deleteEdge(targetFactory, relationProperty) {
303
- return { kind: "deleteEdge", targetFactory, relationProperty };
304
- },
305
- increment(targetFactory, keys, attribute, amount) {
306
- return { kind: "derive", targetFactory, keys, attribute, amount };
307
- },
308
- event(name, payload) {
309
- return { kind: "event", name, payload };
310
- },
311
- idempotentBy(token) {
312
- return { kind: "idempotency", token };
399
+ function wrap(expr) {
400
+ if (isAlreadyWrapped(expr)) return expr;
401
+ if (expr.includes(" AND ") || expr.includes(" OR ")) {
402
+ return `(${expr})`;
313
403
  }
314
- };
315
- function entityWrites(builder) {
316
- const shape = builder(recorder);
317
- for (const phase of ["create", "update", "remove"]) {
318
- const contract = shape[phase];
319
- if (contract !== void 0 && !isLifecycleContract(contract)) {
320
- throw new Error(
321
- `entityWrites: the '${phase}' lifecycle must be built with \`w.lifecycle({...})\` (it carries the \xA72 effect arrays), but received ${JSON.stringify(contract)}.`
322
- );
404
+ return expr;
405
+ }
406
+ function isAlreadyWrapped(expr) {
407
+ if (!expr.startsWith("(") || !expr.endsWith(")")) return false;
408
+ let depth = 0;
409
+ for (let i = 0; i < expr.length; i++) {
410
+ if (expr[i] === "(") depth++;
411
+ else if (expr[i] === ")") {
412
+ depth--;
413
+ if (depth === 0 && i < expr.length - 1) return false;
323
414
  }
324
415
  }
325
- if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
326
- throw new Error(
327
- "entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`); an empty save contract declares nothing."
328
- );
329
- }
330
- return {
331
- [ENTITY_WRITES_MARKER]: true,
332
- ...shape.create !== void 0 ? { create: shape.create } : {},
333
- ...shape.update !== void 0 ? { update: shape.update } : {},
334
- ...shape.remove !== void 0 ? { remove: shape.remove } : {}
335
- };
416
+ return depth === 0;
336
417
  }
337
- function getEntityWrites(modelClass) {
338
- for (const name of Object.getOwnPropertyNames(modelClass)) {
339
- let value;
340
- try {
341
- value = modelClass[name];
342
- } catch {
418
+ function compileRaw(ctx, raw) {
419
+ return compileRawCondition(
420
+ raw,
421
+ (column) => nameAlias(ctx, column),
422
+ (value) => {
423
+ const alias = `:vf${ctx.valueCounter.n++}`;
424
+ ctx.values[alias] = value;
425
+ return alias;
426
+ }
427
+ );
428
+ }
429
+ function compileNode(ctx, node) {
430
+ if (isRawCondition(node)) {
431
+ return compileRaw(ctx, node);
432
+ }
433
+ const clauses = [];
434
+ for (const [key, value] of Object.entries(node)) {
435
+ if (value === void 0) continue;
436
+ if (LOGICAL_KEYS.has(key)) {
437
+ if (key === "and" || key === "or") {
438
+ const parts = value.map((sub) => compileNode(ctx, sub)).filter((s) => s.length > 0);
439
+ if (parts.length === 0) continue;
440
+ if (parts.length === 1) {
441
+ clauses.push(parts[0]);
442
+ } else {
443
+ const sep = key === "and" ? " AND " : " OR ";
444
+ const joined = parts.map((p) => wrap(p)).join(sep);
445
+ clauses.push(`(${joined})`);
446
+ }
447
+ } else {
448
+ const inner = compileNode(ctx, value);
449
+ if (inner.length > 0) clauses.push(`NOT ${wrap(inner)}`);
450
+ }
343
451
  continue;
344
452
  }
345
- if (isEntityWritesDefinition(value)) return value;
453
+ const clause = compileField(ctx, key, value);
454
+ if (clause.length > 0) clauses.push(clause);
346
455
  }
347
- return void 0;
348
- }
349
- function lifecyclePhaseForIntent(intent) {
350
- return intent;
456
+ return joinAnd(clauses);
351
457
  }
352
-
353
- // src/define/mutation.ts
354
- var INPUT_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationInputRef");
355
- function isMutationInputRef(value) {
356
- return typeof value === "object" && value !== null && value[INPUT_REF_BRAND] === true;
357
- }
358
- var ENTITY_REF_RE = /^\$\.entity\[(\d+)\]\.([A-Za-z_$][\w$]*)$/;
359
- var ENTITY_REF_PREFIX_RE = /^\s*\$\s*\.\s*entity\b/;
360
- var ENTITY_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationEntityRef");
361
- function parseEntityRef(leaf) {
362
- if (typeof leaf !== "string") return void 0;
363
- const match = ENTITY_REF_RE.exec(leaf);
364
- if (match === null) {
365
- if (ENTITY_REF_PREFIX_RE.test(leaf)) {
366
- throw new Error(
367
- `mutation: the input value ${JSON.stringify(leaf)} looks like a cross-fragment reference but is malformed. The only supported form is \`$.entity[<index>].<field>\` (e.g. \`$.entity[0].postId\`) \u2014 a single dotted field on a 0-based, non-negative fragment index. Fix the path or, if you meant a literal string, it must not begin with \`$.entity\`.`
368
- );
369
- }
458
+ function compileFilterExpression(filter, metadata) {
459
+ if (!filter || Object.keys(filter).length === 0) {
460
+ return void 0;
461
+ }
462
+ const ctx = {
463
+ names: {},
464
+ values: {},
465
+ fieldMap: new Map(metadata.fields.map((f) => [f.propertyName, f])),
466
+ nameCounter: { n: 0 },
467
+ valueCounter: { n: 0 }
468
+ };
469
+ const expression = compileNode(ctx, filter);
470
+ if (expression.length === 0) {
370
471
  return void 0;
371
472
  }
372
473
  return {
373
- [ENTITY_REF_BRAND]: true,
374
- fragmentIndex: Number(match[1]),
375
- field: match[2],
376
- path: leaf
474
+ filterExpression: expression,
475
+ expressionAttributeNames: ctx.names,
476
+ expressionAttributeValues: ctx.values
377
477
  };
378
478
  }
379
- function makeInputFieldRef(field) {
380
- const token = `{${field}}`;
381
- const origin = `the input field '${field}' (\`$.${field}\`)`;
382
- const target = /* @__PURE__ */ Object.create(null);
383
- const proxy = new Proxy(target, {
384
- get(_t, prop) {
385
- if (prop === INPUT_REF_BRAND) return true;
386
- if (prop === "field") return field;
387
- if (prop === "token") return token;
388
- if (typeof prop === "symbol") return void 0;
389
- throw new Error(
390
- `mutation: unsupported operation '${String(prop)}' on ${origin}. A fragment \`input\` binding only supports a *direct* \`$.field\` reference or a concrete literal \u2014 any transform, indexing, coercion, or concatenation on an input field is not representable as a declarative mutation.`
391
- );
392
- },
393
- set() {
394
- throw new Error(`mutation: cannot assign on ${origin}.`);
479
+ function evaluateFilter(item, filter) {
480
+ for (const [key, value] of Object.entries(filter)) {
481
+ if (value === void 0) continue;
482
+ if (key === "and") {
483
+ if (!value.every((s) => evaluateFilter(item, s)))
484
+ return false;
485
+ continue;
395
486
  }
396
- });
397
- return proxy;
398
- }
399
- function makeInputProxy() {
400
- const cache = /* @__PURE__ */ new Map();
401
- return new Proxy(/* @__PURE__ */ Object.create(null), {
402
- get(_t, prop) {
403
- if (typeof prop === "symbol") return void 0;
404
- const field = prop;
405
- let ref = cache.get(field);
406
- if (!ref) {
407
- ref = makeInputFieldRef(field);
408
- cache.set(field, ref);
487
+ if (key === "or") {
488
+ if (!value.some((s) => evaluateFilter(item, s)))
489
+ return false;
490
+ continue;
491
+ }
492
+ if (key === "not") {
493
+ if (evaluateFilter(item, value)) return false;
494
+ continue;
495
+ }
496
+ if (!evaluateFieldCondition(item[key], item, key, value)) {
497
+ return false;
498
+ }
499
+ }
500
+ return true;
501
+ }
502
+ function cmp(a, b) {
503
+ if (a instanceof Date && b instanceof Date) {
504
+ return a.getTime() - b.getTime();
505
+ }
506
+ if (typeof a === "number" && typeof b === "number") return a - b;
507
+ if (typeof a === "string" && typeof b === "string")
508
+ return a < b ? -1 : a > b ? 1 : 0;
509
+ if (a === b) return 0;
510
+ return a < b ? -1 : 1;
511
+ }
512
+ function eq(a, b) {
513
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
514
+ return a === b;
515
+ }
516
+ function evaluateFieldCondition(actual, item, field, condition) {
517
+ if (!isOperatorObject(condition)) {
518
+ return eq(actual, condition);
519
+ }
520
+ const ops = condition;
521
+ for (const [op, value] of Object.entries(ops)) {
522
+ switch (op) {
523
+ case "eq":
524
+ if (!eq(actual, value)) return false;
525
+ break;
526
+ case "ne":
527
+ if (eq(actual, value)) return false;
528
+ break;
529
+ case "gt":
530
+ if (!(cmp(actual, value) > 0)) return false;
531
+ break;
532
+ case "ge":
533
+ if (!(cmp(actual, value) >= 0)) return false;
534
+ break;
535
+ case "lt":
536
+ if (!(cmp(actual, value) < 0)) return false;
537
+ break;
538
+ case "le":
539
+ if (!(cmp(actual, value) <= 0)) return false;
540
+ break;
541
+ case "between": {
542
+ const [lo, hi] = value;
543
+ if (!(cmp(actual, lo) >= 0 && cmp(actual, hi) <= 0)) return false;
544
+ break;
409
545
  }
410
- return ref;
411
- },
412
- set() {
413
- throw new Error("mutation: cannot assign to the `$` input proxy.");
546
+ case "in":
547
+ if (!value.some((v) => eq(actual, v))) return false;
548
+ break;
549
+ case "beginsWith":
550
+ if (typeof actual !== "string" || !actual.startsWith(value))
551
+ return false;
552
+ break;
553
+ case "contains":
554
+ if (typeof actual !== "string" || !actual.includes(value))
555
+ return false;
556
+ break;
557
+ case "notContains":
558
+ if (typeof actual === "string" && actual.includes(value))
559
+ return false;
560
+ break;
561
+ case "attributeExists": {
562
+ const exists = field in item && actual !== void 0 && actual !== null;
563
+ if (value === false ? exists : !exists) return false;
564
+ break;
565
+ }
566
+ case "attributeType":
567
+ break;
568
+ case "size": {
569
+ const len = typeof actual === "string" || Array.isArray(actual) ? actual.length : void 0;
570
+ if (len !== value) return false;
571
+ break;
572
+ }
573
+ default:
574
+ throw new Error(`Unknown filter operator '${op}' on field '${field}'`);
414
575
  }
415
- });
416
- }
417
- var FRAGMENT_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationFragment");
418
- function isMutationFragment(value) {
419
- return typeof value === "object" && value !== null && value[FRAGMENT_BRAND] === true;
420
- }
421
- function targetEntityRef(target) {
422
- const resolved = target();
423
- const modelClass = resolveModelClass(resolved);
424
- return { name: modelClass.name, modelClass };
576
+ }
577
+ return true;
425
578
  }
426
- function assertFaithfulInput(input, intent, entityName) {
427
- if (input === null || typeof input !== "object") {
579
+
580
+ // src/planner/planner.ts
581
+ function plan(metadata, input) {
582
+ const queryFields = Object.keys(input.key);
583
+ const resolved = resolveKey(queryFields, metadata);
584
+ if (input.consistentRead && resolved.type === "gsi") {
428
585
  throw new Error(
429
- `mutation: the '${intent}' fragment on '${entityName}' must declare an \`input\` object binding model fields to \`$.field\` references or literals.`
586
+ "consistentRead is not supported for GSI queries. Use a primary key query instead."
430
587
  );
431
588
  }
432
- for (const [field, leaf] of Object.entries(input)) {
433
- if (!isMutationInputRef(leaf) && !isConcreteLiteral(leaf)) {
434
- throw new Error(
435
- `mutation: the '${intent}' fragment on '${entityName}' binds model field '${field}' to a value that is neither a \`$.field\` input reference nor a concrete literal. A fragment input is declarative \u2014 only \`$.<field>\` references or literals are allowed, never a computed value.`
436
- );
589
+ const tableName = TableMapping.resolve(metadata.tableName);
590
+ const built = buildKeyCondition(metadata, resolved, input.key);
591
+ const keyCondition = built.keyCondition;
592
+ const additionalProjectionFields = input.updatable ? Array.from(
593
+ /* @__PURE__ */ new Set([...input.additionalProjectionFields ?? [], "PK", "SK"])
594
+ ) : input.additionalProjectionFields;
595
+ const projection = buildProjection(input.select, additionalProjectionFields);
596
+ const compiledFilter = compileFilterExpression(input.filter, metadata);
597
+ const isGetItem = resolved.type === "pk" && !resolved.partial && built.skValue !== void 0 && // GetItem does not support FilterExpression — fall back to Query when a
598
+ // server-side filter is requested so the condition can be applied.
599
+ !compiledFilter;
600
+ const projectionFields2 = projection ? {
601
+ projectionExpression: projection.projectionExpression,
602
+ expressionAttributeNames: projection.expressionAttributeNames
603
+ } : {};
604
+ const filterFields = compiledFilter ? {
605
+ filterExpression: compiledFilter.filterExpression,
606
+ filterExpressionAttributeNames: compiledFilter.expressionAttributeNames,
607
+ filterExpressionAttributeValues: compiledFilter.expressionAttributeValues
608
+ } : {};
609
+ const consistentRead = input.consistentRead && resolved.type === "pk" ? { consistentRead: true } : {};
610
+ let op;
611
+ if (isGetItem) {
612
+ op = {
613
+ type: "GetItem",
614
+ tableName,
615
+ keyCondition,
616
+ ...projectionFields2,
617
+ ...consistentRead
618
+ };
619
+ } else {
620
+ const queryOp = {
621
+ type: "Query",
622
+ tableName,
623
+ keyCondition,
624
+ scanIndexForward: (input.order ?? "ASC") === "ASC",
625
+ ...projectionFields2,
626
+ ...filterFields,
627
+ ...consistentRead
628
+ };
629
+ if (resolved.type === "gsi") {
630
+ queryOp.indexName = resolved.indexName;
631
+ }
632
+ if (built.rangeCondition) {
633
+ queryOp.rangeCondition = built.rangeCondition;
634
+ }
635
+ if (input.limit != null) {
636
+ queryOp.limit = input.limit;
637
+ }
638
+ if (input.after) {
639
+ queryOp.exclusiveStartKey = input.after;
437
640
  }
641
+ op = queryOp;
438
642
  }
439
- return input;
440
- }
441
- function isConcreteLiteral(value) {
442
- const t = typeof value;
443
- return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
643
+ return { operations: [op] };
444
644
  }
445
- function makeFragment(intent, target, options) {
446
- const entity = targetEntityRef(target);
447
- const input = assertFaithfulInput(options.input ?? {}, intent, entity.name);
448
- if (options.use !== void 0 && !isEntityWritesDefinition(options.use)) {
449
- throw new Error(
450
- `mutation: the '${intent}' fragment on '${entity.name}' was given a \`use:\` that is not an \`entityWrites(...)\` save contract. \`use:\` takes the WHOLE \`writes\` set (an \`entityWrites(...)\` value), never a single lifecycle (e.g. \`Model.writes.create\`) \u2014 the fragment's intent already selects the lifecycle.`
645
+ function buildKeyCondition(metadata, resolved, queryKey) {
646
+ if (resolved.type === "pk") {
647
+ if (!metadata.primaryKey) throw new Error("Primary key not defined");
648
+ return buildFromSegments(
649
+ metadata.primaryKey.segmented,
650
+ metadata.primaryKey.inputFieldNames,
651
+ queryKey,
652
+ "PK",
653
+ "SK"
451
654
  );
452
655
  }
453
- return {
454
- [FRAGMENT_BRAND]: true,
455
- intent,
456
- entity,
457
- input,
458
- ...options.use !== void 0 ? { use: options.use } : {}
459
- };
460
- }
461
- var mutationRecorder = {
462
- create: (target, options) => makeFragment("create", target, options),
463
- update: (target, options) => makeFragment("update", target, options),
464
- remove: (target, options) => makeFragment("remove", target, options)
465
- };
466
- var COMMAND_PLAN_BRAND = /* @__PURE__ */ Symbol("graphddb:commandPlan");
467
- function isCommandPlan(value) {
468
- return typeof value === "object" && value !== null && value[COMMAND_PLAN_BRAND] === true;
656
+ const gsiDef = metadata.gsiDefinitions.find(
657
+ (g) => g.indexName === resolved.indexName
658
+ );
659
+ if (!gsiDef) throw new Error(`GSI '${resolved.indexName}' not found`);
660
+ return buildFromSegments(
661
+ gsiDef.segmented,
662
+ gsiDef.inputFieldNames,
663
+ queryKey,
664
+ `${gsiDef.indexName}PK`,
665
+ `${gsiDef.indexName}SK`
666
+ );
469
667
  }
470
- function mutation(name, body) {
471
- if (typeof name !== "string" || name.length === 0) {
472
- throw new Error("mutation: the first argument must be a non-empty mutation name.");
668
+ function buildFromSegments(segmented, inputFieldNames, queryKey, pkAttr, skAttr) {
669
+ const keyInput = {};
670
+ for (const fieldName of inputFieldNames) {
671
+ keyInput[fieldName] = queryKey[fieldName];
473
672
  }
474
- const $ = makeInputProxy();
475
- const result = body(mutationRecorder, $);
476
- if (!Array.isArray(result)) {
477
- throw new Error(
478
- `mutation '${name}': the body must RETURN an array of write fragments (e.g. \`(m, $) => [ m.create(() => Model, { input }) ]\`). The fragments are declarations composed at compile time, not executed calls.`
479
- );
673
+ const { pk, sk, skTruncated } = resolveSegmentedKey(segmented, keyInput);
674
+ const keyCondition = { [pkAttr]: pk };
675
+ if (sk === void 0) {
676
+ return { keyCondition };
480
677
  }
481
- if (result.length === 0) {
482
- throw new Error(
483
- `mutation '${name}': the body returned an empty fragment list. A mutation must declare at least one write fragment.`
484
- );
678
+ if (skTruncated) {
679
+ return {
680
+ keyCondition,
681
+ rangeCondition: { operator: "begins_with", key: skAttr, value: sk }
682
+ };
485
683
  }
486
- result.forEach((fragment, i) => {
487
- if (!isMutationFragment(fragment)) {
488
- throw new Error(
489
- `mutation '${name}': fragment #${i} is not a write fragment. Each list element must be an \`m.create\` / \`m.update\` / \`m.remove\` declaration.`
490
- );
491
- }
492
- });
493
- return { [COMMAND_PLAN_BRAND]: true, name, fragments: result };
684
+ keyCondition[skAttr] = sk;
685
+ return { keyCondition, skValue: sk };
494
686
  }
495
- var definePlan = mutation;
496
687
 
497
- // src/relation/edge-write.ts
498
- var OLD_VALUE_NAMESPACE = "old";
499
- var EDGE_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:edgeWrites");
500
- function isEdgeWritesDefinition(value) {
501
- return typeof value === "object" && value !== null && value[EDGE_WRITES_MARKER] === true;
688
+ // src/executor/executor.ts
689
+ async function execute(operation) {
690
+ return ClientManager.getExecutor().execute(operation);
502
691
  }
503
- function declaration(target, relationProperty) {
504
- return { targetFactory: target, relationProperty };
692
+
693
+ // src/hydrator/hydrator.ts
694
+ function hydrate(rawItems, select, metadata, updatable = false) {
695
+ return rawItems.map((item) => hydrateItem(item, select, metadata, updatable));
505
696
  }
506
- var recorder2 = {
507
- putEdge: declaration,
508
- deleteEdge: declaration,
509
- updateKeyChangeEdge: declaration
510
- };
511
- function edgeWrites(builder) {
512
- const edges = builder(recorder2);
513
- if (edges.length === 0) {
514
- throw new Error(
515
- "edgeWrites(...) must declare at least one edge (w.putEdge / w.deleteEdge / w.updateKeyChangeEdge); an empty declaration writes nothing."
516
- );
517
- }
518
- return { [EDGE_WRITES_MARKER]: true, edges };
697
+ function hydrateItem(raw, select, metadata, updatable = false) {
698
+ const fieldMap = new Map(
699
+ metadata.fields.map((f) => [f.propertyName, f])
700
+ );
701
+ const embeddedMap = new Map(
702
+ metadata.embeddedFields.map((e) => [e.propertyName, e])
703
+ );
704
+ const result = {};
705
+ for (const [fieldName, selectValue] of Object.entries(select)) {
706
+ if (selectValue === true) {
707
+ const value = raw[fieldName];
708
+ if (value !== void 0) {
709
+ const fieldMeta = fieldMap.get(fieldName);
710
+ result[fieldName] = deserializeValue(value, fieldMeta);
711
+ }
712
+ } else if (typeof selectValue === "object" && selectValue !== null) {
713
+ if ("select" in selectValue || isSelectBuilder(selectValue)) {
714
+ continue;
715
+ }
716
+ const rawEmb = raw[fieldName];
717
+ if (rawEmb && typeof rawEmb === "object") {
718
+ result[fieldName] = hydrateEmbedded(
719
+ rawEmb,
720
+ selectValue,
721
+ metadata,
722
+ fieldName
723
+ );
724
+ }
725
+ }
726
+ }
727
+ if (updatable) {
728
+ attachHiddenKey(result, raw);
729
+ }
730
+ return result;
731
+ }
732
+ function hydrateEmbedded(raw, select, _metadata, _fieldName) {
733
+ const result = {};
734
+ for (const [key, selectValue] of Object.entries(select)) {
735
+ if (selectValue === true) {
736
+ if (raw[key] !== void 0) {
737
+ result[key] = raw[key];
738
+ }
739
+ } else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
740
+ const nested = raw[key];
741
+ if (nested && typeof nested === "object") {
742
+ result[key] = hydrateEmbedded(
743
+ nested,
744
+ selectValue,
745
+ _metadata,
746
+ key
747
+ );
748
+ }
749
+ }
750
+ }
751
+ return result;
752
+ }
753
+ function deserializeValue(value, fieldMeta) {
754
+ if (!fieldMeta) return value;
755
+ if (fieldMeta.options?.deserialize) {
756
+ return fieldMeta.options.deserialize(value);
757
+ }
758
+ if (fieldMeta.options?.format === "datetime" && typeof value === "string") {
759
+ return new Date(value);
760
+ }
761
+ if (fieldMeta.options?.format === "date" && typeof value === "string") {
762
+ return /* @__PURE__ */ new Date(value + "T00:00:00.000Z");
763
+ }
764
+ return value;
765
+ }
766
+
767
+ // src/pagination/cursor.ts
768
+ function encodeCursor(lastEvaluatedKey) {
769
+ const json = JSON.stringify(lastEvaluatedKey);
770
+ const bytes = new TextEncoder().encode(json);
771
+ let binary = "";
772
+ for (const b of bytes) {
773
+ binary += String.fromCharCode(b);
774
+ }
775
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
776
+ }
777
+ function decodeCursor(cursor) {
778
+ let base64 = cursor.replace(/-/g, "+").replace(/_/g, "/");
779
+ while (base64.length % 4 !== 0) {
780
+ base64 += "=";
781
+ }
782
+ const binary = atob(base64);
783
+ const bytes = new Uint8Array(binary.length);
784
+ for (let i = 0; i < binary.length; i++) {
785
+ bytes[i] = binary.charCodeAt(i);
786
+ }
787
+ const json = new TextDecoder().decode(bytes);
788
+ return JSON.parse(json);
789
+ }
790
+
791
+ // src/operations/list.ts
792
+ async function executeListInternal(modelClass, key, selectSpec, options = {}) {
793
+ const metadata = MetadataRegistry.get(modelClass);
794
+ const normalized = normalizeTopLevelSelect(selectSpec);
795
+ const select = normalized.select;
796
+ const filter = options.filter ?? normalized.filter;
797
+ const after = options.after ?? normalized.after;
798
+ const limit = options.limit ?? normalized.limit;
799
+ const order = options.order ?? normalized.order;
800
+ const exclusiveStartKey = after ? decodeCursor(after) : void 0;
801
+ const implicitKeys = getImplicitKeyFields(select, metadata);
802
+ const executionPlan = plan(metadata, {
803
+ key,
804
+ select,
805
+ limit,
806
+ after: exclusiveStartKey,
807
+ order,
808
+ filter,
809
+ additionalProjectionFields: implicitKeys.length > 0 ? implicitKeys : void 0,
810
+ updatable: options.updatable ?? false
811
+ });
812
+ const operation = executionPlan.operations[0];
813
+ const result = await execute(operation);
814
+ const rawItems = result.items;
815
+ const hydrated = hydrate(rawItems, select, metadata, options.updatable ?? false);
816
+ const cursor = result.lastEvaluatedKey ? encodeCursor(result.lastEvaluatedKey) : null;
817
+ return { items: hydrated, rawItems, cursor };
818
+ }
819
+ async function executeList(modelClass, key, selectSpec, options = {}) {
820
+ const { items, cursor } = await executeListInternal(
821
+ modelClass,
822
+ key,
823
+ selectSpec,
824
+ options
825
+ );
826
+ return { items, cursor };
827
+ }
828
+
829
+ // src/planner/relation-planner.ts
830
+ function buildRelationQueryKey(rel, rawParentItem) {
831
+ const queryKey = {};
832
+ for (const [targetField, sourceField] of Object.entries(rel.keyBinding)) {
833
+ queryKey[targetField] = rawParentItem[sourceField];
834
+ }
835
+ return queryKey;
836
+ }
837
+ function serializeQueryKey(queryKey) {
838
+ const entries = Object.entries(queryKey).sort(([a], [b]) => a.localeCompare(b));
839
+ return JSON.stringify(entries);
840
+ }
841
+ function planGetItemForQueryKey(metadata, queryKey, select, additionalProjectionFields) {
842
+ const executionPlan = plan(metadata, {
843
+ key: queryKey,
844
+ select,
845
+ additionalProjectionFields
846
+ });
847
+ const operation = executionPlan.operations[0];
848
+ if (operation.type !== "GetItem") {
849
+ throw new Error(
850
+ `Relation target requires GetItem access pattern, got ${operation.type}`
851
+ );
852
+ }
853
+ return operation;
854
+ }
855
+ function planBatchGetForQueryKeys(metadata, queryKeys, select, additionalProjectionFields) {
856
+ if (queryKeys.length === 0) {
857
+ return [];
858
+ }
859
+ const keyFields = [
860
+ ...new Set(queryKeys.flatMap((queryKey) => Object.keys(queryKey)))
861
+ ].filter((field) => !(field in select && select[field] === true));
862
+ const projectionFields2 = [
863
+ .../* @__PURE__ */ new Set([
864
+ ...(additionalProjectionFields ?? []).filter(
865
+ (field) => !(field in select && select[field] === true)
866
+ ),
867
+ ...keyFields
868
+ ])
869
+ ];
870
+ const samplePlan = planGetItemForQueryKey(
871
+ metadata,
872
+ queryKeys[0],
873
+ select,
874
+ projectionFields2.length > 0 ? projectionFields2 : void 0
875
+ );
876
+ const uniqueKeys = dedupeDynamoKeys(
877
+ queryKeys.map(
878
+ (queryKey) => planGetItemForQueryKey(
879
+ metadata,
880
+ queryKey,
881
+ select,
882
+ projectionFields2.length > 0 ? projectionFields2 : void 0
883
+ ).keyCondition
884
+ )
885
+ );
886
+ const operations = [];
887
+ for (let i = 0; i < uniqueKeys.length; i += BATCH_GET_MAX_KEYS) {
888
+ operations.push({
889
+ type: "BatchGetItem",
890
+ tableName: samplePlan.tableName,
891
+ keys: uniqueKeys.slice(i, i + BATCH_GET_MAX_KEYS),
892
+ ...samplePlan.projectionExpression ? { projectionExpression: samplePlan.projectionExpression } : {},
893
+ ...samplePlan.expressionAttributeNames ? { expressionAttributeNames: samplePlan.expressionAttributeNames } : {},
894
+ ...samplePlan.consistentRead ? { consistentRead: true } : {}
895
+ });
896
+ }
897
+ return operations;
898
+ }
899
+ function dedupeDynamoKeys(keys) {
900
+ const seen = /* @__PURE__ */ new Set();
901
+ const result = [];
902
+ for (const key of keys) {
903
+ const serialized = JSON.stringify(
904
+ Object.entries(key).sort(([a], [b]) => a.localeCompare(b))
905
+ );
906
+ if (seen.has(serialized)) continue;
907
+ seen.add(serialized);
908
+ result.push(key);
909
+ }
910
+ return result;
911
+ }
912
+ function planRelationOperations(metadata, select, context = { estimatedParentCount: 1 }) {
913
+ const operations = [];
914
+ const relations = detectRelationFields(select, metadata);
915
+ for (const rel of relations) {
916
+ const selectSpec = normalizeSelectSpec(select[rel.propertyName]);
917
+ const targetClass = rel.targetFactory();
918
+ const targetMeta = MetadataRegistry.get(targetClass);
919
+ if (rel.type === "hasMany") {
920
+ const limit = selectSpec.limit ?? rel.options?.limit?.default ?? 20;
921
+ const tableName = TableMapping.resolve(targetMeta.tableName);
922
+ const listPlan = plan(targetMeta, {
923
+ key: buildPlaceholderHasManyKey(rel, targetMeta),
924
+ select: selectSpec.select,
925
+ limit
926
+ });
927
+ operations.push(...listPlan.operations);
928
+ operations.push(
929
+ ...planRelationOperations(targetMeta, selectSpec.select, {
930
+ estimatedParentCount: limit
931
+ })
932
+ );
933
+ continue;
934
+ }
935
+ const estimatedCount = context.estimatedParentCount ?? 1;
936
+ operations.push(
937
+ ...planBatchGetForExplain(targetMeta, selectSpec.select, estimatedCount)
938
+ );
939
+ operations.push(
940
+ ...planRelationOperations(targetMeta, selectSpec.select, {
941
+ estimatedParentCount: 1
942
+ })
943
+ );
944
+ }
945
+ return operations;
946
+ }
947
+ function buildPlaceholderHasManyKey(rel, targetMeta) {
948
+ const queryKey = {};
949
+ for (const targetField of Object.keys(rel.keyBinding)) {
950
+ queryKey[targetField] = `__${targetField}__`;
951
+ }
952
+ if (targetMeta.primaryKey) {
953
+ for (const fieldName of targetMeta.primaryKey.inputFieldNames) {
954
+ if (!(fieldName in queryKey)) {
955
+ queryKey[fieldName] = `__${fieldName}__`;
956
+ }
957
+ }
958
+ }
959
+ return queryKey;
960
+ }
961
+ function planBatchGetForExplain(metadata, select, estimatedKeyCount) {
962
+ const placeholderKeys = Array.from(
963
+ { length: estimatedKeyCount },
964
+ (_, index) => buildExplainPlaceholderKey(metadata, index)
965
+ );
966
+ return planBatchGetForQueryKeys(metadata, placeholderKeys, select);
967
+ }
968
+ function buildExplainPlaceholderKey(metadata, index) {
969
+ const queryKey = {};
970
+ if (metadata.primaryKey) {
971
+ for (const fieldName of metadata.primaryKey.inputFieldNames) {
972
+ queryKey[fieldName] = `__explain_${fieldName}_${index}__`;
973
+ }
974
+ }
975
+ return queryKey;
976
+ }
977
+
978
+ // src/relation/concurrency.ts
979
+ var RELATION_TRAVERSAL_CONCURRENCY = 16;
980
+ async function mapWithConcurrency(items, limit, worker) {
981
+ if (items.length === 0) {
982
+ return [];
983
+ }
984
+ const effectiveLimit = Math.max(1, Math.min(limit, items.length));
985
+ if (effectiveLimit >= items.length) {
986
+ return Promise.all(items.map((item, index) => worker(item, index)));
987
+ }
988
+ const results = new Array(items.length);
989
+ let nextIndex = 0;
990
+ async function runWorker() {
991
+ while (true) {
992
+ const index = nextIndex++;
993
+ if (index >= items.length) {
994
+ return;
995
+ }
996
+ results[index] = await worker(items[index], index);
997
+ }
998
+ }
999
+ const runners = [];
1000
+ for (let i = 0; i < effectiveLimit; i++) {
1001
+ runners.push(runWorker());
1002
+ }
1003
+ await Promise.all(runners);
1004
+ return results;
1005
+ }
1006
+
1007
+ // src/relation/execution-plan.ts
1008
+ function relationResultPaths(select, metadata, parentPath = "$") {
1009
+ const out = [];
1010
+ const relations = detectRelationFields(select, metadata);
1011
+ for (const rel of [...relations].sort(
1012
+ (a, b) => a.propertyName.localeCompare(b.propertyName)
1013
+ )) {
1014
+ const childSelect = normalizeSelectSpec(select[rel.propertyName]).select ?? {};
1015
+ const targetMeta = MetadataRegistry.get(rel.targetFactory());
1016
+ const childPath = `${parentPath}.${rel.propertyName}`;
1017
+ const isMany = rel.type === "hasMany";
1018
+ const resultPath = isMany ? `${childPath}.items` : childPath;
1019
+ out.push({
1020
+ propertyName: rel.propertyName,
1021
+ parentPath,
1022
+ resultPath,
1023
+ isMany
1024
+ });
1025
+ out.push(...relationResultPaths(childSelect, targetMeta, resultPath));
1026
+ }
1027
+ return out;
1028
+ }
1029
+ function parentResultPath(resultPath) {
1030
+ if (!resultPath.startsWith("$.")) return "$";
1031
+ const tokens = resultPath.slice(2).split(".");
1032
+ if (tokens[tokens.length - 1] === "items") tokens.pop();
1033
+ tokens.pop();
1034
+ return tokens.length === 0 ? "$" : `$.${tokens.join(".")}`;
1035
+ }
1036
+ function deriveExecutionPlan(resultPaths) {
1037
+ if (resultPaths.length <= 1) return void 0;
1038
+ const indexByPath = /* @__PURE__ */ new Map();
1039
+ resultPaths.forEach((path, i) => {
1040
+ indexByPath.set(path, i);
1041
+ });
1042
+ const stageOf = new Array(resultPaths.length);
1043
+ for (let i = 0; i < resultPaths.length; i++) {
1044
+ if (resultPaths[i] === "$") {
1045
+ stageOf[i] = 0;
1046
+ continue;
1047
+ }
1048
+ const parentPath = parentResultPath(resultPaths[i]);
1049
+ const parentIndex = indexByPath.get(parentPath);
1050
+ const parentStage = parentIndex !== void 0 ? stageOf[parentIndex] : 0;
1051
+ stageOf[i] = parentStage + 1;
1052
+ }
1053
+ const stageCount = Math.max(...stageOf) + 1;
1054
+ const groups = Array.from({ length: stageCount }, () => []);
1055
+ for (let i = 0; i < resultPaths.length; i++) {
1056
+ groups[stageOf[i]].push(i);
1057
+ }
1058
+ for (const g of groups) g.sort((a, b) => a - b);
1059
+ return { groups, concurrency: RELATION_TRAVERSAL_CONCURRENCY };
1060
+ }
1061
+ function buildRelationExecutionPlan(select, metadata) {
1062
+ const relationPaths = relationResultPaths(select, metadata);
1063
+ if (relationPaths.length === 0) return void 0;
1064
+ const resultPaths = ["$", ...relationPaths.map((r) => r.resultPath)];
1065
+ const plan2 = deriveExecutionPlan(resultPaths);
1066
+ if (plan2 === void 0) return void 0;
1067
+ return { plan: plan2, resultPaths };
1068
+ }
1069
+ function deriveCompositionPlan(composeCount) {
1070
+ if (composeCount <= 0) {
1071
+ return { stages: [], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
1072
+ }
1073
+ const stage = Array.from({ length: composeCount }, (_unused, i) => i);
1074
+ return { stages: [stage], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
1075
+ }
1076
+ function stageOfResultPath(resolved, resultPath) {
1077
+ const index = resolved.resultPaths.indexOf(resultPath);
1078
+ if (index < 0) return void 0;
1079
+ for (let s = 0; s < resolved.plan.groups.length; s++) {
1080
+ if (resolved.plan.groups[s].includes(index)) return s;
1081
+ }
1082
+ return void 0;
1083
+ }
1084
+
1085
+ // src/relation/traversal.ts
1086
+ function evaluateFilterClientSide(item, filter, _targetMeta) {
1087
+ return evaluateFilter(item, filter);
1088
+ }
1089
+ function relationSpec(value) {
1090
+ return normalizeSelectSpec(value);
1091
+ }
1092
+ function validateDepth(select, metadata, maxDepth, currentDepth = 1) {
1093
+ for (const [fieldName, value] of Object.entries(select)) {
1094
+ if (typeof value === "object" && value !== null && ("select" in value || isSelectBuilder(value))) {
1095
+ const rel = metadata.relations.find((r) => r.propertyName === fieldName);
1096
+ if (!rel) continue;
1097
+ if (currentDepth > maxDepth) {
1098
+ throw new Error(
1099
+ `Relation traversal depth exceeded: '${fieldName}' at depth ${currentDepth} exceeds maxDepth ${maxDepth}. Set maxDepth: ${currentDepth} to allow this.`
1100
+ );
1101
+ }
1102
+ const targetClass = rel.targetFactory();
1103
+ const targetMeta = MetadataRegistry.get(targetClass);
1104
+ const innerSelect = relationSpec(value).select;
1105
+ validateDepth(innerSelect, targetMeta, maxDepth, currentDepth + 1);
1106
+ }
1107
+ }
1108
+ }
1109
+ function relationResultPath(parentPath, rel) {
1110
+ const base = `${parentPath}.${rel.propertyName}`;
1111
+ return rel.type === "hasMany" ? `${base}.items` : base;
1112
+ }
1113
+ function stageRelations(relations, parentPath, plan2) {
1114
+ if (plan2 === void 0 || relations.length === 0) {
1115
+ return relations.length > 0 ? { stages: [[...relations]], concurrency: RELATION_TRAVERSAL_CONCURRENCY } : { stages: [], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
1116
+ }
1117
+ const byStage = /* @__PURE__ */ new Map();
1118
+ const unstaged = [];
1119
+ for (const rel of relations) {
1120
+ const stage = stageOfResultPath(plan2, relationResultPath(parentPath, rel));
1121
+ if (stage === void 0) {
1122
+ unstaged.push(rel);
1123
+ continue;
1124
+ }
1125
+ const bucket = byStage.get(stage);
1126
+ if (bucket) bucket.push(rel);
1127
+ else byStage.set(stage, [rel]);
1128
+ }
1129
+ const stages = [...byStage.keys()].sort((a, b) => a - b).map((s) => byStage.get(s));
1130
+ if (unstaged.length > 0) stages.push(unstaged);
1131
+ return { stages, concurrency: plan2.plan.concurrency };
1132
+ }
1133
+ async function runStaged(relations, parentPath, plan2, worker) {
1134
+ const { stages, concurrency } = stageRelations(relations, parentPath, plan2);
1135
+ for (const stage of stages) {
1136
+ await mapWithConcurrency(stage, concurrency, (rel) => worker(rel));
1137
+ }
1138
+ }
1139
+ function hasCompleteQueryKey(queryKey) {
1140
+ return Object.values(queryKey).every((value) => value != null);
1141
+ }
1142
+ function matchesQueryKey(item, queryKey) {
1143
+ return Object.entries(queryKey).every(
1144
+ ([field, value]) => item[field] === value
1145
+ );
1146
+ }
1147
+ async function fetchBelongsToHasOneBatch(rel, rawParentItems, selectSpec, targetMeta, options, currentDepth, parentPath) {
1148
+ const results = rawParentItems.map(
1149
+ () => null
1150
+ );
1151
+ const queryKeys = [];
1152
+ const parentIndexes = [];
1153
+ for (let i = 0; i < rawParentItems.length; i++) {
1154
+ const queryKey = buildRelationQueryKey(rel, rawParentItems[i]);
1155
+ if (!hasCompleteQueryKey(queryKey)) {
1156
+ continue;
1157
+ }
1158
+ queryKeys.push(queryKey);
1159
+ parentIndexes.push(i);
1160
+ }
1161
+ if (queryKeys.length === 0) {
1162
+ return results;
1163
+ }
1164
+ const implicitKeys = getImplicitKeyFields(selectSpec.select, targetMeta);
1165
+ const projectionExtras = options.updatable ? [...implicitKeys, "PK", "SK"] : implicitKeys;
1166
+ const batchOps = planBatchGetForQueryKeys(
1167
+ targetMeta,
1168
+ queryKeys,
1169
+ selectSpec.select,
1170
+ projectionExtras.length > 0 ? projectionExtras : void 0
1171
+ );
1172
+ const executor = ClientManager.getExecutor();
1173
+ const queryKeyToRaw = /* @__PURE__ */ new Map();
1174
+ for (const operation of batchOps) {
1175
+ const batchResult = await executor.batchGet(operation);
1176
+ for (const item of batchResult.items) {
1177
+ for (const queryKey of queryKeys) {
1178
+ if (!matchesQueryKey(item, queryKey)) {
1179
+ continue;
1180
+ }
1181
+ queryKeyToRaw.set(serializeQueryKey(queryKey), item);
1182
+ }
1183
+ }
1184
+ }
1185
+ const nestedRelations = detectRelationFields(selectSpec.select, targetMeta);
1186
+ const ownPath = relationResultPath(parentPath, rel);
1187
+ await mapWithConcurrency(
1188
+ queryKeys,
1189
+ options.plan?.plan.concurrency ?? RELATION_TRAVERSAL_CONCURRENCY,
1190
+ async (queryKey, j) => {
1191
+ const parentIndex = parentIndexes[j];
1192
+ const raw = queryKeyToRaw.get(serializeQueryKey(queryKey));
1193
+ if (!raw) {
1194
+ return;
1195
+ }
1196
+ const updatable = options.updatable ?? false;
1197
+ let resolvedItem = hydrate([raw], selectSpec.select, targetMeta, updatable)[0] ?? null;
1198
+ if (resolvedItem && nestedRelations.length > 0) {
1199
+ resolvedItem = await resolveRelations(
1200
+ resolvedItem,
1201
+ raw,
1202
+ selectSpec.select,
1203
+ targetMeta,
1204
+ options,
1205
+ currentDepth + 1,
1206
+ ownPath
1207
+ );
1208
+ if (resolvedItem && updatable) {
1209
+ attachHiddenKey(resolvedItem, raw);
1210
+ }
1211
+ }
1212
+ if (resolvedItem && !passesPostLoad(resolvedItem, selectSpec, targetMeta)) {
1213
+ results[parentIndex] = null;
1214
+ } else {
1215
+ results[parentIndex] = resolvedItem;
1216
+ }
1217
+ }
1218
+ );
1219
+ return results;
1220
+ }
1221
+ function passesPostLoad(item, spec, targetMeta) {
1222
+ if (spec.filter && !evaluateFilterClientSide(item, spec.filter, targetMeta)) {
1223
+ return false;
1224
+ }
1225
+ return true;
1226
+ }
1227
+ async function resolveSingleValueRelationsBatch(parentItems, rawParentItems, select, metadata, options, currentDepth, parentPath) {
1228
+ const results = parentItems.map((item, i) => {
1229
+ const copy = { ...item };
1230
+ if (options.updatable) {
1231
+ attachHiddenKey(copy, rawParentItems[i]);
1232
+ }
1233
+ return copy;
1234
+ });
1235
+ const relations = detectRelationFields(select, metadata).filter(
1236
+ (rel) => rel.type === "belongsTo" || rel.type === "hasOne"
1237
+ );
1238
+ await runStaged(relations, parentPath, options.plan, async (rel) => {
1239
+ const selectSpec = relationSpec(select[rel.propertyName]);
1240
+ const targetClass = rel.targetFactory();
1241
+ const targetMeta = MetadataRegistry.get(targetClass);
1242
+ const batchResults = await fetchBelongsToHasOneBatch(
1243
+ rel,
1244
+ rawParentItems,
1245
+ selectSpec,
1246
+ targetMeta,
1247
+ options,
1248
+ currentDepth,
1249
+ parentPath
1250
+ );
1251
+ for (let i = 0; i < results.length; i++) {
1252
+ results[i][rel.propertyName] = batchResults[i];
1253
+ }
1254
+ });
1255
+ return results;
1256
+ }
1257
+ async function resolveRelations(parentItem, rawParentItem, select, metadata, options, currentDepth = 1, currentPath = "$") {
1258
+ const result = { ...parentItem };
1259
+ const relations = detectRelationFields(select, metadata);
1260
+ const plan2 = options.plan ?? (currentPath === "$" ? buildRelationExecutionPlan(select, metadata) : void 0);
1261
+ const opts = plan2 === options.plan ? options : { ...options, plan: plan2 };
1262
+ await runStaged(relations, currentPath, opts.plan, async (rel) => {
1263
+ const selectSpec = relationSpec(select[rel.propertyName]);
1264
+ const ownPath = relationResultPath(currentPath, rel);
1265
+ if (rel.type === "hasMany") {
1266
+ const targetClass2 = rel.targetFactory();
1267
+ const queryKey = buildRelationQueryKey(rel, rawParentItem);
1268
+ const limit = selectSpec.limit ?? rel.options?.limit?.default ?? 20;
1269
+ const order = selectSpec.order ?? rel.options?.order ?? "ASC";
1270
+ const listResult = await executeListInternal(targetClass2, queryKey, selectSpec.select, {
1271
+ limit,
1272
+ after: selectSpec.after,
1273
+ order,
1274
+ filter: selectSpec.filter,
1275
+ updatable: opts.updatable
1276
+ });
1277
+ let items = listResult.items;
1278
+ const rawItems = listResult.rawItems;
1279
+ const targetMeta2 = MetadataRegistry.get(targetClass2);
1280
+ const nestedRelations = detectRelationFields(selectSpec.select, targetMeta2);
1281
+ if (nestedRelations.length > 0) {
1282
+ items = await resolveSingleValueRelationsBatch(
1283
+ items,
1284
+ rawItems,
1285
+ selectSpec.select,
1286
+ targetMeta2,
1287
+ opts,
1288
+ currentDepth + 1,
1289
+ ownPath
1290
+ );
1291
+ }
1292
+ result[rel.propertyName] = {
1293
+ items,
1294
+ cursor: listResult.cursor
1295
+ };
1296
+ return;
1297
+ }
1298
+ const targetClass = rel.targetFactory();
1299
+ const targetMeta = MetadataRegistry.get(targetClass);
1300
+ const [resolvedItem] = await fetchBelongsToHasOneBatch(
1301
+ rel,
1302
+ [rawParentItem],
1303
+ selectSpec,
1304
+ targetMeta,
1305
+ opts,
1306
+ currentDepth,
1307
+ currentPath
1308
+ );
1309
+ result[rel.propertyName] = resolvedItem;
1310
+ });
1311
+ return result;
1312
+ }
1313
+
1314
+ // src/operations/query.ts
1315
+ async function executeQuery(modelClass, key, selectSpec, options) {
1316
+ const metadata = MetadataRegistry.get(modelClass);
1317
+ const queryFields = Object.keys(key);
1318
+ const resolved = resolveKey(queryFields, metadata);
1319
+ if (resolved.type === "gsi" && !resolved.unique) {
1320
+ throw new Error(
1321
+ `Cannot use query() with non-unique GSI '${resolved.indexName}'. Use list() instead.`
1322
+ );
1323
+ }
1324
+ const normalized = normalizeTopLevelSelect(selectSpec);
1325
+ const select = normalized.select;
1326
+ const filter = normalized.filter;
1327
+ const maxDepth = options?.maxDepth ?? 1;
1328
+ const relations = detectRelationFields(select, metadata);
1329
+ if (relations.length > 0) {
1330
+ validateDepth(select, metadata, maxDepth);
1331
+ }
1332
+ const implicitKeys = getImplicitKeyFields(select, metadata);
1333
+ const updatable = options?.updatable ?? false;
1334
+ const executionPlan = plan(metadata, {
1335
+ key,
1336
+ select,
1337
+ filter,
1338
+ consistentRead: options?.consistentRead,
1339
+ additionalProjectionFields: implicitKeys.length > 0 ? implicitKeys : void 0,
1340
+ updatable
1341
+ });
1342
+ const operation = executionPlan.operations[0];
1343
+ const canParallelizeRelations = relations.length > 0 && relations.every(
1344
+ (rel) => Object.values(rel.keyBinding).every(
1345
+ (sourceField) => key[sourceField] != null
1346
+ )
1347
+ );
1348
+ const parentPromise = execute(operation);
1349
+ const relationPromise = canParallelizeRelations ? resolveRelations({}, key, select, metadata, { maxDepth, updatable }, 1) : null;
1350
+ let result;
1351
+ try {
1352
+ result = await parentPromise;
1353
+ } catch (err) {
1354
+ if (relationPromise) await relationPromise.catch(() => void 0);
1355
+ throw err;
1356
+ }
1357
+ if (result.items.length === 0) {
1358
+ if (relationPromise) await relationPromise.catch(() => void 0);
1359
+ return null;
1360
+ }
1361
+ const rawItem = result.items[0];
1362
+ const hydrated = hydrate(result.items, select, metadata, updatable);
1363
+ let hydratedItem = hydrated[0] ?? null;
1364
+ if (hydratedItem && relations.length > 0) {
1365
+ if (relationPromise) {
1366
+ const resolvedRelations = await relationPromise;
1367
+ for (const rel of relations) {
1368
+ hydratedItem[rel.propertyName] = resolvedRelations[rel.propertyName];
1369
+ }
1370
+ } else {
1371
+ hydratedItem = await resolveRelations(
1372
+ hydratedItem,
1373
+ rawItem,
1374
+ select,
1375
+ metadata,
1376
+ { maxDepth, updatable },
1377
+ 1
1378
+ );
1379
+ if (hydratedItem && updatable) {
1380
+ attachHiddenKey(hydratedItem, rawItem);
1381
+ }
1382
+ }
1383
+ }
1384
+ if (options?.hydrate) {
1385
+ return options.hydrate(hydratedItem);
1386
+ }
1387
+ return hydratedItem;
1388
+ }
1389
+
1390
+ // src/operations/explain.ts
1391
+ function executeExplain(modelClass, key, options) {
1392
+ const metadata = MetadataRegistry.get(modelClass);
1393
+ const exclusiveStartKey = options?.after ? decodeCursor(options.after) : void 0;
1394
+ const normalized = normalizeTopLevelSelect(options?.select);
1395
+ const select = normalized.select ?? {};
1396
+ const filter = options?.filter ?? normalized.filter;
1397
+ const mainPlan = plan(metadata, {
1398
+ key,
1399
+ select,
1400
+ limit: options?.limit,
1401
+ after: exclusiveStartKey,
1402
+ order: options?.order,
1403
+ consistentRead: options?.consistentRead,
1404
+ filter
1405
+ });
1406
+ const relationOps = planRelationOperations(metadata, select, {
1407
+ estimatedParentCount: 1
1408
+ });
1409
+ return {
1410
+ operations: [...mainPlan.operations, ...relationOps]
1411
+ };
1412
+ }
1413
+
1414
+ // src/operations/batch.ts
1415
+ var BatchGetResult = class {
1416
+ groups;
1417
+ constructor(groups) {
1418
+ this.groups = groups;
1419
+ }
1420
+ get(model) {
1421
+ const modelClass = resolveModelClass(model);
1422
+ return this.groups.get(modelClass) ?? [];
1423
+ }
1424
+ };
1425
+ function buildScalarSelect(metadata) {
1426
+ const select = {};
1427
+ for (const field of metadata.fields) {
1428
+ select[field.propertyName] = true;
1429
+ }
1430
+ return select;
1431
+ }
1432
+ function buildDynamoKey(modelClass, keyObj) {
1433
+ const deleteInput = buildDeleteInput(modelClass, keyObj);
1434
+ return deleteInput.Key;
1435
+ }
1436
+ function hydrateBatchItems(rawItems, metadata) {
1437
+ if (rawItems.length === 0) {
1438
+ return [];
1439
+ }
1440
+ const select = buildScalarSelect(metadata);
1441
+ const hydrated = hydrate(rawItems, select, metadata);
1442
+ if (metadata.embeddedFields.length === 0) {
1443
+ return hydrated;
1444
+ }
1445
+ return hydrated.map((item, index) => {
1446
+ const raw = rawItems[index];
1447
+ const merged = { ...item };
1448
+ for (const emb of metadata.embeddedFields) {
1449
+ const rawEmb = raw?.[emb.propertyName];
1450
+ if (rawEmb !== void 0 && rawEmb !== null) {
1451
+ merged[emb.propertyName] = rawEmb;
1452
+ }
1453
+ }
1454
+ return merged;
1455
+ });
1456
+ }
1457
+ async function executeBatchGetForTable(tableName, entries) {
1458
+ const groups = /* @__PURE__ */ new Map();
1459
+ if (entries.length === 0) {
1460
+ return groups;
1461
+ }
1462
+ const keys = entries.map((entry) => entry.dynamoKey);
1463
+ const { items: rawItems } = await ClientManager.getExecutor().batchGet({
1464
+ tableName,
1465
+ keys
1466
+ });
1467
+ const entriesByModel = /* @__PURE__ */ new Map();
1468
+ for (const entry of entries) {
1469
+ const existing = entriesByModel.get(entry.modelClass) ?? [];
1470
+ existing.push(entry);
1471
+ entriesByModel.set(entry.modelClass, existing);
1472
+ }
1473
+ for (const [modelClass, modelEntries] of entriesByModel) {
1474
+ const metadata = modelEntries[0].metadata;
1475
+ const expectedKeys = new Set(
1476
+ modelEntries.map(
1477
+ (entry) => `${String(entry.dynamoKey.PK)}::${String(entry.dynamoKey.SK)}`
1478
+ )
1479
+ );
1480
+ const modelRawItems = rawItems.filter(
1481
+ (item) => expectedKeys.has(`${String(item.PK)}::${String(item.SK)}`)
1482
+ );
1483
+ groups.set(modelClass, hydrateBatchItems(modelRawItems, metadata));
1484
+ }
1485
+ return groups;
1486
+ }
1487
+ async function executeBatchGet(requests) {
1488
+ const entriesByTable = /* @__PURE__ */ new Map();
1489
+ for (const request of requests) {
1490
+ const modelClass = resolveModelClass(request.model);
1491
+ const metadata = MetadataRegistry.get(modelClass);
1492
+ const tableName = TableMapping.resolve(metadata.tableName);
1493
+ for (const keyObj of request.keys) {
1494
+ const entry = {
1495
+ modelClass,
1496
+ metadata,
1497
+ dynamoKey: buildDynamoKey(modelClass, keyObj)
1498
+ };
1499
+ const existing = entriesByTable.get(tableName) ?? [];
1500
+ existing.push(entry);
1501
+ entriesByTable.set(tableName, existing);
1502
+ }
1503
+ }
1504
+ const mergedGroups = /* @__PURE__ */ new Map();
1505
+ for (const [, entries] of entriesByTable) {
1506
+ const tableName = TableMapping.resolve(entries[0].metadata.tableName);
1507
+ const tableGroups = await executeBatchGetForTable(tableName, entries);
1508
+ for (const [modelClass, items] of tableGroups) {
1509
+ const existing = mergedGroups.get(modelClass) ?? [];
1510
+ mergedGroups.set(modelClass, existing.concat(items));
1511
+ }
1512
+ }
1513
+ return new BatchGetResult(mergedGroups);
1514
+ }
1515
+ function toExecItem(entry) {
1516
+ if (entry.request.type === "put") {
1517
+ return { type: "put", item: entry.request.putInput.Item };
1518
+ }
1519
+ return { type: "delete", key: entry.request.deleteInput.Key };
1520
+ }
1521
+ async function executeBatchWrite(requests) {
1522
+ const entriesByTable = /* @__PURE__ */ new Map();
1523
+ for (const request of requests) {
1524
+ const modelClass = resolveModelClass(request.model);
1525
+ const metadata = MetadataRegistry.get(modelClass);
1526
+ const tableName = TableMapping.resolve(metadata.tableName);
1527
+ if (request.type === "put") {
1528
+ const putInput = buildPutInput(modelClass, request.item, request.options);
1529
+ const entry2 = {
1530
+ tableName,
1531
+ modelName: modelClass.name,
1532
+ request: { type: "put", putInput }
1533
+ };
1534
+ const existing2 = entriesByTable.get(tableName) ?? [];
1535
+ existing2.push(entry2);
1536
+ entriesByTable.set(tableName, existing2);
1537
+ continue;
1538
+ }
1539
+ const deleteInput = buildDeleteInput(modelClass, request.key, request.options);
1540
+ const entry = {
1541
+ tableName,
1542
+ modelName: modelClass.name,
1543
+ request: { type: "delete", deleteInput }
1544
+ };
1545
+ const existing = entriesByTable.get(tableName) ?? [];
1546
+ existing.push(entry);
1547
+ entriesByTable.set(tableName, existing);
1548
+ }
1549
+ const executor = ClientManager.getExecutor();
1550
+ for (const [tableName, entries] of entriesByTable) {
1551
+ await executor.batchWrite(tableName, entries.map(toExecItem));
1552
+ }
1553
+ if (ChangeCaptureRegistry.hasSubscribers()) {
1554
+ for (const [, entries] of entriesByTable) {
1555
+ for (const entry of entries) {
1556
+ if (entry.request.type === "put") {
1557
+ const item = entry.request.putInput.Item;
1558
+ captureWrite({
1559
+ table: entry.tableName,
1560
+ ...entry.modelName ? { model: entry.modelName } : {},
1561
+ op: "put",
1562
+ keys: { pk: String(item.PK), sk: String(item.SK ?? "") },
1563
+ newItem: item
1564
+ });
1565
+ } else {
1566
+ const dkey = entry.request.deleteInput.Key;
1567
+ captureWrite({
1568
+ table: entry.tableName,
1569
+ ...entry.modelName ? { model: entry.modelName } : {},
1570
+ op: "delete",
1571
+ keys: { pk: String(dkey.PK), sk: String(dkey.SK ?? "") }
1572
+ });
1573
+ }
1574
+ }
1575
+ }
1576
+ }
1577
+ }
1578
+
1579
+ // src/define/entity-writes.ts
1580
+ var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
1581
+ "graphddb:lifecycleContract"
1582
+ );
1583
+ function isLifecycleContract(value) {
1584
+ return typeof value === "object" && value !== null && value[LIFECYCLE_CONTRACT_MARKER] === true;
1585
+ }
1586
+ var ENTITY_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:entityWrites");
1587
+ function isEntityWritesDefinition(value) {
1588
+ return typeof value === "object" && value !== null && value[ENTITY_WRITES_MARKER] === true;
1589
+ }
1590
+ function freezeEffects(effects) {
1591
+ const out = {};
1592
+ if (effects.requires !== void 0) out.requires = effects.requires;
1593
+ if (effects.unique !== void 0) out.unique = effects.unique;
1594
+ if (effects.edges !== void 0) out.edges = effects.edges;
1595
+ if (effects.derive !== void 0) out.derive = effects.derive;
1596
+ if (effects.emits !== void 0) out.emits = effects.emits;
1597
+ if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
1598
+ return out;
1599
+ }
1600
+ var recorder = {
1601
+ lifecycle(effects = {}) {
1602
+ return {
1603
+ [LIFECYCLE_CONTRACT_MARKER]: true,
1604
+ effects: freezeEffects(effects)
1605
+ };
1606
+ },
1607
+ exists(targetFactory, keys) {
1608
+ return { kind: "requires", targetFactory, keys };
1609
+ },
1610
+ unique(spec) {
1611
+ return { kind: "unique", name: spec.name, scope: spec.scope, fields: spec.fields };
1612
+ },
1613
+ putEdge(targetFactory, relationProperty) {
1614
+ return { kind: "putEdge", targetFactory, relationProperty };
1615
+ },
1616
+ deleteEdge(targetFactory, relationProperty) {
1617
+ return { kind: "deleteEdge", targetFactory, relationProperty };
1618
+ },
1619
+ increment(targetFactory, keys, attribute, amount) {
1620
+ return { kind: "derive", targetFactory, keys, attribute, amount };
1621
+ },
1622
+ event(name, payload) {
1623
+ return { kind: "event", name, payload };
1624
+ },
1625
+ idempotentBy(token) {
1626
+ return { kind: "idempotency", token };
1627
+ }
1628
+ };
1629
+ function entityWrites(builder) {
1630
+ const shape = builder(recorder);
1631
+ for (const phase of ["create", "update", "remove"]) {
1632
+ const contract = shape[phase];
1633
+ if (contract !== void 0 && !isLifecycleContract(contract)) {
1634
+ throw new Error(
1635
+ `entityWrites: the '${phase}' lifecycle must be built with \`w.lifecycle({...})\` (it carries the \xA72 effect arrays), but received ${JSON.stringify(contract)}.`
1636
+ );
1637
+ }
1638
+ }
1639
+ if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
1640
+ throw new Error(
1641
+ "entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`); an empty save contract declares nothing."
1642
+ );
1643
+ }
1644
+ return {
1645
+ [ENTITY_WRITES_MARKER]: true,
1646
+ ...shape.create !== void 0 ? { create: shape.create } : {},
1647
+ ...shape.update !== void 0 ? { update: shape.update } : {},
1648
+ ...shape.remove !== void 0 ? { remove: shape.remove } : {}
1649
+ };
1650
+ }
1651
+ function getEntityWrites(modelClass) {
1652
+ for (const name of Object.getOwnPropertyNames(modelClass)) {
1653
+ let value;
1654
+ try {
1655
+ value = modelClass[name];
1656
+ } catch {
1657
+ continue;
1658
+ }
1659
+ if (isEntityWritesDefinition(value)) return value;
1660
+ }
1661
+ return void 0;
1662
+ }
1663
+ function lifecyclePhaseForIntent(intent) {
1664
+ return intent;
1665
+ }
1666
+
1667
+ // src/relation/edge-write.ts
1668
+ var OLD_VALUE_NAMESPACE = "old";
1669
+ var EDGE_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:edgeWrites");
1670
+ function isEdgeWritesDefinition(value) {
1671
+ return typeof value === "object" && value !== null && value[EDGE_WRITES_MARKER] === true;
1672
+ }
1673
+ function declaration(target, relationProperty) {
1674
+ return { targetFactory: target, relationProperty };
1675
+ }
1676
+ var recorder2 = {
1677
+ putEdge: declaration,
1678
+ deleteEdge: declaration,
1679
+ updateKeyChangeEdge: declaration
1680
+ };
1681
+ function edgeWrites(builder) {
1682
+ const edges = builder(recorder2);
1683
+ if (edges.length === 0) {
1684
+ throw new Error(
1685
+ "edgeWrites(...) must declare at least one edge (w.putEdge / w.deleteEdge / w.updateKeyChangeEdge); an empty declaration writes nothing."
1686
+ );
1687
+ }
1688
+ return { [EDGE_WRITES_MARKER]: true, edges };
519
1689
  }
520
1690
  function edgeKeyFields(segmented) {
521
1691
  return [
@@ -531,335 +1701,1244 @@ function assertRelationRoundTrips(entity, edgeFields, decl) {
531
1701
  );
532
1702
  if (!relation) {
533
1703
  throw new Error(
534
- `Edge write declaration references relation '${decl.relationProperty}' on '${targetClass.name}', which has no such relation. Declare the edge with the read-side relation property it materializes.`
1704
+ `Edge write declaration references relation '${decl.relationProperty}' on '${targetClass.name}', which has no such relation. Declare the edge with the read-side relation property it materializes.`
1705
+ );
1706
+ }
1707
+ const boundFields = Object.keys(relation.keyBinding);
1708
+ for (const targetField of boundFields) {
1709
+ if (!edgeFields.has(targetField)) {
1710
+ throw new Error(
1711
+ `Edge write declaration for relation '${decl.relationProperty}' on '${targetClass.name}' binds on field '${targetField}', which is not a key field of the adjacency entity '${entity.tableName}' edge (key fields: ${[...edgeFields].map((f) => `'${f}'`).join(", ")}). The written row would not be reachable by the relation's read \u2014 declare the edge on the entity whose key carries the relation's bound field.`
1712
+ );
1713
+ }
1714
+ }
1715
+ try {
1716
+ resolveKey(boundFields, entity);
1717
+ } catch (cause) {
1718
+ throw new Error(
1719
+ `Edge write declaration for relation '${decl.relationProperty}' on '${targetClass.name}' binds [${boundFields.map((f) => `'${f}'`).join(", ")}], which does not cover any access-pattern partition of the adjacency entity '${entity.tableName}' edge (base key [${edgeKeyFields(entity.primaryKey.segmented).map((f) => `'${f}'`).join(", ")}]). The relation's read could not resolve a partition for the written row, so the edge would be unreadable \u2014 bind every partition-key field of the index the relation reads through. (${cause.message})`
1720
+ );
1721
+ }
1722
+ return relation;
1723
+ }
1724
+ function templateRecord(fields) {
1725
+ const out = {};
1726
+ for (const field of [...fields].sort()) {
1727
+ out[field] = `{${field}}`;
1728
+ }
1729
+ return out;
1730
+ }
1731
+ function keyConditionTemplate(segmented, namespace) {
1732
+ const nameOf = namespace ? (f) => `${namespace}.${f}` : (f) => f;
1733
+ const { pk, sk } = evaluateKey(segmented, "param", void 0, nameOf);
1734
+ const out = { PK: pk };
1735
+ if (sk !== void 0) out.SK = sk;
1736
+ return out;
1737
+ }
1738
+ function putItemTemplate(edgeFields) {
1739
+ return templateRecord(edgeFields);
1740
+ }
1741
+ function deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle) {
1742
+ const entity = MetadataRegistry.get(adjacencyClass);
1743
+ if (!entity.primaryKey) {
1744
+ throw new Error(
1745
+ `Cannot derive edge write items for an entity with no primary key (table '${entity.tableName}'): an edge row needs a key to write / delete.`
1746
+ );
1747
+ }
1748
+ const segmented = entity.primaryKey.segmented;
1749
+ const edgeFields = edgeKeyFields(segmented);
1750
+ assertRelationRoundTrips(entity, new Set(edgeFields), decl);
1751
+ const tableName = TableMapping.resolve(entity.tableName);
1752
+ const entityName = adjacencyClass.name;
1753
+ const put = {
1754
+ type: "Put",
1755
+ tableName,
1756
+ entity: entityName,
1757
+ item: putItemTemplate(edgeFields)
1758
+ };
1759
+ const deleteNew = {
1760
+ type: "Delete",
1761
+ tableName,
1762
+ entity: entityName,
1763
+ keyCondition: keyConditionTemplate(segmented)
1764
+ };
1765
+ const deleteOld = {
1766
+ type: "Delete",
1767
+ tableName,
1768
+ entity: entityName,
1769
+ keyCondition: keyConditionTemplate(segmented, OLD_VALUE_NAMESPACE)
1770
+ };
1771
+ switch (lifecycle) {
1772
+ case "onCreate":
1773
+ return [put];
1774
+ case "onDelete":
1775
+ return [deleteNew];
1776
+ case "onUpdateKeyChange":
1777
+ return [deleteOld, put];
1778
+ }
1779
+ }
1780
+ function deriveEdgeWriteItems(adjacencyClass, writes, lifecycle) {
1781
+ const items = [];
1782
+ for (const decl of writes.edges) {
1783
+ items.push(...deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle));
1784
+ }
1785
+ return items;
1786
+ }
1787
+ function getEdgeWrites(modelClass) {
1788
+ for (const name of Object.getOwnPropertyNames(modelClass)) {
1789
+ let value;
1790
+ try {
1791
+ value = modelClass[name];
1792
+ } catch {
1793
+ continue;
1794
+ }
1795
+ if (isEdgeWritesDefinition(value)) return value;
1796
+ }
1797
+ return void 0;
1798
+ }
1799
+ function deriveModelEdgeWriteItems(modelClass, lifecycle) {
1800
+ const writes = getEdgeWrites(modelClass);
1801
+ if (!writes) {
1802
+ throw new Error(
1803
+ `Model '${modelClass.name}' declares no edge write side (no static \`writes = edgeWrites(...)\`). Declare it to derive adjacency items.`
1804
+ );
1805
+ }
1806
+ return deriveEdgeWriteItems(modelClass, writes, lifecycle);
1807
+ }
1808
+
1809
+ // src/runtime/command-runtime.ts
1810
+ function resolveCommandMethod(contract, methodName) {
1811
+ const method = contract.methods[methodName];
1812
+ if (method === void 0) {
1813
+ throw new Error(
1814
+ `Contract runtime: command method '${methodName}' does not exist on this contract. Available methods: ${Object.keys(contract.methods).sort().join(", ") || "(none)"}.`
535
1815
  );
536
1816
  }
537
- const boundFields = Object.keys(relation.keyBinding);
538
- for (const targetField of boundFields) {
539
- if (!edgeFields.has(targetField)) {
1817
+ return method;
1818
+ }
1819
+ function modelClassOf(op) {
1820
+ return op.entity.modelClass;
1821
+ }
1822
+ function modelStaticOf(op) {
1823
+ return op.entity.modelClass.asModel();
1824
+ }
1825
+ function renderLeaf(leaf, key, params, context) {
1826
+ if (isContractParamRef(leaf)) {
1827
+ if (!(leaf.field in params)) {
540
1828
  throw new Error(
541
- `Edge write declaration for relation '${decl.relationProperty}' on '${targetClass.name}' binds on field '${targetField}', which is not a key field of the adjacency entity '${entity.tableName}' edge (key fields: ${[...edgeFields].map((f) => `'${f}'`).join(", ")}). The written row would not be reachable by the relation's read \u2014 declare the edge on the entity whose key carries the relation's bound field.`
1829
+ `Contract runtime: ${context} references params field '${leaf.field}', which was not supplied. Pass it in the method's params argument.`
542
1830
  );
543
1831
  }
1832
+ return params[leaf.field];
544
1833
  }
545
- try {
546
- resolveKey(boundFields, entity);
547
- } catch (cause) {
1834
+ if (isContractKeyFieldRef(leaf)) {
1835
+ if (leaf.field in key) return key[leaf.field];
1836
+ if (leaf.field in params) return params[leaf.field];
548
1837
  throw new Error(
549
- `Edge write declaration for relation '${decl.relationProperty}' on '${targetClass.name}' binds [${boundFields.map((f) => `'${f}'`).join(", ")}], which does not cover any access-pattern partition of the adjacency entity '${entity.tableName}' edge (base key [${edgeKeyFields(entity.primaryKey.segmented).map((f) => `'${f}'`).join(", ")}]). The relation's read could not resolve a partition for the written row, so the edge would be unreadable \u2014 bind every partition-key field of the index the relation reads through. (${cause.message})`
1838
+ `Contract runtime: ${context} references key field '${leaf.field}', which was not supplied. Pass it in the method's key or params argument.`
550
1839
  );
551
1840
  }
552
- return relation;
1841
+ return leaf;
553
1842
  }
554
- function templateRecord(fields) {
1843
+ function renderRecord(structure, key, params, context) {
555
1844
  const out = {};
556
- for (const field of [...fields].sort()) {
557
- out[field] = `{${field}}`;
1845
+ if (structure === void 0) return out;
1846
+ for (const [field, leaf] of Object.entries(structure)) {
1847
+ out[field] = renderLeaf(leaf, key, params, `${context} field '${field}'`);
558
1848
  }
559
1849
  return out;
560
1850
  }
561
- function keyConditionTemplate(segmented, namespace) {
562
- const nameOf = namespace ? (f) => `${namespace}.${f}` : (f) => f;
563
- const { pk, sk } = evaluateKey(segmented, "param", void 0, nameOf);
564
- const out = { PK: pk };
565
- if (sk !== void 0) out.SK = sk;
1851
+ function renderCondition(condition, key, params, context) {
1852
+ if (condition === void 0) return void 0;
1853
+ if (isRawCondition(condition)) {
1854
+ const parts = condition.parts.map(
1855
+ (part) => isColumn(part) ? part : renderLeaf(part, key, params, `${context} condition`)
1856
+ );
1857
+ return { ...condition, parts };
1858
+ }
1859
+ if (typeof condition === "object" && condition !== null) {
1860
+ const obj = condition;
1861
+ if (obj.notExists === true) return { notExists: true };
1862
+ if (typeof obj.attributeExists === "string") {
1863
+ return { attributeExists: obj.attributeExists };
1864
+ }
1865
+ if (typeof obj.attributeNotExists === "string") {
1866
+ return { attributeNotExists: obj.attributeNotExists };
1867
+ }
1868
+ }
1869
+ return renderRecord(
1870
+ condition,
1871
+ key,
1872
+ params,
1873
+ `${context} condition`
1874
+ );
1875
+ }
1876
+ function renderWrite(op, key, params, contextLabel) {
1877
+ if (!isContractKeyRef(op.keys) && op.operation !== "put") {
1878
+ throw new Error(
1879
+ `Contract runtime: ${contextLabel} did not pass the contract key directly into its '${op.operation}' key position. A command method must call \`Model.${op.operation}(keys, \u2026)\` with the key whole.`
1880
+ );
1881
+ }
1882
+ const base = {
1883
+ operation: op.operation,
1884
+ modelClass: modelClassOf(op),
1885
+ modelStatic: modelStaticOf(op),
1886
+ key,
1887
+ condition: renderCondition(op.condition, key, params, contextLabel)
1888
+ };
1889
+ if (op.operation === "put") {
1890
+ return { ...base, item: renderRecord(op.item, key, params, `${contextLabel} item`) };
1891
+ }
1892
+ if (op.operation === "update") {
1893
+ return { ...base, changes: renderRecord(op.changes, key, params, `${contextLabel} changes`) };
1894
+ }
1895
+ return base;
1896
+ }
1897
+ async function executeSingleWrite(w) {
1898
+ const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
1899
+ if (w.operation === "put") {
1900
+ await executePut(w.modelClass, w.item ?? {}, options);
1901
+ return;
1902
+ }
1903
+ if (w.operation === "update") {
1904
+ await executeUpdate(w.modelClass, w.key, w.changes ?? {}, options);
1905
+ return;
1906
+ }
1907
+ await executeDelete(w.modelClass, w.key, options);
1908
+ }
1909
+ async function readBackProjection(write, returnSelection) {
1910
+ const select = {};
1911
+ for (const field of Object.keys(returnSelection)) {
1912
+ if (returnSelection[field] === true) select[field] = true;
1913
+ }
1914
+ const key = readBackKey(write);
1915
+ return executeQuery(write.modelClass, key, select, { consistentRead: true });
1916
+ }
1917
+ function readBackKey(write) {
1918
+ if (write.operation !== "put") return write.key;
1919
+ const metadata = MetadataRegistry.get(write.modelClass);
1920
+ const item = write.item ?? {};
1921
+ const key = {};
1922
+ const fields = metadata.primaryKey ? metadata.primaryKey.inputFieldNames : Object.keys(item);
1923
+ for (const field of fields) {
1924
+ if (field in item) key[field] = item[field];
1925
+ }
1926
+ return key;
1927
+ }
1928
+ function renderConditionCheck(check, key, params, contextLabel) {
1929
+ const modelClass = check.entity.modelClass;
1930
+ const referencedKey = {};
1931
+ for (const [targetField, inputField] of Object.entries(check.keyBinding)) {
1932
+ if (inputField in params) {
1933
+ referencedKey[targetField] = params[inputField];
1934
+ } else if (inputField in key) {
1935
+ referencedKey[targetField] = key[inputField];
1936
+ } else {
1937
+ throw new Error(
1938
+ `Contract runtime: ${contextLabel} asserts '${check.entity.name}' exists, keyed by input field '${inputField}', which was not supplied. Pass it in the method's params (or key) argument.`
1939
+ );
1940
+ }
1941
+ }
1942
+ const { TableName, Key } = buildDeleteInput(
1943
+ modelClass,
1944
+ referencedKey
1945
+ );
1946
+ const cond = buildConditionExpression({ attributeExists: "PK" });
1947
+ const checkInput = {
1948
+ TableName,
1949
+ Key,
1950
+ ConditionExpression: cond.expression
1951
+ };
1952
+ if (Object.keys(cond.names).length > 0) checkInput.ExpressionAttributeNames = cond.names;
1953
+ if (Object.keys(cond.values).length > 0) checkInput.ExpressionAttributeValues = cond.values;
1954
+ return checkInput;
1955
+ }
1956
+ var RUNTIME_TOKEN_RE = /\{([^{}]+)\}/g;
1957
+ function renderTemplate(template, key, params, contextLabel) {
1958
+ const resolveToken = (token) => {
1959
+ if (token in params) return params[token];
1960
+ if (token in key) return key[token];
1961
+ throw new Error(
1962
+ `Contract runtime: ${contextLabel} references input field '${token}', which was not supplied. Pass it in the method's params (or key) argument` + (token.startsWith("old.") ? ` \u2014 an \`old.*\` token is the pre-mutation value of a foreign key on an onUpdateKeyChange (pass it as \`{ '${token}': \u2026 }\`).` : `.`)
1963
+ );
1964
+ };
1965
+ const whole = /^\{([^{}]+)\}$/.exec(template);
1966
+ if (whole !== null) return resolveToken(whole[1]);
1967
+ return template.replace(RUNTIME_TOKEN_RE, (_m, token) => String(resolveToken(token)));
1968
+ }
1969
+ function renderTemplateRecord(record, key, params, contextLabel) {
1970
+ const out = {};
1971
+ for (const [field, template] of Object.entries(record ?? {})) {
1972
+ out[field] = renderTemplate(template, key, params, contextLabel);
1973
+ }
566
1974
  return out;
567
1975
  }
568
- function putItemTemplate(edgeFields) {
569
- return templateRecord(edgeFields);
1976
+ function renderEdgeWriteItems(edge, key, params, contextLabel, modelBySignature) {
1977
+ const adjacency = edge.entity.modelClass;
1978
+ return edge.items.map((item) => {
1979
+ if (item.type === "Put") {
1980
+ const rendered = renderTemplateRecord(item.item, key, params, `${contextLabel} edge Put`);
1981
+ const out2 = { Put: buildPutInput(adjacency, rendered) };
1982
+ modelBySignature?.set(execItemKeySignature(out2), adjacency.name);
1983
+ return out2;
1984
+ }
1985
+ const renderedKey = renderTemplateRecord(
1986
+ item.keyCondition,
1987
+ key,
1988
+ params,
1989
+ `${contextLabel} edge Delete`
1990
+ );
1991
+ const del = {
1992
+ TableName: TableMapping.resolve(item.tableName),
1993
+ Key: renderedKey
1994
+ };
1995
+ const out = { Delete: del };
1996
+ modelBySignature?.set(execItemKeySignature(out), adjacency.name);
1997
+ return out;
1998
+ });
570
1999
  }
571
- function deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle) {
572
- const entity = MetadataRegistry.get(adjacencyClass);
573
- if (!entity.primaryKey) {
2000
+ function renderDerivedUpdate(update, key, params, contextLabel, modelBySignature) {
2001
+ const metadata = MetadataRegistry.get(update.entity.modelClass);
2002
+ if (!metadata.primaryKey) {
574
2003
  throw new Error(
575
- `Cannot derive edge write items for an entity with no primary key (table '${entity.tableName}'): an edge row needs a key to write / delete.`
2004
+ `Contract runtime: ${contextLabel} derives a counter update on '${update.entity.name}', which has no primary key.`
576
2005
  );
577
2006
  }
578
- const segmented = entity.primaryKey.segmented;
579
- const edgeFields = edgeKeyFields(segmented);
580
- assertRelationRoundTrips(entity, new Set(edgeFields), decl);
581
- const tableName = TableMapping.resolve(entity.tableName);
582
- const entityName = adjacencyClass.name;
583
- const put = {
584
- type: "Put",
585
- tableName,
586
- entity: entityName,
587
- item: putItemTemplate(edgeFields)
2007
+ const keyInput = {};
2008
+ for (const [targetField, inputField] of Object.entries(update.keyBinding)) {
2009
+ keyInput[targetField] = renderTemplate(
2010
+ `{${inputField}}`,
2011
+ key,
2012
+ params,
2013
+ `${contextLabel} derived-update key`
2014
+ );
2015
+ }
2016
+ const { TableName, Key } = buildDeleteInput(
2017
+ update.entity.modelClass,
2018
+ keyInput
2019
+ );
2020
+ const updateInput = {
2021
+ TableName,
2022
+ Key,
2023
+ UpdateExpression: "ADD #a0 :a0",
2024
+ ExpressionAttributeNames: { "#a0": update.attribute },
2025
+ ExpressionAttributeValues: { ":a0": update.amount }
588
2026
  };
589
- const deleteNew = {
590
- type: "Delete",
591
- tableName,
592
- entity: entityName,
593
- keyCondition: keyConditionTemplate(segmented)
2027
+ const out = { Update: updateInput };
2028
+ modelBySignature?.set(execItemKeySignature(out), update.entity.modelClass.name);
2029
+ return out;
2030
+ }
2031
+ function renderUniqueGuardItems(guard, key, params, contextLabel) {
2032
+ return guard.items.map((item) => {
2033
+ if (item.type === "Put") {
2034
+ const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} guard Put`);
2035
+ const put = { TableName: TableMapping.resolve(item.tableName), Item };
2036
+ const cond = buildConditionExpression({ attributeNotExists: "PK" });
2037
+ put.ConditionExpression = cond.expression;
2038
+ if (Object.keys(cond.names).length > 0) put.ExpressionAttributeNames = cond.names;
2039
+ if (Object.keys(cond.values).length > 0) put.ExpressionAttributeValues = cond.values;
2040
+ return { Put: put };
2041
+ }
2042
+ const Key = renderTemplateRecord(item.keyCondition, key, params, `${contextLabel} guard Delete`);
2043
+ const del = { TableName: TableMapping.resolve(item.tableName), Key };
2044
+ return { Delete: del };
2045
+ });
2046
+ }
2047
+ function renderOutboxEventItems(event, key, params, contextLabel) {
2048
+ return event.items.map((item) => {
2049
+ const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} outbox Put`);
2050
+ const put = { TableName: TableMapping.resolve(item.tableName), Item };
2051
+ return { Put: put };
2052
+ });
2053
+ }
2054
+ function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
2055
+ return guard.items.map((item) => {
2056
+ const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} idempotency Put`);
2057
+ const put = { TableName: TableMapping.resolve(item.tableName), Item };
2058
+ const cond = buildConditionExpression({ attributeNotExists: "PK" });
2059
+ put.ConditionExpression = cond.expression;
2060
+ if (Object.keys(cond.names).length > 0) put.ExpressionAttributeNames = cond.names;
2061
+ if (Object.keys(cond.values).length > 0) put.ExpressionAttributeValues = cond.values;
2062
+ return { Put: put };
2063
+ });
2064
+ }
2065
+ async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map()) {
2066
+ const writeItems = writes.map((w) => {
2067
+ const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
2068
+ let item;
2069
+ if (w.operation === "put") {
2070
+ item = { Put: buildPutInput(w.modelClass, w.item ?? {}, options) };
2071
+ } else if (w.operation === "update") {
2072
+ item = { Update: buildUpdateInput(w.modelClass, w.key, w.changes ?? {}, options) };
2073
+ } else {
2074
+ item = { Delete: buildDeleteInput(w.modelClass, w.key, options) };
2075
+ }
2076
+ modelBySignature.set(execItemKeySignature(item), w.modelClass.name);
2077
+ return item;
2078
+ });
2079
+ const checkItems = checks.map((c) => ({ ConditionCheck: c.check }));
2080
+ await commitTransaction(
2081
+ [
2082
+ ...writeItems,
2083
+ ...edgeItems,
2084
+ ...updateItems,
2085
+ ...guardItemsRaw,
2086
+ ...outboxItems,
2087
+ ...idempotencyItems,
2088
+ ...checkItems
2089
+ ],
2090
+ {
2091
+ modelBySignature,
2092
+ limitError: (count) => new Error(
2093
+ `Contract runtime: command method '${methodName}' composes ${count} items (writes + edges + derived updates + uniqueness guards + outbox events + idempotency guard + ConditionChecks) into one TransactWriteItems, but DynamoDB caps a transaction at ${MAX_TRANSACT_ITEMS} items and a transaction is atomic \u2014 it cannot be split. Reduce the fragment / effect count.`
2094
+ )
2095
+ }
2096
+ );
2097
+ }
2098
+ async function executeTransactWrites(writes, methodName) {
2099
+ if (writes.length === 0) return;
2100
+ const modelBySignature = /* @__PURE__ */ new Map();
2101
+ const transactItems = writes.map((w) => {
2102
+ const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
2103
+ let item;
2104
+ if (w.operation === "put") {
2105
+ item = { Put: buildPutInput(w.modelClass, w.item ?? {}, options) };
2106
+ } else if (w.operation === "update") {
2107
+ item = { Update: buildUpdateInput(w.modelClass, w.key, w.changes ?? {}, options) };
2108
+ } else {
2109
+ item = { Delete: buildDeleteInput(w.modelClass, w.key, options) };
2110
+ }
2111
+ modelBySignature.set(execItemKeySignature(item), w.modelClass.name);
2112
+ return item;
2113
+ });
2114
+ await commitTransaction(transactItems, {
2115
+ modelBySignature,
2116
+ limitError: (count) => new Error(
2117
+ `Contract runtime: command method '${methodName}' resolves an array of ${count} keys to a 'transact' (TransactWriteItems) batch, but DynamoDB caps a transaction at ${MAX_TRANSACT_ITEMS} items and a transaction is atomic \u2014 it cannot be split across requests without breaking atomicity. Pass \u2264${MAX_TRANSACT_ITEMS} keys, or declare \`batch: 'batchWrite'\` (a non-atomic BatchWriteItem, chunked automatically) if atomicity is not required.`
2118
+ )
2119
+ });
2120
+ }
2121
+ async function executeParallelWrites(writes) {
2122
+ if (writes.length === 0) return [];
2123
+ const coalescible = writes.every(
2124
+ (w) => w.condition === void 0 && (w.operation === "put" || w.operation === "delete")
2125
+ );
2126
+ if (coalescible) {
2127
+ const requests = writes.map(
2128
+ (w) => w.operation === "put" ? { type: "put", model: w.modelStatic, item: w.item ?? {} } : { type: "delete", model: w.modelStatic, key: w.key }
2129
+ );
2130
+ await executeBatchWrite(requests);
2131
+ return writes.map(() => ({ ok: true }));
2132
+ }
2133
+ return Promise.all(
2134
+ writes.map(async (w) => {
2135
+ try {
2136
+ await executeSingleWrite(w);
2137
+ return { ok: true };
2138
+ } catch (e) {
2139
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
2140
+ }
2141
+ })
2142
+ );
2143
+ }
2144
+ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}) {
2145
+ const method = resolveCommandMethod(contract, methodName);
2146
+ if (Array.isArray(keyOrKeys)) {
2147
+ const keys = keyOrKeys;
2148
+ if (method.ops !== void 0 && method.ops.length > 1) {
2149
+ throw new Error(
2150
+ `Contract runtime: command method '${methodName}' is a multi-fragment mutation (it composes ${method.ops.length} fragments into one atomic TransactWriteItems) and accepts a single key only. Call it once per target, not with a key array.`
2151
+ );
2152
+ }
2153
+ const hasDerivedEffects2 = method.conditionChecks !== void 0 && method.conditionChecks.length > 0 || method.edgeWrites !== void 0 && method.edgeWrites.length > 0 || method.derivedUpdates !== void 0 && method.derivedUpdates.length > 0 || method.uniqueGuards !== void 0 && method.uniqueGuards.length > 0 || method.outboxEvents !== void 0 && method.outboxEvents.length > 0 || method.idempotencyGuard !== void 0;
2154
+ if (hasDerivedEffects2) {
2155
+ throw new Error(
2156
+ `Contract runtime: command method '${methodName}' carries derived effects (referential integrity / edge writes / derived counters / uniqueness guards / outbox events / idempotency guard \u2014 it composes its write + those items into one atomic TransactWriteItems) and accepts a single key only. Call it once per target, not with a key array.`
2157
+ );
2158
+ }
2159
+ const writes = keys.map(
2160
+ (key2, i) => renderWrite(method.op, key2, params, `command method '${methodName}' (key #${i})`)
2161
+ );
2162
+ if (method.mode === "parallel") {
2163
+ return { results: await executeParallelWrites(writes) };
2164
+ }
2165
+ await executeTransactWrites(writes, methodName);
2166
+ return;
2167
+ }
2168
+ const key = keyOrKeys;
2169
+ const conditionChecks = method.conditionChecks ?? [];
2170
+ const edgeWrites2 = method.edgeWrites ?? [];
2171
+ const derivedUpdates = method.derivedUpdates ?? [];
2172
+ const uniqueGuards = method.uniqueGuards ?? [];
2173
+ const outboxEvents = method.outboxEvents ?? [];
2174
+ const idempotencyGuard = method.idempotencyGuard;
2175
+ const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0;
2176
+ const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
2177
+ if (isMultiFragment || hasDerivedEffects) {
2178
+ const ops = method.ops ?? [method.op];
2179
+ const writes = ops.map(
2180
+ (opn, i) => renderWrite(opn, key, params, `command method '${methodName}' (fragment #${i})`)
2181
+ );
2182
+ if (hasDerivedEffects) {
2183
+ const modelBySignature = /* @__PURE__ */ new Map();
2184
+ const edgeItems = edgeWrites2.flatMap(
2185
+ (edge, i) => renderEdgeWriteItems(
2186
+ edge,
2187
+ key,
2188
+ params,
2189
+ `command method '${methodName}' (edge #${i})`,
2190
+ modelBySignature
2191
+ )
2192
+ );
2193
+ const updateItems = derivedUpdates.map(
2194
+ (update, i) => renderDerivedUpdate(
2195
+ update,
2196
+ key,
2197
+ params,
2198
+ `command method '${methodName}' (derive #${i})`,
2199
+ modelBySignature
2200
+ )
2201
+ );
2202
+ const guardItems = uniqueGuards.flatMap(
2203
+ (guard, i) => renderUniqueGuardItems(guard, key, params, `command method '${methodName}' (unique #${i})`)
2204
+ );
2205
+ const outboxItems = outboxEvents.flatMap(
2206
+ (event, i) => renderOutboxEventItems(event, key, params, `command method '${methodName}' (emit #${i})`)
2207
+ );
2208
+ const idempotencyItems = idempotencyGuard !== void 0 ? renderIdempotencyGuardItems(
2209
+ idempotencyGuard,
2210
+ key,
2211
+ params,
2212
+ `command method '${methodName}' (idempotency)`
2213
+ ) : [];
2214
+ const checks = conditionChecks.map((check, i) => ({
2215
+ check: renderConditionCheck(
2216
+ check,
2217
+ key,
2218
+ params,
2219
+ `command method '${methodName}' (requires #${i})`
2220
+ )
2221
+ }));
2222
+ await executeReferentialTransaction(
2223
+ writes,
2224
+ edgeItems,
2225
+ updateItems,
2226
+ guardItems,
2227
+ outboxItems,
2228
+ idempotencyItems,
2229
+ checks,
2230
+ methodName,
2231
+ modelBySignature
2232
+ );
2233
+ } else {
2234
+ await executeTransactWrites(writes, methodName);
2235
+ }
2236
+ if (method.returnSelection !== void 0 && Object.keys(method.returnSelection).length > 0) {
2237
+ return readBackProjection(writes[0], method.returnSelection);
2238
+ }
2239
+ return;
2240
+ }
2241
+ const write = renderWrite(method.op, key, params, `command method '${methodName}'`);
2242
+ await executeSingleWrite(write);
2243
+ if (method.returnSelection !== void 0 && Object.keys(method.returnSelection).length > 0) {
2244
+ return readBackProjection(write, method.returnSelection);
2245
+ }
2246
+ }
2247
+ function renderCompiledOp(op, ctx) {
2248
+ const fragment = op.fragment;
2249
+ const baseOp = op.condition !== void 0 ? withCondition(fragment.op, op.condition) : fragment.op;
2250
+ const modelBySignature = /* @__PURE__ */ new Map();
2251
+ const write = renderWrite(baseOp, op.params, op.params, ctx);
2252
+ const edgeItems = [];
2253
+ const updateItems = [];
2254
+ const guardItems = [];
2255
+ const outboxItems = [];
2256
+ const idempotencyItems = [];
2257
+ const checkItems = [];
2258
+ for (const edge of fragment.edgeWrites ?? []) {
2259
+ edgeItems.push(...renderEdgeWriteItems(edge, op.params, op.params, `${ctx} edge`, modelBySignature));
2260
+ }
2261
+ for (const u of fragment.derivedUpdates ?? []) {
2262
+ updateItems.push(renderDerivedUpdate(u, op.params, op.params, `${ctx} derive`, modelBySignature));
2263
+ }
2264
+ for (const g of fragment.uniqueGuards ?? []) {
2265
+ guardItems.push(...renderUniqueGuardItems(g, op.params, op.params, `${ctx} unique`));
2266
+ }
2267
+ for (const e of fragment.outboxEvents ?? []) {
2268
+ outboxItems.push(...renderOutboxEventItems(e, op.params, op.params, `${ctx} emit`));
2269
+ }
2270
+ if (fragment.idempotencyGuard !== void 0) {
2271
+ idempotencyItems.push(
2272
+ ...renderIdempotencyGuardItems(fragment.idempotencyGuard, op.params, op.params, `${ctx} idempotency`)
2273
+ );
2274
+ }
2275
+ for (const c of fragment.conditionChecks ?? []) {
2276
+ checkItems.push({ check: renderConditionCheck(c, op.params, op.params, `${ctx} requires`) });
2277
+ }
2278
+ const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || checkItems.length > 0;
2279
+ return {
2280
+ write,
2281
+ edgeItems,
2282
+ updateItems,
2283
+ guardItems,
2284
+ outboxItems,
2285
+ idempotencyItems,
2286
+ checkItems,
2287
+ modelBySignature,
2288
+ hasDerivedEffects
594
2289
  };
595
- const deleteOld = {
596
- type: "Delete",
597
- tableName,
598
- entity: entityName,
599
- keyCondition: keyConditionTemplate(segmented, OLD_VALUE_NAMESPACE)
2290
+ }
2291
+ function readBackForCompiledOp(op, write) {
2292
+ const sel = op.result?.select;
2293
+ if (sel === void 0 || Object.keys(sel).length === 0) return void 0;
2294
+ const key = {};
2295
+ for (const f of op.fragment.keyFields) {
2296
+ if (f in op.params) key[f] = op.params[f];
2297
+ }
2298
+ return executeQuery(write.modelClass, key, sel, {
2299
+ consistentRead: op.result?.options?.consistentRead ?? true,
2300
+ maxDepth: op.result?.options?.maxDepth
2301
+ });
2302
+ }
2303
+ async function executeCompiledWriteOp(ops, options) {
2304
+ const label = options.label;
2305
+ const renderedOps = ops.map((op, i) => renderCompiledOp(op, `${label} (op #${i})`));
2306
+ const rendered = renderedOps.map((r) => r.write);
2307
+ const modelBySignature = /* @__PURE__ */ new Map();
2308
+ const edgeItems = [];
2309
+ const updateItems = [];
2310
+ const guardItems = [];
2311
+ const outboxItems = [];
2312
+ const idempotencyItems = [];
2313
+ const checkItems = [];
2314
+ for (const r of renderedOps) {
2315
+ for (const [sig, name] of r.modelBySignature) modelBySignature.set(sig, name);
2316
+ edgeItems.push(...r.edgeItems);
2317
+ updateItems.push(...r.updateItems);
2318
+ guardItems.push(...r.guardItems);
2319
+ outboxItems.push(...r.outboxItems);
2320
+ idempotencyItems.push(...r.idempotencyItems);
2321
+ checkItems.push(...r.checkItems);
2322
+ }
2323
+ const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
2324
+ if (rendered.length === 1 && !hasDerivedEffects) {
2325
+ await executeSingleWrite(rendered[0]);
2326
+ } else {
2327
+ await executeReferentialTransaction(
2328
+ rendered,
2329
+ edgeItems,
2330
+ updateItems,
2331
+ guardItems,
2332
+ outboxItems,
2333
+ idempotencyItems,
2334
+ checkItems,
2335
+ label,
2336
+ modelBySignature
2337
+ );
2338
+ }
2339
+ const readBacks = await Promise.all(
2340
+ ops.map((op, i) => readBackForCompiledOp(op, rendered[i]))
2341
+ );
2342
+ return { readBacks };
2343
+ }
2344
+ async function executeCompiledParallelWrites(ops, options) {
2345
+ const label = options.label;
2346
+ const results = new Array(ops.length);
2347
+ const renderedOps = ops.map((op, i) => renderCompiledOp(op, `${label} (op #${i})`));
2348
+ const coalescibleIdx = [];
2349
+ const individualIdx = [];
2350
+ renderedOps.forEach((r, i) => {
2351
+ const w = r.write;
2352
+ if (!r.hasDerivedEffects && w.condition === void 0 && (w.operation === "put" || w.operation === "delete")) {
2353
+ coalescibleIdx.push(i);
2354
+ } else {
2355
+ individualIdx.push(i);
2356
+ }
2357
+ });
2358
+ await Promise.all([
2359
+ // The coalescible group → ONE BatchWriteItem (chunked, UnprocessedItems retry) via
2360
+ // the shared public-layer coalescing. On success every op reports `{ ok: true }`
2361
+ // plus its read-back; a BatchWriteItem request failure fails the whole group.
2362
+ (async () => {
2363
+ if (coalescibleIdx.length === 0) return;
2364
+ const writes = coalescibleIdx.map((i) => renderedOps[i].write);
2365
+ try {
2366
+ await executeParallelWrites(writes);
2367
+ await Promise.all(
2368
+ coalescibleIdx.map(async (i) => {
2369
+ const value = await readBackForCompiledOp(ops[i], renderedOps[i].write);
2370
+ results[i] = { ok: true, value };
2371
+ })
2372
+ );
2373
+ } catch (e) {
2374
+ const error = e instanceof Error ? e : new Error(String(e));
2375
+ for (const i of coalescibleIdx) results[i] = { ok: false, error };
2376
+ }
2377
+ })(),
2378
+ // Each individual op → its own single-op atomic execution (condition gate / derived
2379
+ // effects / read-back preserved), concurrently and with per-op partial success.
2380
+ ...individualIdx.map(async (i) => {
2381
+ try {
2382
+ const { readBacks } = await executeCompiledWriteOp([ops[i]], {
2383
+ label: `${label} (op #${i})`
2384
+ });
2385
+ results[i] = { ok: true, value: readBacks[0] };
2386
+ } catch (e) {
2387
+ results[i] = { ok: false, error: e instanceof Error ? e : new Error(String(e)) };
2388
+ }
2389
+ })
2390
+ ]);
2391
+ return results;
2392
+ }
2393
+ function withCondition(op, condition) {
2394
+ if (op.condition === void 0) return { ...op, condition };
2395
+ const base = op.condition;
2396
+ if (base.notExists === true && Object.keys(condition).length > 0) {
2397
+ throw new Error(
2398
+ `DDBModel.mutate: a \`condition\` (equality write gate) 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.`
2399
+ );
2400
+ }
2401
+ return { ...op, condition };
2402
+ }
2403
+
2404
+ // src/runtime/envelope.ts
2405
+ async function executeReadEnvelope(envelope) {
2406
+ const aliases = Object.keys(envelope);
2407
+ const results = await Promise.all(
2408
+ aliases.map((alias) => executeReadRoute(alias, envelope[alias]))
2409
+ );
2410
+ const out = {};
2411
+ aliases.forEach((alias, i) => {
2412
+ out[alias] = results[i];
2413
+ });
2414
+ return out;
2415
+ }
2416
+ async function executeReadRoute(alias, route) {
2417
+ if (route === null || typeof route !== "object") {
2418
+ throw new Error(
2419
+ `DDBModel.query: the descriptor for alias '${alias}' is not a read descriptor (\`{ query | list: Model, key, select, options? }\`).`
2420
+ );
2421
+ }
2422
+ const isQuery = route.query !== void 0;
2423
+ const isList = route.list !== void 0;
2424
+ if (isQuery === isList) {
2425
+ throw new Error(
2426
+ `DDBModel.query: the descriptor for alias '${alias}' must carry exactly one of \`query\` or \`list\` (the target model). It declared ${isQuery && isList ? "both" : "neither"}.`
2427
+ );
2428
+ }
2429
+ if (route.key === void 0 || route.select === void 0) {
2430
+ throw new Error(
2431
+ `DDBModel.query: the '${isQuery ? "query" : "list"}' descriptor for alias '${alias}' must declare both \`key\` and \`select\`.`
2432
+ );
2433
+ }
2434
+ const modelClass = resolveModelClass(
2435
+ resolveModelStatic(isQuery ? route.query : route.list, alias)
2436
+ );
2437
+ const opt = route.options ?? {};
2438
+ if (isQuery) {
2439
+ return executeQuery(modelClass, route.key, route.select, {
2440
+ consistentRead: opt.consistentRead,
2441
+ maxDepth: opt.maxDepth
2442
+ });
2443
+ }
2444
+ return executeList(modelClass, route.key, route.select, {
2445
+ limit: opt.limit,
2446
+ after: opt.after,
2447
+ order: opt.order,
2448
+ filter: opt.filter
2449
+ });
2450
+ }
2451
+ var INTENT_KEYS = ["create", "update", "remove"];
2452
+ async function executeWriteEnvelope(envelope, options = {}) {
2453
+ const mode = options.mode ?? "transaction";
2454
+ const aliases = Object.keys(envelope);
2455
+ if (aliases.length === 0) {
2456
+ throw new Error(
2457
+ "DDBModel.mutate: the write envelope is empty. Declare at least one `{ alias: { create | update | remove: Model, key, input? } }` operation."
2458
+ );
2459
+ }
2460
+ const ops = aliases.flatMap((alias) => normalizeWriteOp(alias, envelope[alias]));
2461
+ if (mode === "transaction") {
2462
+ return executeTransactionMode(ops);
2463
+ }
2464
+ return executeParallelMode(ops);
2465
+ }
2466
+ function normalizeWriteOp(alias, d) {
2467
+ if (d === null || typeof d !== "object") {
2468
+ throw new Error(
2469
+ `DDBModel.mutate: the descriptor for alias '${alias}' is not a write descriptor (\`{ create | update | remove: Model, key, input?, condition?, result? }\`).`
2470
+ );
2471
+ }
2472
+ const present = INTENT_KEYS.filter((k) => d[k] !== void 0);
2473
+ if (present.length !== 1) {
2474
+ throw new Error(
2475
+ `DDBModel.mutate: the descriptor for alias '${alias}' must carry exactly one intent key (\`create\` / \`update\` / \`remove\`), but declared ${present.length === 0 ? "none" : present.join(", ")}.`
2476
+ );
2477
+ }
2478
+ const intent = present[0];
2479
+ if (d.key === void 0 || d.key === null || typeof d.key !== "object") {
2480
+ throw new Error(
2481
+ `DDBModel.mutate: the '${intent}' descriptor for alias '${alias}' must declare a \`key\` object (or a non-empty array of key objects) binding the target's primary-key fields.`
2482
+ );
2483
+ }
2484
+ const model = resolveModelStatic(d[intent], alias);
2485
+ const input = d.input ?? {};
2486
+ const base = { alias, intent, model, input, condition: d.condition, result: d.result };
2487
+ if (Array.isArray(d.key)) {
2488
+ const keys = d.key;
2489
+ if (keys.length === 0) {
2490
+ throw new Error(
2491
+ `DDBModel.mutate: the '${intent}' descriptor for alias '${alias}' declared an EMPTY key array. Pass at least one key, or omit the operation.`
2492
+ );
2493
+ }
2494
+ return keys.map((key, bulkIndex) => {
2495
+ if (key === null || typeof key !== "object") {
2496
+ throw new Error(
2497
+ `DDBModel.mutate: the '${intent}' descriptor for alias '${alias}' key #${bulkIndex} is not a key object.`
2498
+ );
2499
+ }
2500
+ return { ...base, key, bulkIndex };
2501
+ });
2502
+ }
2503
+ return [{ ...base, key: d.key }];
2504
+ }
2505
+ function compileWriteOp(op) {
2506
+ const keyFieldNames2 = Object.keys(op.key);
2507
+ const inputFieldNames = Object.keys(op.input);
2508
+ const model = op.model;
2509
+ const body = ($) => {
2510
+ const descriptor = {
2511
+ [op.intent]: (() => model),
2512
+ key: mapToRefs($, keyFieldNames2),
2513
+ ...inputFieldNames.length > 0 ? { input: mapToRefs($, inputFieldNames) } : {}
2514
+ };
2515
+ const map = { [op.alias]: descriptor };
2516
+ return map;
600
2517
  };
601
- switch (lifecycle) {
602
- case "onCreate":
603
- return [put];
604
- case "onDelete":
605
- return [deleteNew];
606
- case "onUpdateKeyChange":
607
- return [deleteOld, put];
2518
+ const plan2 = mutation(body);
2519
+ if (!isCommandPlan(plan2)) {
2520
+ throw new Error("DDBModel.mutate: internal error building the mutation plan.");
608
2521
  }
2522
+ const compiled = compileMutationPlan(plan2);
2523
+ const fragment = compiled.fragments[0];
2524
+ const params = { ...op.input, ...op.key };
2525
+ return {
2526
+ fragment,
2527
+ params,
2528
+ condition: op.condition,
2529
+ result: op.result
2530
+ };
2531
+ }
2532
+ function mapToRefs($, fields) {
2533
+ const out = {};
2534
+ for (const f of fields) out[f] = $[f];
2535
+ return out;
609
2536
  }
610
- function deriveEdgeWriteItems(adjacencyClass, writes, lifecycle) {
611
- const items = [];
612
- for (const decl of writes.edges) {
613
- items.push(...deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle));
614
- }
615
- return items;
2537
+ async function executeTransactionMode(ops) {
2538
+ const compiled = ops.map(compileWriteOp);
2539
+ const { readBacks } = await executeCompiledWriteOp(compiled, {
2540
+ label: "DDBModel.mutate"
2541
+ });
2542
+ const out = {};
2543
+ ops.forEach((op, i) => {
2544
+ if (op.result === void 0) return;
2545
+ if (op.bulkIndex === void 0) {
2546
+ out[op.alias] = readBacks[i];
2547
+ } else {
2548
+ const arr = out[op.alias] ?? [];
2549
+ arr[op.bulkIndex] = readBacks[i];
2550
+ out[op.alias] = arr;
2551
+ }
2552
+ });
2553
+ return out;
616
2554
  }
617
- function getEdgeWrites(modelClass) {
618
- for (const name of Object.getOwnPropertyNames(modelClass)) {
619
- let value;
620
- try {
621
- value = modelClass[name];
622
- } catch {
623
- continue;
2555
+ async function executeParallelMode(ops) {
2556
+ const compiled = ops.map(compileWriteOp);
2557
+ const settled = await executeCompiledParallelWrites(compiled, {
2558
+ label: "DDBModel.mutate"
2559
+ });
2560
+ const out = {};
2561
+ ops.forEach((op, i) => {
2562
+ const res = settled[i];
2563
+ const mapped = res.ok ? { ok: true, value: op.result !== void 0 ? res.value : void 0 } : { ok: false, error: res.error };
2564
+ if (op.bulkIndex === void 0) {
2565
+ out[op.alias] = mapped;
2566
+ } else {
2567
+ const arr = out[op.alias] ?? [];
2568
+ arr[op.bulkIndex] = mapped;
2569
+ out[op.alias] = arr;
624
2570
  }
625
- if (isEdgeWritesDefinition(value)) return value;
2571
+ });
2572
+ return out;
2573
+ }
2574
+ function resolveModelStatic(ref, alias) {
2575
+ if (ref === null || ref === void 0) {
2576
+ throw new Error(`DDBModel: the descriptor for alias '${alias}' names no target model.`);
626
2577
  }
627
- return void 0;
2578
+ if (typeof ref === "function") {
2579
+ const asModel = ref.asModel;
2580
+ if (typeof asModel === "function") {
2581
+ return asModel.call(ref);
2582
+ }
2583
+ return resolveModelStatic(ref(), alias);
2584
+ }
2585
+ return ref;
628
2586
  }
629
- function deriveModelEdgeWriteItems(modelClass, lifecycle) {
630
- const writes = getEdgeWrites(modelClass);
631
- if (!writes) {
632
- throw new Error(
633
- `Model '${modelClass.name}' declares no edge write side (no static \`writes = edgeWrites(...)\`). Declare it to derive adjacency items.`
2587
+
2588
+ // src/model/DDBModel.ts
2589
+ var DDBModel = class {
2590
+ static setClient(client) {
2591
+ ClientManager.setClient(client);
2592
+ }
2593
+ static setTableMapping(mapping) {
2594
+ TableMapping.set(mapping);
2595
+ }
2596
+ static async transaction(fn) {
2597
+ await executeTransaction(fn);
2598
+ }
2599
+ /**
2600
+ * In-process multi-route READ (issue #101 — the unified envelope). Each alias
2601
+ * route (`{ query | list: Model, key, select, options? }`) runs **independently
2602
+ * and in parallel**; the result is an object keyed by the input aliases. A
2603
+ * `query` route delegates to the single-item read path, a `list` route to the
2604
+ * partition read path. Cross-route consistency is NOT guaranteed.
2605
+ */
2606
+ static async query(envelope) {
2607
+ return executeReadEnvelope(envelope);
2608
+ }
2609
+ static mutate(envelope, options) {
2610
+ return executeWriteEnvelope(envelope, options);
2611
+ }
2612
+ static async batchGet(requests) {
2613
+ return executeBatchGet(requests);
2614
+ }
2615
+ static async batchWrite(requests) {
2616
+ return executeBatchWrite(requests);
2617
+ }
2618
+ static asModel() {
2619
+ const modelClass = this;
2620
+ const modelStatic = {
2621
+ col: createColumnMap(),
2622
+ project(select) {
2623
+ return buildProject(select);
2624
+ },
2625
+ relation(select) {
2626
+ return buildRelation(select);
2627
+ },
2628
+ // The public `query` is overloaded (plain / updatable / hydrate, the last
2629
+ // introducing a fresh return-type parameter `R`, issue #53). An overloaded
2630
+ // method on an object literal cannot contextually infer its single
2631
+ // implementation signature, so the implementation is written with explicit
2632
+ // runtime-facing parameter types and assigned through the interface's
2633
+ // method type — the public overloads (the only caller-visible surface)
2634
+ // are untouched.
2635
+ query: ((key, select, options) => {
2636
+ return executeQuery(modelClass, key, select, options);
2637
+ }),
2638
+ list: ((key, select, options) => {
2639
+ return executeList(modelClass, key, select, options);
2640
+ }),
2641
+ explain(key, options) {
2642
+ return executeExplain(
2643
+ modelClass,
2644
+ key,
2645
+ options
2646
+ );
2647
+ },
2648
+ async putItem(item, options) {
2649
+ await executePut(
2650
+ modelClass,
2651
+ item,
2652
+ options
2653
+ );
2654
+ },
2655
+ async updateItem(entity, changes, options) {
2656
+ await executeUpdate(
2657
+ modelClass,
2658
+ entity,
2659
+ changes,
2660
+ options
2661
+ );
2662
+ },
2663
+ async deleteItem(key, options) {
2664
+ await executeDelete(modelClass, key, options);
2665
+ }
2666
+ };
2667
+ return attachModelClass(
2668
+ modelStatic,
2669
+ modelClass
634
2670
  );
635
2671
  }
636
- return deriveEdgeWriteItems(modelClass, writes, lifecycle);
637
- }
2672
+ };
638
2673
 
639
- // src/define/contract.ts
640
- function isCommandBatchMode(value) {
641
- return value === "transact" || value === "batchWrite";
642
- }
643
- var KEY_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyRef");
644
- var KEY_FIELD_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyFieldRef");
645
- function isContractKeyRef(value) {
646
- return typeof value === "object" && value !== null && value[KEY_REF_BRAND] === true;
647
- }
648
- function mintContractKeyFieldRef(field) {
649
- return { [KEY_FIELD_REF_BRAND]: true, field, token: `{key.${field}}` };
2674
+ // src/define/mutation.ts
2675
+ var INPUT_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationInputRef");
2676
+ function isMutationInputRef(value) {
2677
+ return typeof value === "object" && value !== null && value[INPUT_REF_BRAND] === true;
650
2678
  }
651
- function wholeKeysSentinel() {
652
- return { [KEY_REF_BRAND]: true };
2679
+ var ENTITY_REF_RE = /^\$\.entity\[(\d+)\]\.([A-Za-z_$][\w$]*)$/;
2680
+ var ENTITY_REF_PREFIX_RE = /^\s*\$\s*\.\s*entity\b/;
2681
+ var ENTITY_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationEntityRef");
2682
+ function parseEntityRef(leaf) {
2683
+ if (typeof leaf !== "string") return void 0;
2684
+ const match = ENTITY_REF_RE.exec(leaf);
2685
+ if (match === null) {
2686
+ if (ENTITY_REF_PREFIX_RE.test(leaf)) {
2687
+ throw new Error(
2688
+ `mutation: the input value ${JSON.stringify(leaf)} looks like a cross-fragment reference but is malformed. The only supported form is \`$.entity[<index>].<field>\` (e.g. \`$.entity[0].postId\`) \u2014 a single dotted field on a 0-based, non-negative fragment index. Fix the path or, if you meant a literal string, it must not begin with \`$.entity\`.`
2689
+ );
2690
+ }
2691
+ return void 0;
2692
+ }
2693
+ return {
2694
+ [ENTITY_REF_BRAND]: true,
2695
+ fragmentIndex: Number(match[1]),
2696
+ field: match[2],
2697
+ path: leaf
2698
+ };
653
2699
  }
654
- function isContractKeyFieldRef(value) {
655
- return typeof value === "object" && value !== null && value[KEY_FIELD_REF_BRAND] === true;
2700
+ var ALIAS_FIELD_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationAliasFieldRef");
2701
+ function isAliasFieldRef(value) {
2702
+ return typeof value === "object" && value !== null && value[ALIAS_FIELD_REF_BRAND] === true;
656
2703
  }
657
- function makeKeyFieldRef(field, marker, onCoerce) {
658
- const token = `{key.${field}}`;
659
- const origin = `keys field '${field}'`;
2704
+ function makeRootRef(root) {
2705
+ const token = `{${root}}`;
2706
+ const origin = `the placeholder '${root}' (\`$.${root}\`)`;
2707
+ const aliasFieldCache = /* @__PURE__ */ new Map();
660
2708
  const target = /* @__PURE__ */ Object.create(null);
661
2709
  const proxy = new Proxy(target, {
662
2710
  get(_t, prop) {
663
- if (prop === KEY_FIELD_REF_BRAND) return true;
664
- if (prop === "field") return field;
2711
+ if (prop === INPUT_REF_BRAND) return true;
2712
+ if (prop === "field") return root;
665
2713
  if (prop === "token") return token;
666
- if (prop === Symbol.toPrimitive) {
667
- return (hint) => {
668
- onCoerce();
669
- return hint === "number" ? NaN : marker;
670
- };
671
- }
672
- if (prop === "toString" || prop === Symbol.toStringTag) {
673
- return () => {
674
- onCoerce();
675
- return marker;
676
- };
2714
+ if (typeof prop === "symbol") return void 0;
2715
+ if (prop === "toString" || prop === "valueOf" || prop === "constructor") {
2716
+ throw new Error(
2717
+ `mutation: unsupported operation '${prop}' on ${origin}. A descriptor \`key\` / \`input\` value only supports a direct \`$.field\` input reference, a \`$.alias.field\` cross-fragment reference, or a concrete literal \u2014 never a transform or coercion.`
2718
+ );
677
2719
  }
678
- if (prop === "valueOf") {
679
- return () => {
680
- onCoerce();
681
- return NaN;
682
- };
2720
+ let ref = aliasFieldCache.get(prop);
2721
+ if (!ref) {
2722
+ ref = { [ALIAS_FIELD_REF_BRAND]: true, alias: root, field: prop };
2723
+ aliasFieldCache.set(prop, ref);
683
2724
  }
684
- if (typeof prop === "symbol") return void 0;
685
- const name = String(prop);
686
- throw new Error(
687
- `publicQueryModel/publicCommandModel: unsupported operation '${name}' on ${origin}. A contract method body only supports a *direct* key-field reference (\`keys.field\`) or the whole \`keys\` passed into a model op's key position; any transform, indexing, or coercion on a key field is not representable as a declarative contract method.`
688
- );
2725
+ return ref;
689
2726
  },
690
2727
  set() {
691
- throw new Error(`publicQueryModel/publicCommandModel: cannot assign on ${origin}.`);
692
- },
693
- defineProperty() {
694
- throw new Error(
695
- `publicQueryModel/publicCommandModel: cannot define a property on ${origin}.`
696
- );
697
- },
698
- deleteProperty() {
699
- throw new Error(`publicQueryModel/publicCommandModel: cannot delete on ${origin}.`);
2728
+ throw new Error(`mutation: cannot assign on ${origin}.`);
700
2729
  }
701
2730
  });
702
2731
  return proxy;
703
2732
  }
704
- function makeKeySentinel(pass, onFieldAccess, onCoerce) {
2733
+ function makeInputProxy() {
705
2734
  const cache = /* @__PURE__ */ new Map();
706
- const target = /* @__PURE__ */ Object.create(null);
707
- const proxy = new Proxy(target, {
2735
+ return new Proxy(/* @__PURE__ */ Object.create(null), {
708
2736
  get(_t, prop) {
709
- if (prop === KEY_REF_BRAND) return true;
710
- if (prop === Symbol.toPrimitive) {
711
- return (hint) => {
712
- onCoerce();
713
- return hint === "number" ? NaN : "[contract key]";
714
- };
715
- }
716
- if (prop === "toString" || prop === Symbol.toStringTag) {
717
- return () => {
718
- onCoerce();
719
- return "[contract key]";
720
- };
721
- }
722
- if (prop === "valueOf") {
723
- return () => {
724
- onCoerce();
725
- return NaN;
726
- };
727
- }
728
- if (prop === Symbol.iterator || prop === Symbol.asyncIterator || prop === "then" || prop === "constructor" || prop === "prototype" || typeof prop === "symbol" && typeof Symbol.for === "function" && (prop === /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") || prop.description === "nodejs.util.inspect.custom")) {
729
- return void 0;
730
- }
731
- if (typeof prop === "symbol") {
732
- throw new Error(
733
- "publicQueryModel/publicCommandModel: unsupported symbol access on `keys`."
734
- );
735
- }
736
- onFieldAccess(prop);
737
- let ref = cache.get(prop);
2737
+ if (typeof prop === "symbol") return void 0;
2738
+ const root = prop;
2739
+ let ref = cache.get(root);
738
2740
  if (!ref) {
739
- ref = makeKeyFieldRef(prop, paramMarker(pass, `key.${prop}`), onCoerce);
740
- cache.set(prop, ref);
2741
+ ref = makeRootRef(root);
2742
+ cache.set(root, ref);
741
2743
  }
742
2744
  return ref;
743
2745
  },
744
2746
  set() {
745
- throw new Error("publicQueryModel/publicCommandModel: cannot assign to `keys`.");
746
- },
747
- defineProperty() {
748
- throw new Error(
749
- "publicQueryModel/publicCommandModel: cannot define a property on `keys`."
750
- );
751
- },
752
- deleteProperty() {
753
- throw new Error(
754
- "publicQueryModel/publicCommandModel: cannot delete a field of `keys`."
755
- );
2747
+ throw new Error("mutation: cannot assign to the `$` placeholder proxy.");
756
2748
  }
757
2749
  });
758
- return proxy;
759
2750
  }
760
- var PARAM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractParamRef");
761
- function isContractParamRef(value) {
762
- return typeof value === "object" && value !== null && value[PARAM_REF_BRAND] === true;
2751
+ var FRAGMENT_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationFragment");
2752
+ function isMutationFragment(value) {
2753
+ return typeof value === "object" && value !== null && value[FRAGMENT_BRAND] === true;
763
2754
  }
764
- function mintContractParamRef(field) {
765
- return { [PARAM_REF_BRAND]: true, field, token: `{${field}}` };
2755
+ var INTENT_KEYS2 = ["create", "update", "remove"];
2756
+ function resolveDescriptorTarget(descriptor, alias) {
2757
+ const present = INTENT_KEYS2.filter((k) => descriptor[k] !== void 0);
2758
+ if (present.length === 0) {
2759
+ throw new Error(
2760
+ `mutation: the descriptor for alias '${alias}' has no intent key. Each descriptor must carry exactly one of \`create\` / \`update\` / \`remove\` whose value is the target model (or a \`() => Model\` thunk), e.g. \`{ create: GroupMembership, key, input }\`.`
2761
+ );
2762
+ }
2763
+ if (present.length > 1) {
2764
+ throw new Error(
2765
+ `mutation: the descriptor for alias '${alias}' declares multiple intent keys (${present.join(", ")}). A descriptor is exactly one write \u2014 declare a single \`create\` / \`update\` / \`remove\`.`
2766
+ );
2767
+ }
2768
+ const intent = present[0];
2769
+ const ref = descriptor[intent];
2770
+ const resolved = resolveModelRef(ref, intent, alias);
2771
+ const modelClass = resolveModelClass(resolved);
2772
+ return { intent, entity: { name: modelClass.name, modelClass } };
766
2773
  }
767
- var PARAM_MARKER_TAGS = [
768
- "Bxqv",
769
- "Ymnoprstuwy0123456789WIDEMARKERBODYFILLERY"
770
- ];
771
- var PARAM_PASS_COUNT = PARAM_MARKER_TAGS.length;
772
- function paramMarker(pass, field) {
773
- const tag = PARAM_MARKER_TAGS[pass];
774
- return `${tag}::${field}::${tag}`;
2774
+ function resolveModelRef(ref, intent, alias) {
2775
+ if (ref === null || ref === void 0) {
2776
+ throw new Error(
2777
+ `mutation: the '${intent}' descriptor for alias '${alias}' names no target model.`
2778
+ );
2779
+ }
2780
+ if (typeof ref === "function") {
2781
+ const proto = ref.prototype;
2782
+ const isModelClass = proto instanceof DDBModel || typeof ref === "function" && ref.prototype instanceof DDBModel;
2783
+ if (isModelClass) {
2784
+ return ref;
2785
+ }
2786
+ return ref();
2787
+ }
2788
+ return ref;
775
2789
  }
776
- function makeParamFieldRef(field, marker, onCoerce) {
777
- const token = `{${field}}`;
778
- const origin = `params field '${field}'`;
779
- const target = /* @__PURE__ */ Object.create(null);
780
- const proxy = new Proxy(target, {
781
- get(_t, prop) {
782
- if (prop === PARAM_REF_BRAND) return true;
783
- if (prop === "token") return token;
784
- if (prop === "field") return field;
785
- if (prop === Symbol.toPrimitive) {
786
- return (hint) => {
787
- onCoerce();
788
- return hint === "number" ? NaN : marker;
789
- };
790
- }
791
- if (prop === "toString" || prop === Symbol.toStringTag) {
792
- return () => {
793
- onCoerce();
794
- return marker;
795
- };
796
- }
797
- if (prop === "valueOf") {
798
- return () => {
799
- onCoerce();
800
- return NaN;
801
- };
802
- }
803
- if (typeof prop === "symbol") return void 0;
804
- const name = String(prop);
805
- throw new Error(
806
- `publicQueryModel/publicCommandModel: unsupported operation '${name}' on ${origin}. A contract method body only supports a *direct* field reference (\`params.field\`) or a concrete literal at a value position; any string transform, property access (e.g. \`.length\`, \`[i]\`, \`.toUpperCase()\`), arithmetic, or value branching on a parameter is not representable as a declarative contract method.`
807
- );
808
- },
809
- set() {
810
- throw new Error(
811
- `publicQueryModel/publicCommandModel: cannot assign on ${origin}.`
812
- );
813
- },
814
- defineProperty() {
2790
+ function assertFaithfulInput(input, intent, entityName) {
2791
+ if (input === null || typeof input !== "object") {
2792
+ throw new Error(
2793
+ `mutation: the '${intent}' fragment on '${entityName}' must declare an \`input\` object binding model fields to \`$.field\` references or literals.`
2794
+ );
2795
+ }
2796
+ for (const [field, leaf] of Object.entries(input)) {
2797
+ if (!isMutationInputRef(leaf) && !isAliasFieldRef(leaf) && !isConcreteLiteral(leaf)) {
815
2798
  throw new Error(
816
- `publicQueryModel/publicCommandModel: cannot define a property on ${origin}.`
2799
+ `mutation: the '${intent}' fragment on '${entityName}' binds model field '${field}' to a value that is neither a \`$.field\` input reference, a \`$.alias.field\` cross-fragment reference, nor a concrete literal. A fragment input is declarative \u2014 only \`$.field\` / \`$.alias.field\` references or literals are allowed, never a computed value.`
817
2800
  );
818
- },
819
- deleteProperty() {
2801
+ }
2802
+ }
2803
+ return input;
2804
+ }
2805
+ function isConcreteLiteral(value) {
2806
+ const t = typeof value;
2807
+ return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
2808
+ }
2809
+ function makeFragment(intent, entity, input, use) {
2810
+ const checked = assertFaithfulInput(input ?? {}, intent, entity.name);
2811
+ if (use !== void 0 && !isEntityWritesDefinition(use)) {
2812
+ throw new Error(
2813
+ `mutation: the '${intent}' fragment on '${entity.name}' was given a \`use:\` that is not an \`entityWrites(...)\` save contract. \`use:\` takes the WHOLE \`writes\` set (an \`entityWrites(...)\` value), never a single lifecycle (e.g. \`Model.writes.create\`) \u2014 the fragment's intent already selects the lifecycle.`
2814
+ );
2815
+ }
2816
+ return {
2817
+ [FRAGMENT_BRAND]: true,
2818
+ intent,
2819
+ entity,
2820
+ input: checked,
2821
+ ...use !== void 0 ? { use } : {}
2822
+ };
2823
+ }
2824
+ var COMMAND_PLAN_BRAND = /* @__PURE__ */ Symbol("graphddb:commandPlan");
2825
+ function isCommandPlan(value) {
2826
+ return typeof value === "object" && value !== null && value[COMMAND_PLAN_BRAND] === true;
2827
+ }
2828
+ function mergeBindings(descriptor, intent, alias) {
2829
+ const merged = {};
2830
+ const key = descriptor.key;
2831
+ if (key === void 0 || key === null || typeof key !== "object") {
2832
+ throw new Error(
2833
+ `mutation: the '${intent}' descriptor for alias '${alias}' must declare a \`key\` object binding the target's primary-key fields (\`{ field: $.field, \u2026 }\`).`
2834
+ );
2835
+ }
2836
+ for (const [field, leaf] of Object.entries(key)) merged[field] = leaf;
2837
+ const input = descriptor.input;
2838
+ if (input !== void 0 && input !== null) {
2839
+ if (typeof input !== "object") {
820
2840
  throw new Error(
821
- `publicQueryModel/publicCommandModel: cannot delete on ${origin}.`
2841
+ `mutation: the '${intent}' descriptor for alias '${alias}' has a non-object \`input\`.`
822
2842
  );
823
2843
  }
824
- });
825
- return proxy;
826
- }
827
- function makeParamsProxy(pass, onAccess, onCoerce) {
828
- const cache = /* @__PURE__ */ new Map();
829
- return new Proxy(/* @__PURE__ */ Object.create(null), {
830
- get(_t, prop) {
831
- if (typeof prop === "symbol") {
832
- if (prop === Symbol.iterator || prop === Symbol.asyncIterator || prop === Symbol.toStringTag || typeof Symbol.for === "function" && (prop === /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") || prop.description === "nodejs.util.inspect.custom")) {
833
- return void 0;
834
- }
2844
+ for (const [field, leaf] of Object.entries(input)) {
2845
+ if (field in merged) {
835
2846
  throw new Error(
836
- "publicQueryModel/publicCommandModel: unsupported symbol access on `params`. Reference only declared param fields directly; any JS transform / branching / coercion on a parameter is not representable as a declarative contract method."
2847
+ `mutation: the '${intent}' descriptor for alias '${alias}' binds field '${field}' in both \`key\` and \`input\`. A primary-key field belongs in \`key\` only.`
837
2848
  );
838
2849
  }
839
- onAccess(prop);
840
- let ref = cache.get(prop);
841
- if (!ref) {
842
- ref = makeParamFieldRef(prop, paramMarker(pass, prop), () => onCoerce(prop));
843
- cache.set(prop, ref);
2850
+ merged[field] = leaf;
2851
+ }
2852
+ }
2853
+ return merged;
2854
+ }
2855
+ function resolveAliasRefs(input, aliasToIndex, alias) {
2856
+ const out = {};
2857
+ for (const [field, leaf] of Object.entries(input)) {
2858
+ if (isAliasFieldRef(leaf)) {
2859
+ const index = aliasToIndex.get(leaf.alias);
2860
+ if (index === void 0) {
2861
+ throw new Error(
2862
+ `mutation: the descriptor for alias '${alias}' binds '${field}' to \`$.${leaf.alias}.${leaf.field}\`, but no descriptor is aliased '${leaf.alias}'. A cross-fragment reference must name another alias in the same mutation map.`
2863
+ );
844
2864
  }
845
- return ref;
846
- },
847
- set() {
848
- throw new Error(
849
- "publicQueryModel/publicCommandModel: cannot assign to a field of `params`."
850
- );
851
- },
852
- defineProperty() {
853
- throw new Error(
854
- "publicQueryModel/publicCommandModel: cannot define a property on `params`."
855
- );
856
- },
857
- deleteProperty() {
2865
+ out[field] = `$.entity[${index}].${leaf.field}`;
2866
+ } else {
2867
+ out[field] = leaf;
2868
+ }
2869
+ }
2870
+ return out;
2871
+ }
2872
+ function mutation(nameOrBody, maybeBody) {
2873
+ let name;
2874
+ let body;
2875
+ if (typeof nameOrBody === "string") {
2876
+ if (maybeBody === void 0) {
2877
+ throw new Error("mutation: a name was given but no body function.");
2878
+ }
2879
+ name = nameOrBody;
2880
+ body = maybeBody;
2881
+ } else {
2882
+ body = nameOrBody;
2883
+ name = "mutation";
2884
+ }
2885
+ if (typeof body !== "function") {
2886
+ throw new Error(
2887
+ "mutation: the body must be a function `$ => ({ alias: descriptor, \u2026 })` returning an alias \u2192 write-descriptor map."
2888
+ );
2889
+ }
2890
+ const $ = makeInputProxy();
2891
+ const map = body($);
2892
+ if (map === null || typeof map !== "object" || Array.isArray(map)) {
2893
+ throw new Error(
2894
+ `mutation '${name}': the body must RETURN an alias \u2192 descriptor object (e.g. \`$ => ({ membership: { create: Model, key, input } })\`). Note the \`({ \u2026 })\` parentheses \u2014 an arrow returning an object literal needs them.`
2895
+ );
2896
+ }
2897
+ const aliases = Object.keys(map);
2898
+ if (aliases.length === 0) {
2899
+ throw new Error(
2900
+ `mutation '${name}': the body returned an empty descriptor map. A mutation must declare at least one write descriptor.`
2901
+ );
2902
+ }
2903
+ const aliasToIndex = /* @__PURE__ */ new Map();
2904
+ aliases.forEach((alias, i) => aliasToIndex.set(alias, i));
2905
+ const fragments = aliases.map((alias) => {
2906
+ const descriptor = map[alias];
2907
+ if (descriptor === null || typeof descriptor !== "object") {
858
2908
  throw new Error(
859
- "publicQueryModel/publicCommandModel: cannot delete a field of `params`."
2909
+ `mutation '${name}': the value for alias '${alias}' is not a write descriptor (an object \`{ create | update | remove: Model, key, input? }\`).`
860
2910
  );
861
2911
  }
2912
+ const { intent, entity } = resolveDescriptorTarget(descriptor, alias);
2913
+ const merged = mergeBindings(descriptor, intent, alias);
2914
+ const resolved = resolveAliasRefs(merged, aliasToIndex, alias);
2915
+ return makeFragment(intent, entity, resolved, descriptor.use);
862
2916
  });
2917
+ return { [COMMAND_PLAN_BRAND]: true, name, fragments };
2918
+ }
2919
+ var definePlan = mutation;
2920
+
2921
+ // src/define/contract.ts
2922
+ var KEY_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyRef");
2923
+ var KEY_FIELD_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyFieldRef");
2924
+ function isContractKeyRef(value) {
2925
+ return typeof value === "object" && value !== null && value[KEY_REF_BRAND] === true;
2926
+ }
2927
+ function mintContractKeyFieldRef(field) {
2928
+ return { [KEY_FIELD_REF_BRAND]: true, field, token: `{key.${field}}` };
2929
+ }
2930
+ function wholeKeysSentinel() {
2931
+ return { [KEY_REF_BRAND]: true };
2932
+ }
2933
+ function isContractKeyFieldRef(value) {
2934
+ return typeof value === "object" && value !== null && value[KEY_FIELD_REF_BRAND] === true;
2935
+ }
2936
+ var PARAM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractParamRef");
2937
+ function isContractParamRef(value) {
2938
+ return typeof value === "object" && value !== null && value[PARAM_REF_BRAND] === true;
2939
+ }
2940
+ function mintContractParamRef(field) {
2941
+ return { [PARAM_REF_BRAND]: true, field, token: `{${field}}` };
863
2942
  }
864
2943
  var FROM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractFromRef");
865
2944
  var COMPOSE_NODE_BRAND = /* @__PURE__ */ Symbol("graphddb:contractComposeNode");
@@ -925,69 +3004,10 @@ function entityRef(model) {
925
3004
  const modelClass = resolveModelClass(model);
926
3005
  return { name: modelClass.name, modelClass };
927
3006
  }
928
- var PUT_HAS_NO_KEY = { [KEY_REF_BRAND]: true };
929
- function assertFaithfulKeys(keys, op) {
930
- if (isContractKeyRef(keys)) return keys;
931
- if (keys !== null && typeof keys === "object" && !isContractKeyFieldRef(keys)) {
932
- const record = keys;
933
- for (const [field, leaf] of Object.entries(record)) {
934
- if (!isContractKeyFieldRef(leaf) && !isConcreteLiteral2(leaf)) {
935
- throw new Error(
936
- `publicQueryModel/publicCommandModel: the \`${op}\` operation's key field '${field}' is neither a direct \`keys.${field}\` reference nor a concrete literal. A rebuilt partition key may only place \`keys.<field>\` references or literals at its leaves; a transformed value is not representable as a declarative contract method.`
937
- );
938
- }
939
- }
940
- return record;
941
- }
942
- throw new Error(
943
- `publicQueryModel/publicCommandModel: the \`${op}\` operation must receive the method's \`keys\` argument *directly* in its key position, or a partition key rebuilt from \`keys.<field>\` references. The value passed is not a valid contract key \u2014 it was transformed, coerced, or replaced, which is not representable as a declarative contract method.`
944
- );
945
- }
946
3007
  function isConcreteLiteral2(value) {
947
3008
  const t = typeof value;
948
3009
  return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
949
3010
  }
950
- var activeSink = null;
951
- function recordContractOp(operation, model, args) {
952
- const entity = entityRef(model);
953
- let keys;
954
- if (operation === "put") {
955
- keys = PUT_HAS_NO_KEY;
956
- } else {
957
- keys = assertFaithfulKeys(args.keys, operation);
958
- }
959
- const { select, compose } = extractCompose(args.select, operation, entity.name);
960
- const batch = args.batch === void 0 || args.batch === null ? void 0 : args.batch;
961
- const op = {
962
- __isContractMethodOp: true,
963
- entity,
964
- operation,
965
- keys,
966
- ...select !== void 0 ? { select } : {},
967
- ...args.changes !== void 0 ? { changes: args.changes } : {},
968
- ...args.item !== void 0 ? { item: args.item } : {},
969
- ...args.condition !== void 0 ? { condition: args.condition } : {},
970
- // Captured raw (cast through the field type); validated in the factory.
971
- ...batch !== void 0 ? { batch } : {},
972
- ...compose.length > 0 ? { compose } : {}
973
- };
974
- activeSink.push(op);
975
- return op;
976
- }
977
- function assertBatchModeDeclaration(name, op) {
978
- const batch = op.batch;
979
- if (batch === void 0) return;
980
- if (!isCommandBatchMode(batch)) {
981
- throw new Error(
982
- `publicCommandModel: method '${name}' declared an invalid \`batch\` mode \u2014 \`batch\` must be 'transact' (atomic TransactWriteItems, \u226425, condition-capable) or 'batchWrite' (non-atomic BatchWriteItem, no conditions), but received ${JSON.stringify(batch)}.`
983
- );
984
- }
985
- if (batch === "batchWrite" && op.condition !== void 0) {
986
- throw new Error(
987
- `publicCommandModel: method '${name}' declares a 'batchWrite' batch mode but also a write condition \u2014 DynamoDB's BatchWriteItem has no ConditionExpression and cannot carry one. Declare \`batch: 'transact'\` (an atomic TransactWriteItems, which supports the \`{ notExists }\` / equality condition subset) for a conditional batched write.`
988
- );
989
- }
990
- }
991
3011
  function extractCompose(select, operation, entityName) {
992
3012
  if (select === void 0) return { select: void 0, compose: [] };
993
3013
  const projection = {};
@@ -1006,225 +3026,6 @@ function extractCompose(select, operation, entityName) {
1006
3026
  }
1007
3027
  return { select: projection, compose };
1008
3028
  }
1009
- function isRecordingContractMethod() {
1010
- return activeSink !== null;
1011
- }
1012
- function runMethodPass(body, pass, accessed, coerced, keyCoerced) {
1013
- const keys = makeKeySentinel(
1014
- pass,
1015
- () => {
1016
- },
1017
- () => {
1018
- keyCoerced.value = true;
1019
- }
1020
- );
1021
- const params = makeParamsProxy(
1022
- pass,
1023
- (field) => accessed.add(field),
1024
- (field) => coerced.add(field)
1025
- );
1026
- const sink = [];
1027
- const previous = activeSink;
1028
- activeSink = sink;
1029
- try {
1030
- body(keys, params);
1031
- } finally {
1032
- activeSink = previous;
1033
- }
1034
- if (sink.length === 0) {
1035
- throw new Error(
1036
- "publicQueryModel/publicCommandModel: a method body must resolve to exactly one model operation (e.g. `return Model.query(keys, select, params)`). No model operation was recorded \u2014 the body did not call `Model.query` / `Model.list` / `Model.put` / `Model.update` / `Model.delete`."
1037
- );
1038
- }
1039
- if (sink.length > 1) {
1040
- throw new Error(
1041
- `publicQueryModel/publicCommandModel: a method body must resolve to exactly one model operation, but it recorded ${sink.length}. A contract method is a single declarative operation; multiple writes belong in a transaction (defineTransaction), and branching across operations is not representable as a declarative contract method.`
1042
- );
1043
- }
1044
- return sink[0];
1045
- }
1046
- function verifyOps(a, b, surfaced) {
1047
- if (a.operation !== b.operation) {
1048
- throw new Error(
1049
- "publicQueryModel/publicCommandModel: the method body is not deterministic (it resolved to different operations across evaluation passes). A contract method must be a pure, declarative description of a single operation."
1050
- );
1051
- }
1052
- if (a.entity.name !== b.entity.name) {
1053
- throw new Error(
1054
- "publicQueryModel/publicCommandModel: the method body targeted different models across evaluation passes; it must be a pure, declarative description."
1055
- );
1056
- }
1057
- verifyKeySlot(a.keys, b.keys, `${a.operation} key`);
1058
- verifyRecord(a.select, b.select, surfaced, `${a.operation} select`);
1059
- verifyRecord(a.changes, b.changes, surfaced, `${a.operation} changes`);
1060
- verifyRecord(a.item, b.item, surfaced, `${a.operation} item`);
1061
- verifyRecord(
1062
- a.condition,
1063
- b.condition,
1064
- surfaced,
1065
- `${a.operation} condition`
1066
- );
1067
- verifyCompose(a.compose, b.compose, `${a.operation} compose`);
1068
- if (a.batch !== b.batch) {
1069
- throw new Error(
1070
- `publicCommandModel: the method body declared a different \`batch\` mode across evaluation passes ('${String(a.batch)}' vs '${String(b.batch)}'); the batched-write mode must be a fixed literal declaration.`
1071
- );
1072
- }
1073
- }
1074
- function verifyCompose(a, b, context) {
1075
- const ca = a ?? [];
1076
- const cb = b ?? [];
1077
- const byAsA = new Map(ca.map((c) => [c.as, c]));
1078
- const byAsB = new Map(cb.map((c) => [c.as, c]));
1079
- if (byAsA.size !== ca.length || byAsB.size !== cb.length) {
1080
- throw new Error(
1081
- `publicQueryModel: ${context} declares two compositions at the same select property; each \`query(...)\` must attach to a distinct property.`
1082
- );
1083
- }
1084
- const keysA = [...byAsA.keys()].sort();
1085
- const keysB = [...byAsB.keys()].sort();
1086
- if (keysA.length !== keysB.length || keysA.some((x, i) => x !== keysB[i])) {
1087
- throw new Error(
1088
- `publicQueryModel: ${context} property set diverged between evaluation passes; the method body must declare a fixed set of compositions.`
1089
- );
1090
- }
1091
- for (const as of keysA) {
1092
- const na = byAsA.get(as);
1093
- const nb = byAsB.get(as);
1094
- if (na.method !== nb.method) {
1095
- throw new Error(
1096
- `publicQueryModel: ${context} '${as}' references a different contract method across evaluation passes; a composition must point at a single, stable contract method.`
1097
- );
1098
- }
1099
- const fieldsA = Object.keys(na.bind).sort();
1100
- const fieldsB = Object.keys(nb.bind).sort();
1101
- if (fieldsA.length !== fieldsB.length || fieldsA.some((x, i) => x !== fieldsB[i])) {
1102
- throw new Error(
1103
- `publicQueryModel: ${context} '${as}' binds a different set of child key fields across evaluation passes; the binding must be a fixed declarative map.`
1104
- );
1105
- }
1106
- for (const field of fieldsA) {
1107
- if (na.bind[field].path !== nb.bind[field].path) {
1108
- throw new Error(
1109
- `publicQueryModel: ${context} '${as}' binding '${field}' resolves to a different \`from\` path across evaluation passes; the binding must be a literal declarative path.`
1110
- );
1111
- }
1112
- }
1113
- }
1114
- }
1115
- function verifyKeySlot(a, b, context) {
1116
- const wholeA = isContractKeyRef(a);
1117
- const wholeB = isContractKeyRef(b);
1118
- if (wholeA && wholeB) return;
1119
- if (wholeA !== wholeB) {
1120
- throw new Error(
1121
- `publicQueryModel/publicCommandModel: ${context} is the whole \`keys\` in one evaluation pass but a rebuilt key in the other; the method body must be a pure, declarative description.`
1122
- );
1123
- }
1124
- verifyRecord(
1125
- a,
1126
- b,
1127
- /* @__PURE__ */ new Set(),
1128
- context
1129
- );
1130
- }
1131
- function verifyRecord(a, b, surfaced, context) {
1132
- if (a === void 0 && b === void 0) return;
1133
- if (a === void 0 || b === void 0) {
1134
- throw new Error(
1135
- `publicQueryModel/publicCommandModel: ${context} present in one evaluation pass but not the other; the method body must be a pure, declarative description.`
1136
- );
1137
- }
1138
- const ka = Object.keys(a).sort();
1139
- const kb = Object.keys(b).sort();
1140
- if (ka.length !== kb.length || ka.some((x, i) => x !== kb[i])) {
1141
- throw new Error(
1142
- `publicQueryModel/publicCommandModel: ${context} field set diverged between evaluation passes; the method body must be a pure, declarative description.`
1143
- );
1144
- }
1145
- for (const field of ka) {
1146
- verifyLeaf(a[field], b[field], surfaced, `${context} field '${field}'`);
1147
- }
1148
- }
1149
- function refTokenOf(value) {
1150
- if (isContractParamRef(value)) return value.token;
1151
- if (isContractKeyFieldRef(value)) return value.token;
1152
- return void 0;
1153
- }
1154
- function verifyLeaf(va, vb, surfaced, context) {
1155
- const ra = refTokenOf(va);
1156
- const rb = refTokenOf(vb);
1157
- if (ra !== void 0 || rb !== void 0) {
1158
- if (ra === void 0 || rb === void 0) {
1159
- throw new Error(
1160
- `publicQueryModel/publicCommandModel: ${context} is a field reference in one evaluation pass but a transformed value in the other \u2014 a coercion / transform escape was applied. Use a direct \`params.field\` / \`keys.field\` reference or a concrete literal.`
1161
- );
1162
- }
1163
- if (ra !== rb) {
1164
- throw new Error(
1165
- `publicQueryModel/publicCommandModel: ${context} resolves to different field references across evaluation passes \u2014 the value is not a stable, declarative reference.`
1166
- );
1167
- }
1168
- if (isContractParamRef(va)) surfaced.add(ra);
1169
- return;
1170
- }
1171
- const na = normalizeLiteral(va);
1172
- const nb = normalizeLiteral(vb);
1173
- if (na !== nb) {
1174
- throw new Error(
1175
- `publicQueryModel/publicCommandModel: ${context} is not a stable concrete literal \u2014 evaluating the method body twice produced different values ('${na}' vs '${nb}'). A parameter was coerced or transformed into the value (e.g. \`String(params.x).toUpperCase()\`, \`\${params.x}-suffix\`, or arithmetic). Only a direct field reference or a concrete literal is allowed at a value position.`
1176
- );
1177
- }
1178
- }
1179
- function normalizeLiteral(value) {
1180
- if (value === null) return "t:null";
1181
- if (value === void 0) return "t:undef";
1182
- if (value instanceof Date) return `t:date:${value.toISOString()}`;
1183
- const t = typeof value;
1184
- if (t === "string") return `t:s:${value}`;
1185
- if (t === "boolean") return `t:b:${String(value)}`;
1186
- if (t === "number") {
1187
- if (Number.isNaN(value)) {
1188
- throw new Error(
1189
- "publicQueryModel/publicCommandModel: a value leaf coerced to NaN, which means arithmetic was performed on a parameter. Only direct field references or concrete literals are allowed at value positions."
1190
- );
1191
- }
1192
- return `t:n:${String(value)}`;
1193
- }
1194
- throw new Error(
1195
- `publicQueryModel/publicCommandModel: a value leaf has unsupported type '${t}'. Only a direct \`params.field\` reference or a concrete literal (string / number / boolean / Date) is allowed at a value position.`
1196
- );
1197
- }
1198
- function resolveMethodOp(name, body) {
1199
- const accessed = /* @__PURE__ */ new Set();
1200
- const coerced = /* @__PURE__ */ new Set();
1201
- const keyCoerced = { value: false };
1202
- const ops = [];
1203
- for (let pass = 0; pass < PARAM_PASS_COUNT; pass++) {
1204
- ops.push(runMethodPass(body, pass, accessed, coerced, keyCoerced));
1205
- }
1206
- if (keyCoerced.value) {
1207
- throw new Error(
1208
- `publicQueryModel/publicCommandModel: method '${name}' coerces its \`keys\` argument to a primitive (e.g. via \`\${keys}\`, \`String(keys)\`, string concatenation, or a comparison). \`keys\` may only be passed *directly* into a model operation's key position; coercing or transforming it is not representable as a declarative contract method.`
1209
- );
1210
- }
1211
- if (coerced.size > 0) {
1212
- const field = [...coerced][0];
1213
- throw new Error(
1214
- `publicQueryModel/publicCommandModel: method '${name}' coerces params field '${field}' to a primitive (e.g. via \`String(params.${field})\`, a template literal \`\${\u2026}\`, string concatenation, or a comparison) and then consumes it in a transform or branch \u2014 for example \`String(params.${field}).slice(0, n)\` or \`\${params.${field}}-suffix\`. A contract method only supports a *direct* field reference or a concrete literal at a value position; a coerced / transformed parameter is not representable as a declarative contract method.`
1215
- );
1216
- }
1217
- const surfaced = /* @__PURE__ */ new Set();
1218
- verifyOps(ops[0], ops[1], surfaced);
1219
- for (const field of accessed) {
1220
- if (!surfaced.has(`{${field}}`)) {
1221
- throw new Error(
1222
- `publicQueryModel/publicCommandModel: method '${name}' accesses params field '${field}' but it never appears as a field reference in the recorded operation. This happens when a parameter is consumed by a value branch (e.g. \`params.${field} === 'x' ? \u2026 : \u2026\`) or a string transform instead of being referenced directly. A contract method only supports direct \`params.field\` references or concrete literals at value positions.`
1223
- );
1224
- }
1225
- }
1226
- return ops[0];
1227
- }
1228
3029
  function deriveReadFacts(op) {
1229
3030
  if (op.operation === "list") {
1230
3031
  return { resolution: "range", inputArity: "single" };
@@ -1247,103 +3048,32 @@ function deriveReadFacts(op) {
1247
3048
  }
1248
3049
  return { resolution: "point", inputArity: "either" };
1249
3050
  }
1250
- function assertDeclaredArity(name, declared, resolution, derived) {
1251
- if (declared === void 0) return;
1252
- if (declared === derived) return;
1253
- const hint = resolution === "range" ? `it resolved to a 'range' read (a partition \`Query\`), whose arity is always 'single' \u2014 use \`range(body)\`, not \`point(...)\`.` : derived === "single" ? `it resolved to a non-coalescible unique-GSI 'point' read (a per-key GSI \`Query\`; \`BatchGetItem\` cannot read a GSI), whose arity is 'single' \u2014 use \`point(body, { coalesce: false })\`.` : `it resolved to a coalescible base-table 'point' read (a key array \u2192 one \`BatchGetItem\`), whose arity is 'either' \u2014 use \`point(body)\` (\`coalesce\` defaults to \`true\`).`;
1254
- throw new Error(
1255
- `publicQueryModel: method '${name}' declared input arity '${declared}', but ${hint} The declared arity is threaded into the type-level call surface, so it must match the resolved access pattern.`
1256
- );
1257
- }
1258
- var ARITY_TAG = "__contractArity";
1259
- function tagArity(arity, body) {
1260
- if (typeof body !== "function") {
1261
- throw new Error(
1262
- `publicQueryModel: point()/range() must wrap a method body function (\`(keys, params) => Model.query(...) | Model.list(...)\`), received ${body === null ? "null" : typeof body}.`
1263
- );
1264
- }
1265
- Object.defineProperty(body, ARITY_TAG, { value: arity, enumerable: false });
1266
- return body;
1267
- }
1268
- function declaredArityOf(body) {
1269
- if (typeof body !== "function" && typeof body !== "object") return void 0;
1270
- if (body === null) return void 0;
1271
- const tag = body[ARITY_TAG];
1272
- return tag === "single" || tag === "either" || tag === "array" ? tag : void 0;
1273
- }
1274
- function point(body, options) {
1275
- const arity = options?.coalesce === false ? "single" : "either";
1276
- return tagArity(arity, body);
1277
- }
1278
- function range(body) {
1279
- return tagArity("single", body);
1280
- }
1281
3051
  function isQueryModelContract(value) {
1282
3052
  return typeof value === "object" && value !== null && value.__isContract === true && value.kind === "query";
1283
3053
  }
1284
3054
  function isCommandModelContract(value) {
1285
3055
  return typeof value === "object" && value !== null && value.__isContract === true && value.kind === "command";
1286
3056
  }
1287
- function publicQueryModel(modelOrKeyFields, maybeKeyFields) {
1288
- const { keyFields } = normalizeFactoryArgs("publicQueryModel", modelOrKeyFields, maybeKeyFields);
1289
- return (methods) => buildQueryContract(methods, keyFields);
1290
- }
1291
- function normalizeFactoryArgs(factory, modelOrKeyFields, maybeKeyFields) {
1292
- let model;
1293
- let keyFields;
1294
- if (Array.isArray(modelOrKeyFields)) {
1295
- keyFields = modelOrKeyFields;
1296
- } else if (modelOrKeyFields !== void 0) {
1297
- model = modelOrKeyFields;
1298
- keyFields = maybeKeyFields;
1299
- }
1300
- if (model !== void 0) resolveModelClass(model);
1301
- if (keyFields !== void 0) assertKeyFieldList(factory, keyFields);
1302
- return { keyFields };
1303
- }
1304
- function assertKeyFieldList(factory, keyFields) {
1305
- if (keyFields.length === 0) {
1306
- throw new Error(
1307
- `${factory}: the explicit Key-field list is empty. Supply at least one Key field (e.g. \`['email']\`), or omit the list to default to the model's primary key.`
1308
- );
1309
- }
1310
- const seen = /* @__PURE__ */ new Set();
1311
- for (const field of keyFields) {
1312
- if (typeof field !== "string" || field.length === 0) {
1313
- throw new Error(
1314
- `${factory}: an explicit Key field must be a non-empty string, but the list ${JSON.stringify(keyFields)} contains ${JSON.stringify(field)}.`
1315
- );
1316
- }
1317
- if (seen.has(field)) {
1318
- throw new Error(
1319
- `${factory}: the explicit Key-field list ${JSON.stringify(keyFields)} repeats the field '${field}'; each Key field must appear once.`
1320
- );
1321
- }
1322
- seen.add(field);
1323
- }
1324
- }
1325
- function withKeyFields(op, keyFields) {
1326
- if (keyFields === void 0) return op;
1327
- if (op.operation === "put") return op;
1328
- if (!isContractKeyRef(op.keys)) return op;
1329
- return { ...op, keyFields };
3057
+ function publicQueryModel(methods) {
3058
+ return buildQueryContract(methods);
1330
3059
  }
1331
- function buildQueryContract(methods, keyFields) {
3060
+ function buildQueryContract(methods) {
1332
3061
  const resolved = {};
1333
3062
  for (const [name, body] of Object.entries(methods)) {
1334
- if (typeof body !== "function") {
3063
+ let op;
3064
+ if (isPublicReadDescriptor(body)) {
3065
+ op = readDescriptorToOp(name, body);
3066
+ } else {
1335
3067
  throw new Error(
1336
- `publicQueryModel: method '${name}' must be a function (keys, params) => Model.query(...) | Model.list(...).`
3068
+ `publicQueryModel: method '${name}' must be a read descriptor (\`{ query | list: Model, key, select, options? }\`). The legacy (keys, params) => Model.query(...) | Model.list(...) closure form has been removed (#101).`
1337
3069
  );
1338
3070
  }
1339
- const op = withKeyFields(resolveMethodOp(name, body), keyFields);
1340
3071
  if (op.operation !== "query" && op.operation !== "list") {
1341
3072
  throw new Error(
1342
3073
  `publicQueryModel: method '${name}' resolves to a '${op.operation}' (write) operation; a query model may only resolve to a read (\`Model.query\` / \`Model.list\`). Use publicCommandModel for writes.`
1343
3074
  );
1344
3075
  }
1345
3076
  const { resolution, inputArity } = deriveReadFacts(op);
1346
- assertDeclaredArity(name, declaredArityOf(body), resolution, inputArity);
1347
3077
  resolved[name] = {
1348
3078
  __methodKind: "query",
1349
3079
  op,
@@ -1356,115 +3086,61 @@ function buildQueryContract(methods, keyFields) {
1356
3086
  for (const spec of Object.values(resolved)) methodSpecToContract.set(spec, contract);
1357
3087
  return contract;
1358
3088
  }
1359
- function publicCommandModel(modelOrKeyFields, maybeKeyFields) {
1360
- const { keyFields } = normalizeFactoryArgs("publicCommandModel", modelOrKeyFields, maybeKeyFields);
1361
- return (methods) => buildCommandContract(methods, keyFields);
3089
+ function publicCommandModel(methods) {
3090
+ return buildCommandContract(methods);
1362
3091
  }
1363
- function buildCommandContract(methods, keyFields) {
3092
+ function buildCommandContract(methods) {
1364
3093
  const resolved = {};
1365
3094
  for (const [name, body] of Object.entries(methods)) {
1366
- if (isPlannedCommandMethod(body)) {
1367
- resolved[name] = buildPlannedCommandMethod(name, body, keyFields);
3095
+ if (isPublicWriteDescriptor(body)) {
3096
+ const { plan: plan2, select, condition, mode } = writeDescriptorToPlan(name, body);
3097
+ resolved[name] = finalizeDescriptorCommand(name, plan2, select, condition, mode);
1368
3098
  continue;
1369
3099
  }
1370
- if (typeof body !== "function") {
1371
- throw new Error(
1372
- `publicCommandModel: method '${name}' must be a function (keys, params) => Model.put(...) | Model.update(...) | Model.delete(...), or a \`command({ input, select }).plan(mutation)\` planned method (#83).`
3100
+ if (isPublicComposeDescriptor(body)) {
3101
+ const select = body.result !== void 0 && body.result.select !== void 0 ? body.result.select : void 0;
3102
+ resolved[name] = finalizeDescriptorCommand(
3103
+ name,
3104
+ body.command,
3105
+ select,
3106
+ void 0,
3107
+ body.mode ?? "transaction"
1373
3108
  );
3109
+ continue;
1374
3110
  }
1375
- const op = withKeyFields(resolveMethodOp(name, body), keyFields);
1376
- if (op.operation !== "put" && op.operation !== "update" && op.operation !== "delete") {
1377
- throw new Error(
1378
- `publicCommandModel: method '${name}' resolves to a '${op.operation}' (read) operation; a command model may only resolve to a write (\`Model.put\` / \`Model.update\` / \`Model.delete\`). Use publicQueryModel for reads.`
3111
+ if (isCommandPlan(body)) {
3112
+ resolved[name] = finalizeDescriptorCommand(
3113
+ name,
3114
+ body,
3115
+ void 0,
3116
+ void 0,
3117
+ "transaction"
1379
3118
  );
3119
+ continue;
1380
3120
  }
1381
- assertBatchModeDeclaration(name, op);
1382
- resolved[name] = {
1383
- __methodKind: "command",
1384
- op,
1385
- // A single key → one write op; a key array → a batched write. Both forms
1386
- // are point writes against known keys, so the accepted arity is 'either'.
1387
- inputArity: "either",
1388
- result: commandResultOf(op),
1389
- ...op.batch !== void 0 ? { batch: op.batch } : {}
1390
- };
3121
+ throw new Error(
3122
+ `publicCommandModel: method '${name}' must be a write descriptor (\`{ create | update | remove: Model, key, input?, condition?, result?, mode? }\`), a composite \`{ command: mutation(...), input?, result?, mode? }\` descriptor, or a bare \`mutation($ => ({ \u2026 }))\` plan. The legacy closure form (\`(keys, params) => Model.putItem(...)\`) and the \`command(...).plan(...)\` builder have been removed (#101).`
3123
+ );
1391
3124
  }
1392
3125
  return { __isContract: true, kind: "command", methods: resolved };
1393
3126
  }
1394
- function commandResultOf(op) {
1395
- return op.operation === "update" ? "entity" : "void";
1396
- }
1397
3127
  var PLANNED_COMMAND_BRAND = /* @__PURE__ */ Symbol("graphddb:plannedCommandMethod");
1398
3128
  function isPlannedCommandMethod(value) {
1399
- return typeof value === "object" && value !== null && value[PLANNED_COMMAND_BRAND] === true;
1400
- }
1401
- function command(spec) {
1402
- const input = spec.input;
1403
- if (typeof input !== "object" || input === null) {
1404
- throw new Error(
1405
- "command({ input, select }): `input` must be a record of `param.*` placeholders (e.g. `{ userId: param.string(), title: param.string() }`)."
1406
- );
1407
- }
1408
- for (const [field, p] of Object.entries(input)) {
1409
- if (!isParam(p)) {
1410
- throw new Error(
1411
- `command({ input }): input field '${field}' must be a \`param.*\` placeholder (e.g. \`param.string()\`), not ${JSON.stringify(p)}.`
1412
- );
1413
- }
1414
- }
1415
- const select = spec.select;
1416
- if (select !== void 0) {
1417
- if (typeof select !== "object" || select === null) {
1418
- throw new Error(
1419
- "command({ select }): `select` must be a boolean field map (e.g. `{ postId: true, title: true }`), or omitted for no return projection."
1420
- );
1421
- }
1422
- for (const [field, flag] of Object.entries(select)) {
1423
- if (typeof flag !== "boolean") {
1424
- throw new Error(
1425
- `command({ select }): return projection field '${field}' must be a boolean flag (\`true\` / \`false\`), not ${JSON.stringify(flag)}.`
1426
- );
1427
- }
1428
- }
1429
- }
1430
- return {
1431
- plan(plan) {
1432
- if (!isCommandPlan(plan)) {
1433
- throw new Error(
1434
- "command(...).plan(...): expected a `mutation(...)` / `definePlan(...)` CommandPlan (the internal write-plan composition), but received a non-plan value."
1435
- );
1436
- }
1437
- return {
1438
- [PLANNED_COMMAND_BRAND]: true,
1439
- plan,
1440
- input,
1441
- select
1442
- };
1443
- }
1444
- };
1445
- }
1446
- function buildPlannedCommandMethod(name, planned, keyFields) {
1447
- const plan = compileMutationPlan(planned.plan);
1448
- const primary = plan.fragments[0];
1449
- if (keyFields !== void 0) {
1450
- const want = [...keyFields].sort().join(",");
1451
- const got = [...primary.keyFields].sort().join(",");
1452
- if (want !== got) {
1453
- throw new Error(
1454
- `publicCommandModel: planned method '${name}' writes '${primary.op.entity.name}' keyed by its primary key [${primary.keyFields.join(", ")}], but the factory was given an explicit Key-field list [${keyFields.join(", ")}]. A mutation-derived command is keyed by the written entity's primary key; omit the explicit list or match it.`
1455
- );
1456
- }
1457
- }
3129
+ return typeof value === "object" && value !== null && value[PLANNED_COMMAND_BRAND] === true;
3130
+ }
3131
+ function buildPlannedCommandMethod(name, planned) {
3132
+ const plan2 = compileMutationPlan(planned.plan);
3133
+ const primary = plan2.fragments[0];
1458
3134
  const stamp = (fragment) => fragment.op.operation === "put" ? fragment.op : { ...fragment.op, keyFields: fragment.keyFields };
1459
3135
  const op = stamp(primary);
1460
3136
  const returnSelection = normalizeReturnSelection(planned.select);
1461
- const ops = plan.fragments.length > 1 ? plan.fragments.map(stamp) : void 0;
1462
- const conditionChecks = plan.fragments.flatMap((f) => f.conditionChecks ?? []);
1463
- const edgeWrites2 = plan.fragments.flatMap((f) => f.edgeWrites ?? []);
1464
- const derivedUpdates = plan.fragments.flatMap((f) => f.derivedUpdates ?? []);
1465
- const uniqueGuards = plan.fragments.flatMap((f) => f.uniqueGuards ?? []);
1466
- const outboxEvents = plan.fragments.flatMap((f) => f.outboxEvents ?? []);
1467
- const idempotencyGuards = plan.fragments.map((f) => f.idempotencyGuard).filter((g) => g !== void 0);
3137
+ const ops = plan2.fragments.length > 1 ? plan2.fragments.map(stamp) : void 0;
3138
+ const conditionChecks = plan2.fragments.flatMap((f) => f.conditionChecks ?? []);
3139
+ const edgeWrites2 = plan2.fragments.flatMap((f) => f.edgeWrites ?? []);
3140
+ const derivedUpdates = plan2.fragments.flatMap((f) => f.derivedUpdates ?? []);
3141
+ const uniqueGuards = plan2.fragments.flatMap((f) => f.uniqueGuards ?? []);
3142
+ const outboxEvents = plan2.fragments.flatMap((f) => f.outboxEvents ?? []);
3143
+ const idempotencyGuards = plan2.fragments.map((f) => f.idempotencyGuard).filter((g) => g !== void 0);
1468
3144
  if (idempotencyGuards.length > 1) {
1469
3145
  throw new Error(
1470
3146
  `publicCommandModel: planned method '${name}' declares an idempotency guard on more than one fragment. Idempotency is a single command-level client token (it keys the whole atomic write); declare \`w.idempotentBy(...)\` on at most one fragment's lifecycle.`
@@ -1488,6 +3164,21 @@ function buildPlannedCommandMethod(name, planned, keyFields) {
1488
3164
  ...returnSelection !== void 0 ? { returnSelection } : {}
1489
3165
  };
1490
3166
  }
3167
+ function finalizeDescriptorCommand(name, plan2, select, condition, mode) {
3168
+ const planned = {
3169
+ [PLANNED_COMMAND_BRAND]: true,
3170
+ plan: plan2,
3171
+ input: {},
3172
+ select
3173
+ };
3174
+ const built = buildPlannedCommandMethod(name, planned);
3175
+ const op = condition !== void 0 ? { ...built.op, condition } : built.op;
3176
+ return {
3177
+ ...built,
3178
+ op,
3179
+ mode
3180
+ };
3181
+ }
1491
3182
  function normalizeReturnSelection(select) {
1492
3183
  if (select === void 0) return void 0;
1493
3184
  const out = {};
@@ -1496,6 +3187,117 @@ function normalizeReturnSelection(select) {
1496
3187
  }
1497
3188
  return Object.keys(out).length > 0 ? out : void 0;
1498
3189
  }
3190
+ var READ_INTENT_KEYS = ["query", "list"];
3191
+ var WRITE_INTENT_KEYS = ["create", "update", "remove"];
3192
+ function isPublicComposeDescriptor(value) {
3193
+ return typeof value === "object" && value !== null && isCommandPlan(value.command);
3194
+ }
3195
+ function isPublicReadDescriptor(value) {
3196
+ return typeof value === "object" && value !== null && READ_INTENT_KEYS.some((k) => value[k] !== void 0);
3197
+ }
3198
+ function isPublicWriteDescriptor(value) {
3199
+ return typeof value === "object" && value !== null && WRITE_INTENT_KEYS.some((k) => value[k] !== void 0);
3200
+ }
3201
+ function resolveDescriptorModel(ref, name) {
3202
+ if (ref === null || ref === void 0) {
3203
+ throw new Error(`publicQueryModel/publicCommandModel: method '${name}' names no target model.`);
3204
+ }
3205
+ if (typeof ref === "function") {
3206
+ const asModel = ref.asModel;
3207
+ if (typeof asModel === "function") return asModel.call(ref);
3208
+ return resolveDescriptorModel(ref(), name);
3209
+ }
3210
+ return ref;
3211
+ }
3212
+ function descriptorKeyRecord(record, name, what) {
3213
+ const out = {};
3214
+ for (const [field, leaf] of Object.entries(record)) {
3215
+ if (isParam(leaf)) {
3216
+ out[field] = mintContractKeyFieldRef(field);
3217
+ } else if (isConcreteLiteral2(leaf)) {
3218
+ out[field] = leaf;
3219
+ } else {
3220
+ throw new Error(
3221
+ `publicQueryModel/publicCommandModel: method '${name}' ${what} field '${field}' must be a \`param.*\` placeholder or a concrete literal, not ${JSON.stringify(leaf)}.`
3222
+ );
3223
+ }
3224
+ }
3225
+ return out;
3226
+ }
3227
+ function descriptorConditionRecord(record, name) {
3228
+ const out = {};
3229
+ for (const [field, leaf] of Object.entries(record)) {
3230
+ if (isParam(leaf)) {
3231
+ out[field] = mintContractParamRef(field);
3232
+ } else {
3233
+ out[field] = leaf;
3234
+ }
3235
+ }
3236
+ return out;
3237
+ }
3238
+ function readDescriptorToOp(name, d) {
3239
+ const isQuery = d.query !== void 0;
3240
+ const isList = d.list !== void 0;
3241
+ if (isQuery === isList) {
3242
+ throw new Error(
3243
+ `publicQueryModel: method '${name}' must carry exactly one of \`query\` or \`list\` (the target model); it declared ${isQuery && isList ? "both" : "neither"}.`
3244
+ );
3245
+ }
3246
+ if (d.key === void 0 || d.select === void 0) {
3247
+ throw new Error(
3248
+ `publicQueryModel: the '${isQuery ? "query" : "list"}' descriptor for method '${name}' must declare both \`key\` and \`select\`.`
3249
+ );
3250
+ }
3251
+ const model = resolveDescriptorModel(isQuery ? d.query : d.list, name);
3252
+ const entity = entityRef(model);
3253
+ const operation = isQuery ? "query" : "list";
3254
+ const keyFieldNames2 = Object.keys(d.key);
3255
+ const keyRecord = descriptorKeyRecord(d.key, name, `${operation} key`);
3256
+ const { select, compose } = extractCompose({ ...d.select }, operation, entity.name);
3257
+ const keys = isQuery ? wholeKeysSentinel() : keyRecord;
3258
+ return {
3259
+ __isContractMethodOp: true,
3260
+ entity,
3261
+ operation,
3262
+ keys,
3263
+ ...isQuery ? { keyFields: keyFieldNames2 } : {},
3264
+ ...select !== void 0 ? { select } : {},
3265
+ ...compose.length > 0 ? { compose } : {}
3266
+ };
3267
+ }
3268
+ function writeDescriptorToPlan(name, d) {
3269
+ const present = WRITE_INTENT_KEYS.filter((k) => d[k] !== void 0);
3270
+ if (present.length !== 1) {
3271
+ throw new Error(
3272
+ `publicCommandModel: method '${name}' must carry exactly one intent key (\`create\` / \`update\` / \`remove\`), but declared ${present.length === 0 ? "none" : present.join(", ")}.`
3273
+ );
3274
+ }
3275
+ const intent = present[0];
3276
+ if (d.key === void 0 || d.key === null || typeof d.key !== "object") {
3277
+ throw new Error(
3278
+ `publicCommandModel: the '${intent}' descriptor for method '${name}' must declare a \`key\` object binding the target's primary-key fields.`
3279
+ );
3280
+ }
3281
+ const model = resolveDescriptorModel(d[intent], name);
3282
+ const keyFieldNames2 = Object.keys(d.key);
3283
+ const inputFieldNames = d.input !== void 0 ? Object.keys(d.input) : [];
3284
+ const plan2 = mutation(name, ($) => {
3285
+ const refMap = (fields) => {
3286
+ const out = {};
3287
+ for (const f of fields) out[f] = $[f];
3288
+ return out;
3289
+ };
3290
+ const descriptor = {
3291
+ [intent]: (() => model),
3292
+ key: refMap(keyFieldNames2),
3293
+ ...inputFieldNames.length > 0 ? { input: refMap(inputFieldNames) } : {}
3294
+ };
3295
+ return { [name]: descriptor };
3296
+ });
3297
+ const select = d.result !== void 0 && d.result.select !== void 0 ? d.result.select : void 0;
3298
+ const condition = d.condition !== void 0 ? descriptorConditionRecord(d.condition, name) : void 0;
3299
+ return { plan: plan2, select, condition, mode: d.mode ?? "transaction" };
3300
+ }
1499
3301
 
1500
3302
  // src/spec/mutation-command.ts
1501
3303
  var INTENT_OPERATION = {
@@ -1928,18 +3730,18 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null) {
1928
3730
  ...idempotencyGuard !== void 0 ? { idempotencyGuard } : {}
1929
3731
  };
1930
3732
  }
1931
- function compileSingleFragmentPlan(plan) {
1932
- if (!isCommandPlan(plan)) {
3733
+ function compileSingleFragmentPlan(plan2) {
3734
+ if (!isCommandPlan(plan2)) {
1933
3735
  throw new Error(
1934
3736
  "mutation compiler: expected a `mutation(...)` CommandPlan, but received a non-plan value."
1935
3737
  );
1936
3738
  }
1937
- if (plan.fragments.length !== 1) {
3739
+ if (plan2.fragments.length !== 1) {
1938
3740
  throw new Error(
1939
- `mutation '${plan.name}': compileSingleFragmentPlan expects exactly one fragment, but the plan declares ${plan.fragments.length}. Use compileMutationPlan for the N-fragment atomic merge (issue #90).`
3741
+ `mutation '${plan2.name}': compileSingleFragmentPlan expects exactly one fragment, but the plan declares ${plan2.fragments.length}. Use compileMutationPlan for the N-fragment atomic merge (issue #90).`
1940
3742
  );
1941
3743
  }
1942
- return compileFragment(plan.fragments[0], 0, null);
3744
+ return compileFragment(plan2.fragments[0], 0, null);
1943
3745
  }
1944
3746
  var MAX_TRANSACT_COMPOSE_ITEMS = 25;
1945
3747
  function keySignature(fragment) {
@@ -1963,26 +3765,26 @@ function keyLeafToken(leaf) {
1963
3765
  if (isContractParamRef(leaf)) return `input:${leaf.field}`;
1964
3766
  return `literal:${JSON.stringify(leaf)}`;
1965
3767
  }
1966
- function compileMutationPlan(plan) {
1967
- if (!isCommandPlan(plan)) {
3768
+ function compileMutationPlan(plan2) {
3769
+ if (!isCommandPlan(plan2)) {
1968
3770
  throw new Error(
1969
3771
  "mutation compiler: expected a `mutation(...)` CommandPlan, but received a non-plan value."
1970
3772
  );
1971
3773
  }
1972
- const fragments = plan.fragments;
3774
+ const fragments = plan2.fragments;
1973
3775
  const compiled = [];
1974
3776
  for (let i = 0; i < fragments.length; i++) {
1975
3777
  const resolveEntityRef = (ref, consumerIndex, consumerField) => {
1976
3778
  if (ref.fragmentIndex >= consumerIndex) {
1977
3779
  throw new Error(
1978
- `mutation '${plan.name}': fragment #${consumerIndex} binds '${consumerField}' to '${ref.path}', a CIRCULAR cross-fragment dependency \u2014 a fragment may only reference a value produced by an EARLIER fragment (a lower index), never itself or a later one. A \`TransactWriteItems\` is atomic and cannot read one item's value while writing another, so the reference graph must be acyclic.`
3780
+ `mutation '${plan2.name}': fragment #${consumerIndex} binds '${consumerField}' to '${ref.path}', a CIRCULAR cross-fragment dependency \u2014 a fragment may only reference a value produced by an EARLIER fragment (a lower index), never itself or a later one. A \`TransactWriteItems\` is atomic and cannot read one item's value while writing another, so the reference graph must be acyclic.`
1979
3781
  );
1980
3782
  }
1981
3783
  const producer = compiled[ref.fragmentIndex];
1982
3784
  const resolved = producedFieldLeaf(producer, ref.field);
1983
3785
  if (resolved === void 0) {
1984
3786
  throw new Error(
1985
- `mutation '${plan.name}': fragment #${consumerIndex} binds '${consumerField}' to '${ref.path}', but fragment #${ref.fragmentIndex} ('${producer.op.entity.name}') does not write a field '${ref.field}'. A cross-fragment reference must name a field the producing fragment binds in its \`input\`.`
3787
+ `mutation '${plan2.name}': fragment #${consumerIndex} binds '${consumerField}' to '${ref.path}', but fragment #${ref.fragmentIndex} ('${producer.op.entity.name}') does not write a field '${ref.field}'. A cross-fragment reference must name a field the producing fragment binds in its \`input\`.`
1986
3788
  );
1987
3789
  }
1988
3790
  return resolved;
@@ -1995,17 +3797,17 @@ function compileMutationPlan(plan) {
1995
3797
  const prior = byKey.get(sig);
1996
3798
  if (prior !== void 0) {
1997
3799
  throw new Error(
1998
- `mutation '${plan.name}': fragments #${prior} and #${i} both write the same item ('${compiled[i].op.entity.name}', key ${describeKey(compiled[i])}) in one \`TransactWriteItems\`. DynamoDB rejects a transaction that operates on the same primary key more than once \u2014 split the conflicting writes into separate mutations, or merge them into a single fragment.`
3800
+ `mutation '${plan2.name}': fragments #${prior} and #${i} both write the same item ('${compiled[i].op.entity.name}', key ${describeKey(compiled[i])}) in one \`TransactWriteItems\`. DynamoDB rejects a transaction that operates on the same primary key more than once \u2014 split the conflicting writes into separate mutations, or merge them into a single fragment.`
1999
3801
  );
2000
3802
  }
2001
3803
  byKey.set(sig, i);
2002
3804
  }
2003
3805
  if (compiled.length > MAX_TRANSACT_COMPOSE_ITEMS) {
2004
3806
  throw new Error(
2005
- `mutation '${plan.name}': composes ${compiled.length} write fragments into one \`TransactWriteItems\`, but DynamoDB caps a transaction at ${MAX_TRANSACT_COMPOSE_ITEMS} items and a transaction is atomic \u2014 it cannot be split across requests without breaking atomicity. Reduce the fragment count to \u2264${MAX_TRANSACT_COMPOSE_ITEMS}.`
3807
+ `mutation '${plan2.name}': composes ${compiled.length} write fragments into one \`TransactWriteItems\`, but DynamoDB caps a transaction at ${MAX_TRANSACT_COMPOSE_ITEMS} items and a transaction is atomic \u2014 it cannot be split across requests without breaking atomicity. Reduce the fragment count to \u2264${MAX_TRANSACT_COMPOSE_ITEMS}.`
2006
3808
  );
2007
3809
  }
2008
- return { name: plan.name, fragments: compiled };
3810
+ return { name: plan2.name, fragments: compiled };
2009
3811
  }
2010
3812
  function producedFieldLeaf(producer, field) {
2011
3813
  const op = producer.op;
@@ -2029,17 +3831,24 @@ function describeKey(fragment) {
2029
3831
  }
2030
3832
 
2031
3833
  // src/spec/guard.ts
2032
- function conditionInputToSpec(condition, context, renderLeaf) {
3834
+ function conditionInputToSpec(condition, context, renderLeaf2, renderTreeLeaf, renderRawSlot) {
2033
3835
  if (condition === void 0 || condition === null) return void 0;
3836
+ if (isRawCondition(condition)) {
3837
+ const { expression, names, values } = serializeRawCondition(
3838
+ condition,
3839
+ renderRawSlot
3840
+ );
3841
+ return { kind: "raw", expression, names, values };
3842
+ }
2034
3843
  if (typeof condition !== "object") {
2035
3844
  throw new Error(
2036
- `${context}: write condition must be an object (\`{ notExists: true }\`, \`{ attributeExists: '<field>' }\`, \`{ attributeNotExists: '<field>' }\`, or field equality).`
3845
+ `${context}: write condition must be an object (\`{ notExists: true }\`, \`{ attributeExists: '<field>' }\`, \`{ attributeNotExists: '<field>' }\`, or a declarative field condition).`
2037
3846
  );
2038
3847
  }
2039
3848
  const obj = condition;
2040
3849
  if (obj.notExists === true) return { kind: "notExists" };
2041
- if ("attributeExists" in obj) {
2042
- if (typeof obj.attributeExists !== "string" || obj.attributeExists.length === 0) {
3850
+ if ("attributeExists" in obj && typeof obj.attributeExists === "string") {
3851
+ if (obj.attributeExists.length === 0) {
2043
3852
  throw new Error(
2044
3853
  `${context}: \`attributeExists\` must name a non-empty attribute field.`
2045
3854
  );
@@ -2054,12 +3863,122 @@ function conditionInputToSpec(condition, context, renderLeaf) {
2054
3863
  }
2055
3864
  return { kind: "attributeNotExists", field: obj.attributeNotExists };
2056
3865
  }
3866
+ if (usesOperatorTree(obj)) {
3867
+ const leaf = renderTreeLeaf ?? ((value, name) => defaultTreeLeaf(value, name, context));
3868
+ return { kind: "expr", declarative: buildTree(obj, context, leaf) };
3869
+ }
2057
3870
  const fields = {};
2058
3871
  for (const key of Object.keys(obj).sort()) {
2059
- fields[key] = renderLeaf(key, obj[key]);
3872
+ fields[key] = renderLeaf2(key, obj[key]);
2060
3873
  }
2061
3874
  return { kind: "equals", fields };
2062
3875
  }
3876
+ function assertNotNestedRaw(sub, context) {
3877
+ if (isRawCondition(sub)) {
3878
+ throw new Error(
3879
+ `${context}: a raw \`cond\` write condition may be used as a whole condition, but not nested inside an \`and\` / \`or\` / \`not\` group of a serializable (public-contract / Python-bridge) command. Move the \`cond\` to the top level, or express the group declaratively.`
3880
+ );
3881
+ }
3882
+ }
3883
+ var LOGICAL_KEYS2 = /* @__PURE__ */ new Set(["and", "or", "not"]);
3884
+ var OPERATOR_KEYS2 = /* @__PURE__ */ new Set([
3885
+ "eq",
3886
+ "ne",
3887
+ "gt",
3888
+ "ge",
3889
+ "lt",
3890
+ "le",
3891
+ "between",
3892
+ "in",
3893
+ "beginsWith",
3894
+ "contains",
3895
+ "notContains",
3896
+ "attributeExists",
3897
+ "attributeType",
3898
+ "size"
3899
+ ]);
3900
+ function isOperatorObject2(value) {
3901
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date) {
3902
+ return false;
3903
+ }
3904
+ const keys = Object.keys(value);
3905
+ if (keys.length === 0) return false;
3906
+ return keys.every((k) => OPERATOR_KEYS2.has(k));
3907
+ }
3908
+ function usesOperatorTree(obj) {
3909
+ for (const [key, value] of Object.entries(obj)) {
3910
+ if (LOGICAL_KEYS2.has(key)) return true;
3911
+ if (isOperatorObject2(value)) return true;
3912
+ }
3913
+ return false;
3914
+ }
3915
+ function defaultTreeLeaf(value, name, context) {
3916
+ void name;
3917
+ if (value instanceof Date) return value.toISOString();
3918
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
3919
+ return value;
3920
+ }
3921
+ throw new Error(
3922
+ `${context}: condition operand must be a concrete literal or a param; got ${typeof value}.`
3923
+ );
3924
+ }
3925
+ function conditionParamName(field, op, index) {
3926
+ if (op === "between") return `${field}_${index === 0 ? "lo" : "hi"}`;
3927
+ if (op === "in") return `${field}_${index ?? 0}`;
3928
+ if (op === "size") return `${field}_size`;
3929
+ return field;
3930
+ }
3931
+ function buildTree(obj, context, renderTreeLeaf) {
3932
+ const out = {};
3933
+ for (const key of Object.keys(obj).sort()) {
3934
+ const value = obj[key];
3935
+ if (value === void 0) continue;
3936
+ if (key === "and" || key === "or") {
3937
+ out[key] = value.map((sub) => {
3938
+ assertNotNestedRaw(sub, context);
3939
+ return buildTree(sub, context, renderTreeLeaf);
3940
+ });
3941
+ continue;
3942
+ }
3943
+ if (key === "not") {
3944
+ assertNotNestedRaw(value, context);
3945
+ out[key] = buildTree(
3946
+ value,
3947
+ context,
3948
+ renderTreeLeaf
3949
+ );
3950
+ continue;
3951
+ }
3952
+ if (!isOperatorObject2(value)) {
3953
+ out[key] = { eq: renderTreeLeaf(value, conditionParamName(key, "eq")) };
3954
+ continue;
3955
+ }
3956
+ const ops = value;
3957
+ const rendered = {};
3958
+ for (const op of Object.keys(ops).sort()) {
3959
+ const opVal = ops[op];
3960
+ if (op === "between") {
3961
+ const [lo, hi] = opVal;
3962
+ rendered[op] = [
3963
+ renderTreeLeaf(lo, conditionParamName(key, op, 0)),
3964
+ renderTreeLeaf(hi, conditionParamName(key, op, 1))
3965
+ ];
3966
+ } else if (op === "in") {
3967
+ rendered[op] = opVal.map(
3968
+ (v, i) => renderTreeLeaf(v, conditionParamName(key, op, i))
3969
+ );
3970
+ } else if (op === "attributeExists") {
3971
+ rendered[op] = opVal;
3972
+ } else if (op === "attributeType") {
3973
+ rendered[op] = String(opVal);
3974
+ } else {
3975
+ rendered[op] = renderTreeLeaf(opVal, conditionParamName(key, op));
3976
+ }
3977
+ }
3978
+ out[key] = rendered;
3979
+ }
3980
+ return out;
3981
+ }
2063
3982
  function assertSupportedCondition(commandName, condition) {
2064
3983
  if (condition.kind === "notExists") return;
2065
3984
  if (condition.kind === "attributeExists" || condition.kind === "attributeNotExists") {
@@ -2086,8 +4005,25 @@ function assertSupportedCondition(commandName, condition) {
2086
4005
  }
2087
4006
  return;
2088
4007
  }
4008
+ if (condition.kind === "raw") {
4009
+ if (typeof condition.expression !== "string" || condition.expression.length === 0) {
4010
+ throw new Error(
4011
+ `Command '${commandName}' has an empty raw \`cond\` write condition.`
4012
+ );
4013
+ }
4014
+ return;
4015
+ }
4016
+ if (condition.kind === "expr") {
4017
+ const tree = condition.declarative;
4018
+ if (!tree || Object.keys(tree).length === 0) {
4019
+ throw new Error(
4020
+ `Command '${commandName}' has an empty declarative write condition.`
4021
+ );
4022
+ }
4023
+ return;
4024
+ }
2089
4025
  throw new Error(
2090
- `Command '${commandName}' has an unsupported write condition. The Python bridge supports '{ notExists }' or field equality only.`
4026
+ `Command '${commandName}' has an unsupported write condition.`
2091
4027
  );
2092
4028
  }
2093
4029
  function assertJsonSerializable(value, path = "$") {
@@ -2500,17 +4436,17 @@ function verifyWrite(instrs, surfaced) {
2500
4436
  "defineTransaction: write operation diverged between evaluation passes; the callback must be a pure, declarative description of the writes."
2501
4437
  );
2502
4438
  }
2503
- verifyRecord2(instrs.map((x) => x.item), surfaced, `${op} item`);
2504
- verifyRecord2(instrs.map((x) => x.key), surfaced, `${op} key`);
2505
- verifyRecord2(instrs.map((x) => x.changes), surfaced, `${op} changes`);
2506
- verifyRecord2(
4439
+ verifyRecord(instrs.map((x) => x.item), surfaced, `${op} item`);
4440
+ verifyRecord(instrs.map((x) => x.key), surfaced, `${op} key`);
4441
+ verifyRecord(instrs.map((x) => x.changes), surfaced, `${op} changes`);
4442
+ verifyRecord(
2507
4443
  instrs.map((x) => x.condition),
2508
4444
  surfaced,
2509
4445
  `${op} condition`
2510
4446
  );
2511
4447
  verifyWhen(instrs.map((x) => x.when), surfaced, `${op} when`);
2512
4448
  }
2513
- function verifyRecord2(records, surfaced, context) {
4449
+ function verifyRecord(records, surfaced, context) {
2514
4450
  const present = records.filter((r) => r !== void 0);
2515
4451
  if (present.length === 0) return;
2516
4452
  if (present.length !== records.length) {
@@ -2528,7 +4464,7 @@ function verifyRecord2(records, surfaced, context) {
2528
4464
  }
2529
4465
  }
2530
4466
  for (const field of keys) {
2531
- verifyLeaf2(
4467
+ verifyLeaf(
2532
4468
  present.map((r) => r[field]),
2533
4469
  surfaced,
2534
4470
  `${context} field '${field}'`
@@ -2546,9 +4482,9 @@ function verifyWhen(whens, surfaced, context) {
2546
4482
  for (const w of present) {
2547
4483
  if (isTransactionRef(w.left)) surfaced.add(w.left.token);
2548
4484
  }
2549
- verifyLeaf2(present.map((w) => w.right), surfaced, `${context} right-hand value`);
4485
+ verifyLeaf(present.map((w) => w.right), surfaced, `${context} right-hand value`);
2550
4486
  }
2551
- function verifyLeaf2(values, surfaced, context) {
4487
+ function verifyLeaf(values, surfaced, context) {
2552
4488
  const refTokens = values.map((v) => isTransactionRef(v) ? v.token : void 0);
2553
4489
  const anyRef = refTokens.some((t) => t !== void 0);
2554
4490
  if (anyRef) {
@@ -2566,7 +4502,7 @@ function verifyLeaf2(values, surfaced, context) {
2566
4502
  surfaced.add(token);
2567
4503
  return;
2568
4504
  }
2569
- const norm = values.map(normalizeLiteral2);
4505
+ const norm = values.map(normalizeLiteral);
2570
4506
  for (let i = 1; i < norm.length; i++) {
2571
4507
  if (norm[i] !== norm[0]) {
2572
4508
  throw new Error(
@@ -2575,7 +4511,7 @@ function verifyLeaf2(values, surfaced, context) {
2575
4511
  }
2576
4512
  }
2577
4513
  }
2578
- function normalizeLiteral2(value) {
4514
+ function normalizeLiteral(value) {
2579
4515
  if (value === null) return "t:null";
2580
4516
  if (value instanceof Date) return `t:date:${value.toISOString()}`;
2581
4517
  const t = typeof value;
@@ -2636,6 +4572,17 @@ function leafTemplate(value, context) {
2636
4572
  `defineTransaction: ${context} must be a field reference (from \`p\` or a forEach element) or a concrete literal; got ${typeof value}.`
2637
4573
  );
2638
4574
  }
4575
+ function conditionTreeLeaf(value, name, context) {
4576
+ void name;
4577
+ if (isTransactionRef(value)) return { $param: tokenName(value.token) };
4578
+ if (value instanceof Date) return value.toISOString();
4579
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
4580
+ return value;
4581
+ }
4582
+ throw new Error(
4583
+ `defineTransaction: ${context} condition operand must be a field reference (from \`p\` or a forEach element) or a concrete literal; got ${typeof value}.`
4584
+ );
4585
+ }
2639
4586
  function templateRecord2(structure, context) {
2640
4587
  const out = {};
2641
4588
  for (const field of Object.keys(structure).sort()) {
@@ -2679,7 +4626,12 @@ function conditionSpec(condition, commandName) {
2679
4626
  const spec = conditionInputToSpec(
2680
4627
  condition,
2681
4628
  commandName,
2682
- (field, value) => leafTemplate(value, `condition field '${field}'`)
4629
+ (field, value) => leafTemplate(value, `condition field '${field}'`),
4630
+ (value, name) => conditionTreeLeaf(value, name, commandName),
4631
+ // A raw `cond` value slot in a transaction is a field reference
4632
+ // (`p.*` / forEach element) → a `{ $param }` marker carrying its token name;
4633
+ // a concrete literal falls through to the default renderer (issue #114-B).
4634
+ (part) => isTransactionRef(part) ? { $param: tokenName(part.token) } : void 0
2683
4635
  );
2684
4636
  if (spec) assertSupportedCondition(commandName, spec);
2685
4637
  return spec;
@@ -2866,113 +4818,6 @@ ${rest.map((v) => ` - ${v.message}`).join("\n")})` : "";
2866
4818
  throw new Error(first.message + suffix);
2867
4819
  }
2868
4820
 
2869
- // src/relation/concurrency.ts
2870
- var RELATION_TRAVERSAL_CONCURRENCY = 16;
2871
- async function mapWithConcurrency(items, limit, worker) {
2872
- if (items.length === 0) {
2873
- return [];
2874
- }
2875
- const effectiveLimit = Math.max(1, Math.min(limit, items.length));
2876
- if (effectiveLimit >= items.length) {
2877
- return Promise.all(items.map((item, index) => worker(item, index)));
2878
- }
2879
- const results = new Array(items.length);
2880
- let nextIndex = 0;
2881
- async function runWorker() {
2882
- while (true) {
2883
- const index = nextIndex++;
2884
- if (index >= items.length) {
2885
- return;
2886
- }
2887
- results[index] = await worker(items[index], index);
2888
- }
2889
- }
2890
- const runners = [];
2891
- for (let i = 0; i < effectiveLimit; i++) {
2892
- runners.push(runWorker());
2893
- }
2894
- await Promise.all(runners);
2895
- return results;
2896
- }
2897
-
2898
- // src/relation/execution-plan.ts
2899
- function relationResultPaths(select, metadata, parentPath = "$") {
2900
- const out = [];
2901
- const relations = detectRelationFields(select, metadata);
2902
- for (const rel of [...relations].sort(
2903
- (a, b) => a.propertyName.localeCompare(b.propertyName)
2904
- )) {
2905
- const childSelect = normalizeSelectSpec(select[rel.propertyName]).select ?? {};
2906
- const targetMeta = MetadataRegistry.get(rel.targetFactory());
2907
- const childPath = `${parentPath}.${rel.propertyName}`;
2908
- const isMany = rel.type === "hasMany";
2909
- const resultPath = isMany ? `${childPath}.items` : childPath;
2910
- out.push({
2911
- propertyName: rel.propertyName,
2912
- parentPath,
2913
- resultPath,
2914
- isMany
2915
- });
2916
- out.push(...relationResultPaths(childSelect, targetMeta, resultPath));
2917
- }
2918
- return out;
2919
- }
2920
- function parentResultPath(resultPath) {
2921
- if (!resultPath.startsWith("$.")) return "$";
2922
- const tokens = resultPath.slice(2).split(".");
2923
- if (tokens[tokens.length - 1] === "items") tokens.pop();
2924
- tokens.pop();
2925
- return tokens.length === 0 ? "$" : `$.${tokens.join(".")}`;
2926
- }
2927
- function deriveExecutionPlan(resultPaths) {
2928
- if (resultPaths.length <= 1) return void 0;
2929
- const indexByPath = /* @__PURE__ */ new Map();
2930
- resultPaths.forEach((path, i) => {
2931
- indexByPath.set(path, i);
2932
- });
2933
- const stageOf = new Array(resultPaths.length);
2934
- for (let i = 0; i < resultPaths.length; i++) {
2935
- if (resultPaths[i] === "$") {
2936
- stageOf[i] = 0;
2937
- continue;
2938
- }
2939
- const parentPath = parentResultPath(resultPaths[i]);
2940
- const parentIndex = indexByPath.get(parentPath);
2941
- const parentStage = parentIndex !== void 0 ? stageOf[parentIndex] : 0;
2942
- stageOf[i] = parentStage + 1;
2943
- }
2944
- const stageCount = Math.max(...stageOf) + 1;
2945
- const groups = Array.from({ length: stageCount }, () => []);
2946
- for (let i = 0; i < resultPaths.length; i++) {
2947
- groups[stageOf[i]].push(i);
2948
- }
2949
- for (const g of groups) g.sort((a, b) => a - b);
2950
- return { groups, concurrency: RELATION_TRAVERSAL_CONCURRENCY };
2951
- }
2952
- function buildRelationExecutionPlan(select, metadata) {
2953
- const relationPaths = relationResultPaths(select, metadata);
2954
- if (relationPaths.length === 0) return void 0;
2955
- const resultPaths = ["$", ...relationPaths.map((r) => r.resultPath)];
2956
- const plan = deriveExecutionPlan(resultPaths);
2957
- if (plan === void 0) return void 0;
2958
- return { plan, resultPaths };
2959
- }
2960
- function deriveCompositionPlan(composeCount) {
2961
- if (composeCount <= 0) {
2962
- return { stages: [], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
2963
- }
2964
- const stage = Array.from({ length: composeCount }, (_unused, i) => i);
2965
- return { stages: [stage], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
2966
- }
2967
- function stageOfResultPath(resolved, resultPath) {
2968
- const index = resolved.resultPaths.indexOf(resultPath);
2969
- if (index < 0) return void 0;
2970
- for (let s = 0; s < resolved.plan.groups.length; s++) {
2971
- if (resolved.plan.groups[s].includes(index)) return s;
2972
- }
2973
- return void 0;
2974
- }
2975
-
2976
4821
  // src/spec/contract-boundary-check.ts
2977
4822
  function buildOwnershipIndex(contexts) {
2978
4823
  const modelOwner = /* @__PURE__ */ new Map();
@@ -3202,12 +5047,12 @@ function buildHasManyOperation(rel, targetMeta, select, limit, filter, resultPat
3202
5047
  const keyCondition2 = {
3203
5048
  PK: pk2
3204
5049
  };
3205
- const range3 = sk2 !== void 0 && sk2 !== "" ? { operator: "begins_with", key: "SK", value: sk2 } : void 0;
5050
+ const range2 = sk2 !== void 0 && sk2 !== "" ? { operator: "begins_with", key: "SK", value: sk2 } : void 0;
3206
5051
  return {
3207
5052
  type: "Query",
3208
5053
  tableName,
3209
5054
  keyCondition: keyCondition2,
3210
- ...range3 ? { rangeCondition: range3 } : {},
5055
+ ...range2 ? { rangeCondition: range2 } : {},
3211
5056
  projection: projectionFields(select),
3212
5057
  limit,
3213
5058
  resultPath,
@@ -3223,13 +5068,13 @@ function buildHasManyOperation(rel, targetMeta, select, limit, filter, resultPat
3223
5068
  resolved.partial
3224
5069
  );
3225
5070
  const keyCondition = { [`${gsi.indexName}PK`]: pk };
3226
- const range2 = sk !== void 0 && sk !== "" ? { operator: "begins_with", key: `${gsi.indexName}SK`, value: sk } : void 0;
5071
+ const range = sk !== void 0 && sk !== "" ? { operator: "begins_with", key: `${gsi.indexName}SK`, value: sk } : void 0;
3227
5072
  return {
3228
5073
  type: "Query",
3229
5074
  tableName,
3230
5075
  indexName: gsi.indexName,
3231
5076
  keyCondition,
3232
- ...range2 ? { rangeCondition: range2 } : {},
5077
+ ...range ? { rangeCondition: range } : {},
3233
5078
  projection: projectionFields(select),
3234
5079
  limit,
3235
5080
  resultPath,
@@ -3337,9 +5182,32 @@ function extractCondition(def) {
3337
5182
  return conditionInputToSpec(
3338
5183
  def.condition,
3339
5184
  `Command on '${def.entity.name}'`,
3340
- (field, value) => templateLeaf(field, value)
5185
+ (field, value) => templateLeaf(field, value),
5186
+ (value, name) => conditionTreeLeaf2(value, name),
5187
+ // A raw `cond` value slot (issue #114-B): a contract param / key ref carries
5188
+ // its own name; a plain `param.*` falls through to the positional default.
5189
+ (part) => {
5190
+ if (isContractParamRef(part)) return { $param: tokenName2(part.token) };
5191
+ if (isContractKeyFieldRef(part)) return { $param: part.field };
5192
+ return void 0;
5193
+ }
3341
5194
  );
3342
5195
  }
5196
+ function conditionTreeLeaf2(value, name) {
5197
+ if (isContractParamRef(value)) return { $param: tokenName2(value.token) };
5198
+ if (isContractKeyFieldRef(value)) return { $param: value.field };
5199
+ if (isParam(value)) return { $param: name };
5200
+ if (value instanceof Date) return value.toISOString();
5201
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
5202
+ return value;
5203
+ }
5204
+ throw new Error(
5205
+ `A declarative write-condition operand must be a concrete literal or a param reference; got ${typeof value}.`
5206
+ );
5207
+ }
5208
+ function tokenName2(token) {
5209
+ return token.startsWith("{") && token.endsWith("}") ? token.slice(1, -1) : token;
5210
+ }
3343
5211
  function buildQuerySpec(def) {
3344
5212
  const operations = buildReadOperations(def);
3345
5213
  const executionPlan = deriveExecutionPlan(operations.map((op) => op.resultPath));
@@ -3497,6 +5365,7 @@ function convertLeaf(leaf, bindField, place, context) {
3497
5365
  );
3498
5366
  }
3499
5367
  function convertCondition(condition, place, context) {
5368
+ if (isRawCondition(condition)) return condition;
3500
5369
  if (typeof condition === "object" && condition !== null) {
3501
5370
  const obj = condition;
3502
5371
  if (obj.notExists === true) return { notExists: true };
@@ -3659,8 +5528,32 @@ function toElementRecord(record, keyParams) {
3659
5528
  return out;
3660
5529
  }
3661
5530
  function toElementCondition(condition, keyParams) {
3662
- if (condition.kind !== "equals") return condition;
3663
- return { kind: "equals", fields: toElementRecord(condition.fields, keyParams) };
5531
+ if (condition.kind === "equals") {
5532
+ return { kind: "equals", fields: toElementRecord(condition.fields, keyParams) };
5533
+ }
5534
+ if (condition.kind === "expr") {
5535
+ return { kind: "expr", declarative: toElementTree(condition.declarative, keyParams) };
5536
+ }
5537
+ if (condition.kind === "raw") {
5538
+ return {
5539
+ kind: "raw",
5540
+ expression: condition.expression,
5541
+ names: condition.names,
5542
+ values: toElementTree(condition.values, keyParams)
5543
+ };
5544
+ }
5545
+ return condition;
5546
+ }
5547
+ function toElementTree(node, keyParams) {
5548
+ if (Array.isArray(node)) return node.map((n) => toElementTree(n, keyParams));
5549
+ if (node === null || typeof node !== "object") return node;
5550
+ const obj = node;
5551
+ if (typeof obj.$param === "string" && !obj.$param.startsWith("item.")) {
5552
+ return keyParams.has(obj.$param) ? { $param: `item.${obj.$param}` } : obj;
5553
+ }
5554
+ const out = {};
5555
+ for (const [k, v] of Object.entries(obj)) out[k] = toElementTree(v, keyParams);
5556
+ return out;
3664
5557
  }
3665
5558
  function synthesizeBatchTransaction(commandSpec, op) {
3666
5559
  const keyParams = new Set(keyParamNames(op));
@@ -3933,15 +5826,9 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
3933
5826
  }
3934
5827
  return { params: sortedParams(params), items, maxItems: items.length };
3935
5828
  }
3936
- function batchTargetFor(mode, op, commandSpec, contractName, methodName, opName, transactionSpecs) {
3937
- if (mode === void 0) return void 0;
3938
- if (mode === "batchWrite") {
3939
- if (commandSpec.condition !== void 0) {
3940
- throw new Error(
3941
- `Contract '${contractName}.${methodName}': a 'batchWrite' batched form cannot carry a write condition \u2014 DynamoDB's BatchWriteItem has no ConditionExpression. Declare \`batch: 'transact'\` for a conditional batch.`
3942
- );
3943
- }
3944
- return { mode: "batchWrite", operation: opName };
5829
+ function modeTargetFor(mode, op, commandSpec, contractName, methodName, opName, transactionSpecs) {
5830
+ if (mode === "parallel") {
5831
+ return { mode: "parallel", operation: opName };
3945
5832
  }
3946
5833
  const txName = batchTxName(contractName, methodName);
3947
5834
  transactionSpecs[txName] = synthesizeBatchTransaction(commandSpec, op);
@@ -4076,15 +5963,15 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
4076
5963
  returnSelection
4077
5964
  );
4078
5965
  }
4079
- const batch = batchTargetFor(
4080
- method.batch,
5966
+ const batch = method.mode !== void 0 && !promotedToTransaction ? modeTargetFor(
5967
+ method.mode,
4081
5968
  op,
4082
5969
  commandSpec,
4083
5970
  contractName,
4084
5971
  methodName,
4085
5972
  opName,
4086
5973
  transactionSpecs
4087
- );
5974
+ ) : void 0;
4088
5975
  const single = promotedToTransaction ? { mode: "transaction", transaction: opName } : { mode: "op", operation: opName };
4089
5976
  methods[methodName] = {
4090
5977
  inputArity: method.inputArity,
@@ -4197,13 +6084,28 @@ export {
4197
6084
  buildManifest,
4198
6085
  isSelectBuilder,
4199
6086
  normalizeSelectSpec,
4200
- normalizeTopLevelSelect,
4201
- buildProject,
4202
- buildRelation,
4203
6087
  detectRelationFields,
4204
6088
  getImplicitKeyFields,
4205
- param,
4206
- isParam,
6089
+ buildProjection,
6090
+ compileFilterExpression,
6091
+ evaluateFilter,
6092
+ plan,
6093
+ execute,
6094
+ hydrate,
6095
+ encodeCursor,
6096
+ decodeCursor,
6097
+ executeListInternal,
6098
+ executeList,
6099
+ RELATION_TRAVERSAL_CONCURRENCY,
6100
+ mapWithConcurrency,
6101
+ deriveCompositionPlan,
6102
+ validateDepth,
6103
+ resolveRelations,
6104
+ executeQuery,
6105
+ executeExplain,
6106
+ BatchGetResult,
6107
+ executeBatchGet,
6108
+ executeBatchWrite,
4207
6109
  LIFECYCLE_CONTRACT_MARKER,
4208
6110
  isLifecycleContract,
4209
6111
  ENTITY_WRITES_MARKER,
@@ -4211,11 +6113,6 @@ export {
4211
6113
  entityWrites,
4212
6114
  getEntityWrites,
4213
6115
  lifecyclePhaseForIntent,
4214
- isMutationInputRef,
4215
- isMutationFragment,
4216
- isCommandPlan,
4217
- mutation,
4218
- definePlan,
4219
6116
  OLD_VALUE_NAMESPACE,
4220
6117
  EDGE_WRITES_MARKER,
4221
6118
  isEdgeWritesDefinition,
@@ -4228,7 +6125,13 @@ export {
4228
6125
  compileFragment,
4229
6126
  compileSingleFragmentPlan,
4230
6127
  compileMutationPlan,
4231
- isCommandBatchMode,
6128
+ executeCommandMethod,
6129
+ DDBModel,
6130
+ isMutationInputRef,
6131
+ isMutationFragment,
6132
+ isCommandPlan,
6133
+ mutation,
6134
+ definePlan,
4232
6135
  isContractKeyRef,
4233
6136
  mintContractKeyFieldRef,
4234
6137
  wholeKeysSentinel,
@@ -4240,21 +6143,12 @@ export {
4240
6143
  from,
4241
6144
  query,
4242
6145
  contractOfMethodSpec,
4243
- recordContractOp,
4244
- isRecordingContractMethod,
4245
- point,
4246
- range,
4247
6146
  isQueryModelContract,
4248
6147
  isCommandModelContract,
4249
6148
  publicQueryModel,
4250
6149
  publicCommandModel,
4251
6150
  isPlannedCommandMethod,
4252
- command,
4253
- RELATION_TRAVERSAL_CONCURRENCY,
4254
- mapWithConcurrency,
4255
- buildRelationExecutionPlan,
4256
- deriveCompositionPlan,
4257
- stageOfResultPath,
6151
+ conditionParamName,
4258
6152
  assertSupportedCondition,
4259
6153
  assertJsonSerializable,
4260
6154
  assertBundleSerializable,