diff --git a/services/reports/Cargo.lock b/services/reports/Cargo.lock --- a/services/reports/Cargo.lock +++ b/services/reports/Cargo.lock @@ -1377,6 +1377,7 @@ dependencies = [ "anyhow", "aws-config", + "aws-sdk-dynamodb", "chrono", "clap", "comm-services-lib", diff --git a/services/reports/Cargo.toml b/services/reports/Cargo.toml --- a/services/reports/Cargo.toml +++ b/services/reports/Cargo.toml @@ -9,6 +9,7 @@ [dependencies] anyhow = "1.0" aws-config = "0.55" +aws-sdk-dynamodb = "0.27" chrono = { version = "0.4", features = ["serde"] } clap = { version = "4.0", features = ["derive", "env"] } comm-services-lib = { path = "../comm-services-lib", features = [ diff --git a/services/reports/src/database/item.rs b/services/reports/src/database/item.rs new file mode 100644 --- /dev/null +++ b/services/reports/src/database/item.rs @@ -0,0 +1,116 @@ +use aws_sdk_dynamodb::types::AttributeValue; +use comm_services_lib::database::{self, DBItemError, TryFromAttribute}; +use num_traits::FromPrimitive; +use tracing::debug; + +use super::constants::*; + +use crate::report_types::*; + +// DB conversions for report types + +// ReportID +impl From for AttributeValue { + fn from(value: ReportID) -> Self { + AttributeValue::S(value.into()) + } +} +impl From<&ReportID> for AttributeValue { + fn from(value: &ReportID) -> Self { + AttributeValue::S(value.clone().into()) + } +} +impl TryFrom> for ReportID { + type Error = database::DBItemError; + + fn try_from(value: Option) -> Result { + let raw = String::try_from_attr(ATTR_REPORT_ID, value)?; + Ok(ReportID::from(raw)) + } +} + +// ReportType +impl From for AttributeValue { + fn from(value: ReportType) -> Self { + let num = value as u8; + AttributeValue::N(num.to_string()) + } +} +impl TryFromAttribute for ReportType { + fn try_from_attr( + attribute_name: impl Into, + attribute: Option, + ) -> Result { + let attr_name = attribute_name.into(); + let num: u8 = database::parse_int_attribute(&attr_name, attribute)?; + ::from_u8(num).ok_or_else(|| { + database::DBItemError::new( + attr_name, + database::Value::String(num.to_string()), + database::DBItemAttributeError::IncorrectType, + ) + }) + } +} + +// ReportPlatform +impl From for AttributeValue { + fn from(value: ReportPlatform) -> Self { + let raw = value.to_string().to_lowercase(); + AttributeValue::S(raw) + } +} +impl TryFromAttribute for ReportPlatform { + fn try_from_attr( + attribute_name: impl Into, + attribute: Option, + ) -> Result { + let attr_name = attribute_name.into(); + let raw = String::try_from_attr(&attr_name, attribute)?; + // serde_json understands only quoted strings + let quoted = format!("\"{raw}\""); + serde_json::from_str("ed).map_err(|err| { + debug!("Failed to deserialize ReportPlatform: {}", err); + DBItemError::new( + attr_name, + database::Value::String(raw), + database::DBItemAttributeError::IncorrectType, + ) + }) + } +} + +#[cfg(test)] +mod tests { + use comm_services_lib::database::AttributeTryInto; + + use super::*; + + #[test] + fn test_platform_conversions() -> anyhow::Result<()> { + let platform = ReportPlatform::MacOS; + + let attribute: AttributeValue = platform.into(); + + assert_eq!(attribute, AttributeValue::S("macos".to_string())); + + let converted_back: ReportPlatform = + Some(attribute).attr_try_into("foo")?; + + assert!(matches!(converted_back, ReportPlatform::MacOS)); + + Ok(()) + } + + #[test] + fn test_type_conversions() -> anyhow::Result<()> { + let report_type = ReportType::MediaMission; + let numeric_type = (report_type as u8).to_string(); + let attr: AttributeValue = report_type.into(); + assert_eq!(attr, AttributeValue::N(numeric_type.to_string())); + + let converted_back: ReportType = Some(attr).attr_try_into("foo")?; + assert!(matches!(converted_back, ReportType::MediaMission)); + Ok(()) + } +} diff --git a/services/reports/src/database/mod.rs b/services/reports/src/database/mod.rs new file mode 100644 --- /dev/null +++ b/services/reports/src/database/mod.rs @@ -0,0 +1,14 @@ +pub mod item; + +mod constants { + pub const TABLE_NAME: &str = "reports-service-reports"; + + pub const ATTR_REPORT_ID: &str = "reportID"; + pub const ATTR_USER_ID: &str = "userID"; + pub const ATTR_REPORT_TYPE: &str = "type"; + pub const ATTR_PLATFORM: &str = "platform"; + pub const ATTR_BLOB_INFO: &str = "blobInfo"; + pub const ATTR_REPORT_CONTENT: &str = "content"; + pub const ATTR_ENCRYPTION_KEY: &str = "encryptionKey"; + pub const ATTR_CREATION_TIME: &str = "creationTime"; +} diff --git a/services/reports/src/main.rs b/services/reports/src/main.rs --- a/services/reports/src/main.rs +++ b/services/reports/src/main.rs @@ -1,4 +1,5 @@ pub mod config; +pub mod database; pub mod report_types; use anyhow::Result; diff --git a/services/terraform/modules/shared/dynamodb.tf b/services/terraform/modules/shared/dynamodb.tf --- a/services/terraform/modules/shared/dynamodb.tf +++ b/services/terraform/modules/shared/dynamodb.tf @@ -289,3 +289,14 @@ type = "S" } } + +resource "aws_dynamodb_table" "reports-service-reports" { + name = "reports-service-reports" + hash_key = "reportID" + billing_mode = "PAY_PER_REQUEST" + + attribute { + name = "reportID" + type = "S" + } +}