feat: Implement person and tag management services
- Added `Person` and `Tag` models to the core library. - Created `PersonService` and `TagService` traits with implementations for managing persons and tags. - Introduced repositories for `Person`, `Tag`, `FaceRegion`, and `PersonShare` with PostgreSQL support. - Updated authorization logic to include permissions for accessing and editing persons. - Enhanced the schema to support new models and relationships. - Implemented database migrations for new tables related to persons and tags. - Added request and response structures for API interactions with persons and tags.
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
-- Create the 'albums' table
|
|
||||||
CREATE TABLE albums (
|
CREATE TABLE albums (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
owner_id UUID NOT NULL REFERENCES users (id),
|
owner_id UUID NOT NULL REFERENCES users (id),
|
||||||
@@ -9,15 +8,12 @@ CREATE TABLE albums (
|
|||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create the 'album_media' join table
|
|
||||||
-- This links media items to albums
|
|
||||||
CREATE TABLE album_media (
|
CREATE TABLE album_media (
|
||||||
album_id UUID NOT NULL REFERENCES albums (id) ON DELETE CASCADE,
|
album_id UUID NOT NULL REFERENCES albums (id) ON DELETE CASCADE,
|
||||||
media_id UUID NOT NULL REFERENCES media (id) ON DELETE CASCADE,
|
media_id UUID NOT NULL REFERENCES media (id) ON DELETE CASCADE,
|
||||||
PRIMARY KEY (album_id, media_id) -- Ensures no duplicates
|
PRIMARY KEY (album_id, media_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Indexes for faster lookups
|
|
||||||
CREATE INDEX idx_albums_owner_id ON albums (owner_id);
|
CREATE INDEX idx_albums_owner_id ON albums (owner_id);
|
||||||
|
|
||||||
CREATE INDEX idx_album_media_media_id ON album_media (media_id);
|
CREATE INDEX idx_album_media_media_id ON album_media (media_id);
|
||||||
12
libertas_api/migrations/20251115085433_create_tags_table.sql
Normal file
12
libertas_api/migrations/20251115085433_create_tags_table.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE tags (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE media_tags (
|
||||||
|
media_id UUID NOT NULL REFERENCES media (id) ON DELETE CASCADE,
|
||||||
|
tag_id UUID NOT NULL REFERENCES tags (id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (media_id, tag_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_media_tags_tag_id ON media_tags (tag_id);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE people (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
owner_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE face_regions (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
media_id UUID NOT NULL REFERENCES media (id) ON DELETE CASCADE,
|
||||||
|
person_id UUID REFERENCES people (id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
x_min REAL NOT NULL,
|
||||||
|
y_min REAL NOT NULL,
|
||||||
|
x_max REAL NOT NULL,
|
||||||
|
y_max REAL NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_people_owner_id ON people (owner_id);
|
||||||
|
CREATE INDEX idx_face_regions_media_id ON face_regions (media_id);
|
||||||
|
CREATE INDEX idx_face_regions_person_id ON face_regions (person_id);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
CREATE TYPE person_permission AS ENUM (
|
||||||
|
'view',
|
||||||
|
'can_use'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE person_shares (
|
||||||
|
person_id UUID NOT NULL REFERENCES people (id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
|
permission person_permission NOT NULL,
|
||||||
|
PRIMARY KEY (person_id, user_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_person_shares_user_id ON person_shares (user_id);
|
||||||
@@ -5,14 +5,13 @@ use libertas_core::{
|
|||||||
error::{CoreError, CoreResult},
|
error::{CoreError, CoreResult},
|
||||||
};
|
};
|
||||||
use libertas_infra::factory::{
|
use libertas_infra::factory::{
|
||||||
build_album_repository, build_album_share_repository, build_database_pool, build_media_metadata_repository, build_media_repository, build_user_repository
|
build_album_repository, build_album_share_repository, build_database_pool, build_face_region_repository, build_media_metadata_repository, build_media_repository, build_person_repository, build_person_share_repository, build_tag_repository, build_user_repository
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
security::{Argon2Hasher, JwtGenerator},
|
security::{Argon2Hasher, JwtGenerator},
|
||||||
services::{
|
services::{
|
||||||
album_service::AlbumServiceImpl, media_service::MediaServiceImpl,
|
album_service::AlbumServiceImpl, media_service::MediaServiceImpl, person_service::PersonServiceImpl, tag_service::TagServiceImpl, user_service::UserServiceImpl
|
||||||
user_service::UserServiceImpl,
|
|
||||||
},
|
},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
@@ -31,6 +30,10 @@ pub async fn build_app_state(config: AppConfig) -> CoreResult<AppState> {
|
|||||||
let album_share_repo = build_album_share_repository(&config.database, db_pool.clone()).await?;
|
let album_share_repo = build_album_share_repository(&config.database, db_pool.clone()).await?;
|
||||||
let media_metadata_repo =
|
let media_metadata_repo =
|
||||||
build_media_metadata_repository(&config.database, db_pool.clone()).await?;
|
build_media_metadata_repository(&config.database, db_pool.clone()).await?;
|
||||||
|
let tag_repo = build_tag_repository(&config.database, db_pool.clone()).await?;
|
||||||
|
let person_repo = build_person_repository(&config.database, db_pool.clone()).await?;
|
||||||
|
let face_region_repo = build_face_region_repository(&config.database, db_pool.clone()).await?;
|
||||||
|
let person_share_repo = build_person_share_repository(&config.database, db_pool.clone()).await?;
|
||||||
|
|
||||||
let hasher = Arc::new(Argon2Hasher::default());
|
let hasher = Arc::new(Argon2Hasher::default());
|
||||||
let tokenizer = Arc::new(JwtGenerator::new(config.jwt_secret.clone()));
|
let tokenizer = Arc::new(JwtGenerator::new(config.jwt_secret.clone()));
|
||||||
@@ -51,14 +54,26 @@ pub async fn build_app_state(config: AppConfig) -> CoreResult<AppState> {
|
|||||||
));
|
));
|
||||||
let album_service = Arc::new(AlbumServiceImpl::new(
|
let album_service = Arc::new(AlbumServiceImpl::new(
|
||||||
album_repo,
|
album_repo,
|
||||||
media_repo,
|
media_repo.clone(),
|
||||||
album_share_repo,
|
album_share_repo,
|
||||||
));
|
));
|
||||||
|
let tag_service = Arc::new(TagServiceImpl::new(
|
||||||
|
tag_repo,
|
||||||
|
media_repo.clone(),
|
||||||
|
));
|
||||||
|
let person_service = Arc::new(PersonServiceImpl::new(
|
||||||
|
person_repo,
|
||||||
|
face_region_repo,
|
||||||
|
media_repo.clone(),
|
||||||
|
person_share_repo,
|
||||||
|
));
|
||||||
|
|
||||||
Ok(AppState {
|
Ok(AppState {
|
||||||
user_service,
|
user_service,
|
||||||
media_service,
|
media_service,
|
||||||
album_service,
|
album_service,
|
||||||
|
tag_service,
|
||||||
|
person_service,
|
||||||
token_generator: tokenizer,
|
token_generator: tokenizer,
|
||||||
nats_client,
|
nats_client,
|
||||||
config,
|
config,
|
||||||
|
|||||||
@@ -2,3 +2,5 @@ pub mod album_handlers;
|
|||||||
pub mod auth_handlers;
|
pub mod auth_handlers;
|
||||||
pub mod media_handlers;
|
pub mod media_handlers;
|
||||||
pub mod user_handlers;
|
pub mod user_handlers;
|
||||||
|
pub mod tag_handlers;
|
||||||
|
pub mod person_handlers;
|
||||||
152
libertas_api/src/handlers/person_handlers.rs
Normal file
152
libertas_api/src/handlers/person_handlers.rs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::{get, post, put},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::ApiError,
|
||||||
|
middleware::auth::UserId,
|
||||||
|
schema::{
|
||||||
|
AssignFaceRequest, CreatePersonRequest, FaceRegionResponse, PersonResponse,
|
||||||
|
SharePersonRequest, UpdatePersonRequest,
|
||||||
|
},
|
||||||
|
state::AppState,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn people_routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(list_people).post(create_person))
|
||||||
|
.route(
|
||||||
|
"/{person_id}",
|
||||||
|
get(get_person)
|
||||||
|
.put(update_person)
|
||||||
|
.delete(delete_person),
|
||||||
|
)
|
||||||
|
.route("/{person_id}/share", post(share_person).delete(unshare_person))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn face_routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/media/{media_id}/faces", get(list_faces_for_media))
|
||||||
|
.route("/faces/{face_id}/person", put(assign_face_to_person))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_person(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Json(payload): Json<CreatePersonRequest>,
|
||||||
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
|
let person = state
|
||||||
|
.person_service
|
||||||
|
.create_person(&payload.name, user_id)
|
||||||
|
.await?;
|
||||||
|
Ok((StatusCode::CREATED, Json(PersonResponse::from(person))))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_person(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Path(person_id): Path<Uuid>,
|
||||||
|
) -> Result<Json<PersonResponse>, ApiError> {
|
||||||
|
let person = state
|
||||||
|
.person_service
|
||||||
|
.get_person(person_id, user_id)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(PersonResponse::from(person)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_people(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
) -> Result<Json<Vec<PersonResponse>>, ApiError> {
|
||||||
|
let people = state.person_service.list_people(user_id).await?;
|
||||||
|
let response = people.into_iter().map(PersonResponse::from).collect();
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_person(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Path(person_id): Path<Uuid>,
|
||||||
|
Json(payload): Json<UpdatePersonRequest>,
|
||||||
|
) -> Result<Json<PersonResponse>, ApiError> {
|
||||||
|
let person = state
|
||||||
|
.person_service
|
||||||
|
.update_person(person_id, &payload.name, user_id)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(PersonResponse::from(person)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_person(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Path(person_id): Path<Uuid>,
|
||||||
|
) -> Result<StatusCode, ApiError> {
|
||||||
|
state
|
||||||
|
.person_service
|
||||||
|
.delete_person(person_id, user_id)
|
||||||
|
.await?;
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn share_person(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Path(person_id): Path<Uuid>,
|
||||||
|
Json(payload): Json<SharePersonRequest>,
|
||||||
|
) -> Result<StatusCode, ApiError> {
|
||||||
|
state
|
||||||
|
.person_service
|
||||||
|
.share_person(
|
||||||
|
person_id,
|
||||||
|
payload.target_user_id,
|
||||||
|
payload.permission,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(StatusCode::OK)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unshare_person(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Path(person_id): Path<Uuid>,
|
||||||
|
Json(payload): Json<SharePersonRequest>,
|
||||||
|
) -> Result<StatusCode, ApiError> {
|
||||||
|
state
|
||||||
|
.person_service
|
||||||
|
.unshare_person(person_id, payload.target_user_id, user_id)
|
||||||
|
.await?;
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_faces_for_media(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Path(media_id): Path<Uuid>,
|
||||||
|
) -> Result<Json<Vec<FaceRegionResponse>>, ApiError> {
|
||||||
|
let faces = state
|
||||||
|
.person_service
|
||||||
|
.list_faces_for_media(media_id, user_id)
|
||||||
|
.await?;
|
||||||
|
let response = faces.into_iter().map(FaceRegionResponse::from).collect();
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn assign_face_to_person(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Path(face_id): Path<Uuid>,
|
||||||
|
Json(payload): Json<AssignFaceRequest>,
|
||||||
|
) -> Result<Json<FaceRegionResponse>, ApiError> {
|
||||||
|
let face = state
|
||||||
|
.person_service
|
||||||
|
.assign_face_to_person(face_id, payload.person_id, user_id)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(FaceRegionResponse::from(face)))
|
||||||
|
}
|
||||||
53
libertas_api/src/handlers/tag_handlers.rs
Normal file
53
libertas_api/src/handlers/tag_handlers.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use axum::{Json, Router, extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::{delete, post}};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{error::ApiError, middleware::auth::UserId, schema::{MediaTagsRequest, TagResponse}, state::AppState};
|
||||||
|
|
||||||
|
pub fn tag_routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/", post(add_tags_to_media).get(list_tags_for_media))
|
||||||
|
.route(
|
||||||
|
"/{tag_name}",
|
||||||
|
delete(remove_tag_from_media),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_tags_to_media(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Path(media_id): Path<Uuid>,
|
||||||
|
Json(payload): Json<MediaTagsRequest>,
|
||||||
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
|
let tags = state
|
||||||
|
.tag_service
|
||||||
|
.add_tags_to_media(media_id, &payload.tags, user_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response: Vec<TagResponse> = tags.into_iter().map(TagResponse::from).collect();
|
||||||
|
Ok((StatusCode::CREATED, Json(response)))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_tags_for_media(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Path(media_id): Path<Uuid>,
|
||||||
|
) -> Result<Json<Vec<TagResponse>>, ApiError> {
|
||||||
|
let tags = state
|
||||||
|
.tag_service
|
||||||
|
.list_tags_for_media(media_id, user_id)
|
||||||
|
.await?;
|
||||||
|
let response = tags.into_iter().map(TagResponse::from).collect();
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_tag_from_media(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
UserId(user_id): UserId,
|
||||||
|
Path((media_id, tag_name)): Path<(Uuid, String)>,
|
||||||
|
) -> Result<StatusCode, ApiError> {
|
||||||
|
state
|
||||||
|
.tag_service
|
||||||
|
.remove_tags_from_media(media_id, &[tag_name], user_id)
|
||||||
|
.await?;
|
||||||
|
Ok(StatusCode::NO_CONTENT)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use axum::{Router, routing::get};
|
use axum::{Router, routing::get};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
handlers::{album_handlers, auth_handlers, media_handlers, user_handlers},
|
handlers::{album_handlers, auth_handlers, media_handlers, person_handlers, tag_handlers, user_handlers},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -10,11 +10,17 @@ pub fn api_routes(max_upload_size: usize) -> Router<AppState> {
|
|||||||
let user_routes = user_handlers::user_routes();
|
let user_routes = user_handlers::user_routes();
|
||||||
let media_routes = media_handlers::media_routes(max_upload_size);
|
let media_routes = media_handlers::media_routes(max_upload_size);
|
||||||
let album_routes = album_handlers::album_routes();
|
let album_routes = album_handlers::album_routes();
|
||||||
|
let media_tag_routes = tag_handlers::tag_routes();
|
||||||
|
let people_routes = person_handlers::people_routes();
|
||||||
|
let face_routes = person_handlers::face_routes();
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/v1/health", get(|| async { "OK" }))
|
.route("/api/v1/health", get(|| async { "OK" }))
|
||||||
.nest("/api/v1/auth", auth_routes)
|
.nest("/api/v1/auth", auth_routes)
|
||||||
.nest("/api/v1/users", user_routes)
|
.nest("/api/v1/users", user_routes)
|
||||||
.nest("/api/v1/media", media_routes)
|
.nest("/api/v1/media", media_routes)
|
||||||
|
.nest("/api/v1/media/{media_id}/tags", media_tag_routes)
|
||||||
.nest("/api/v1/albums", album_routes)
|
.nest("/api/v1/albums", album_routes)
|
||||||
|
.nest("/api/v1/people", people_routes)
|
||||||
|
.nest("/api/v1", face_routes)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use libertas_core::{models::{Album, AlbumPermission, MediaMetadata}};
|
use libertas_core::models::{Album, AlbumPermission, FaceRegion, MediaMetadata, Person, PersonPermission, Tag};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -121,3 +121,78 @@ pub struct MediaDetailsResponse {
|
|||||||
pub metadata: Vec<MediaMetadataResponse>,
|
pub metadata: Vec<MediaMetadataResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct TagResponse {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Tag> for TagResponse {
|
||||||
|
fn from(tag: Tag) -> Self {
|
||||||
|
Self { id: tag.id, name: tag.name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct MediaTagsRequest {
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct PersonResponse {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub owner_id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Person> for PersonResponse {
|
||||||
|
fn from(person: Person) -> Self {
|
||||||
|
Self { id: person.id, owner_id: person.owner_id, name: person.name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreatePersonRequest {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdatePersonRequest {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct FaceRegionResponse {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub media_id: Uuid,
|
||||||
|
pub person_id: Option<Uuid>,
|
||||||
|
pub x_min: f32,
|
||||||
|
pub y_min: f32,
|
||||||
|
pub x_max: f32,
|
||||||
|
pub y_max: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FaceRegion> for FaceRegionResponse {
|
||||||
|
fn from(face: FaceRegion) -> Self {
|
||||||
|
Self {
|
||||||
|
id: face.id,
|
||||||
|
media_id: face.media_id,
|
||||||
|
person_id: face.person_id,
|
||||||
|
x_min: face.x_min,
|
||||||
|
y_min: face.y_min,
|
||||||
|
x_max: face.x_max,
|
||||||
|
y_max: face.y_max,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct AssignFaceRequest {
|
||||||
|
pub person_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SharePersonRequest {
|
||||||
|
pub target_user_id: Uuid,
|
||||||
|
pub permission: PersonPermission,
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
pub mod album_service;
|
pub mod album_service;
|
||||||
pub mod media_service;
|
pub mod media_service;
|
||||||
pub mod user_service;
|
pub mod user_service;
|
||||||
|
pub mod tag_service;
|
||||||
|
pub mod person_service;
|
||||||
228
libertas_api/src/services/person_service.rs
Normal file
228
libertas_api/src/services/person_service.rs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use libertas_core::{authz, error::{CoreError, CoreResult}, models::{FaceRegion, Media, Person, PersonPermission}, repositories::{FaceRegionRepository, MediaRepository, PersonRepository, PersonShareRepository}, services::PersonService};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct PersonServiceImpl {
|
||||||
|
person_repo: Arc<dyn PersonRepository>,
|
||||||
|
face_repo: Arc<dyn FaceRegionRepository>,
|
||||||
|
media_repo: Arc<dyn MediaRepository>,
|
||||||
|
person_share_repo: Arc<dyn PersonShareRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersonServiceImpl {
|
||||||
|
pub fn new(
|
||||||
|
person_repo: Arc<dyn PersonRepository>,
|
||||||
|
face_repo: Arc<dyn FaceRegionRepository>,
|
||||||
|
media_repo: Arc<dyn MediaRepository>,
|
||||||
|
person_share_repo: Arc<dyn PersonShareRepository>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
person_repo,
|
||||||
|
face_repo,
|
||||||
|
media_repo,
|
||||||
|
person_share_repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_and_authorize_person_owner(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Person> {
|
||||||
|
let person = self
|
||||||
|
.person_repo
|
||||||
|
.find_by_id(person_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(CoreError::NotFound("Person".to_string(), person_id))?;
|
||||||
|
|
||||||
|
if person.owner_id != user_id {
|
||||||
|
return Err(CoreError::Auth(
|
||||||
|
"User must be the owner to perform this action".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(person)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_and_authorize_person_access(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Person> {
|
||||||
|
let person = self
|
||||||
|
.person_repo
|
||||||
|
.find_by_id(person_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(CoreError::NotFound("Person".to_string(), person_id))?;
|
||||||
|
|
||||||
|
let share_permission = self.person_share_repo.get_user_permission(person_id, user_id).await?;
|
||||||
|
|
||||||
|
if !authz::can_access_person(user_id, &person, share_permission) {
|
||||||
|
return Err(CoreError::Auth(
|
||||||
|
"User does not have permission to access this person".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(person)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_and_authorize_person_usage(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Person> {
|
||||||
|
let person = self
|
||||||
|
.person_repo
|
||||||
|
.find_by_id(person_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(CoreError::NotFound("Person".to_string(), person_id))?;
|
||||||
|
|
||||||
|
let share_permission = self.person_share_repo.get_user_permission(person_id, user_id).await?;
|
||||||
|
|
||||||
|
if !authz::can_edit_person(user_id, &person, share_permission) {
|
||||||
|
return Err(CoreError::Auth(
|
||||||
|
"User does not have permission to use this person".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(person)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authorize_media_access(
|
||||||
|
&self,
|
||||||
|
media_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Media> {
|
||||||
|
let media = self.media_repo.find_by_id(media_id).await?.ok_or(CoreError::NotFound("Media".to_string(), media_id))?;
|
||||||
|
|
||||||
|
if !authz::is_owner(user_id, &media) {
|
||||||
|
return Err(CoreError::Auth(
|
||||||
|
"User does not have permission to access this media".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(media)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl PersonService for PersonServiceImpl {
|
||||||
|
async fn create_person(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
owner_id: Uuid,
|
||||||
|
) -> CoreResult<Person> {
|
||||||
|
let person = Person {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
owner_id,
|
||||||
|
name: name.to_string(),
|
||||||
|
thumbnail_media_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.person_repo.create(person.clone()).await?;
|
||||||
|
|
||||||
|
Ok(person)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<Person> {
|
||||||
|
self.get_and_authorize_person_access(person_id, user_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_people(&self, user_id: Uuid) -> CoreResult<Vec<Person>> {
|
||||||
|
let mut owned_people = self.person_repo.list_by_user(user_id).await?;
|
||||||
|
|
||||||
|
let shared_people_with_perms = self
|
||||||
|
.person_share_repo
|
||||||
|
.list_people_shared_with_user(user_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let shared_people = shared_people_with_perms
|
||||||
|
.into_iter()
|
||||||
|
.map(|(person, _permission)| person)
|
||||||
|
.collect::<Vec<Person>>();
|
||||||
|
|
||||||
|
owned_people.extend(shared_people);
|
||||||
|
|
||||||
|
Ok(owned_people)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_person(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
name: &str,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Person> {
|
||||||
|
let mut person = self.get_and_authorize_person_owner(person_id, user_id).await?;
|
||||||
|
person.name = name.to_string();
|
||||||
|
self.person_repo.update(person.clone()).await?;
|
||||||
|
Ok(person)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<()> {
|
||||||
|
self.get_and_authorize_person_owner(person_id, user_id).await?;
|
||||||
|
self.person_repo.delete(person_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn assign_face_to_person(
|
||||||
|
&self,
|
||||||
|
face_region_id: Uuid,
|
||||||
|
person_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<FaceRegion> {
|
||||||
|
self.get_and_authorize_person_usage(person_id, user_id).await?;
|
||||||
|
|
||||||
|
let mut face = self
|
||||||
|
.face_repo
|
||||||
|
.find_by_id(face_region_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(CoreError::NotFound("FaceRegion".to_string(), face_region_id))?;
|
||||||
|
|
||||||
|
self.authorize_media_access(face.media_id, user_id).await?;
|
||||||
|
|
||||||
|
self.face_repo
|
||||||
|
.update_person_id(face_region_id, person_id)
|
||||||
|
.await?;
|
||||||
|
face.person_id = Some(person_id);
|
||||||
|
|
||||||
|
Ok(face)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_faces_for_media(
|
||||||
|
&self,
|
||||||
|
media_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Vec<FaceRegion>> {
|
||||||
|
self.authorize_media_access(media_id, user_id).await?;
|
||||||
|
|
||||||
|
self.face_repo.find_by_media_id(media_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn share_person(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
target_user_id: Uuid,
|
||||||
|
permission: PersonPermission,
|
||||||
|
owner_id: Uuid,
|
||||||
|
) -> CoreResult<()> {
|
||||||
|
self.get_and_authorize_person_owner(person_id, owner_id).await?;
|
||||||
|
|
||||||
|
self.person_share_repo
|
||||||
|
.create_or_update_share(person_id, target_user_id, permission)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unshare_person(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
target_user_id: Uuid,
|
||||||
|
owner_id: Uuid,
|
||||||
|
) -> CoreResult<()> {
|
||||||
|
self.get_and_authorize_person_owner(person_id, owner_id).await?;
|
||||||
|
|
||||||
|
self.person_share_repo
|
||||||
|
.remove_share(person_id, target_user_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
92
libertas_api/src/services/tag_service.rs
Normal file
92
libertas_api/src/services/tag_service.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use libertas_core::{authz, error::{CoreError, CoreResult}, models::{Media, Tag}, repositories::{MediaRepository, TagRepository}, services::TagService};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub struct TagServiceImpl {
|
||||||
|
tag_repo: Arc<dyn TagRepository>,
|
||||||
|
media_repo: Arc<dyn MediaRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagServiceImpl {
|
||||||
|
pub fn new(
|
||||||
|
tag_repo: Arc<dyn TagRepository>,
|
||||||
|
media_repo: Arc<dyn MediaRepository>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
tag_repo,
|
||||||
|
media_repo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn authorize_media_access(
|
||||||
|
&self,
|
||||||
|
media_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Media> {
|
||||||
|
let media = self.media_repo.find_by_id(media_id).await?.ok_or(CoreError::NotFound("Media".to_string(), media_id))?;
|
||||||
|
|
||||||
|
if !authz::is_owner(user_id, &media) {
|
||||||
|
return Err(CoreError::Auth(
|
||||||
|
"User does not have permission to access this media".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(media)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TagService for TagServiceImpl {
|
||||||
|
async fn add_tags_to_media(
|
||||||
|
&self,
|
||||||
|
media_id: Uuid,
|
||||||
|
tag_names: &[String],
|
||||||
|
user_id: Uuid,
|
||||||
|
|
||||||
|
) -> CoreResult<Vec<Tag>> {
|
||||||
|
self.authorize_media_access(media_id, user_id).await?;
|
||||||
|
|
||||||
|
let mut tag_ids = Vec::new();
|
||||||
|
let tags = self.tag_repo.find_or_create_tags(tag_names).await?;
|
||||||
|
for tag in &tags {
|
||||||
|
tag_ids.push(tag.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tag_repo.add_tags_to_media(media_id, &tag_ids).await?;
|
||||||
|
|
||||||
|
Ok(tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_tags_from_media(
|
||||||
|
&self,
|
||||||
|
media_id: Uuid,
|
||||||
|
tag_names: &[String],
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<()> {
|
||||||
|
self.authorize_media_access(media_id, user_id).await?;
|
||||||
|
|
||||||
|
let tags = self.tag_repo.find_or_create_tags(tag_names).await?;
|
||||||
|
let mut tag_ids = Vec::new();
|
||||||
|
for tag in &tags {
|
||||||
|
tag_ids.push(tag.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tag_repo.remove_tags_from_media(media_id, &tag_ids).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_tags_for_media(
|
||||||
|
&self,
|
||||||
|
media_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Vec<Tag>> {
|
||||||
|
self.authorize_media_access(media_id, user_id).await?;
|
||||||
|
|
||||||
|
let tags = self.tag_repo.list_tags_for_media(media_id).await?;
|
||||||
|
|
||||||
|
Ok(tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use libertas_core::{
|
use libertas_core::{
|
||||||
config::AppConfig,
|
config::AppConfig,
|
||||||
services::{AlbumService, MediaService, UserService},
|
services::{AlbumService, MediaService, PersonService, TagService, UserService},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::security::TokenGenerator;
|
use crate::security::TokenGenerator;
|
||||||
@@ -12,6 +12,8 @@ pub struct AppState {
|
|||||||
pub user_service: Arc<dyn UserService>,
|
pub user_service: Arc<dyn UserService>,
|
||||||
pub media_service: Arc<dyn MediaService>,
|
pub media_service: Arc<dyn MediaService>,
|
||||||
pub album_service: Arc<dyn AlbumService>,
|
pub album_service: Arc<dyn AlbumService>,
|
||||||
|
pub tag_service: Arc<dyn TagService>,
|
||||||
|
pub person_service: Arc<dyn PersonService>,
|
||||||
pub token_generator: Arc<dyn TokenGenerator>,
|
pub token_generator: Arc<dyn TokenGenerator>,
|
||||||
pub nats_client: async_nats::Client,
|
pub nats_client: async_nats::Client,
|
||||||
pub config: AppConfig,
|
pub config: AppConfig,
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::models::{Album, AlbumPermission, Media, Role, User};
|
use crate::models::{Album, AlbumPermission, Media, Person, PersonPermission, Role, User};
|
||||||
|
|
||||||
pub trait Ownable {
|
pub trait Ownable {
|
||||||
fn owner_id(&self) -> Uuid;
|
fn owner_id(&self) -> Uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ownable for Media {
|
impl Ownable for Media {
|
||||||
fn owner_id(&self) -> Uuid {
|
fn owner_id(&self) -> Uuid {
|
||||||
self.owner_id
|
self.owner_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ownable for Album {
|
impl Ownable for Album {
|
||||||
fn owner_id(&self) -> Uuid {
|
fn owner_id(&self) -> Uuid {
|
||||||
self.owner_id
|
self.owner_id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Ownable for Person {
|
||||||
|
fn owner_id(&self) -> Uuid {
|
||||||
|
self.owner_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_admin(user: &User) -> bool {
|
pub fn is_admin(user: &User) -> bool {
|
||||||
user.role == Role::Admin
|
user.role == Role::Admin
|
||||||
}
|
}
|
||||||
@@ -39,3 +47,11 @@ pub fn can_contribute_to_album(
|
|||||||
) -> bool {
|
) -> bool {
|
||||||
is_owner(user_id, album) || share_permission == Some(AlbumPermission::Contribute)
|
is_owner(user_id, album) || share_permission == Some(AlbumPermission::Contribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn can_access_person(user_id: Uuid, person: &Person, share_permission: Option<PersonPermission>) -> bool {
|
||||||
|
is_owner(user_id, person) || share_permission.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_edit_person(user_id: Uuid, person: &Person, share_permission: Option<PersonPermission>) -> bool {
|
||||||
|
is_owner(user_id, person) || share_permission == Some(PersonPermission::CanUse)
|
||||||
|
}
|
||||||
@@ -93,6 +93,7 @@ pub struct Album {
|
|||||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct Person {
|
pub struct Person {
|
||||||
pub id: uuid::Uuid,
|
pub id: uuid::Uuid,
|
||||||
pub owner_id: uuid::Uuid,
|
pub owner_id: uuid::Uuid,
|
||||||
@@ -100,6 +101,7 @@ pub struct Person {
|
|||||||
pub thumbnail_media_id: Option<uuid::Uuid>,
|
pub thumbnail_media_id: Option<uuid::Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct FaceRegion {
|
pub struct FaceRegion {
|
||||||
pub id: uuid::Uuid,
|
pub id: uuid::Uuid,
|
||||||
pub media_id: uuid::Uuid,
|
pub media_id: uuid::Uuid,
|
||||||
@@ -150,4 +152,40 @@ pub struct AlbumShare {
|
|||||||
pub struct MediaBundle {
|
pub struct MediaBundle {
|
||||||
pub media: Media,
|
pub media: Media,
|
||||||
pub metadata: Vec<MediaMetadata>,
|
pub metadata: Vec<MediaMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Tag {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||||
|
pub enum PersonPermission {
|
||||||
|
View,
|
||||||
|
CanUse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersonPermission {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
PersonPermission::View => "view",
|
||||||
|
PersonPermission::CanUse => "can_use",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for PersonPermission {
|
||||||
|
fn from(s: &str) -> Self {
|
||||||
|
match s {
|
||||||
|
"can_use" => PersonPermission::CanUse,
|
||||||
|
_ => PersonPermission::View,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PersonShare {
|
||||||
|
pub person_id: uuid::Uuid,
|
||||||
|
pub user_id: uuid::Uuid,
|
||||||
|
pub permission: PersonPermission,
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::CoreResult,
|
error::CoreResult,
|
||||||
models::{Album, AlbumPermission, Media, MediaMetadata, User}, schema::ListMediaOptions,
|
models::{Album, AlbumPermission, FaceRegion, Media, MediaMetadata, Person, PersonPermission, Tag, User}, schema::ListMediaOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -57,4 +57,54 @@ pub trait AlbumShareRepository: Send + Sync {
|
|||||||
pub trait MediaMetadataRepository: Send + Sync {
|
pub trait MediaMetadataRepository: Send + Sync {
|
||||||
async fn create_batch(&self, metadata: &[MediaMetadata]) -> CoreResult<()>;
|
async fn create_batch(&self, metadata: &[MediaMetadata]) -> CoreResult<()>;
|
||||||
async fn find_by_media_id(&self, media_id: Uuid) -> CoreResult<Vec<MediaMetadata>>;
|
async fn find_by_media_id(&self, media_id: Uuid) -> CoreResult<Vec<MediaMetadata>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait TagRepository: Send + Sync {
|
||||||
|
async fn find_or_create_tags(&self, tag_names: &[String]) -> CoreResult<Vec<Tag>>;
|
||||||
|
async fn add_tags_to_media(&self, media_id: Uuid, tag_ids: &[Uuid]) -> CoreResult<()>;
|
||||||
|
async fn remove_tags_from_media(&self, media_id: Uuid, tag_ids: &[Uuid]) -> CoreResult<()>;
|
||||||
|
async fn list_tags_for_media(&self, media_id: Uuid) -> CoreResult<Vec<Tag>>;
|
||||||
|
async fn find_tag_by_name(&self, name: &str) -> CoreResult<Option<Tag>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait PersonRepository: Send + Sync {
|
||||||
|
async fn create(&self, person: Person) -> CoreResult<()>;
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> CoreResult<Option<Person>>;
|
||||||
|
async fn list_by_user(&self, user_id: Uuid) -> CoreResult<Vec<Person>>;
|
||||||
|
async fn update(&self, person: Person) -> CoreResult<()>;
|
||||||
|
async fn delete(&self, id: Uuid) -> CoreResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait FaceRegionRepository: Send + Sync {
|
||||||
|
async fn create_batch(&self, face_regions: &[FaceRegion]) -> CoreResult<()>;
|
||||||
|
async fn find_by_media_id(&self, media_id: Uuid) -> CoreResult<Vec<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 delete(&self, face_region_id: Uuid) -> CoreResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait PersonShareRepository: Send + Sync {
|
||||||
|
async fn create_or_update_share(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
permission: PersonPermission,
|
||||||
|
) -> CoreResult<()>;
|
||||||
|
|
||||||
|
async fn remove_share(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
||||||
|
|
||||||
|
async fn get_user_permission(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Option<PersonPermission>>;
|
||||||
|
|
||||||
|
async fn list_people_shared_with_user(
|
||||||
|
&self,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Vec<(Person, PersonPermission)>>;
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::CoreResult,
|
error::CoreResult,
|
||||||
models::{Album, Media, MediaBundle, User},
|
models::{Album, FaceRegion, Media, MediaBundle, Person, PersonPermission, Tag, User},
|
||||||
schema::{
|
schema::{
|
||||||
AddMediaToAlbumData, CreateAlbumData, CreateUserData, ListMediaOptions, LoginUserData, ShareAlbumData, UpdateAlbumData, UploadMediaData
|
AddMediaToAlbumData, CreateAlbumData, CreateUserData, ListMediaOptions, LoginUserData, ShareAlbumData, UpdateAlbumData, UploadMediaData
|
||||||
},
|
},
|
||||||
@@ -40,3 +40,48 @@ pub trait AlbumService: Send + Sync {
|
|||||||
) -> CoreResult<Album>;
|
) -> CoreResult<Album>;
|
||||||
async fn delete_album(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
async fn delete_album(&self, album_id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait TagService: Send + Sync {
|
||||||
|
async fn add_tags_to_media(&self, media_id: Uuid, tag_names: &[String], user_id: Uuid) -> CoreResult<Vec<Tag>>;
|
||||||
|
async fn remove_tags_from_media(&self, media_id: Uuid, tag_names: &[String], user_id: Uuid) -> CoreResult<()>;
|
||||||
|
async fn list_tags_for_media(&self, media_id: Uuid, user_id: Uuid) -> CoreResult<Vec<Tag>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait PersonService: Send + Sync {
|
||||||
|
async fn create_person(&self, name: &str, owner_id: Uuid) -> CoreResult<Person>;
|
||||||
|
async fn get_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<Person>;
|
||||||
|
async fn list_people(&self, user_id: Uuid) -> CoreResult<Vec<Person>>;
|
||||||
|
async fn update_person(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
name: &str,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Person>;
|
||||||
|
async fn delete_person(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
||||||
|
|
||||||
|
async fn assign_face_to_person(
|
||||||
|
&self,
|
||||||
|
face_region_id: Uuid,
|
||||||
|
person_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<FaceRegion>;
|
||||||
|
|
||||||
|
async fn list_faces_for_media(&self, media_id: Uuid, user_id: Uuid) -> CoreResult<Vec<FaceRegion>>;
|
||||||
|
|
||||||
|
async fn share_person(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
target_user_id: Uuid,
|
||||||
|
permission: PersonPermission,
|
||||||
|
owner_id: Uuid,
|
||||||
|
) -> CoreResult<()>;
|
||||||
|
|
||||||
|
async fn unshare_person(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
target_user_id: Uuid,
|
||||||
|
owner_id: Uuid,
|
||||||
|
) -> CoreResult<()>;
|
||||||
|
}
|
||||||
@@ -76,4 +76,44 @@ pub struct PostgresAlbumShare {
|
|||||||
pub album_id: uuid::Uuid,
|
pub album_id: uuid::Uuid,
|
||||||
pub user_id: uuid::Uuid,
|
pub user_id: uuid::Uuid,
|
||||||
pub permission: PostgresAlbumPermission,
|
pub permission: PostgresAlbumPermission,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub struct PostgresTag {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub struct PostgresPerson {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub owner_id: uuid::Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub struct PostgresFaceRegion {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub media_id: uuid::Uuid,
|
||||||
|
pub person_id: Option<uuid::Uuid>,
|
||||||
|
pub x_min: f32,
|
||||||
|
pub y_min: f32,
|
||||||
|
pub x_max: f32,
|
||||||
|
pub y_max: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, sqlx::Type, PartialEq, Eq, Deserialize)]
|
||||||
|
#[sqlx(rename_all = "lowercase")]
|
||||||
|
#[sqlx(type_name = "person_permission")]
|
||||||
|
pub enum PostgresPersonPermission {
|
||||||
|
View,
|
||||||
|
CanUse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub struct PostgresPersonShared {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub owner_id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub permission: PostgresPersonPermission,
|
||||||
}
|
}
|
||||||
@@ -102,4 +102,60 @@ pub async fn build_media_metadata_repository(
|
|||||||
"Sqlite media metadata repository not implemented".to_string(),
|
"Sqlite media metadata repository not implemented".to_string(),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn build_tag_repository(
|
||||||
|
_db_config: &DatabaseConfig,
|
||||||
|
pool: DatabasePool,
|
||||||
|
) -> CoreResult<Arc<dyn libertas_core::repositories::TagRepository>> {
|
||||||
|
match pool {
|
||||||
|
DatabasePool::Postgres(pg_pool) => Ok(Arc::new(
|
||||||
|
crate::repositories::tag_repository::PostgresTagRepository::new(pg_pool),
|
||||||
|
)),
|
||||||
|
DatabasePool::Sqlite(_sqlite_pool) => Err(CoreError::Database(
|
||||||
|
"Sqlite tag repository not implemented".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn build_person_repository(
|
||||||
|
_db_config: &DatabaseConfig,
|
||||||
|
pool: DatabasePool,
|
||||||
|
) -> CoreResult<Arc<dyn libertas_core::repositories::PersonRepository>> {
|
||||||
|
match pool {
|
||||||
|
DatabasePool::Postgres(pg_pool) => Ok(Arc::new(
|
||||||
|
crate::repositories::person_repository::PostgresPersonRepository::new(pg_pool),
|
||||||
|
)),
|
||||||
|
DatabasePool::Sqlite(_sqlite_pool) => Err(CoreError::Database(
|
||||||
|
"Sqlite person repository not implemented".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn build_face_region_repository(
|
||||||
|
_db_config: &DatabaseConfig,
|
||||||
|
pool: DatabasePool,
|
||||||
|
) -> CoreResult<Arc<dyn libertas_core::repositories::FaceRegionRepository>> {
|
||||||
|
match pool {
|
||||||
|
DatabasePool::Postgres(pg_pool) => Ok(Arc::new(
|
||||||
|
crate::repositories::face_region_repository::PostgresFaceRegionRepository::new(pg_pool),
|
||||||
|
)),
|
||||||
|
DatabasePool::Sqlite(_sqlite_pool) => Err(CoreError::Database(
|
||||||
|
"Sqlite face region repository not implemented".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn build_person_share_repository(
|
||||||
|
_db_config: &DatabaseConfig,
|
||||||
|
pool: DatabasePool,
|
||||||
|
) -> CoreResult<Arc<dyn libertas_core::repositories::PersonShareRepository>> {
|
||||||
|
match pool {
|
||||||
|
DatabasePool::Postgres(pg_pool) => Ok(Arc::new(
|
||||||
|
crate::repositories::person_share_repository::PostgresPersonShareRepository::new(pg_pool),
|
||||||
|
)),
|
||||||
|
DatabasePool::Sqlite(_sqlite_pool) => Err(CoreError::Database(
|
||||||
|
"Sqlite person share repository not implemented".to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use libertas_core::models::{Album, AlbumPermission, AlbumShare, Media, MediaMetadata, MediaMetadataSource, Role, User};
|
use libertas_core::models::{Album, AlbumPermission, AlbumShare, FaceRegion, Media, MediaMetadata, MediaMetadataSource, Person, PersonPermission, Role, Tag, User};
|
||||||
|
|
||||||
use crate::db_models::{PostgresAlbum, PostgresAlbumPermission, PostgresAlbumShare, PostgresMedia, PostgresMediaMetadata, PostgresMediaMetadataSource, PostgresRole, PostgresUser};
|
use crate::db_models::{PostgresAlbum, PostgresAlbumPermission, PostgresAlbumShare, PostgresFaceRegion, PostgresMedia, PostgresMediaMetadata, PostgresMediaMetadataSource, PostgresPerson, PostgresPersonPermission, PostgresPersonShared, PostgresRole, PostgresTag, PostgresUser};
|
||||||
|
|
||||||
impl From<PostgresRole> for Role {
|
impl From<PostgresRole> for Role {
|
||||||
fn from(pg_role: PostgresRole) -> Self {
|
fn from(pg_role: PostgresRole) -> Self {
|
||||||
@@ -121,4 +121,69 @@ impl From<PostgresAlbumShare> for AlbumShare {
|
|||||||
permission: AlbumPermission::from(pg_share.permission),
|
permission: AlbumPermission::from(pg_share.permission),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PostgresTag> for Tag {
|
||||||
|
fn from(pg_tag: PostgresTag) -> Self {
|
||||||
|
Tag {
|
||||||
|
id: pg_tag.id,
|
||||||
|
name: pg_tag.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PostgresPerson> for Person {
|
||||||
|
fn from(pg_person: PostgresPerson) -> Self {
|
||||||
|
Person {
|
||||||
|
id: pg_person.id,
|
||||||
|
owner_id: pg_person.owner_id,
|
||||||
|
name: pg_person.name,
|
||||||
|
thumbnail_media_id: None, // Not in the DB schema
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PostgresFaceRegion> for FaceRegion {
|
||||||
|
fn from(pg_face: PostgresFaceRegion) -> Self {
|
||||||
|
FaceRegion {
|
||||||
|
id: pg_face.id,
|
||||||
|
media_id: pg_face.media_id,
|
||||||
|
person_id: pg_face.person_id,
|
||||||
|
x_min: pg_face.x_min,
|
||||||
|
y_min: pg_face.y_min,
|
||||||
|
x_max: pg_face.x_max,
|
||||||
|
y_max: pg_face.y_max,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PostgresPersonPermission> for PersonPermission {
|
||||||
|
fn from(pg_perm: PostgresPersonPermission) -> Self {
|
||||||
|
match pg_perm {
|
||||||
|
PostgresPersonPermission::View => PersonPermission::View,
|
||||||
|
PostgresPersonPermission::CanUse => PersonPermission::CanUse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PersonPermission> for PostgresPersonPermission {
|
||||||
|
fn from(perm: PersonPermission) -> Self {
|
||||||
|
match perm {
|
||||||
|
PersonPermission::View => PostgresPersonPermission::View,
|
||||||
|
PersonPermission::CanUse => PostgresPersonPermission::CanUse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PostgresPersonShared> for (Person, PersonPermission) {
|
||||||
|
fn from(pg_shared: PostgresPersonShared) -> Self {
|
||||||
|
let person = Person {
|
||||||
|
id: pg_shared.id,
|
||||||
|
owner_id: pg_shared.owner_id,
|
||||||
|
name: pg_shared.name,
|
||||||
|
thumbnail_media_id: None,
|
||||||
|
};
|
||||||
|
let permission = PersonPermission::from(pg_shared.permission);
|
||||||
|
(person, permission)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
128
libertas_infra/src/repositories/face_region_repository.rs
Normal file
128
libertas_infra/src/repositories/face_region_repository.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use libertas_core::{error::{CoreError, CoreResult}, models::FaceRegion, repositories::FaceRegionRepository};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::db_models::PostgresFaceRegion;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PostgresFaceRegionRepository {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresFaceRegionRepository {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FaceRegionRepository for PostgresFaceRegionRepository {
|
||||||
|
async fn create_batch(&self, face_regions: &[FaceRegion]) -> CoreResult<()> {
|
||||||
|
if face_regions.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ids = Vec::with_capacity(face_regions.len());
|
||||||
|
let mut media_ids = Vec::with_capacity(face_regions.len());
|
||||||
|
let mut person_ids = Vec::with_capacity(face_regions.len());
|
||||||
|
let mut x_mins = Vec::with_capacity(face_regions.len());
|
||||||
|
let mut y_mins = Vec::with_capacity(face_regions.len());
|
||||||
|
let mut x_maxs = Vec::with_capacity(face_regions.len());
|
||||||
|
let mut y_maxs = Vec::with_capacity(face_regions.len());
|
||||||
|
|
||||||
|
for fr in face_regions {
|
||||||
|
ids.push(fr.id);
|
||||||
|
media_ids.push(fr.media_id);
|
||||||
|
person_ids.push(fr.person_id);
|
||||||
|
x_mins.push(fr.x_min);
|
||||||
|
y_mins.push(fr.y_min);
|
||||||
|
x_maxs.push(fr.x_max);
|
||||||
|
y_maxs.push(fr.y_max);
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO face_regions (id, media_id, person_id, x_min, y_min, x_max, y_max)
|
||||||
|
SELECT * FROM unnest(
|
||||||
|
$1::uuid[], $2::uuid[], $3::uuid[],
|
||||||
|
$4::real[], $5::real[], $6::real[], $7::real[]
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
&ids,
|
||||||
|
&media_ids,
|
||||||
|
&person_ids as &[Option<Uuid>],
|
||||||
|
&x_mins,
|
||||||
|
&y_mins,
|
||||||
|
&x_maxs,
|
||||||
|
&y_maxs
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_media_id(&self, media_id: Uuid) -> CoreResult<Vec<FaceRegion>> {
|
||||||
|
let pg_faces = sqlx::query_as!(
|
||||||
|
PostgresFaceRegion,
|
||||||
|
r#"
|
||||||
|
SELECT id, media_id, person_id, x_min, y_min, x_max, y_max
|
||||||
|
FROM face_regions
|
||||||
|
WHERE media_id = $1
|
||||||
|
"#,
|
||||||
|
media_id
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(pg_faces.into_iter().map(FaceRegion::from).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(&self, face_region_id: Uuid) -> CoreResult<Option<FaceRegion>> {
|
||||||
|
let pg_face = sqlx::query_as!(
|
||||||
|
PostgresFaceRegion,
|
||||||
|
r#"
|
||||||
|
SELECT id, media_id, person_id, x_min, y_min, x_max, y_max
|
||||||
|
FROM face_regions
|
||||||
|
WHERE id = $1
|
||||||
|
"#,
|
||||||
|
face_region_id
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(pg_face.map(FaceRegion::from))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_person_id(&self, face_region_id: Uuid, person_id: Uuid) -> CoreResult<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
UPDATE face_regions
|
||||||
|
SET person_id = $1
|
||||||
|
WHERE id = $2
|
||||||
|
"#,
|
||||||
|
person_id,
|
||||||
|
face_region_id
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, face_region_id: Uuid) -> CoreResult<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
DELETE FROM face_regions
|
||||||
|
WHERE id = $1
|
||||||
|
"#,
|
||||||
|
face_region_id
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,4 +2,8 @@ pub mod album_repository;
|
|||||||
pub mod album_share_repository;
|
pub mod album_share_repository;
|
||||||
pub mod media_repository;
|
pub mod media_repository;
|
||||||
pub mod user_repository;
|
pub mod user_repository;
|
||||||
pub mod media_metadata_repository;
|
pub mod media_metadata_repository;
|
||||||
|
pub mod tag_repository;
|
||||||
|
pub mod person_repository;
|
||||||
|
pub mod face_region_repository;
|
||||||
|
pub mod person_share_repository;
|
||||||
98
libertas_infra/src/repositories/person_repository.rs
Normal file
98
libertas_infra/src/repositories/person_repository.rs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use libertas_core::{error::{CoreError, CoreResult}, models::Person, repositories::PersonRepository};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::db_models::PostgresPerson;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PostgresPersonRepository {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresPersonRepository {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl PersonRepository for PostgresPersonRepository {
|
||||||
|
async fn create(&self, person: Person) -> CoreResult<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO people (id, owner_id, name)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
"#,
|
||||||
|
person.id,
|
||||||
|
person.owner_id,
|
||||||
|
person.name
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_by_id(&self, id: Uuid) -> CoreResult<Option<Person>> {
|
||||||
|
let pg_person = sqlx::query_as!(
|
||||||
|
PostgresPerson,
|
||||||
|
r#"
|
||||||
|
SELECT id, owner_id, name
|
||||||
|
FROM people
|
||||||
|
WHERE id = $1
|
||||||
|
"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(pg_person.map(Person::from))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_by_user(&self, user_id: Uuid) -> CoreResult<Vec<Person>> {
|
||||||
|
let pg_people = sqlx::query_as!(
|
||||||
|
PostgresPerson,
|
||||||
|
r#"
|
||||||
|
SELECT id, owner_id, name
|
||||||
|
FROM people
|
||||||
|
WHERE owner_id = $1
|
||||||
|
"#,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(pg_people.into_iter().map(Person::from).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&self, person: Person) -> CoreResult<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
UPDATE people
|
||||||
|
SET name = $1
|
||||||
|
WHERE id = $2
|
||||||
|
"#,
|
||||||
|
person.name,
|
||||||
|
person.id
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete(&self, id: Uuid) -> CoreResult<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
DELETE FROM people
|
||||||
|
WHERE id = $1
|
||||||
|
"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
106
libertas_infra/src/repositories/person_share_repository.rs
Normal file
106
libertas_infra/src/repositories/person_share_repository.rs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use libertas_core::{
|
||||||
|
error::{CoreError, CoreResult},
|
||||||
|
models::{Person, PersonPermission},
|
||||||
|
repositories::PersonShareRepository,
|
||||||
|
};
|
||||||
|
use sqlx::{types::Uuid, PgPool};
|
||||||
|
|
||||||
|
use crate::db_models::{PostgresPersonPermission, PostgresPersonShared};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PostgresPersonShareRepository {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresPersonShareRepository {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl PersonShareRepository for PostgresPersonShareRepository {
|
||||||
|
async fn create_or_update_share(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
permission: PersonPermission,
|
||||||
|
) -> CoreResult<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO person_shares (person_id, user_id, permission)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (person_id, user_id)
|
||||||
|
DO UPDATE SET permission = $3
|
||||||
|
"#,
|
||||||
|
person_id,
|
||||||
|
user_id,
|
||||||
|
PostgresPersonPermission::from(permission) as PostgresPersonPermission,
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_share(&self, person_id: Uuid, user_id: Uuid) -> CoreResult<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
DELETE FROM person_shares
|
||||||
|
WHERE person_id = $1 AND user_id = $2
|
||||||
|
"#,
|
||||||
|
person_id,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_user_permission(
|
||||||
|
&self,
|
||||||
|
person_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Option<PersonPermission>> {
|
||||||
|
let row = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT permission as "permission: PostgresPersonPermission"
|
||||||
|
FROM person_shares
|
||||||
|
WHERE person_id = $1 AND user_id = $2
|
||||||
|
"#,
|
||||||
|
person_id,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(row.map(|r| r.permission.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_people_shared_with_user(
|
||||||
|
&self,
|
||||||
|
user_id: Uuid,
|
||||||
|
) -> CoreResult<Vec<(Person, PersonPermission)>> {
|
||||||
|
let shared_people = sqlx::query_as!(
|
||||||
|
PostgresPersonShared,
|
||||||
|
r#"
|
||||||
|
SELECT p.id, p.owner_id, p.name, ps.permission as "permission: PostgresPersonPermission"
|
||||||
|
FROM people p
|
||||||
|
JOIN person_shares ps ON p.id = ps.person_id
|
||||||
|
WHERE ps.user_id = $1
|
||||||
|
"#,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(shared_people
|
||||||
|
.into_iter()
|
||||||
|
.map(PostgresPersonShared::into)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
132
libertas_infra/src/repositories/tag_repository.rs
Normal file
132
libertas_infra/src/repositories/tag_repository.rs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use libertas_core::{error::{CoreError, CoreResult}, models::Tag, repositories::TagRepository};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::db_models::PostgresTag;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PostgresTagRepository {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresTagRepository {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TagRepository for PostgresTagRepository {
|
||||||
|
async fn find_or_create_tags(&self, tag_names: &[String]) -> CoreResult<Vec<Tag>> {
|
||||||
|
if tag_names.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_ids: Vec<Uuid> = (0..tag_names.len()).map(|_| Uuid::new_v4()).collect();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO tags (id, name)
|
||||||
|
SELECT * FROM unnest($1::uuid[], $2::text[])
|
||||||
|
ON CONFLICT (name) DO NOTHING
|
||||||
|
"#,
|
||||||
|
&new_ids,
|
||||||
|
tag_names,
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
let pg_tags = sqlx::query_as!(
|
||||||
|
PostgresTag,
|
||||||
|
r#"
|
||||||
|
SELECT id, name
|
||||||
|
FROM tags
|
||||||
|
WHERE name = ANY($1)
|
||||||
|
"#,
|
||||||
|
tag_names
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(pg_tags.into_iter().map(Tag::from).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_tags_to_media(&self, media_id: Uuid, tag_ids: &[Uuid]) -> CoreResult<()> {
|
||||||
|
if tag_ids.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO media_tags (media_id, tag_id)
|
||||||
|
SELECT $1, tag_id FROM unnest($2::uuid[]) AS tag_id
|
||||||
|
ON CONFLICT (media_id, tag_id) DO NOTHING
|
||||||
|
"#,
|
||||||
|
media_id,
|
||||||
|
tag_ids
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_tags_from_media(&self, media_id: Uuid, tag_ids: &[Uuid]) -> CoreResult<()> {
|
||||||
|
if tag_ids.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
DELETE FROM media_tags
|
||||||
|
WHERE media_id = $1 AND tag_id = ANY($2)
|
||||||
|
"#,
|
||||||
|
media_id,
|
||||||
|
tag_ids
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_tags_for_media(&self, media_id: Uuid) -> CoreResult<Vec<Tag>> {
|
||||||
|
let pg_tags = sqlx::query_as!(
|
||||||
|
PostgresTag,
|
||||||
|
r#"
|
||||||
|
SELECT t.id, t.name
|
||||||
|
FROM tags t
|
||||||
|
JOIN media_tags mt ON t.id = mt.tag_id
|
||||||
|
WHERE mt.media_id = $1
|
||||||
|
"#,
|
||||||
|
media_id
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(pg_tags.into_iter().map(Tag::from).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_tag_by_name(&self, name: &str) -> CoreResult<Option<Tag>> {
|
||||||
|
let pg_tag = sqlx::query_as!(
|
||||||
|
PostgresTag,
|
||||||
|
r#"
|
||||||
|
SELECT id, name
|
||||||
|
FROM tags
|
||||||
|
WHERE name = $1
|
||||||
|
"#,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| CoreError::Database(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(pg_tag.map(Tag::from))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user