alien_bindings/providers/kv/
aws_dynamodb.rs1use crate::error::{map_cloud_client_error, ErrorData, Result};
2use crate::traits::{Binding, Kv, PutOptions, ScanResult};
3use alien_aws_clients::dynamodb::*;
4use alien_error::AlienError;
5use async_trait::async_trait;
6use base64::{prelude::BASE64_STANDARD, Engine};
7use chrono::Utc;
8use std::collections::HashMap;
9use std::fmt::{Debug, Formatter};
10
11use super::{validate_key, validate_value};
12
13pub struct AwsDynamodbKv {
18 client: DynamoDbClient,
19 table_name: String,
20}
21
22impl Debug for AwsDynamodbKv {
23 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
24 f.debug_struct("AwsDynamodbKv")
25 .field("table_name", &self.table_name)
26 .finish()
27 }
28}
29
30impl AwsDynamodbKv {
31 pub fn new(table_name: String, client: DynamoDbClient) -> Self {
32 Self { client, table_name }
33 }
34
35 fn hash_bucket(&self, key: &str) -> String {
37 use std::collections::hash_map::DefaultHasher;
38 use std::hash::{Hash, Hasher};
39
40 let mut hasher = DefaultHasher::new();
41 key.hash(&mut hasher);
42 let bucket_id = hasher.finish() % 16; format!("bucket_{}", bucket_id)
44 }
45
46 fn is_expired(&self, ttl_epoch: Option<i64>) -> bool {
48 if let Some(ttl_timestamp) = ttl_epoch {
49 let now = Utc::now().timestamp();
50 now >= ttl_timestamp
51 } else {
52 false
53 }
54 }
55}
56
57impl Binding for AwsDynamodbKv {}
58
59#[async_trait]
60impl Kv for AwsDynamodbKv {
61 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
62 validate_key(key)?;
63
64 let bucket = self.hash_bucket(key);
65 let mut primary_key = HashMap::new();
66 primary_key.insert("pk".to_string(), AttributeValue::s(bucket));
67 primary_key.insert("sk".to_string(), AttributeValue::s(key.to_string()));
68
69 let request = GetItemRequest::builder()
70 .table_name(self.table_name.clone())
71 .key(primary_key)
72 .consistent_read(true)
77 .build();
78
79 let response = self.client.get_item(request).await.map_err(|e| {
80 map_cloud_client_error(
81 e,
82 format!("Failed to get item with key '{}'", key),
83 Some(key.to_string()),
84 )
85 })?;
86
87 if let Some(item) = response.item {
88 if let Some(ttl_attr) = item.get("ttl") {
90 if let Some(ttl_epoch) = ttl_attr.n.as_ref().and_then(|s| s.parse::<i64>().ok()) {
91 if self.is_expired(Some(ttl_epoch)) {
92 return Ok(None); }
94 }
95 }
96
97 let value = item
98 .get("value")
99 .and_then(|attr| attr.b.as_ref())
100 .and_then(|base64_value| BASE64_STANDARD.decode(base64_value).ok())
101 .ok_or_else(|| {
102 AlienError::new(ErrorData::CloudPlatformError {
103 message: format!("Missing or invalid value attribute for key '{}'", key),
104 resource_id: Some(key.to_string()),
105 })
106 })?;
107
108 Ok(Some(value))
109 } else {
110 Ok(None)
111 }
112 }
113
114 async fn put(&self, key: &str, value: Vec<u8>, options: Option<PutOptions>) -> Result<bool> {
115 validate_key(key)?;
116 validate_value(&value)?;
117
118 let bucket = self.hash_bucket(key);
119 let options = options.unwrap_or_default();
120
121 let mut item = HashMap::new();
122 item.insert("pk".to_string(), AttributeValue::s(bucket));
123 item.insert("sk".to_string(), AttributeValue::s(key.to_string()));
124 item.insert(
125 "value".to_string(),
126 AttributeValue::b(BASE64_STANDARD.encode(&value)),
127 );
128
129 if let Some(ttl) = options.ttl {
130 let expires_at = (Utc::now() + ttl).timestamp();
131 item.insert("ttl".to_string(), AttributeValue::n(expires_at.to_string()));
132 }
133
134 let request = if options.if_not_exists {
135 let mut expression_attribute_names = HashMap::new();
142 expression_attribute_names.insert("#ttl".to_string(), "ttl".to_string());
143 let mut expression_attribute_values = HashMap::new();
144 expression_attribute_values.insert(
145 ":now".to_string(),
146 AttributeValue::n(Utc::now().timestamp().to_string()),
147 );
148 PutItemRequest::builder()
149 .table_name(self.table_name.clone())
150 .item(item)
151 .condition_expression(
152 "(attribute_not_exists(pk) AND attribute_not_exists(sk)) OR (attribute_exists(#ttl) AND #ttl <= :now)"
153 .to_string(),
154 )
155 .expression_attribute_names(expression_attribute_names)
156 .expression_attribute_values(expression_attribute_values)
157 .build()
158 } else {
159 PutItemRequest::builder()
160 .table_name(self.table_name.clone())
161 .item(item)
162 .build()
163 };
164
165 match self.client.put_item(request).await {
166 Ok(_) => Ok(true),
167 Err(e) => {
168 if options.if_not_exists {
170 if let Some(alien_client_core::ErrorData::RemoteResourceConflict { .. }) =
171 &e.error
172 {
173 return Ok(false);
174 }
175 }
176 Err(map_cloud_client_error(
177 e,
178 format!("Failed to put item with key '{}'", key),
179 Some(key.to_string()),
180 ))
181 }
182 }
183 }
184
185 async fn delete(&self, key: &str) -> Result<()> {
186 validate_key(key)?;
187
188 let bucket = self.hash_bucket(key);
189 let mut primary_key = HashMap::new();
190 primary_key.insert("pk".to_string(), AttributeValue::s(bucket));
191 primary_key.insert("sk".to_string(), AttributeValue::s(key.to_string()));
192
193 let request = DeleteItemRequest::builder()
194 .table_name(self.table_name.clone())
195 .key(primary_key)
196 .build();
197
198 self.client.delete_item(request).await.map_err(|e| {
199 map_cloud_client_error(
200 e,
201 format!("Failed to delete item with key '{}'", key),
202 Some(key.to_string()),
203 )
204 })?;
205
206 Ok(())
207 }
208
209 async fn exists(&self, key: &str) -> Result<bool> {
210 validate_key(key)?;
211
212 let bucket = self.hash_bucket(key);
213 let mut primary_key = HashMap::new();
214 primary_key.insert("pk".to_string(), AttributeValue::s(bucket));
215 primary_key.insert("sk".to_string(), AttributeValue::s(key.to_string()));
216
217 let mut expression_attribute_names = HashMap::new();
219 expression_attribute_names.insert("#ttl".to_string(), "ttl".to_string());
220
221 let request = GetItemRequest::builder()
222 .table_name(self.table_name.clone())
223 .key(primary_key)
224 .projection_expression("pk, #ttl".to_string()) .expression_attribute_names(expression_attribute_names)
226 .consistent_read(true)
227 .build();
228
229 let response = self.client.get_item(request).await.map_err(|e| {
230 map_cloud_client_error(
231 e,
232 format!("Failed to check existence of item with key '{}'", key),
233 Some(key.to_string()),
234 )
235 })?;
236
237 if let Some(item) = response.item {
238 if let Some(ttl_attr) = item.get("ttl") {
240 if let Some(ttl_epoch) = ttl_attr.n.as_ref().and_then(|s| s.parse::<i64>().ok()) {
241 if self.is_expired(Some(ttl_epoch)) {
242 return Ok(false); }
244 }
245 }
246 Ok(true)
247 } else {
248 Ok(false)
249 }
250 }
251
252 async fn scan_prefix(
253 &self,
254 prefix: &str,
255 limit: Option<usize>,
256 _cursor: Option<String>,
257 ) -> Result<ScanResult> {
258 validate_key(prefix)?; let mut all_items = Vec::new();
263 let mut total_fetched = 0;
264 let limit = limit.unwrap_or(1000);
265
266 for bucket_id in 0..16 {
269 if total_fetched >= limit {
270 break;
271 }
272
273 let bucket = format!("bucket_{}", bucket_id);
274 let mut expression_attribute_values = HashMap::new();
275 expression_attribute_values.insert(":bucket".to_string(), AttributeValue::s(bucket));
276 expression_attribute_values
277 .insert(":prefix".to_string(), AttributeValue::s(prefix.to_string()));
278
279 let request = QueryRequest::builder()
281 .table_name(self.table_name.clone())
282 .key_condition_expression("pk = :bucket AND begins_with(sk, :prefix)".to_string())
283 .expression_attribute_values(expression_attribute_values)
284 .limit((limit - total_fetched) as i32)
285 .build();
286
287 let response = self.client.query(request).await.map_err(|e| {
288 map_cloud_client_error(
289 e,
290 format!("Failed to scan prefix '{}' in bucket {}", prefix, bucket_id),
291 Some(prefix.to_string()),
292 )
293 })?;
294
295 for item in response.items {
297 if total_fetched >= limit {
298 break;
299 }
300
301 if let Some(ttl_attr) = item.get("ttl") {
303 if let Some(ttl_epoch) = ttl_attr.n.as_ref().and_then(|s| s.parse::<i64>().ok())
304 {
305 if self.is_expired(Some(ttl_epoch)) {
306 continue; }
308 }
309 }
310
311 if let (Some(key_attr), Some(value_attr)) = (item.get("sk"), item.get("value")) {
312 if let (Some(key), Some(base64_value)) =
313 (key_attr.s.as_ref(), value_attr.b.as_ref())
314 {
315 if let Ok(value) = BASE64_STANDARD.decode(base64_value) {
316 all_items.push((key.clone(), value));
317 total_fetched += 1;
318 }
319 }
320 }
321 }
322 }
323
324 Ok(ScanResult {
327 items: all_items,
328 next_cursor: None,
329 })
330 }
331}