Skip to main content

dynamodb_facade/operations/
get.rs

1use std::future::{Future, IntoFuture};
2use std::pin::Pin;
3
4use super::*;
5
6use aws_sdk_dynamodb::operation::get_item::builders::GetItemFluentBuilder;
7
8/// Builder for a DynamoDB `GetItem` request.
9///
10/// Constructed via [`DynamoDBItemOp::get`] (typed, with a concrete `T`) or
11/// [`GetItemRequest::new`] (stand-alone, raw output). The builder provides:
12///
13/// - **Output format** — the result can be deserialized into `T`.
14///   Call [`.raw()`][GetItemRequest::raw] to receive an untyped [`Item<TD>`]
15///   instead (one-way).
16/// - **Projection** — call [`.project()`][GetItemRequest::project] to limit
17///   which attributes are returned. This can only be called once and
18///   automatically switches to raw output, since the projected result may
19///   not contain all fields required for deserialization.
20///
21/// The builder implements [`IntoFuture`], so it can
22/// be `.await`ed directly without calling `.execute()` explicitly.
23///
24/// # Errors
25///
26/// Returns [`Err`] if the DynamoDB request fails or if deserialization of
27/// the returned attributes fails.
28///
29/// # Examples
30///
31/// ```no_run
32/// # use dynamodb_facade::test_fixtures::*;
33/// use dynamodb_facade::{DynamoDBItemOp, KeyId};
34///
35/// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
36/// # let client = cclient.clone();
37/// // Simple get
38/// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1")).await?;
39///
40/// # let client = cclient.clone();
41/// // Consistent read
42/// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1"))
43///     .consistent_read()
44///     .await?;
45/// # Ok(())
46/// # }
47/// ```
48#[must_use = "builder does nothing until awaited or executed"]
49pub struct GetItemRequest<
50    TD: TableDefinition,
51    T = (),
52    O: OutputFormat = Raw,
53    P: ProjectionState = NoProjection,
54> {
55    builder: GetItemFluentBuilder,
56    _marker: PhantomData<(TD, T, O, P)>,
57}
58
59// -- Stand-alone constructor (T = (), O = Raw)
60
61impl<TD: TableDefinition> GetItemRequest<TD> {
62    /// Creates a stand-alone `GetItemRequest` with raw output (`T = ()`, `O = Raw`).
63    ///
64    /// Use this when you do not have a concrete item type and want to work with
65    /// the raw [`Item<TD>`] map directly. For typed access, prefer
66    /// [`DynamoDBItemOp::get`] instead.
67    ///
68    /// # Examples
69    ///
70    /// ```no_run
71    /// # use dynamodb_facade::test_fixtures::*;
72    /// use dynamodb_facade::{GetItemRequest, Key};
73    ///
74    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
75    /// # let key: Key<PlatformTable> = sample_user_item().into_key_only();
76    /// let raw_item = GetItemRequest::<PlatformTable>::new(client, key).await?;
77    /// # Ok(())
78    /// # }
79    /// ```
80    pub fn new(client: aws_sdk_dynamodb::Client, key: Key<TD>) -> Self {
81        Self::_new(client, key)
82    }
83}
84
85// -- Common methods (all states) --------------------------------------------
86
87impl<TD: TableDefinition, T, O: OutputFormat, P: ProjectionState> GetItemRequest<TD, T, O, P> {
88    /// Creates a new `GetItemRequest` targeting the given key.
89    pub(super) fn _new(client: aws_sdk_dynamodb::Client, key: Key<TD>) -> Self {
90        let table_name = TD::table_name();
91        tracing::debug!(table_name, ?key, "GetItem");
92        Self {
93            builder: client
94                .get_item()
95                .table_name(table_name)
96                .set_key(Some(key.into_inner())),
97            _marker: PhantomData,
98        }
99    }
100
101    /// Enables strongly consistent reads for this request.
102    ///
103    /// By default DynamoDB uses eventually consistent reads. Enabling consistent
104    /// reads guarantees the most up-to-date data but consumes twice the read
105    /// capacity units and is not supported on Global Secondary Indexes.
106    ///
107    /// # Examples
108    ///
109    /// ```no_run
110    /// # use dynamodb_facade::test_fixtures::*;
111    /// use dynamodb_facade::{DynamoDBItemOp, KeyId};
112    ///
113    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
114    /// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1"))
115    ///     .consistent_read()
116    ///     .await?;
117    /// # Ok(())
118    /// # }
119    /// ```
120    pub fn consistent_read(mut self) -> Self {
121        tracing::debug!("GetItem consistent_read");
122        self.builder = self.builder.consistent_read(true);
123        self
124    }
125
126    /// Consumes the builder and returns the underlying SDK
127    /// [`GetItemFluentBuilder`].
128    ///
129    /// Use this escape hatch when you need to set options not exposed by this
130    /// facade, such as `expression_attribute_names` for a manual projection, or
131    /// when integrating with code that expects the raw SDK builder.
132    ///
133    /// # Examples
134    ///
135    /// ```no_run
136    /// # use dynamodb_facade::test_fixtures::*;
137    /// use dynamodb_facade::{DynamoDBItemOp, KeyId};
138    ///
139    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
140    /// let sdk_builder = User::get(client, KeyId::pk("user-1")).into_inner();
141    /// // configure sdk_builder further, then call .send().await
142    /// # Ok(())
143    /// # }
144    /// ```
145    pub fn into_inner(self) -> GetItemFluentBuilder {
146        self.builder
147    }
148}
149
150// -- Projection (NoProjection only) -----------------------------------------
151
152impl<TD: TableDefinition, T, O: OutputFormat> GetItemRequest<TD, T, O, NoProjection> {
153    /// Applies a projection expression, limiting the attributes returned.
154    ///
155    /// This method can only be called once. It forces the output to raw
156    /// [`Item<TD>`] because projected results may not contain all fields
157    /// required for deserialization into `T`.
158    ///
159    /// # Examples
160    ///
161    /// ```no_run
162    /// # use dynamodb_facade::test_fixtures::*;
163    /// use dynamodb_facade::{AttributeDefinition, DynamoDBItemOp, KeyId, Projection};
164    ///
165    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
166    /// // Fetch only the "name" and "email" attributes
167    /// let partial /* : Option<Item<PlatformTable>> */ =
168    ///     User::get(client, KeyId::pk("user-1"))
169    ///         .project(Projection::new(["name", Email::NAME]))
170    ///         .await?;
171    /// // partial: contains only "PK", "SK", "name" and "email"
172    /// # Ok(())
173    /// # }
174    /// ```
175    pub fn project(
176        mut self,
177        projection: Projection<'_, TD>,
178    ) -> GetItemRequest<TD, T, Raw, AlreadyHasProjection> {
179        tracing::debug!(%projection, "GetItem project");
180        self.builder = projection.apply_projection(self.builder);
181        GetItemRequest {
182            builder: self.builder,
183            _marker: PhantomData,
184        }
185    }
186}
187
188// -- Output format transition (preserve P) ----------------------------------
189
190impl<TD: TableDefinition, T, P: ProjectionState> GetItemRequest<TD, T, Typed, P> {
191    /// Switches the output format from `Typed` to `Raw`.
192    ///
193    /// After calling `.raw()`, [`execute`][GetItemRequest::execute] returns
194    /// `Option<Item<TD>>` instead of `Option<T>`. This transition is one-way —
195    /// you cannot switch back to `Typed`.
196    ///
197    /// # Examples
198    ///
199    /// ```no_run
200    /// # use dynamodb_facade::test_fixtures::*;
201    /// use dynamodb_facade::{DynamoDBItemOp, KeyId};
202    ///
203    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
204    /// let raw_item = User::get(client, KeyId::pk("user-1"))
205    ///     .raw()
206    ///     .await?;
207    /// // raw_item: Option<Item<PlatformTable>>
208    /// if let Some(item) = raw_item {
209    ///     let name = item.get("name");
210    /// }
211    /// # Ok(())
212    /// # }
213    /// ```
214    pub fn raw(self) -> GetItemRequest<TD, T, Raw, P> {
215        GetItemRequest {
216            builder: self.builder,
217            _marker: PhantomData,
218        }
219    }
220}
221
222// -- Terminal: Typed (any P) ------------------------------------------------
223
224impl<TD: TableDefinition, T: DynamoDBItem<TD> + DeserializeOwned, P: ProjectionState>
225    GetItemRequest<TD, T, Typed, P>
226{
227    /// Sends the `GetItem` request and returns the item deserialized as `T`.
228    ///
229    /// Returns `Ok(None)` if no item exists for the given key.
230    ///
231    /// This method is also available implicitly via `.await` because
232    /// [`GetItemRequest`] implements [`IntoFuture`].
233    ///
234    /// # Errors
235    ///
236    /// Returns [`Err`] if the DynamoDB request fails or if deserialization of
237    /// the returned attributes fails.
238    ///
239    /// # Examples
240    ///
241    /// ```no_run
242    /// # use dynamodb_facade::test_fixtures::*;
243    /// use dynamodb_facade::{DynamoDBItemOp, KeyId};
244    ///
245    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
246    /// let user /* : Option<User> */ = User::get(client, KeyId::pk("user-1"))
247    ///     .consistent_read()
248    ///     .execute()
249    ///     .await?;
250    /// # Ok(())
251    /// # }
252    /// ```
253    #[tracing::instrument(level = "debug", skip(self), name = "get_execute")]
254    pub fn execute(self) -> impl Future<Output = Result<Option<T>>> + Send + 'static {
255        let builder = self.builder;
256        async move {
257            builder
258                .send()
259                .await?
260                .item
261                .map(Item::from_dynamodb_response)
262                .map(T::try_from_item)
263                .transpose()
264        }
265    }
266}
267
268impl<TD: TableDefinition, T: DynamoDBItem<TD> + DeserializeOwned, P: ProjectionState> IntoFuture
269    for GetItemRequest<TD, T, Typed, P>
270{
271    type Output = Result<Option<T>>;
272    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
273
274    fn into_future(self) -> Self::IntoFuture {
275        Box::pin(self.execute())
276    }
277}
278
279// -- Terminal: Raw (any P) --------------------------------------------------
280
281impl<TD: TableDefinition, T, P: ProjectionState> GetItemRequest<TD, T, Raw, P> {
282    /// Sends the `GetItem` request and returns the raw item map.
283    ///
284    /// Returns `Ok(None)` if no item exists for the given key.
285    ///
286    /// This method is also available implicitly via `.await` because
287    /// [`GetItemRequest`] implements [`IntoFuture`].
288    ///
289    /// # Errors
290    ///
291    /// Returns [`Err`] if the DynamoDB request fails.
292    ///
293    /// # Examples
294    ///
295    /// ```no_run
296    /// # use dynamodb_facade::test_fixtures::*;
297    /// use dynamodb_facade::{DynamoDBItemOp, KeyId};
298    ///
299    /// # async fn example(client: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
300    /// let raw = User::get(client, KeyId::pk("user-1"))
301    ///     .raw()
302    ///     .execute()
303    ///     .await?;
304    /// // raw: Option<Item<PlatformTable>>
305    /// if let Some(item) = raw {
306    ///     let name = item.get("name");
307    /// }
308    /// # Ok(())
309    /// # }
310    /// ```
311    #[tracing::instrument(level = "debug", skip(self), name = "get_execute_raw")]
312    pub fn execute(self) -> impl Future<Output = Result<Option<Item<TD>>>> + Send + 'static {
313        let builder = self.builder;
314        async move { Ok(builder.send().await?.item.map(Item::from_dynamodb_response)) }
315    }
316}
317
318impl<TD: TableDefinition, T, P: ProjectionState> IntoFuture for GetItemRequest<TD, T, Raw, P> {
319    type Output = Result<Option<Item<TD>>>;
320    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
321
322    fn into_future(self) -> Self::IntoFuture {
323        Box::pin(self.execute())
324    }
325}