easy_dynamodb/
lib.rs

1#![allow(static_mut_refs)]
2pub mod error;
3
4use aws_config::retry::RetryConfig;
5use aws_config::timeout::TimeoutConfig;
6use aws_sdk_dynamodb::types::AttributeValue;
7use aws_sdk_dynamodb::Error;
8use error::DynamoException;
9use slog::{debug, o, Logger};
10
11use std::collections::HashMap;
12use std::fmt::Debug;
13use std::time::Duration;
14
15static mut CLI: Option<Client> = None;
16
17pub trait Document {
18    fn document_type() -> String;
19}
20
21pub fn init(
22    logger: Logger,
23    access_key_id: String,
24    secret_access_key: String,
25    region: String,
26    table_name: String,
27    key_field: String,
28
29    // TODO: not implement sort key yet
30    sort_key_field: Option<String>,
31    endpoint: Option<String>,
32) {
33    unsafe {
34        CLI = Some(Client::new(
35            logger,
36            access_key_id,
37            secret_access_key,
38            region,
39            table_name,
40            key_field,
41            sort_key_field,
42            endpoint,
43        ));
44    }
45}
46
47pub fn get_client(logger: &Logger) -> Client {
48    let mut cli = unsafe { CLI.clone().unwrap() };
49    cli.log = logger.new(o!("crate" => "easy-dynamodb"));
50    cli
51}
52
53#[derive(Clone, Debug)]
54pub struct Client {
55    client: aws_sdk_dynamodb::Client,
56    table_name: String,
57    log: Logger,
58    key_field: String,
59    #[allow(dead_code)]
60    sort_key_field: Option<String>,
61}
62
63impl Client {
64    pub fn new(
65        logger: Logger,
66        access_key_id: String,
67        secret_access_key: String,
68        region: String,
69        table_name: String,
70        key_field: String,
71
72        // TODO: not implement sort key yet
73        sort_key_field: Option<String>,
74        endpoint: Option<String>,
75    ) -> Self {
76        use aws_config::Region;
77        use aws_sdk_dynamodb::Config;
78
79        let timeout_config = TimeoutConfig::builder()
80            .operation_attempt_timeout(Duration::from_secs(5))
81            .build();
82
83        let retry_config = RetryConfig::standard().with_max_attempts(3);
84        let config = Config::builder()
85            .credentials_provider(aws_credential_types::Credentials::from_keys(
86                access_key_id,
87                secret_access_key,
88                None,
89            ))
90            .region(Region::new(region))
91            .set_timeout_config(Some(timeout_config))
92            .set_retry_config(Some(retry_config))
93            .clone();
94
95        let config = match endpoint {
96            Some(endpoint_url) => config.endpoint_url(endpoint_url),
97            None => config,
98        };
99
100        let config = config.build();
101
102        Client {
103            client: aws_sdk_dynamodb::Client::from_conf(config),
104            table_name,
105            key_field,
106            sort_key_field,
107            log: logger.new(o!("crate" => "easy-dynamodb", "struct" => "Client")),
108        }
109    }
110
111    pub fn get_client(&self) -> aws_sdk_dynamodb::Client {
112        self.client.clone()
113    }
114
115    fn get_log(&self, method: &'static str) -> Logger {
116        self.log.new(o!("method" => method))
117    }
118
119    pub async fn upsert<T>(&self, doc: T) -> Result<(), DynamoException>
120    where
121        T: Debug + serde::Serialize,
122    {
123        let log = self.get_log("create");
124        debug!(log, "{:?}", doc);
125        let value = match serde_json::to_value(doc) {
126            Ok(value) => value,
127            Err(e) => return Err(DynamoException::DynamoSerializeException(format!("{e:?}"))),
128        };
129        let item = match serde_dynamo::to_item(value) {
130            Ok(item) => item,
131            Err(e) => return Err(DynamoException::DynamoSerializeException(format!("{e:?}"))),
132        };
133
134        match self
135            .client
136            .put_item()
137            .table_name(&self.table_name)
138            .set_item(Some(item))
139            .send()
140            .await
141        {
142            Ok(_) => Ok(()),
143            Err(e) => Err(DynamoException::DynamoPutItemException(format!("{e:?}"))),
144        }
145    }
146
147    pub async fn create<T>(&self, doc: T) -> Result<(), DynamoException>
148    where
149        T: Debug + serde::Serialize,
150    {
151        let log = self.get_log("create");
152        debug!(log, "{:?}", doc);
153        let value = match serde_json::to_value(doc) {
154            Ok(value) => value,
155            Err(e) => return Err(DynamoException::DynamoSerializeException(format!("{e:?}"))),
156        };
157        let item: std::collections::HashMap<::std::string::String, AttributeValue> =
158            match serde_dynamo::to_item(value) {
159                Ok(item) => item,
160                Err(e) => return Err(DynamoException::DynamoSerializeException(format!("{e:?}"))),
161            };
162        let condition = format!("attribute_not_exists({})", self.key_field);
163
164        match self
165            .client
166            .put_item()
167            .table_name(&self.table_name)
168            .set_item(Some(item.clone()))
169            .condition_expression(&condition)
170            .send()
171            .await
172        {
173            Ok(_) => Ok(()),
174            Err(e) => Err(DynamoException::DynamoPutItemException(format!(
175                "{e:?}, {item:?}"
176            ))),
177        }
178    }
179
180    pub async fn update<T>(&self, key: &str, fields: Vec<(&str, T)>) -> Result<(), DynamoException>
181    where
182        T: Debug + serde::Serialize,
183    {
184        let log = self.get_log("update");
185        debug!(log, "{:?} {:?}", key, fields);
186
187        let (mut names, values, condition) = self.to_attribute_names_and_values(fields)?;
188
189        let update_expression = format!("SET {}", &condition.join(", "));
190
191        let key_field = self.key_field.clone();
192        let key_value = AttributeValue::S(key.to_string());
193
194        let condition_expression = format!("attribute_exists(#key)");
195        names.insert("#key".to_string(), key_field.clone().into());
196
197        debug!(log, "update_expression({:?}), key_field({:?}), key_value({:?}), condition_expression({:?}) names({names:?}), values({values:?})", update_expression, key_field, key_value, condition_expression);
198
199        match self
200            .client
201            .update_item()
202            .table_name(&self.table_name)
203            .key(key_field, key_value)
204            .update_expression(&update_expression)
205            .set_expression_attribute_names(Some(names))
206            .set_expression_attribute_values(Some(values))
207            .condition_expression(condition_expression)
208            .send()
209            .await
210        {
211            Ok(e) => {
212                debug!(log, "succeed {:?}", e);
213                Ok(())
214            }
215            Err(e) => Err(DynamoException::DynamoUpdateItemException(format!("{e:?}"))),
216        }
217    }
218
219    pub async fn delete(&self, key: &str) -> Result<(), DynamoException> {
220        let log = self.get_log("delete");
221        debug!(log, "{:?}", key);
222
223        match self
224            .client
225            .delete_item()
226            .table_name(&self.table_name)
227            .key(self.key_field.clone(), AttributeValue::S(key.to_string()))
228            .send()
229            .await
230        {
231            Ok(_) => Ok(()),
232            Err(e) => Err(DynamoException::DynamoDeleteItemException(format!("{e:?}"))),
233        }
234    }
235
236    pub async fn get<T>(&self, key: &str) -> Result<Option<T>, DynamoException>
237    where
238        T: Debug + serde::de::DeserializeOwned,
239    {
240        let log = self.get_log("get");
241        debug!(log, "{:?}", key);
242        let resp = match self
243            .client
244            .get_item()
245            .table_name(&self.table_name)
246            .key(self.key_field.clone(), AttributeValue::S(key.to_string()))
247            .send()
248            .await
249        {
250            Ok(resp) => resp,
251            Err(e) => return Err(DynamoException::DynamoGetItemException(format!("{e:?}"))),
252        };
253
254        Ok(match resp.item {
255            Some(item) => {
256                debug!(log, "item: {:?}", item);
257                let value: T = match serde_dynamo::from_item(item) {
258                    Ok(value) => value,
259                    Err(e) => {
260                        return Err(DynamoException::DynamoSerializeException(format!("{e:?}")))
261                    }
262                };
263                Some(value)
264            }
265            None => None,
266        })
267    }
268
269    fn to_attribute_names_and_values<F>(
270        &self,
271        filter: Vec<(&str, F)>,
272    ) -> Result<
273        (
274            HashMap<String, String>,
275            HashMap<String, AttributeValue>,
276            Vec<String>,
277        ),
278        DynamoException,
279    >
280    where
281        F: Debug + serde::Serialize,
282    {
283        let mut names = HashMap::new();
284        let mut values = HashMap::new();
285        let mut condition = vec![];
286
287        for (name, value) in filter.iter() {
288            names.insert(format!("#{name}"), name.to_string().clone());
289            let value = match serde_dynamo::to_attribute_value(value) {
290                Ok(value) => value,
291                Err(e) => return Err(DynamoException::DynamoSerializeException(format!("{e:?}"))),
292            };
293            values.insert(format!(":{name}"), value);
294
295            condition.push(format!("#{name} = :{name}"));
296        }
297
298        Ok((names, values, condition))
299    }
300
301    pub async fn find<T, F>(
302        &self,
303        index: &str,
304        bookmark: Option<String>,
305        size: Option<i32>,
306        filter: Vec<(&str, F)>,
307    ) -> Result<(Option<Vec<T>>, Option<String>), DynamoException>
308    where
309        T: Debug + serde::de::DeserializeOwned,
310        F: Debug + serde::Serialize,
311    {
312        let log = self.get_log("find");
313        debug!(
314            log,
315            "index: {:?} bookmark: {:?} size: {:?} filter: {:?}", index, bookmark, size, filter
316        );
317
318        let (names, values, condition) = self.to_attribute_names_and_values(filter)?;
319        let size = size.unwrap_or(10);
320        let bookmark = self.decode_bookmark(bookmark);
321        let key_condition = &condition.join(" AND ");
322
323        debug!(
324            log,
325            "key_condition: {:?} names: {:?} values: {:?} size: {:?}",
326            key_condition,
327            names,
328            values,
329            size
330        );
331
332        let resp = match self
333            .client
334            .query()
335            .table_name(&self.table_name)
336            .set_exclusive_start_key(bookmark)
337            .index_name(index)
338            .key_condition_expression(key_condition)
339            .set_expression_attribute_names(Some(names))
340            .set_expression_attribute_values(Some(values))
341            .limit(size)
342            .send()
343            .await
344        {
345            Ok(resp) => resp,
346            Err(e) => {
347                return Err(DynamoException::DynamoQueryException(format!("{e:?}")));
348            }
349        };
350
351        crate::debug!(log, "response {:?}", resp);
352
353        let docs = match resp.items {
354            Some(items) => match serde_dynamo::from_items(items) {
355                Ok(value) => Some(value),
356                Err(e) => return Err(DynamoException::DynamoSerializeException(format!("{e:?}"))),
357            },
358            None => None,
359        };
360
361        crate::debug!(log, "docs: {:?}", docs);
362
363        let bookmark = self.encode_bookmark(resp.last_evaluated_key);
364        Ok((docs, bookmark))
365    }
366
367    pub async fn increment(
368        &self,
369        key: &str,
370        field: &str,
371        value: i64,
372    ) -> Result<(), DynamoException> {
373        let log = self.get_log("increment");
374        debug!(log, "{:?} {:?} {:?}", key, field, value);
375
376        let update_expression = format!("ADD #cnt :val");
377        let condition_expression = format!("attribute_exists(#key)");
378
379        let mut names = HashMap::new();
380        names.insert("#cnt".to_string(), field.into());
381        names.insert("#key".to_string(), self.key_field.clone().into());
382
383        let mut values = HashMap::new();
384        values.insert(":val".to_string(), AttributeValue::N(value.to_string()));
385
386        match self
387            .client
388            .update_item()
389            .table_name(&self.table_name)
390            .key(self.key_field.clone(), AttributeValue::S(key.to_string()))
391            .update_expression(&update_expression)
392            .condition_expression(condition_expression)
393            .set_expression_attribute_names(Some(names))
394            .set_expression_attribute_values(Some(values))
395            .send()
396            .await
397        {
398            Ok(_) => Ok(()),
399            Err(e) => Err(DynamoException::DynamoIncrementException(format!("{e:?}"))),
400        }
401    }
402
403    fn encode_bookmark(&self, bookmark: Option<HashMap<String, AttributeValue>>) -> Option<String> {
404        if bookmark.is_none() {
405            return None;
406        }
407
408        let bookmark = bookmark.unwrap();
409        let bookmark = BookmarkModel::new(bookmark);
410        Some(bookmark.to_string())
411    }
412
413    fn decode_bookmark(&self, bookmark: Option<String>) -> Option<HashMap<String, AttributeValue>> {
414        if bookmark.is_none() {
415            return None;
416        }
417
418        let bookmark = BookmarkModel::from_string(&bookmark.unwrap());
419        let mut result = HashMap::new();
420
421        for (key, value) in bookmark.keys.iter().zip(bookmark.values.iter()) {
422            result.insert(key.clone(), value.clone().into());
423        }
424
425        Some(result)
426    }
427}
428
429#[derive(Debug, serde::Serialize, serde::Deserialize)]
430struct BookmarkModel {
431    keys: Vec<String>,
432    values: Vec<serde_dynamo::AttributeValue>,
433}
434
435impl BookmarkModel {
436    fn new(bookmark: HashMap<String, AttributeValue>) -> Self {
437        let mut keys = vec![];
438        let mut values = vec![];
439
440        for (key, value) in bookmark {
441            keys.push(key.clone());
442            values.push(value.into());
443        }
444        BookmarkModel { keys, values }
445    }
446
447    fn to_string(&self) -> String {
448        serde_json::to_string(self).unwrap()
449    }
450
451    fn from_string(s: &str) -> Self {
452        serde_json::from_str(s).unwrap()
453    }
454}
455
456impl Client {
457    pub async fn table_exists(&self) -> Result<bool, Error> {
458        let request = self.client.describe_table().table_name(&self.table_name);
459
460        let resp = request.send().await;
461        match resp {
462            Ok(_) => Ok(true),
463            Err(_) => Ok(false),
464        }
465    }
466
467    pub async fn list_tables(&self) -> Result<Vec<String>, Error> {
468        let paginator = self.client.list_tables().into_paginator().items().send();
469        let table_names = paginator.collect::<Result<Vec<_>, _>>().await?;
470
471        Ok(table_names)
472    }
473
474    pub async fn get_total_items(&self) -> Result<u64, Error> {
475        use aws_sdk_dynamodb::types::Select;
476
477        let request = self
478            .client
479            .scan()
480            .table_name(&self.table_name)
481            .select(Select::Count);
482
483        let resp = request.send().await?;
484        let count = resp.count as u64;
485        Ok(count)
486    }
487
488    pub async fn scan_table_items(&self) -> Result<Vec<HashMap<String, AttributeValue>>, Error> {
489        let request = self.client.scan().table_name(&self.table_name);
490
491        let resp = request.send().await?;
492        let result = resp.items.unwrap_or_else(|| vec![]);
493
494        Ok(result)
495    }
496}
497
498// #[cfg(test)]
499// mod dyanomdb_tests {
500//     use std::thread;
501
502//     use super::*;
503
504//     #[derive(Debug, serde::Serialize, serde::Deserialize)]
505//     struct TestModel {
506//         key: String,
507//         id: String,
508//         created_at: i64,
509//     }
510
511//     #[derive(Debug, serde::Serialize, serde::Deserialize)]
512//     struct TestModelV2 {
513//         key: String,
514//         id: String,
515//         created_at: i64,
516//         str_field: String,
517//         bool_field: bool,
518//     }
519
520//     #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
521//     struct IndexModel {
522//         key: String,
523//         id: String,
524//         created_at: i64,
525//         r#type: String,
526//     }
527
528//     #[derive(Debug, serde::Serialize, serde::Deserialize)]
529//     enum TestField {
530//         #[serde(untagged)]
531//         N(i64),
532//         #[serde(untagged)]
533//         S(String),
534//         #[serde(untagged)]
535//         B(bool),
536//     }
537
538//     fn new_cli() -> Client {
539//         Client::new(
540//             slog::Logger::root(slog::Discard, o!("goal" => "testing")),
541//             option_env!("AWS_ACCESS_KEY_ID")
542//                 .expect("AWS_ACCESS_KEY_ID is required")
543//                 .to_string(),
544//             option_env!("AWS_SECRET_ACCESS_KEY")
545//                 .expect("AWS_SECRET_ACCESS_KEY is required")
546//                 .to_string(),
547//             option_env!("AWS_REGION")
548//                 .unwrap_or("ap-northeast-2")
549//                 .to_string(),
550//             option_env!("AWS_DYNAMODB_TABLE")
551//                 .expect("AWS_DYNAMODB_TABLE is required")
552//                 .to_string(),
553//             option_env!("AWS_DYNAMODB_KEY").unwrap_or("key").to_string(),
554//             None,
555//             None,
556//         )
557//     }
558
559//     #[tokio::test]
560//     async fn test_create() {
561//         let client = new_cli();
562//         let ts = chrono::Utc::now().timestamp_nanos_opt();
563//         assert!(ts.is_some(), "timestamp is none");
564//         let ts = ts.unwrap();
565
566//         let result = client
567//             .create(TestModel {
568//                 key: format!("test_create_key-{ts}"),
569//                 id: format!("test_create_id-{ts}"),
570//                 created_at: ts,
571//             })
572//             .await;
573
574//         assert!(result.is_ok(), "test_create failed: {result:?}");
575//     }
576
577//     #[tokio::test]
578//     async fn test_create_duplicated_error() {
579//         let client = new_cli();
580//         let ts = chrono::Utc::now().timestamp_nanos_opt();
581//         assert!(ts.is_some(), "timestamp is none");
582//         let ts = ts.unwrap();
583
584//         let result = client
585//             .create(TestModel {
586//                 key: format!("test_create_duplicated_error_key-{ts}"),
587//                 id: format!("test_create_duplicated_error_id-{ts}"),
588//                 created_at: ts,
589//             })
590//             .await;
591
592//         assert!(result.is_ok(), "{result:?}");
593
594//         let result = client
595//             .create(TestModel {
596//                 key: format!("test_create_duplicated_error_key-{ts}"),
597//                 id: format!("test_create_duplicated_error_id-{ts}"),
598//                 created_at: 0,
599//             })
600//             .await;
601
602//         assert!(
603//             matches!(result, Err(DynamoException::DynamoPutItemException(_))),
604//             "{result:?}"
605//         );
606//     }
607
608//     #[tokio::test]
609//     async fn test_get() {
610//         let client = new_cli();
611//         let ts = chrono::Utc::now().timestamp_nanos_opt();
612//         assert!(ts.is_some(), "timestamp is none");
613//         let ts = ts.unwrap();
614
615//         let result = client
616//             .create(TestModel {
617//                 key: format!("test_get_key-{ts}"),
618//                 id: format!("test_get_id-{ts}"),
619//                 created_at: ts,
620//             })
621//             .await;
622
623//         assert!(result.is_ok(), "{result:?}");
624//         let key = format!(
625//             "test_get_{}-{ts}",
626//             option_env!("AWS_DYNAMODB_KEY").unwrap_or("key")
627//         );
628
629//         let doc = client.get(&key).await;
630//         assert!(matches!(doc, Ok(Some(_))), "{doc:?}");
631//         let doc: TestModel = doc.unwrap().unwrap();
632
633//         assert!(doc.created_at == ts, "{doc:?}");
634//         assert!(doc.id == format!("test_get_id-{ts}"), "{doc:?}");
635//         assert!(doc.key == format!("test_get_key-{ts}"), "{doc:?}");
636//     }
637
638//     #[tokio::test]
639//     async fn test_update() {
640//         let client = new_cli();
641//         let ts = chrono::Utc::now().timestamp_nanos_opt();
642//         assert!(ts.is_some(), "timestamp is none");
643//         let ts = ts.unwrap();
644
645//         let result = client
646//             .create(TestModel {
647//                 key: format!("test_update_key-{ts}"),
648//                 id: format!("test_update_id-{ts}"),
649//                 created_at: ts,
650//             })
651//             .await;
652
653//         assert!(result.is_ok(), "{result:?}");
654
655//         let key = format!(
656//             "test_update_{}-{ts}",
657//             option_env!("AWS_DYNAMODB_KEY").unwrap_or("key")
658//         );
659//         let result = client
660//             .update(
661//                 &key,
662//                 vec![
663//                     ("created_at", TestField::N(0)),
664//                     ("str_field", TestField::S("updated".to_string())),
665//                     ("bool_field", TestField::B(true)),
666//                 ],
667//             )
668//             .await;
669
670//         assert!(result.is_ok(), "{result:?}");
671
672//         let doc_v1 = client.get::<TestModel>(&key).await;
673//         assert!(matches!(doc_v1, Ok(Some(_))), "{doc_v1:?}");
674//         let doc_v1 = doc_v1.unwrap().unwrap();
675
676//         assert!(doc_v1.created_at == 0, "{doc_v1:?}");
677//         assert!(doc_v1.id == format!("test_update_id-{ts}"), "{doc_v1:?}");
678//         assert!(doc_v1.key == format!("test_update_key-{ts}"), "{doc_v1:?}");
679
680//         let doc_v2 = client.get::<TestModelV2>(&key).await;
681//         assert!(matches!(doc_v2, Ok(Some(_))), "{doc_v2:?}");
682//         let doc_v2 = doc_v2.unwrap().unwrap();
683
684//         assert!(doc_v2.bool_field, "{doc_v2:?}");
685//         assert!(doc_v2.str_field == "updated".to_string(), "{doc_v2:?}");
686//         assert!(doc_v2.created_at == 0, "{doc_v2:?}");
687//         assert!(doc_v2.id == format!("test_update_id-{ts}"), "{doc_v2:?}");
688//         assert!(doc_v2.key == format!("test_update_key-{ts}"), "{doc_v2:?}");
689//     }
690
691//     #[tokio::test]
692//     async fn test_find() {
693//         let client = new_cli();
694
695//         let key_prefix = "test_find";
696//         let ts = chrono::Utc::now().timestamp_nanos_opt();
697//         assert!(ts.is_some(), "timestamp is none");
698//         let ts = ts.unwrap();
699
700//         for i in 0..10 {
701//             let result = client
702//                 .create(IndexModel {
703//                     key: format!("{key_prefix}_key-{ts}_{i}"),
704//                     id: format!("{key_prefix}_id-{ts}_{i}"),
705//                     created_at: ts,
706//                     r#type: format!("type-{ts}-1").to_string(),
707//                 })
708//                 .await;
709
710//             assert!(result.is_ok(), "{result:?}");
711//         }
712
713//         for i in 0..10 {
714//             let result = client
715//                 .create(IndexModel {
716//                     key: format!("{key_prefix}_key-{ts}_{i}_2"),
717//                     id: format!("{key_prefix}_id-{ts}_{i}_2"),
718//                     created_at: ts,
719//                     r#type: format!("type-{ts}-2").to_string(),
720//                 })
721//                 .await;
722
723//             assert!(result.is_ok(), "{result:?}");
724//         }
725
726//         thread::sleep(std::time::Duration::from_millis(100));
727
728//         let result = client
729//             .find(
730//                 "type-index",
731//                 None,
732//                 Some(6),
733//                 vec![("type", format!("type-{ts}-1"))],
734//             )
735//             .await;
736
737//         assert!(matches!(result, Ok((Some(_), Some(_)))), "{result:?}");
738//         let (docs, bookmark) = result.unwrap();
739//         let (docs, bookmark): (Vec<IndexModel>, String) = (docs.unwrap(), bookmark.unwrap());
740
741//         assert!(docs.len() == 6, "{docs:?}");
742//         assert!(bookmark.len() > 0, "{bookmark:?}");
743
744//         let result = client
745//             .find(
746//                 "type-index",
747//                 Some(bookmark),
748//                 Some(6),
749//                 vec![("type", format!("type-{ts}-1"))],
750//             )
751//             .await;
752
753//         assert!(matches!(result, Ok((Some(_), None))), "{result:?}");
754//         let (docs, _) = result.unwrap();
755//         let docs: Vec<IndexModel> = docs.unwrap();
756
757//         assert!(docs.len() == 4, "{docs:?}");
758//     }
759
760//     #[tokio::test]
761//     async fn test_delete() {
762//         let client = new_cli();
763//         let ts = chrono::Utc::now().timestamp_nanos_opt();
764//         assert!(ts.is_some(), "timestamp is none");
765//         let ts = ts.unwrap();
766
767//         let result = client
768//             .create(TestModel {
769//                 key: format!("test_delete_key-{ts}"),
770//                 id: format!("test_delete_id-{ts}"),
771//                 created_at: ts,
772//             })
773//             .await;
774
775//         thread::sleep(std::time::Duration::from_millis(100));
776//         assert!(result.is_ok(), "{result:?}");
777
778//         let key = format!(
779//             "test_delete_{}-{ts}",
780//             option_env!("AWS_DYNAMODB_KEY").unwrap_or("key")
781//         );
782//         let result = client.delete(&key).await;
783
784//         assert!(result.is_ok(), "{result:?}");
785
786//         thread::sleep(std::time::Duration::from_millis(100));
787//         let doc = client.get::<TestModel>(&key).await;
788//         assert!(matches!(doc, Ok(None)), "{doc:?}");
789//     }
790
791//     #[tokio::test]
792//     async fn test_increment() {
793//         let client = new_cli();
794//         let ts = chrono::Utc::now().timestamp_nanos_opt();
795//         assert!(ts.is_some(), "timestamp is none");
796//         let ts = ts.unwrap();
797
798//         let result = client
799//             .create(TestModel {
800//                 key: format!("test_increment_key-{ts}"),
801//                 id: format!("test_increment_id-{ts}"),
802//                 created_at: ts,
803//             })
804//             .await;
805
806//         assert!(result.is_ok(), "test_increment creation failed: {result:?}");
807//         thread::sleep(std::time::Duration::from_millis(100));
808//         let result = client
809//             .increment(&format!("test_increment_id-{ts}"), "created_at", 1)
810//             .await;
811//         assert!(result.is_ok(), "test_increment addition failed: {result:?}");
812
813//         thread::sleep(std::time::Duration::from_millis(100));
814
815//         let doc = client
816//             .get::<TestModel>(&format!("test_increment_id-{ts}"))
817//             .await;
818
819//         assert!(matches!(doc, Ok(Some(_))), "{doc:?}");
820//         let doc = doc.unwrap().unwrap();
821//         assert!(doc.created_at == ts + 1, "{doc:?}");
822//     }
823// }