diff --git a/services/identity/src/database/device_list.rs b/services/identity/src/database/device_list.rs --- a/services/identity/src/database/device_list.rs +++ b/services/identity/src/database/device_list.rs @@ -5,6 +5,7 @@ use aws_sdk_dynamodb::model::AttributeValue; use chrono::{DateTime, Utc}; +use tracing::warn; use crate::{ constants::devices_table::*, @@ -62,6 +63,15 @@ } } +impl From for AttributeValue { + fn from(value: DeviceListKeyAttribute) -> Self { + AttributeValue::S(format!( + "{DEVICE_LIST_KEY_PREFIX}{}", + value.0.to_rfc3339() + )) + } +} + impl TryFrom> for DeviceIDAttribute { type Error = DBItemError; fn try_from(value: Option) -> Result { @@ -81,6 +91,33 @@ } } +impl TryFrom> for DeviceListKeyAttribute { + type Error = DBItemError; + fn try_from(value: Option) -> Result { + let item_id = parse_string_attribute(ATTR_ITEM_ID, value)?; + + // remove the device-list- prefix, then parse the timestamp + let timestamp: DateTime = item_id + .strip_prefix(DEVICE_LIST_KEY_PREFIX) + .ok_or_else(|| DBItemError { + attribute_name: ATTR_ITEM_ID.to_string(), + attribute_value: Some(AttributeValue::S(item_id.clone())), + attribute_error: DBItemAttributeError::InvalidValue, + }) + .and_then(|s| { + s.parse().map_err(|e| { + DBItemError::new( + ATTR_ITEM_ID.to_string(), + Some(AttributeValue::S(item_id.clone())), + DBItemAttributeError::InvalidTimestamp(e), + ) + }) + })?; + + Ok(Self(timestamp)) + } +} + impl TryFrom for DeviceRow { type Error = DBItemError; @@ -227,3 +264,79 @@ }) } } + +impl TryFrom for DeviceListRow { + type Error = DBItemError; + + fn try_from(mut attrs: RawAttributes) -> Result { + let user_id = + parse_string_attribute(ATTR_USER_ID, attrs.remove(ATTR_USER_ID))?; + let DeviceListKeyAttribute(timestamp) = + attrs.remove(ATTR_ITEM_ID).try_into()?; + + // validate timestamps are in sync + let timestamps_match = attrs + .remove(ATTR_TIMESTAMP) + .and_then(|attr| attr.as_n().ok().cloned()) + .and_then(|val| val.parse::().ok()) + .filter(|val| *val == timestamp.timestamp_millis()) + .is_some(); + if !timestamps_match { + warn!( + "DeviceList timestamp mismatch for (userID={}, itemID={})", + &user_id, + timestamp.to_rfc3339() + ); + } + + // this should be a list of strings + let device_ids = attrs + .remove(ATTR_DEVICE_IDS) + .ok_or_else(|| { + DBItemError::new( + ATTR_DEVICE_IDS.to_string(), + None, + DBItemAttributeError::Missing, + ) + })? + .to_vec(ATTR_DEVICE_IDS)? + .iter() + .map(|v| v.to_string("device_ids[?]").cloned()) + .collect::, DBItemError>>()?; + + Ok(Self { + user_id, + timestamp, + device_ids, + }) + } +} + +impl From for RawAttributes { + fn from(device_list: DeviceListRow) -> Self { + let mut attrs = HashMap::new(); + attrs.insert( + ATTR_USER_ID.to_string(), + AttributeValue::S(device_list.user_id.clone()), + ); + attrs.insert( + ATTR_ITEM_ID.to_string(), + DeviceListKeyAttribute(device_list.timestamp).into(), + ); + attrs.insert( + ATTR_TIMESTAMP.to_string(), + AttributeValue::N(device_list.timestamp.timestamp_millis().to_string()), + ); + attrs.insert( + ATTR_DEVICE_IDS.to_string(), + AttributeValue::L( + device_list + .device_ids + .into_iter() + .map(AttributeValue::S) + .collect(), + ), + ); + attrs + } +}