graphddb 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,31 @@
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
+ createColumnMap,
17
+ execItemKeySignature,
18
+ executeDelete,
19
+ executePut,
20
+ executeTransaction,
21
+ executeUpdate,
22
+ isColumn,
6
23
  pkTemplate,
7
24
  resolveKey,
8
25
  resolveModelClass,
26
+ resolveSegmentedKey,
9
27
  segmentFieldNames,
28
+ serializeFieldValue,
10
29
  skTemplate
11
30
  } from "./chunk-347U24SB.js";
12
31
 
@@ -262,6 +281,1367 @@ function isParam(value) {
262
281
  return typeof value === "object" && value !== null && "kind" in value && (value.kind === "string" || value.kind === "number" || value.kind === "literal" || value.kind === "array");
263
282
  }
264
283
 
284
+ // src/planner/projection.ts
285
+ function buildProjection(select, additionalFields) {
286
+ const paths = [];
287
+ const names = {};
288
+ let nameCounter = 0;
289
+ function addPath(segments) {
290
+ const aliased = segments.map((seg) => {
291
+ const alias = `#p${nameCounter++}`;
292
+ names[alias] = seg;
293
+ return alias;
294
+ });
295
+ paths.push(aliased.join("."));
296
+ }
297
+ function traverse(selectObj, parentSegments) {
298
+ for (const [fieldName, value] of Object.entries(selectObj)) {
299
+ if (value === true) {
300
+ addPath([...parentSegments, fieldName]);
301
+ } else if (typeof value === "object" && value !== null) {
302
+ if ("select" in value || isSelectBuilder(value)) {
303
+ continue;
304
+ }
305
+ traverse(value, [...parentSegments, fieldName]);
306
+ }
307
+ }
308
+ }
309
+ traverse(select, []);
310
+ if (additionalFields) {
311
+ for (const fieldName of additionalFields) {
312
+ addPath([fieldName]);
313
+ }
314
+ }
315
+ if (paths.length === 0) {
316
+ return void 0;
317
+ }
318
+ return {
319
+ projectionExpression: paths.join(", "),
320
+ expressionAttributeNames: names
321
+ };
322
+ }
323
+
324
+ // src/select/cond.ts
325
+ var RAW_COND = /* @__PURE__ */ Symbol.for("graphddb.rawCond");
326
+ function isRawCondition(value) {
327
+ return typeof value === "object" && value !== null && value[RAW_COND] === true;
328
+ }
329
+ function cond(template, ...parts) {
330
+ return {
331
+ [RAW_COND]: true,
332
+ template,
333
+ parts
334
+ };
335
+ }
336
+ function compileRawCondition(raw, allocName, allocValue) {
337
+ let out = raw.template[0];
338
+ for (let i = 0; i < raw.parts.length; i++) {
339
+ const part = raw.parts[i];
340
+ if (isColumn(part)) {
341
+ out += allocName(part.name);
342
+ } else {
343
+ out += allocValue(part);
344
+ }
345
+ out += raw.template[i + 1];
346
+ }
347
+ return out;
348
+ }
349
+
350
+ // src/expression/filter-expression.ts
351
+ var LOGICAL_KEYS = /* @__PURE__ */ new Set(["and", "or", "not"]);
352
+ var OPERATOR_KEYS = /* @__PURE__ */ new Set([
353
+ "eq",
354
+ "ne",
355
+ "gt",
356
+ "ge",
357
+ "lt",
358
+ "le",
359
+ "between",
360
+ "in",
361
+ "beginsWith",
362
+ "contains",
363
+ "notContains",
364
+ "attributeExists",
365
+ "attributeType",
366
+ "size"
367
+ ]);
368
+ function nameAlias(ctx, column) {
369
+ for (const [alias2, col] of Object.entries(ctx.names)) {
370
+ if (col === column) return alias2;
371
+ }
372
+ const alias = `#f${ctx.nameCounter.n++}`;
373
+ ctx.names[alias] = column;
374
+ return alias;
375
+ }
376
+ function valueAlias(ctx, field, raw) {
377
+ const alias = `:vf${ctx.valueCounter.n++}`;
378
+ const fieldMeta = ctx.fieldMap.get(field);
379
+ ctx.values[alias] = fieldMeta ? serializeFieldValue(raw, fieldMeta) : raw;
380
+ return alias;
381
+ }
382
+ function compileField(ctx, field, condition) {
383
+ const nAlias = nameAlias(ctx, field);
384
+ if (!isOperatorObject(condition)) {
385
+ const vAlias = valueAlias(ctx, field, condition);
386
+ return `${nAlias} = ${vAlias}`;
387
+ }
388
+ const ops = condition;
389
+ const clauses = [];
390
+ for (const [op, value] of Object.entries(ops)) {
391
+ switch (op) {
392
+ case "eq":
393
+ clauses.push(`${nAlias} = ${valueAlias(ctx, field, value)}`);
394
+ break;
395
+ case "ne":
396
+ clauses.push(`${nAlias} <> ${valueAlias(ctx, field, value)}`);
397
+ break;
398
+ case "gt":
399
+ clauses.push(`${nAlias} > ${valueAlias(ctx, field, value)}`);
400
+ break;
401
+ case "ge":
402
+ clauses.push(`${nAlias} >= ${valueAlias(ctx, field, value)}`);
403
+ break;
404
+ case "lt":
405
+ clauses.push(`${nAlias} < ${valueAlias(ctx, field, value)}`);
406
+ break;
407
+ case "le":
408
+ clauses.push(`${nAlias} <= ${valueAlias(ctx, field, value)}`);
409
+ break;
410
+ case "between": {
411
+ const [lo, hi] = value;
412
+ clauses.push(
413
+ `${nAlias} BETWEEN ${valueAlias(ctx, field, lo)} AND ${valueAlias(ctx, field, hi)}`
414
+ );
415
+ break;
416
+ }
417
+ case "in": {
418
+ const arr = value;
419
+ const aliases = arr.map((v) => valueAlias(ctx, field, v));
420
+ clauses.push(`${nAlias} IN (${aliases.join(", ")})`);
421
+ break;
422
+ }
423
+ case "beginsWith":
424
+ clauses.push(`begins_with(${nAlias}, ${valueAlias(ctx, field, value)})`);
425
+ break;
426
+ case "contains":
427
+ clauses.push(`contains(${nAlias}, ${valueAlias(ctx, field, value)})`);
428
+ break;
429
+ case "notContains":
430
+ clauses.push(
431
+ `NOT contains(${nAlias}, ${valueAlias(ctx, field, value)})`
432
+ );
433
+ break;
434
+ case "attributeExists":
435
+ clauses.push(
436
+ value === false ? `attribute_not_exists(${nAlias})` : `attribute_exists(${nAlias})`
437
+ );
438
+ break;
439
+ case "attributeType":
440
+ clauses.push(
441
+ `attribute_type(${nAlias}, ${valueAlias(ctx, field, value)})`
442
+ );
443
+ break;
444
+ case "size":
445
+ clauses.push(`size(${nAlias}) = ${valueAlias(ctx, field, value)}`);
446
+ break;
447
+ default:
448
+ throw new Error(`Unknown filter operator '${op}' on field '${field}'`);
449
+ }
450
+ }
451
+ return joinAnd(clauses);
452
+ }
453
+ function isOperatorObject(value) {
454
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || value instanceof Uint8Array || value instanceof Set) {
455
+ return false;
456
+ }
457
+ const keys = Object.keys(value);
458
+ if (keys.length === 0) return false;
459
+ return keys.every((k) => OPERATOR_KEYS.has(k));
460
+ }
461
+ function joinAnd(clauses) {
462
+ if (clauses.length === 1) return clauses[0];
463
+ return clauses.map((c) => wrap(c)).join(" AND ");
464
+ }
465
+ function wrap(expr) {
466
+ if (isAlreadyWrapped(expr)) return expr;
467
+ if (expr.includes(" AND ") || expr.includes(" OR ")) {
468
+ return `(${expr})`;
469
+ }
470
+ return expr;
471
+ }
472
+ function isAlreadyWrapped(expr) {
473
+ if (!expr.startsWith("(") || !expr.endsWith(")")) return false;
474
+ let depth = 0;
475
+ for (let i = 0; i < expr.length; i++) {
476
+ if (expr[i] === "(") depth++;
477
+ else if (expr[i] === ")") {
478
+ depth--;
479
+ if (depth === 0 && i < expr.length - 1) return false;
480
+ }
481
+ }
482
+ return depth === 0;
483
+ }
484
+ function compileRaw(ctx, raw) {
485
+ return compileRawCondition(
486
+ raw,
487
+ (column) => nameAlias(ctx, column),
488
+ (value) => {
489
+ const alias = `:vf${ctx.valueCounter.n++}`;
490
+ ctx.values[alias] = value;
491
+ return alias;
492
+ }
493
+ );
494
+ }
495
+ function compileNode(ctx, node) {
496
+ if (isRawCondition(node)) {
497
+ return compileRaw(ctx, node);
498
+ }
499
+ const clauses = [];
500
+ for (const [key, value] of Object.entries(node)) {
501
+ if (value === void 0) continue;
502
+ if (LOGICAL_KEYS.has(key)) {
503
+ if (key === "and" || key === "or") {
504
+ const parts = value.map((sub) => compileNode(ctx, sub)).filter((s) => s.length > 0);
505
+ if (parts.length === 0) continue;
506
+ if (parts.length === 1) {
507
+ clauses.push(parts[0]);
508
+ } else {
509
+ const sep = key === "and" ? " AND " : " OR ";
510
+ const joined = parts.map((p) => wrap(p)).join(sep);
511
+ clauses.push(`(${joined})`);
512
+ }
513
+ } else {
514
+ const inner = compileNode(ctx, value);
515
+ if (inner.length > 0) clauses.push(`NOT ${wrap(inner)}`);
516
+ }
517
+ continue;
518
+ }
519
+ const clause = compileField(ctx, key, value);
520
+ if (clause.length > 0) clauses.push(clause);
521
+ }
522
+ return joinAnd(clauses);
523
+ }
524
+ function compileFilterExpression(filter, metadata) {
525
+ if (!filter || Object.keys(filter).length === 0) {
526
+ return void 0;
527
+ }
528
+ const ctx = {
529
+ names: {},
530
+ values: {},
531
+ fieldMap: new Map(metadata.fields.map((f) => [f.propertyName, f])),
532
+ nameCounter: { n: 0 },
533
+ valueCounter: { n: 0 }
534
+ };
535
+ const expression = compileNode(ctx, filter);
536
+ if (expression.length === 0) {
537
+ return void 0;
538
+ }
539
+ return {
540
+ filterExpression: expression,
541
+ expressionAttributeNames: ctx.names,
542
+ expressionAttributeValues: ctx.values
543
+ };
544
+ }
545
+ function evaluateFilter(item, filter) {
546
+ for (const [key, value] of Object.entries(filter)) {
547
+ if (value === void 0) continue;
548
+ if (key === "and") {
549
+ if (!value.every((s) => evaluateFilter(item, s)))
550
+ return false;
551
+ continue;
552
+ }
553
+ if (key === "or") {
554
+ if (!value.some((s) => evaluateFilter(item, s)))
555
+ return false;
556
+ continue;
557
+ }
558
+ if (key === "not") {
559
+ if (evaluateFilter(item, value)) return false;
560
+ continue;
561
+ }
562
+ if (!evaluateFieldCondition(item[key], item, key, value)) {
563
+ return false;
564
+ }
565
+ }
566
+ return true;
567
+ }
568
+ function cmp(a, b) {
569
+ if (a instanceof Date && b instanceof Date) {
570
+ return a.getTime() - b.getTime();
571
+ }
572
+ if (typeof a === "number" && typeof b === "number") return a - b;
573
+ if (typeof a === "string" && typeof b === "string")
574
+ return a < b ? -1 : a > b ? 1 : 0;
575
+ if (a === b) return 0;
576
+ return a < b ? -1 : 1;
577
+ }
578
+ function eq(a, b) {
579
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
580
+ return a === b;
581
+ }
582
+ function evaluateFieldCondition(actual, item, field, condition) {
583
+ if (!isOperatorObject(condition)) {
584
+ return eq(actual, condition);
585
+ }
586
+ const ops = condition;
587
+ for (const [op, value] of Object.entries(ops)) {
588
+ switch (op) {
589
+ case "eq":
590
+ if (!eq(actual, value)) return false;
591
+ break;
592
+ case "ne":
593
+ if (eq(actual, value)) return false;
594
+ break;
595
+ case "gt":
596
+ if (!(cmp(actual, value) > 0)) return false;
597
+ break;
598
+ case "ge":
599
+ if (!(cmp(actual, value) >= 0)) return false;
600
+ break;
601
+ case "lt":
602
+ if (!(cmp(actual, value) < 0)) return false;
603
+ break;
604
+ case "le":
605
+ if (!(cmp(actual, value) <= 0)) return false;
606
+ break;
607
+ case "between": {
608
+ const [lo, hi] = value;
609
+ if (!(cmp(actual, lo) >= 0 && cmp(actual, hi) <= 0)) return false;
610
+ break;
611
+ }
612
+ case "in":
613
+ if (!value.some((v) => eq(actual, v))) return false;
614
+ break;
615
+ case "beginsWith":
616
+ if (typeof actual !== "string" || !actual.startsWith(value))
617
+ return false;
618
+ break;
619
+ case "contains":
620
+ if (typeof actual !== "string" || !actual.includes(value))
621
+ return false;
622
+ break;
623
+ case "notContains":
624
+ if (typeof actual === "string" && actual.includes(value))
625
+ return false;
626
+ break;
627
+ case "attributeExists": {
628
+ const exists = field in item && actual !== void 0 && actual !== null;
629
+ if (value === false ? exists : !exists) return false;
630
+ break;
631
+ }
632
+ case "attributeType":
633
+ break;
634
+ case "size": {
635
+ const len = typeof actual === "string" || Array.isArray(actual) ? actual.length : void 0;
636
+ if (len !== value) return false;
637
+ break;
638
+ }
639
+ default:
640
+ throw new Error(`Unknown filter operator '${op}' on field '${field}'`);
641
+ }
642
+ }
643
+ return true;
644
+ }
645
+
646
+ // src/planner/planner.ts
647
+ function plan(metadata, input) {
648
+ const queryFields = Object.keys(input.key);
649
+ const resolved = resolveKey(queryFields, metadata);
650
+ if (input.consistentRead && resolved.type === "gsi") {
651
+ throw new Error(
652
+ "consistentRead is not supported for GSI queries. Use a primary key query instead."
653
+ );
654
+ }
655
+ const tableName = TableMapping.resolve(metadata.tableName);
656
+ const built = buildKeyCondition(metadata, resolved, input.key);
657
+ const keyCondition = built.keyCondition;
658
+ const additionalProjectionFields = input.updatable ? Array.from(
659
+ /* @__PURE__ */ new Set([...input.additionalProjectionFields ?? [], "PK", "SK"])
660
+ ) : input.additionalProjectionFields;
661
+ const projection = buildProjection(input.select, additionalProjectionFields);
662
+ const compiledFilter = compileFilterExpression(input.filter, metadata);
663
+ const isGetItem = resolved.type === "pk" && !resolved.partial && built.skValue !== void 0 && // GetItem does not support FilterExpression — fall back to Query when a
664
+ // server-side filter is requested so the condition can be applied.
665
+ !compiledFilter;
666
+ const projectionFields2 = projection ? {
667
+ projectionExpression: projection.projectionExpression,
668
+ expressionAttributeNames: projection.expressionAttributeNames
669
+ } : {};
670
+ const filterFields = compiledFilter ? {
671
+ filterExpression: compiledFilter.filterExpression,
672
+ filterExpressionAttributeNames: compiledFilter.expressionAttributeNames,
673
+ filterExpressionAttributeValues: compiledFilter.expressionAttributeValues
674
+ } : {};
675
+ const consistentRead = input.consistentRead && resolved.type === "pk" ? { consistentRead: true } : {};
676
+ let op;
677
+ if (isGetItem) {
678
+ op = {
679
+ type: "GetItem",
680
+ tableName,
681
+ keyCondition,
682
+ ...projectionFields2,
683
+ ...consistentRead
684
+ };
685
+ } else {
686
+ const queryOp = {
687
+ type: "Query",
688
+ tableName,
689
+ keyCondition,
690
+ scanIndexForward: (input.order ?? "ASC") === "ASC",
691
+ ...projectionFields2,
692
+ ...filterFields,
693
+ ...consistentRead
694
+ };
695
+ if (resolved.type === "gsi") {
696
+ queryOp.indexName = resolved.indexName;
697
+ }
698
+ if (built.rangeCondition) {
699
+ queryOp.rangeCondition = built.rangeCondition;
700
+ }
701
+ if (input.limit != null) {
702
+ queryOp.limit = input.limit;
703
+ }
704
+ if (input.after) {
705
+ queryOp.exclusiveStartKey = input.after;
706
+ }
707
+ op = queryOp;
708
+ }
709
+ return { operations: [op] };
710
+ }
711
+ function buildKeyCondition(metadata, resolved, queryKey) {
712
+ if (resolved.type === "pk") {
713
+ if (!metadata.primaryKey) throw new Error("Primary key not defined");
714
+ return buildFromSegments(
715
+ metadata.primaryKey.segmented,
716
+ metadata.primaryKey.inputFieldNames,
717
+ queryKey,
718
+ "PK",
719
+ "SK"
720
+ );
721
+ }
722
+ const gsiDef = metadata.gsiDefinitions.find(
723
+ (g) => g.indexName === resolved.indexName
724
+ );
725
+ if (!gsiDef) throw new Error(`GSI '${resolved.indexName}' not found`);
726
+ return buildFromSegments(
727
+ gsiDef.segmented,
728
+ gsiDef.inputFieldNames,
729
+ queryKey,
730
+ `${gsiDef.indexName}PK`,
731
+ `${gsiDef.indexName}SK`
732
+ );
733
+ }
734
+ function buildFromSegments(segmented, inputFieldNames, queryKey, pkAttr, skAttr) {
735
+ const keyInput = {};
736
+ for (const fieldName of inputFieldNames) {
737
+ keyInput[fieldName] = queryKey[fieldName];
738
+ }
739
+ const { pk, sk, skTruncated } = resolveSegmentedKey(segmented, keyInput);
740
+ const keyCondition = { [pkAttr]: pk };
741
+ if (sk === void 0) {
742
+ return { keyCondition };
743
+ }
744
+ if (skTruncated) {
745
+ return {
746
+ keyCondition,
747
+ rangeCondition: { operator: "begins_with", key: skAttr, value: sk }
748
+ };
749
+ }
750
+ keyCondition[skAttr] = sk;
751
+ return { keyCondition, skValue: sk };
752
+ }
753
+
754
+ // src/executor/executor.ts
755
+ async function execute(operation) {
756
+ return ClientManager.getExecutor().execute(operation);
757
+ }
758
+
759
+ // src/hydrator/hydrator.ts
760
+ function hydrate(rawItems, select, metadata, updatable = false) {
761
+ return rawItems.map((item) => hydrateItem(item, select, metadata, updatable));
762
+ }
763
+ function hydrateItem(raw, select, metadata, updatable = false) {
764
+ const fieldMap = new Map(
765
+ metadata.fields.map((f) => [f.propertyName, f])
766
+ );
767
+ const embeddedMap = new Map(
768
+ metadata.embeddedFields.map((e) => [e.propertyName, e])
769
+ );
770
+ const result = {};
771
+ for (const [fieldName, selectValue] of Object.entries(select)) {
772
+ if (selectValue === true) {
773
+ const value = raw[fieldName];
774
+ if (value !== void 0) {
775
+ const fieldMeta = fieldMap.get(fieldName);
776
+ result[fieldName] = deserializeValue(value, fieldMeta);
777
+ }
778
+ } else if (typeof selectValue === "object" && selectValue !== null) {
779
+ if ("select" in selectValue || isSelectBuilder(selectValue)) {
780
+ continue;
781
+ }
782
+ const rawEmb = raw[fieldName];
783
+ if (rawEmb && typeof rawEmb === "object") {
784
+ result[fieldName] = hydrateEmbedded(
785
+ rawEmb,
786
+ selectValue,
787
+ metadata,
788
+ fieldName
789
+ );
790
+ }
791
+ }
792
+ }
793
+ if (updatable) {
794
+ attachHiddenKey(result, raw);
795
+ }
796
+ return result;
797
+ }
798
+ function hydrateEmbedded(raw, select, _metadata, _fieldName) {
799
+ const result = {};
800
+ for (const [key, selectValue] of Object.entries(select)) {
801
+ if (selectValue === true) {
802
+ if (raw[key] !== void 0) {
803
+ result[key] = raw[key];
804
+ }
805
+ } else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
806
+ const nested = raw[key];
807
+ if (nested && typeof nested === "object") {
808
+ result[key] = hydrateEmbedded(
809
+ nested,
810
+ selectValue,
811
+ _metadata,
812
+ key
813
+ );
814
+ }
815
+ }
816
+ }
817
+ return result;
818
+ }
819
+ function deserializeValue(value, fieldMeta) {
820
+ if (!fieldMeta) return value;
821
+ if (fieldMeta.options?.deserialize) {
822
+ return fieldMeta.options.deserialize(value);
823
+ }
824
+ if (fieldMeta.options?.format === "datetime" && typeof value === "string") {
825
+ return new Date(value);
826
+ }
827
+ if (fieldMeta.options?.format === "date" && typeof value === "string") {
828
+ return /* @__PURE__ */ new Date(value + "T00:00:00.000Z");
829
+ }
830
+ return value;
831
+ }
832
+
833
+ // src/pagination/cursor.ts
834
+ function encodeCursor(lastEvaluatedKey) {
835
+ const json = JSON.stringify(lastEvaluatedKey);
836
+ const bytes = new TextEncoder().encode(json);
837
+ let binary = "";
838
+ for (const b of bytes) {
839
+ binary += String.fromCharCode(b);
840
+ }
841
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
842
+ }
843
+ function decodeCursor(cursor) {
844
+ let base64 = cursor.replace(/-/g, "+").replace(/_/g, "/");
845
+ while (base64.length % 4 !== 0) {
846
+ base64 += "=";
847
+ }
848
+ const binary = atob(base64);
849
+ const bytes = new Uint8Array(binary.length);
850
+ for (let i = 0; i < binary.length; i++) {
851
+ bytes[i] = binary.charCodeAt(i);
852
+ }
853
+ const json = new TextDecoder().decode(bytes);
854
+ return JSON.parse(json);
855
+ }
856
+
857
+ // src/operations/list.ts
858
+ async function executeListInternal(modelClass, key, selectSpec, options = {}) {
859
+ const metadata = MetadataRegistry.get(modelClass);
860
+ const normalized = normalizeTopLevelSelect(selectSpec);
861
+ const select = normalized.select;
862
+ const filter = options.filter ?? normalized.filter;
863
+ const after = options.after ?? normalized.after;
864
+ const limit = options.limit ?? normalized.limit;
865
+ const order = options.order ?? normalized.order;
866
+ const exclusiveStartKey = after ? decodeCursor(after) : void 0;
867
+ const implicitKeys = getImplicitKeyFields(select, metadata);
868
+ const executionPlan = plan(metadata, {
869
+ key,
870
+ select,
871
+ limit,
872
+ after: exclusiveStartKey,
873
+ order,
874
+ filter,
875
+ additionalProjectionFields: implicitKeys.length > 0 ? implicitKeys : void 0,
876
+ updatable: options.updatable ?? false
877
+ });
878
+ const operation = executionPlan.operations[0];
879
+ const result = await execute(operation);
880
+ const rawItems = result.items;
881
+ const hydrated = hydrate(rawItems, select, metadata, options.updatable ?? false);
882
+ const cursor = result.lastEvaluatedKey ? encodeCursor(result.lastEvaluatedKey) : null;
883
+ return { items: hydrated, rawItems, cursor };
884
+ }
885
+ async function executeList(modelClass, key, selectSpec, options = {}) {
886
+ const { items, cursor } = await executeListInternal(
887
+ modelClass,
888
+ key,
889
+ selectSpec,
890
+ options
891
+ );
892
+ return { items, cursor };
893
+ }
894
+
895
+ // src/planner/relation-planner.ts
896
+ function buildRelationQueryKey(rel, rawParentItem) {
897
+ const queryKey = {};
898
+ for (const [targetField, sourceField] of Object.entries(rel.keyBinding)) {
899
+ queryKey[targetField] = rawParentItem[sourceField];
900
+ }
901
+ return queryKey;
902
+ }
903
+ function serializeQueryKey(queryKey) {
904
+ const entries = Object.entries(queryKey).sort(([a], [b]) => a.localeCompare(b));
905
+ return JSON.stringify(entries);
906
+ }
907
+ function planGetItemForQueryKey(metadata, queryKey, select, additionalProjectionFields) {
908
+ const executionPlan = plan(metadata, {
909
+ key: queryKey,
910
+ select,
911
+ additionalProjectionFields
912
+ });
913
+ const operation = executionPlan.operations[0];
914
+ if (operation.type !== "GetItem") {
915
+ throw new Error(
916
+ `Relation target requires GetItem access pattern, got ${operation.type}`
917
+ );
918
+ }
919
+ return operation;
920
+ }
921
+ function planBatchGetForQueryKeys(metadata, queryKeys, select, additionalProjectionFields) {
922
+ if (queryKeys.length === 0) {
923
+ return [];
924
+ }
925
+ const keyFields = [
926
+ ...new Set(queryKeys.flatMap((queryKey) => Object.keys(queryKey)))
927
+ ].filter((field) => !(field in select && select[field] === true));
928
+ const projectionFields2 = [
929
+ .../* @__PURE__ */ new Set([
930
+ ...(additionalProjectionFields ?? []).filter(
931
+ (field) => !(field in select && select[field] === true)
932
+ ),
933
+ ...keyFields
934
+ ])
935
+ ];
936
+ const samplePlan = planGetItemForQueryKey(
937
+ metadata,
938
+ queryKeys[0],
939
+ select,
940
+ projectionFields2.length > 0 ? projectionFields2 : void 0
941
+ );
942
+ const uniqueKeys = dedupeDynamoKeys(
943
+ queryKeys.map(
944
+ (queryKey) => planGetItemForQueryKey(
945
+ metadata,
946
+ queryKey,
947
+ select,
948
+ projectionFields2.length > 0 ? projectionFields2 : void 0
949
+ ).keyCondition
950
+ )
951
+ );
952
+ const operations = [];
953
+ for (let i = 0; i < uniqueKeys.length; i += BATCH_GET_MAX_KEYS) {
954
+ operations.push({
955
+ type: "BatchGetItem",
956
+ tableName: samplePlan.tableName,
957
+ keys: uniqueKeys.slice(i, i + BATCH_GET_MAX_KEYS),
958
+ ...samplePlan.projectionExpression ? { projectionExpression: samplePlan.projectionExpression } : {},
959
+ ...samplePlan.expressionAttributeNames ? { expressionAttributeNames: samplePlan.expressionAttributeNames } : {},
960
+ ...samplePlan.consistentRead ? { consistentRead: true } : {}
961
+ });
962
+ }
963
+ return operations;
964
+ }
965
+ function dedupeDynamoKeys(keys) {
966
+ const seen = /* @__PURE__ */ new Set();
967
+ const result = [];
968
+ for (const key of keys) {
969
+ const serialized = JSON.stringify(
970
+ Object.entries(key).sort(([a], [b]) => a.localeCompare(b))
971
+ );
972
+ if (seen.has(serialized)) continue;
973
+ seen.add(serialized);
974
+ result.push(key);
975
+ }
976
+ return result;
977
+ }
978
+ function planRelationOperations(metadata, select, context = { estimatedParentCount: 1 }) {
979
+ const operations = [];
980
+ const relations = detectRelationFields(select, metadata);
981
+ for (const rel of relations) {
982
+ const selectSpec = normalizeSelectSpec(select[rel.propertyName]);
983
+ const targetClass = rel.targetFactory();
984
+ const targetMeta = MetadataRegistry.get(targetClass);
985
+ if (rel.type === "hasMany") {
986
+ const limit = selectSpec.limit ?? rel.options?.limit?.default ?? 20;
987
+ const tableName = TableMapping.resolve(targetMeta.tableName);
988
+ const listPlan = plan(targetMeta, {
989
+ key: buildPlaceholderHasManyKey(rel, targetMeta),
990
+ select: selectSpec.select,
991
+ limit
992
+ });
993
+ operations.push(...listPlan.operations);
994
+ operations.push(
995
+ ...planRelationOperations(targetMeta, selectSpec.select, {
996
+ estimatedParentCount: limit
997
+ })
998
+ );
999
+ continue;
1000
+ }
1001
+ const estimatedCount = context.estimatedParentCount ?? 1;
1002
+ operations.push(
1003
+ ...planBatchGetForExplain(targetMeta, selectSpec.select, estimatedCount)
1004
+ );
1005
+ operations.push(
1006
+ ...planRelationOperations(targetMeta, selectSpec.select, {
1007
+ estimatedParentCount: 1
1008
+ })
1009
+ );
1010
+ }
1011
+ return operations;
1012
+ }
1013
+ function buildPlaceholderHasManyKey(rel, targetMeta) {
1014
+ const queryKey = {};
1015
+ for (const targetField of Object.keys(rel.keyBinding)) {
1016
+ queryKey[targetField] = `__${targetField}__`;
1017
+ }
1018
+ if (targetMeta.primaryKey) {
1019
+ for (const fieldName of targetMeta.primaryKey.inputFieldNames) {
1020
+ if (!(fieldName in queryKey)) {
1021
+ queryKey[fieldName] = `__${fieldName}__`;
1022
+ }
1023
+ }
1024
+ }
1025
+ return queryKey;
1026
+ }
1027
+ function planBatchGetForExplain(metadata, select, estimatedKeyCount) {
1028
+ const placeholderKeys = Array.from(
1029
+ { length: estimatedKeyCount },
1030
+ (_, index) => buildExplainPlaceholderKey(metadata, index)
1031
+ );
1032
+ return planBatchGetForQueryKeys(metadata, placeholderKeys, select);
1033
+ }
1034
+ function buildExplainPlaceholderKey(metadata, index) {
1035
+ const queryKey = {};
1036
+ if (metadata.primaryKey) {
1037
+ for (const fieldName of metadata.primaryKey.inputFieldNames) {
1038
+ queryKey[fieldName] = `__explain_${fieldName}_${index}__`;
1039
+ }
1040
+ }
1041
+ return queryKey;
1042
+ }
1043
+
1044
+ // src/relation/concurrency.ts
1045
+ var RELATION_TRAVERSAL_CONCURRENCY = 16;
1046
+ async function mapWithConcurrency(items, limit, worker) {
1047
+ if (items.length === 0) {
1048
+ return [];
1049
+ }
1050
+ const effectiveLimit = Math.max(1, Math.min(limit, items.length));
1051
+ if (effectiveLimit >= items.length) {
1052
+ return Promise.all(items.map((item, index) => worker(item, index)));
1053
+ }
1054
+ const results = new Array(items.length);
1055
+ let nextIndex = 0;
1056
+ async function runWorker() {
1057
+ while (true) {
1058
+ const index = nextIndex++;
1059
+ if (index >= items.length) {
1060
+ return;
1061
+ }
1062
+ results[index] = await worker(items[index], index);
1063
+ }
1064
+ }
1065
+ const runners = [];
1066
+ for (let i = 0; i < effectiveLimit; i++) {
1067
+ runners.push(runWorker());
1068
+ }
1069
+ await Promise.all(runners);
1070
+ return results;
1071
+ }
1072
+
1073
+ // src/relation/execution-plan.ts
1074
+ function relationResultPaths(select, metadata, parentPath = "$") {
1075
+ const out = [];
1076
+ const relations = detectRelationFields(select, metadata);
1077
+ for (const rel of [...relations].sort(
1078
+ (a, b) => a.propertyName.localeCompare(b.propertyName)
1079
+ )) {
1080
+ const childSelect = normalizeSelectSpec(select[rel.propertyName]).select ?? {};
1081
+ const targetMeta = MetadataRegistry.get(rel.targetFactory());
1082
+ const childPath = `${parentPath}.${rel.propertyName}`;
1083
+ const isMany = rel.type === "hasMany";
1084
+ const resultPath = isMany ? `${childPath}.items` : childPath;
1085
+ out.push({
1086
+ propertyName: rel.propertyName,
1087
+ parentPath,
1088
+ resultPath,
1089
+ isMany
1090
+ });
1091
+ out.push(...relationResultPaths(childSelect, targetMeta, resultPath));
1092
+ }
1093
+ return out;
1094
+ }
1095
+ function parentResultPath(resultPath) {
1096
+ if (!resultPath.startsWith("$.")) return "$";
1097
+ const tokens = resultPath.slice(2).split(".");
1098
+ if (tokens[tokens.length - 1] === "items") tokens.pop();
1099
+ tokens.pop();
1100
+ return tokens.length === 0 ? "$" : `$.${tokens.join(".")}`;
1101
+ }
1102
+ function deriveExecutionPlan(resultPaths) {
1103
+ if (resultPaths.length <= 1) return void 0;
1104
+ const indexByPath = /* @__PURE__ */ new Map();
1105
+ resultPaths.forEach((path, i) => {
1106
+ indexByPath.set(path, i);
1107
+ });
1108
+ const stageOf = new Array(resultPaths.length);
1109
+ for (let i = 0; i < resultPaths.length; i++) {
1110
+ if (resultPaths[i] === "$") {
1111
+ stageOf[i] = 0;
1112
+ continue;
1113
+ }
1114
+ const parentPath = parentResultPath(resultPaths[i]);
1115
+ const parentIndex = indexByPath.get(parentPath);
1116
+ const parentStage = parentIndex !== void 0 ? stageOf[parentIndex] : 0;
1117
+ stageOf[i] = parentStage + 1;
1118
+ }
1119
+ const stageCount = Math.max(...stageOf) + 1;
1120
+ const groups = Array.from({ length: stageCount }, () => []);
1121
+ for (let i = 0; i < resultPaths.length; i++) {
1122
+ groups[stageOf[i]].push(i);
1123
+ }
1124
+ for (const g of groups) g.sort((a, b) => a - b);
1125
+ return { groups, concurrency: RELATION_TRAVERSAL_CONCURRENCY };
1126
+ }
1127
+ function buildRelationExecutionPlan(select, metadata) {
1128
+ const relationPaths = relationResultPaths(select, metadata);
1129
+ if (relationPaths.length === 0) return void 0;
1130
+ const resultPaths = ["$", ...relationPaths.map((r) => r.resultPath)];
1131
+ const plan2 = deriveExecutionPlan(resultPaths);
1132
+ if (plan2 === void 0) return void 0;
1133
+ return { plan: plan2, resultPaths };
1134
+ }
1135
+ function deriveCompositionPlan(composeCount) {
1136
+ if (composeCount <= 0) {
1137
+ return { stages: [], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
1138
+ }
1139
+ const stage = Array.from({ length: composeCount }, (_unused, i) => i);
1140
+ return { stages: [stage], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
1141
+ }
1142
+ function stageOfResultPath(resolved, resultPath) {
1143
+ const index = resolved.resultPaths.indexOf(resultPath);
1144
+ if (index < 0) return void 0;
1145
+ for (let s = 0; s < resolved.plan.groups.length; s++) {
1146
+ if (resolved.plan.groups[s].includes(index)) return s;
1147
+ }
1148
+ return void 0;
1149
+ }
1150
+
1151
+ // src/relation/traversal.ts
1152
+ function evaluateFilterClientSide(item, filter, _targetMeta) {
1153
+ return evaluateFilter(item, filter);
1154
+ }
1155
+ function relationSpec(value) {
1156
+ return normalizeSelectSpec(value);
1157
+ }
1158
+ function validateDepth(select, metadata, maxDepth, currentDepth = 1) {
1159
+ for (const [fieldName, value] of Object.entries(select)) {
1160
+ if (typeof value === "object" && value !== null && ("select" in value || isSelectBuilder(value))) {
1161
+ const rel = metadata.relations.find((r) => r.propertyName === fieldName);
1162
+ if (!rel) continue;
1163
+ if (currentDepth > maxDepth) {
1164
+ throw new Error(
1165
+ `Relation traversal depth exceeded: '${fieldName}' at depth ${currentDepth} exceeds maxDepth ${maxDepth}. Set maxDepth: ${currentDepth} to allow this.`
1166
+ );
1167
+ }
1168
+ const targetClass = rel.targetFactory();
1169
+ const targetMeta = MetadataRegistry.get(targetClass);
1170
+ const innerSelect = relationSpec(value).select;
1171
+ validateDepth(innerSelect, targetMeta, maxDepth, currentDepth + 1);
1172
+ }
1173
+ }
1174
+ }
1175
+ function relationResultPath(parentPath, rel) {
1176
+ const base = `${parentPath}.${rel.propertyName}`;
1177
+ return rel.type === "hasMany" ? `${base}.items` : base;
1178
+ }
1179
+ function stageRelations(relations, parentPath, plan2) {
1180
+ if (plan2 === void 0 || relations.length === 0) {
1181
+ return relations.length > 0 ? { stages: [[...relations]], concurrency: RELATION_TRAVERSAL_CONCURRENCY } : { stages: [], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
1182
+ }
1183
+ const byStage = /* @__PURE__ */ new Map();
1184
+ const unstaged = [];
1185
+ for (const rel of relations) {
1186
+ const stage = stageOfResultPath(plan2, relationResultPath(parentPath, rel));
1187
+ if (stage === void 0) {
1188
+ unstaged.push(rel);
1189
+ continue;
1190
+ }
1191
+ const bucket = byStage.get(stage);
1192
+ if (bucket) bucket.push(rel);
1193
+ else byStage.set(stage, [rel]);
1194
+ }
1195
+ const stages = [...byStage.keys()].sort((a, b) => a - b).map((s) => byStage.get(s));
1196
+ if (unstaged.length > 0) stages.push(unstaged);
1197
+ return { stages, concurrency: plan2.plan.concurrency };
1198
+ }
1199
+ async function runStaged(relations, parentPath, plan2, worker) {
1200
+ const { stages, concurrency } = stageRelations(relations, parentPath, plan2);
1201
+ for (const stage of stages) {
1202
+ await mapWithConcurrency(stage, concurrency, (rel) => worker(rel));
1203
+ }
1204
+ }
1205
+ function hasCompleteQueryKey(queryKey) {
1206
+ return Object.values(queryKey).every((value) => value != null);
1207
+ }
1208
+ function matchesQueryKey(item, queryKey) {
1209
+ return Object.entries(queryKey).every(
1210
+ ([field, value]) => item[field] === value
1211
+ );
1212
+ }
1213
+ async function fetchBelongsToHasOneBatch(rel, rawParentItems, selectSpec, targetMeta, options, currentDepth, parentPath) {
1214
+ const results = rawParentItems.map(
1215
+ () => null
1216
+ );
1217
+ const queryKeys = [];
1218
+ const parentIndexes = [];
1219
+ for (let i = 0; i < rawParentItems.length; i++) {
1220
+ const queryKey = buildRelationQueryKey(rel, rawParentItems[i]);
1221
+ if (!hasCompleteQueryKey(queryKey)) {
1222
+ continue;
1223
+ }
1224
+ queryKeys.push(queryKey);
1225
+ parentIndexes.push(i);
1226
+ }
1227
+ if (queryKeys.length === 0) {
1228
+ return results;
1229
+ }
1230
+ const implicitKeys = getImplicitKeyFields(selectSpec.select, targetMeta);
1231
+ const projectionExtras = options.updatable ? [...implicitKeys, "PK", "SK"] : implicitKeys;
1232
+ const batchOps = planBatchGetForQueryKeys(
1233
+ targetMeta,
1234
+ queryKeys,
1235
+ selectSpec.select,
1236
+ projectionExtras.length > 0 ? projectionExtras : void 0
1237
+ );
1238
+ const executor = ClientManager.getExecutor();
1239
+ const queryKeyToRaw = /* @__PURE__ */ new Map();
1240
+ for (const operation of batchOps) {
1241
+ const batchResult = await executor.batchGet(operation);
1242
+ for (const item of batchResult.items) {
1243
+ for (const queryKey of queryKeys) {
1244
+ if (!matchesQueryKey(item, queryKey)) {
1245
+ continue;
1246
+ }
1247
+ queryKeyToRaw.set(serializeQueryKey(queryKey), item);
1248
+ }
1249
+ }
1250
+ }
1251
+ const nestedRelations = detectRelationFields(selectSpec.select, targetMeta);
1252
+ const ownPath = relationResultPath(parentPath, rel);
1253
+ await mapWithConcurrency(
1254
+ queryKeys,
1255
+ options.plan?.plan.concurrency ?? RELATION_TRAVERSAL_CONCURRENCY,
1256
+ async (queryKey, j) => {
1257
+ const parentIndex = parentIndexes[j];
1258
+ const raw = queryKeyToRaw.get(serializeQueryKey(queryKey));
1259
+ if (!raw) {
1260
+ return;
1261
+ }
1262
+ const updatable = options.updatable ?? false;
1263
+ let resolvedItem = hydrate([raw], selectSpec.select, targetMeta, updatable)[0] ?? null;
1264
+ if (resolvedItem && nestedRelations.length > 0) {
1265
+ resolvedItem = await resolveRelations(
1266
+ resolvedItem,
1267
+ raw,
1268
+ selectSpec.select,
1269
+ targetMeta,
1270
+ options,
1271
+ currentDepth + 1,
1272
+ ownPath
1273
+ );
1274
+ if (resolvedItem && updatable) {
1275
+ attachHiddenKey(resolvedItem, raw);
1276
+ }
1277
+ }
1278
+ if (resolvedItem && !passesPostLoad(resolvedItem, selectSpec, targetMeta)) {
1279
+ results[parentIndex] = null;
1280
+ } else {
1281
+ results[parentIndex] = resolvedItem;
1282
+ }
1283
+ }
1284
+ );
1285
+ return results;
1286
+ }
1287
+ function passesPostLoad(item, spec, targetMeta) {
1288
+ if (spec.filter && !evaluateFilterClientSide(item, spec.filter, targetMeta)) {
1289
+ return false;
1290
+ }
1291
+ return true;
1292
+ }
1293
+ async function resolveSingleValueRelationsBatch(parentItems, rawParentItems, select, metadata, options, currentDepth, parentPath) {
1294
+ const results = parentItems.map((item, i) => {
1295
+ const copy = { ...item };
1296
+ if (options.updatable) {
1297
+ attachHiddenKey(copy, rawParentItems[i]);
1298
+ }
1299
+ return copy;
1300
+ });
1301
+ const relations = detectRelationFields(select, metadata).filter(
1302
+ (rel) => rel.type === "belongsTo" || rel.type === "hasOne"
1303
+ );
1304
+ await runStaged(relations, parentPath, options.plan, async (rel) => {
1305
+ const selectSpec = relationSpec(select[rel.propertyName]);
1306
+ const targetClass = rel.targetFactory();
1307
+ const targetMeta = MetadataRegistry.get(targetClass);
1308
+ const batchResults = await fetchBelongsToHasOneBatch(
1309
+ rel,
1310
+ rawParentItems,
1311
+ selectSpec,
1312
+ targetMeta,
1313
+ options,
1314
+ currentDepth,
1315
+ parentPath
1316
+ );
1317
+ for (let i = 0; i < results.length; i++) {
1318
+ results[i][rel.propertyName] = batchResults[i];
1319
+ }
1320
+ });
1321
+ return results;
1322
+ }
1323
+ async function resolveRelations(parentItem, rawParentItem, select, metadata, options, currentDepth = 1, currentPath = "$") {
1324
+ const result = { ...parentItem };
1325
+ const relations = detectRelationFields(select, metadata);
1326
+ const plan2 = options.plan ?? (currentPath === "$" ? buildRelationExecutionPlan(select, metadata) : void 0);
1327
+ const opts = plan2 === options.plan ? options : { ...options, plan: plan2 };
1328
+ await runStaged(relations, currentPath, opts.plan, async (rel) => {
1329
+ const selectSpec = relationSpec(select[rel.propertyName]);
1330
+ const ownPath = relationResultPath(currentPath, rel);
1331
+ if (rel.type === "hasMany") {
1332
+ const targetClass2 = rel.targetFactory();
1333
+ const queryKey = buildRelationQueryKey(rel, rawParentItem);
1334
+ const limit = selectSpec.limit ?? rel.options?.limit?.default ?? 20;
1335
+ const order = selectSpec.order ?? rel.options?.order ?? "ASC";
1336
+ const listResult = await executeListInternal(targetClass2, queryKey, selectSpec.select, {
1337
+ limit,
1338
+ after: selectSpec.after,
1339
+ order,
1340
+ filter: selectSpec.filter,
1341
+ updatable: opts.updatable
1342
+ });
1343
+ let items = listResult.items;
1344
+ const rawItems = listResult.rawItems;
1345
+ const targetMeta2 = MetadataRegistry.get(targetClass2);
1346
+ const nestedRelations = detectRelationFields(selectSpec.select, targetMeta2);
1347
+ if (nestedRelations.length > 0) {
1348
+ items = await resolveSingleValueRelationsBatch(
1349
+ items,
1350
+ rawItems,
1351
+ selectSpec.select,
1352
+ targetMeta2,
1353
+ opts,
1354
+ currentDepth + 1,
1355
+ ownPath
1356
+ );
1357
+ }
1358
+ result[rel.propertyName] = {
1359
+ items,
1360
+ cursor: listResult.cursor
1361
+ };
1362
+ return;
1363
+ }
1364
+ const targetClass = rel.targetFactory();
1365
+ const targetMeta = MetadataRegistry.get(targetClass);
1366
+ const [resolvedItem] = await fetchBelongsToHasOneBatch(
1367
+ rel,
1368
+ [rawParentItem],
1369
+ selectSpec,
1370
+ targetMeta,
1371
+ opts,
1372
+ currentDepth,
1373
+ currentPath
1374
+ );
1375
+ result[rel.propertyName] = resolvedItem;
1376
+ });
1377
+ return result;
1378
+ }
1379
+
1380
+ // src/operations/query.ts
1381
+ async function executeQuery(modelClass, key, selectSpec, options) {
1382
+ const metadata = MetadataRegistry.get(modelClass);
1383
+ const queryFields = Object.keys(key);
1384
+ const resolved = resolveKey(queryFields, metadata);
1385
+ if (resolved.type === "gsi" && !resolved.unique) {
1386
+ throw new Error(
1387
+ `Cannot use query() with non-unique GSI '${resolved.indexName}'. Use list() instead.`
1388
+ );
1389
+ }
1390
+ const normalized = normalizeTopLevelSelect(selectSpec);
1391
+ const select = normalized.select;
1392
+ const filter = normalized.filter;
1393
+ const maxDepth = options?.maxDepth ?? 1;
1394
+ const relations = detectRelationFields(select, metadata);
1395
+ if (relations.length > 0) {
1396
+ validateDepth(select, metadata, maxDepth);
1397
+ }
1398
+ const implicitKeys = getImplicitKeyFields(select, metadata);
1399
+ const updatable = options?.updatable ?? false;
1400
+ const executionPlan = plan(metadata, {
1401
+ key,
1402
+ select,
1403
+ filter,
1404
+ consistentRead: options?.consistentRead,
1405
+ additionalProjectionFields: implicitKeys.length > 0 ? implicitKeys : void 0,
1406
+ updatable
1407
+ });
1408
+ const operation = executionPlan.operations[0];
1409
+ const canParallelizeRelations = relations.length > 0 && relations.every(
1410
+ (rel) => Object.values(rel.keyBinding).every(
1411
+ (sourceField) => key[sourceField] != null
1412
+ )
1413
+ );
1414
+ const parentPromise = execute(operation);
1415
+ const relationPromise = canParallelizeRelations ? resolveRelations({}, key, select, metadata, { maxDepth, updatable }, 1) : null;
1416
+ let result;
1417
+ try {
1418
+ result = await parentPromise;
1419
+ } catch (err) {
1420
+ if (relationPromise) await relationPromise.catch(() => void 0);
1421
+ throw err;
1422
+ }
1423
+ if (result.items.length === 0) {
1424
+ if (relationPromise) await relationPromise.catch(() => void 0);
1425
+ return null;
1426
+ }
1427
+ const rawItem = result.items[0];
1428
+ const hydrated = hydrate(result.items, select, metadata, updatable);
1429
+ let hydratedItem = hydrated[0] ?? null;
1430
+ if (hydratedItem && relations.length > 0) {
1431
+ if (relationPromise) {
1432
+ const resolvedRelations = await relationPromise;
1433
+ for (const rel of relations) {
1434
+ hydratedItem[rel.propertyName] = resolvedRelations[rel.propertyName];
1435
+ }
1436
+ } else {
1437
+ hydratedItem = await resolveRelations(
1438
+ hydratedItem,
1439
+ rawItem,
1440
+ select,
1441
+ metadata,
1442
+ { maxDepth, updatable },
1443
+ 1
1444
+ );
1445
+ if (hydratedItem && updatable) {
1446
+ attachHiddenKey(hydratedItem, rawItem);
1447
+ }
1448
+ }
1449
+ }
1450
+ if (options?.hydrate) {
1451
+ return options.hydrate(hydratedItem);
1452
+ }
1453
+ return hydratedItem;
1454
+ }
1455
+
1456
+ // src/operations/explain.ts
1457
+ function executeExplain(modelClass, key, options) {
1458
+ const metadata = MetadataRegistry.get(modelClass);
1459
+ const exclusiveStartKey = options?.after ? decodeCursor(options.after) : void 0;
1460
+ const normalized = normalizeTopLevelSelect(options?.select);
1461
+ const select = normalized.select ?? {};
1462
+ const filter = options?.filter ?? normalized.filter;
1463
+ const mainPlan = plan(metadata, {
1464
+ key,
1465
+ select,
1466
+ limit: options?.limit,
1467
+ after: exclusiveStartKey,
1468
+ order: options?.order,
1469
+ consistentRead: options?.consistentRead,
1470
+ filter
1471
+ });
1472
+ const relationOps = planRelationOperations(metadata, select, {
1473
+ estimatedParentCount: 1
1474
+ });
1475
+ return {
1476
+ operations: [...mainPlan.operations, ...relationOps]
1477
+ };
1478
+ }
1479
+
1480
+ // src/operations/batch.ts
1481
+ var BatchGetResult = class {
1482
+ groups;
1483
+ constructor(groups) {
1484
+ this.groups = groups;
1485
+ }
1486
+ get(model) {
1487
+ const modelClass = resolveModelClass(model);
1488
+ return this.groups.get(modelClass) ?? [];
1489
+ }
1490
+ };
1491
+ function buildScalarSelect(metadata) {
1492
+ const select = {};
1493
+ for (const field of metadata.fields) {
1494
+ select[field.propertyName] = true;
1495
+ }
1496
+ return select;
1497
+ }
1498
+ function buildDynamoKey(modelClass, keyObj) {
1499
+ const deleteInput = buildDeleteInput(modelClass, keyObj);
1500
+ return deleteInput.Key;
1501
+ }
1502
+ function hydrateBatchItems(rawItems, metadata) {
1503
+ if (rawItems.length === 0) {
1504
+ return [];
1505
+ }
1506
+ const select = buildScalarSelect(metadata);
1507
+ const hydrated = hydrate(rawItems, select, metadata);
1508
+ if (metadata.embeddedFields.length === 0) {
1509
+ return hydrated;
1510
+ }
1511
+ return hydrated.map((item, index) => {
1512
+ const raw = rawItems[index];
1513
+ const merged = { ...item };
1514
+ for (const emb of metadata.embeddedFields) {
1515
+ const rawEmb = raw?.[emb.propertyName];
1516
+ if (rawEmb !== void 0 && rawEmb !== null) {
1517
+ merged[emb.propertyName] = rawEmb;
1518
+ }
1519
+ }
1520
+ return merged;
1521
+ });
1522
+ }
1523
+ async function executeBatchGetForTable(tableName, entries) {
1524
+ const groups = /* @__PURE__ */ new Map();
1525
+ if (entries.length === 0) {
1526
+ return groups;
1527
+ }
1528
+ const keys = entries.map((entry) => entry.dynamoKey);
1529
+ const { items: rawItems } = await ClientManager.getExecutor().batchGet({
1530
+ tableName,
1531
+ keys
1532
+ });
1533
+ const entriesByModel = /* @__PURE__ */ new Map();
1534
+ for (const entry of entries) {
1535
+ const existing = entriesByModel.get(entry.modelClass) ?? [];
1536
+ existing.push(entry);
1537
+ entriesByModel.set(entry.modelClass, existing);
1538
+ }
1539
+ for (const [modelClass, modelEntries] of entriesByModel) {
1540
+ const metadata = modelEntries[0].metadata;
1541
+ const expectedKeys = new Set(
1542
+ modelEntries.map(
1543
+ (entry) => `${String(entry.dynamoKey.PK)}::${String(entry.dynamoKey.SK)}`
1544
+ )
1545
+ );
1546
+ const modelRawItems = rawItems.filter(
1547
+ (item) => expectedKeys.has(`${String(item.PK)}::${String(item.SK)}`)
1548
+ );
1549
+ groups.set(modelClass, hydrateBatchItems(modelRawItems, metadata));
1550
+ }
1551
+ return groups;
1552
+ }
1553
+ async function executeBatchGet(requests) {
1554
+ const entriesByTable = /* @__PURE__ */ new Map();
1555
+ for (const request of requests) {
1556
+ const modelClass = resolveModelClass(request.model);
1557
+ const metadata = MetadataRegistry.get(modelClass);
1558
+ const tableName = TableMapping.resolve(metadata.tableName);
1559
+ for (const keyObj of request.keys) {
1560
+ const entry = {
1561
+ modelClass,
1562
+ metadata,
1563
+ dynamoKey: buildDynamoKey(modelClass, keyObj)
1564
+ };
1565
+ const existing = entriesByTable.get(tableName) ?? [];
1566
+ existing.push(entry);
1567
+ entriesByTable.set(tableName, existing);
1568
+ }
1569
+ }
1570
+ const mergedGroups = /* @__PURE__ */ new Map();
1571
+ for (const [, entries] of entriesByTable) {
1572
+ const tableName = TableMapping.resolve(entries[0].metadata.tableName);
1573
+ const tableGroups = await executeBatchGetForTable(tableName, entries);
1574
+ for (const [modelClass, items] of tableGroups) {
1575
+ const existing = mergedGroups.get(modelClass) ?? [];
1576
+ mergedGroups.set(modelClass, existing.concat(items));
1577
+ }
1578
+ }
1579
+ return new BatchGetResult(mergedGroups);
1580
+ }
1581
+ function toExecItem(entry) {
1582
+ if (entry.request.type === "put") {
1583
+ return { type: "put", item: entry.request.putInput.Item };
1584
+ }
1585
+ return { type: "delete", key: entry.request.deleteInput.Key };
1586
+ }
1587
+ async function executeBatchWrite(requests) {
1588
+ const entriesByTable = /* @__PURE__ */ new Map();
1589
+ for (const request of requests) {
1590
+ const modelClass = resolveModelClass(request.model);
1591
+ const metadata = MetadataRegistry.get(modelClass);
1592
+ const tableName = TableMapping.resolve(metadata.tableName);
1593
+ if (request.type === "put") {
1594
+ const putInput = buildPutInput(modelClass, request.item, request.options);
1595
+ const entry2 = {
1596
+ tableName,
1597
+ modelName: modelClass.name,
1598
+ request: { type: "put", putInput }
1599
+ };
1600
+ const existing2 = entriesByTable.get(tableName) ?? [];
1601
+ existing2.push(entry2);
1602
+ entriesByTable.set(tableName, existing2);
1603
+ continue;
1604
+ }
1605
+ const deleteInput = buildDeleteInput(modelClass, request.key, request.options);
1606
+ const entry = {
1607
+ tableName,
1608
+ modelName: modelClass.name,
1609
+ request: { type: "delete", deleteInput }
1610
+ };
1611
+ const existing = entriesByTable.get(tableName) ?? [];
1612
+ existing.push(entry);
1613
+ entriesByTable.set(tableName, existing);
1614
+ }
1615
+ const executor = ClientManager.getExecutor();
1616
+ for (const [tableName, entries] of entriesByTable) {
1617
+ await executor.batchWrite(tableName, entries.map(toExecItem));
1618
+ }
1619
+ if (ChangeCaptureRegistry.hasSubscribers()) {
1620
+ for (const [, entries] of entriesByTable) {
1621
+ for (const entry of entries) {
1622
+ if (entry.request.type === "put") {
1623
+ const item = entry.request.putInput.Item;
1624
+ captureWrite({
1625
+ table: entry.tableName,
1626
+ ...entry.modelName ? { model: entry.modelName } : {},
1627
+ op: "put",
1628
+ keys: { pk: String(item.PK), sk: String(item.SK ?? "") },
1629
+ newItem: item
1630
+ });
1631
+ } else {
1632
+ const dkey = entry.request.deleteInput.Key;
1633
+ captureWrite({
1634
+ table: entry.tableName,
1635
+ ...entry.modelName ? { model: entry.modelName } : {},
1636
+ op: "delete",
1637
+ keys: { pk: String(dkey.PK), sk: String(dkey.SK ?? "") }
1638
+ });
1639
+ }
1640
+ }
1641
+ }
1642
+ }
1643
+ }
1644
+
265
1645
  // src/define/entity-writes.ts
266
1646
  var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
267
1647
  "graphddb:lifecycleContract"
@@ -269,86 +1649,1087 @@ var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
269
1649
  function isLifecycleContract(value) {
270
1650
  return typeof value === "object" && value !== null && value[LIFECYCLE_CONTRACT_MARKER] === true;
271
1651
  }
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;
1652
+ var ENTITY_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:entityWrites");
1653
+ function isEntityWritesDefinition(value) {
1654
+ return typeof value === "object" && value !== null && value[ENTITY_WRITES_MARKER] === true;
1655
+ }
1656
+ function freezeEffects(effects) {
1657
+ const out = {};
1658
+ if (effects.requires !== void 0) out.requires = effects.requires;
1659
+ if (effects.unique !== void 0) out.unique = effects.unique;
1660
+ if (effects.edges !== void 0) out.edges = effects.edges;
1661
+ if (effects.derive !== void 0) out.derive = effects.derive;
1662
+ if (effects.emits !== void 0) out.emits = effects.emits;
1663
+ if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
1664
+ return out;
1665
+ }
1666
+ var recorder = {
1667
+ lifecycle(effects = {}) {
1668
+ return {
1669
+ [LIFECYCLE_CONTRACT_MARKER]: true,
1670
+ effects: freezeEffects(effects)
1671
+ };
1672
+ },
1673
+ exists(targetFactory, keys) {
1674
+ return { kind: "requires", targetFactory, keys };
1675
+ },
1676
+ unique(spec) {
1677
+ return { kind: "unique", name: spec.name, scope: spec.scope, fields: spec.fields };
1678
+ },
1679
+ putEdge(targetFactory, relationProperty) {
1680
+ return { kind: "putEdge", targetFactory, relationProperty };
1681
+ },
1682
+ deleteEdge(targetFactory, relationProperty) {
1683
+ return { kind: "deleteEdge", targetFactory, relationProperty };
1684
+ },
1685
+ increment(targetFactory, keys, attribute, amount) {
1686
+ return { kind: "derive", targetFactory, keys, attribute, amount };
1687
+ },
1688
+ event(name, payload) {
1689
+ return { kind: "event", name, payload };
1690
+ },
1691
+ idempotentBy(token) {
1692
+ return { kind: "idempotency", token };
1693
+ }
1694
+ };
1695
+ function entityWrites(builder) {
1696
+ const shape = builder(recorder);
1697
+ for (const phase of ["create", "update", "remove"]) {
1698
+ const contract = shape[phase];
1699
+ if (contract !== void 0 && !isLifecycleContract(contract)) {
1700
+ throw new Error(
1701
+ `entityWrites: the '${phase}' lifecycle must be built with \`w.lifecycle({...})\` (it carries the \xA72 effect arrays), but received ${JSON.stringify(contract)}.`
1702
+ );
1703
+ }
1704
+ }
1705
+ if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
1706
+ throw new Error(
1707
+ "entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`); an empty save contract declares nothing."
1708
+ );
1709
+ }
1710
+ return {
1711
+ [ENTITY_WRITES_MARKER]: true,
1712
+ ...shape.create !== void 0 ? { create: shape.create } : {},
1713
+ ...shape.update !== void 0 ? { update: shape.update } : {},
1714
+ ...shape.remove !== void 0 ? { remove: shape.remove } : {}
1715
+ };
1716
+ }
1717
+ function getEntityWrites(modelClass) {
1718
+ for (const name of Object.getOwnPropertyNames(modelClass)) {
1719
+ let value;
1720
+ try {
1721
+ value = modelClass[name];
1722
+ } catch {
1723
+ continue;
1724
+ }
1725
+ if (isEntityWritesDefinition(value)) return value;
1726
+ }
1727
+ return void 0;
1728
+ }
1729
+ function lifecyclePhaseForIntent(intent) {
1730
+ return intent;
1731
+ }
1732
+
1733
+ // src/relation/edge-write.ts
1734
+ var OLD_VALUE_NAMESPACE = "old";
1735
+ var EDGE_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:edgeWrites");
1736
+ function isEdgeWritesDefinition(value) {
1737
+ return typeof value === "object" && value !== null && value[EDGE_WRITES_MARKER] === true;
1738
+ }
1739
+ function declaration(target, relationProperty) {
1740
+ return { targetFactory: target, relationProperty };
1741
+ }
1742
+ var recorder2 = {
1743
+ putEdge: declaration,
1744
+ deleteEdge: declaration,
1745
+ updateKeyChangeEdge: declaration
1746
+ };
1747
+ function edgeWrites(builder) {
1748
+ const edges = builder(recorder2);
1749
+ if (edges.length === 0) {
1750
+ throw new Error(
1751
+ "edgeWrites(...) must declare at least one edge (w.putEdge / w.deleteEdge / w.updateKeyChangeEdge); an empty declaration writes nothing."
1752
+ );
1753
+ }
1754
+ return { [EDGE_WRITES_MARKER]: true, edges };
1755
+ }
1756
+ function edgeKeyFields(segmented) {
1757
+ return [
1758
+ ...segmentFieldNames(segmented.pkSegments),
1759
+ ...segmentFieldNames(segmented.skSegments)
1760
+ ];
1761
+ }
1762
+ function assertRelationRoundTrips(entity, edgeFields, decl) {
1763
+ const targetClass = decl.targetFactory();
1764
+ const targetMeta = MetadataRegistry.get(targetClass);
1765
+ const relation = targetMeta.relations.find(
1766
+ (r) => r.propertyName === decl.relationProperty
1767
+ );
1768
+ if (!relation) {
1769
+ throw new Error(
1770
+ `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.`
1771
+ );
1772
+ }
1773
+ const boundFields = Object.keys(relation.keyBinding);
1774
+ for (const targetField of boundFields) {
1775
+ if (!edgeFields.has(targetField)) {
1776
+ throw new Error(
1777
+ `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.`
1778
+ );
1779
+ }
1780
+ }
1781
+ try {
1782
+ resolveKey(boundFields, entity);
1783
+ } catch (cause) {
1784
+ throw new Error(
1785
+ `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})`
1786
+ );
1787
+ }
1788
+ return relation;
1789
+ }
1790
+ function templateRecord(fields) {
1791
+ const out = {};
1792
+ for (const field of [...fields].sort()) {
1793
+ out[field] = `{${field}}`;
1794
+ }
1795
+ return out;
1796
+ }
1797
+ function keyConditionTemplate(segmented, namespace) {
1798
+ const nameOf = namespace ? (f) => `${namespace}.${f}` : (f) => f;
1799
+ const { pk, sk } = evaluateKey(segmented, "param", void 0, nameOf);
1800
+ const out = { PK: pk };
1801
+ if (sk !== void 0) out.SK = sk;
1802
+ return out;
1803
+ }
1804
+ function putItemTemplate(edgeFields) {
1805
+ return templateRecord(edgeFields);
1806
+ }
1807
+ function deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle) {
1808
+ const entity = MetadataRegistry.get(adjacencyClass);
1809
+ if (!entity.primaryKey) {
1810
+ throw new Error(
1811
+ `Cannot derive edge write items for an entity with no primary key (table '${entity.tableName}'): an edge row needs a key to write / delete.`
1812
+ );
1813
+ }
1814
+ const segmented = entity.primaryKey.segmented;
1815
+ const edgeFields = edgeKeyFields(segmented);
1816
+ assertRelationRoundTrips(entity, new Set(edgeFields), decl);
1817
+ const tableName = TableMapping.resolve(entity.tableName);
1818
+ const entityName = adjacencyClass.name;
1819
+ const put = {
1820
+ type: "Put",
1821
+ tableName,
1822
+ entity: entityName,
1823
+ item: putItemTemplate(edgeFields)
1824
+ };
1825
+ const deleteNew = {
1826
+ type: "Delete",
1827
+ tableName,
1828
+ entity: entityName,
1829
+ keyCondition: keyConditionTemplate(segmented)
1830
+ };
1831
+ const deleteOld = {
1832
+ type: "Delete",
1833
+ tableName,
1834
+ entity: entityName,
1835
+ keyCondition: keyConditionTemplate(segmented, OLD_VALUE_NAMESPACE)
1836
+ };
1837
+ switch (lifecycle) {
1838
+ case "onCreate":
1839
+ return [put];
1840
+ case "onDelete":
1841
+ return [deleteNew];
1842
+ case "onUpdateKeyChange":
1843
+ return [deleteOld, put];
1844
+ }
1845
+ }
1846
+ function deriveEdgeWriteItems(adjacencyClass, writes, lifecycle) {
1847
+ const items = [];
1848
+ for (const decl of writes.edges) {
1849
+ items.push(...deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle));
1850
+ }
1851
+ return items;
1852
+ }
1853
+ function getEdgeWrites(modelClass) {
1854
+ for (const name of Object.getOwnPropertyNames(modelClass)) {
1855
+ let value;
1856
+ try {
1857
+ value = modelClass[name];
1858
+ } catch {
1859
+ continue;
1860
+ }
1861
+ if (isEdgeWritesDefinition(value)) return value;
1862
+ }
1863
+ return void 0;
1864
+ }
1865
+ function deriveModelEdgeWriteItems(modelClass, lifecycle) {
1866
+ const writes = getEdgeWrites(modelClass);
1867
+ if (!writes) {
1868
+ throw new Error(
1869
+ `Model '${modelClass.name}' declares no edge write side (no static \`writes = edgeWrites(...)\`). Declare it to derive adjacency items.`
1870
+ );
1871
+ }
1872
+ return deriveEdgeWriteItems(modelClass, writes, lifecycle);
1873
+ }
1874
+
1875
+ // src/runtime/command-runtime.ts
1876
+ function resolveCommandMethod(contract, methodName) {
1877
+ const method = contract.methods[methodName];
1878
+ if (method === void 0) {
1879
+ throw new Error(
1880
+ `Contract runtime: command method '${methodName}' does not exist on this contract. Available methods: ${Object.keys(contract.methods).sort().join(", ") || "(none)"}.`
1881
+ );
1882
+ }
1883
+ return method;
1884
+ }
1885
+ function modelClassOf(op) {
1886
+ return op.entity.modelClass;
1887
+ }
1888
+ function modelStaticOf(op) {
1889
+ return op.entity.modelClass.asModel();
1890
+ }
1891
+ function renderLeaf(leaf, key, params, context) {
1892
+ if (isContractParamRef(leaf)) {
1893
+ if (!(leaf.field in params)) {
1894
+ throw new Error(
1895
+ `Contract runtime: ${context} references params field '${leaf.field}', which was not supplied. Pass it in the method's params argument.`
1896
+ );
1897
+ }
1898
+ return params[leaf.field];
1899
+ }
1900
+ if (isContractKeyFieldRef(leaf)) {
1901
+ if (leaf.field in key) return key[leaf.field];
1902
+ if (leaf.field in params) return params[leaf.field];
1903
+ throw new Error(
1904
+ `Contract runtime: ${context} references key field '${leaf.field}', which was not supplied. Pass it in the method's key or params argument.`
1905
+ );
1906
+ }
1907
+ return leaf;
1908
+ }
1909
+ function renderRecord(structure, key, params, context) {
1910
+ const out = {};
1911
+ if (structure === void 0) return out;
1912
+ for (const [field, leaf] of Object.entries(structure)) {
1913
+ out[field] = renderLeaf(leaf, key, params, `${context} field '${field}'`);
1914
+ }
1915
+ return out;
1916
+ }
1917
+ function renderCondition(condition, key, params, context) {
1918
+ if (condition === void 0) return void 0;
1919
+ if (typeof condition === "object" && condition !== null) {
1920
+ const obj = condition;
1921
+ if (obj.notExists === true) return { notExists: true };
1922
+ if (typeof obj.attributeExists === "string") {
1923
+ return { attributeExists: obj.attributeExists };
1924
+ }
1925
+ if (typeof obj.attributeNotExists === "string") {
1926
+ return { attributeNotExists: obj.attributeNotExists };
1927
+ }
1928
+ }
1929
+ return renderRecord(
1930
+ condition,
1931
+ key,
1932
+ params,
1933
+ `${context} condition`
1934
+ );
1935
+ }
1936
+ function renderWrite(op, key, params, contextLabel) {
1937
+ if (!isContractKeyRef(op.keys) && op.operation !== "put") {
1938
+ throw new Error(
1939
+ `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.`
1940
+ );
1941
+ }
1942
+ const base = {
1943
+ operation: op.operation,
1944
+ modelClass: modelClassOf(op),
1945
+ modelStatic: modelStaticOf(op),
1946
+ key,
1947
+ condition: renderCondition(op.condition, key, params, contextLabel)
1948
+ };
1949
+ if (op.operation === "put") {
1950
+ return { ...base, item: renderRecord(op.item, key, params, `${contextLabel} item`) };
1951
+ }
1952
+ if (op.operation === "update") {
1953
+ return { ...base, changes: renderRecord(op.changes, key, params, `${contextLabel} changes`) };
1954
+ }
1955
+ return base;
1956
+ }
1957
+ async function executeSingleWrite(w) {
1958
+ const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
1959
+ if (w.operation === "put") {
1960
+ await executePut(w.modelClass, w.item ?? {}, options);
1961
+ return;
1962
+ }
1963
+ if (w.operation === "update") {
1964
+ await executeUpdate(w.modelClass, w.key, w.changes ?? {}, options);
1965
+ return;
1966
+ }
1967
+ await executeDelete(w.modelClass, w.key, options);
275
1968
  }
276
- function freezeEffects(effects) {
1969
+ async function readBackProjection(write, returnSelection) {
1970
+ const select = {};
1971
+ for (const field of Object.keys(returnSelection)) {
1972
+ if (returnSelection[field] === true) select[field] = true;
1973
+ }
1974
+ const key = readBackKey(write);
1975
+ return executeQuery(write.modelClass, key, select, { consistentRead: true });
1976
+ }
1977
+ function readBackKey(write) {
1978
+ if (write.operation !== "put") return write.key;
1979
+ const metadata = MetadataRegistry.get(write.modelClass);
1980
+ const item = write.item ?? {};
1981
+ const key = {};
1982
+ const fields = metadata.primaryKey ? metadata.primaryKey.inputFieldNames : Object.keys(item);
1983
+ for (const field of fields) {
1984
+ if (field in item) key[field] = item[field];
1985
+ }
1986
+ return key;
1987
+ }
1988
+ function renderConditionCheck(check, key, params, contextLabel) {
1989
+ const modelClass = check.entity.modelClass;
1990
+ const referencedKey = {};
1991
+ for (const [targetField, inputField] of Object.entries(check.keyBinding)) {
1992
+ if (inputField in params) {
1993
+ referencedKey[targetField] = params[inputField];
1994
+ } else if (inputField in key) {
1995
+ referencedKey[targetField] = key[inputField];
1996
+ } else {
1997
+ throw new Error(
1998
+ `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.`
1999
+ );
2000
+ }
2001
+ }
2002
+ const { TableName, Key } = buildDeleteInput(
2003
+ modelClass,
2004
+ referencedKey
2005
+ );
2006
+ const cond2 = buildConditionExpression({ attributeExists: "PK" });
2007
+ const checkInput = {
2008
+ TableName,
2009
+ Key,
2010
+ ConditionExpression: cond2.expression
2011
+ };
2012
+ if (Object.keys(cond2.names).length > 0) checkInput.ExpressionAttributeNames = cond2.names;
2013
+ if (Object.keys(cond2.values).length > 0) checkInput.ExpressionAttributeValues = cond2.values;
2014
+ return checkInput;
2015
+ }
2016
+ var RUNTIME_TOKEN_RE = /\{([^{}]+)\}/g;
2017
+ function renderTemplate(template, key, params, contextLabel) {
2018
+ const resolveToken = (token) => {
2019
+ if (token in params) return params[token];
2020
+ if (token in key) return key[token];
2021
+ throw new Error(
2022
+ `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 }\`).` : `.`)
2023
+ );
2024
+ };
2025
+ const whole = /^\{([^{}]+)\}$/.exec(template);
2026
+ if (whole !== null) return resolveToken(whole[1]);
2027
+ return template.replace(RUNTIME_TOKEN_RE, (_m, token) => String(resolveToken(token)));
2028
+ }
2029
+ function renderTemplateRecord(record, key, params, contextLabel) {
277
2030
  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;
2031
+ for (const [field, template] of Object.entries(record ?? {})) {
2032
+ out[field] = renderTemplate(template, key, params, contextLabel);
2033
+ }
284
2034
  return out;
285
2035
  }
286
- var recorder = {
287
- lifecycle(effects = {}) {
288
- return {
289
- [LIFECYCLE_CONTRACT_MARKER]: true,
290
- effects: freezeEffects(effects)
2036
+ function renderEdgeWriteItems(edge, key, params, contextLabel, modelBySignature) {
2037
+ const adjacency = edge.entity.modelClass;
2038
+ return edge.items.map((item) => {
2039
+ if (item.type === "Put") {
2040
+ const rendered = renderTemplateRecord(item.item, key, params, `${contextLabel} edge Put`);
2041
+ const out2 = { Put: buildPutInput(adjacency, rendered) };
2042
+ modelBySignature?.set(execItemKeySignature(out2), adjacency.name);
2043
+ return out2;
2044
+ }
2045
+ const renderedKey = renderTemplateRecord(
2046
+ item.keyCondition,
2047
+ key,
2048
+ params,
2049
+ `${contextLabel} edge Delete`
2050
+ );
2051
+ const del = {
2052
+ TableName: TableMapping.resolve(item.tableName),
2053
+ Key: renderedKey
291
2054
  };
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 };
2055
+ const out = { Delete: del };
2056
+ modelBySignature?.set(execItemKeySignature(out), adjacency.name);
2057
+ return out;
2058
+ });
2059
+ }
2060
+ function renderDerivedUpdate(update, key, params, contextLabel, modelBySignature) {
2061
+ const metadata = MetadataRegistry.get(update.entity.modelClass);
2062
+ if (!metadata.primaryKey) {
2063
+ throw new Error(
2064
+ `Contract runtime: ${contextLabel} derives a counter update on '${update.entity.name}', which has no primary key.`
2065
+ );
313
2066
  }
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)) {
2067
+ const keyInput = {};
2068
+ for (const [targetField, inputField] of Object.entries(update.keyBinding)) {
2069
+ keyInput[targetField] = renderTemplate(
2070
+ `{${inputField}}`,
2071
+ key,
2072
+ params,
2073
+ `${contextLabel} derived-update key`
2074
+ );
2075
+ }
2076
+ const { TableName, Key } = buildDeleteInput(
2077
+ update.entity.modelClass,
2078
+ keyInput
2079
+ );
2080
+ const updateInput = {
2081
+ TableName,
2082
+ Key,
2083
+ UpdateExpression: "ADD #a0 :a0",
2084
+ ExpressionAttributeNames: { "#a0": update.attribute },
2085
+ ExpressionAttributeValues: { ":a0": update.amount }
2086
+ };
2087
+ const out = { Update: updateInput };
2088
+ modelBySignature?.set(execItemKeySignature(out), update.entity.modelClass.name);
2089
+ return out;
2090
+ }
2091
+ function renderUniqueGuardItems(guard, key, params, contextLabel) {
2092
+ return guard.items.map((item) => {
2093
+ if (item.type === "Put") {
2094
+ const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} guard Put`);
2095
+ const put = { TableName: TableMapping.resolve(item.tableName), Item };
2096
+ const cond2 = buildConditionExpression({ attributeNotExists: "PK" });
2097
+ put.ConditionExpression = cond2.expression;
2098
+ if (Object.keys(cond2.names).length > 0) put.ExpressionAttributeNames = cond2.names;
2099
+ if (Object.keys(cond2.values).length > 0) put.ExpressionAttributeValues = cond2.values;
2100
+ return { Put: put };
2101
+ }
2102
+ const Key = renderTemplateRecord(item.keyCondition, key, params, `${contextLabel} guard Delete`);
2103
+ const del = { TableName: TableMapping.resolve(item.tableName), Key };
2104
+ return { Delete: del };
2105
+ });
2106
+ }
2107
+ function renderOutboxEventItems(event, key, params, contextLabel) {
2108
+ return event.items.map((item) => {
2109
+ const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} outbox Put`);
2110
+ const put = { TableName: TableMapping.resolve(item.tableName), Item };
2111
+ return { Put: put };
2112
+ });
2113
+ }
2114
+ function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
2115
+ return guard.items.map((item) => {
2116
+ const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} idempotency Put`);
2117
+ const put = { TableName: TableMapping.resolve(item.tableName), Item };
2118
+ const cond2 = buildConditionExpression({ attributeNotExists: "PK" });
2119
+ put.ConditionExpression = cond2.expression;
2120
+ if (Object.keys(cond2.names).length > 0) put.ExpressionAttributeNames = cond2.names;
2121
+ if (Object.keys(cond2.values).length > 0) put.ExpressionAttributeValues = cond2.values;
2122
+ return { Put: put };
2123
+ });
2124
+ }
2125
+ async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map()) {
2126
+ const writeItems = writes.map((w) => {
2127
+ const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
2128
+ let item;
2129
+ if (w.operation === "put") {
2130
+ item = { Put: buildPutInput(w.modelClass, w.item ?? {}, options) };
2131
+ } else if (w.operation === "update") {
2132
+ item = { Update: buildUpdateInput(w.modelClass, w.key, w.changes ?? {}, options) };
2133
+ } else {
2134
+ item = { Delete: buildDeleteInput(w.modelClass, w.key, options) };
2135
+ }
2136
+ modelBySignature.set(execItemKeySignature(item), w.modelClass.name);
2137
+ return item;
2138
+ });
2139
+ const checkItems = checks.map((c) => ({ ConditionCheck: c.check }));
2140
+ await commitTransaction(
2141
+ [
2142
+ ...writeItems,
2143
+ ...edgeItems,
2144
+ ...updateItems,
2145
+ ...guardItemsRaw,
2146
+ ...outboxItems,
2147
+ ...idempotencyItems,
2148
+ ...checkItems
2149
+ ],
2150
+ {
2151
+ modelBySignature,
2152
+ limitError: (count) => new Error(
2153
+ `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.`
2154
+ )
2155
+ }
2156
+ );
2157
+ }
2158
+ async function executeTransactWrites(writes, methodName) {
2159
+ if (writes.length === 0) return;
2160
+ const modelBySignature = /* @__PURE__ */ new Map();
2161
+ const transactItems = writes.map((w) => {
2162
+ const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
2163
+ let item;
2164
+ if (w.operation === "put") {
2165
+ item = { Put: buildPutInput(w.modelClass, w.item ?? {}, options) };
2166
+ } else if (w.operation === "update") {
2167
+ item = { Update: buildUpdateInput(w.modelClass, w.key, w.changes ?? {}, options) };
2168
+ } else {
2169
+ item = { Delete: buildDeleteInput(w.modelClass, w.key, options) };
2170
+ }
2171
+ modelBySignature.set(execItemKeySignature(item), w.modelClass.name);
2172
+ return item;
2173
+ });
2174
+ await commitTransaction(transactItems, {
2175
+ modelBySignature,
2176
+ limitError: (count) => new Error(
2177
+ `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.`
2178
+ )
2179
+ });
2180
+ }
2181
+ async function executeParallelWrites(writes) {
2182
+ if (writes.length === 0) return [];
2183
+ const coalescible = writes.every(
2184
+ (w) => w.condition === void 0 && (w.operation === "put" || w.operation === "delete")
2185
+ );
2186
+ if (coalescible) {
2187
+ const requests = writes.map(
2188
+ (w) => w.operation === "put" ? { type: "put", model: w.modelStatic, item: w.item ?? {} } : { type: "delete", model: w.modelStatic, key: w.key }
2189
+ );
2190
+ await executeBatchWrite(requests);
2191
+ return writes.map(() => ({ ok: true }));
2192
+ }
2193
+ return Promise.all(
2194
+ writes.map(async (w) => {
2195
+ try {
2196
+ await executeSingleWrite(w);
2197
+ return { ok: true };
2198
+ } catch (e) {
2199
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
2200
+ }
2201
+ })
2202
+ );
2203
+ }
2204
+ async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}) {
2205
+ const method = resolveCommandMethod(contract, methodName);
2206
+ if (Array.isArray(keyOrKeys)) {
2207
+ const keys = keyOrKeys;
2208
+ if (method.ops !== void 0 && method.ops.length > 1) {
320
2209
  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)}.`
2210
+ `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.`
2211
+ );
2212
+ }
2213
+ 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;
2214
+ if (hasDerivedEffects2) {
2215
+ throw new Error(
2216
+ `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.`
322
2217
  );
323
2218
  }
2219
+ const writes = keys.map(
2220
+ (key2, i) => renderWrite(method.op, key2, params, `command method '${methodName}' (key #${i})`)
2221
+ );
2222
+ if (method.mode === "parallel") {
2223
+ return { results: await executeParallelWrites(writes) };
2224
+ }
2225
+ await executeTransactWrites(writes, methodName);
2226
+ return;
324
2227
  }
325
- if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
2228
+ const key = keyOrKeys;
2229
+ const conditionChecks = method.conditionChecks ?? [];
2230
+ const edgeWrites2 = method.edgeWrites ?? [];
2231
+ const derivedUpdates = method.derivedUpdates ?? [];
2232
+ const uniqueGuards = method.uniqueGuards ?? [];
2233
+ const outboxEvents = method.outboxEvents ?? [];
2234
+ const idempotencyGuard = method.idempotencyGuard;
2235
+ const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0;
2236
+ const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
2237
+ if (isMultiFragment || hasDerivedEffects) {
2238
+ const ops = method.ops ?? [method.op];
2239
+ const writes = ops.map(
2240
+ (opn, i) => renderWrite(opn, key, params, `command method '${methodName}' (fragment #${i})`)
2241
+ );
2242
+ if (hasDerivedEffects) {
2243
+ const modelBySignature = /* @__PURE__ */ new Map();
2244
+ const edgeItems = edgeWrites2.flatMap(
2245
+ (edge, i) => renderEdgeWriteItems(
2246
+ edge,
2247
+ key,
2248
+ params,
2249
+ `command method '${methodName}' (edge #${i})`,
2250
+ modelBySignature
2251
+ )
2252
+ );
2253
+ const updateItems = derivedUpdates.map(
2254
+ (update, i) => renderDerivedUpdate(
2255
+ update,
2256
+ key,
2257
+ params,
2258
+ `command method '${methodName}' (derive #${i})`,
2259
+ modelBySignature
2260
+ )
2261
+ );
2262
+ const guardItems = uniqueGuards.flatMap(
2263
+ (guard, i) => renderUniqueGuardItems(guard, key, params, `command method '${methodName}' (unique #${i})`)
2264
+ );
2265
+ const outboxItems = outboxEvents.flatMap(
2266
+ (event, i) => renderOutboxEventItems(event, key, params, `command method '${methodName}' (emit #${i})`)
2267
+ );
2268
+ const idempotencyItems = idempotencyGuard !== void 0 ? renderIdempotencyGuardItems(
2269
+ idempotencyGuard,
2270
+ key,
2271
+ params,
2272
+ `command method '${methodName}' (idempotency)`
2273
+ ) : [];
2274
+ const checks = conditionChecks.map((check, i) => ({
2275
+ check: renderConditionCheck(
2276
+ check,
2277
+ key,
2278
+ params,
2279
+ `command method '${methodName}' (requires #${i})`
2280
+ )
2281
+ }));
2282
+ await executeReferentialTransaction(
2283
+ writes,
2284
+ edgeItems,
2285
+ updateItems,
2286
+ guardItems,
2287
+ outboxItems,
2288
+ idempotencyItems,
2289
+ checks,
2290
+ methodName,
2291
+ modelBySignature
2292
+ );
2293
+ } else {
2294
+ await executeTransactWrites(writes, methodName);
2295
+ }
2296
+ if (method.returnSelection !== void 0 && Object.keys(method.returnSelection).length > 0) {
2297
+ return readBackProjection(writes[0], method.returnSelection);
2298
+ }
2299
+ return;
2300
+ }
2301
+ const write = renderWrite(method.op, key, params, `command method '${methodName}'`);
2302
+ await executeSingleWrite(write);
2303
+ if (method.returnSelection !== void 0 && Object.keys(method.returnSelection).length > 0) {
2304
+ return readBackProjection(write, method.returnSelection);
2305
+ }
2306
+ }
2307
+ function renderCompiledOp(op, ctx) {
2308
+ const fragment = op.fragment;
2309
+ const baseOp = op.condition !== void 0 ? withCondition(fragment.op, op.condition) : fragment.op;
2310
+ const modelBySignature = /* @__PURE__ */ new Map();
2311
+ const write = renderWrite(baseOp, op.params, op.params, ctx);
2312
+ const edgeItems = [];
2313
+ const updateItems = [];
2314
+ const guardItems = [];
2315
+ const outboxItems = [];
2316
+ const idempotencyItems = [];
2317
+ const checkItems = [];
2318
+ for (const edge of fragment.edgeWrites ?? []) {
2319
+ edgeItems.push(...renderEdgeWriteItems(edge, op.params, op.params, `${ctx} edge`, modelBySignature));
2320
+ }
2321
+ for (const u of fragment.derivedUpdates ?? []) {
2322
+ updateItems.push(renderDerivedUpdate(u, op.params, op.params, `${ctx} derive`, modelBySignature));
2323
+ }
2324
+ for (const g of fragment.uniqueGuards ?? []) {
2325
+ guardItems.push(...renderUniqueGuardItems(g, op.params, op.params, `${ctx} unique`));
2326
+ }
2327
+ for (const e of fragment.outboxEvents ?? []) {
2328
+ outboxItems.push(...renderOutboxEventItems(e, op.params, op.params, `${ctx} emit`));
2329
+ }
2330
+ if (fragment.idempotencyGuard !== void 0) {
2331
+ idempotencyItems.push(
2332
+ ...renderIdempotencyGuardItems(fragment.idempotencyGuard, op.params, op.params, `${ctx} idempotency`)
2333
+ );
2334
+ }
2335
+ for (const c of fragment.conditionChecks ?? []) {
2336
+ checkItems.push({ check: renderConditionCheck(c, op.params, op.params, `${ctx} requires`) });
2337
+ }
2338
+ const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || checkItems.length > 0;
2339
+ return {
2340
+ write,
2341
+ edgeItems,
2342
+ updateItems,
2343
+ guardItems,
2344
+ outboxItems,
2345
+ idempotencyItems,
2346
+ checkItems,
2347
+ modelBySignature,
2348
+ hasDerivedEffects
2349
+ };
2350
+ }
2351
+ function readBackForCompiledOp(op, write) {
2352
+ const sel = op.result?.select;
2353
+ if (sel === void 0 || Object.keys(sel).length === 0) return void 0;
2354
+ const key = {};
2355
+ for (const f of op.fragment.keyFields) {
2356
+ if (f in op.params) key[f] = op.params[f];
2357
+ }
2358
+ return executeQuery(write.modelClass, key, sel, {
2359
+ consistentRead: op.result?.options?.consistentRead ?? true,
2360
+ maxDepth: op.result?.options?.maxDepth
2361
+ });
2362
+ }
2363
+ async function executeCompiledWriteOp(ops, options) {
2364
+ const label = options.label;
2365
+ const renderedOps = ops.map((op, i) => renderCompiledOp(op, `${label} (op #${i})`));
2366
+ const rendered = renderedOps.map((r) => r.write);
2367
+ const modelBySignature = /* @__PURE__ */ new Map();
2368
+ const edgeItems = [];
2369
+ const updateItems = [];
2370
+ const guardItems = [];
2371
+ const outboxItems = [];
2372
+ const idempotencyItems = [];
2373
+ const checkItems = [];
2374
+ for (const r of renderedOps) {
2375
+ for (const [sig, name] of r.modelBySignature) modelBySignature.set(sig, name);
2376
+ edgeItems.push(...r.edgeItems);
2377
+ updateItems.push(...r.updateItems);
2378
+ guardItems.push(...r.guardItems);
2379
+ outboxItems.push(...r.outboxItems);
2380
+ idempotencyItems.push(...r.idempotencyItems);
2381
+ checkItems.push(...r.checkItems);
2382
+ }
2383
+ const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
2384
+ if (rendered.length === 1 && !hasDerivedEffects) {
2385
+ await executeSingleWrite(rendered[0]);
2386
+ } else {
2387
+ await executeReferentialTransaction(
2388
+ rendered,
2389
+ edgeItems,
2390
+ updateItems,
2391
+ guardItems,
2392
+ outboxItems,
2393
+ idempotencyItems,
2394
+ checkItems,
2395
+ label,
2396
+ modelBySignature
2397
+ );
2398
+ }
2399
+ const readBacks = await Promise.all(
2400
+ ops.map((op, i) => readBackForCompiledOp(op, rendered[i]))
2401
+ );
2402
+ return { readBacks };
2403
+ }
2404
+ async function executeCompiledParallelWrites(ops, options) {
2405
+ const label = options.label;
2406
+ const results = new Array(ops.length);
2407
+ const renderedOps = ops.map((op, i) => renderCompiledOp(op, `${label} (op #${i})`));
2408
+ const coalescibleIdx = [];
2409
+ const individualIdx = [];
2410
+ renderedOps.forEach((r, i) => {
2411
+ const w = r.write;
2412
+ if (!r.hasDerivedEffects && w.condition === void 0 && (w.operation === "put" || w.operation === "delete")) {
2413
+ coalescibleIdx.push(i);
2414
+ } else {
2415
+ individualIdx.push(i);
2416
+ }
2417
+ });
2418
+ await Promise.all([
2419
+ // The coalescible group → ONE BatchWriteItem (chunked, UnprocessedItems retry) via
2420
+ // the shared public-layer coalescing. On success every op reports `{ ok: true }`
2421
+ // plus its read-back; a BatchWriteItem request failure fails the whole group.
2422
+ (async () => {
2423
+ if (coalescibleIdx.length === 0) return;
2424
+ const writes = coalescibleIdx.map((i) => renderedOps[i].write);
2425
+ try {
2426
+ await executeParallelWrites(writes);
2427
+ await Promise.all(
2428
+ coalescibleIdx.map(async (i) => {
2429
+ const value = await readBackForCompiledOp(ops[i], renderedOps[i].write);
2430
+ results[i] = { ok: true, value };
2431
+ })
2432
+ );
2433
+ } catch (e) {
2434
+ const error = e instanceof Error ? e : new Error(String(e));
2435
+ for (const i of coalescibleIdx) results[i] = { ok: false, error };
2436
+ }
2437
+ })(),
2438
+ // Each individual op → its own single-op atomic execution (condition gate / derived
2439
+ // effects / read-back preserved), concurrently and with per-op partial success.
2440
+ ...individualIdx.map(async (i) => {
2441
+ try {
2442
+ const { readBacks } = await executeCompiledWriteOp([ops[i]], {
2443
+ label: `${label} (op #${i})`
2444
+ });
2445
+ results[i] = { ok: true, value: readBacks[0] };
2446
+ } catch (e) {
2447
+ results[i] = { ok: false, error: e instanceof Error ? e : new Error(String(e)) };
2448
+ }
2449
+ })
2450
+ ]);
2451
+ return results;
2452
+ }
2453
+ function withCondition(op, condition) {
2454
+ if (op.condition === void 0) return { ...op, condition };
2455
+ const base = op.condition;
2456
+ if (base.notExists === true && Object.keys(condition).length > 0) {
326
2457
  throw new Error(
327
- "entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`); an empty save contract declares nothing."
2458
+ `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.`
2459
+ );
2460
+ }
2461
+ return { ...op, condition };
2462
+ }
2463
+
2464
+ // src/runtime/envelope.ts
2465
+ async function executeReadEnvelope(envelope) {
2466
+ const aliases = Object.keys(envelope);
2467
+ const results = await Promise.all(
2468
+ aliases.map((alias) => executeReadRoute(alias, envelope[alias]))
2469
+ );
2470
+ const out = {};
2471
+ aliases.forEach((alias, i) => {
2472
+ out[alias] = results[i];
2473
+ });
2474
+ return out;
2475
+ }
2476
+ async function executeReadRoute(alias, route) {
2477
+ if (route === null || typeof route !== "object") {
2478
+ throw new Error(
2479
+ `DDBModel.query: the descriptor for alias '${alias}' is not a read descriptor (\`{ query | list: Model, key, select, options? }\`).`
2480
+ );
2481
+ }
2482
+ const isQuery = route.query !== void 0;
2483
+ const isList = route.list !== void 0;
2484
+ if (isQuery === isList) {
2485
+ throw new Error(
2486
+ `DDBModel.query: the descriptor for alias '${alias}' must carry exactly one of \`query\` or \`list\` (the target model). It declared ${isQuery && isList ? "both" : "neither"}.`
2487
+ );
2488
+ }
2489
+ if (route.key === void 0 || route.select === void 0) {
2490
+ throw new Error(
2491
+ `DDBModel.query: the '${isQuery ? "query" : "list"}' descriptor for alias '${alias}' must declare both \`key\` and \`select\`.`
2492
+ );
2493
+ }
2494
+ const modelClass = resolveModelClass(
2495
+ resolveModelStatic(isQuery ? route.query : route.list, alias)
2496
+ );
2497
+ const opt = route.options ?? {};
2498
+ if (isQuery) {
2499
+ return executeQuery(modelClass, route.key, route.select, {
2500
+ consistentRead: opt.consistentRead,
2501
+ maxDepth: opt.maxDepth
2502
+ });
2503
+ }
2504
+ return executeList(modelClass, route.key, route.select, {
2505
+ limit: opt.limit,
2506
+ after: opt.after,
2507
+ order: opt.order,
2508
+ filter: opt.filter
2509
+ });
2510
+ }
2511
+ var INTENT_KEYS = ["create", "update", "remove"];
2512
+ async function executeWriteEnvelope(envelope, options = {}) {
2513
+ const mode = options.mode ?? "transaction";
2514
+ const aliases = Object.keys(envelope);
2515
+ if (aliases.length === 0) {
2516
+ throw new Error(
2517
+ "DDBModel.mutate: the write envelope is empty. Declare at least one `{ alias: { create | update | remove: Model, key, input? } }` operation."
2518
+ );
2519
+ }
2520
+ const ops = aliases.flatMap((alias) => normalizeWriteOp(alias, envelope[alias]));
2521
+ if (mode === "transaction") {
2522
+ return executeTransactionMode(ops);
2523
+ }
2524
+ return executeParallelMode(ops);
2525
+ }
2526
+ function normalizeWriteOp(alias, d) {
2527
+ if (d === null || typeof d !== "object") {
2528
+ throw new Error(
2529
+ `DDBModel.mutate: the descriptor for alias '${alias}' is not a write descriptor (\`{ create | update | remove: Model, key, input?, condition?, result? }\`).`
2530
+ );
2531
+ }
2532
+ const present = INTENT_KEYS.filter((k) => d[k] !== void 0);
2533
+ if (present.length !== 1) {
2534
+ throw new Error(
2535
+ `DDBModel.mutate: the descriptor for alias '${alias}' must carry exactly one intent key (\`create\` / \`update\` / \`remove\`), but declared ${present.length === 0 ? "none" : present.join(", ")}.`
2536
+ );
2537
+ }
2538
+ const intent = present[0];
2539
+ if (d.key === void 0 || d.key === null || typeof d.key !== "object") {
2540
+ throw new Error(
2541
+ `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.`
328
2542
  );
329
2543
  }
2544
+ const model = resolveModelStatic(d[intent], alias);
2545
+ const input = d.input ?? {};
2546
+ const base = { alias, intent, model, input, condition: d.condition, result: d.result };
2547
+ if (Array.isArray(d.key)) {
2548
+ const keys = d.key;
2549
+ if (keys.length === 0) {
2550
+ throw new Error(
2551
+ `DDBModel.mutate: the '${intent}' descriptor for alias '${alias}' declared an EMPTY key array. Pass at least one key, or omit the operation.`
2552
+ );
2553
+ }
2554
+ return keys.map((key, bulkIndex) => {
2555
+ if (key === null || typeof key !== "object") {
2556
+ throw new Error(
2557
+ `DDBModel.mutate: the '${intent}' descriptor for alias '${alias}' key #${bulkIndex} is not a key object.`
2558
+ );
2559
+ }
2560
+ return { ...base, key, bulkIndex };
2561
+ });
2562
+ }
2563
+ return [{ ...base, key: d.key }];
2564
+ }
2565
+ function compileWriteOp(op) {
2566
+ const keyFieldNames2 = Object.keys(op.key);
2567
+ const inputFieldNames = Object.keys(op.input);
2568
+ const model = op.model;
2569
+ const body = ($) => {
2570
+ const descriptor = {
2571
+ [op.intent]: (() => model),
2572
+ key: mapToRefs($, keyFieldNames2),
2573
+ ...inputFieldNames.length > 0 ? { input: mapToRefs($, inputFieldNames) } : {}
2574
+ };
2575
+ const map = { [op.alias]: descriptor };
2576
+ return map;
2577
+ };
2578
+ const plan2 = mutation(body);
2579
+ if (!isCommandPlan(plan2)) {
2580
+ throw new Error("DDBModel.mutate: internal error building the mutation plan.");
2581
+ }
2582
+ const compiled = compileMutationPlan(plan2);
2583
+ const fragment = compiled.fragments[0];
2584
+ const params = { ...op.input, ...op.key };
330
2585
  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 } : {}
2586
+ fragment,
2587
+ params,
2588
+ condition: op.condition,
2589
+ result: op.result
335
2590
  };
336
2591
  }
337
- function getEntityWrites(modelClass) {
338
- for (const name of Object.getOwnPropertyNames(modelClass)) {
339
- let value;
340
- try {
341
- value = modelClass[name];
342
- } catch {
343
- continue;
2592
+ function mapToRefs($, fields) {
2593
+ const out = {};
2594
+ for (const f of fields) out[f] = $[f];
2595
+ return out;
2596
+ }
2597
+ async function executeTransactionMode(ops) {
2598
+ const compiled = ops.map(compileWriteOp);
2599
+ const { readBacks } = await executeCompiledWriteOp(compiled, {
2600
+ label: "DDBModel.mutate"
2601
+ });
2602
+ const out = {};
2603
+ ops.forEach((op, i) => {
2604
+ if (op.result === void 0) return;
2605
+ if (op.bulkIndex === void 0) {
2606
+ out[op.alias] = readBacks[i];
2607
+ } else {
2608
+ const arr = out[op.alias] ?? [];
2609
+ arr[op.bulkIndex] = readBacks[i];
2610
+ out[op.alias] = arr;
344
2611
  }
345
- if (isEntityWritesDefinition(value)) return value;
2612
+ });
2613
+ return out;
2614
+ }
2615
+ async function executeParallelMode(ops) {
2616
+ const compiled = ops.map(compileWriteOp);
2617
+ const settled = await executeCompiledParallelWrites(compiled, {
2618
+ label: "DDBModel.mutate"
2619
+ });
2620
+ const out = {};
2621
+ ops.forEach((op, i) => {
2622
+ const res = settled[i];
2623
+ const mapped = res.ok ? { ok: true, value: op.result !== void 0 ? res.value : void 0 } : { ok: false, error: res.error };
2624
+ if (op.bulkIndex === void 0) {
2625
+ out[op.alias] = mapped;
2626
+ } else {
2627
+ const arr = out[op.alias] ?? [];
2628
+ arr[op.bulkIndex] = mapped;
2629
+ out[op.alias] = arr;
2630
+ }
2631
+ });
2632
+ return out;
2633
+ }
2634
+ function resolveModelStatic(ref, alias) {
2635
+ if (ref === null || ref === void 0) {
2636
+ throw new Error(`DDBModel: the descriptor for alias '${alias}' names no target model.`);
2637
+ }
2638
+ if (typeof ref === "function") {
2639
+ const asModel = ref.asModel;
2640
+ if (typeof asModel === "function") {
2641
+ return asModel.call(ref);
2642
+ }
2643
+ return resolveModelStatic(ref(), alias);
2644
+ }
2645
+ return ref;
2646
+ }
2647
+
2648
+ // src/model/DDBModel.ts
2649
+ var DDBModel = class {
2650
+ static setClient(client) {
2651
+ ClientManager.setClient(client);
2652
+ }
2653
+ static setTableMapping(mapping) {
2654
+ TableMapping.set(mapping);
2655
+ }
2656
+ static async transaction(fn) {
2657
+ await executeTransaction(fn);
2658
+ }
2659
+ /**
2660
+ * In-process multi-route READ (issue #101 — the unified envelope). Each alias
2661
+ * route (`{ query | list: Model, key, select, options? }`) runs **independently
2662
+ * and in parallel**; the result is an object keyed by the input aliases. A
2663
+ * `query` route delegates to the single-item read path, a `list` route to the
2664
+ * partition read path. Cross-route consistency is NOT guaranteed.
2665
+ */
2666
+ static async query(envelope) {
2667
+ return executeReadEnvelope(envelope);
2668
+ }
2669
+ static mutate(envelope, options) {
2670
+ return executeWriteEnvelope(envelope, options);
2671
+ }
2672
+ static async batchGet(requests) {
2673
+ return executeBatchGet(requests);
2674
+ }
2675
+ static async batchWrite(requests) {
2676
+ return executeBatchWrite(requests);
2677
+ }
2678
+ static asModel() {
2679
+ const modelClass = this;
2680
+ const modelStatic = {
2681
+ col: createColumnMap(),
2682
+ project(select) {
2683
+ return buildProject(select);
2684
+ },
2685
+ relation(select) {
2686
+ return buildRelation(select);
2687
+ },
2688
+ // The public `query` is overloaded (plain / updatable / hydrate, the last
2689
+ // introducing a fresh return-type parameter `R`, issue #53). An overloaded
2690
+ // method on an object literal cannot contextually infer its single
2691
+ // implementation signature, so the implementation is written with explicit
2692
+ // runtime-facing parameter types and assigned through the interface's
2693
+ // method type — the public overloads (the only caller-visible surface)
2694
+ // are untouched.
2695
+ query: ((key, select, options) => {
2696
+ return executeQuery(modelClass, key, select, options);
2697
+ }),
2698
+ list: ((key, select, options) => {
2699
+ return executeList(modelClass, key, select, options);
2700
+ }),
2701
+ explain(key, options) {
2702
+ return executeExplain(
2703
+ modelClass,
2704
+ key,
2705
+ options
2706
+ );
2707
+ },
2708
+ async putItem(item, options) {
2709
+ await executePut(
2710
+ modelClass,
2711
+ item,
2712
+ options
2713
+ );
2714
+ },
2715
+ async updateItem(entity, changes, options) {
2716
+ await executeUpdate(
2717
+ modelClass,
2718
+ entity,
2719
+ changes,
2720
+ options
2721
+ );
2722
+ },
2723
+ async deleteItem(key, options) {
2724
+ await executeDelete(modelClass, key, options);
2725
+ }
2726
+ };
2727
+ return attachModelClass(
2728
+ modelStatic,
2729
+ modelClass
2730
+ );
346
2731
  }
347
- return void 0;
348
- }
349
- function lifecyclePhaseForIntent(intent) {
350
- return intent;
351
- }
2732
+ };
352
2733
 
353
2734
  // src/define/mutation.ts
354
2735
  var INPUT_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationInputRef");
@@ -376,19 +2757,32 @@ function parseEntityRef(leaf) {
376
2757
  path: leaf
377
2758
  };
378
2759
  }
379
- function makeInputFieldRef(field) {
380
- const token = `{${field}}`;
381
- const origin = `the input field '${field}' (\`$.${field}\`)`;
2760
+ var ALIAS_FIELD_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationAliasFieldRef");
2761
+ function isAliasFieldRef(value) {
2762
+ return typeof value === "object" && value !== null && value[ALIAS_FIELD_REF_BRAND] === true;
2763
+ }
2764
+ function makeRootRef(root) {
2765
+ const token = `{${root}}`;
2766
+ const origin = `the placeholder '${root}' (\`$.${root}\`)`;
2767
+ const aliasFieldCache = /* @__PURE__ */ new Map();
382
2768
  const target = /* @__PURE__ */ Object.create(null);
383
2769
  const proxy = new Proxy(target, {
384
2770
  get(_t, prop) {
385
2771
  if (prop === INPUT_REF_BRAND) return true;
386
- if (prop === "field") return field;
2772
+ if (prop === "field") return root;
387
2773
  if (prop === "token") return token;
388
2774
  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
- );
2775
+ if (prop === "toString" || prop === "valueOf" || prop === "constructor") {
2776
+ throw new Error(
2777
+ `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.`
2778
+ );
2779
+ }
2780
+ let ref = aliasFieldCache.get(prop);
2781
+ if (!ref) {
2782
+ ref = { [ALIAS_FIELD_REF_BRAND]: true, alias: root, field: prop };
2783
+ aliasFieldCache.set(prop, ref);
2784
+ }
2785
+ return ref;
392
2786
  },
393
2787
  set() {
394
2788
  throw new Error(`mutation: cannot assign on ${origin}.`);
@@ -401,16 +2795,16 @@ function makeInputProxy() {
401
2795
  return new Proxy(/* @__PURE__ */ Object.create(null), {
402
2796
  get(_t, prop) {
403
2797
  if (typeof prop === "symbol") return void 0;
404
- const field = prop;
405
- let ref = cache.get(field);
2798
+ const root = prop;
2799
+ let ref = cache.get(root);
406
2800
  if (!ref) {
407
- ref = makeInputFieldRef(field);
408
- cache.set(field, ref);
2801
+ ref = makeRootRef(root);
2802
+ cache.set(root, ref);
409
2803
  }
410
2804
  return ref;
411
2805
  },
412
2806
  set() {
413
- throw new Error("mutation: cannot assign to the `$` input proxy.");
2807
+ throw new Error("mutation: cannot assign to the `$` placeholder proxy.");
414
2808
  }
415
2809
  });
416
2810
  }
@@ -418,10 +2812,40 @@ var FRAGMENT_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationFragment");
418
2812
  function isMutationFragment(value) {
419
2813
  return typeof value === "object" && value !== null && value[FRAGMENT_BRAND] === true;
420
2814
  }
421
- function targetEntityRef(target) {
422
- const resolved = target();
2815
+ var INTENT_KEYS2 = ["create", "update", "remove"];
2816
+ function resolveDescriptorTarget(descriptor, alias) {
2817
+ const present = INTENT_KEYS2.filter((k) => descriptor[k] !== void 0);
2818
+ if (present.length === 0) {
2819
+ throw new Error(
2820
+ `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 }\`.`
2821
+ );
2822
+ }
2823
+ if (present.length > 1) {
2824
+ throw new Error(
2825
+ `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\`.`
2826
+ );
2827
+ }
2828
+ const intent = present[0];
2829
+ const ref = descriptor[intent];
2830
+ const resolved = resolveModelRef(ref, intent, alias);
423
2831
  const modelClass = resolveModelClass(resolved);
424
- return { name: modelClass.name, modelClass };
2832
+ return { intent, entity: { name: modelClass.name, modelClass } };
2833
+ }
2834
+ function resolveModelRef(ref, intent, alias) {
2835
+ if (ref === null || ref === void 0) {
2836
+ throw new Error(
2837
+ `mutation: the '${intent}' descriptor for alias '${alias}' names no target model.`
2838
+ );
2839
+ }
2840
+ if (typeof ref === "function") {
2841
+ const proto = ref.prototype;
2842
+ const isModelClass = proto instanceof DDBModel || typeof ref === "function" && ref.prototype instanceof DDBModel;
2843
+ if (isModelClass) {
2844
+ return ref;
2845
+ }
2846
+ return ref();
2847
+ }
2848
+ return ref;
425
2849
  }
426
2850
  function assertFaithfulInput(input, intent, entityName) {
427
2851
  if (input === null || typeof input !== "object") {
@@ -430,9 +2854,9 @@ function assertFaithfulInput(input, intent, entityName) {
430
2854
  );
431
2855
  }
432
2856
  for (const [field, leaf] of Object.entries(input)) {
433
- if (!isMutationInputRef(leaf) && !isConcreteLiteral(leaf)) {
2857
+ if (!isMutationInputRef(leaf) && !isAliasFieldRef(leaf) && !isConcreteLiteral(leaf)) {
434
2858
  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.`
2859
+ `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.`
436
2860
  );
437
2861
  }
438
2862
  }
@@ -442,10 +2866,9 @@ function isConcreteLiteral(value) {
442
2866
  const t = typeof value;
443
2867
  return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
444
2868
  }
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)) {
2869
+ function makeFragment(intent, entity, input, use) {
2870
+ const checked = assertFaithfulInput(input ?? {}, intent, entity.name);
2871
+ if (use !== void 0 && !isEntityWritesDefinition(use)) {
449
2872
  throw new Error(
450
2873
  `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.`
451
2874
  );
@@ -454,308 +2877,121 @@ function makeFragment(intent, target, options) {
454
2877
  [FRAGMENT_BRAND]: true,
455
2878
  intent,
456
2879
  entity,
457
- input,
458
- ...options.use !== void 0 ? { use: options.use } : {}
2880
+ input: checked,
2881
+ ...use !== void 0 ? { use } : {}
459
2882
  };
460
2883
  }
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
2884
  var COMMAND_PLAN_BRAND = /* @__PURE__ */ Symbol("graphddb:commandPlan");
467
2885
  function isCommandPlan(value) {
468
2886
  return typeof value === "object" && value !== null && value[COMMAND_PLAN_BRAND] === true;
469
2887
  }
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.");
473
- }
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
- );
480
- }
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
- );
485
- }
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 };
494
- }
495
- var definePlan = mutation;
496
-
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;
502
- }
503
- function declaration(target, relationProperty) {
504
- return { targetFactory: target, relationProperty };
505
- }
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 };
519
- }
520
- function edgeKeyFields(segmented) {
521
- return [
522
- ...segmentFieldNames(segmented.pkSegments),
523
- ...segmentFieldNames(segmented.skSegments)
524
- ];
525
- }
526
- function assertRelationRoundTrips(entity, edgeFields, decl) {
527
- const targetClass = decl.targetFactory();
528
- const targetMeta = MetadataRegistry.get(targetClass);
529
- const relation = targetMeta.relations.find(
530
- (r) => r.propertyName === decl.relationProperty
531
- );
532
- if (!relation) {
2888
+ function mergeBindings(descriptor, intent, alias) {
2889
+ const merged = {};
2890
+ const key = descriptor.key;
2891
+ if (key === void 0 || key === null || typeof key !== "object") {
533
2892
  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.`
2893
+ `mutation: the '${intent}' descriptor for alias '${alias}' must declare a \`key\` object binding the target's primary-key fields (\`{ field: $.field, \u2026 }\`).`
535
2894
  );
536
2895
  }
537
- const boundFields = Object.keys(relation.keyBinding);
538
- for (const targetField of boundFields) {
539
- if (!edgeFields.has(targetField)) {
2896
+ for (const [field, leaf] of Object.entries(key)) merged[field] = leaf;
2897
+ const input = descriptor.input;
2898
+ if (input !== void 0 && input !== null) {
2899
+ if (typeof input !== "object") {
540
2900
  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.`
2901
+ `mutation: the '${intent}' descriptor for alias '${alias}' has a non-object \`input\`.`
542
2902
  );
543
2903
  }
544
- }
545
- try {
546
- resolveKey(boundFields, entity);
547
- } catch (cause) {
548
- 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})`
550
- );
551
- }
552
- return relation;
553
- }
554
- function templateRecord(fields) {
555
- const out = {};
556
- for (const field of [...fields].sort()) {
557
- out[field] = `{${field}}`;
558
- }
559
- return out;
560
- }
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;
566
- return out;
567
- }
568
- function putItemTemplate(edgeFields) {
569
- return templateRecord(edgeFields);
570
- }
571
- function deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle) {
572
- const entity = MetadataRegistry.get(adjacencyClass);
573
- if (!entity.primaryKey) {
574
- 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.`
576
- );
577
- }
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)
588
- };
589
- const deleteNew = {
590
- type: "Delete",
591
- tableName,
592
- entity: entityName,
593
- keyCondition: keyConditionTemplate(segmented)
594
- };
595
- const deleteOld = {
596
- type: "Delete",
597
- tableName,
598
- entity: entityName,
599
- keyCondition: keyConditionTemplate(segmented, OLD_VALUE_NAMESPACE)
600
- };
601
- switch (lifecycle) {
602
- case "onCreate":
603
- return [put];
604
- case "onDelete":
605
- return [deleteNew];
606
- case "onUpdateKeyChange":
607
- return [deleteOld, put];
608
- }
609
- }
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;
616
- }
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;
624
- }
625
- if (isEdgeWritesDefinition(value)) return value;
626
- }
627
- return void 0;
628
- }
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.`
634
- );
635
- }
636
- return deriveEdgeWriteItems(modelClass, writes, lifecycle);
637
- }
638
-
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}}` };
650
- }
651
- function wholeKeysSentinel() {
652
- return { [KEY_REF_BRAND]: true };
653
- }
654
- function isContractKeyFieldRef(value) {
655
- return typeof value === "object" && value !== null && value[KEY_FIELD_REF_BRAND] === true;
656
- }
657
- function makeKeyFieldRef(field, marker, onCoerce) {
658
- const token = `{key.${field}}`;
659
- const origin = `keys field '${field}'`;
660
- const target = /* @__PURE__ */ Object.create(null);
661
- const proxy = new Proxy(target, {
662
- get(_t, prop) {
663
- if (prop === KEY_FIELD_REF_BRAND) return true;
664
- if (prop === "field") return field;
665
- 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
- };
677
- }
678
- if (prop === "valueOf") {
679
- return () => {
680
- onCoerce();
681
- return NaN;
682
- };
2904
+ for (const [field, leaf] of Object.entries(input)) {
2905
+ if (field in merged) {
2906
+ throw new Error(
2907
+ `mutation: the '${intent}' descriptor for alias '${alias}' binds field '${field}' in both \`key\` and \`input\`. A primary-key field belongs in \`key\` only.`
2908
+ );
683
2909
  }
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
- );
689
- },
690
- 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}.`);
2910
+ merged[field] = leaf;
700
2911
  }
701
- });
702
- return proxy;
2912
+ }
2913
+ return merged;
703
2914
  }
704
- function makeKeySentinel(pass, onFieldAccess, onCoerce) {
705
- const cache = /* @__PURE__ */ new Map();
706
- const target = /* @__PURE__ */ Object.create(null);
707
- const proxy = new Proxy(target, {
708
- 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") {
2915
+ function resolveAliasRefs(input, aliasToIndex, alias) {
2916
+ const out = {};
2917
+ for (const [field, leaf] of Object.entries(input)) {
2918
+ if (isAliasFieldRef(leaf)) {
2919
+ const index = aliasToIndex.get(leaf.alias);
2920
+ if (index === void 0) {
732
2921
  throw new Error(
733
- "publicQueryModel/publicCommandModel: unsupported symbol access on `keys`."
2922
+ `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.`
734
2923
  );
735
2924
  }
736
- onFieldAccess(prop);
737
- let ref = cache.get(prop);
738
- if (!ref) {
739
- ref = makeKeyFieldRef(prop, paramMarker(pass, `key.${prop}`), onCoerce);
740
- cache.set(prop, ref);
741
- }
742
- return ref;
743
- },
744
- 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() {
2925
+ out[field] = `$.entity[${index}].${leaf.field}`;
2926
+ } else {
2927
+ out[field] = leaf;
2928
+ }
2929
+ }
2930
+ return out;
2931
+ }
2932
+ function mutation(nameOrBody, maybeBody) {
2933
+ let name;
2934
+ let body;
2935
+ if (typeof nameOrBody === "string") {
2936
+ if (maybeBody === void 0) {
2937
+ throw new Error("mutation: a name was given but no body function.");
2938
+ }
2939
+ name = nameOrBody;
2940
+ body = maybeBody;
2941
+ } else {
2942
+ body = nameOrBody;
2943
+ name = "mutation";
2944
+ }
2945
+ if (typeof body !== "function") {
2946
+ throw new Error(
2947
+ "mutation: the body must be a function `$ => ({ alias: descriptor, \u2026 })` returning an alias \u2192 write-descriptor map."
2948
+ );
2949
+ }
2950
+ const $ = makeInputProxy();
2951
+ const map = body($);
2952
+ if (map === null || typeof map !== "object" || Array.isArray(map)) {
2953
+ throw new Error(
2954
+ `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.`
2955
+ );
2956
+ }
2957
+ const aliases = Object.keys(map);
2958
+ if (aliases.length === 0) {
2959
+ throw new Error(
2960
+ `mutation '${name}': the body returned an empty descriptor map. A mutation must declare at least one write descriptor.`
2961
+ );
2962
+ }
2963
+ const aliasToIndex = /* @__PURE__ */ new Map();
2964
+ aliases.forEach((alias, i) => aliasToIndex.set(alias, i));
2965
+ const fragments = aliases.map((alias) => {
2966
+ const descriptor = map[alias];
2967
+ if (descriptor === null || typeof descriptor !== "object") {
753
2968
  throw new Error(
754
- "publicQueryModel/publicCommandModel: cannot delete a field of `keys`."
2969
+ `mutation '${name}': the value for alias '${alias}' is not a write descriptor (an object \`{ create | update | remove: Model, key, input? }\`).`
755
2970
  );
756
2971
  }
2972
+ const { intent, entity } = resolveDescriptorTarget(descriptor, alias);
2973
+ const merged = mergeBindings(descriptor, intent, alias);
2974
+ const resolved = resolveAliasRefs(merged, aliasToIndex, alias);
2975
+ return makeFragment(intent, entity, resolved, descriptor.use);
757
2976
  });
758
- return proxy;
2977
+ return { [COMMAND_PLAN_BRAND]: true, name, fragments };
2978
+ }
2979
+ var definePlan = mutation;
2980
+
2981
+ // src/define/contract.ts
2982
+ var KEY_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyRef");
2983
+ var KEY_FIELD_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyFieldRef");
2984
+ function isContractKeyRef(value) {
2985
+ return typeof value === "object" && value !== null && value[KEY_REF_BRAND] === true;
2986
+ }
2987
+ function mintContractKeyFieldRef(field) {
2988
+ return { [KEY_FIELD_REF_BRAND]: true, field, token: `{key.${field}}` };
2989
+ }
2990
+ function wholeKeysSentinel() {
2991
+ return { [KEY_REF_BRAND]: true };
2992
+ }
2993
+ function isContractKeyFieldRef(value) {
2994
+ return typeof value === "object" && value !== null && value[KEY_FIELD_REF_BRAND] === true;
759
2995
  }
760
2996
  var PARAM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractParamRef");
761
2997
  function isContractParamRef(value) {
@@ -764,103 +3000,6 @@ function isContractParamRef(value) {
764
3000
  function mintContractParamRef(field) {
765
3001
  return { [PARAM_REF_BRAND]: true, field, token: `{${field}}` };
766
3002
  }
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}`;
775
- }
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() {
815
- throw new Error(
816
- `publicQueryModel/publicCommandModel: cannot define a property on ${origin}.`
817
- );
818
- },
819
- deleteProperty() {
820
- throw new Error(
821
- `publicQueryModel/publicCommandModel: cannot delete on ${origin}.`
822
- );
823
- }
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
- }
835
- 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."
837
- );
838
- }
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);
844
- }
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() {
858
- throw new Error(
859
- "publicQueryModel/publicCommandModel: cannot delete a field of `params`."
860
- );
861
- }
862
- });
863
- }
864
3003
  var FROM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractFromRef");
865
3004
  var COMPOSE_NODE_BRAND = /* @__PURE__ */ Symbol("graphddb:contractComposeNode");
866
3005
  function isContractFromRef(value) {
@@ -925,69 +3064,10 @@ function entityRef(model) {
925
3064
  const modelClass = resolveModelClass(model);
926
3065
  return { name: modelClass.name, modelClass };
927
3066
  }
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
3067
  function isConcreteLiteral2(value) {
947
3068
  const t = typeof value;
948
3069
  return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
949
3070
  }
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
3071
  function extractCompose(select, operation, entityName) {
992
3072
  if (select === void 0) return { select: void 0, compose: [] };
993
3073
  const projection = {};
@@ -996,234 +3076,15 @@ function extractCompose(select, operation, entityName) {
996
3076
  if (isContractComposeNode(value)) {
997
3077
  if (operation !== "query" && operation !== "list") {
998
3078
  throw new Error(
999
- `publicQueryModel: an External Query composition (\`query(...)\`) at select property '${field}' is only valid on a read; the recorded op on '${entityName}' is a '${operation}'.`
1000
- );
1001
- }
1002
- compose.push({ as: field, method: value.method, bind: value.bind });
1003
- continue;
1004
- }
1005
- projection[field] = value;
1006
- }
1007
- return { select: projection, compose };
1008
- }
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
- );
3079
+ `publicQueryModel: an External Query composition (\`query(...)\`) at select property '${field}' is only valid on a read; the recorded op on '${entityName}' is a '${operation}'.`
3080
+ );
3081
+ }
3082
+ compose.push({ as: field, method: value.method, bind: value.bind });
3083
+ continue;
1224
3084
  }
3085
+ projection[field] = value;
1225
3086
  }
1226
- return ops[0];
3087
+ return { select: projection, compose };
1227
3088
  }
1228
3089
  function deriveReadFacts(op) {
1229
3090
  if (op.operation === "list") {
@@ -1247,103 +3108,32 @@ function deriveReadFacts(op) {
1247
3108
  }
1248
3109
  return { resolution: "point", inputArity: "either" };
1249
3110
  }
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
3111
  function isQueryModelContract(value) {
1282
3112
  return typeof value === "object" && value !== null && value.__isContract === true && value.kind === "query";
1283
3113
  }
1284
3114
  function isCommandModelContract(value) {
1285
3115
  return typeof value === "object" && value !== null && value.__isContract === true && value.kind === "command";
1286
3116
  }
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 };
3117
+ function publicQueryModel(methods) {
3118
+ return buildQueryContract(methods);
1330
3119
  }
1331
- function buildQueryContract(methods, keyFields) {
3120
+ function buildQueryContract(methods) {
1332
3121
  const resolved = {};
1333
3122
  for (const [name, body] of Object.entries(methods)) {
1334
- if (typeof body !== "function") {
3123
+ let op;
3124
+ if (isPublicReadDescriptor(body)) {
3125
+ op = readDescriptorToOp(name, body);
3126
+ } else {
1335
3127
  throw new Error(
1336
- `publicQueryModel: method '${name}' must be a function (keys, params) => Model.query(...) | Model.list(...).`
3128
+ `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
3129
  );
1338
3130
  }
1339
- const op = withKeyFields(resolveMethodOp(name, body), keyFields);
1340
3131
  if (op.operation !== "query" && op.operation !== "list") {
1341
3132
  throw new Error(
1342
3133
  `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
3134
  );
1344
3135
  }
1345
3136
  const { resolution, inputArity } = deriveReadFacts(op);
1346
- assertDeclaredArity(name, declaredArityOf(body), resolution, inputArity);
1347
3137
  resolved[name] = {
1348
3138
  __methodKind: "query",
1349
3139
  op,
@@ -1356,115 +3146,61 @@ function buildQueryContract(methods, keyFields) {
1356
3146
  for (const spec of Object.values(resolved)) methodSpecToContract.set(spec, contract);
1357
3147
  return contract;
1358
3148
  }
1359
- function publicCommandModel(modelOrKeyFields, maybeKeyFields) {
1360
- const { keyFields } = normalizeFactoryArgs("publicCommandModel", modelOrKeyFields, maybeKeyFields);
1361
- return (methods) => buildCommandContract(methods, keyFields);
3149
+ function publicCommandModel(methods) {
3150
+ return buildCommandContract(methods);
1362
3151
  }
1363
- function buildCommandContract(methods, keyFields) {
3152
+ function buildCommandContract(methods) {
1364
3153
  const resolved = {};
1365
3154
  for (const [name, body] of Object.entries(methods)) {
1366
- if (isPlannedCommandMethod(body)) {
1367
- resolved[name] = buildPlannedCommandMethod(name, body, keyFields);
3155
+ if (isPublicWriteDescriptor(body)) {
3156
+ const { plan: plan2, select, condition, mode } = writeDescriptorToPlan(name, body);
3157
+ resolved[name] = finalizeDescriptorCommand(name, plan2, select, condition, mode);
1368
3158
  continue;
1369
3159
  }
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).`
3160
+ if (isPublicComposeDescriptor(body)) {
3161
+ const select = body.result !== void 0 && body.result.select !== void 0 ? body.result.select : void 0;
3162
+ resolved[name] = finalizeDescriptorCommand(
3163
+ name,
3164
+ body.command,
3165
+ select,
3166
+ void 0,
3167
+ body.mode ?? "transaction"
1373
3168
  );
3169
+ continue;
1374
3170
  }
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.`
3171
+ if (isCommandPlan(body)) {
3172
+ resolved[name] = finalizeDescriptorCommand(
3173
+ name,
3174
+ body,
3175
+ void 0,
3176
+ void 0,
3177
+ "transaction"
1379
3178
  );
3179
+ continue;
1380
3180
  }
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
- };
3181
+ throw new Error(
3182
+ `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).`
3183
+ );
1391
3184
  }
1392
3185
  return { __isContract: true, kind: "command", methods: resolved };
1393
3186
  }
1394
- function commandResultOf(op) {
1395
- return op.operation === "update" ? "entity" : "void";
1396
- }
1397
3187
  var PLANNED_COMMAND_BRAND = /* @__PURE__ */ Symbol("graphddb:plannedCommandMethod");
1398
3188
  function isPlannedCommandMethod(value) {
1399
3189
  return typeof value === "object" && value !== null && value[PLANNED_COMMAND_BRAND] === true;
1400
3190
  }
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
- }
3191
+ function buildPlannedCommandMethod(name, planned) {
3192
+ const plan2 = compileMutationPlan(planned.plan);
3193
+ const primary = plan2.fragments[0];
1458
3194
  const stamp = (fragment) => fragment.op.operation === "put" ? fragment.op : { ...fragment.op, keyFields: fragment.keyFields };
1459
3195
  const op = stamp(primary);
1460
3196
  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);
3197
+ const ops = plan2.fragments.length > 1 ? plan2.fragments.map(stamp) : void 0;
3198
+ const conditionChecks = plan2.fragments.flatMap((f) => f.conditionChecks ?? []);
3199
+ const edgeWrites2 = plan2.fragments.flatMap((f) => f.edgeWrites ?? []);
3200
+ const derivedUpdates = plan2.fragments.flatMap((f) => f.derivedUpdates ?? []);
3201
+ const uniqueGuards = plan2.fragments.flatMap((f) => f.uniqueGuards ?? []);
3202
+ const outboxEvents = plan2.fragments.flatMap((f) => f.outboxEvents ?? []);
3203
+ const idempotencyGuards = plan2.fragments.map((f) => f.idempotencyGuard).filter((g) => g !== void 0);
1468
3204
  if (idempotencyGuards.length > 1) {
1469
3205
  throw new Error(
1470
3206
  `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 +3224,21 @@ function buildPlannedCommandMethod(name, planned, keyFields) {
1488
3224
  ...returnSelection !== void 0 ? { returnSelection } : {}
1489
3225
  };
1490
3226
  }
3227
+ function finalizeDescriptorCommand(name, plan2, select, condition, mode) {
3228
+ const planned = {
3229
+ [PLANNED_COMMAND_BRAND]: true,
3230
+ plan: plan2,
3231
+ input: {},
3232
+ select
3233
+ };
3234
+ const built = buildPlannedCommandMethod(name, planned);
3235
+ const op = condition !== void 0 ? { ...built.op, condition } : built.op;
3236
+ return {
3237
+ ...built,
3238
+ op,
3239
+ mode
3240
+ };
3241
+ }
1491
3242
  function normalizeReturnSelection(select) {
1492
3243
  if (select === void 0) return void 0;
1493
3244
  const out = {};
@@ -1496,6 +3247,117 @@ function normalizeReturnSelection(select) {
1496
3247
  }
1497
3248
  return Object.keys(out).length > 0 ? out : void 0;
1498
3249
  }
3250
+ var READ_INTENT_KEYS = ["query", "list"];
3251
+ var WRITE_INTENT_KEYS = ["create", "update", "remove"];
3252
+ function isPublicComposeDescriptor(value) {
3253
+ return typeof value === "object" && value !== null && isCommandPlan(value.command);
3254
+ }
3255
+ function isPublicReadDescriptor(value) {
3256
+ return typeof value === "object" && value !== null && READ_INTENT_KEYS.some((k) => value[k] !== void 0);
3257
+ }
3258
+ function isPublicWriteDescriptor(value) {
3259
+ return typeof value === "object" && value !== null && WRITE_INTENT_KEYS.some((k) => value[k] !== void 0);
3260
+ }
3261
+ function resolveDescriptorModel(ref, name) {
3262
+ if (ref === null || ref === void 0) {
3263
+ throw new Error(`publicQueryModel/publicCommandModel: method '${name}' names no target model.`);
3264
+ }
3265
+ if (typeof ref === "function") {
3266
+ const asModel = ref.asModel;
3267
+ if (typeof asModel === "function") return asModel.call(ref);
3268
+ return resolveDescriptorModel(ref(), name);
3269
+ }
3270
+ return ref;
3271
+ }
3272
+ function descriptorKeyRecord(record, name, what) {
3273
+ const out = {};
3274
+ for (const [field, leaf] of Object.entries(record)) {
3275
+ if (isParam(leaf)) {
3276
+ out[field] = mintContractKeyFieldRef(field);
3277
+ } else if (isConcreteLiteral2(leaf)) {
3278
+ out[field] = leaf;
3279
+ } else {
3280
+ throw new Error(
3281
+ `publicQueryModel/publicCommandModel: method '${name}' ${what} field '${field}' must be a \`param.*\` placeholder or a concrete literal, not ${JSON.stringify(leaf)}.`
3282
+ );
3283
+ }
3284
+ }
3285
+ return out;
3286
+ }
3287
+ function descriptorConditionRecord(record, name) {
3288
+ const out = {};
3289
+ for (const [field, leaf] of Object.entries(record)) {
3290
+ if (isParam(leaf)) {
3291
+ out[field] = mintContractParamRef(field);
3292
+ } else {
3293
+ out[field] = leaf;
3294
+ }
3295
+ }
3296
+ return out;
3297
+ }
3298
+ function readDescriptorToOp(name, d) {
3299
+ const isQuery = d.query !== void 0;
3300
+ const isList = d.list !== void 0;
3301
+ if (isQuery === isList) {
3302
+ throw new Error(
3303
+ `publicQueryModel: method '${name}' must carry exactly one of \`query\` or \`list\` (the target model); it declared ${isQuery && isList ? "both" : "neither"}.`
3304
+ );
3305
+ }
3306
+ if (d.key === void 0 || d.select === void 0) {
3307
+ throw new Error(
3308
+ `publicQueryModel: the '${isQuery ? "query" : "list"}' descriptor for method '${name}' must declare both \`key\` and \`select\`.`
3309
+ );
3310
+ }
3311
+ const model = resolveDescriptorModel(isQuery ? d.query : d.list, name);
3312
+ const entity = entityRef(model);
3313
+ const operation = isQuery ? "query" : "list";
3314
+ const keyFieldNames2 = Object.keys(d.key);
3315
+ const keyRecord = descriptorKeyRecord(d.key, name, `${operation} key`);
3316
+ const { select, compose } = extractCompose({ ...d.select }, operation, entity.name);
3317
+ const keys = isQuery ? wholeKeysSentinel() : keyRecord;
3318
+ return {
3319
+ __isContractMethodOp: true,
3320
+ entity,
3321
+ operation,
3322
+ keys,
3323
+ ...isQuery ? { keyFields: keyFieldNames2 } : {},
3324
+ ...select !== void 0 ? { select } : {},
3325
+ ...compose.length > 0 ? { compose } : {}
3326
+ };
3327
+ }
3328
+ function writeDescriptorToPlan(name, d) {
3329
+ const present = WRITE_INTENT_KEYS.filter((k) => d[k] !== void 0);
3330
+ if (present.length !== 1) {
3331
+ throw new Error(
3332
+ `publicCommandModel: method '${name}' must carry exactly one intent key (\`create\` / \`update\` / \`remove\`), but declared ${present.length === 0 ? "none" : present.join(", ")}.`
3333
+ );
3334
+ }
3335
+ const intent = present[0];
3336
+ if (d.key === void 0 || d.key === null || typeof d.key !== "object") {
3337
+ throw new Error(
3338
+ `publicCommandModel: the '${intent}' descriptor for method '${name}' must declare a \`key\` object binding the target's primary-key fields.`
3339
+ );
3340
+ }
3341
+ const model = resolveDescriptorModel(d[intent], name);
3342
+ const keyFieldNames2 = Object.keys(d.key);
3343
+ const inputFieldNames = d.input !== void 0 ? Object.keys(d.input) : [];
3344
+ const plan2 = mutation(name, ($) => {
3345
+ const refMap = (fields) => {
3346
+ const out = {};
3347
+ for (const f of fields) out[f] = $[f];
3348
+ return out;
3349
+ };
3350
+ const descriptor = {
3351
+ [intent]: (() => model),
3352
+ key: refMap(keyFieldNames2),
3353
+ ...inputFieldNames.length > 0 ? { input: refMap(inputFieldNames) } : {}
3354
+ };
3355
+ return { [name]: descriptor };
3356
+ });
3357
+ const select = d.result !== void 0 && d.result.select !== void 0 ? d.result.select : void 0;
3358
+ const condition = d.condition !== void 0 ? descriptorConditionRecord(d.condition, name) : void 0;
3359
+ return { plan: plan2, select, condition, mode: d.mode ?? "transaction" };
3360
+ }
1499
3361
 
1500
3362
  // src/spec/mutation-command.ts
1501
3363
  var INTENT_OPERATION = {
@@ -1928,18 +3790,18 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null) {
1928
3790
  ...idempotencyGuard !== void 0 ? { idempotencyGuard } : {}
1929
3791
  };
1930
3792
  }
1931
- function compileSingleFragmentPlan(plan) {
1932
- if (!isCommandPlan(plan)) {
3793
+ function compileSingleFragmentPlan(plan2) {
3794
+ if (!isCommandPlan(plan2)) {
1933
3795
  throw new Error(
1934
3796
  "mutation compiler: expected a `mutation(...)` CommandPlan, but received a non-plan value."
1935
3797
  );
1936
3798
  }
1937
- if (plan.fragments.length !== 1) {
3799
+ if (plan2.fragments.length !== 1) {
1938
3800
  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).`
3801
+ `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
3802
  );
1941
3803
  }
1942
- return compileFragment(plan.fragments[0], 0, null);
3804
+ return compileFragment(plan2.fragments[0], 0, null);
1943
3805
  }
1944
3806
  var MAX_TRANSACT_COMPOSE_ITEMS = 25;
1945
3807
  function keySignature(fragment) {
@@ -1963,26 +3825,26 @@ function keyLeafToken(leaf) {
1963
3825
  if (isContractParamRef(leaf)) return `input:${leaf.field}`;
1964
3826
  return `literal:${JSON.stringify(leaf)}`;
1965
3827
  }
1966
- function compileMutationPlan(plan) {
1967
- if (!isCommandPlan(plan)) {
3828
+ function compileMutationPlan(plan2) {
3829
+ if (!isCommandPlan(plan2)) {
1968
3830
  throw new Error(
1969
3831
  "mutation compiler: expected a `mutation(...)` CommandPlan, but received a non-plan value."
1970
3832
  );
1971
3833
  }
1972
- const fragments = plan.fragments;
3834
+ const fragments = plan2.fragments;
1973
3835
  const compiled = [];
1974
3836
  for (let i = 0; i < fragments.length; i++) {
1975
3837
  const resolveEntityRef = (ref, consumerIndex, consumerField) => {
1976
3838
  if (ref.fragmentIndex >= consumerIndex) {
1977
3839
  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.`
3840
+ `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
3841
  );
1980
3842
  }
1981
3843
  const producer = compiled[ref.fragmentIndex];
1982
3844
  const resolved = producedFieldLeaf(producer, ref.field);
1983
3845
  if (resolved === void 0) {
1984
3846
  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\`.`
3847
+ `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
3848
  );
1987
3849
  }
1988
3850
  return resolved;
@@ -1995,17 +3857,17 @@ function compileMutationPlan(plan) {
1995
3857
  const prior = byKey.get(sig);
1996
3858
  if (prior !== void 0) {
1997
3859
  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.`
3860
+ `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
3861
  );
2000
3862
  }
2001
3863
  byKey.set(sig, i);
2002
3864
  }
2003
3865
  if (compiled.length > MAX_TRANSACT_COMPOSE_ITEMS) {
2004
3866
  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}.`
3867
+ `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
3868
  );
2007
3869
  }
2008
- return { name: plan.name, fragments: compiled };
3870
+ return { name: plan2.name, fragments: compiled };
2009
3871
  }
2010
3872
  function producedFieldLeaf(producer, field) {
2011
3873
  const op = producer.op;
@@ -2029,7 +3891,7 @@ function describeKey(fragment) {
2029
3891
  }
2030
3892
 
2031
3893
  // src/spec/guard.ts
2032
- function conditionInputToSpec(condition, context, renderLeaf) {
3894
+ function conditionInputToSpec(condition, context, renderLeaf2) {
2033
3895
  if (condition === void 0 || condition === null) return void 0;
2034
3896
  if (typeof condition !== "object") {
2035
3897
  throw new Error(
@@ -2056,7 +3918,7 @@ function conditionInputToSpec(condition, context, renderLeaf) {
2056
3918
  }
2057
3919
  const fields = {};
2058
3920
  for (const key of Object.keys(obj).sort()) {
2059
- fields[key] = renderLeaf(key, obj[key]);
3921
+ fields[key] = renderLeaf2(key, obj[key]);
2060
3922
  }
2061
3923
  return { kind: "equals", fields };
2062
3924
  }
@@ -2500,17 +4362,17 @@ function verifyWrite(instrs, surfaced) {
2500
4362
  "defineTransaction: write operation diverged between evaluation passes; the callback must be a pure, declarative description of the writes."
2501
4363
  );
2502
4364
  }
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(
4365
+ verifyRecord(instrs.map((x) => x.item), surfaced, `${op} item`);
4366
+ verifyRecord(instrs.map((x) => x.key), surfaced, `${op} key`);
4367
+ verifyRecord(instrs.map((x) => x.changes), surfaced, `${op} changes`);
4368
+ verifyRecord(
2507
4369
  instrs.map((x) => x.condition),
2508
4370
  surfaced,
2509
4371
  `${op} condition`
2510
4372
  );
2511
4373
  verifyWhen(instrs.map((x) => x.when), surfaced, `${op} when`);
2512
4374
  }
2513
- function verifyRecord2(records, surfaced, context) {
4375
+ function verifyRecord(records, surfaced, context) {
2514
4376
  const present = records.filter((r) => r !== void 0);
2515
4377
  if (present.length === 0) return;
2516
4378
  if (present.length !== records.length) {
@@ -2528,7 +4390,7 @@ function verifyRecord2(records, surfaced, context) {
2528
4390
  }
2529
4391
  }
2530
4392
  for (const field of keys) {
2531
- verifyLeaf2(
4393
+ verifyLeaf(
2532
4394
  present.map((r) => r[field]),
2533
4395
  surfaced,
2534
4396
  `${context} field '${field}'`
@@ -2546,9 +4408,9 @@ function verifyWhen(whens, surfaced, context) {
2546
4408
  for (const w of present) {
2547
4409
  if (isTransactionRef(w.left)) surfaced.add(w.left.token);
2548
4410
  }
2549
- verifyLeaf2(present.map((w) => w.right), surfaced, `${context} right-hand value`);
4411
+ verifyLeaf(present.map((w) => w.right), surfaced, `${context} right-hand value`);
2550
4412
  }
2551
- function verifyLeaf2(values, surfaced, context) {
4413
+ function verifyLeaf(values, surfaced, context) {
2552
4414
  const refTokens = values.map((v) => isTransactionRef(v) ? v.token : void 0);
2553
4415
  const anyRef = refTokens.some((t) => t !== void 0);
2554
4416
  if (anyRef) {
@@ -2566,7 +4428,7 @@ function verifyLeaf2(values, surfaced, context) {
2566
4428
  surfaced.add(token);
2567
4429
  return;
2568
4430
  }
2569
- const norm = values.map(normalizeLiteral2);
4431
+ const norm = values.map(normalizeLiteral);
2570
4432
  for (let i = 1; i < norm.length; i++) {
2571
4433
  if (norm[i] !== norm[0]) {
2572
4434
  throw new Error(
@@ -2575,7 +4437,7 @@ function verifyLeaf2(values, surfaced, context) {
2575
4437
  }
2576
4438
  }
2577
4439
  }
2578
- function normalizeLiteral2(value) {
4440
+ function normalizeLiteral(value) {
2579
4441
  if (value === null) return "t:null";
2580
4442
  if (value instanceof Date) return `t:date:${value.toISOString()}`;
2581
4443
  const t = typeof value;
@@ -2866,113 +4728,6 @@ ${rest.map((v) => ` - ${v.message}`).join("\n")})` : "";
2866
4728
  throw new Error(first.message + suffix);
2867
4729
  }
2868
4730
 
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
4731
  // src/spec/contract-boundary-check.ts
2977
4732
  function buildOwnershipIndex(contexts) {
2978
4733
  const modelOwner = /* @__PURE__ */ new Map();
@@ -3202,12 +4957,12 @@ function buildHasManyOperation(rel, targetMeta, select, limit, filter, resultPat
3202
4957
  const keyCondition2 = {
3203
4958
  PK: pk2
3204
4959
  };
3205
- const range3 = sk2 !== void 0 && sk2 !== "" ? { operator: "begins_with", key: "SK", value: sk2 } : void 0;
4960
+ const range2 = sk2 !== void 0 && sk2 !== "" ? { operator: "begins_with", key: "SK", value: sk2 } : void 0;
3206
4961
  return {
3207
4962
  type: "Query",
3208
4963
  tableName,
3209
4964
  keyCondition: keyCondition2,
3210
- ...range3 ? { rangeCondition: range3 } : {},
4965
+ ...range2 ? { rangeCondition: range2 } : {},
3211
4966
  projection: projectionFields(select),
3212
4967
  limit,
3213
4968
  resultPath,
@@ -3223,13 +4978,13 @@ function buildHasManyOperation(rel, targetMeta, select, limit, filter, resultPat
3223
4978
  resolved.partial
3224
4979
  );
3225
4980
  const keyCondition = { [`${gsi.indexName}PK`]: pk };
3226
- const range2 = sk !== void 0 && sk !== "" ? { operator: "begins_with", key: `${gsi.indexName}SK`, value: sk } : void 0;
4981
+ const range = sk !== void 0 && sk !== "" ? { operator: "begins_with", key: `${gsi.indexName}SK`, value: sk } : void 0;
3227
4982
  return {
3228
4983
  type: "Query",
3229
4984
  tableName,
3230
4985
  indexName: gsi.indexName,
3231
4986
  keyCondition,
3232
- ...range2 ? { rangeCondition: range2 } : {},
4987
+ ...range ? { rangeCondition: range } : {},
3233
4988
  projection: projectionFields(select),
3234
4989
  limit,
3235
4990
  resultPath,
@@ -3933,15 +5688,9 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
3933
5688
  }
3934
5689
  return { params: sortedParams(params), items, maxItems: items.length };
3935
5690
  }
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 };
5691
+ function modeTargetFor(mode, op, commandSpec, contractName, methodName, opName, transactionSpecs) {
5692
+ if (mode === "parallel") {
5693
+ return { mode: "parallel", operation: opName };
3945
5694
  }
3946
5695
  const txName = batchTxName(contractName, methodName);
3947
5696
  transactionSpecs[txName] = synthesizeBatchTransaction(commandSpec, op);
@@ -4076,15 +5825,15 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
4076
5825
  returnSelection
4077
5826
  );
4078
5827
  }
4079
- const batch = batchTargetFor(
4080
- method.batch,
5828
+ const batch = method.mode !== void 0 && !promotedToTransaction ? modeTargetFor(
5829
+ method.mode,
4081
5830
  op,
4082
5831
  commandSpec,
4083
5832
  contractName,
4084
5833
  methodName,
4085
5834
  opName,
4086
5835
  transactionSpecs
4087
- );
5836
+ ) : void 0;
4088
5837
  const single = promotedToTransaction ? { mode: "transaction", transaction: opName } : { mode: "op", operation: opName };
4089
5838
  methods[methodName] = {
4090
5839
  inputArity: method.inputArity,
@@ -4197,13 +5946,31 @@ export {
4197
5946
  buildManifest,
4198
5947
  isSelectBuilder,
4199
5948
  normalizeSelectSpec,
4200
- normalizeTopLevelSelect,
4201
- buildProject,
4202
- buildRelation,
4203
5949
  detectRelationFields,
4204
5950
  getImplicitKeyFields,
4205
5951
  param,
4206
5952
  isParam,
5953
+ buildProjection,
5954
+ cond,
5955
+ compileFilterExpression,
5956
+ evaluateFilter,
5957
+ plan,
5958
+ execute,
5959
+ hydrate,
5960
+ encodeCursor,
5961
+ decodeCursor,
5962
+ executeListInternal,
5963
+ executeList,
5964
+ RELATION_TRAVERSAL_CONCURRENCY,
5965
+ mapWithConcurrency,
5966
+ deriveCompositionPlan,
5967
+ validateDepth,
5968
+ resolveRelations,
5969
+ executeQuery,
5970
+ executeExplain,
5971
+ BatchGetResult,
5972
+ executeBatchGet,
5973
+ executeBatchWrite,
4207
5974
  LIFECYCLE_CONTRACT_MARKER,
4208
5975
  isLifecycleContract,
4209
5976
  ENTITY_WRITES_MARKER,
@@ -4211,11 +5978,6 @@ export {
4211
5978
  entityWrites,
4212
5979
  getEntityWrites,
4213
5980
  lifecyclePhaseForIntent,
4214
- isMutationInputRef,
4215
- isMutationFragment,
4216
- isCommandPlan,
4217
- mutation,
4218
- definePlan,
4219
5981
  OLD_VALUE_NAMESPACE,
4220
5982
  EDGE_WRITES_MARKER,
4221
5983
  isEdgeWritesDefinition,
@@ -4228,7 +5990,13 @@ export {
4228
5990
  compileFragment,
4229
5991
  compileSingleFragmentPlan,
4230
5992
  compileMutationPlan,
4231
- isCommandBatchMode,
5993
+ executeCommandMethod,
5994
+ DDBModel,
5995
+ isMutationInputRef,
5996
+ isMutationFragment,
5997
+ isCommandPlan,
5998
+ mutation,
5999
+ definePlan,
4232
6000
  isContractKeyRef,
4233
6001
  mintContractKeyFieldRef,
4234
6002
  wholeKeysSentinel,
@@ -4240,21 +6008,11 @@ export {
4240
6008
  from,
4241
6009
  query,
4242
6010
  contractOfMethodSpec,
4243
- recordContractOp,
4244
- isRecordingContractMethod,
4245
- point,
4246
- range,
4247
6011
  isQueryModelContract,
4248
6012
  isCommandModelContract,
4249
6013
  publicQueryModel,
4250
6014
  publicCommandModel,
4251
6015
  isPlannedCommandMethod,
4252
- command,
4253
- RELATION_TRAVERSAL_CONCURRENCY,
4254
- mapWithConcurrency,
4255
- buildRelationExecutionPlan,
4256
- deriveCompositionPlan,
4257
- stageOfResultPath,
4258
6016
  assertSupportedCondition,
4259
6017
  assertJsonSerializable,
4260
6018
  assertBundleSerializable,