effect-dynamodb 1.32.0 → 1.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/Entity.d.ts +49 -2
  2. package/dist/Entity.d.ts.map +1 -1
  3. package/dist/Entity.js +291 -55
  4. package/dist/Entity.js.map +1 -1
  5. package/dist/EventStore.d.ts +255 -12
  6. package/dist/EventStore.d.ts.map +1 -1
  7. package/dist/EventStore.js +396 -54
  8. package/dist/EventStore.js.map +1 -1
  9. package/dist/Transaction.d.ts +18 -4
  10. package/dist/Transaction.d.ts.map +1 -1
  11. package/dist/Transaction.js +23 -3
  12. package/dist/Transaction.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/internal/AttributeValueEquals.d.ts +10 -0
  18. package/dist/internal/AttributeValueEquals.d.ts.map +1 -0
  19. package/dist/internal/AttributeValueEquals.js +47 -0
  20. package/dist/internal/AttributeValueEquals.js.map +1 -0
  21. package/dist/internal/BoundCrud.d.ts +8 -0
  22. package/dist/internal/BoundCrud.d.ts.map +1 -1
  23. package/dist/internal/BoundCrud.js.map +1 -1
  24. package/dist/internal/TransactPlan.d.ts +77 -0
  25. package/dist/internal/TransactPlan.d.ts.map +1 -0
  26. package/dist/internal/TransactPlan.js +135 -0
  27. package/dist/internal/TransactPlan.js.map +1 -0
  28. package/dist/internal/TransactWriteOps.d.ts +140 -15
  29. package/dist/internal/TransactWriteOps.d.ts.map +1 -1
  30. package/dist/internal/TransactWriteOps.js +154 -7
  31. package/dist/internal/TransactWriteOps.js.map +1 -1
  32. package/dist/internal/TransactableOps.d.ts +26 -6
  33. package/dist/internal/TransactableOps.d.ts.map +1 -1
  34. package/dist/internal/TransactableOps.js +33 -7
  35. package/dist/internal/TransactableOps.js.map +1 -1
  36. package/package.json +2 -2
package/dist/Entity.js CHANGED
@@ -18,6 +18,7 @@ import { normalizeGsiConfig } from "@effect-dynamodb/schema/KeyComposer.js";
18
18
  import { deriveSourceText } from "@effect-dynamodb/schema/VectorIndex.js";
19
19
  import { Cause, Context, Crypto, Data, DateTime, Effect, Equal, Option, Schema, Stream, } from "effect";
20
20
  import { DynamoClient } from "./DynamoClient.js";
21
+ import { attributeValueEquals } from "./internal/AttributeValueEquals.js";
21
22
  import { isBoundOp, makeBoundAppend, makeBoundDelete, makeBoundGet, makeBoundPut, makeBoundUpdate, } from "./internal/BoundCrud.js";
22
23
  import { BoundQueryImpl, entityNaming, } from "./internal/BoundQuery.js";
23
24
  import { compileExpr, createConditionOps, emptyPartProblem, isExpr, nonEmptyCondition, parseShorthand, parseSimpleShorthand, toExpr, } from "./internal/Expr.js";
@@ -26,6 +27,7 @@ import { BASE_DELAY_MS, MAX_RETRIES } from "./internal/Hydrate.js";
26
27
  import { decodeImage, imageEntityType, ownRowsRawRow, packAttributeValueSync, packedExprRefusal, packedProjectionRefusal, packItemSync, unpackRow, withRegistry, } from "./internal/Packed.js";
27
28
  import { compilePath, createPathBuilder } from "./internal/PathBuilder.js";
28
29
  import { freshIncarnationToken, generateTimestampPrimitive, INCARNATION_TOKEN, isProjectionTombstone, overTombstone, UNSENTINELED_DEFAULTS, } from "./internal/TransactableOps.js";
30
+ import { planWrite } from "./internal/TransactPlan.js";
29
31
  import { batchEntries, diagnosedValidationError, diagnoseFailure, keyAttributesOf, targetsFor, transactItemTarget, } from "./internal/WriteDiagnosis.js";
30
32
  import { decodeSparseFields, encodeSparseFields, fromAttributeMap, toAttributeMap, toAttributeValue, } from "./Marshaller.js";
31
33
  import * as Query from "./Query.js";
@@ -138,46 +140,6 @@ class MissingForCreate extends Data.TaggedError("MissingForCreate") {
138
140
  }
139
141
  /** @internal Re-exported for tests: what DynamoDB counts against an expression's 300 operators. */
140
142
  export { countOperators };
141
- const bytesKey = (bytes) => Array.from(bytes).join(",");
142
- /** Structural equality of two marshalled values (sets compare as sets). */
143
- const attributeValueEquals = (a, b) => {
144
- if (a === undefined || b === undefined)
145
- return a === b;
146
- const [kind] = Object.keys(a);
147
- if (kind === undefined || Object.keys(b)[0] !== kind)
148
- return false;
149
- const x = a[kind];
150
- const y = b[kind];
151
- switch (kind) {
152
- case "L": {
153
- const xs = x;
154
- const ys = y;
155
- return xs.length === ys.length && xs.every((v, i) => attributeValueEquals(v, ys[i]));
156
- }
157
- case "M": {
158
- const xm = x;
159
- const ym = y;
160
- const keys = Object.keys(xm);
161
- return (keys.length === Object.keys(ym).length &&
162
- keys.every((k) => attributeValueEquals(xm[k], ym[k])));
163
- }
164
- case "SS":
165
- case "NS": {
166
- const xs = new Set(x);
167
- const ys = y;
168
- return xs.size === new Set(ys).size && ys.every((v) => xs.has(v));
169
- }
170
- case "B":
171
- return bytesKey(x) === bytesKey(y);
172
- case "BS": {
173
- const xs = new Set(x.map(bytesKey));
174
- const ys = y.map(bytesKey);
175
- return xs.size === new Set(ys).size && ys.every((v) => xs.has(v));
176
- }
177
- default:
178
- return x === y;
179
- }
180
- };
181
143
  /**
182
144
  * The top-level attributes an UpdateExpression writes (SET / REMOVE / ADD /
183
145
  * DELETE targets), resolved through its attribute names.
@@ -3900,22 +3862,29 @@ const makeImpl = (config, precomputedData) => {
3900
3862
  // stored bytes (they depend on the codec and threshold it was written
3901
3863
  // under): an unread write reads it and compares the unpacked JSON
3902
3864
  // (#147). The write still requires the item (`attribute_exists`), and
3903
- // no library write changes an immutable value in between.
3865
+ // no library write changes an immutable value in between. A missing
3866
+ // item takes the path the write's rejection would have taken — create
3867
+ // it, or fail — without sending a write that needs the item: an op that
3868
+ // read its row missing writes nothing that requires it, which is what
3869
+ // `transactWrite`'s planner relies on.
3904
3870
  if (plainWrite && immutables.some((asserted) => asserted.packed)) {
3905
3871
  const stored = yield* client.getItem({
3906
3872
  TableName: tableName,
3907
3873
  Key: marshalledKey,
3908
3874
  ConsistentRead: true,
3909
3875
  });
3910
- if (stored.Item !== undefined) {
3911
- const packedChange = yield* packedImmutableMismatch(immutables, stored.Item);
3912
- if (packedChange !== undefined) {
3913
- return yield* new ValidationError({
3914
- entityType,
3915
- operation: "update",
3916
- cause: packedChange,
3917
- });
3918
- }
3876
+ if (stored.Item === undefined) {
3877
+ return yield* createsOnMissing
3878
+ ? new MissingForCreate({ input: uState.updates })
3879
+ : missingItem();
3880
+ }
3881
+ const packedChange = yield* packedImmutableMismatch(immutables, stored.Item);
3882
+ if (packedChange !== undefined) {
3883
+ return yield* new ValidationError({
3884
+ entityType,
3885
+ operation: "update",
3886
+ cause: packedChange,
3887
+ });
3919
3888
  }
3920
3889
  }
3921
3890
  // Build UpdateExpression
@@ -4293,9 +4262,12 @@ const makeImpl = (config, precomputedData) => {
4293
4262
  // Sparse-map .clearMap — Get-then-Update helper. Reads the current
4294
4263
  // item with a consistent read to discover which `<prefix>#*` attrs
4295
4264
  // exist, then folds the resulting REMOVEs into this same UpdateItem.
4296
- // The version CAS (when configured) provides atomicity; non-versioned
4297
- // entities are best-effort (a concurrent writer can add a new bucket
4298
- // between the read and the update — that bucket survives).
4265
+ // On a versioned entity the update is conditioned on the version that
4266
+ // read found (`casRead`), so a writer adding a bucket in between makes
4267
+ // it fail rather than survive the clear; a stale `expectedVersion` is
4268
+ // refused before anything is sent. Non-versioned entities are
4269
+ // best-effort (a concurrent writer can add a new bucket between the read
4270
+ // and the update — that bucket survives).
4299
4271
  if (uState.sparseClearFields && uState.sparseClearFields.length > 0) {
4300
4272
  for (const field of uState.sparseClearFields) {
4301
4273
  if (!hasSparseFields || !(field in sparseFields)) {
@@ -4313,6 +4285,20 @@ const makeImpl = (config, precomputedData) => {
4313
4285
  ConsistentRead: true,
4314
4286
  });
4315
4287
  if (clearGetResult.Item) {
4288
+ if (systemFields.version && casRead === undefined && retainSnapshot === undefined) {
4289
+ yield* checkVersion(clearGetResult.Item, "update");
4290
+ const clearedRaw = fromAttributeMap(clearGetResult.Item);
4291
+ const version = clearedRaw[systemFields.version] ?? 0;
4292
+ if (evExpected !== undefined && version !== evExpected) {
4293
+ return yield* new OptimisticLockError({
4294
+ entityType,
4295
+ key: encodedKey,
4296
+ expectedVersion: evExpected,
4297
+ actualVersion: version,
4298
+ });
4299
+ }
4300
+ casRead = { raw: clearGetResult.Item, version };
4301
+ }
4316
4302
  const clearItemKeys = Object.keys(clearGetResult.Item);
4317
4303
  for (const field of uState.sparseClearFields) {
4318
4304
  const sparse = sparseFields[field];
@@ -4556,6 +4542,220 @@ const makeImpl = (config, precomputedData) => {
4556
4542
  return yield* updateResult(requested.returnValues ?? "allNew", undefined, item, new Set(Object.keys(item)), mode, key);
4557
4543
  })));
4558
4544
  const update = (key) => new EntityUpdateImpl((mode, requested) => updateOrCreate(key, mode, requested), emptyUpdateState, self, key);
4545
+ /**
4546
+ * Run one of this entity's write ops with its write recorded instead of
4547
+ * sent (`planWrite`), and say how to read a cancellation of what it would
4548
+ * have written. The op is the standalone op itself, so the plan carries
4549
+ * every read it makes, every item it writes and every guard it puts on them
4550
+ * — see `internal/TransactPlan.ts`.
4551
+ *
4552
+ * The verdict follows a guarded put's (`_planPut`): a cancelled sentinel
4553
+ * reservation is the `UniqueConstraintViolation` the standalone op reports;
4554
+ * a cancelled main item is the caller's condition when there is one
4555
+ * (`TransactionCancelled`); anything else is a lost race — the row changed
4556
+ * between the read and the transaction — and `transactWrite` plans it again
4557
+ * from a fresh read.
4558
+ *
4559
+ * `payload` is what the caller asked to write. With the op's own read of
4560
+ * the row, it lets a reservation say which values it reserves.
4561
+ */
4562
+ const planned = (operation, key,
4563
+ /** The op itself — a factory, because a missing row means running it again. */
4564
+ op, request) => Effect.gen(function* () {
4565
+ const encodedKey = yield* encodeKey(key, `${operation}.decodeKey`);
4566
+ const primaryKey = composePrimaryKey(encodedKey);
4567
+ const ownKey = toAttributeMap(primaryKey);
4568
+ const recorded = yield* planWrite(operation, op(), ownKey);
4569
+ let { items, ownRow } = recorded;
4570
+ // An op that wrote without reading its row relies on its write's own
4571
+ // condition to find the row missing — a standalone `update` then
4572
+ // creates it (or fails `ItemNotFound`), `patch` fails
4573
+ // `ConditionalCheckFailed`. A transaction cannot follow that fallback,
4574
+ // so the row is read here, and a missing row is answered to the op as
4575
+ // its write would have been answered: it takes the same path.
4576
+ if (!recorded.readOwnRow && items.length > 0) {
4577
+ const client = yield* DynamoClient;
4578
+ const { name: tableName } = yield* tableTag;
4579
+ const read = yield* client.getItem({
4580
+ TableName: tableName,
4581
+ Key: ownKey,
4582
+ ConsistentRead: true,
4583
+ });
4584
+ if (read.Item === undefined) {
4585
+ ;
4586
+ ({ items, ownRow } = yield* planWrite(operation, op(), ownKey, { rowMissing: true }));
4587
+ }
4588
+ else {
4589
+ ownRow = read.Item;
4590
+ // A pinned version the row no longer has is refused before sending,
4591
+ // as the read-first paths do — not retried as if it were a race.
4592
+ if (request.expectedVersion !== undefined && systemFields.version) {
4593
+ const stored = fromAttributeMap(read.Item)[systemFields.version];
4594
+ const actual = typeof stored === "number" ? stored : 0;
4595
+ if (actual !== request.expectedVersion) {
4596
+ return yield* new OptimisticLockError({
4597
+ entityType,
4598
+ key: encodedKey,
4599
+ expectedVersion: request.expectedVersion,
4600
+ actualVersion: actual,
4601
+ });
4602
+ }
4603
+ }
4604
+ }
4605
+ }
4606
+ const pkField = config.indexes.primary.pk.field;
4607
+ const skField = config.indexes.primary.sk.field;
4608
+ const isOwn = (row) => {
4609
+ if (row === undefined)
4610
+ return false;
4611
+ const plain = fromAttributeMap(row);
4612
+ return plain[pkField] === primaryKey[pkField] && plain[skField] === primaryKey[skField];
4613
+ };
4614
+ // What the row looks like after the write, in domain names: the main
4615
+ // Put's item when the op writes the whole row, else the row it read
4616
+ // overlaid with the payload.
4617
+ const mainPut = items.find((i) => i.Put !== undefined && isOwn(i.Put.Item))?.Put?.Item;
4618
+ const read = ownRow === undefined ? {} : toDomainView(fromAttributeMap(ownRow));
4619
+ const after = mainPut !== undefined
4620
+ ? toDomainView(fromAttributeMap(mainPut))
4621
+ : { ...read, ...request.payload };
4622
+ if (mainPut === undefined) {
4623
+ for (const [field, delta] of Object.entries(request.deltas ?? {})) {
4624
+ const base = read[field];
4625
+ after[field] = (typeof base === "number" ? base : 0) + delta;
4626
+ }
4627
+ }
4628
+ const sentinelPrefix = `${entityType}._unique.`;
4629
+ const versionPrefix = DynamoSchema.composeVersionKeyPrefix(schema, entityType);
4630
+ const roles = items.map((item) => {
4631
+ if (isOwn(item.Put?.Item) || isOwn(item.Update?.Key) || isOwn(item.Delete?.Key)) {
4632
+ return { _tag: "main" };
4633
+ }
4634
+ // A retain snapshot: in the row's partition, at a version key.
4635
+ const putRow = item.Put?.Item === undefined ? undefined : fromAttributeMap(item.Put.Item);
4636
+ const putSk = putRow?.[skField];
4637
+ if (putRow !== undefined &&
4638
+ putRow[pkField] === primaryKey[pkField] &&
4639
+ typeof putSk === "string" &&
4640
+ putSk.startsWith(versionPrefix)) {
4641
+ return { _tag: "snapshot", version: storedVersionOf(item.Put?.Item) ?? 0 };
4642
+ }
4643
+ const tag = item.Put?.Item?.__edd_e__?.S;
4644
+ if (item.Put === undefined ||
4645
+ item.Put.ConditionExpression === undefined ||
4646
+ tag === undefined ||
4647
+ !tag.startsWith(sentinelPrefix)) {
4648
+ return { _tag: "side" };
4649
+ }
4650
+ const constraintName = tag.slice(sentinelPrefix.length);
4651
+ const constraintDef = config.unique?.[constraintName];
4652
+ const reserved = constraintDef === undefined
4653
+ ? undefined
4654
+ : composeUniqueSentinel(schema, entityType, constraintName, constraintDef, after);
4655
+ const putKey = fromAttributeMap(item.Put.Item ?? {});
4656
+ // Only values that recompose THIS sentinel are reported — never a guess.
4657
+ const fields = reserved !== undefined &&
4658
+ reserved.key.pk === putKey[pkField] &&
4659
+ reserved.key.sk === putKey[skField]
4660
+ ? reserved.fieldsRecord
4661
+ : {};
4662
+ return {
4663
+ _tag: "reserve",
4664
+ error: new UniqueConstraintViolation({ entityType, constraint: constraintName, fields }),
4665
+ };
4666
+ });
4667
+ // How a rejected main item reads, as a guarded put's does: compare the
4668
+ // stored item the cancellation returns (`ALL_OLD`) with the row the
4669
+ // plan was built from. Versioned: its version and incarnation.
4670
+ // Otherwise: every attribute the main item's condition names — the
4671
+ // op's own guard inputs are all among them, so if none changed the
4672
+ // guard held, and only the caller's condition can have rejected it.
4673
+ const mainItem = items[roles.findIndex((role) => role._tag === "main")];
4674
+ const guarded = mainItem?.Update ?? mainItem?.Put ?? mainItem?.Delete;
4675
+ const conditionAttrs = [
4676
+ ...new Set((guarded?.ConditionExpression?.match(/#[A-Za-z0-9_]+/g) ?? []).flatMap((token) => {
4677
+ const attr = guarded?.ExpressionAttributeNames?.[token];
4678
+ return attr === undefined ? [] : [attr];
4679
+ })),
4680
+ ];
4681
+ const readVersion = storedVersionOf(ownRow);
4682
+ const changedFrom = (stored) => conditionAttrs.filter((attr) => !attributeValueEquals(stored[attr], ownRow?.[attr]));
4683
+ const raced = (stored) => systemFields.version
4684
+ ? storedVersionOf(stored) !== readVersion ||
4685
+ (incarnationAttr !== undefined &&
4686
+ !attributeValueEquals(stored[incarnationAttr], ownRow?.[incarnationAttr]))
4687
+ : changedFrom(stored).length > 0;
4688
+ /** A lost race: the error once retries run out, as a guarded put reports it. */
4689
+ const lostRace = (stored) => ({
4690
+ _tag: "Retry",
4691
+ stored,
4692
+ error: systemFields.version
4693
+ ? new OptimisticLockError({
4694
+ entityType,
4695
+ key: encodedKey,
4696
+ expectedVersion: readVersion ?? 0,
4697
+ actualVersion: storedVersionOf(stored) ?? -1,
4698
+ })
4699
+ : new ConcurrentModification({
4700
+ entityType,
4701
+ key: encodedKey,
4702
+ attributes: stored === undefined ? [] : changedFrom(stored).map(domainNameOf),
4703
+ current: Option.none(),
4704
+ }),
4705
+ });
4706
+ return {
4707
+ items,
4708
+ // A taken unique value fails; a snapshot the row's own history
4709
+ // already holds fails (`historyConflict`, as standalone — re-reading
4710
+ // will not change it); the caller's own condition rejecting an
4711
+ // unchanged row is a condition. Everything else is a race, retried —
4712
+ // and every attempt plans again from fresh reads, so a row now
4713
+ // missing takes the op's missing-row path and a pinned version the
4714
+ // row no longer has fails `OptimisticLockError`, as standalone.
4715
+ verdict: (reasons) => {
4716
+ let main;
4717
+ let sideFailed = false;
4718
+ for (const [i, role] of roles.entries()) {
4719
+ const reason = reasons[i];
4720
+ if (reason?.Code !== "ConditionalCheckFailed")
4721
+ continue;
4722
+ switch (role._tag) {
4723
+ case "reserve":
4724
+ return { _tag: "Fail", error: role.error };
4725
+ case "snapshot":
4726
+ return ownRow === undefined
4727
+ ? lostRace(undefined)
4728
+ : { _tag: "Fail", error: historyConflict(role.version, operation) };
4729
+ case "main":
4730
+ main =
4731
+ reason.Item === undefined
4732
+ ? "absent"
4733
+ : reason.Item;
4734
+ break;
4735
+ case "side":
4736
+ sideFailed = true;
4737
+ }
4738
+ }
4739
+ if (main === undefined)
4740
+ return sideFailed ? lostRace(undefined) : undefined;
4741
+ if (main === "absent")
4742
+ return lostRace(undefined);
4743
+ if (raced(main) || !request.callerCondition)
4744
+ return lostRace(main);
4745
+ return { _tag: "Condition" };
4746
+ },
4747
+ };
4748
+ });
4749
+ /** Compile an update for `Transaction.transactWrite` — see `_planUpdate`. */
4750
+ const planUpdate = (key, uState) => planned("transactWrite.update", key, () => updateOrCreate(key, "native", uState), {
4751
+ payload: (uState.updates ?? {}),
4752
+ deltas: {
4753
+ ...uState.add,
4754
+ ...Object.fromEntries(Object.entries(uState.subtract ?? {}).map(([field, by]) => [field, -by])),
4755
+ },
4756
+ callerCondition: uState.condition !== undefined,
4757
+ expectedVersion: uState.expectedVersion,
4758
+ });
4559
4759
  // ---------------------------------------------------------------------------
4560
4760
  // delete operation
4561
4761
  // ---------------------------------------------------------------------------
@@ -4923,6 +5123,8 @@ const makeImpl = (config, precomputedData) => {
4923
5123
  // ---------------------------------------------------------------------------
4924
5124
  // deleteIfExists operation — delete + attribute_exists condition
4925
5125
  // ---------------------------------------------------------------------------
5126
+ /** Compile a delete for `Transaction.transactWrite` — see `_planDelete`. */
5127
+ const planDelete = (key, opts) => planned("transactWrite.delete", key, () => del(key)._builder({ ...opts, returnValues: undefined }), { payload: {}, callerCondition: opts.condition !== undefined });
4926
5128
  const deleteIfExists = (key) => {
4927
5129
  const op = del(key);
4928
5130
  return new EntityDeleteImpl(op._builder, op._entity, op._key, op._condition, op._returnValues, true);
@@ -6533,6 +6735,11 @@ const makeImpl = (config, precomputedData) => {
6533
6735
  _unsentineledDefaults: unsentineledDefaults,
6534
6736
  _planPut: planPut,
6535
6737
  _multiItemWriteFeatures: multiItemWriteFeatures(),
6738
+ _planUpdate: (key, state) => planUpdate(key, state).pipe(
6739
+ // Planning stops at the write, so only what an update raises BEFORE
6740
+ // writing can surface; anything else is a defect (`isPlanUpdateError`).
6741
+ Effect.catch((e) => (isPlanUpdateError(e) ? Effect.fail(e) : Effect.die(e)))),
6742
+ _planDelete: (key, opts) => planDelete(key, opts).pipe(Effect.catch((e) => (isPlanDeleteError(e) ? Effect.fail(e) : Effect.die(e)))),
6536
6743
  _liveRows: liveRows,
6537
6744
  _attachPrototype: attachPrototype,
6538
6745
  _overTombstone: (tableName, item) => Effect.gen(function* () {
@@ -6867,6 +7074,28 @@ export const bind = (entity) => Effect.gen(function* () {
6867
7074
  scanFetch: (...combinators) => provide(Query.execute(applyQuery(entity.scan(), combinators))),
6868
7075
  };
6869
7076
  });
7077
+ const tagged = (tags) => (e) => typeof e === "object" && e !== null && "_tag" in e && tags.has(String(e._tag));
7078
+ const clientErrorTags = [
7079
+ "DynamoError",
7080
+ "ThrottlingError",
7081
+ "DynamoValidationError",
7082
+ "InternalServerError",
7083
+ "ResourceNotFoundError",
7084
+ ];
7085
+ const planDeleteErrorTags = [
7086
+ ...clientErrorTags,
7087
+ "ValidationError",
7088
+ "ConditionalCheckFailed",
7089
+ "ItemNotFound",
7090
+ "TransactionOverflow",
7091
+ ];
7092
+ const isPlanDeleteError = tagged(new Set(planDeleteErrorTags));
7093
+ const isPlanUpdateError = tagged(new Set([
7094
+ ...planDeleteErrorTags,
7095
+ "OptimisticLockError",
7096
+ "RefNotFound",
7097
+ "UniqueConstraintViolation",
7098
+ ]));
6870
7099
  const isEntityOp = (op) => EntityOpTypeId in op;
6871
7100
  const isEntityDelete = (op) => EntityDeleteTypeId in op;
6872
7101
  /**
@@ -6919,13 +7148,15 @@ export const extractTransactable = (op) => {
6919
7148
  };
6920
7149
  }
6921
7150
  if (target._opType === "update") {
6922
- // Every multi-item path (transactions, `Batch.write`, `EventStore`
6923
- // additional items) refuses an update, so no guard is carried here.
7151
+ // `Transaction.transactWrite` plans an update from its whole state
7152
+ // (`_planUpdate`), which applies `patch`'s guard itself; every other
7153
+ // multi-item path refuses an update.
6924
7154
  return {
6925
7155
  opType: "update",
6926
7156
  entity: target._entity,
6927
7157
  key: target._key,
6928
7158
  condition: target._updateState?.condition,
7159
+ updateState: target._updateState,
6929
7160
  };
6930
7161
  }
6931
7162
  }
@@ -6939,6 +7170,11 @@ export const extractTransactable = (op) => {
6939
7170
  condition: withGuard(target._mustExist
6940
7171
  ? { attributeExists: [target._entity.indexes.primary.pk.field] }
6941
7172
  : undefined, target._condition),
7173
+ deleteRequest: {
7174
+ condition: target._condition,
7175
+ mustExist: target._mustExist === true,
7176
+ returnValues: target._returnValues,
7177
+ },
6942
7178
  };
6943
7179
  }
6944
7180
  return undefined;