feat: Implement merge person functionality with associated request and repository methods

This commit is contained in:
2025-11-15 17:46:36 +01:00
parent 8a735c7c26
commit b80c4e0895
6 changed files with 91 additions and 14 deletions

View File

@@ -1,9 +1,9 @@
use axum::{ use axum::{
Json, Router,
extract::{Path, State}, extract::{Path, State},
http::StatusCode, http::StatusCode,
response::IntoResponse, response::IntoResponse,
routing::{get, post, put}, routing::{get, post, put},
Json, Router,
}; };
use uuid::Uuid; use uuid::Uuid;
@@ -12,8 +12,8 @@ use crate::{
error::ApiError, error::ApiError,
middleware::auth::UserId, middleware::auth::UserId,
schema::{ schema::{
AssignFaceRequest, CreatePersonRequest, FaceRegionResponse, PersonResponse, AssignFaceRequest, CreatePersonRequest, FaceRegionResponse, MergePersonRequest,
SharePersonRequest, UpdatePersonRequest, PersonResponse, SharePersonRequest, UpdatePersonRequest,
}, },
state::AppState, state::AppState,
}; };
@@ -23,11 +23,13 @@ pub fn people_routes() -> Router<AppState> {
.route("/", get(list_people).post(create_person)) .route("/", get(list_people).post(create_person))
.route( .route(
"/{person_id}", "/{person_id}",
get(get_person) get(get_person).put(update_person).delete(delete_person),
.put(update_person)
.delete(delete_person),
) )
.route("/{person_id}/share", post(share_person).delete(unshare_person)) .route(
"/{person_id}/share",
post(share_person).delete(unshare_person),
)
.route("/{person_id}/merge", post(merge_person))
} }
pub fn face_routes() -> Router<AppState> { pub fn face_routes() -> Router<AppState> {
@@ -53,10 +55,7 @@ async fn get_person(
UserId(user_id): UserId, UserId(user_id): UserId,
Path(person_id): Path<Uuid>, Path(person_id): Path<Uuid>,
) -> Result<Json<PersonResponse>, ApiError> { ) -> Result<Json<PersonResponse>, ApiError> {
let person = state let person = state.person_service.get_person(person_id, user_id).await?;
.person_service
.get_person(person_id, user_id)
.await?;
Ok(Json(PersonResponse::from(person))) Ok(Json(PersonResponse::from(person)))
} }
@@ -150,3 +149,16 @@ async fn assign_face_to_person(
.await?; .await?;
Ok(Json(FaceRegionResponse::from(face))) Ok(Json(FaceRegionResponse::from(face)))
} }
async fn merge_person(
State(state): State<AppState>,
UserId(user_id): UserId,
Path(target_person_id): Path<Uuid>,
Json(payload): Json<MergePersonRequest>,
) -> Result<StatusCode, ApiError> {
state
.person_service
.merge_people(target_person_id, payload.source_person_id, user_id)
.await?;
Ok(StatusCode::NO_CONTENT)
}

View File

@@ -239,3 +239,8 @@ pub struct PublicAlbumBundleResponse {
pub album: AlbumResponse, pub album: AlbumResponse,
pub media: Vec<MediaResponse>, pub media: Vec<MediaResponse>,
} }
#[derive(Deserialize)]
pub struct MergePersonRequest {
pub source_person_id: Uuid,
}

View File

@@ -183,4 +183,36 @@ impl PersonService for PersonServiceImpl {
.remove_share(person_id, target_user_id) .remove_share(person_id, target_user_id)
.await .await
} }
async fn merge_people(
&self,
target_person_id: Uuid,
source_person_id: Uuid,
user_id: Uuid,
) -> CoreResult<()> {
if target_person_id == source_person_id {
return Err(CoreError::Validation(
"Cannot merge the same person".to_string(),
));
}
self.auth_service
.check_permission(
Some(user_id),
authz::Permission::EditPerson(target_person_id),
)
.await?;
self.auth_service
.check_permission(
Some(user_id),
authz::Permission::EditPerson(source_person_id),
)
.await?;
self.face_repo
.reassign_person(source_person_id, target_person_id)
.await?;
self.person_repo.delete(source_person_id).await
}
} }

View File

@@ -99,6 +99,7 @@ pub trait FaceRegionRepository: Send + Sync {
async fn find_by_id(&self, face_region_id: Uuid) -> CoreResult<Option<FaceRegion>>; async fn find_by_id(&self, face_region_id: Uuid) -> CoreResult<Option<FaceRegion>>;
async fn update_person_id(&self, face_region_id: Uuid, person_id: Uuid) -> CoreResult<()>; async fn update_person_id(&self, face_region_id: Uuid, person_id: Uuid) -> CoreResult<()>;
async fn delete(&self, face_region_id: Uuid) -> CoreResult<()>; async fn delete(&self, face_region_id: Uuid) -> CoreResult<()>;
async fn reassign_person(&self, old_person_id: Uuid, new_person_id: Uuid) -> CoreResult<()>;
} }
#[async_trait] #[async_trait]

View File

@@ -106,6 +106,13 @@ pub trait PersonService: Send + Sync {
target_user_id: Uuid, target_user_id: Uuid,
owner_id: Uuid, owner_id: Uuid,
) -> CoreResult<()>; ) -> CoreResult<()>;
async fn merge_people(
&self,
target_person_id: Uuid,
source_person_id: Uuid,
user_id: Uuid,
) -> CoreResult<()>;
} }
#[async_trait] #[async_trait]

View File

@@ -1,5 +1,9 @@
use async_trait::async_trait; use async_trait::async_trait;
use libertas_core::{error::{CoreError, CoreResult}, models::FaceRegion, repositories::FaceRegionRepository}; use libertas_core::{
error::{CoreError, CoreResult},
models::FaceRegion,
repositories::FaceRegionRepository,
};
use sqlx::PgPool; use sqlx::PgPool;
use uuid::Uuid; use uuid::Uuid;
@@ -125,4 +129,20 @@ impl FaceRegionRepository for PostgresFaceRegionRepository {
.map_err(|e| CoreError::Database(e.to_string()))?; .map_err(|e| CoreError::Database(e.to_string()))?;
Ok(()) Ok(())
} }
async fn reassign_person(&self, old_person_id: Uuid, new_person_id: Uuid) -> CoreResult<()> {
sqlx::query!(
r#"
UPDATE face_regions
SET person_id = $1
WHERE person_id = $2
"#,
new_person_id,
old_person_id
)
.execute(&self.pool)
.await
.map_err(|e| CoreError::Database(e.to_string()))?;
Ok(())
}
} }