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:
@@ -76,4 +76,44 @@ pub struct PostgresAlbumShare {
|
||||
pub album_id: uuid::Uuid,
|
||||
pub user_id: uuid::Uuid,
|
||||
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(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
fn from(pg_role: PostgresRole) -> Self {
|
||||
@@ -121,4 +121,69 @@ impl From<PostgresAlbumShare> for AlbumShare {
|
||||
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 media_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