momento_functions_host/aws/
ddb.rs

1//! Host interfaces for working with AWS DynamoDB
2//!
3//! It is recommended that you write type bindings for the types in your tables.
4//! See the examples on [Item] for how to do this.
5
6use std::collections::HashMap;
7
8use base64::Engine;
9use momento_functions_wit::host::momento::host;
10use serde::{Deserialize, Serialize};
11
12use crate::FunctionResult;
13
14use super::auth;
15
16/// Dynamodb client for host interfaces.
17///
18/// This client uses Momento's host-provided AWS communication channel, which
19/// is kept hot at all times. When your Function has not run in several days or more,
20/// the channel is still hot and ready, keeping your Function invocations predictable
21/// even when your demand is unpredictable.
22pub struct DynamoDBClient {
23    client: host::aws_ddb::Client,
24}
25
26impl DynamoDBClient {
27    /// Create a new DynamoDB client.
28    ///
29    /// ```rust
30    /// # use momento_functions_host::aws::auth::AwsCredentialsProvider;
31    /// # use momento_functions_host::aws::ddb::DynamoDBClient;
32    /// # use momento_functions_host::build_environment_aws_credentials;
33    /// # use momento_functions_host::FunctionResult;
34    /// # fn f() -> FunctionResult<()> {
35    /// let client = DynamoDBClient::new(
36    ///     &AwsCredentialsProvider::new(
37    ///         "us-east-1",
38    ///         build_environment_aws_credentials!()
39    ///     )?
40    /// );
41    /// # Ok(())
42    /// # }
43    /// ```
44    pub fn new(credentials: &auth::AwsCredentialsProvider) -> Self {
45        Self {
46            client: host::aws_ddb::Client::new(credentials.resource()),
47        }
48    }
49
50    /// Get an item from a DynamoDB table.
51    ///
52    /// Examples:
53    /// ________
54    /// Custom bound types:
55    /// ```rust
56    /// use momento_functions_host::aws::ddb::{AttributeValue, DynamoDBClient, Item};
57    /// use momento_functions_host::{FunctionResult, Error};
58    ///
59    /// /// Look up an item from a DynamoDB table and deserialize it into a MyStruct.
60    /// /// Returns None if the item does not exist.
61    /// fn get_my_struct(client: &DynamoDBClient, which_one: &str) -> FunctionResult<Option<MyStruct>> {
62    ///     client.get_item("my_table", ("some_attribute", which_one))
63    /// }
64    ///
65    /// struct MyStruct {
66    ///     some_attribute: String,
67    /// }
68    ///
69    /// // Boilerplate to convert into dynamodb format
70    /// impl From<MyStruct> for Item {
71    ///     fn from(value: MyStruct) -> Self {
72    ///         [
73    ///             ("some_attribute", AttributeValue::from(value.some_attribute)),
74    ///         ].into()
75    ///     }
76    /// }
77    ///
78    /// // Boilerplate to convert from dynamodb format
79    /// impl TryFrom<Item> for MyStruct {
80    ///     type Error = Error;
81    ///     fn try_from(mut value: Item) -> Result<Self, Self::Error> {
82    ///         Ok(Self {
83    ///             some_attribute: value.attributes.remove("some_attribute").ok_or("missing some_attribute")?.try_into()?,
84    ///         })
85    ///     }
86    /// }
87    pub fn get_item<V, E>(
88        &self,
89        table_name: impl Into<String>,
90        key: impl Into<Key>,
91    ) -> FunctionResult<Option<V>>
92    where
93        V: TryFrom<Item, Error = E>,
94        crate::Error: From<E>,
95    {
96        match self.get_item_raw(table_name, key)? {
97            Some(item) => Ok(Some(V::try_from(item)?)),
98            None => Ok(None),
99        }
100    }
101
102    /// Get an item from a DynamoDB table.
103    ///
104    /// Examples:
105    /// ________
106    /// ```rust
107    /// use momento_functions_host::aws::ddb::{DynamoDBClient, Item};
108    /// use momento_functions_host::FunctionResult;
109    ///
110    /// /// Read an item from a DynamoDB table "my_table" with a S key attribute "some_attribute".
111    /// fn get_some_item(client: &DynamoDBClient, which_one: &str) -> FunctionResult<Option<Item>> {
112    ///     client.get_item_raw("my_table", ("some_attribute", which_one))
113    /// }
114    /// ```
115    pub fn get_item_raw(
116        &self,
117        table_name: impl Into<String>,
118        key: impl Into<Key>,
119    ) -> FunctionResult<Option<Item>> {
120        let key: Key = key.into();
121
122        let output = self.client.get_item(&host::aws_ddb::GetItemRequest {
123            table_name: table_name.into(),
124            key: key.into(),
125            consistent_read: false,
126            return_consumed_capacity: host::aws_ddb::ReturnConsumedCapacity::None,
127            projection_expression: None,
128            expression_attribute_names: None,
129        })?;
130
131        match output.item {
132            Some(item) => {
133                match item {
134                    // {
135                    //   "profile_picture": { "B": "base64 string" },
136                    //   "is_valid": { "BOOL": true },
137                    //   "pictures": { "BS": ["base64 1", "base64 2"] },
138                    //   "friends": { "L": [{ "S": "bob" }, { "S": "alice" }] },
139                    //   "relationship": { "M": { "bob": {"S": "best friend"}, "alice": { "S": "second best friend" } } },
140                    //   "age": { "N": "23" },
141                    //   "favorite_birthdays": { "NS": ["17", "25"] },
142                    //   "children": { "NULL": true },
143                    //   "name": { "S": "arthur" },
144                    //   "friends": { "SS": ["bob", "alice"] }
145                    // }
146                    host::aws_ddb::Item::Json(j) => serde_json::from_str(&j).map_err(|e| {
147                        crate::Error::MessageError(format!(
148                            "failed to deserialize host json as item: {e}"
149                        ))
150                    }),
151                }
152            }
153            None => Ok(None),
154        }
155    }
156
157    /// Put an item into a DynamoDB table.
158    ///
159    /// Examples:
160    /// Raw item:
161    /// ________
162    /// ```rust
163    /// # use momento_functions_host::aws::ddb::DynamoDBClient;
164    /// # use momento_functions_host::FunctionResult;
165    ///
166    /// # fn put_some_item(client: &DynamoDBClient) -> FunctionResult<()> {
167    /// client.put_item(
168    ///     "my_table",
169    ///     [
170    ///         ("some_attribute", "some S value"),
171    ///         ("some_other_attribute", "some other S value"),
172    ///     ]
173    /// )
174    /// # }
175    /// ```
176    /// ________
177    /// Custom bound types:
178    /// ```rust
179    /// use momento_functions_host::aws::ddb::{AttributeValue, DynamoDBClient, Item};
180    /// use momento_functions_host::{FunctionResult, Error};
181    ///
182    /// /// Store an item in a DynamoDB table by serializing a MyStruct.
183    /// fn put_my_struct(client: &DynamoDBClient, which_one: MyStruct) -> FunctionResult<()> {
184    ///     client.put_item("my_table", which_one)
185    /// }
186    ///
187    /// struct MyStruct {
188    ///     some_attribute: String,
189    /// }
190    ///
191    /// // Boilerplate to convert into dynamodb format
192    /// impl From<MyStruct> for Item {
193    ///     fn from(value: MyStruct) -> Self {
194    ///         [
195    ///             ("some_attribute", AttributeValue::from(value.some_attribute)),
196    ///         ].into()
197    ///     }
198    /// }
199    ///
200    /// // Boilerplate to convert from dynamodb format
201    /// impl TryFrom<Item> for MyStruct {
202    ///     type Error = Error;
203    ///     fn try_from(mut value: Item) -> Result<Self, Self::Error> {
204    ///         Ok(Self {
205    ///             some_attribute: value.attributes.remove("some_attribute").ok_or("missing some_attribute")?.try_into()?,
206    ///         })
207    ///     }
208    /// }
209    pub fn put_item(
210        &self,
211        table_name: impl Into<String>,
212        item: impl Into<Item>,
213    ) -> FunctionResult<()> {
214        let item: Item = item.into();
215
216        let _output = self.client.put_item(&host::aws_ddb::PutItemRequest {
217            table_name: table_name.into(),
218            item: host::aws_ddb::Item::Json(serde_json::to_string(&item).map_err(|e| {
219                crate::Error::MessageError(format!("failed to serialize item as json: {e}"))
220            })?),
221            condition: None,
222            return_values: host::aws_ddb::ReturnValues::None,
223            return_consumed_capacity: host::aws_ddb::ReturnConsumedCapacity::None,
224        })?;
225
226        Ok(())
227    }
228}
229
230/// DynamoDB key type
231pub enum Key {
232    /// Hash key only
233    Hash {
234        /// Hash key name
235        key: String,
236        /// Hash key value
237        value: KeyValue,
238    },
239    /// Hash and range key
240    HashRange {
241        /// Hash key name
242        hash_key: String,
243        /// Hash key value
244        hash_value: KeyValue,
245        /// Range key name
246        range_key: String,
247        /// Range key value
248        range_value: KeyValue,
249    },
250}
251
252/// DynamoDB value type for keys
253#[derive(Debug, serde::Serialize, serde::Deserialize)]
254pub enum KeyValue {
255    /// S value
256    #[serde(rename = "S")]
257    String(String),
258    /// N value
259    #[serde(rename = "N")]
260    Number(i64),
261    /// B value
262    #[serde(rename = "B")]
263    Binary(Vec<u8>),
264}
265
266impl<K, V> From<(K, V)> for Key
267where
268    K: Into<String>,
269    V: Into<KeyValue>,
270{
271    fn from((k, v): (K, V)) -> Self {
272        Key::Hash {
273            key: k.into(),
274            value: v.into(),
275        }
276    }
277}
278
279impl From<Key> for Vec<host::aws_ddb::KeyAttribute> {
280    fn from(value: Key) -> Self {
281        match value {
282            Key::Hash { key, value } => vec![host::aws_ddb::KeyAttribute {
283                name: key,
284                value: value.into(),
285            }],
286            Key::HashRange {
287                hash_key,
288                hash_value,
289                range_key,
290                range_value,
291            } => vec![
292                host::aws_ddb::KeyAttribute {
293                    name: hash_key,
294                    value: hash_value.into(),
295                },
296                host::aws_ddb::KeyAttribute {
297                    name: range_key,
298                    value: range_value.into(),
299                },
300            ],
301        }
302    }
303}
304
305impl From<String> for KeyValue {
306    fn from(value: String) -> Self {
307        KeyValue::String(value)
308    }
309}
310impl From<&str> for KeyValue {
311    fn from(value: &str) -> Self {
312        KeyValue::String(value.to_string())
313    }
314}
315impl From<i64> for KeyValue {
316    fn from(value: i64) -> Self {
317        KeyValue::Number(value)
318    }
319}
320impl From<Vec<u8>> for KeyValue {
321    fn from(value: Vec<u8>) -> Self {
322        KeyValue::Binary(value)
323    }
324}
325impl From<&[u8]> for KeyValue {
326    fn from(value: &[u8]) -> Self {
327        KeyValue::Binary(value.to_vec())
328    }
329}
330impl From<KeyValue> for host::aws_ddb::KeyValue {
331    fn from(value: KeyValue) -> Self {
332        match value {
333            KeyValue::String(s) => host::aws_ddb::KeyValue::S(s),
334            KeyValue::Number(n) => host::aws_ddb::KeyValue::N(n.to_string()),
335            KeyValue::Binary(b) => host::aws_ddb::KeyValue::B(
336                base64::engine::general_purpose::STANDARD_NO_PAD.encode(b),
337            ),
338        }
339    }
340}
341
342impl From<host::aws_ddb::DdbError> for crate::Error {
343    fn from(e: host::aws_ddb::DdbError) -> Self {
344        match e {
345            host::aws_ddb::DdbError::Unauthorized(u) => Self::MessageError(u),
346            host::aws_ddb::DdbError::Malformed(s) => Self::MessageError(s),
347            host::aws_ddb::DdbError::Other(o) => Self::MessageError(o),
348        }
349    }
350}
351
352/// dynamodb-formatted json looks something like this:
353/// ```json
354/// {
355///   "profile_picture": { "B": "base64 string" },
356///   "is_valid": { "BOOL": true },
357///   "pictures": { "BS": ["base64 1", "base64 2"] },
358///   "friends": { "L": [{ "S": "bob" }, { "S": "alice" }] },
359///   "relationship": { "M": { "bob": {"S": "best friend"}, "alice": { "S": "second best friend" } } },
360///   "age": { "N": "23" },
361///   "favorite_birthdays": { "NS": ["17", "25"] },
362///   "children": { "NULL": true },
363///   "name": { "S": "arthur" },
364///   "friends": { "SS": ["bob", "alice"] }
365/// }
366/// ```
367/// This stuff exists mostly because WIT maintainers consider list<t> to be dependent on t,
368/// which causes much consternation with regard to serialization. Eventually they will
369/// likely work it out like json, protocol buffers, msgpack, and many other serialization
370/// formats before it.
371///
372/// Examples:
373/// ________
374/// Basic explicit lists:
375/// ```rust
376/// use momento_functions_host::aws::ddb::Item;
377/// let item: Item = vec![("some key", "some value")].into();
378/// let item: Item = vec![("some key", 42)].into();
379/// ```
380/// ________
381/// Custom bound types:
382/// ```rust
383/// use momento_functions_host::aws::ddb::{AttributeValue, Item};
384/// use momento_functions_host::Error;
385/// struct MyStruct {
386///     some_attribute: String,
387/// }
388///
389/// // convert into dynamodb format
390/// impl From<MyStruct> for Item {
391///     fn from(value: MyStruct) -> Self {
392///         [
393///             ("some_attribute", AttributeValue::from(value.some_attribute)),
394///         ].into()
395///     }
396/// }
397///
398/// // convert from dynamodb format
399/// impl TryFrom<Item> for MyStruct {
400///     type Error = Error;
401///     fn try_from(mut value: Item) -> Result<Self, Self::Error> {
402///         Ok(Self {
403///             some_attribute: value.attributes.remove("some_attribute").ok_or("missing some_attribute")?.try_into()?,
404///         })
405///     }
406/// }
407///
408/// let item: Item = MyStruct { some_attribute: "some value".to_string() }.into();
409/// ```
410#[derive(Debug, Serialize, Deserialize)]
411pub struct Item {
412    /// The item object
413    #[serde(flatten)]
414    pub attributes: HashMap<String, AttributeValue>,
415}
416
417/// A value within the item object
418#[derive(Debug, Serialize, Deserialize)]
419pub enum AttributeValue {
420    /// A B value
421    #[serde(rename = "B")]
422    Binary(String),
423    /// A BOOL value
424    #[serde(rename = "BOOL")]
425    Boolean(bool),
426    /// A BS value
427    #[serde(rename = "BS")]
428    BinarySet(Vec<String>),
429    /// An L value
430    #[serde(rename = "L")]
431    List(Vec<AttributeValue>),
432    /// An M value
433    #[serde(rename = "M")]
434    Map(HashMap<String, AttributeValue>),
435    /// An N value
436    #[serde(rename = "N")]
437    Number(String),
438    /// An NS value
439    #[serde(rename = "NS")]
440    NumberSet(Vec<String>),
441    /// A NULL value
442    #[serde(rename = "NULL")]
443    Null(bool),
444    /// An S value
445    #[serde(rename = "S")]
446    String(String),
447    /// An SS value
448    #[serde(rename = "SS")]
449    StringSet(Vec<String>),
450}
451
452impl From<String> for AttributeValue {
453    fn from(value: String) -> Self {
454        AttributeValue::String(value)
455    }
456}
457impl From<&str> for AttributeValue {
458    fn from(value: &str) -> Self {
459        AttributeValue::String(value.to_string())
460    }
461}
462impl From<bool> for AttributeValue {
463    fn from(value: bool) -> Self {
464        AttributeValue::Boolean(value)
465    }
466}
467impl From<Vec<AttributeValue>> for AttributeValue {
468    fn from(value: Vec<AttributeValue>) -> Self {
469        AttributeValue::List(value)
470    }
471}
472impl<S> From<HashMap<S, AttributeValue>> for AttributeValue
473where
474    S: Into<String>,
475{
476    fn from(value: HashMap<S, AttributeValue>) -> Self {
477        AttributeValue::Map(value.into_iter().map(|(k, v)| (k.into(), v)).collect())
478    }
479}
480impl From<i64> for AttributeValue {
481    fn from(value: i64) -> Self {
482        AttributeValue::Number(value.to_string())
483    }
484}
485impl From<Vec<u8>> for AttributeValue {
486    fn from(value: Vec<u8>) -> Self {
487        AttributeValue::Binary(base64::engine::general_purpose::STANDARD_NO_PAD.encode(value))
488    }
489}
490impl TryFrom<AttributeValue> for String {
491    type Error = crate::Error;
492    fn try_from(value: AttributeValue) -> FunctionResult<Self> {
493        match value {
494            AttributeValue::String(s) => Ok(s),
495            _ => Err(Self::Error::MessageError("not a string".to_string())),
496        }
497    }
498}
499impl TryFrom<AttributeValue> for bool {
500    type Error = crate::Error;
501    fn try_from(value: AttributeValue) -> FunctionResult<Self> {
502        match value {
503            AttributeValue::Boolean(b) => Ok(b),
504            _ => Err(Self::Error::MessageError("not a bool".to_string())),
505        }
506    }
507}
508impl TryFrom<AttributeValue> for i64 {
509    type Error = crate::Error;
510    fn try_from(value: AttributeValue) -> FunctionResult<Self> {
511        match value {
512            AttributeValue::Number(n) => n
513                .parse::<i64>()
514                .map_err(|e| Self::Error::MessageError(format!("invalid number: {e}"))),
515            _ => Err(Self::Error::MessageError("not a number".to_string())),
516        }
517    }
518}
519impl TryFrom<AttributeValue> for Vec<u8> {
520    type Error = crate::Error;
521    fn try_from(value: AttributeValue) -> FunctionResult<Self> {
522        match value {
523            AttributeValue::Binary(b) => base64::engine::general_purpose::STANDARD_NO_PAD
524                .decode(b)
525                .map_err(|e| Self::Error::MessageError(format!("invalid base64: {e}"))),
526            _ => Err(Self::Error::MessageError("not a binary".to_string())),
527        }
528    }
529}
530
531impl<I, S, V> From<I> for Item
532where
533    I: IntoIterator<Item = (S, V)>,
534    S: Into<String>,
535    V: Into<AttributeValue>,
536{
537    fn from(value: I) -> Self {
538        Item {
539            attributes: value
540                .into_iter()
541                .map(|(k, v)| (k.into(), v.into()))
542                .collect(),
543        }
544    }
545}