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// }