use crate::TableConfig;
use crate::error::{DynamoToolsError, Result};
use aws_config::meta::region::RegionProviderChain;
use aws_config::{BehaviorVersion, Region};
use aws_sdk_dynamodb::config::Credentials;
use aws_sdk_dynamodb::types::{AttributeValue, PutRequest, WriteRequest};
use aws_sdk_dynamodb::{Client, operation::create_table::CreateTableInput};
use serde_json::Value;
use std::{collections::HashMap, fs, path::Path};
#[cfg(feature = "test_utils")]
use tokio::runtime::Runtime;
#[derive(Debug)]
pub struct DynamodbConnector {
client: Option<Client>,
created_tables: HashMap<String, String>,
#[cfg(feature = "test_utils")]
config: TableConfig,
}
impl DynamodbConnector {
pub async fn load(config_path: impl AsRef<Path>) -> Result<Self> {
let config = TableConfig::load_from_file(config_path)?;
DynamodbConnector::try_new(config).await
}
pub fn client(&self) -> Result<&Client> {
self.client
.as_ref()
.ok_or_else(|| DynamoToolsError::Internal("Client instance is missing".to_string()))
}
pub fn get_created_table_name(&self, base_name: &str) -> Option<&str> {
self.created_tables.get(base_name).map(|s| s.as_str())
}
pub fn get_all_created_table_names(&self) -> &HashMap<String, String> {
&self.created_tables
}
pub async fn try_new(config: TableConfig) -> Result<Self> {
let endpoint = config.endpoint.clone();
#[cfg(feature = "test_utils")]
let connector_config = config.clone();
let base_sdk_config_builder = aws_config::defaults(BehaviorVersion::latest()).region(
RegionProviderChain::first_try(Region::new(config.region.clone()))
.or_default_provider(),
);
let loaded_sdk_config = base_sdk_config_builder.load().await;
let builder = aws_sdk_dynamodb::config::Builder::from(&loaded_sdk_config);
let dynamodb_config = if let Some(url) = endpoint.as_ref() {
builder
.endpoint_url(url)
.credentials_provider(Credentials::for_tests())
.build()
} else {
builder.build()
};
let client = Client::from_conf(dynamodb_config);
let mut created_tables = HashMap::new();
for table_info in config.tables {
let base_table_name = table_info.table_name.clone();
let seed_file = table_info.seed_data_file.clone(); let mut input = CreateTableInput::try_from(table_info)?;
let unique_table_name = format!("{}-{}", base_table_name, xid::new());
input.table_name = Some(unique_table_name.clone());
let create_table_builder = client
.create_table()
.table_name(&unique_table_name)
.set_key_schema(input.key_schema)
.set_attribute_definitions(input.attribute_definitions)
.set_global_secondary_indexes(input.global_secondary_indexes)
.set_local_secondary_indexes(input.local_secondary_indexes);
let create_table_builder = match input.provisioned_throughput {
Some(pt) => create_table_builder.provisioned_throughput(pt),
None => create_table_builder.billing_mode(input.billing_mode.ok_or_else(|| {
DynamoToolsError::MissingField(format!(
"Billing mode missing for table '{}' with no throughput",
base_table_name
))
})?),
};
create_table_builder
.send()
.await
.map_err(DynamoToolsError::TableCreation)?;
created_tables.insert(base_table_name.clone(), unique_table_name.clone());
if let Some(file_path) = seed_file {
println!(
"[INFO] Seeding data for table '{}' from file '{}'",
unique_table_name, file_path
);
let content = fs::read_to_string(&file_path)
.map_err(|e| DynamoToolsError::SeedFileRead(file_path.clone(), e))?;
let items_json: Vec<Value> = serde_json::from_str(&content)
.map_err(|e| DynamoToolsError::SeedJsonParse(file_path.clone(), e))?;
let mut write_requests = Vec::new();
for item_value in items_json {
let item_map: HashMap<String, AttributeValue> =
serde_dynamo::to_item(item_value)?;
let put_request = PutRequest::builder()
.set_item(Some(item_map))
.build()
.map_err(|e| {
DynamoToolsError::Internal(format!("Failed to build PutRequest: {}", e))
})?;
write_requests.push(WriteRequest::builder().put_request(put_request).build());
}
for chunk in write_requests.chunks(25) {
let request_items =
HashMap::from([(unique_table_name.clone(), chunk.to_vec())]);
client
.batch_write_item()
.set_request_items(Some(request_items))
.send()
.await
.map_err(|e| {
DynamoToolsError::SeedBatchWrite(unique_table_name.clone(), e)
})?;
println!(
"[INFO] Wrote batch of {} items to table '{}'",
chunk.len(),
unique_table_name
);
}
}
}
Ok(Self {
client: Some(client),
created_tables,
#[cfg(feature = "test_utils")]
config: connector_config,
})
}
}
#[cfg(feature = "test_utils")]
impl Drop for DynamodbConnector {
fn drop(&mut self) {
if !self.config.delete_on_exit || self.config.endpoint.is_none() {
println!(
"[INFO] Skipping delete on drop (delete_on_exit: {}, endpoint: {:?})",
self.config.delete_on_exit, self.config.endpoint
);
return;
}
if let Some(client) = self.client.take() {
let tables_to_delete = self.created_tables.clone();
println!(
"[INFO] Drop: Attempting to delete tables: {:?}",
tables_to_delete.values()
);
for (_base_name, unique_name) in tables_to_delete {
let client_clone = client.clone(); std::thread::spawn(move || {
let rt = match Runtime::new() {
Ok(rt) => rt,
Err(e) => {
eprintln!(
"[ERROR] Failed to create Tokio runtime for table deletion: {}",
e
);
return;
}
};
rt.block_on(async move {
match client_clone
.delete_table()
.table_name(&unique_name)
.send()
.await
{
Ok(_) => println!("[INFO] Deleted table: {}", unique_name),
Err(e) => {
eprintln!("[ERROR] Failed to delete table '{}': {}", unique_name, e)
}
}
});
});
}
}
}
}