graphddb 0.2.4 → 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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ChangeCaptureRegistry,
3
3
  resolveModelClass
4
- } from "./chunk-GCPEUPAA.js";
4
+ } from "./chunk-4TYK3AV6.js";
5
5
 
6
6
  // src/cdc/prng.ts
7
7
  var SeededRandom = class {
@@ -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-GCPEUPAA.js";
40
+ } from "./chunk-4TYK3AV6.js";
36
41
 
37
42
  // src/spec/types.ts
38
43
  var SPEC_VERSION = "1.0";
@@ -810,23 +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(
814
- operation,
815
- options.retry !== void 0 ? { retry: options.retry } : void 0
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
816
821
  );
817
- const rawItems = result.items;
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
+ }
818
842
  const hydrated = hydrate(rawItems, select, metadata, options.updatable ?? false);
819
- const cursor = result.lastEvaluatedKey ? encodeCursor(result.lastEvaluatedKey) : null;
843
+ const cursor = lastEvaluatedKey ? encodeCursor(lastEvaluatedKey) : null;
820
844
  return { items: hydrated, rawItems, cursor };
821
845
  }
822
846
  async function executeList(modelClass, key, selectSpec, options = {}) {
823
- const { items, cursor } = await executeListInternal(
824
- modelClass,
825
- key,
826
- selectSpec,
827
- options
828
- );
829
- 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
+ }
830
883
  }
831
884
 
832
885
  // src/planner/relation-planner.ts
@@ -1174,12 +1227,23 @@ async function fetchBelongsToHasOneBatch(rel, rawParentItems, selectSpec, target
1174
1227
  );
1175
1228
  const executor = ClientManager.getExecutor();
1176
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;
1177
1233
  for (const operation of batchOps) {
1178
- const batchResult = await executor.batchGet(
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(
1179
1243
  operation,
1180
1244
  options.retry !== void 0 ? { retry: options.retry } : void 0
1181
- );
1182
- for (const item of batchResult.items) {
1245
+ )).items;
1246
+ for (const item of items) {
1183
1247
  for (const queryKey of queryKeys) {
1184
1248
  if (!matchesQueryKey(item, queryKey)) {
1185
1249
  continue;
@@ -1265,9 +1329,11 @@ async function resolveRelations(parentItem, rawParentItem, select, metadata, opt
1265
1329
  const relations = detectRelationFields(select, metadata);
1266
1330
  const plan2 = options.plan ?? (currentPath === "$" ? buildRelationExecutionPlan(select, metadata) : void 0);
1267
1331
  const opts = plan2 === options.plan ? options : { ...options, plan: plan2 };
1332
+ const basePath = opts.relationPath ?? [];
1268
1333
  await runStaged(relations, currentPath, opts.plan, async (rel) => {
1269
1334
  const selectSpec = relationSpec(select[rel.propertyName]);
1270
1335
  const ownPath = relationResultPath(currentPath, rel);
1336
+ const relPath = [...basePath, rel.propertyName];
1271
1337
  if (rel.type === "hasMany") {
1272
1338
  const targetClass2 = rel.targetFactory();
1273
1339
  const queryKey = buildRelationQueryKey(rel, rawParentItem);
@@ -1279,7 +1345,9 @@ async function resolveRelations(parentItem, rawParentItem, select, metadata, opt
1279
1345
  order,
1280
1346
  filter: selectSpec.filter,
1281
1347
  updatable: opts.updatable,
1282
- retry: opts.retry
1348
+ retry: opts.retry,
1349
+ mw: opts.mw,
1350
+ relationPath: relPath
1283
1351
  });
1284
1352
  let items = listResult.items;
1285
1353
  const rawItems = listResult.rawItems;
@@ -1291,7 +1359,7 @@ async function resolveRelations(parentItem, rawParentItem, select, metadata, opt
1291
1359
  rawItems,
1292
1360
  selectSpec.select,
1293
1361
  targetMeta2,
1294
- opts,
1362
+ { ...opts, relationPath: relPath },
1295
1363
  currentDepth + 1,
1296
1364
  ownPath
1297
1365
  );
@@ -1320,6 +1388,36 @@ async function resolveRelations(parentItem, rawParentItem, select, metadata, opt
1320
1388
 
1321
1389
  // src/operations/query.ts
1322
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) {
1323
1421
  const metadata = MetadataRegistry.get(modelClass);
1324
1422
  const queryFields = Object.keys(key);
1325
1423
  const resolved = resolveKey(queryFields, metadata);
@@ -1353,11 +1451,25 @@ async function executeQuery(modelClass, key, selectSpec, options) {
1353
1451
  )
1354
1452
  );
1355
1453
  const retry = options?.retry;
1356
- const parentPromise = execute(
1357
- operation,
1358
- retry !== void 0 ? { retry } : void 0
1359
- );
1360
- const relationPromise = canParallelizeRelations ? resolveRelations({}, key, select, metadata, { maxDepth, updatable, retry }, 1) : null;
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;
1361
1473
  let result;
1362
1474
  try {
1363
1475
  result = await parentPromise;
@@ -1384,7 +1496,7 @@ async function executeQuery(modelClass, key, selectSpec, options) {
1384
1496
  rawItem,
1385
1497
  select,
1386
1498
  metadata,
1387
- { maxDepth, updatable, retry },
1499
+ { maxDepth, updatable, retry, mw, relationPath: [] },
1388
1500
  1
1389
1501
  );
1390
1502
  if (hydratedItem && updatable) {
@@ -1465,16 +1577,37 @@ function hydrateBatchItems(rawItems, metadata) {
1465
1577
  return merged;
1466
1578
  });
1467
1579
  }
1468
- async function executeBatchGetForTable(tableName, entries) {
1580
+ async function executeBatchGetForTable(tableName, entries, mw, opModel) {
1469
1581
  const groups = /* @__PURE__ */ new Map();
1470
1582
  if (entries.length === 0) {
1471
1583
  return groups;
1472
1584
  }
1473
1585
  const keys = entries.map((entry) => entry.dynamoKey);
1474
- const { items: rawItems } = await ClientManager.getExecutor().batchGet({
1475
- tableName,
1476
- keys
1477
- });
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
+ }
1478
1611
  const entriesByModel = /* @__PURE__ */ new Map();
1479
1612
  for (const entry of entries) {
1480
1613
  const existing = entriesByModel.get(entry.modelClass) ?? [];
@@ -1495,12 +1628,42 @@ async function executeBatchGetForTable(tableName, entries) {
1495
1628
  }
1496
1629
  return groups;
1497
1630
  }
1498
- 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) {
1499
1658
  const entriesByTable = /* @__PURE__ */ new Map();
1659
+ const firstModelClassByTable = /* @__PURE__ */ new Map();
1500
1660
  for (const request of requests) {
1501
1661
  const modelClass = resolveModelClass(request.model);
1502
1662
  const metadata = MetadataRegistry.get(modelClass);
1503
1663
  const tableName = TableMapping.resolve(metadata.tableName);
1664
+ if (!firstModelClassByTable.has(tableName)) {
1665
+ firstModelClassByTable.set(tableName, modelClass);
1666
+ }
1504
1667
  for (const keyObj of request.keys) {
1505
1668
  const entry = {
1506
1669
  modelClass,
@@ -1515,7 +1678,13 @@ async function executeBatchGet(requests) {
1515
1678
  const mergedGroups = /* @__PURE__ */ new Map();
1516
1679
  for (const [, entries] of entriesByTable) {
1517
1680
  const tableName = TableMapping.resolve(entries[0].metadata.tableName);
1518
- 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
+ );
1519
1688
  for (const [modelClass, items] of tableGroups) {
1520
1689
  const existing = mergedGroups.get(modelClass) ?? [];
1521
1690
  mergedGroups.set(modelClass, existing.concat(items));
@@ -2076,7 +2245,7 @@ function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
2076
2245
  return { Put: put };
2077
2246
  });
2078
2247
  }
2079
- async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map(), retry) {
2248
+ async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map(), retry, middleware) {
2080
2249
  const writeItems = writes.map((w) => {
2081
2250
  const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
2082
2251
  let item;
@@ -2104,6 +2273,7 @@ async function executeReferentialTransaction(writes, edgeItems, updateItems, gua
2104
2273
  {
2105
2274
  modelBySignature,
2106
2275
  ...retry !== void 0 ? { retry } : {},
2276
+ ...middleware !== void 0 ? { middleware } : {},
2107
2277
  limitError: (count) => new Error(
2108
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.`
2109
2279
  )
@@ -2317,7 +2487,24 @@ function readBackForCompiledOp(op, write) {
2317
2487
  }
2318
2488
  async function executeCompiledWriteOp(ops, options) {
2319
2489
  const label = options.label;
2320
- 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
+ );
2321
2508
  const rendered = renderedOps.map((r) => r.write);
2322
2509
  const modelBySignature = /* @__PURE__ */ new Map();
2323
2510
  const edgeItems = [];
@@ -2336,8 +2523,13 @@ async function executeCompiledWriteOp(ops, options) {
2336
2523
  checkItems.push(...r.checkItems);
2337
2524
  }
2338
2525
  const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
2526
+ const origins = writeMw.active ? rendered.map((w) => ({ model: ctxModelFor(w.modelClass), kind: w.operation })) : [];
2339
2527
  if (rendered.length === 1 && !hasDerivedEffects) {
2340
- await executeSingleWrite(rendered[0], options.retry);
2528
+ if (writeMw.active) {
2529
+ await persistSingleWriteWithHooks(rendered[0], writeMw, origins, txId, options.retry);
2530
+ } else {
2531
+ await executeSingleWrite(rendered[0], options.retry);
2532
+ }
2341
2533
  } else {
2342
2534
  await executeReferentialTransaction(
2343
2535
  rendered,
@@ -2349,23 +2541,115 @@ async function executeCompiledWriteOp(ops, options) {
2349
2541
  checkItems,
2350
2542
  label,
2351
2543
  modelBySignature,
2352
- options.retry
2544
+ options.retry,
2545
+ writeMw.active ? { runtime: writeMw, origins, transaction: txId } : void 0
2353
2546
  );
2354
2547
  }
2548
+ if (writeMw.active) {
2549
+ for (let i = writeCtxs.length - 1; i >= 0; i--) {
2550
+ await writeMw.runWriteAfter(writeCtxs[i], {});
2551
+ }
2552
+ }
2355
2553
  const readBacks = await Promise.all(
2356
2554
  ops.map((op, i) => readBackForCompiledOp(op, rendered[i]))
2357
2555
  );
2358
2556
  return { readBacks };
2359
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
+ }
2360
2643
  async function executeCompiledParallelWrites(ops, options) {
2361
2644
  const label = options.label;
2362
2645
  const results = new Array(ops.length);
2363
2646
  const renderedOps = ops.map((op, i) => renderCompiledOp(op, `${label} (op #${i})`));
2364
2647
  const coalescibleIdx = [];
2365
2648
  const individualIdx = [];
2649
+ const hooksActive = buildWriteRuntime(options.context).active;
2366
2650
  renderedOps.forEach((r, i) => {
2367
2651
  const w = r.write;
2368
- 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")) {
2369
2653
  coalescibleIdx.push(i);
2370
2654
  } else {
2371
2655
  individualIdx.push(i);
@@ -2397,7 +2681,8 @@ async function executeCompiledParallelWrites(ops, options) {
2397
2681
  try {
2398
2682
  const { readBacks } = await executeCompiledWriteOp([ops[i]], {
2399
2683
  label: `${label} (op #${i})`,
2400
- ...options.retry !== void 0 ? { retry: options.retry } : {}
2684
+ ...options.retry !== void 0 ? { retry: options.retry } : {},
2685
+ ...options.context !== void 0 ? { context: options.context } : {}
2401
2686
  });
2402
2687
  results[i] = { ok: true, value: readBacks[0] };
2403
2688
  } catch (e) {
@@ -2455,14 +2740,16 @@ async function executeReadRoute(alias, route) {
2455
2740
  if (isQuery) {
2456
2741
  return executeQuery(modelClass, route.key, route.select, {
2457
2742
  consistentRead: opt.consistentRead,
2458
- maxDepth: opt.maxDepth
2743
+ maxDepth: opt.maxDepth,
2744
+ context: opt.context
2459
2745
  });
2460
2746
  }
2461
2747
  return executeList(modelClass, route.key, route.select, {
2462
2748
  limit: opt.limit,
2463
2749
  after: opt.after,
2464
2750
  order: opt.order,
2465
- filter: opt.filter
2751
+ filter: opt.filter,
2752
+ context: opt.context
2466
2753
  });
2467
2754
  }
2468
2755
  var INTENT_KEYS = ["create", "update", "remove"];
@@ -2476,9 +2763,9 @@ async function executeWriteEnvelope(envelope, options = {}) {
2476
2763
  }
2477
2764
  const ops = aliases.flatMap((alias) => normalizeWriteOp(alias, envelope[alias]));
2478
2765
  if (mode === "transaction") {
2479
- return executeTransactionMode(ops, options.retry);
2766
+ return executeTransactionMode(ops, options.retry, options.context);
2480
2767
  }
2481
- return executeParallelMode(ops, options.retry);
2768
+ return executeParallelMode(ops, options.retry, options.context);
2482
2769
  }
2483
2770
  function normalizeWriteOp(alias, d) {
2484
2771
  if (d === null || typeof d !== "object") {
@@ -2551,11 +2838,12 @@ function mapToRefs($, fields) {
2551
2838
  for (const f of fields) out[f] = $[f];
2552
2839
  return out;
2553
2840
  }
2554
- async function executeTransactionMode(ops, retry) {
2841
+ async function executeTransactionMode(ops, retry, context) {
2555
2842
  const compiled = ops.map(compileWriteOp);
2556
2843
  const { readBacks } = await executeCompiledWriteOp(compiled, {
2557
2844
  label: "DDBModel.mutate",
2558
- ...retry !== void 0 ? { retry } : {}
2845
+ ...retry !== void 0 ? { retry } : {},
2846
+ ...context !== void 0 ? { context } : {}
2559
2847
  });
2560
2848
  const out = {};
2561
2849
  ops.forEach((op, i) => {
@@ -2570,11 +2858,12 @@ async function executeTransactionMode(ops, retry) {
2570
2858
  });
2571
2859
  return out;
2572
2860
  }
2573
- async function executeParallelMode(ops, retry) {
2861
+ async function executeParallelMode(ops, retry, context) {
2574
2862
  const compiled = ops.map(compileWriteOp);
2575
2863
  const settled = await executeCompiledParallelWrites(compiled, {
2576
2864
  label: "DDBModel.mutate",
2577
- ...retry !== void 0 ? { retry } : {}
2865
+ ...retry !== void 0 ? { retry } : {},
2866
+ ...context !== void 0 ? { context } : {}
2578
2867
  });
2579
2868
  const out = {};
2580
2869
  ops.forEach((op, i) => {
@@ -2634,8 +2923,36 @@ var DDBModel = class {
2634
2923
  static setRetryPolicy(policy) {
2635
2924
  ClientManager.setRetryPolicy(policy);
2636
2925
  }
2637
- static async transaction(fn) {
2638
- await executeTransaction(fn);
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);
2639
2956
  }
2640
2957
  /**
2641
2958
  * In-process multi-route READ (issue #101 — the unified envelope). Each alias
@@ -2650,8 +2967,20 @@ var DDBModel = class {
2650
2967
  static mutate(envelope, options) {
2651
2968
  return executeWriteEnvelope(envelope, options);
2652
2969
  }
2653
- static async batchGet(requests) {
2654
- 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);
2655
2984
  }
2656
2985
  static async batchWrite(requests) {
2657
2986
  return executeBatchWrite(requests);
package/dist/cli.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  buildBridgeBundle,
4
4
  normalizeSelectSpec
5
- } from "./chunk-IQEOJVHI.js";
5
+ } from "./chunk-SGYBE2OV.js";
6
6
  import {
7
7
  MetadataRegistry
8
- } from "./chunk-GCPEUPAA.js";
8
+ } from "./chunk-4TYK3AV6.js";
9
9
 
10
10
  // src/cli.ts
11
11
  import { createRequire } from "module";