graphddb 0.2.3 → 0.2.5

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.
@@ -4,17 +4,22 @@ import {
4
4
  ClientManager,
5
5
  MAX_TRANSACT_ITEMS,
6
6
  MetadataRegistry,
7
+ NO_MIDDLEWARE,
7
8
  TableMapping,
8
9
  attachHiddenKey,
9
10
  attachModelClass,
10
11
  buildConditionExpression,
11
12
  buildDeleteInput,
12
13
  buildPutInput,
14
+ buildReadRuntime,
13
15
  buildUpdateInput,
16
+ buildWriteRuntime,
14
17
  captureWrite,
18
+ chunkArray,
15
19
  commitTransaction,
16
20
  compileRawCondition,
17
21
  createColumnMap,
22
+ ctxModelFor,
18
23
  execItemKeySignature,
19
24
  executeDelete,
20
25
  executePut,
@@ -32,7 +37,7 @@ import {
32
37
  serializeFieldValue,
33
38
  serializeRawCondition,
34
39
  skTemplate
35
- } from "./chunk-FWDJSGF3.js";
40
+ } from "./chunk-4TYK3AV6.js";
36
41
 
37
42
  // src/spec/types.ts
38
43
  var SPEC_VERSION = "1.0";
@@ -686,8 +691,8 @@ function buildFromSegments(segmented, inputFieldNames, queryKey, pkAttr, skAttr)
686
691
  }
687
692
 
688
693
  // src/executor/executor.ts
689
- async function execute(operation) {
690
- return ClientManager.getExecutor().execute(operation);
694
+ async function execute(operation, options) {
695
+ return ClientManager.getExecutor().execute(operation, options);
691
696
  }
692
697
 
693
698
  // src/hydrator/hydrator.ts
@@ -810,20 +815,71 @@ async function executeListInternal(modelClass, key, selectSpec, options = {}) {
810
815
  updatable: options.updatable ?? false
811
816
  });
812
817
  const operation = executionPlan.operations[0];
813
- const result = await execute(operation);
814
- const rawItems = result.items;
818
+ const mw = options.mw ?? NO_MIDDLEWARE;
819
+ const sendList = (op) => execute(op, options.retry !== void 0 ? { retry: options.retry } : void 0).then(
820
+ (r) => r
821
+ );
822
+ let lastEvaluatedKey;
823
+ let rawItems;
824
+ if (mw.active) {
825
+ let captured;
826
+ const opModel = ctxModelFor(modelClass);
827
+ rawItems = await mw.runOp(
828
+ operation,
829
+ options.relationPath ?? [],
830
+ opModel,
831
+ async (op) => {
832
+ captured = await sendList(op);
833
+ return captured.items;
834
+ }
835
+ );
836
+ lastEvaluatedKey = captured?.lastEvaluatedKey;
837
+ } else {
838
+ const result = await sendList(operation);
839
+ rawItems = result.items;
840
+ lastEvaluatedKey = result.lastEvaluatedKey;
841
+ }
815
842
  const hydrated = hydrate(rawItems, select, metadata, options.updatable ?? false);
816
- const cursor = result.lastEvaluatedKey ? encodeCursor(result.lastEvaluatedKey) : null;
843
+ const cursor = lastEvaluatedKey ? encodeCursor(lastEvaluatedKey) : null;
817
844
  return { items: hydrated, rawItems, cursor };
818
845
  }
819
846
  async function executeList(modelClass, key, selectSpec, options = {}) {
820
- const { items, cursor } = await executeListInternal(
821
- modelClass,
822
- key,
823
- selectSpec,
824
- options
825
- );
826
- return { items, cursor };
847
+ const mw = buildReadRuntime(options.context);
848
+ if (!mw.active) {
849
+ const { items, cursor } = await executeListInternal(
850
+ modelClass,
851
+ key,
852
+ selectSpec,
853
+ options
854
+ );
855
+ return { items, cursor };
856
+ }
857
+ const ctxModel = ctxModelFor(modelClass);
858
+ const opts = { ...options };
859
+ const params = {
860
+ key: { ...key },
861
+ select: selectSpec && typeof selectSpec === "object" ? selectSpec : {},
862
+ options: opts
863
+ };
864
+ let reqCtx;
865
+ try {
866
+ reqCtx = await mw.runRequestBefore("list", ctxModel, params);
867
+ const { items, cursor } = await executeListInternal(
868
+ modelClass,
869
+ params.key,
870
+ params.select,
871
+ // Thread the SAME runtime + an empty (root) relation path so R2/R3 fire on
872
+ // the partition Query; carry the (possibly R1-mutated) per-call options.
873
+ { ...params.options, mw, relationPath: [] }
874
+ );
875
+ const result = { items, cursor };
876
+ return await mw.runRequestAfter(reqCtx, result);
877
+ } catch (err) {
878
+ return mw.runRequestError(
879
+ reqCtx ?? mw.requestCtx("list", ctxModel, params),
880
+ err
881
+ );
882
+ }
827
883
  }
828
884
 
829
885
  // src/planner/relation-planner.ts
@@ -1171,9 +1227,23 @@ async function fetchBelongsToHasOneBatch(rel, rawParentItems, selectSpec, target
1171
1227
  );
1172
1228
  const executor = ClientManager.getExecutor();
1173
1229
  const queryKeyToRaw = /* @__PURE__ */ new Map();
1230
+ const mw = options.mw ?? NO_MIDDLEWARE;
1231
+ const opPath = [...options.relationPath ?? [], rel.propertyName];
1232
+ const opModel = mw.active ? ctxModelFor(rel.targetFactory()) : void 0;
1174
1233
  for (const operation of batchOps) {
1175
- const batchResult = await executor.batchGet(operation);
1176
- for (const item of batchResult.items) {
1234
+ const items = mw.active ? await mw.runOp(
1235
+ operation,
1236
+ opPath,
1237
+ opModel,
1238
+ (op) => executor.batchGet(
1239
+ op,
1240
+ options.retry !== void 0 ? { retry: options.retry } : void 0
1241
+ ).then((r) => r.items)
1242
+ ) : (await executor.batchGet(
1243
+ operation,
1244
+ options.retry !== void 0 ? { retry: options.retry } : void 0
1245
+ )).items;
1246
+ for (const item of items) {
1177
1247
  for (const queryKey of queryKeys) {
1178
1248
  if (!matchesQueryKey(item, queryKey)) {
1179
1249
  continue;
@@ -1259,9 +1329,11 @@ async function resolveRelations(parentItem, rawParentItem, select, metadata, opt
1259
1329
  const relations = detectRelationFields(select, metadata);
1260
1330
  const plan2 = options.plan ?? (currentPath === "$" ? buildRelationExecutionPlan(select, metadata) : void 0);
1261
1331
  const opts = plan2 === options.plan ? options : { ...options, plan: plan2 };
1332
+ const basePath = opts.relationPath ?? [];
1262
1333
  await runStaged(relations, currentPath, opts.plan, async (rel) => {
1263
1334
  const selectSpec = relationSpec(select[rel.propertyName]);
1264
1335
  const ownPath = relationResultPath(currentPath, rel);
1336
+ const relPath = [...basePath, rel.propertyName];
1265
1337
  if (rel.type === "hasMany") {
1266
1338
  const targetClass2 = rel.targetFactory();
1267
1339
  const queryKey = buildRelationQueryKey(rel, rawParentItem);
@@ -1272,7 +1344,10 @@ async function resolveRelations(parentItem, rawParentItem, select, metadata, opt
1272
1344
  after: selectSpec.after,
1273
1345
  order,
1274
1346
  filter: selectSpec.filter,
1275
- updatable: opts.updatable
1347
+ updatable: opts.updatable,
1348
+ retry: opts.retry,
1349
+ mw: opts.mw,
1350
+ relationPath: relPath
1276
1351
  });
1277
1352
  let items = listResult.items;
1278
1353
  const rawItems = listResult.rawItems;
@@ -1284,7 +1359,7 @@ async function resolveRelations(parentItem, rawParentItem, select, metadata, opt
1284
1359
  rawItems,
1285
1360
  selectSpec.select,
1286
1361
  targetMeta2,
1287
- opts,
1362
+ { ...opts, relationPath: relPath },
1288
1363
  currentDepth + 1,
1289
1364
  ownPath
1290
1365
  );
@@ -1313,6 +1388,36 @@ async function resolveRelations(parentItem, rawParentItem, select, metadata, opt
1313
1388
 
1314
1389
  // src/operations/query.ts
1315
1390
  async function executeQuery(modelClass, key, selectSpec, options) {
1391
+ const mw = buildReadRuntime(options?.context);
1392
+ if (!mw.active) {
1393
+ return executeQueryInner(modelClass, key, selectSpec, options, mw);
1394
+ }
1395
+ const ctxModel = ctxModelFor(modelClass);
1396
+ const opts = { ...options ?? {} };
1397
+ const params = {
1398
+ key: { ...key },
1399
+ select: selectSpec && typeof selectSpec === "object" ? selectSpec : {},
1400
+ options: opts
1401
+ };
1402
+ let reqCtx;
1403
+ try {
1404
+ reqCtx = await mw.runRequestBefore("query", ctxModel, params);
1405
+ const result = await executeQueryInner(
1406
+ modelClass,
1407
+ params.key,
1408
+ params.select,
1409
+ params.options,
1410
+ mw
1411
+ );
1412
+ return await mw.runRequestAfter(reqCtx, result);
1413
+ } catch (err) {
1414
+ return mw.runRequestError(
1415
+ reqCtx ?? mw.requestCtx("query", ctxModel, params),
1416
+ err
1417
+ );
1418
+ }
1419
+ }
1420
+ async function executeQueryInner(modelClass, key, selectSpec, options, mw) {
1316
1421
  const metadata = MetadataRegistry.get(modelClass);
1317
1422
  const queryFields = Object.keys(key);
1318
1423
  const resolved = resolveKey(queryFields, metadata);
@@ -1345,8 +1450,26 @@ async function executeQuery(modelClass, key, selectSpec, options) {
1345
1450
  (sourceField) => key[sourceField] != null
1346
1451
  )
1347
1452
  );
1348
- const parentPromise = execute(operation);
1349
- const relationPromise = canParallelizeRelations ? resolveRelations({}, key, select, metadata, { maxDepth, updatable }, 1) : null;
1453
+ const retry = options?.retry;
1454
+ const sendRoot = () => execute(operation, retry !== void 0 ? { retry } : void 0);
1455
+ const parentPromise = mw.active ? (() => {
1456
+ const opModel = ctxModelFor(modelClass);
1457
+ return mw.runOp(operation, [], opModel, async (op) => {
1458
+ const r = await execute(
1459
+ op,
1460
+ retry !== void 0 ? { retry } : void 0
1461
+ );
1462
+ return r.items;
1463
+ }).then((items) => ({ items }));
1464
+ })() : sendRoot();
1465
+ const relationPromise = canParallelizeRelations ? resolveRelations(
1466
+ {},
1467
+ key,
1468
+ select,
1469
+ metadata,
1470
+ { maxDepth, updatable, retry, mw, relationPath: [] },
1471
+ 1
1472
+ ) : null;
1350
1473
  let result;
1351
1474
  try {
1352
1475
  result = await parentPromise;
@@ -1373,7 +1496,7 @@ async function executeQuery(modelClass, key, selectSpec, options) {
1373
1496
  rawItem,
1374
1497
  select,
1375
1498
  metadata,
1376
- { maxDepth, updatable },
1499
+ { maxDepth, updatable, retry, mw, relationPath: [] },
1377
1500
  1
1378
1501
  );
1379
1502
  if (hydratedItem && updatable) {
@@ -1454,16 +1577,37 @@ function hydrateBatchItems(rawItems, metadata) {
1454
1577
  return merged;
1455
1578
  });
1456
1579
  }
1457
- async function executeBatchGetForTable(tableName, entries) {
1580
+ async function executeBatchGetForTable(tableName, entries, mw, opModel) {
1458
1581
  const groups = /* @__PURE__ */ new Map();
1459
1582
  if (entries.length === 0) {
1460
1583
  return groups;
1461
1584
  }
1462
1585
  const keys = entries.map((entry) => entry.dynamoKey);
1463
- const { items: rawItems } = await ClientManager.getExecutor().batchGet({
1464
- tableName,
1465
- keys
1466
- });
1586
+ const sendChunk = (op) => execute(op).then((r) => r.items);
1587
+ let rawItems;
1588
+ if (mw.active) {
1589
+ rawItems = [];
1590
+ for (const chunk of chunkArray(keys, BATCH_GET_MAX_KEYS)) {
1591
+ const operation = {
1592
+ type: "BatchGetItem",
1593
+ tableName,
1594
+ keys: chunk
1595
+ };
1596
+ const chunkItems = await mw.runOp(
1597
+ operation,
1598
+ [],
1599
+ opModel,
1600
+ (op) => sendChunk(op)
1601
+ );
1602
+ rawItems.push(...chunkItems);
1603
+ }
1604
+ } else {
1605
+ const { items } = await ClientManager.getExecutor().batchGet({
1606
+ tableName,
1607
+ keys
1608
+ });
1609
+ rawItems = items;
1610
+ }
1467
1611
  const entriesByModel = /* @__PURE__ */ new Map();
1468
1612
  for (const entry of entries) {
1469
1613
  const existing = entriesByModel.get(entry.modelClass) ?? [];
@@ -1484,12 +1628,42 @@ async function executeBatchGetForTable(tableName, entries) {
1484
1628
  }
1485
1629
  return groups;
1486
1630
  }
1487
- async function executeBatchGet(requests) {
1631
+ async function executeBatchGet(requests, options) {
1632
+ const mw = buildReadRuntime(options?.context);
1633
+ if (!mw.active || requests.length === 0) {
1634
+ return executeBatchGetInner(requests, mw);
1635
+ }
1636
+ const firstModel = ctxModelFor(resolveModelClass(requests[0].model));
1637
+ const reqList = [...requests];
1638
+ const params = {
1639
+ key: {},
1640
+ select: {},
1641
+ options: {},
1642
+ requests: reqList
1643
+ };
1644
+ let reqCtx;
1645
+ try {
1646
+ reqCtx = await mw.runRequestBefore("batchGet", firstModel, params);
1647
+ const effectiveRequests = params.requests;
1648
+ const result = await executeBatchGetInner(effectiveRequests, mw);
1649
+ return await mw.runRequestAfter(reqCtx, result);
1650
+ } catch (err) {
1651
+ return mw.runRequestError(
1652
+ reqCtx ?? mw.requestCtx("batchGet", firstModel, params),
1653
+ err
1654
+ );
1655
+ }
1656
+ }
1657
+ async function executeBatchGetInner(requests, mw) {
1488
1658
  const entriesByTable = /* @__PURE__ */ new Map();
1659
+ const firstModelClassByTable = /* @__PURE__ */ new Map();
1489
1660
  for (const request of requests) {
1490
1661
  const modelClass = resolveModelClass(request.model);
1491
1662
  const metadata = MetadataRegistry.get(modelClass);
1492
1663
  const tableName = TableMapping.resolve(metadata.tableName);
1664
+ if (!firstModelClassByTable.has(tableName)) {
1665
+ firstModelClassByTable.set(tableName, modelClass);
1666
+ }
1493
1667
  for (const keyObj of request.keys) {
1494
1668
  const entry = {
1495
1669
  modelClass,
@@ -1504,7 +1678,13 @@ async function executeBatchGet(requests) {
1504
1678
  const mergedGroups = /* @__PURE__ */ new Map();
1505
1679
  for (const [, entries] of entriesByTable) {
1506
1680
  const tableName = TableMapping.resolve(entries[0].metadata.tableName);
1507
- const tableGroups = await executeBatchGetForTable(tableName, entries);
1681
+ const opModel = mw.active ? ctxModelFor(firstModelClassByTable.get(tableName) ?? entries[0].modelClass) : void 0;
1682
+ const tableGroups = await executeBatchGetForTable(
1683
+ tableName,
1684
+ entries,
1685
+ mw,
1686
+ opModel
1687
+ );
1508
1688
  for (const [modelClass, items] of tableGroups) {
1509
1689
  const existing = mergedGroups.get(modelClass) ?? [];
1510
1690
  mergedGroups.set(modelClass, existing.concat(items));
@@ -1894,8 +2074,11 @@ function renderWrite(op, key, params, contextLabel) {
1894
2074
  }
1895
2075
  return base;
1896
2076
  }
1897
- async function executeSingleWrite(w) {
1898
- const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
2077
+ async function executeSingleWrite(w, retry) {
2078
+ const options = w.condition !== void 0 || retry !== void 0 ? {
2079
+ ...w.condition !== void 0 ? { condition: w.condition } : {},
2080
+ ...retry !== void 0 ? { retry } : {}
2081
+ } : void 0;
1899
2082
  if (w.operation === "put") {
1900
2083
  await executePut(w.modelClass, w.item ?? {}, options);
1901
2084
  return;
@@ -2062,7 +2245,7 @@ function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
2062
2245
  return { Put: put };
2063
2246
  });
2064
2247
  }
2065
- async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map()) {
2248
+ async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map(), retry, middleware) {
2066
2249
  const writeItems = writes.map((w) => {
2067
2250
  const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
2068
2251
  let item;
@@ -2089,6 +2272,8 @@ async function executeReferentialTransaction(writes, edgeItems, updateItems, gua
2089
2272
  ],
2090
2273
  {
2091
2274
  modelBySignature,
2275
+ ...retry !== void 0 ? { retry } : {},
2276
+ ...middleware !== void 0 ? { middleware } : {},
2092
2277
  limitError: (count) => new Error(
2093
2278
  `Contract runtime: command method '${methodName}' composes ${count} items (writes + edges + derived updates + uniqueness guards + outbox events + idempotency guard + ConditionChecks) into one TransactWriteItems, but DynamoDB caps a transaction at ${MAX_TRANSACT_ITEMS} items and a transaction is atomic \u2014 it cannot be split. Reduce the fragment / effect count.`
2094
2279
  )
@@ -2302,7 +2487,24 @@ function readBackForCompiledOp(op, write) {
2302
2487
  }
2303
2488
  async function executeCompiledWriteOp(ops, options) {
2304
2489
  const label = options.label;
2305
- const renderedOps = ops.map((op, i) => renderCompiledOp(op, `${label} (op #${i})`));
2490
+ const writeMw = buildWriteRuntime(options.context);
2491
+ const writeCtxs = [];
2492
+ const txId = { id: /* @__PURE__ */ Symbol("graphddb.mutate") };
2493
+ let rewrites = [];
2494
+ if (writeMw.active) {
2495
+ const w1ed = [];
2496
+ for (const op of ops) {
2497
+ const r = await runEnvelopeW1(op, writeMw, txId, writeCtxs);
2498
+ w1ed.push(r.op);
2499
+ rewrites.push(r.rewritten ?? null);
2500
+ }
2501
+ ops = w1ed;
2502
+ } else {
2503
+ rewrites = ops.map(() => null);
2504
+ }
2505
+ const renderedOps = ops.map(
2506
+ (op, i) => rewrites[i] !== null ? rewrittenCompiledOp(rewrites[i]) : renderCompiledOp(op, `${label} (op #${i})`)
2507
+ );
2306
2508
  const rendered = renderedOps.map((r) => r.write);
2307
2509
  const modelBySignature = /* @__PURE__ */ new Map();
2308
2510
  const edgeItems = [];
@@ -2321,8 +2523,13 @@ async function executeCompiledWriteOp(ops, options) {
2321
2523
  checkItems.push(...r.checkItems);
2322
2524
  }
2323
2525
  const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
2526
+ const origins = writeMw.active ? rendered.map((w) => ({ model: ctxModelFor(w.modelClass), kind: w.operation })) : [];
2324
2527
  if (rendered.length === 1 && !hasDerivedEffects) {
2325
- await executeSingleWrite(rendered[0]);
2528
+ if (writeMw.active) {
2529
+ await persistSingleWriteWithHooks(rendered[0], writeMw, origins, txId, options.retry);
2530
+ } else {
2531
+ await executeSingleWrite(rendered[0], options.retry);
2532
+ }
2326
2533
  } else {
2327
2534
  await executeReferentialTransaction(
2328
2535
  rendered,
@@ -2333,23 +2540,116 @@ async function executeCompiledWriteOp(ops, options) {
2333
2540
  idempotencyItems,
2334
2541
  checkItems,
2335
2542
  label,
2336
- modelBySignature
2543
+ modelBySignature,
2544
+ options.retry,
2545
+ writeMw.active ? { runtime: writeMw, origins, transaction: txId } : void 0
2337
2546
  );
2338
2547
  }
2548
+ if (writeMw.active) {
2549
+ for (let i = writeCtxs.length - 1; i >= 0; i--) {
2550
+ await writeMw.runWriteAfter(writeCtxs[i], {});
2551
+ }
2552
+ }
2339
2553
  const readBacks = await Promise.all(
2340
2554
  ops.map((op, i) => readBackForCompiledOp(op, rendered[i]))
2341
2555
  );
2342
2556
  return { readBacks };
2343
2557
  }
2558
+ async function persistSingleWriteWithHooks(w, runtime, origins, transaction, retry) {
2559
+ const options = w.condition !== void 0 || retry !== void 0 ? {
2560
+ ...w.condition !== void 0 ? { condition: w.condition } : {},
2561
+ ...retry !== void 0 ? { retry } : {}
2562
+ } : void 0;
2563
+ let item;
2564
+ if (w.operation === "put") {
2565
+ item = { Put: buildPutInput(w.modelClass, w.item ?? {}, options) };
2566
+ } else if (w.operation === "update") {
2567
+ item = { Update: buildUpdateInput(w.modelClass, w.key, w.changes ?? {}, options) };
2568
+ } else {
2569
+ item = { Delete: buildDeleteInput(w.modelClass, w.key, options) };
2570
+ }
2571
+ const persistCtx = runtime.persistCtx([item], origins, transaction);
2572
+ await runtime.runPersist(persistCtx, async (items) => {
2573
+ const sent = items[0];
2574
+ const sendOpts = retry !== void 0 ? { retry } : void 0;
2575
+ const exec = ClientManager.getExecutor();
2576
+ if ("Put" in sent) return exec.put(sent.Put, sendOpts);
2577
+ if ("Update" in sent) return exec.update(sent.Update, sendOpts);
2578
+ if ("Delete" in sent) return exec.delete(sent.Delete, sendOpts);
2579
+ throw new Error("graphddb middleware: a single-op persist received a ConditionCheck.");
2580
+ });
2581
+ }
2582
+ async function runEnvelopeW1(op, runtime, transaction, writeCtxs) {
2583
+ const kind = op.fragment.op.operation;
2584
+ const keyFields = new Set(op.fragment.keyFields);
2585
+ const input = {};
2586
+ if (kind === "put") {
2587
+ input.item = { ...op.params };
2588
+ } else {
2589
+ const key = {};
2590
+ const changes = {};
2591
+ for (const [k, v] of Object.entries(op.params)) {
2592
+ if (keyFields.has(k)) key[k] = v;
2593
+ else changes[k] = v;
2594
+ }
2595
+ input.key = key;
2596
+ input.changes = changes;
2597
+ }
2598
+ const modelClass = op.fragment.op.entity.modelClass;
2599
+ const ctx = runtime.writeCtx(kind, ctxModelFor(modelClass), input, transaction);
2600
+ await runtime.runWriteBefore(ctx);
2601
+ writeCtxs.push(ctx);
2602
+ if (ctx.kind !== kind) {
2603
+ return { op, rewritten: renderRewrittenWrite(modelClass, ctx) };
2604
+ }
2605
+ const merged = ctx.kind === "put" ? { ...ctx.input.item ?? {} } : { ...ctx.input.key ?? {}, ...ctx.input.changes ?? {} };
2606
+ return { op: { ...op, params: merged } };
2607
+ }
2608
+ function renderRewrittenWrite(modelClass, ctx) {
2609
+ const modelStatic = modelClass.asModel();
2610
+ const condition = ctx.input.options?.condition;
2611
+ const base = {
2612
+ modelClass,
2613
+ modelStatic,
2614
+ ...condition !== void 0 ? { condition } : {}
2615
+ };
2616
+ if (ctx.kind === "put") {
2617
+ const item = ctx.input.item ?? {};
2618
+ return { ...base, operation: "put", key: item, item };
2619
+ }
2620
+ if (ctx.kind === "update") {
2621
+ return {
2622
+ ...base,
2623
+ operation: "update",
2624
+ key: ctx.input.key ?? {},
2625
+ changes: ctx.input.changes ?? {}
2626
+ };
2627
+ }
2628
+ return { ...base, operation: "delete", key: ctx.input.key ?? {} };
2629
+ }
2630
+ function rewrittenCompiledOp(write) {
2631
+ return {
2632
+ write,
2633
+ edgeItems: [],
2634
+ updateItems: [],
2635
+ guardItems: [],
2636
+ outboxItems: [],
2637
+ idempotencyItems: [],
2638
+ checkItems: [],
2639
+ modelBySignature: /* @__PURE__ */ new Map(),
2640
+ hasDerivedEffects: false
2641
+ };
2642
+ }
2344
2643
  async function executeCompiledParallelWrites(ops, options) {
2345
2644
  const label = options.label;
2346
2645
  const results = new Array(ops.length);
2347
2646
  const renderedOps = ops.map((op, i) => renderCompiledOp(op, `${label} (op #${i})`));
2348
2647
  const coalescibleIdx = [];
2349
2648
  const individualIdx = [];
2649
+ const hooksActive = buildWriteRuntime(options.context).active;
2350
2650
  renderedOps.forEach((r, i) => {
2351
2651
  const w = r.write;
2352
- if (!r.hasDerivedEffects && w.condition === void 0 && (w.operation === "put" || w.operation === "delete")) {
2652
+ if (!hooksActive && !r.hasDerivedEffects && w.condition === void 0 && (w.operation === "put" || w.operation === "delete")) {
2353
2653
  coalescibleIdx.push(i);
2354
2654
  } else {
2355
2655
  individualIdx.push(i);
@@ -2380,7 +2680,9 @@ async function executeCompiledParallelWrites(ops, options) {
2380
2680
  ...individualIdx.map(async (i) => {
2381
2681
  try {
2382
2682
  const { readBacks } = await executeCompiledWriteOp([ops[i]], {
2383
- label: `${label} (op #${i})`
2683
+ label: `${label} (op #${i})`,
2684
+ ...options.retry !== void 0 ? { retry: options.retry } : {},
2685
+ ...options.context !== void 0 ? { context: options.context } : {}
2384
2686
  });
2385
2687
  results[i] = { ok: true, value: readBacks[0] };
2386
2688
  } catch (e) {
@@ -2438,14 +2740,16 @@ async function executeReadRoute(alias, route) {
2438
2740
  if (isQuery) {
2439
2741
  return executeQuery(modelClass, route.key, route.select, {
2440
2742
  consistentRead: opt.consistentRead,
2441
- maxDepth: opt.maxDepth
2743
+ maxDepth: opt.maxDepth,
2744
+ context: opt.context
2442
2745
  });
2443
2746
  }
2444
2747
  return executeList(modelClass, route.key, route.select, {
2445
2748
  limit: opt.limit,
2446
2749
  after: opt.after,
2447
2750
  order: opt.order,
2448
- filter: opt.filter
2751
+ filter: opt.filter,
2752
+ context: opt.context
2449
2753
  });
2450
2754
  }
2451
2755
  var INTENT_KEYS = ["create", "update", "remove"];
@@ -2459,9 +2763,9 @@ async function executeWriteEnvelope(envelope, options = {}) {
2459
2763
  }
2460
2764
  const ops = aliases.flatMap((alias) => normalizeWriteOp(alias, envelope[alias]));
2461
2765
  if (mode === "transaction") {
2462
- return executeTransactionMode(ops);
2766
+ return executeTransactionMode(ops, options.retry, options.context);
2463
2767
  }
2464
- return executeParallelMode(ops);
2768
+ return executeParallelMode(ops, options.retry, options.context);
2465
2769
  }
2466
2770
  function normalizeWriteOp(alias, d) {
2467
2771
  if (d === null || typeof d !== "object") {
@@ -2534,10 +2838,12 @@ function mapToRefs($, fields) {
2534
2838
  for (const f of fields) out[f] = $[f];
2535
2839
  return out;
2536
2840
  }
2537
- async function executeTransactionMode(ops) {
2841
+ async function executeTransactionMode(ops, retry, context) {
2538
2842
  const compiled = ops.map(compileWriteOp);
2539
2843
  const { readBacks } = await executeCompiledWriteOp(compiled, {
2540
- label: "DDBModel.mutate"
2844
+ label: "DDBModel.mutate",
2845
+ ...retry !== void 0 ? { retry } : {},
2846
+ ...context !== void 0 ? { context } : {}
2541
2847
  });
2542
2848
  const out = {};
2543
2849
  ops.forEach((op, i) => {
@@ -2552,10 +2858,12 @@ async function executeTransactionMode(ops) {
2552
2858
  });
2553
2859
  return out;
2554
2860
  }
2555
- async function executeParallelMode(ops) {
2861
+ async function executeParallelMode(ops, retry, context) {
2556
2862
  const compiled = ops.map(compileWriteOp);
2557
2863
  const settled = await executeCompiledParallelWrites(compiled, {
2558
- label: "DDBModel.mutate"
2864
+ label: "DDBModel.mutate",
2865
+ ...retry !== void 0 ? { retry } : {},
2866
+ ...context !== void 0 ? { context } : {}
2559
2867
  });
2560
2868
  const out = {};
2561
2869
  ops.forEach((op, i) => {
@@ -2587,14 +2895,64 @@ function resolveModelStatic(ref, alias) {
2587
2895
 
2588
2896
  // src/model/DDBModel.ts
2589
2897
  var DDBModel = class {
2898
+ /**
2899
+ * Register the `DynamoDBClient` GraphDDB sends through.
2900
+ *
2901
+ * IMPORTANT (issue #111): GraphDDB now owns single-op throttle / transient-error
2902
+ * retry (see {@link setRetryPolicy}), so a client left at the AWS SDK default
2903
+ * `maxAttempts: 3` retries MULTIPLICATIVELY underneath the library. Construct the
2904
+ * client with `maxAttempts: 1` so the library is the single source of truth for
2905
+ * retry — e.g. `new DynamoDBClient({ maxAttempts: 1 })`. GraphDDB does NOT mutate
2906
+ * a client you pass in.
2907
+ */
2590
2908
  static setClient(client) {
2591
2909
  ClientManager.setClient(client);
2592
2910
  }
2593
2911
  static setTableMapping(mapping) {
2594
2912
  TableMapping.set(mapping);
2595
2913
  }
2596
- static async transaction(fn) {
2597
- await executeTransaction(fn);
2914
+ /**
2915
+ * Configure the global single-op retry policy (issue #111) — exponential backoff
2916
+ * + jitter with a bounded attempt cap, applied to every GetItem / Query / Put /
2917
+ * Update / Delete / relation-fan-out send and to `TransactWriteItems`. Mirrors
2918
+ * {@link setClient} / {@link setTableMapping}. A sensible default is ALWAYS on; call
2919
+ * this only to tune it (or pass `null` to restore the default). A per-call
2920
+ * `retry?: RetryPolicy | false` on an operation's options overrides this globally
2921
+ * for that call (`false` disables retry).
2922
+ */
2923
+ static setRetryPolicy(policy) {
2924
+ ClientManager.setRetryPolicy(policy);
2925
+ }
2926
+ /**
2927
+ * Register a {@link Middleware} (issue #50; read path #138, write path #139).
2928
+ * One registered object may carry read and/or write hooks.
2929
+ *
2930
+ * - **Read** R1–R5 — request entry (`read.before`), each physical op before send
2931
+ * / after raw fetch (`read.op.before` / `read.op.afterFetch`, INCLUDING every
2932
+ * relation fan-out fetch), the final hydrated result (`read.afterFetch`), and on
2933
+ * error (`read.op.onError` / `read.onError`).
2934
+ * - **Write** W1–W5 — the logical write before effect derivation (`write.before`,
2935
+ * may mutate `ctx.input` / `ctx.kind` incl. a delete→update rewrite), after commit
2936
+ * (`write.after`), the physical persist on the fully composed batch
2937
+ * (`write.persist.before` / `write.persist.after`, fires ONCE per atomic
2938
+ * transaction), and on error (`write.persist.onError` / `write.onError`).
2939
+ *
2940
+ * Hooks may observe, mutate `ctx`, `throw` to cancel, or (on `onError`) recover by
2941
+ * returning a value; the library imposes no semantics. Ordering: `before*` runs
2942
+ * first-registered-first (FIFO); `after*` / `onError` run last-registered-first
2943
+ * (LIFO). Mirrors {@link setRetryPolicy} — a host-runtime global stored on
2944
+ * {@link ClientManager}; never serialized (the bridge #48 is unaffected). Per-call
2945
+ * host context flows in via the read's / write's `{ context }` option.
2946
+ */
2947
+ static use(mw) {
2948
+ ClientManager.use(mw);
2949
+ }
2950
+ /** Remove every registered middleware (read AND write; teardown / tests). See {@link use}. */
2951
+ static clearMiddleware() {
2952
+ ClientManager.clearMiddleware();
2953
+ }
2954
+ static async transaction(fn, options) {
2955
+ await executeTransaction(fn, options);
2598
2956
  }
2599
2957
  /**
2600
2958
  * In-process multi-route READ (issue #101 — the unified envelope). Each alias
@@ -2609,8 +2967,20 @@ var DDBModel = class {
2609
2967
  static mutate(envelope, options) {
2610
2968
  return executeWriteEnvelope(envelope, options);
2611
2969
  }
2612
- static async batchGet(requests) {
2613
- return executeBatchGet(requests);
2970
+ /**
2971
+ * Multi-key, multi-model batch read. Each {@link BatchGetRequest} contributes its
2972
+ * keys to a chunked `BatchGetItem` per physical table; results are re-grouped by
2973
+ * model on the returned {@link BatchGetResult}.
2974
+ *
2975
+ * Fires the read hooks (issue #142): request-level R1/R4/R5 with kind
2976
+ * `'batchGet'`, and op-level R2/R3/R5 for each underlying physical `BatchGetItem`
2977
+ * op. The optional `{ context }` is exposed to every hook as `ctx.context` and
2978
+ * threaded UNCHANGED to every op (absent ⇒ `{}`), so a global hook (e.g. a
2979
+ * tenant-scope R2 FilterExpression) applies across the whole batch. With no
2980
+ * middleware registered the read path is unchanged (zero-overhead fast path).
2981
+ */
2982
+ static async batchGet(requests, options) {
2983
+ return executeBatchGet(requests, options);
2614
2984
  }
2615
2985
  static async batchWrite(requests) {
2616
2986
  return executeBatchWrite(requests);