Skip to main content

dynamodb_facade/operations/
delete.rs

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