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}