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 {}