diff --git a/services/blob/src/database/types.rs b/services/blob/src/database/types.rs index e9e6c5cd3..5639b5786 100644 --- a/services/blob/src/database/types.rs +++ b/services/blob/src/database/types.rs @@ -1,241 +1,241 @@ use aws_sdk_dynamodb::types::AttributeValue; use chrono::{DateTime, Utc}; use comm_services_lib::database::{ parse_string_attribute, parse_timestamp_attribute, DBItemError, Value, }; use derive_more::Constructor; use std::collections::HashMap; use crate::{config::CONFIG, constants::db::*, s3::S3Path}; use super::errors::Error as DBError; /// Represents a database row in the DynamoDB SDK format pub(super) type RawAttributes = HashMap; /// A convenience Result type for database operations pub(super) type DBResult = Result; /// Represents a type-safe version of a DynamoDB blob table item. /// Each row can be either a blob item or a holder assignment. /// /// It implements the `TryFrom` trait to convert from raw DynamoDB /// `AttributeValue`s to the type-safe version. pub enum DBRow { BlobItem(BlobItemRow), HolderAssignment(HolderAssignmentRow), } impl TryFrom for DBRow { type Error = DBError; fn try_from(attributes: RawAttributes) -> Result { let holder = parse_string_attribute( ATTR_HOLDER, attributes.get(ATTR_HOLDER).cloned(), )?; let row = match holder.as_str() { BLOB_ITEM_ROW_HOLDER_VALUE => DBRow::BlobItem(attributes.try_into()?), _ => DBRow::HolderAssignment(attributes.try_into()?), }; Ok(row) } } /// Represents an input payload for inserting a blob item into the database. /// This contains only the business logic related attributes #[derive(Debug)] pub struct BlobItemInput { pub blob_hash: String, pub s3_path: S3Path, } impl BlobItemInput { pub fn new(blob_hash: impl Into) -> Self { let blob_hash: String = blob_hash.into(); BlobItemInput { blob_hash: blob_hash.clone(), s3_path: S3Path { bucket_name: CONFIG.s3_bucket_name.clone(), object_name: blob_hash, }, } } } /// A struct representing a blob item row in the table in a type-safe way /// /// It implements the `TryFrom` trait to convert from raw DynamoDB /// `AttributeValue`s to the type-safe version. #[derive(Debug)] pub struct BlobItemRow { pub blob_hash: String, pub s3_path: S3Path, pub unchecked: bool, pub created_at: DateTime, pub last_modified: DateTime, } impl TryFrom for BlobItemRow { type Error = DBError; fn try_from(mut attributes: RawAttributes) -> Result { let blob_hash = parse_string_attribute( ATTR_BLOB_HASH, attributes.remove(ATTR_BLOB_HASH), )?; let s3_path = parse_string_attribute(ATTR_S3_PATH, attributes.remove(ATTR_S3_PATH))?; let created_at = parse_timestamp_attribute( ATTR_CREATED_AT, attributes.remove(ATTR_CREATED_AT), )?; let last_modified = parse_timestamp_attribute( ATTR_LAST_MODIFIED, attributes.remove(ATTR_LAST_MODIFIED), )?; let unchecked = is_raw_row_unchecked(&attributes, UncheckedKind::Blob)?; let s3_path = S3Path::from_full_path(&s3_path).map_err(DBError::from)?; Ok(BlobItemRow { blob_hash, s3_path, unchecked, created_at, last_modified, }) } } /// A struct representing a holder assignment table row in a type-safe way /// /// It implements the `TryFrom` trait to convert from raw DynamoDB /// `AttributeValue`s to the type-safe version. #[derive(Debug)] pub struct HolderAssignmentRow { pub blob_hash: String, pub holder: String, pub unchecked: bool, pub created_at: DateTime, pub last_modified: DateTime, } impl TryFrom for HolderAssignmentRow { type Error = DBError; fn try_from(mut attributes: RawAttributes) -> Result { let holder = parse_string_attribute(ATTR_HOLDER, attributes.remove(ATTR_HOLDER))?; let blob_hash = parse_string_attribute( ATTR_BLOB_HASH, attributes.remove(ATTR_BLOB_HASH), )?; let created_at = parse_timestamp_attribute( ATTR_CREATED_AT, attributes.remove(ATTR_CREATED_AT), )?; let last_modified = parse_timestamp_attribute( ATTR_LAST_MODIFIED, attributes.remove(ATTR_LAST_MODIFIED), )?; let unchecked = is_raw_row_unchecked(&attributes, UncheckedKind::Holder)?; Ok(HolderAssignmentRow { blob_hash, holder, unchecked, created_at, last_modified, }) } } /// Represents a composite primary key for a DynamoDB table row /// /// It implements `TryFrom` and `Into` traits to conveniently use it /// in DynamoDB queries #[derive(Clone, Constructor, Debug)] pub struct PrimaryKey { pub blob_hash: String, pub holder: String, } impl PrimaryKey { /// Creates a primary key for a row containing a blob item data /// Rows queried by primary keys created by this function will /// be of type `BlobItemRow` pub fn for_blob_item(blob_hash: impl Into) -> Self { PrimaryKey { blob_hash: blob_hash.into(), holder: BLOB_ITEM_ROW_HOLDER_VALUE.to_string(), } } } impl TryFrom for PrimaryKey { type Error = DBError; fn try_from(mut attributes: RawAttributes) -> Result { let blob_hash = parse_string_attribute( ATTR_BLOB_HASH, attributes.remove(ATTR_BLOB_HASH), )?; let holder = parse_string_attribute(ATTR_HOLDER, attributes.remove(ATTR_HOLDER))?; Ok(PrimaryKey { blob_hash, holder }) } } // useful for convenient calls: // ddb.get_item().set_key(Some(partition_key.into())) impl Into for PrimaryKey { fn into(self) -> RawAttributes { HashMap::from([ ( ATTR_BLOB_HASH.to_string(), AttributeValue::S(self.blob_hash), ), (ATTR_HOLDER.to_string(), AttributeValue::S(self.holder)), ]) } } /// Represents possible values for the `unchecked` attribute value pub enum UncheckedKind { Blob, Holder, } impl UncheckedKind { pub fn str_value(&self) -> &'static str { match self { UncheckedKind::Blob => "blob", UncheckedKind::Holder => "holder", } } } impl Into for UncheckedKind { fn into(self) -> AttributeValue { AttributeValue::S(self.str_value().to_string()) } } fn is_raw_row_unchecked( row: &RawAttributes, kind: UncheckedKind, ) -> DBResult { let Some(AttributeValue::S(value)) = row.get(ATTR_UNCHECKED) else { // The unchecked attribute not exists return Ok(false); }; if value != kind.str_value() { // The unchecked attribute exists but has an incorrect value return Err(DBError::Attribute(DBItemError::new( - ATTR_UNCHECKED, + ATTR_UNCHECKED.to_string(), Value::String(value.to_string()), comm_services_lib::database::DBItemAttributeError::IncorrectType, ))); } Ok(true) } diff --git a/services/comm-services-lib/src/database.rs b/services/comm-services-lib/src/database.rs index 579acacc9..ae6d6eedf 100644 --- a/services/comm-services-lib/src/database.rs +++ b/services/comm-services-lib/src/database.rs @@ -1,248 +1,251 @@ use aws_sdk_dynamodb::types::AttributeValue; use aws_sdk_dynamodb::Error as DynamoDBError; use chrono::{DateTime, Utc}; use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::num::ParseIntError; use std::str::FromStr; #[derive( Debug, derive_more::Display, derive_more::From, derive_more::Error, )] pub enum Error { #[display(...)] AwsSdk(DynamoDBError), #[display(...)] Attribute(DBItemError), } #[derive(Debug)] pub enum Value { AttributeValue(Option), String(String), } #[derive(Debug, derive_more::Error, derive_more::Constructor)] pub struct DBItemError { - attribute_name: &'static str, + attribute_name: String, attribute_value: Value, attribute_error: DBItemAttributeError, } impl Display for DBItemError { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { match &self.attribute_error { DBItemAttributeError::Missing => { write!(f, "Attribute {} is missing", self.attribute_name) } DBItemAttributeError::IncorrectType => write!( f, "Value for attribute {} has incorrect type: {:?}", self.attribute_name, self.attribute_value ), error => write!( f, "Error regarding attribute {} with value {:?}: {}", self.attribute_name, self.attribute_value, error ), } } } #[derive(Debug, derive_more::Display, derive_more::Error)] pub enum DBItemAttributeError { #[display(...)] Missing, #[display(...)] IncorrectType, #[display(...)] TimestampOutOfRange, #[display(...)] InvalidTimestamp(chrono::ParseError), #[display(...)] InvalidNumberFormat(ParseIntError), } pub fn parse_string_attribute( - attribute_name: &'static str, + attribute_name: impl Into, attribute_value: Option, ) -> Result { match attribute_value { Some(AttributeValue::S(value)) => Ok(value), Some(_) => Err(DBItemError::new( - attribute_name, + attribute_name.into(), Value::AttributeValue(attribute_value), DBItemAttributeError::IncorrectType, )), None => Err(DBItemError::new( - attribute_name, + attribute_name.into(), Value::AttributeValue(attribute_value), DBItemAttributeError::Missing, )), } } pub fn parse_bool_attribute( - attribute_name: &'static str, + attribute_name: impl Into, attribute_value: Option, ) -> Result { match attribute_value { Some(AttributeValue::Bool(value)) => Ok(value), Some(_) => Err(DBItemError::new( - attribute_name, + attribute_name.into(), Value::AttributeValue(attribute_value), DBItemAttributeError::IncorrectType, )), None => Err(DBItemError::new( - attribute_name, + attribute_name.into(), Value::AttributeValue(attribute_value), DBItemAttributeError::Missing, )), } } pub fn parse_int_attribute( - attribute_name: &'static str, + attribute_name: impl Into, attribute_value: Option, ) -> Result where T: FromStr, { match &attribute_value { Some(AttributeValue::N(numeric_str)) => { - parse_integer(attribute_name, &numeric_str) + parse_integer(attribute_name, numeric_str) } Some(_) => Err(DBItemError::new( - attribute_name, + attribute_name.into(), Value::AttributeValue(attribute_value), DBItemAttributeError::IncorrectType, )), None => Err(DBItemError::new( - attribute_name, + attribute_name.into(), Value::AttributeValue(attribute_value), DBItemAttributeError::Missing, )), } } pub fn parse_datetime_attribute( - attribute_name: &'static str, + attribute_name: impl Into, attribute_value: Option, ) -> Result, DBItemError> { if let Some(AttributeValue::S(datetime)) = &attribute_value { // parse() accepts a relaxed RFC3339 string datetime.parse().map_err(|e| { DBItemError::new( - attribute_name, + attribute_name.into(), Value::AttributeValue(attribute_value), DBItemAttributeError::InvalidTimestamp(e), ) }) } else { Err(DBItemError::new( - attribute_name, + attribute_name.into(), Value::AttributeValue(attribute_value), DBItemAttributeError::Missing, )) } } /// Parses the UTC timestamp in milliseconds from a DynamoDB numeric attribute pub fn parse_timestamp_attribute( - attribute_name: &'static str, + attribute_name: impl Into, attribute_value: Option, ) -> Result, DBItemError> { - let timestamp = - parse_int_attribute::(attribute_name, attribute_value.clone())?; + let attribute_name: String = attribute_name.into(); + let timestamp = parse_int_attribute::( + attribute_name.clone(), + attribute_value.clone(), + )?; let naive_datetime = chrono::NaiveDateTime::from_timestamp_millis(timestamp) .ok_or_else(|| { DBItemError::new( attribute_name, Value::AttributeValue(attribute_value), DBItemAttributeError::TimestampOutOfRange, ) })?; Ok(DateTime::from_utc(naive_datetime, Utc)) } pub fn parse_map_attribute( - attribute_name: &'static str, + attribute_name: impl Into, attribute_value: Option, ) -> Result, DBItemError> { match attribute_value { Some(AttributeValue::M(map)) => Ok(map), Some(_) => Err(DBItemError::new( - attribute_name, + attribute_name.into(), Value::AttributeValue(attribute_value), DBItemAttributeError::IncorrectType, )), None => Err(DBItemError::new( - attribute_name, + attribute_name.into(), Value::AttributeValue(attribute_value), DBItemAttributeError::Missing, )), } } pub fn parse_integer( - attribute_name: &'static str, + attribute_name: impl Into, attribute_value: &str, ) -> Result where T: FromStr, { attribute_value.parse::().map_err(|e| { DBItemError::new( - attribute_name, - Value::String(attribute_value.to_string()), + attribute_name.into(), + Value::String(attribute_value.into()), DBItemAttributeError::InvalidNumberFormat(e), ) }) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_integer() { assert!(parse_integer::("some_attr", "123").is_ok()); assert!(parse_integer::("negative", "-123").is_ok()); assert!(parse_integer::("float", "3.14").is_err()); assert!(parse_integer::("NaN", "foo").is_err()); assert!(parse_integer::("negative_uint", "-123").is_err()); assert!(parse_integer::("too_large", "65536").is_err()); } #[test] fn test_parse_timestamp() { let timestamp = Utc::now().timestamp_millis(); let attr = AttributeValue::N(timestamp.to_string()); let parsed_timestamp = parse_timestamp_attribute("some_attr", Some(attr)); assert!(parsed_timestamp.is_ok()); assert_eq!(parsed_timestamp.unwrap().timestamp_millis(), timestamp); } #[test] fn test_parse_invalid_timestamp() { let attr = AttributeValue::N("foo".to_string()); let parsed_timestamp = parse_timestamp_attribute("some_attr", Some(attr)); assert!(parsed_timestamp.is_err()); } #[test] fn test_parse_timestamp_out_of_range() { let attr = AttributeValue::N(i64::MAX.to_string()); let parsed_timestamp = parse_timestamp_attribute("some_attr", Some(attr)); assert!(parsed_timestamp.is_err()); assert!(matches!( parsed_timestamp.unwrap_err().attribute_error, DBItemAttributeError::TimestampOutOfRange )); } }