use super::*; impl Store { #[tracing::instrument(name = "db_create_known_device", skip_all, fields(identifiers = input.identifiers.len()))] pub async fn create_known_device(&self, input: KnownDeviceInput) -> Result { let display_name = normalize_required_text(&input.display_name, "display_name")?; let notes = normalize_optional_text(input.notes.as_deref()); let identifiers = input .identifiers .into_iter() .map(normalize_device_identifier) .collect::>>()?; let device_id = format!("dev-{}", Uuid::new_v4()); let now = now_unix(); let mut tx = self.begin_write().await?; let pinned = if input.pinned { 1_i64 } else { 0_i64 }; let now_i64 = i64::try_from(now).context("known device timestamp overflow")?; sqlx::query!( "INSERT INTO known_devices (device_id, display_name, pinned, created_at_unix, updated_at_unix, notes) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", device_id, display_name, pinned, now_i64, now_i64, notes ) .execute(&mut *tx) .await .context("failed persisting known device")?; for identifier in &identifiers { insert_device_identifier_tx(&mut tx, &device_id, identifier, now).await?; } tx.commit() .await .context("failed committing known device transaction")?; self.get_known_device(&device_id) .await? .ok_or_else(|| anyhow::anyhow!("created known device disappeared")) } #[tracing::instrument(name = "db_list_known_devices", skip_all)] pub async fn list_known_devices(&self) -> Result> { let rows = list_known_device_rows(&self.pool).await?; let mut out = Vec::with_capacity(rows.len()); for row in rows { let device_id = known_device_row_device_id(&row).to_string(); out.push(self.known_device_from_row(row, &device_id).await?); } Ok(out) } #[tracing::instrument(name = "db_get_known_device", skip_all)] pub async fn get_known_device(&self, device_id: &str) -> Result> { let row = get_known_device_row(&self.pool, device_id).await?; match row { Some(row) => self.known_device_from_row(row, device_id).await.map(Some), None => Ok(None), } } #[tracing::instrument(name = "db_forget_known_device", skip_all)] pub async fn forget_known_device(&self, device_id: &str) -> Result { let result = sqlx::query!("DELETE FROM known_devices WHERE device_id = ?1", device_id) .execute(&self.pool) .await .context("failed deleting known device")?; Ok(result.rows_affected() > 0) } #[tracing::instrument(name = "db_merge_known_devices", skip_all)] pub async fn merge_known_devices( &self, target_device_id: &str, source_device_id: &str, ) -> Result> { if target_device_id == source_device_id { return self.get_known_device(target_device_id).await; } let now = now_unix(); let now_i64 = i64::try_from(now).context("known device timestamp overflow")?; let mut tx = self.begin_write().await?; let target_exists = sqlx::query_scalar!( r#"SELECT COUNT(*) as "count!: i64" FROM known_devices WHERE device_id = ?1"#, target_device_id ) .fetch_one(&mut *tx) .await .context("failed checking target known device existence")?; if target_exists == 0 { return Ok(None); } let source_exists = sqlx::query_scalar!( r#"SELECT COUNT(*) as "count!: i64" FROM known_devices WHERE device_id = ?1"#, source_device_id ) .fetch_one(&mut *tx) .await .context("failed checking source known device existence")?; if source_exists == 0 { return Ok(None); } sqlx::query!( "UPDATE device_identifiers SET device_id = ?1 WHERE device_id = ?2", target_device_id, source_device_id ) .execute(&mut *tx) .await .context("failed moving source identifiers to target device")?; sqlx::query!( "DELETE FROM known_devices WHERE device_id = ?1", source_device_id ) .execute(&mut *tx) .await .context("failed deleting merged source known device")?; sqlx::query!( "UPDATE known_devices SET updated_at_unix = ?1 WHERE device_id = ?2", now_i64, target_device_id ) .execute(&mut *tx) .await .context("failed updating merged target known device timestamp")?; tx.commit() .await .context("failed committing known device merge transaction")?; self.get_known_device(target_device_id).await } #[tracing::instrument(name = "db_attach_device_identifier", skip_all)] pub async fn attach_device_identifier( &self, device_id: &str, input: DeviceIdentifierInput, ) -> Result> { let identifier = normalize_device_identifier(input)?; let now = now_unix(); let mut tx = self.begin_write().await?; let exists = sqlx::query_scalar!( r#"SELECT COUNT(*) as "count!: i64" FROM known_devices WHERE device_id = ?1"#, device_id ) .fetch_one(&mut *tx) .await .context("failed checking known device existence")?; if exists == 0 { return Ok(None); } insert_device_identifier_tx(&mut tx, device_id, &identifier, now).await?; let now_i64 = i64::try_from(now).context("known device timestamp overflow")?; sqlx::query!( "UPDATE known_devices SET updated_at_unix = ?1 WHERE device_id = ?2", now_i64, device_id ) .execute(&mut *tx) .await .context("failed updating known device timestamp")?; tx.commit() .await .context("failed committing device identifier transaction")?; self.get_known_device(device_id).await } #[tracing::instrument(name = "db_detach_device_identifier", skip_all)] pub async fn detach_device_identifier( &self, device_id: &str, identifier_key: &str, ) -> Result> { let now = now_unix(); let now_i64 = i64::try_from(now).context("known device timestamp overflow")?; let mut tx = self.begin_write().await?; let exists = sqlx::query_scalar!( r#"SELECT COUNT(*) as "count!: i64" FROM known_devices WHERE device_id = ?1"#, device_id ) .fetch_one(&mut *tx) .await .context("failed checking known device existence")?; if exists == 0 { return Ok(None); } sqlx::query!( "DELETE FROM device_identifiers WHERE device_id = ?1 AND identifier_key = ?2", device_id, identifier_key ) .execute(&mut *tx) .await .context("failed detaching device identifier")?; sqlx::query!( "UPDATE known_devices SET updated_at_unix = ?1 WHERE device_id = ?2", now_i64, device_id ) .execute(&mut *tx) .await .context("failed updating known device timestamp")?; tx.commit() .await .context("failed committing device identifier detach transaction")?; self.get_known_device(device_id).await } #[cfg_attr(not(test), allow(dead_code))] pub async fn lookup_known_device_by_identifier( &self, input: DeviceIdentifierInput, ) -> Result> { let identifier = normalize_device_identifier(input)?; let identifier_key = identifier.identifier_key; let device_id = sqlx::query_scalar!( "SELECT device_id FROM device_identifiers WHERE identifier_key = ?1", identifier_key ) .fetch_optional(&self.pool) .await .context("failed looking up known device identifier")?; match device_id { Some(device_id) => self.get_known_device(&device_id).await, None => Ok(None), } } async fn known_device_from_row( &self, row: KnownDeviceRow, device_id: &str, ) -> Result { let identifiers = self.list_device_identifiers(device_id).await?; known_device_from_row_and_identifiers(row, identifiers) } async fn list_device_identifiers(&self, device_id: &str) -> Result> { let rows = list_device_identifier_rows(&self.pool, device_id).await?; rows.into_iter().map(device_identifier_from_row).collect() } }