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
253pub enum KeyValue {
254    /// S value
255    String(String),
256    /// N value
257    Number(i64),
258    /// B value
259    Binary(Vec<u8>),
260}
261
262impl<K, V> From<(K, V)> for Key
263where
264    K: Into<String>,
265    V: Into<KeyValue>,
266{
267    fn from((k, v): (K, V)) -> Self {
268        Key::Hash {
269            key: k.into(),
270            value: v.into(),
271        }
272    }
273}
274
275impl From<Key> for Vec<host::aws_ddb::KeyAttribute> {
276    fn from(value: Key) -> Self {
277        match value {
278            Key::Hash { key, value } => vec![host::aws_ddb::KeyAttribute {
279                name: key,
280                value: value.into(),
281            }],
282            Key::HashRange {
283                hash_key,
284                hash_value,
285                range_key,
286                range_value,
287            } => vec![
288                host::aws_ddb::KeyAttribute {
289                    name: hash_key,
290                    value: hash_value.into(),
291                },
292                host::aws_ddb::KeyAttribute {
293                    name: range_key,
294                    value: range_value.into(),
295                },
296            ],
297        }
298    }
299}
300
301impl From<String> for KeyValue {
302    fn from(value: String) -> Self {
303        KeyValue::String(value)
304    }
305}
306impl From<&str> for KeyValue {
307    fn from(value: &str) -> Self {
308        KeyValue::String(value.to_string())
309    }
310}
311impl From<i64> for KeyValue {
312    fn from(value: i64) -> Self {
313        KeyValue::Number(value)
314    }
315}
316impl From<Vec<u8>> for KeyValue {
317    fn from(value: Vec<u8>) -> Self {
318        KeyValue::Binary(value)
319    }
320}
321impl From<&[u8]> for KeyValue {
322    fn from(value: &[u8]) -> Self {
323        KeyValue::Binary(value.to_vec())
324    }
325}
326impl From<KeyValue> for host::aws_ddb::KeyValue {
327    fn from(value: KeyValue) -> Self {
328        match value {
329            KeyValue::String(s) => host::aws_ddb::KeyValue::S(s),
330            KeyValue::Number(n) => host::aws_ddb::KeyValue::N(n.to_string()),
331            KeyValue::Binary(b) => host::aws_ddb::KeyValue::B(
332                base64::engine::general_purpose::STANDARD_NO_PAD.encode(b),
333            ),
334        }
335    }
336}
337
338impl From<host::aws_ddb::DdbError> for crate::Error {
339    fn from(e: host::aws_ddb::DdbError) -> Self {
340        match e {
341            host::aws_ddb::DdbError::Unauthorized(u) => Self::MessageError(u),
342            host::aws_ddb::DdbError::Malformed(s) => Self::MessageError(s),
343            host::aws_ddb::DdbError::Other(o) => Self::MessageError(o),
344        }
345    }
346}
347
348/// dynamodb-formatted json looks something like this:
349/// ```json
350/// {
351///   "profile_picture": { "B": "base64 string" },
352///   "is_valid": { "BOOL": true },
353///   "pictures": { "BS": ["base64 1", "base64 2"] },
354///   "friends": { "L": [{ "S": "bob" }, { "S": "alice" }] },
355///   "relationship": { "M": { "bob": {"S": "best friend"}, "alice": { "S": "second best friend" } } },
356///   "age": { "N": "23" },
357///   "favorite_birthdays": { "NS": ["17", "25"] },
358///   "children": { "NULL": true },
359///   "name": { "S": "arthur" },
360///   "friends": { "SS": ["bob", "alice"] }
361/// }
362/// ```
363/// This stuff exists mostly because WIT maintainers consider list<t> to be dependent on t,
364/// which causes much consternation with regard to serialization. Eventually they will
365/// likely work it out like json, protocol buffers, msgpack, and many other serialization
366/// formats before it.
367///
368/// Examples:
369/// ________
370/// Basic explicit lists:
371/// ```rust
372/// use momento_functions_host::aws::ddb::Item;
373/// let item: Item = vec![("some key", "some value")].into();
374/// let item: Item = vec![("some key", 42)].into();
375/// ```
376/// ________
377/// Custom bound types:
378/// ```rust
379/// use momento_functions_host::aws::ddb::{AttributeValue, Item};
380/// use momento_functions_host::Error;
381/// struct MyStruct {
382///     some_attribute: String,
383/// }
384///
385/// // convert into dynamodb format
386/// impl From<MyStruct> for Item {
387///     fn from(value: MyStruct) -> Self {
388///         [
389///             ("some_attribute", AttributeValue::from(value.some_attribute)),
390///         ].into()
391///     }
392/// }
393///
394/// // convert from dynamodb format
395/// impl TryFrom<Item> for MyStruct {
396///     type Error = Error;
397///     fn try_from(mut value: Item) -> Result<Self, Self::Error> {
398///         Ok(Self {
399///             some_attribute: value.attributes.remove("some_attribute").ok_or("missing some_attribute")?.try_into()?,
400///         })
401///     }
402/// }
403///
404/// let item: Item = MyStruct { some_attribute: "some value".to_string() }.into();
405/// ```
406#[derive(Debug, Serialize, Deserialize)]
407pub struct Item {
408    /// The item object
409    #[serde(flatten)]
410    pub attributes: HashMap<String, AttributeValue>,
411}
412
413/// A value within the item object
414#[derive(Debug, Serialize, Deserialize)]
415pub enum AttributeValue {
416    /// A B value
417    #[serde(rename = "B")]
418    Binary(String),
419    /// A BOOL value
420    #[serde(rename = "BOOL")]
421    Boolean(bool),
422    /// A BS value
423    #[serde(rename = "BS")]
424    BinarySet(Vec<String>),
425    /// An L value
426    #[serde(rename = "L")]
427    List(Vec<AttributeValue>),
428    /// An M value
429    #[serde(rename = "M")]
430    Map(HashMap<String, AttributeValue>),
431    /// An N value
432    #[serde(rename = "N")]
433    Number(String),
434    /// An NS value
435    #[serde(rename = "NS")]
436    NumberSet(Vec<String>),
437    /// A NULL value
438    #[serde(rename = "NULL")]
439    Null(bool),
440    /// An S value
441    #[serde(rename = "S")]
442    String(String),
443    /// An SS value
444    #[serde(rename = "SS")]
445    StringSet(Vec<String>),
446}
447
448impl From<String> for AttributeValue {
449    fn from(value: String) -> Self {
450        AttributeValue::String(value)
451    }
452}
453impl From<&str> for AttributeValue {
454    fn from(value: &str) -> Self {
455        AttributeValue::String(value.to_string())
456    }
457}
458impl From<bool> for AttributeValue {
459    fn from(value: bool) -> Self {
460        AttributeValue::Boolean(value)
461    }
462}
463impl From<Vec<AttributeValue>> for AttributeValue {
464    fn from(value: Vec<AttributeValue>) -> Self {
465        AttributeValue::List(value)
466    }
467}
468impl<S> From<HashMap<S, AttributeValue>> for AttributeValue
469where
470    S: Into<String>,
471{
472    fn from(value: HashMap<S, AttributeValue>) -> Self {
473        AttributeValue::Map(value.into_iter().map(|(k, v)| (k.into(), v)).collect())
474    }
475}
476impl From<i64> for AttributeValue {
477    fn from(value: i64) -> Self {
478        AttributeValue::Number(value.to_string())
479    }
480}
481impl From<Vec<u8>> for AttributeValue {
482    fn from(value: Vec<u8>) -> Self {
483        AttributeValue::Binary(base64::engine::general_purpose::STANDARD_NO_PAD.encode(value))
484    }
485}
486impl TryFrom<AttributeValue> for String {
487    type Error = crate::Error;
488    fn try_from(value: AttributeValue) -> FunctionResult<Self> {
489        match value {
490            AttributeValue::String(s) => Ok(s),
491            _ => Err(Self::Error::MessageError("not a string".to_string())),
492        }
493    }
494}
495impl TryFrom<AttributeValue> for bool {
496    type Error = crate::Error;
497    fn try_from(value: AttributeValue) -> FunctionResult<Self> {
498        match value {
499            AttributeValue::Boolean(b) => Ok(b),
500            _ => Err(Self::Error::MessageError("not a bool".to_string())),
501        }
502    }
503}
504impl TryFrom<AttributeValue> for i64 {
505    type Error = crate::Error;
506    fn try_from(value: AttributeValue) -> FunctionResult<Self> {
507        match value {
508            AttributeValue::Number(n) => n
509                .parse::<i64>()
510                .map_err(|e| Self::Error::MessageError(format!("invalid number: {e}"))),
511            _ => Err(Self::Error::MessageError("not a number".to_string())),
512        }
513    }
514}
515impl TryFrom<AttributeValue> for Vec<u8> {
516    type Error = crate::Error;
517    fn try_from(value: AttributeValue) -> FunctionResult<Self> {
518        match value {
519            AttributeValue::Binary(b) => base64::engine::general_purpose::STANDARD_NO_PAD
520                .decode(b)
521                .map_err(|e| Self::Error::MessageError(format!("invalid base64: {e}"))),
522            _ => Err(Self::Error::MessageError("not a binary".to_string())),
523        }
524    }
525}
526
527impl<I, S, V> From<I> for Item
528where
529    I: IntoIterator<Item = (S, V)>,
530    S: Into<String>,
531    V: Into<AttributeValue>,
532{
533    fn from(value: I) -> Self {
534        Item {
535            attributes: value
536                .into_iter()
537                .map(|(k, v)| (k.into(), v.into()))
538                .collect(),
539        }
540    }
541}