Skip to main content

dynamodb_facade/operations/
transactions.rs

1use super::*;
2
3use aws_sdk_dynamodb::types::{
4    TransactWriteItem,
5    builders::{ConditionCheckBuilder, DeleteBuilder, PutBuilder, UpdateBuilder},
6};
7
8/// Builder for a `Put` operation inside a DynamoDB transaction.
9///
10/// Constructed via [`DynamoDBItemTransactOp::transact_put`]. Optionally add a
11/// condition that must hold for the put to succeed, via
12/// [`.condition()`][TransactPutRequest::condition],
13/// [`.exists()`][TransactPutRequest::exists], or
14/// [`.not_exists()`][TransactPutRequest::not_exists]. DynamoDB accepts a
15/// single condition expression per operation, so this can only be called once.
16///
17/// Call [`.build()`][TransactPutRequest::build] to produce a
18/// [`TransactWriteItem`] that can be passed to the SDK's
19/// `transact_write_items()` builder.
20///
21/// # Examples
22///
23/// Atomically create an enrollment and increment the user's enrollment count:
24///
25/// ```no_run
26/// # use dynamodb_facade::test_fixtures::*;
27/// use dynamodb_facade::{DynamoDBItemTransactOp, KeyId, Update};
28///
29/// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
30/// let enrollment = sample_enrollment();
31///
32/// client
33///     .transact_write_items()
34///     .transact_items(enrollment.transact_put().not_exists().build())
35///     .transact_items(
36///         User::transact_update_by_id(
37///             KeyId::pk("user-1"),
38///             Update::init_increment("enrollment_count", 0, 1),
39///         )
40///         .exists()
41///         .build(),
42///     )
43///     .send()
44///     .await?;
45/// # Ok(())
46/// # }
47/// ```
48#[must_use = "builder does nothing until .build() is called"]
49pub struct TransactPutRequest<TD: TableDefinition, T = (), C: ConditionState = NoCondition> {
50    builder: PutBuilder,
51    _marker: PhantomData<(TD, T, C)>,
52}
53
54// -- Common methods (all states) --------------------------------------------
55
56impl<TD: TableDefinition, T, C: ConditionState> TransactPutRequest<TD, T, C> {
57    /// Creates a new `TransactPutRequest` from a raw [`Item`].
58    ///
59    /// Prefer [`DynamoDBItemTransactOp::transact_put`] for typed construction.
60    ///
61    /// # Examples
62    ///
63    /// ```no_run
64    /// # use dynamodb_facade::test_fixtures::*;
65    /// use dynamodb_facade::{TransactPutRequest, DynamoDBItemTransactOp, KeyId, Update};
66    ///
67    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
68    /// // Create a user from a raw Item and atomically initialize an enrollment
69    /// let user_item = sample_user_item();
70    /// let enrollment = sample_enrollment();
71    /// client
72    ///     .transact_write_items()
73    ///     .transact_items(
74    ///         TransactPutRequest::<PlatformTable>::new(user_item).build(),
75    ///     )
76    ///     .transact_items(enrollment.transact_put().not_exists().build())
77    ///     .send()
78    ///     .await?;
79    /// # Ok(())
80    /// # }
81    /// ```
82    pub fn new(item: Item<TD>) -> Self {
83        let table_name = TD::table_name();
84        tracing::debug!(table_name, "TransactPut");
85        Self {
86            builder: PutBuilder::default()
87                .table_name(table_name)
88                .set_item(Some(item.into_inner())),
89            _marker: PhantomData,
90        }
91    }
92
93    /// Consumes the builder and returns the underlying SDK [`PutBuilder`].
94    ///
95    /// Use this escape hatch when you need to set options not exposed by this
96    /// facade.
97    pub fn into_inner(self) -> PutBuilder {
98        self.builder
99    }
100
101    /// Finalizes the builder and returns a [`TransactWriteItem`].
102    ///
103    /// The returned value can be passed directly to the SDK's
104    /// `transact_write_items().transact_items(...)` call.
105    ///
106    /// # Examples
107    ///
108    /// ```no_run
109    /// # use dynamodb_facade::test_fixtures::*;
110    /// use dynamodb_facade::{DynamoDBItemTransactOp, KeyId, Update};
111    ///
112    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
113    /// let enrollment = sample_enrollment();
114    ///
115    /// // Atomically create an enrollment and increment the user's counter
116    /// client
117    ///     .transact_write_items()
118    ///     .transact_items(enrollment.transact_put().not_exists().build())
119    ///     .transact_items(
120    ///         User::transact_update_by_id(
121    ///             KeyId::pk("user-1"),
122    ///             Update::init_increment("enrollment_count", 0, 1),
123    ///         )
124    ///         .exists()
125    ///         .build(),
126    ///     )
127    ///     .send()
128    ///     .await?;
129    /// # Ok(())
130    /// # }
131    /// ```
132    pub fn build(self) -> TransactWriteItem {
133        TransactWriteItem::builder()
134            .put(self.builder.build().expect("mandatory attributes set"))
135            .build()
136    }
137}
138
139// -- Condition (NoCondition only) -------------------------------------------
140
141impl<TD: TableDefinition, T> TransactPutRequest<TD, T, NoCondition> {
142    /// Adds a condition expression that must be satisfied for the put to succeed.
143    ///
144    /// DynamoDB accepts a single condition expression per operation, so this
145    /// method can only be called once. If the condition fails, the entire
146    /// transaction is cancelled with `TransactionCanceledException`.
147    ///
148    /// # Examples
149    ///
150    /// ```no_run
151    /// # use dynamodb_facade::test_fixtures::*;
152    /// use dynamodb_facade::{DynamoDBItemOp, DynamoDBItemTransactOp, Condition};
153    ///
154    /// let transact_item = sample_enrollment()
155    ///     .transact_put()
156    ///     .condition(
157    ///         Enrollment::not_exists() |
158    ///         Condition::not_exists("completed_at")
159    ///     )
160    ///     .build();
161    /// ```
162    pub fn condition(
163        mut self,
164        condition: Condition<'_>,
165    ) -> TransactPutRequest<TD, T, AlreadyHasCondition> {
166        tracing::debug!(%condition, "TransactPut condition");
167        self.builder = condition.apply(self.builder);
168        TransactPutRequest {
169            builder: self.builder,
170            _marker: PhantomData,
171        }
172    }
173}
174
175impl<TD: TableDefinition, T: DynamoDBItem<TD>> TransactPutRequest<TD, T, NoCondition> {
176    /// Adds an `attribute_exists(<PK>)` condition.
177    ///
178    /// # Examples
179    ///
180    /// ```no_run
181    /// # use dynamodb_facade::test_fixtures::*;
182    /// use dynamodb_facade::DynamoDBItemTransactOp;
183    ///
184    /// let transact_item = sample_enrollment().transact_put().exists().build();
185    /// ```
186    pub fn exists(mut self) -> TransactPutRequest<TD, T, AlreadyHasCondition> {
187        self.builder = T::exists().apply(self.builder);
188        TransactPutRequest {
189            builder: self.builder,
190            _marker: PhantomData,
191        }
192    }
193
194    /// Adds an `attribute_not_exists(<PK>)` condition.
195    ///
196    /// Use this to implement create-only semantics within a transaction.
197    ///
198    /// # Examples
199    ///
200    /// ```no_run
201    /// # use dynamodb_facade::test_fixtures::*;
202    /// use dynamodb_facade::DynamoDBItemTransactOp;
203    ///
204    /// let transact_item = sample_enrollment().transact_put().not_exists().build();
205    /// ```
206    pub fn not_exists(mut self) -> TransactPutRequest<TD, T, AlreadyHasCondition> {
207        self.builder = T::not_exists().apply(self.builder);
208        TransactPutRequest {
209            builder: self.builder,
210            _marker: PhantomData,
211        }
212    }
213}
214
215/// Builder for a `Delete` operation inside a DynamoDB transaction.
216///
217/// Constructed via [`DynamoDBItemTransactOp::transact_delete`] or
218/// [`DynamoDBItemTransactOp::transact_delete_by_id`]. Optionally add a
219/// condition that must hold for the delete to succeed, via
220/// [`.condition()`][TransactDeleteRequest::condition], or
221/// [`.exists()`][TransactDeleteRequest::exists]. DynamoDB accepts a
222/// single condition expression per operation, so this can only be called once.
223///
224/// Call [`.build()`][TransactDeleteRequest::build] to produce a
225/// [`TransactWriteItem`] that can be passed to the SDK's
226/// `transact_write_items()` builder.
227///
228/// # Examples
229///
230/// Atomically remove an enrollment and decrement the user's enrollment count:
231///
232/// ```no_run
233/// # use dynamodb_facade::test_fixtures::*;
234/// use dynamodb_facade::{DynamoDBItemTransactOp, Condition, KeyId, Update};
235///
236/// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
237/// client
238///     .transact_write_items()
239///     .transact_items(
240///         Enrollment::transact_delete_by_id(KeyId::pk("user-1").sk("course-42"))
241///             .exists()
242///             .build(),
243///     )
244///     .transact_items(
245///         User::transact_update_by_id(
246///             KeyId::pk("user-1"),
247///             Update::decrement("enrollment_count", 1),
248///         )
249///         .condition(Condition::exists("enrollment_count"))
250///         .build(),
251///     )
252///     .send()
253///     .await?;
254/// # Ok(())
255/// # }
256/// ```
257#[must_use = "builder does nothing until .build() is called"]
258pub struct TransactDeleteRequest<TD: TableDefinition, T = (), C: ConditionState = NoCondition> {
259    builder: DeleteBuilder,
260    _marker: PhantomData<(TD, T, C)>,
261}
262
263// -- Common methods (all states) --------------------------------------------
264
265impl<TD: TableDefinition, T, C: ConditionState> TransactDeleteRequest<TD, T, C> {
266    /// Creates a new `TransactDeleteRequest` from a raw [`Key`].
267    ///
268    /// Prefer [`DynamoDBItemTransactOp::transact_delete`] or
269    /// [`DynamoDBItemTransactOp::transact_delete_by_id`] for typed construction.
270    pub fn new(key: Key<TD>) -> Self {
271        let table_name = TD::table_name();
272        tracing::debug!(table_name, key = ?key, "TransactDelete");
273        Self {
274            builder: DeleteBuilder::default()
275                .table_name(table_name)
276                .set_key(Some(key.into_inner())),
277            _marker: PhantomData,
278        }
279    }
280
281    /// Consumes the builder and returns the underlying SDK [`DeleteBuilder`].
282    ///
283    /// Use this escape hatch when you need to set options not exposed by this
284    /// facade.
285    pub fn into_inner(self) -> DeleteBuilder {
286        self.builder
287    }
288
289    /// Finalizes the builder and returns a [`TransactWriteItem`].
290    ///
291    /// The returned value can be passed directly to the SDK's
292    /// `transact_write_items().transact_items(...)` call.
293    ///
294    /// # Examples
295    ///
296    /// ```no_run
297    /// # use dynamodb_facade::test_fixtures::*;
298    /// use dynamodb_facade::{DynamoDBItemTransactOp, Condition, KeyId, Update};
299    ///
300    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
301    /// // Atomically remove an enrollment and decrement the user's counter
302    /// client
303    ///     .transact_write_items()
304    ///     .transact_items(
305    ///         Enrollment::transact_delete_by_id(KeyId::pk("user-1").sk("course-42"))
306    ///             .exists()
307    ///             .build(),
308    ///     )
309    ///     .transact_items(
310    ///         User::transact_update_by_id(
311    ///             KeyId::pk("user-1"),
312    ///             Update::decrement("enrollment_count", 1),
313    ///         )
314    ///         .condition(Condition::exists("enrollment_count"))
315    ///         .build(),
316    ///     )
317    ///     .send()
318    ///     .await?;
319    /// # Ok(())
320    /// # }
321    /// ```
322    pub fn build(self) -> TransactWriteItem {
323        TransactWriteItem::builder()
324            .delete(self.builder.build().expect("mandatory attributes set"))
325            .build()
326    }
327}
328
329// -- Condition (NoCondition only) -------------------------------------------
330
331impl<TD: TableDefinition, T> TransactDeleteRequest<TD, T, NoCondition> {
332    /// Adds a condition expression that must be satisfied for the delete to succeed.
333    ///
334    /// DynamoDB accepts a single condition expression per operation, so this
335    /// method can only be called once. If the condition fails, the entire
336    /// transaction is cancelled.
337    ///
338    /// # Examples
339    ///
340    /// ```no_run
341    /// # use dynamodb_facade::test_fixtures::*;
342    /// use dynamodb_facade::{DynamoDBItemOp, DynamoDBItemTransactOp, Condition};
343    ///
344    /// let transact_item = sample_enrollment()
345    ///     .transact_delete()
346    ///     .condition(Enrollment::exists() & Condition::not_exists("completed_at"))
347    ///     .build();
348    /// ```
349    pub fn condition(
350        mut self,
351        condition: Condition<'_>,
352    ) -> TransactDeleteRequest<TD, T, AlreadyHasCondition> {
353        tracing::debug!(%condition, "TransactDelete condition");
354        self.builder = condition.apply(self.builder);
355        TransactDeleteRequest {
356            builder: self.builder,
357            _marker: PhantomData,
358        }
359    }
360}
361
362impl<TD: TableDefinition, T: DynamoDBItem<TD>> TransactDeleteRequest<TD, T, NoCondition> {
363    /// Adds an `attribute_exists(<PK>)` condition.
364    ///
365    /// # Examples
366    ///
367    /// ```no_run
368    /// # use dynamodb_facade::test_fixtures::*;
369    /// use dynamodb_facade::DynamoDBItemTransactOp;
370    ///
371    /// let transact_item = sample_enrollment().transact_delete().exists().build();
372    /// ```
373    pub fn exists(self) -> TransactDeleteRequest<TD, T, AlreadyHasCondition> {
374        self.condition(T::exists())
375    }
376}
377
378/// Builder for an `Update` operation inside a DynamoDB transaction.
379///
380/// Constructed via [`DynamoDBItemTransactOp::transact_update`] or
381/// [`DynamoDBItemTransactOp::transact_update_by_id`]. Optionally add a
382/// condition that must hold for the update to succeed, via
383/// [`.condition()`][TransactUpdateRequest::condition],
384/// [`.exists()`][TransactUpdateRequest::exists], or
385/// [`.not_exists()`][TransactUpdateRequest::not_exists]. DynamoDB accepts a
386/// single condition expression per operation, so this can only be called once.
387///
388/// Call [`.build()`][TransactUpdateRequest::build] to produce a
389/// [`TransactWriteItem`] that can be passed to the SDK's
390/// `transact_write_items()` builder.
391///
392/// # Examples
393///
394/// Atomically promote a user to instructor and create their first enrollment:
395///
396/// ```no_run
397/// # use dynamodb_facade::test_fixtures::*;
398/// use dynamodb_facade::{Condition, DynamoDBItemTransactOp, KeyId, Update};
399///
400/// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
401/// let enrollment = sample_enrollment();
402///
403/// client
404///     .transact_write_items()
405///     .transact_items(
406///         User::transact_update_by_id(
407///             KeyId::pk("user-1"),
408///             Update::set("role", "instructor"),
409///         )
410///         .condition(Condition::not_exists("role"))
411///         .build(),
412///     )
413///     .transact_items(enrollment.transact_put().not_exists().build())
414///     .send()
415///     .await?;
416/// # Ok(())
417/// # }
418/// ```
419#[must_use = "builder does nothing until .build() is called"]
420pub struct TransactUpdateRequest<TD: TableDefinition, T = (), C: ConditionState = NoCondition> {
421    builder: UpdateBuilder,
422    _marker: PhantomData<(TD, T, C)>,
423}
424
425// -- Common methods (all states) --------------------------------------------
426
427impl<TD: TableDefinition, T, C: ConditionState> TransactUpdateRequest<TD, T, C> {
428    /// Creates a new `TransactUpdateRequest` from a raw [`Key`] and an [`Update`] expression.
429    ///
430    /// Prefer [`DynamoDBItemTransactOp::transact_update`] or
431    /// [`DynamoDBItemTransactOp::transact_update_by_id`] for typed construction.
432    pub fn new(key: Key<TD>, update: Update<'_>) -> Self {
433        let table_name = TD::table_name();
434        tracing::debug!(table_name, key = ?key, %update, "TransactUpdate");
435        Self {
436            builder: update.apply(
437                UpdateBuilder::default()
438                    .table_name(table_name)
439                    .set_key(Some(key.into_inner())),
440            ),
441            _marker: PhantomData,
442        }
443    }
444
445    /// Consumes the builder and returns the underlying SDK [`UpdateBuilder`].
446    ///
447    /// Use this escape hatch when you need to set options not exposed by this
448    /// facade.
449    pub fn into_inner(self) -> UpdateBuilder {
450        self.builder
451    }
452
453    /// Finalizes the builder and returns a [`TransactWriteItem`].
454    ///
455    /// The returned value can be passed directly to the SDK's
456    /// `transact_write_items().transact_items(...)` call.
457    ///
458    /// # Examples
459    ///
460    /// ```no_run
461    /// # use dynamodb_facade::test_fixtures::*;
462    /// use dynamodb_facade::{Condition, DynamoDBItemTransactOp, KeyId, Update};
463    ///
464    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
465    /// let enrollment = sample_enrollment();
466    ///
467    /// // Atomically promote a user and create an enrollment
468    /// client
469    ///     .transact_write_items()
470    ///     .transact_items(
471    ///         User::transact_update_by_id(
472    ///             KeyId::pk("user-1"),
473    ///             Update::set("role", "instructor"),
474    ///         )
475    ///         .condition(Condition::not_exists("role"))
476    ///         .build(),
477    ///     )
478    ///     .transact_items(enrollment.transact_put().not_exists().build())
479    ///     .send()
480    ///     .await?;
481    /// # Ok(())
482    /// # }
483    /// ```
484    pub fn build(self) -> TransactWriteItem {
485        TransactWriteItem::builder()
486            .update(
487                self.builder
488                    .build()
489                    .expect("Update expression is always set"),
490            )
491            .build()
492    }
493}
494
495// -- Condition (NoCondition only) -------------------------------------------
496
497impl<TD: TableDefinition, T> TransactUpdateRequest<TD, T, NoCondition> {
498    /// Adds a condition expression that must be satisfied for the update to succeed.
499    ///
500    /// DynamoDB accepts a single condition expression per operation, so this
501    /// method can only be called once. If the condition fails, the entire
502    /// transaction is cancelled.
503    ///
504    /// # Examples
505    ///
506    /// ```no_run
507    /// # use dynamodb_facade::test_fixtures::*;
508    /// use dynamodb_facade::{DynamoDBItemTransactOp, KeyId, Update, Condition};
509    ///
510    /// let transact_item = User::transact_update_by_id(
511    ///     KeyId::pk("user-1"),
512    ///     Update::set("role", "instructor"),
513    /// )
514    /// .condition(Condition::not_exists("role"))
515    /// .build();
516    /// ```
517    pub fn condition(
518        mut self,
519        condition: Condition<'_>,
520    ) -> TransactUpdateRequest<TD, T, AlreadyHasCondition> {
521        tracing::debug!(%condition, "TransactUpdate condition");
522        self.builder = condition.apply(self.builder);
523        TransactUpdateRequest {
524            builder: self.builder,
525            _marker: PhantomData,
526        }
527    }
528}
529
530impl<TD: TableDefinition, T: DynamoDBItem<TD>> TransactUpdateRequest<TD, T, NoCondition> {
531    /// Adds an `attribute_exists(<PK>)` condition.
532    ///
533    /// # Examples
534    ///
535    /// ```no_run
536    /// # use dynamodb_facade::test_fixtures::*;
537    /// use dynamodb_facade::{DynamoDBItemTransactOp, KeyId, Update};
538    ///
539    /// let transact_item = User::transact_update_by_id(
540    ///     KeyId::pk("user-1"),
541    ///     Update::increment("enrollment_count", 1),
542    /// )
543    /// .exists()
544    /// .build();
545    /// ```
546    pub fn exists(mut self) -> TransactUpdateRequest<TD, T, AlreadyHasCondition> {
547        self.builder = T::exists().apply(self.builder);
548        TransactUpdateRequest {
549            builder: self.builder,
550            _marker: PhantomData,
551        }
552    }
553
554    /// Adds an `attribute_not_exists(<PK>)` condition.
555    ///
556    /// Useful for upsert-style updates that must only apply when the item does
557    /// not yet exist.
558    ///
559    /// # Examples
560    ///
561    /// ```no_run
562    /// # use dynamodb_facade::test_fixtures::*;
563    /// use dynamodb_facade::{DynamoDBItemTransactOp, KeyId, Update};
564    ///
565    /// // Initialize enrollment progress only if the enrollment doesn't exist yet
566    /// let transact_item = Enrollment::transact_update_by_id(
567    ///     KeyId::pk("user-1").sk("course-42"),
568    ///     Update::set("progress", 0.0),
569    /// )
570    /// .not_exists()
571    /// .build();
572    /// ```
573    pub fn not_exists(mut self) -> TransactUpdateRequest<TD, T, AlreadyHasCondition> {
574        self.builder = T::not_exists().apply(self.builder);
575        TransactUpdateRequest {
576            builder: self.builder,
577            _marker: PhantomData,
578        }
579    }
580}
581
582/// Builder for a `ConditionCheck` operation inside a DynamoDB transaction.
583///
584/// A condition check does not mutate any item — it only asserts that a
585/// condition holds. If the condition fails, the entire transaction is
586/// cancelled. Use this to enforce invariants on items that are not otherwise
587/// being modified in the same transaction.
588///
589/// Constructed via [`DynamoDBItemTransactOp::transact_condition`] or
590/// [`DynamoDBItemTransactOp::transact_condition_by_id`].
591///
592/// Call [`.build()`][TransactConditionCheckRequest::build] to produce a
593/// [`TransactWriteItem`] that can be passed to the SDK's
594/// `transact_write_items()` builder.
595///
596/// # Examples
597///
598/// Verify the user is an admin before toggling maintenance mode:
599///
600/// ```no_run
601/// # use dynamodb_facade::test_fixtures::*;
602/// use dynamodb_facade::{DynamoDBItemOp, DynamoDBItemTransactOp, Condition, KeyId, Update};
603///
604/// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
605/// let user = sample_user();
606///
607/// client
608///     .transact_write_items()
609///     .transact_items(
610///         user.transact_condition(
611///             User::exists() & Condition::eq("role", "admin"),
612///         )
613///         .build(),
614///     )
615///     .transact_items(
616///         PlatformConfig::transact_update_by_id(
617///             KeyId::NONE,
618///             Update::set("maintenance_mode", true),
619///         )
620///         .exists()
621///         .build(),
622///     )
623///     .send()
624///     .await?;
625/// # Ok(())
626/// # }
627/// ```
628#[must_use = "builder does nothing until .build() is called"]
629pub struct TransactConditionCheckRequest<TD: TableDefinition, T = ()> {
630    builder: ConditionCheckBuilder,
631    _marker: PhantomData<(TD, T)>,
632}
633
634impl<TD: TableDefinition, T> TransactConditionCheckRequest<TD, T> {
635    /// Creates a new `TransactConditionCheckRequest` from a raw [`Key`] and a [`Condition`].
636    ///
637    /// Prefer [`DynamoDBItemTransactOp::transact_condition`] or
638    /// [`DynamoDBItemTransactOp::transact_condition_by_id`] for typed construction.
639    pub fn new(key: Key<TD>, condition: Condition<'_>) -> Self {
640        let table_name = TD::table_name();
641        tracing::debug!(table_name, key = ?key, %condition, "TransactConditionCheck");
642        Self {
643            builder: condition.apply(
644                ConditionCheckBuilder::default()
645                    .table_name(table_name)
646                    .set_key(Some(key.into_inner())),
647            ),
648            _marker: PhantomData,
649        }
650    }
651
652    /// Consumes the builder and returns the underlying SDK [`ConditionCheckBuilder`].
653    ///
654    /// Use this escape hatch when you need to set options not exposed by this
655    /// facade.
656    pub fn into_inner(self) -> ConditionCheckBuilder {
657        self.builder
658    }
659
660    /// Finalizes the builder and returns a [`TransactWriteItem`].
661    ///
662    /// The returned value can be passed directly to the SDK's
663    /// `transact_write_items().transact_items(...)` call.
664    ///
665    /// # Examples
666    ///
667    /// ```no_run
668    /// # use dynamodb_facade::test_fixtures::*;
669    /// use dynamodb_facade::{DynamoDBItemOp, DynamoDBItemTransactOp, Condition, KeyId, Update};
670    ///
671    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
672    /// let user = sample_user();
673    ///
674    /// // Verify the user is an admin before toggling maintenance mode
675    /// client
676    ///     .transact_write_items()
677    ///     .transact_items(
678    ///         user.transact_condition(
679    ///             User::exists() & Condition::eq("role", "admin"),
680    ///         )
681    ///         .build(),
682    ///     )
683    ///     .transact_items(
684    ///         PlatformConfig::transact_update_by_id(
685    ///             KeyId::NONE,
686    ///             Update::set("maintenance_mode", true),
687    ///         )
688    ///         .exists()
689    ///         .build(),
690    ///     )
691    ///     .send()
692    ///     .await?;
693    /// # Ok(())
694    /// # }
695    /// ```
696    pub fn build(self) -> TransactWriteItem {
697        TransactWriteItem::builder()
698            .condition_check(self.builder.build().expect("mandatory attributes set"))
699            .build()
700    }
701}
702
703// ---------------------------------------------------------------------------
704// DynamoDBItemTransactOp trait
705// ---------------------------------------------------------------------------
706
707/// Entry points for building DynamoDB `TransactWriteItems` operations.
708///
709/// This trait is **blanket-implemented** for every type that implements
710/// [`DynamoDBItemOp<TD>`]. You never implement it manually.
711///
712/// Each method returns a typed builder ([`TransactPutRequest`],
713/// [`TransactDeleteRequest`], [`TransactUpdateRequest`], or
714/// [`TransactConditionCheckRequest`]) that can be configured with optional
715/// conditions and then finalized with `.build()` to produce a
716/// [`TransactWriteItem`].
717///
718/// Collect the `TransactWriteItem` values and pass them to the SDK's
719/// `client.transact_write_items().transact_items(...)` builder to execute the
720/// transaction atomically.
721///
722/// # Examples
723///
724/// ```no_run
725/// # use dynamodb_facade::test_fixtures::*;
726/// use dynamodb_facade::{DynamoDBItemTransactOp, KeyId, Update};
727///
728/// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
729/// let enrollment = sample_enrollment();
730///
731/// // Atomically create an enrollment and increment the user's enrollment count
732/// client
733///     .transact_write_items()
734///     .transact_items(enrollment.transact_put().not_exists().build())
735///     .transact_items(
736///         User::transact_update_by_id(
737///             KeyId::pk("user-1"),
738///             Update::init_increment("enrollment_count", 0, 1),
739///         )
740///         .exists()
741///         .build(),
742///     )
743///     .send()
744///     .await?;
745/// # Ok(())
746/// # }
747/// ```
748pub trait DynamoDBItemTransactOp<TD: TableDefinition>: DynamoDBItemOp<TD> {
749    /// Creates a [`TransactPutRequest`] for this item.
750    ///
751    /// Serializes `self` into a DynamoDB item map. Use `.not_exists()` or
752    /// `.condition(cond)` to add a guard before calling `.build()`.
753    ///
754    /// # Panics
755    ///
756    /// Panics if serializing `self` via [`DynamoDBItem::to_item`] fails. See
757    /// [`DynamoDBItem::to_item`] for the conditions under which this can
758    /// happen — it is the caller's responsibility to provide a compatible
759    /// [`Serialize`] implementation.
760    ///
761    /// # Examples
762    ///
763    /// ```no_run
764    /// # use dynamodb_facade::test_fixtures::*;
765    /// use dynamodb_facade::DynamoDBItemTransactOp;
766    ///
767    /// let transact_item = sample_enrollment().transact_put().not_exists().build();
768    /// ```
769    fn transact_put(&self) -> TransactPutRequest<TD, Self>
770    where
771        Self: Serialize,
772    {
773        TransactPutRequest::new(self.to_item())
774    }
775
776    /// Creates a [`TransactDeleteRequest`] for this item's key.
777    ///
778    /// Use `.exists()` or `.condition(cond)` to add a guard before calling
779    /// `.build()`.
780    ///
781    /// # Examples
782    ///
783    /// ```no_run
784    /// # use dynamodb_facade::test_fixtures::*;
785    /// use dynamodb_facade::DynamoDBItemTransactOp;
786    ///
787    /// let transact_item = sample_enrollment().transact_delete().exists().build();
788    /// ```
789    fn transact_delete(&self) -> TransactDeleteRequest<TD, Self> {
790        TransactDeleteRequest::new(self.get_key())
791    }
792
793    /// Creates a [`TransactDeleteRequest`] from a key ID, without loading the item.
794    ///
795    /// # Examples
796    ///
797    /// ```no_run
798    /// # use dynamodb_facade::test_fixtures::*;
799    /// use dynamodb_facade::{DynamoDBItemTransactOp, KeyId};
800    ///
801    /// let transact_item = Enrollment::transact_delete_by_id(KeyId::pk("user-1").sk("course-42"))
802    ///     .exists()
803    ///     .build();
804    /// ```
805    fn transact_delete_by_id(key_id: Self::KeyId<'_>) -> TransactDeleteRequest<TD, Self> {
806        TransactDeleteRequest::new(Self::get_key_from_id(key_id))
807    }
808
809    /// Creates a [`TransactUpdateRequest`] using the key of `self` and the given [`Update`].
810    ///
811    /// Use `.exists()` or `.condition(cond)` to add a guard before calling
812    /// `.build()`.
813    ///
814    /// # Examples
815    ///
816    /// ```no_run
817    /// # use dynamodb_facade::test_fixtures::*;
818    /// use dynamodb_facade::{DynamoDBItemTransactOp, Update};
819    ///
820    /// let transact_item = sample_user()
821    ///     .transact_update(Update::set("role", "instructor"))
822    ///     .exists()
823    ///     .build();
824    /// ```
825    fn transact_update(&self, update: Update<'_>) -> TransactUpdateRequest<TD, Self> {
826        TransactUpdateRequest::new(self.get_key(), update)
827    }
828
829    /// Creates a [`TransactUpdateRequest`] from a key ID and an [`Update`] expression.
830    ///
831    /// Use `.exists()` or `.condition(cond)` to add a guard before calling
832    /// `.build()`.
833    ///
834    /// # Examples
835    ///
836    /// ```no_run
837    /// # use dynamodb_facade::test_fixtures::*;
838    /// use dynamodb_facade::{DynamoDBItemTransactOp, KeyId, Update};
839    ///
840    /// let transact_item = User::transact_update_by_id(
841    ///     KeyId::pk("user-1"),
842    ///     Update::increment("enrollment_count", 1),
843    /// )
844    /// .exists()
845    /// .build();
846    /// ```
847    fn transact_update_by_id(
848        key_id: Self::KeyId<'_>,
849        update: Update<'_>,
850    ) -> TransactUpdateRequest<TD, Self> {
851        TransactUpdateRequest::new(Self::get_key_from_id(key_id), update)
852    }
853
854    /// Creates a [`TransactConditionCheckRequest`] using the key of `self`.
855    ///
856    /// The condition check does not mutate any item — it only asserts that the
857    /// given condition holds. If it fails, the entire transaction is cancelled.
858    ///
859    /// # Examples
860    ///
861    /// ```no_run
862    /// # use dynamodb_facade::test_fixtures::*;
863    /// use dynamodb_facade::{DynamoDBItemOp, DynamoDBItemTransactOp, Condition};
864    ///
865    /// let transact_check = sample_user()
866    ///     .transact_condition(User::exists() & Condition::eq("role", "admin"))
867    ///     .build();
868    /// ```
869    fn transact_condition(
870        &self,
871        condition: Condition<'_>,
872    ) -> TransactConditionCheckRequest<TD, Self> {
873        TransactConditionCheckRequest::new(self.get_key(), condition)
874    }
875
876    /// Creates a [`TransactConditionCheckRequest`] from a key ID.
877    ///
878    /// Use this when you have the key components but not a loaded item
879    /// instance.
880    ///
881    /// # Examples
882    ///
883    /// ```no_run
884    /// # use dynamodb_facade::test_fixtures::*;
885    /// use dynamodb_facade::{DynamoDBItemOp, DynamoDBItemTransactOp, KeyId, Condition};
886    ///
887    /// let transact_check = User::transact_condition_by_id(
888    ///         KeyId::pk("user-1"),
889    ///         User::exists() & Condition::eq("role", "admin"),
890    ///     )
891    ///     .build();
892    /// ```
893    fn transact_condition_by_id(
894        key_id: Self::KeyId<'_>,
895        condition: Condition<'_>,
896    ) -> TransactConditionCheckRequest<TD, Self> {
897        TransactConditionCheckRequest::new(Self::get_key_from_id(key_id), condition)
898    }
899}
900
901impl<TD: TableDefinition, DBI: DynamoDBItemOp<TD>> DynamoDBItemTransactOp<TD> for DBI {}