Skip to main content

dynamodb_facade/operations/
put.rs

1use std::future::{Future, IntoFuture};
2use std::pin::Pin;
3
4use super::*;
5
6use aws_sdk_dynamodb::operation::put_item::builders::PutItemFluentBuilder;
7
8/// Builder for a DynamoDB `PutItem` request.
9///
10/// Constructed via [`DynamoDBItemOp::put`] (typed, with a concrete `T`) or
11/// [`PutItemRequest::new`] (stand-alone, raw output). The builder provides:
12///
13/// - **Output format** — the result can be deserialized into `T`.
14///   Call [`.raw()`][PutItemRequest::raw] to receive an untyped [`Item<TD>`]
15///   instead (one-way).
16/// - **Return value** — by default nothing is returned. Call
17///   [`.return_old()`][PutItemRequest::return_old] to request the previous
18///   item, or [`.return_none()`][PutItemRequest::return_none] to revert.
19/// - **Condition** — optionally add a guard expression via
20///   [`.condition()`][PutItemRequest::condition],
21///   [`.exists()`][PutItemRequest::exists], or
22///   [`.not_exists()`][PutItemRequest::not_exists]. DynamoDB accepts a
23///   single condition expression per request, so this can only be called once.
24///
25/// The builder implements [`IntoFuture`], so it can
26/// be `.await`ed directly.
27///
28/// # Errors
29///
30/// Returns [`Err`] if the DynamoDB request fails, if a condition expression
31/// is set and the condition check fails
32/// (`ConditionalCheckFailedException`), or if serialization of `self` fails.
33///
34/// # Examples
35///
36/// ```no_run
37/// # use dynamodb_facade::test_fixtures::*;
38/// use dynamodb_facade::{DynamoDBItemOp, Condition};
39///
40/// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
41/// let user = sample_user();
42///
43/// # let client = cclient.clone();
44/// // Simple put
45/// user.put(client).await?;
46///
47/// # let client = cclient.clone();
48/// // Create-only: fails if item already exists
49/// user.put(client).not_exists().await?;
50///
51/// # let client = cclient.clone();
52/// // Custom condition
53/// user.put(client)
54///     .condition(User::not_exists() | Condition::lt("expiration_timestamp", 1_700_000_000))
55///     .await?;
56///
57/// # let client = cclient.clone();
58/// // Put and return the old item
59/// let old /* : Option<User> */ = user.put(client).return_old().await?;
60/// # Ok(())
61/// # }
62/// ```
63#[must_use = "builder does nothing until awaited or executed"]
64pub struct PutItemRequest<
65    TD: TableDefinition,
66    T = (),
67    O: OutputFormat = Raw,
68    R: ReturnValue = ReturnNothing,
69    C: ConditionState = NoCondition,
70> {
71    builder: PutItemFluentBuilder,
72    _marker: PhantomData<(TD, T, O, R, C)>,
73}
74
75// -- Common methods (all states) --------------------------------------------
76
77impl<TD: TableDefinition, T, R: ReturnValue, O: OutputFormat, C: ConditionState>
78    PutItemRequest<TD, T, O, R, C>
79{
80    /// Consumes the builder and returns the underlying SDK
81    /// [`PutItemFluentBuilder`].
82    ///
83    /// Use this escape hatch when you need to set options not exposed by this
84    /// facade, or when integrating with code that expects the raw SDK builder.
85    ///
86    /// # Examples
87    ///
88    /// ```no_run
89    /// # use dynamodb_facade::test_fixtures::*;
90    /// use dynamodb_facade::DynamoDBItemOp;
91    ///
92    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
93    /// let sdk_builder = sample_user().put(client).into_inner();
94    /// // configure sdk_builder further, then call .send().await
95    /// # Ok(())
96    /// # }
97    /// ```
98    pub fn into_inner(self) -> PutItemFluentBuilder {
99        self.builder
100    }
101}
102
103// -- Stand-alone constructor (ReturnNothing, NoCondition, T = (), O = Raw)
104
105impl<TD: TableDefinition> PutItemRequest<TD, (), Raw> {
106    /// Creates a stand-alone `PutItemRequest` with raw output (`T = ()`, `O = Raw`).
107    ///
108    /// Use this when you already have an [`Item<TD>`] and do not need typed
109    /// deserialization of the old value. For typed access, prefer
110    /// [`DynamoDBItemOp::put`] instead.
111    ///
112    /// # Examples
113    ///
114    /// ```no_run
115    /// # use dynamodb_facade::test_fixtures::*;
116    /// use dynamodb_facade::PutItemRequest;
117    ///
118    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
119    /// let item = sample_user_item();
120    /// PutItemRequest::<PlatformTable>::new(client, item).await?;
121    /// # Ok(())
122    /// # }
123    /// ```
124    pub fn new(client: aws_sdk_dynamodb::Client, item: Item<TD>) -> Self {
125        Self::_new(client, item)
126    }
127}
128
129// -- Constructor (any R, any O, any C) ------------------------------
130
131impl<TD: TableDefinition, T, O: OutputFormat, R: ReturnValue, C: ConditionState>
132    PutItemRequest<TD, T, O, R, C>
133{
134    /// Creates a new `PutItemRequest` with the given item.
135    pub(super) fn _new(client: aws_sdk_dynamodb::Client, item: Item<TD>) -> Self {
136        let table_name = TD::table_name();
137        tracing::debug!(table_name, "PutItem");
138        Self {
139            builder: client
140                .put_item()
141                .table_name(table_name)
142                .set_item(Some(item.into_inner())),
143            _marker: PhantomData,
144        }
145    }
146}
147
148// -- Return-value transitions (preserve O, C) -------------------------------
149
150impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState>
151    PutItemRequest<TD, T, O, ReturnNothing, C>
152{
153    /// Requests that DynamoDB return the item's previous attributes after the put.
154    ///
155    /// When executed, [`execute`][PutItemRequest::execute] returns
156    /// `Option<T>` (typed) or `Option<Item<TD>>` (raw) — `None` if no item
157    /// previously existed at that key.
158    ///
159    /// # Examples
160    ///
161    /// ```no_run
162    /// # use dynamodb_facade::test_fixtures::*;
163    /// use dynamodb_facade::DynamoDBItemOp;
164    ///
165    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
166    /// let user = sample_user();
167    /// let old /* : Option<User> */ = user.put(client).return_old().await?;
168    /// // old is None if this was the first put, Some(prev_user) otherwise
169    /// # Ok(())
170    /// # }
171    /// ```
172    pub fn return_old(self) -> PutItemRequest<TD, T, O, Return<Old>, C> {
173        tracing::debug!("PutItem return_old");
174        PutItemRequest {
175            builder: self.builder,
176            _marker: PhantomData,
177        }
178    }
179}
180
181impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState>
182    PutItemRequest<TD, T, O, Return<Old>, C>
183{
184    /// Reverts the return-value setting so that nothing is returned.
185    ///
186    /// After this call, [`execute`][PutItemRequest::execute] returns `()`
187    /// instead of the old item.
188    ///
189    /// # Examples
190    ///
191    /// ```no_run
192    /// # use dynamodb_facade::test_fixtures::*;
193    /// use dynamodb_facade::DynamoDBItemOp;
194    ///
195    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
196    /// let user = sample_user();
197    /// // Start with return_old, then decide we don't need the old value
198    /// user.put(client).return_old().return_none().await?;
199    /// # Ok(())
200    /// # }
201    /// ```
202    pub fn return_none(self) -> PutItemRequest<TD, T, O, ReturnNothing, C> {
203        tracing::debug!("PutItem return_none");
204        PutItemRequest {
205            builder: self.builder,
206            _marker: PhantomData,
207        }
208    }
209}
210
211// -- Condition (NoCondition only) -------------------------------------------
212
213impl<TD: TableDefinition, T, O: OutputFormat, R: ReturnValue>
214    PutItemRequest<TD, T, O, R, NoCondition>
215{
216    /// Adds a condition expression that must be satisfied for the put to succeed.
217    ///
218    /// DynamoDB accepts a single condition expression per request, so this
219    /// method can only be called once. If the condition fails at runtime,
220    /// DynamoDB returns a `ConditionalCheckFailedException`.
221    ///
222    /// For the common item exists/not_exists cases, prefer
223    /// the [`.exists()`][PutItemRequest::exists] and
224    /// [`.not_exists()`][PutItemRequest::not_exists] shorthands.
225    ///
226    /// # Examples
227    ///
228    /// ```no_run
229    /// # use dynamodb_facade::test_fixtures::*;
230    /// use dynamodb_facade::{DynamoDBItemOp, Condition};
231    ///
232    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
233    /// let user = sample_user();
234    /// // Put only if the item does not exist OR its TTL has expired
235    /// user.put(client)
236    ///     .condition(User::not_exists() | Condition::lt("expiration_timestamp", 1_700_000_000))
237    ///     .await?;
238    /// # Ok(())
239    /// # }
240    /// ```
241    pub fn condition(
242        mut self,
243        condition: Condition<'_>,
244    ) -> PutItemRequest<TD, T, O, R, AlreadyHasCondition> {
245        tracing::debug!(%condition, "PutItem condition");
246        self.builder = condition.apply(self.builder);
247        PutItemRequest {
248            builder: self.builder,
249            _marker: PhantomData,
250        }
251    }
252}
253
254impl<TD: TableDefinition, T: DynamoDBItem<TD>, O: OutputFormat, R: ReturnValue>
255    PutItemRequest<TD, T, O, R, NoCondition>
256{
257    /// Adds an `attribute_exists(<PK>)` condition, requiring the item to already exist.
258    ///
259    /// # Examples
260    ///
261    /// ```no_run
262    /// # use dynamodb_facade::test_fixtures::*;
263    /// use dynamodb_facade::DynamoDBItemOp;
264    ///
265    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
266    /// // Overwrite only if the item already exists
267    /// sample_user().put(client).exists().await?;
268    /// # Ok(())
269    /// # }
270    /// ```
271    pub fn exists(self) -> PutItemRequest<TD, T, O, R, AlreadyHasCondition> {
272        self.condition(T::exists())
273    }
274
275    /// Adds an `attribute_not_exists(<PK>)` condition, requiring the item to not yet exist.
276    ///
277    /// Use this to implement create-only (insert-if-absent) semantics.
278    ///
279    /// # Examples
280    ///
281    /// ```no_run
282    /// # use dynamodb_facade::test_fixtures::*;
283    /// use dynamodb_facade::DynamoDBItemOp;
284    ///
285    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
286    /// // Create-only: fails if user already exists
287    /// sample_user().put(client).not_exists().await?;
288    /// # Ok(())
289    /// # }
290    /// ```
291    pub fn not_exists(self) -> PutItemRequest<TD, T, O, R, AlreadyHasCondition> {
292        self.condition(T::not_exists())
293    }
294}
295
296// -- Output format transition (preserve R, C) -------------------------------
297
298impl<TD: TableDefinition, T, R: ReturnValue, C: ConditionState> PutItemRequest<TD, T, Typed, R, C> {
299    /// Switches the output format from `Typed` to `Raw`.
300    ///
301    /// After calling `.raw()`, [`execute`][PutItemRequest::execute] returns
302    /// `Option<Item<TD>>` instead of `Option<T>` when `Return<Old>` is active.
303    /// This transition is one-way.
304    ///
305    /// # Examples
306    ///
307    /// ```no_run
308    /// # use dynamodb_facade::test_fixtures::*;
309    /// use dynamodb_facade::DynamoDBItemOp;
310    ///
311    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
312    /// let old_raw = sample_user()
313    ///     .put(client)
314    ///     .return_old()
315    ///     .raw()
316    ///     .await?;
317    /// // old_raw: Option<Item<PlatformTable>>
318    /// # Ok(())
319    /// # }
320    /// ```
321    pub fn raw(self) -> PutItemRequest<TD, T, Raw, R, C> {
322        PutItemRequest {
323            builder: self.builder,
324            _marker: PhantomData,
325        }
326    }
327}
328
329// -- Terminal: ReturnNothing (any O, any C) ---------------------------------
330
331impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState>
332    PutItemRequest<TD, T, O, ReturnNothing, C>
333{
334    /// Sends the `PutItem` request, returning nothing on success.
335    ///
336    /// This method is also available implicitly via `.await`.
337    ///
338    /// # Errors
339    ///
340    /// Returns [`Err`] if the DynamoDB request fails or if a condition
341    /// expression is set and the check fails
342    /// (`ConditionalCheckFailedException`).
343    ///
344    /// # Examples
345    ///
346    /// ```no_run
347    /// # use dynamodb_facade::test_fixtures::*;
348    /// use dynamodb_facade::DynamoDBItemOp;
349    ///
350    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
351    /// sample_user().put(client).not_exists().execute().await?;
352    /// # Ok(())
353    /// # }
354    /// ```
355    #[tracing::instrument(level = "debug", skip(self), name = "put_execute")]
356    pub fn execute(self) -> impl Future<Output = Result<()>> + Send + 'static {
357        let builder = self.builder;
358        async move {
359            builder.return_values(SDKReturnValue::None).send().await?;
360            Ok(())
361        }
362    }
363}
364
365impl<TD: TableDefinition, T, O: OutputFormat, C: ConditionState> IntoFuture
366    for PutItemRequest<TD, T, O, ReturnNothing, C>
367{
368    type Output = Result<()>;
369    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
370
371    fn into_future(self) -> Self::IntoFuture {
372        Box::pin(self.execute())
373    }
374}
375
376// -- Terminal: ReturnItem<Old> + Typed (any C) ------------------------------
377
378impl<TD: TableDefinition, T: DynamoDBItem<TD> + DeserializeOwned, C: ConditionState>
379    PutItemRequest<TD, T, Typed, Return<Old>, C>
380{
381    /// Sends the `PutItem` request and returns the previous item deserialized as `T`.
382    ///
383    /// Returns `Ok(None)` if no item previously existed at the key.
384    ///
385    /// This method is also available implicitly via `.await`.
386    ///
387    /// # Errors
388    ///
389    /// Returns [`Err`] if the DynamoDB request fails, if a condition check
390    /// fails, or if deserialization of the returned attributes fails.
391    ///
392    /// # Examples
393    ///
394    /// ```no_run
395    /// # use dynamodb_facade::test_fixtures::*;
396    /// use dynamodb_facade::DynamoDBItemOp;
397    ///
398    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
399    /// let old /* : Option<User> */ = sample_user().put(client).return_old().execute().await?;
400    /// // old is None on first put, Some(previous_user) on subsequent puts
401    /// # Ok(())
402    /// # }
403    /// ```
404    #[tracing::instrument(level = "debug", skip(self), name = "put_execute_old")]
405    pub fn execute(self) -> impl Future<Output = Result<Option<T>>> + Send + 'static {
406        let builder = self.builder;
407        async move {
408            builder
409                .return_values(SDKReturnValue::AllOld)
410                .send()
411                .await?
412                .attributes
413                .map(Item::from_dynamodb_response)
414                .map(T::try_from_item)
415                .transpose()
416        }
417    }
418}
419
420impl<TD: TableDefinition, T: DynamoDBItem<TD> + DeserializeOwned, C: ConditionState> IntoFuture
421    for PutItemRequest<TD, T, Typed, Return<Old>, C>
422{
423    type Output = Result<Option<T>>;
424    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
425
426    fn into_future(self) -> Self::IntoFuture {
427        Box::pin(self.execute())
428    }
429}
430
431// -- Terminal: ReturnItem<Old> + Raw (any C) --------------------------------
432
433impl<TD: TableDefinition, T, C: ConditionState> PutItemRequest<TD, T, Raw, Return<Old>, C> {
434    /// Sends the `PutItem` request and returns the previous raw item map.
435    ///
436    /// Returns `Ok(None)` if no item previously existed at the key.
437    ///
438    /// This method is also available implicitly via `.await`.
439    ///
440    /// # Errors
441    ///
442    /// Returns [`Err`] if the DynamoDB request fails or if a condition check
443    /// fails.
444    ///
445    /// # Examples
446    ///
447    /// ```no_run
448    /// # use dynamodb_facade::test_fixtures::*;
449    /// use dynamodb_facade::DynamoDBItemOp;
450    ///
451    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
452    /// let old_raw = sample_user()
453    ///     .put(client)
454    ///     .return_old()
455    ///     .raw()
456    ///     .execute()
457    ///     .await?;
458    /// // old_raw: Option<Item<PlatformTable>>
459    /// # Ok(())
460    /// # }
461    /// ```
462    #[tracing::instrument(level = "debug", skip(self), name = "put_execute_old_raw")]
463    pub fn execute(self) -> impl Future<Output = Result<Option<Item<TD>>>> + Send + 'static {
464        let builder = self.builder;
465        async move {
466            Ok(builder
467                .return_values(SDKReturnValue::AllOld)
468                .send()
469                .await
470                .map(|out| out.attributes.map(Item::from_dynamodb_response))?)
471        }
472    }
473}
474
475impl<TD: TableDefinition, T, C: ConditionState> IntoFuture
476    for PutItemRequest<TD, T, Raw, Return<Old>, C>
477{
478    type Output = Result<Option<Item<TD>>>;
479    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
480
481    fn into_future(self) -> Self::IntoFuture {
482        Box::pin(self.execute())
483    }
484}