use sqlx::PgPool; use crate::db::enums::SyncPlatform; use crate::db::models::*; use crate::db::{SyncAppId, SyncDeviceId, UserId}; use crate::error::Result; // ── Sync Devices ── /// Register or update a sync device. /// /// Upserts on the `(app_id, user_id, device_name)` unique constraint. /// On conflict (same device re-registering), updates the platform string /// and bumps `last_seen_at` so stale-device detection stays accurate. #[tracing::instrument(skip_all)] pub async fn upsert_sync_device( pool: &PgPool, app_id: SyncAppId, user_id: UserId, device_name: &str, platform: SyncPlatform, ) -> Result { let device = sqlx::query_as::<_, DbSyncDevice>( r#" INSERT INTO sync_devices (app_id, user_id, device_name, platform) VALUES ($1, $2, $3, $4) ON CONFLICT (app_id, user_id, device_name) DO UPDATE SET platform = EXCLUDED.platform, last_seen_at = NOW() RETURNING * "#, ) .bind(app_id) .bind(user_id) .bind(device_name) .bind(platform) .fetch_one(pool) .await?; Ok(device) } /// List all devices for a user within an app. #[tracing::instrument(skip_all)] pub async fn get_sync_devices( pool: &PgPool, app_id: SyncAppId, user_id: UserId, ) -> Result> { let devices = sqlx::query_as::<_, DbSyncDevice>( "SELECT * FROM sync_devices WHERE app_id = $1 AND user_id = $2 ORDER BY last_seen_at DESC LIMIT 100", ) .bind(app_id) .bind(user_id) .fetch_all(pool) .await?; Ok(devices) } /// Delete a device by ID (only if owned by user within app). #[tracing::instrument(skip_all)] pub async fn delete_sync_device( pool: &PgPool, device_id: SyncDeviceId, app_id: SyncAppId, user_id: UserId, ) -> Result { let result = sqlx::query( "DELETE FROM sync_devices WHERE id = $1 AND app_id = $2 AND user_id = $3", ) .bind(device_id) .bind(app_id) .bind(user_id) .execute(pool) .await?; Ok(result.rows_affected() > 0) } /// Touch last_seen_at for a device. #[tracing::instrument(skip_all)] pub async fn touch_sync_device(pool: &PgPool, device_id: SyncDeviceId) -> Result<()> { sqlx::query("UPDATE sync_devices SET last_seen_at = NOW() WHERE id = $1") .bind(device_id) .execute(pool) .await?; Ok(()) }