refactor: dedup JSONB name/value helpers, add profile fields validation
Some checks failed
lint / lint (push) Failing after 9m29s
test / unit (push) Has been cancelled

Extract parse/serialize into postgres::jsonb, used by user,
remote_actor, and postgres-federation. Validate profile fields
in update_profile use case (max 4, name≤64, value≤256).
This commit is contained in:
2026-05-29 13:59:39 +02:00
parent 805bd9534f
commit 020a79704f
5 changed files with 46 additions and 45 deletions

View File

@@ -0,0 +1,20 @@
pub fn parse_name_value(v: Option<serde_json::Value>) -> Vec<(String, String)> {
v.and_then(|v| v.as_array().cloned())
.map(|arr| {
arr.into_iter()
.filter_map(|item| {
let name = item.get("name")?.as_str()?.to_string();
let value = item.get("value")?.as_str()?.to_string();
Some((name, value))
})
.collect()
})
.unwrap_or_default()
}
pub fn serialize_name_value(fields: &[(String, String)]) -> serde_json::Value {
fields
.iter()
.map(|(n, v)| serde_json::json!({"name": n, "value": v}))
.collect()
}

View File

@@ -8,6 +8,7 @@ pub mod engagement;
pub mod failed_event;
pub mod feed;
pub mod follow;
pub(crate) mod jsonb;
pub mod like;
pub mod notification;
pub mod outbox;

View File

@@ -23,11 +23,7 @@ impl RemoteActorRepository for PgRemoteActorRepository {
} else {
Some(a.also_known_as.iter().map(|s| s.as_str()).collect())
};
let attachment_json: serde_json::Value = a
.attachment
.iter()
.map(|(n, v)| serde_json::json!({"name": n, "value": v}))
.collect();
let attachment_json = crate::jsonb::serialize_name_value(&a.attachment);
sqlx::query(
"INSERT INTO remote_actors(url,handle,display_name,avatar_url,last_fetched_at,
bio,banner_url,outbox_url,followers_url,following_url,also_known_as,attachment)
@@ -101,19 +97,7 @@ impl RemoteActorRepository for PgRemoteActorRepository {
following_url: r.following_url,
inbox_url: r.inbox_url,
shared_inbox_url: r.shared_inbox_url,
attachment: r
.attachment
.and_then(|v| v.as_array().cloned())
.map(|arr| {
arr.into_iter()
.filter_map(|item| {
let name = item.get("name")?.as_str()?.to_string();
let value = item.get("value")?.as_str()?.to_string();
Some((name, value))
})
.collect()
})
.unwrap_or_default(),
attachment: crate::jsonb::parse_name_value(r.attachment),
})
})
}

View File

@@ -37,20 +37,6 @@ pub struct UserRow {
pub updated_at: DateTime<Utc>,
}
fn parse_profile_fields(v: Option<serde_json::Value>) -> Vec<(String, String)> {
v.and_then(|v| v.as_array().cloned())
.map(|arr| {
arr.into_iter()
.filter_map(|item| {
let name = item.get("name")?.as_str()?.to_string();
let value = item.get("value")?.as_str()?.to_string();
Some((name, value))
})
.collect()
})
.unwrap_or_default()
}
impl From<UserRow> for User {
fn from(r: UserRow) -> Self {
User {
@@ -63,7 +49,7 @@ impl From<UserRow> for User {
avatar_url: r.avatar_url,
header_url: r.header_url,
custom_css: r.custom_css,
profile_fields: parse_profile_fields(r.profile_fields),
profile_fields: crate::jsonb::parse_name_value(r.profile_fields),
local: r.local,
created_at: r.created_at,
updated_at: r.updated_at,
@@ -238,11 +224,7 @@ impl UserReader for PgUserRepository {
#[async_trait]
impl UserWriter for PgUserRepository {
async fn save(&self, user: &User) -> Result<(), DomainError> {
let profile_fields_json: serde_json::Value = user
.profile_fields
.iter()
.map(|(n, v)| serde_json::json!({"name": n, "value": v}))
.collect();
let profile_fields_json = crate::jsonb::serialize_name_value(&user.profile_fields);
sqlx::query(
"INSERT INTO users (id,username,email,password_hash,display_name,bio,avatar_url,header_url,custom_css,profile_fields,local,created_at,updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
@@ -290,13 +272,10 @@ impl UserWriter for PgUserRepository {
user_id: &UserId,
input: UpdateProfileInput,
) -> Result<(), DomainError> {
let profile_fields_json: Option<serde_json::Value> =
input.profile_fields.as_ref().map(|fields| {
fields
.iter()
.map(|(n, v)| serde_json::json!({"name": n, "value": v}))
.collect()
});
let profile_fields_json: Option<serde_json::Value> = input
.profile_fields
.as_ref()
.map(|f| crate::jsonb::serialize_name_value(f));
sqlx::query(
"UPDATE users SET \
display_name = COALESCE($2, display_name), \

View File

@@ -1,4 +1,7 @@
const MAX_TOP_FRIENDS: usize = 8;
const MAX_PROFILE_FIELDS: usize = 4;
const MAX_FIELD_NAME_LEN: usize = 64;
const MAX_FIELD_VALUE_LEN: usize = 256;
use bytes::Bytes;
use domain::{
@@ -55,6 +58,20 @@ pub async fn update_profile(
user_id: &UserId,
input: UpdateProfileInput,
) -> Result<(), DomainError> {
if let Some(ref fields) = input.profile_fields {
if fields.len() > MAX_PROFILE_FIELDS {
return Err(DomainError::InvalidInput(format!(
"profile fields: max {MAX_PROFILE_FIELDS}"
)));
}
for (name, value) in fields {
if name.len() > MAX_FIELD_NAME_LEN || value.len() > MAX_FIELD_VALUE_LEN {
return Err(DomainError::InvalidInput(
"profile field name or value too long".into(),
));
}
}
}
users.update_profile(user_id, input).await?;
events
.publish(&DomainEvent::ProfileUpdated {