From 74d74a128b7852adab1e454e987bbec5c74c60a7 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Thu, 4 Dec 2025 00:12:53 +0100 Subject: [PATCH] feat: add media filtering by tag name --- libertas-frontend/src/routes/media/index.tsx | 1 + libertas_infra/src/query_builder.rs | 49 +++++++++++++++++++ .../src/repositories/media_repository.rs | 39 ++++++++------- 3 files changed, 70 insertions(+), 19 deletions(-) diff --git a/libertas-frontend/src/routes/media/index.tsx b/libertas-frontend/src/routes/media/index.tsx index 9460ac4..9aab844 100644 --- a/libertas-frontend/src/routes/media/index.tsx +++ b/libertas-frontend/src/routes/media/index.tsx @@ -138,6 +138,7 @@ function MediaPage() { Filename + Tag Make Model ISO diff --git a/libertas_infra/src/query_builder.rs b/libertas_infra/src/query_builder.rs index 4d3e872..081132d 100644 --- a/libertas_infra/src/query_builder.rs +++ b/libertas_infra/src/query_builder.rs @@ -273,6 +273,55 @@ impl FilterStrategy for MetadataFilterStrategy { } } +pub struct TagFilterStrategy; + +impl FilterStrategy for TagFilterStrategy { + fn can_handle(&self, field: &str) -> bool { + field == "tag.name" + } + + fn apply_join<'a>( + &self, + _query: &mut SqlxQueryBuilder<'a, sqlx::Postgres>, + _field: &'a str, + ) -> CoreResult<()> { + // We use EXISTS subqueries in apply_condition, so no main query join is needed. + Ok(()) + } + + fn apply_condition<'a>( + &self, + query: &mut SqlxQueryBuilder<'a, sqlx::Postgres>, + condition: &'a libertas_core::schema::FilterCondition, + ) -> CoreResult<()> { + use libertas_core::schema::FilterOperator; + + let op = match condition.operator { + FilterOperator::Eq => "=", + FilterOperator::Neq => "!=", + FilterOperator::Like => "ILIKE", + FilterOperator::Gt => ">", + FilterOperator::Lt => "<", + FilterOperator::Gte => ">=", + FilterOperator::Lte => "<=", + }; + + query.push(" AND EXISTS (SELECT 1 FROM media_tags mt JOIN tags t ON mt.tag_id = t.id WHERE mt.media_id = media.id AND t.name "); + query.push(op); + query.push(" "); + + if condition.operator == FilterOperator::Like { + query.push_bind(format!("%{}%", condition.value)); + } else { + query.push_bind(&condition.value); + } + + query.push(" ) "); + + Ok(()) + } +} + pub trait QueryBuilder { fn apply_options_to_query<'a>( &self, diff --git a/libertas_infra/src/repositories/media_repository.rs b/libertas_infra/src/repositories/media_repository.rs index d113145..05e8d86 100644 --- a/libertas_infra/src/repositories/media_repository.rs +++ b/libertas_infra/src/repositories/media_repository.rs @@ -25,17 +25,22 @@ impl PostgresMediaRepository { .allowed_sort_columns .clone() .unwrap_or_else(|| vec!["created_at".to_string(), "original_filename".to_string()]); - + allowed_columns.push("date_taken".to_string()); let sort_strategies: Vec> = vec![ - Box::new(crate::query_builder::StandardSortStrategy::new(allowed_columns.clone())), + Box::new(crate::query_builder::StandardSortStrategy::new( + allowed_columns.clone(), + )), Box::new(crate::query_builder::MetadataSortStrategy), ]; let filter_strategies: Vec> = vec![ - Box::new(crate::query_builder::StandardFilterStrategy::new(allowed_columns)), + Box::new(crate::query_builder::StandardFilterStrategy::new( + allowed_columns, + )), Box::new(crate::query_builder::MetadataFilterStrategy), + Box::new(crate::query_builder::TagFilterStrategy), ]; Self { @@ -119,15 +124,14 @@ impl MediaRepository for PostgresMediaRepository { ) -> CoreResult<(Vec, i64)> { let count_base_sql = "SELECT COUNT(DISTINCT media.id) as total FROM media"; let mut count_query = sqlx::QueryBuilder::new(count_base_sql); - + count_query = self.query_builder.apply_joins(count_query, options)?; count_query.push(" WHERE media.owner_id = "); count_query.push_bind(user_id); - let (mut count_query, metadata_filter_count) = self - .query_builder - .apply_conditions(count_query, options)?; + let (mut count_query, metadata_filter_count) = + self.query_builder.apply_conditions(count_query, options)?; if metadata_filter_count > 0 { count_query.push(" GROUP BY media.id "); @@ -148,15 +152,14 @@ impl MediaRepository for PostgresMediaRepository { let data_base_sql = "SELECT media.id, media.owner_id, media.storage_path, media.original_filename, media.mime_type, media.hash, media.created_at, media.thumbnail_path, media.date_taken FROM media"; let mut data_query = sqlx::QueryBuilder::new(data_base_sql); - + data_query = self.query_builder.apply_joins(data_query, options)?; data_query.push(" WHERE media.owner_id = "); data_query.push_bind(user_id); - let (mut data_query, metadata_filter_count) = self - .query_builder - .apply_conditions(data_query, options)?; + let (mut data_query, metadata_filter_count) = + self.query_builder.apply_conditions(data_query, options)?; if metadata_filter_count > 0 { data_query.push(" GROUP BY media.id "); @@ -192,15 +195,14 @@ impl MediaRepository for PostgresMediaRepository { JOIN face_regions fr ON media.id = fr.media_id "; let mut count_query = sqlx::QueryBuilder::new(count_base_sql); - + count_query = self.query_builder.apply_joins(count_query, options)?; count_query.push(" WHERE fr.person_id = "); count_query.push_bind(person_id); - let (mut count_query, _metadata_filter_count) = self - .query_builder - .apply_conditions(count_query, options)?; + let (mut count_query, _metadata_filter_count) = + self.query_builder.apply_conditions(count_query, options)?; let total_items_result = count_query .build_query_scalar() @@ -216,15 +218,14 @@ impl MediaRepository for PostgresMediaRepository { JOIN face_regions fr ON media.id = fr.media_id "; let mut data_query = sqlx::QueryBuilder::new(data_base_sql); - + data_query = self.query_builder.apply_joins(data_query, options)?; data_query.push(" WHERE fr.person_id = "); data_query.push_bind(person_id); - let (mut data_query, _metadata_filter_count) = self - .query_builder - .apply_conditions(data_query, options)?; + let (mut data_query, _metadata_filter_count) = + self.query_builder.apply_conditions(data_query, options)?; data_query.push(" GROUP BY media.id ");