Skip to content

Commit 86c4785

Browse files
BlankllCopilot
andauthored
feat: implement query with sort key and refactor rust dynamo client (#205)
feat: implement query with sort key and refactor rust dynamo client - [x] implement query with sort key - [x] refactor rust dynamo client implement - [x] fix scan not passing index name issue Refs: #142 --------- Signed-off-by: seven <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 8b7ed79 commit 86c4785

File tree

13 files changed

+696
-558
lines changed

13 files changed

+696
-558
lines changed

src-tauri/src/common/json_utils.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use aws_sdk_dynamodb::types::AttributeValue;
2+
3+
pub fn convert_json_to_attr_value(value: &serde_json::Value) -> Option<AttributeValue> {
4+
match value {
5+
serde_json::Value::String(s) => Some(AttributeValue::S(s.clone())),
6+
serde_json::Value::Number(n) => Some(AttributeValue::N(n.to_string())),
7+
serde_json::Value::Bool(b) => Some(AttributeValue::Bool(*b)),
8+
serde_json::Value::Null => Some(AttributeValue::Null(true)),
9+
serde_json::Value::Array(arr) => Some(AttributeValue::L(
10+
arr.iter()
11+
.filter_map(|v| convert_json_to_attr_value(v))
12+
.collect(),
13+
)),
14+
serde_json::Value::Object(map) => Some(AttributeValue::M(
15+
map.iter()
16+
.filter_map(|(k, v)| convert_json_to_attr_value(v).map(|av| (k.clone(), av)))
17+
.collect(),
18+
)),
19+
}
20+
}

src-tauri/src/common/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub mod http_client;
2+
pub mod json_utils;
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use crate::common::json_utils::convert_json_to_attr_value;
2+
use crate::dynamo::types::ApiResponse;
3+
use aws_sdk_dynamodb::types::AttributeValue;
4+
use aws_sdk_dynamodb::Client;
5+
use serde_json::Value;
6+
7+
pub struct CreateItemInput<'a> {
8+
pub table_name: &'a str,
9+
pub payload: &'a Value,
10+
}
11+
12+
pub async fn create_item(
13+
client: &Client,
14+
input: CreateItemInput<'_>,
15+
) -> Result<ApiResponse, String> {
16+
if let Some(attributes) = input.payload.get("attributes").and_then(|v| v.as_array()) {
17+
let mut put_item = client.put_item().table_name(input.table_name);
18+
19+
for attr in attributes {
20+
if let (Some(key), Some(value), Some(attr_type)) = (
21+
attr.get("key").and_then(|v| v.as_str()),
22+
attr.get("value"),
23+
attr.get("type").and_then(|v| v.as_str()),
24+
) {
25+
let attr_value = match attr_type {
26+
"S" => value.as_str().map(|s| AttributeValue::S(s.to_string())),
27+
"N" => value.as_f64().map(|n| AttributeValue::N(n.to_string())),
28+
"B" => value.as_str().map(|s| {
29+
AttributeValue::B(aws_sdk_dynamodb::primitives::Blob::new(
30+
base64::decode(s).unwrap_or_default(),
31+
))
32+
}),
33+
"BOOL" => value.as_bool().map(AttributeValue::Bool),
34+
"NULL" => Some(AttributeValue::Null(true)),
35+
"SS" => value.as_array().map(|arr| {
36+
AttributeValue::Ss(
37+
arr.iter()
38+
.filter_map(|v| v.as_str().map(|s| s.to_string()))
39+
.collect(),
40+
)
41+
}),
42+
"NS" => value.as_array().map(|arr| {
43+
AttributeValue::Ns(
44+
arr.iter()
45+
.filter_map(|v| v.as_f64().map(|n| n.to_string()))
46+
.collect(),
47+
)
48+
}),
49+
"BS" => value.as_array().map(|arr| {
50+
AttributeValue::Bs(
51+
arr.iter()
52+
.filter_map(|v| {
53+
v.as_str().map(|s| {
54+
aws_sdk_dynamodb::primitives::Blob::new(
55+
base64::decode(s).unwrap_or_default(),
56+
)
57+
})
58+
})
59+
.collect(),
60+
)
61+
}),
62+
"L" => value.as_array().map(|arr| {
63+
AttributeValue::L(
64+
arr.iter()
65+
.filter_map(|v| {
66+
// Recursively convert each element
67+
convert_json_to_attr_value(v)
68+
})
69+
.collect(),
70+
)
71+
}),
72+
"M" => value.as_object().map(|map| {
73+
AttributeValue::M(
74+
map.iter()
75+
.filter_map(|(k, v)| {
76+
convert_json_to_attr_value(v).map(|av| (k.clone(), av))
77+
})
78+
.collect(),
79+
)
80+
}),
81+
_ => None,
82+
};
83+
if let Some(av) = attr_value {
84+
put_item = put_item.item(key, av);
85+
}
86+
}
87+
}
88+
89+
match put_item.send().await {
90+
Ok(_) => Ok(ApiResponse {
91+
status: 200,
92+
message: "Item created successfully".to_string(),
93+
data: None,
94+
}),
95+
Err(e) => Ok(ApiResponse {
96+
status: 500,
97+
message: format!("Failed to create item: {}", e),
98+
data: None,
99+
}),
100+
}
101+
} else {
102+
Ok(ApiResponse {
103+
status: 400,
104+
message: "Attributes array is required".to_string(),
105+
data: None,
106+
})
107+
}
108+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use crate::dynamo::types::ApiResponse;
2+
use aws_sdk_dynamodb::Client;
3+
use serde_json::json;
4+
5+
pub async fn describe_table(client: &Client, table_name: &str) -> Result<ApiResponse, String> {
6+
match client
7+
.describe_table()
8+
.table_name(table_name)
9+
.send()
10+
.await
11+
{
12+
Ok(response) => {
13+
// Create a custom serializable structure with the data we need
14+
let table_info = json!({
15+
"id": response.table().and_then(|t| t.table_id()),
16+
"name": response.table().map(|t| t.table_name()),
17+
"status": response.table().and_then(|t| t.table_status().map(|s| s.as_str().to_string())),
18+
"itemCount": response.table().and_then(|t| t.item_count()),
19+
"sizeBytes": response.table().and_then(|t| t.table_size_bytes()),
20+
"keySchema": response.table().and_then(|t| {
21+
Some(t.key_schema().iter().map(|k| {
22+
json!({
23+
"attributeName": k.attribute_name(),
24+
"keyType": format!("{:?}", k.key_type())
25+
})
26+
}).collect::<Vec<_>>())
27+
}),
28+
"attributeDefinitions": response.table().and_then(|t| {
29+
Some(t.attribute_definitions().iter().map(|a| {
30+
json!({
31+
"attributeName": a.attribute_name(),
32+
"attributeType": format!("{:?}", a.attribute_type())
33+
})
34+
}).collect::<Vec<_>>())
35+
}),
36+
"indices": response.table().map(|t| {
37+
let mut indices = Vec::new();
38+
39+
// Add Global Secondary Indexes
40+
let gsi_list = t.global_secondary_indexes();
41+
if !gsi_list.is_empty() {
42+
for gsi in gsi_list {
43+
let index_info = json!({
44+
"type": "GSI",
45+
"name": gsi.index_name(),
46+
"status": gsi.index_status().map(|s| s.as_str().to_string()),
47+
"keySchema": gsi.key_schema().iter().map(|k| {
48+
json!({
49+
"attributeName": k.attribute_name(),
50+
"keyType": format!("{:?}", k.key_type())
51+
})
52+
}).collect::<Vec<_>>(),
53+
"provisionedThroughput": gsi.provisioned_throughput().map(|pt| json!({
54+
"readCapacityUnits": pt.read_capacity_units(),
55+
"writeCapacityUnits": pt.write_capacity_units()
56+
}))
57+
});
58+
indices.push(index_info);
59+
}
60+
}
61+
62+
// Add Local Secondary Indexes
63+
let lsi_list = t.local_secondary_indexes();
64+
if !lsi_list.is_empty() {
65+
for lsi in lsi_list {
66+
let index_info = json!({
67+
"type": "LSI",
68+
"name": lsi.index_name(),
69+
"keySchema": lsi.key_schema().iter().map(|k| {
70+
json!({
71+
"attributeName": k.attribute_name(),
72+
"keyType": format!("{:?}", k.key_type())
73+
})
74+
}).collect::<Vec<_>>()
75+
});
76+
indices.push(index_info);
77+
}
78+
}
79+
80+
indices
81+
}),
82+
"creationDateTime": response.table().and_then(|t|
83+
t.creation_date_time().map(|dt| dt.to_string())),
84+
});
85+
86+
Ok(ApiResponse {
87+
status: 200,
88+
message: "Table described successfully".to_string(),
89+
data: Some(table_info),
90+
})
91+
}
92+
Err(e) => Ok(ApiResponse {
93+
status: 500,
94+
message: format!("Failed to describe table: {}", e),
95+
data: None,
96+
}),
97+
}
98+
}

src-tauri/src/dynamo/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pub mod create_item;
2+
pub mod describe_table;
3+
pub mod query_table;
4+
pub mod scan_table;
5+
pub mod types;

0 commit comments

Comments
 (0)