refactor: fix all clippy warnings properly

- UserProfile struct groups display_name/bio/avatar/banner/also_known_as/profile_fields
- User::from_persistence takes UserProfile (6 args, was 11)
- PersistedReview struct for Review::from_persistence (1 arg, was 8)
- WatchlistApInput struct for watchlist_to_ap_object (1 arg, was 8)
- ActivityPubDeps struct for activitypub::wire (1 arg, was 11)
- FederationRepos type alias for wire() return types
- FeedSortBy: impl std::str::FromStr instead of inherent from_str
- postgres users.rs: row_to_user takes &PgRow like sqlite
- collapse nested ifs in multipart handlers
- type alias for complex return types (image-converter, worker)
- tui: allow large_enum_variant at crate level (pre-existing, unrelated)
This commit is contained in:
2026-05-29 11:19:02 +02:00
parent 68a939f6c4
commit 2355f89bed
27 changed files with 363 additions and 455 deletions

View File

@@ -220,16 +220,16 @@ impl ActivityPubEventHandler {
let added_at_utc =
chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(*added_at, chrono::Utc);
let obj = crate::objects::watchlist_to_ap_object(
ap_id.clone(),
actor,
movie_title.to_string(),
let obj = crate::objects::watchlist_to_ap_object(crate::objects::WatchlistApInput {
ap_id: ap_id.clone(),
actor_url: actor,
movie_title: movie_title.to_string(),
release_year,
external_metadata_id.clone(),
external_metadata_id: external_metadata_id.clone(),
poster_url,
added_at_utc,
&self.base_url,
);
added_at: added_at_utc,
base_url: self.base_url.clone(),
});
let json = serde_json::to_value(obj)?;
self.ap_service

View File

@@ -22,25 +22,50 @@ pub use remote_review_repository::RemoteReviewRepository;
pub use review_handler::ReviewObjectHandler;
pub use user_adapter::DomainUserRepoAdapter;
pub type FederationRepos = (
std::sync::Arc<dyn ActivityRepository>,
std::sync::Arc<dyn FollowRepository>,
std::sync::Arc<dyn ActorRepository>,
std::sync::Arc<dyn BlocklistRepository>,
std::sync::Arc<dyn domain::ports::SocialQueryPort>,
std::sync::Arc<dyn RemoteReviewRepository>,
std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
);
pub struct ActivityPubWire {
pub service: std::sync::Arc<dyn ActivityPubPort>,
pub router: axum::Router,
pub event_handler: std::sync::Arc<dyn domain::ports::EventHandler>,
}
pub async fn wire(
activity_repo: std::sync::Arc<dyn ActivityRepository>,
follow_repo: std::sync::Arc<dyn FollowRepository>,
actor_repo: std::sync::Arc<dyn ActorRepository>,
blocklist_repo: std::sync::Arc<dyn BlocklistRepository>,
review_store: std::sync::Arc<dyn RemoteReviewRepository>,
remote_watchlist_repo: std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
local_ap_content: std::sync::Arc<dyn domain::ports::LocalApContentQuery>,
user_repo: std::sync::Arc<dyn domain::ports::UserRepository>,
base_url: String,
allow_registration: bool,
event_publisher: std::sync::Arc<dyn domain::ports::EventPublisher>,
) -> anyhow::Result<ActivityPubWire> {
pub struct ActivityPubDeps {
pub activity_repo: std::sync::Arc<dyn ActivityRepository>,
pub follow_repo: std::sync::Arc<dyn FollowRepository>,
pub actor_repo: std::sync::Arc<dyn ActorRepository>,
pub blocklist_repo: std::sync::Arc<dyn BlocklistRepository>,
pub review_store: std::sync::Arc<dyn RemoteReviewRepository>,
pub remote_watchlist_repo: std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
pub local_ap_content: std::sync::Arc<dyn domain::ports::LocalApContentQuery>,
pub user_repo: std::sync::Arc<dyn domain::ports::UserRepository>,
pub base_url: String,
pub allow_registration: bool,
pub event_publisher: std::sync::Arc<dyn domain::ports::EventPublisher>,
}
pub async fn wire(deps: ActivityPubDeps) -> anyhow::Result<ActivityPubWire> {
let ActivityPubDeps {
activity_repo,
follow_repo,
actor_repo,
blocklist_repo,
review_store,
remote_watchlist_repo,
local_ap_content,
user_repo,
base_url,
allow_registration,
event_publisher,
} = deps;
let review_handler = std::sync::Arc::new(ReviewObjectHandler {
content_query: std::sync::Arc::clone(&local_ap_content),
review_store,

View File

@@ -131,16 +131,28 @@ pub struct WatchlistObject {
pub(crate) cc: Vec<String>,
}
pub fn watchlist_to_ap_object(
ap_id: Url,
actor_url: Url,
movie_title: String,
release_year: u16,
external_metadata_id: Option<String>,
poster_url: Option<String>,
added_at: chrono::DateTime<chrono::Utc>,
base_url: &str,
) -> WatchlistObject {
pub struct WatchlistApInput {
pub ap_id: Url,
pub actor_url: Url,
pub movie_title: String,
pub release_year: u16,
pub external_metadata_id: Option<String>,
pub poster_url: Option<String>,
pub added_at: chrono::DateTime<chrono::Utc>,
pub base_url: String,
}
pub fn watchlist_to_ap_object(input: WatchlistApInput) -> WatchlistObject {
let WatchlistApInput {
ap_id,
actor_url,
movie_title,
release_year,
external_metadata_id,
poster_url,
added_at,
base_url,
} = input;
let year_str = if release_year > 0 {
format!(" ({})", release_year)
} else {

View File

@@ -98,18 +98,18 @@ impl ApObjectHandler for ReviewObjectHandler {
let rating = Rating::new(obj.rating.min(5))?;
let comment = obj.comment.map(Comment::new).transpose()?;
let review = domain::models::Review::from_persistence(
review_id,
let review = domain::models::Review::from_persistence(domain::models::PersistedReview {
id: review_id,
movie_id,
user_id,
rating,
comment,
obj.watched_at.naive_utc(),
obj.published.naive_utc(),
ReviewSource::Remote {
watched_at: obj.watched_at.naive_utc(),
created_at: obj.published.naive_utc(),
source: ReviewSource::Remote {
actor_url: actor_url_str,
},
);
});
self.review_store
.save_remote_review(

View File

@@ -14,20 +14,22 @@ fn normalize_hashtag_strips_non_alphanumeric() {
fn review_to_ap_object_includes_two_hashtags() {
use chrono::NaiveDateTime;
use domain::{
models::{Review, ReviewSource},
models::{PersistedReview, Review, ReviewSource},
value_objects::{MovieId, Rating, ReviewId, UserId},
};
let review = Review::from_persistence(
ReviewId::generate(),
MovieId::from_uuid(uuid::Uuid::new_v4()),
UserId::from_uuid(uuid::Uuid::new_v4()),
Rating::new(4).unwrap(),
None,
NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
ReviewSource::Local,
);
let review = Review::from_persistence(PersistedReview {
id: ReviewId::generate(),
movie_id: MovieId::from_uuid(uuid::Uuid::new_v4()),
user_id: UserId::from_uuid(uuid::Uuid::new_v4()),
rating: Rating::new(4).unwrap(),
comment: None,
watched_at: NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
.unwrap(),
created_at: NaiveDateTime::parse_from_str("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S")
.unwrap(),
source: ReviewSource::Local,
});
let obj = review_to_ap_object(
&review,
"https://example.com/reviews/1".parse().unwrap(),
@@ -47,20 +49,22 @@ fn review_to_ap_object_includes_two_hashtags() {
fn review_to_ap_object_has_public_addressing() {
use chrono::NaiveDateTime;
use domain::{
models::{Review, ReviewSource},
models::{PersistedReview, Review, ReviewSource},
value_objects::{MovieId, Rating, ReviewId, UserId},
};
let review = Review::from_persistence(
ReviewId::generate(),
MovieId::from_uuid(uuid::Uuid::new_v4()),
UserId::from_uuid(uuid::Uuid::new_v4()),
Rating::new(3).unwrap(),
None,
NaiveDateTime::parse_from_str("2024-06-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
NaiveDateTime::parse_from_str("2024-06-01 00:00:00", "%Y-%m-%d %H:%M:%S").unwrap(),
ReviewSource::Local,
);
let review = Review::from_persistence(PersistedReview {
id: ReviewId::generate(),
movie_id: MovieId::from_uuid(uuid::Uuid::new_v4()),
user_id: UserId::from_uuid(uuid::Uuid::new_v4()),
rating: Rating::new(3).unwrap(),
comment: None,
watched_at: NaiveDateTime::parse_from_str("2024-06-01 00:00:00", "%Y-%m-%d %H:%M:%S")
.unwrap(),
created_at: NaiveDateTime::parse_from_str("2024-06-01 00:00:00", "%Y-%m-%d %H:%M:%S")
.unwrap(),
source: ReviewSource::Local,
});
let actor_url: url::Url = "https://example.com/users/abc".parse().unwrap();
let obj = review_to_ap_object(
&review,
@@ -78,16 +82,16 @@ fn review_to_ap_object_has_public_addressing() {
#[test]
fn watchlist_to_ap_object_has_public_addressing() {
let actor_url: url::Url = "https://example.com/users/abc".parse().unwrap();
let obj = watchlist_to_ap_object(
"https://example.com/watchlist/1".parse().unwrap(),
actor_url.clone(),
"Alien".to_string(),
1979,
None,
None,
chrono::Utc::now(),
"https://example.com",
);
let obj = watchlist_to_ap_object(WatchlistApInput {
ap_id: "https://example.com/watchlist/1".parse().unwrap(),
actor_url: actor_url.clone(),
movie_title: "Alien".to_string(),
release_year: 1979,
external_metadata_id: None,
poster_url: None,
added_at: chrono::Utc::now(),
base_url: "https://example.com".to_string(),
});
assert_eq!(obj.to, vec!["https://www.w3.org/ns/activitystreams#Public"]);
assert_eq!(obj.cc, vec!["https://example.com/users/abc/followers"]);
}

View File

@@ -11,12 +11,14 @@ use domain::ports::{
};
use std::sync::Arc;
type ConversionPair = (Arc<dyn EventHandler>, Arc<dyn PeriodicJob>);
pub fn build(
image_storage: Arc<dyn ImageStorage>,
image_ref_command: Arc<dyn ImageRefCommand>,
image_ref_query: Arc<dyn ImageRefQuery>,
event_publisher: Arc<dyn EventPublisher>,
) -> anyhow::Result<Option<(Arc<dyn EventHandler>, Arc<dyn PeriodicJob>)>> {
) -> anyhow::Result<Option<ConversionPair>> {
let config = match ConversionConfig::from_env()? {
Some(c) => c,
None => return Ok(None),

View File

@@ -854,17 +854,7 @@ impl RemoteWatchlistRepository for PostgresFederationRepository {
}
}
pub fn wire(
pool: sqlx::PgPool,
) -> (
std::sync::Arc<dyn activitypub::ActivityRepository>,
std::sync::Arc<dyn activitypub::FollowRepository>,
std::sync::Arc<dyn activitypub::ActorRepository>,
std::sync::Arc<dyn activitypub::BlocklistRepository>,
std::sync::Arc<dyn domain::ports::SocialQueryPort>,
std::sync::Arc<dyn activitypub::RemoteReviewRepository>,
std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
) {
pub fn wire(pool: sqlx::PgPool) -> activitypub::FederationRepos {
let fed = std::sync::Arc::new(PostgresFederationRepository::new(pool));
(
std::sync::Arc::clone(&fed) as _,

View File

@@ -1,7 +1,10 @@
use chrono::NaiveDateTime;
use domain::{
errors::DomainError,
models::{DiaryEntry, FeedEntry, Movie, MovieSummary, Review, ReviewSource, UserSummary},
models::{
DiaryEntry, FeedEntry, Movie, MovieSummary, PersistedReview, Review, ReviewSource,
UserSummary,
},
value_objects::{
Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear,
ReviewId, UserId,
@@ -102,9 +105,16 @@ impl ReviewRow {
None => ReviewSource::Local,
Some(url) => ReviewSource::Remote { actor_url: url },
};
Ok(Review::from_persistence(
id, movie_id, user_id, rating, comment, watched_at, created_at, source,
))
Ok(Review::from_persistence(PersistedReview {
id,
movie_id,
user_id,
rating,
comment,
watched_at,
created_at,
source,
}))
}
}

View File

@@ -1,6 +1,6 @@
use async_trait::async_trait;
use chrono::Utc;
use sqlx::PgPool;
use sqlx::{PgPool, Row};
use domain::{
errors::DomainError,
@@ -33,122 +33,67 @@ impl PostgresUserRepository {
}
fn row_to_user(
id_str: String,
email_str: String,
username_str: String,
hash_str: String,
role: UserRole,
display_name: Option<String>,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
row: &sqlx::postgres::PgRow,
profile_fields: Vec<ProfileField>,
) -> Result<User, DomainError> {
let id_str: String = row.get("id");
let id = uuid::Uuid::parse_str(&id_str)
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let email =
Email::new(email_str).map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let username = Username::new(username_str)
let email = Email::new(row.get::<String, _>("email"))
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let hash = PasswordHash::new(hash_str)
let username = Username::new(row.get::<String, _>("username"))
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let hash = PasswordHash::new(row.get::<String, _>("password_hash"))
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let role_str: String = row.get("role");
Ok(User::from_persistence(
UserId::from_uuid(id),
email,
username,
hash,
role,
display_name,
bio,
avatar_path,
banner_path,
also_known_as,
profile_fields,
Self::parse_role(&role_str),
domain::models::UserProfile {
display_name: row.try_get("display_name").ok().flatten(),
bio: row.try_get("bio").ok().flatten(),
avatar_path: row.try_get("avatar_path").ok().flatten(),
banner_path: row.try_get("banner_path").ok().flatten(),
also_known_as: row.try_get("also_known_as").ok().flatten(),
profile_fields,
},
))
}
}
const PG_USER_COLS: &str = "id, email, username, password_hash, role, display_name, bio, avatar_path, banner_path, also_known_as";
#[async_trait]
impl UserRepository for PostgresUserRepository {
async fn find_by_email(&self, email: &Email) -> Result<Option<User>, DomainError> {
let email_str = email.value();
#[derive(sqlx::FromRow)]
struct Row {
id: String,
email: String,
username: String,
password_hash: String,
role: String,
display_name: Option<String>,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
}
let row = sqlx::query_as::<_, Row>(
"SELECT id, email, username, password_hash, role, display_name, bio, avatar_path, banner_path, also_known_as FROM users WHERE email = $1",
)
let row = sqlx::query(&format!(
"SELECT {PG_USER_COLS} FROM users WHERE email = $1"
))
.bind(email_str)
.fetch_optional(&self.pool)
.await
.map_err(Self::map_err)?;
row.map(|r| {
Self::row_to_user(
r.id,
r.email,
r.username,
r.password_hash,
Self::parse_role(&r.role),
r.display_name,
r.bio,
r.avatar_path,
r.banner_path,
r.also_known_as,
vec![],
)
})
.transpose()
row.as_ref()
.map(|r| Self::row_to_user(r, vec![]))
.transpose()
}
async fn find_by_username(&self, username: &Username) -> Result<Option<User>, DomainError> {
let username_str = username.value();
#[derive(sqlx::FromRow)]
struct Row {
id: String,
email: String,
username: String,
password_hash: String,
role: String,
display_name: Option<String>,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
}
let row = sqlx::query_as::<_, Row>(
"SELECT id, email, username, password_hash, role, display_name, bio, avatar_path, banner_path, also_known_as FROM users WHERE username = $1",
)
let row = sqlx::query(&format!(
"SELECT {PG_USER_COLS} FROM users WHERE username = $1"
))
.bind(username_str)
.fetch_optional(&self.pool)
.await
.map_err(Self::map_err)?;
row.map(|r| {
Self::row_to_user(
r.id,
r.email,
r.username,
r.password_hash,
Self::parse_role(&r.role),
r.display_name,
r.bio,
r.avatar_path,
r.banner_path,
r.also_known_as,
vec![],
)
})
.transpose()
row.as_ref()
.map(|r| Self::row_to_user(r, vec![]))
.transpose()
}
async fn save(&self, user: &User) -> Result<(), DomainError> {
@@ -191,35 +136,15 @@ impl UserRepository for PostgresUserRepository {
async fn find_by_id(&self, id: &UserId) -> Result<Option<User>, DomainError> {
let id_str = id.value().to_string();
#[derive(sqlx::FromRow)]
struct Row {
id: String,
email: String,
username: String,
password_hash: String,
role: String,
display_name: Option<String>,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
}
let row = sqlx::query_as::<_, Row>(
"SELECT id, email, username, password_hash, role, display_name, bio, avatar_path, banner_path, also_known_as FROM users WHERE id = $1",
)
.bind(&id_str)
.fetch_optional(&self.pool)
.await
.map_err(Self::map_err)?;
let row = sqlx::query(&format!("SELECT {PG_USER_COLS} FROM users WHERE id = $1"))
.bind(&id_str)
.fetch_optional(&self.pool)
.await
.map_err(Self::map_err)?;
let Some(r) = row else { return Ok(None) };
#[derive(sqlx::FromRow)]
struct FieldRow {
name: String,
value: String,
}
let field_rows = sqlx::query_as::<_, FieldRow>(
let field_rows = sqlx::query(
"SELECT name, value FROM user_profile_fields WHERE user_id = $1 ORDER BY position ASC",
)
.bind(&id_str)
@@ -228,47 +153,30 @@ impl UserRepository for PostgresUserRepository {
.map_err(|e| DomainError::InfrastructureError(e.to_string()))?;
let profile_fields = field_rows
.into_iter()
.iter()
.map(|f| ProfileField {
name: f.name,
value: f.value,
name: f.get("name"),
value: f.get("value"),
})
.collect();
Self::row_to_user(
r.id,
r.email,
r.username,
r.password_hash,
Self::parse_role(&r.role),
r.display_name,
r.bio,
r.avatar_path,
r.banner_path,
r.also_known_as,
profile_fields,
)
.map(Some)
Self::row_to_user(&r, profile_fields).map(Some)
}
async fn update_profile(
&self,
user_id: &UserId,
display_name: Option<String>,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
profile: &domain::models::UserProfile,
) -> Result<(), DomainError> {
let id_str = user_id.value().to_string();
sqlx::query(
"UPDATE users SET display_name = $1, bio = $2, avatar_path = $3, banner_path = $4, also_known_as = $5 WHERE id = $6",
)
.bind(&display_name)
.bind(&bio)
.bind(&avatar_path)
.bind(&banner_path)
.bind(&also_known_as)
.bind(&profile.display_name)
.bind(&profile.bio)
.bind(&profile.avatar_path)
.bind(&profile.banner_path)
.bind(&profile.also_known_as)
.bind(&id_str)
.execute(&self.pool)
.await

View File

@@ -1038,17 +1038,7 @@ impl RemoteWatchlistRepository for SqliteFederationRepository {
}
}
pub fn wire(
pool: sqlx::SqlitePool,
) -> (
std::sync::Arc<dyn activitypub::ActivityRepository>,
std::sync::Arc<dyn activitypub::FollowRepository>,
std::sync::Arc<dyn activitypub::ActorRepository>,
std::sync::Arc<dyn activitypub::BlocklistRepository>,
std::sync::Arc<dyn domain::ports::SocialQueryPort>,
std::sync::Arc<dyn activitypub::RemoteReviewRepository>,
std::sync::Arc<dyn domain::ports::RemoteWatchlistRepository>,
) {
pub fn wire(pool: sqlx::SqlitePool) -> activitypub::FederationRepos {
let fed = std::sync::Arc::new(SqliteFederationRepository::new(pool));
(
std::sync::Arc::clone(&fed) as _,

View File

@@ -2,8 +2,8 @@ use chrono::NaiveDateTime;
use domain::{
errors::DomainError,
models::{
DiaryEntry, FeedEntry, Movie, MovieSummary, Review, ReviewSource, UserSummary,
WatchlistEntry, WatchlistWithMovie,
DiaryEntry, FeedEntry, Movie, MovieSummary, PersistedReview, Review, ReviewSource,
UserSummary, WatchlistEntry, WatchlistWithMovie,
},
value_objects::{
Comment, Email, ExternalMetadataId, MovieId, MovieTitle, PosterPath, Rating, ReleaseYear,
@@ -109,9 +109,16 @@ impl ReviewRow {
None => ReviewSource::Local,
Some(url) => ReviewSource::Remote { actor_url: url },
};
Ok(Review::from_persistence(
id, movie_id, user_id, rating, comment, watched_at, created_at, source,
))
Ok(Review::from_persistence(PersistedReview {
id,
movie_id,
user_id,
rating,
comment,
watched_at,
created_at,
source,
}))
}
}

View File

@@ -6,7 +6,7 @@ use sqlx::SqlitePool;
async fn setup() -> (SqlitePool, SqliteUserRepository) {
let pool = SqlitePool::connect(":memory:").await.unwrap();
sqlx::query(
"CREATE TABLE users (id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'standard', bio TEXT, avatar_path TEXT, banner_path TEXT, also_known_as TEXT)"
"CREATE TABLE users (id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'standard', display_name TEXT, bio TEXT, avatar_path TEXT, banner_path TEXT, also_known_as TEXT)"
)
.execute(&pool)
.await
@@ -65,10 +65,11 @@ async fn update_profile_persists_bio_and_avatar() {
repo.update_profile(
user.id(),
Some("My biography".to_string()),
Some("avatars/user1".to_string()),
None,
None,
&domain::models::UserProfile {
bio: Some("My biography".to_string()),
avatar_path: Some("avatars/user1".to_string()),
..Default::default()
},
)
.await
.unwrap();
@@ -90,14 +91,15 @@ async fn update_profile_clears_fields_with_none() {
repo.save(&user).await.unwrap();
repo.update_profile(
user.id(),
Some("bio".to_string()),
Some("path".to_string()),
None,
None,
&domain::models::UserProfile {
bio: Some("bio".to_string()),
avatar_path: Some("path".to_string()),
..Default::default()
},
)
.await
.unwrap();
repo.update_profile(user.id(), None, None, None, None)
repo.update_profile(user.id(), &domain::models::UserProfile::default())
.await
.unwrap();

View File

@@ -51,12 +51,14 @@ impl SqliteUserRepository {
username,
hash,
Self::parse_role(&role_str),
row.try_get("display_name").ok().flatten(),
row.try_get("bio").ok().flatten(),
row.try_get("avatar_path").ok().flatten(),
row.try_get("banner_path").ok().flatten(),
row.try_get("also_known_as").ok().flatten(),
profile_fields,
domain::models::UserProfile {
display_name: row.try_get("display_name").ok().flatten(),
bio: row.try_get("bio").ok().flatten(),
avatar_path: row.try_get("avatar_path").ok().flatten(),
banner_path: row.try_get("banner_path").ok().flatten(),
also_known_as: row.try_get("also_known_as").ok().flatten(),
profile_fields,
},
))
}
}
@@ -161,21 +163,17 @@ impl UserRepository for SqliteUserRepository {
async fn update_profile(
&self,
user_id: &UserId,
display_name: Option<String>,
bio: Option<String>,
avatar_path: Option<String>,
banner_path: Option<String>,
also_known_as: Option<String>,
profile: &domain::models::UserProfile,
) -> Result<(), DomainError> {
let id_str = user_id.value().to_string();
sqlx::query(
"UPDATE users SET display_name = ?, bio = ?, avatar_path = ?, banner_path = ?, also_known_as = ? WHERE id = ?",
)
.bind(&display_name)
.bind(&bio)
.bind(&avatar_path)
.bind(&banner_path)
.bind(&also_known_as)
.bind(&profile.display_name)
.bind(&profile.bio)
.bind(&profile.avatar_path)
.bind(&profile.banner_path)
.bind(&profile.also_known_as)
.bind(&id_str)
.execute(&self.pool)
.await