feat(search): implement search filtering in repository SQL queries

Wire SearchFilter into each repository's list method with entity-specific
searchable columns: roasters (name, country, city), roasts (name, roaster,
origin, producer), bags (roaster, roast), brews (roast, roaster),
gear (make, model). Timeline passes None.
This commit is contained in:
Jon Seager 2026-02-03 12:16:50 +00:00
parent f616958e3c
commit 1e151123a0
No known key found for this signature in database
6 changed files with 47 additions and 3 deletions

View file

@ -160,7 +160,10 @@ impl BagRepository for SqlBagRepository {
&self, &self,
filter: BagFilter, filter: BagFilter,
request: &ListRequest<BagSortKey>, request: &ListRequest<BagSortKey>,
search: Option<&str>,
) -> Result<Page<BagWithRoast>, RepositoryError> { ) -> Result<Page<BagWithRoast>, RepositoryError> {
use crate::infrastructure::repositories::pagination::SearchFilter;
let order_clause = Self::order_clause(request); let order_clause = Self::order_clause(request);
// Build WHERE clause from filter // Build WHERE clause from filter
@ -172,16 +175,21 @@ impl BagRepository for SqlBagRepository {
}; };
let count_query = match &where_clause { let count_query = match &where_clause {
Some(w) => format!("SELECT COUNT(*) FROM bags b WHERE {w}"), Some(w) => format!(
None => "SELECT COUNT(*) FROM bags".to_string(), "SELECT COUNT(*) FROM bags b JOIN roasts r ON b.roast_id = r.id JOIN roasters rr ON r.roaster_id = rr.id WHERE {w}"
),
None => "SELECT COUNT(*) FROM bags b JOIN roasts r ON b.roast_id = r.id JOIN roasters rr ON r.roaster_id = rr.id".to_string(),
}; };
let sf = search.and_then(|t| SearchFilter::new(t, vec!["rr.name", "r.name"]));
crate::infrastructure::repositories::pagination::paginate( crate::infrastructure::repositories::pagination::paginate(
&self.pool, &self.pool,
request, request,
&base_query, &base_query,
&count_query, &count_query,
&order_clause, &order_clause,
sf.as_ref(),
|record| Ok(Self::to_domain_with_roast(record)), |record| Ok(Self::to_domain_with_roast(record)),
) )
.await .await

View file

@ -197,7 +197,10 @@ impl BrewRepository for SqlBrewRepository {
&self, &self,
filter: BrewFilter, filter: BrewFilter,
request: &ListRequest<BrewSortKey>, request: &ListRequest<BrewSortKey>,
search: Option<&str>,
) -> Result<Page<BrewWithDetails>, RepositoryError> { ) -> Result<Page<BrewWithDetails>, RepositoryError> {
use crate::infrastructure::repositories::pagination::SearchFilter;
let order_clause = Self::order_clause(request); let order_clause = Self::order_clause(request);
let where_clause = Self::build_where_clause(&filter); let where_clause = Self::build_where_clause(&filter);
@ -209,6 +212,8 @@ impl BrewRepository for SqlBrewRepository {
let count_base = r" let count_base = r"
SELECT COUNT(*) FROM brews br SELECT COUNT(*) FROM brews br
JOIN bags b ON br.bag_id = b.id JOIN bags b ON br.bag_id = b.id
JOIN roasts r ON b.roast_id = r.id
JOIN roasters rr ON r.roaster_id = rr.id
"; ";
let count_query = match &where_clause { let count_query = match &where_clause {
@ -216,12 +221,15 @@ impl BrewRepository for SqlBrewRepository {
None => count_base.to_string(), None => count_base.to_string(),
}; };
let sf = search.and_then(|t| SearchFilter::new(t, vec!["r.name", "rr.name"]));
crate::infrastructure::repositories::pagination::paginate( crate::infrastructure::repositories::pagination::paginate(
&self.pool, &self.pool,
request, request,
&base_query, &base_query,
&count_query, &count_query,
&order_clause, &order_clause,
sf.as_ref(),
|record| Ok(Self::to_domain_with_details(record)), |record| Ok(Self::to_domain_with_details(record)),
) )
.await .await

View file

@ -100,7 +100,10 @@ impl GearRepository for SqlGearRepository {
&self, &self,
filter: GearFilter, filter: GearFilter,
request: &ListRequest<GearSortKey>, request: &ListRequest<GearSortKey>,
search: Option<&str>,
) -> Result<Page<Gear>, RepositoryError> { ) -> Result<Page<Gear>, RepositoryError> {
use crate::infrastructure::repositories::pagination::SearchFilter;
let order_clause = Self::order_clause(request); let order_clause = Self::order_clause(request);
let where_clause = Self::build_where_clause(&filter); let where_clause = Self::build_where_clause(&filter);
@ -118,12 +121,15 @@ impl GearRepository for SqlGearRepository {
None => "SELECT COUNT(*) FROM gear".to_string(), None => "SELECT COUNT(*) FROM gear".to_string(),
}; };
let sf = search.and_then(|t| SearchFilter::new(t, vec!["make", "model"]));
crate::infrastructure::repositories::pagination::paginate( crate::infrastructure::repositories::pagination::paginate(
&self.pool, &self.pool,
request, request,
&base_query, &base_query,
&count_query, &count_query,
&order_clause, &order_clause,
sf.as_ref(),
Self::to_domain, Self::to_domain,
) )
.await .await

View file

@ -190,11 +190,16 @@ impl RoasterRepository for SqlRoasterRepository {
async fn list( async fn list(
&self, &self,
request: &ListRequest<RoasterSortKey>, request: &ListRequest<RoasterSortKey>,
search: Option<&str>,
) -> Result<Page<Roaster>, RepositoryError> { ) -> Result<Page<Roaster>, RepositoryError> {
use crate::infrastructure::repositories::pagination::SearchFilter;
let order_clause = Self::order_clause(request); let order_clause = Self::order_clause(request);
let base_query = let base_query =
"SELECT id, name, slug, country, city, homepage, notes, created_at FROM roasters"; "SELECT id, name, slug, country, city, homepage, notes, created_at FROM roasters";
let count_query = "SELECT COUNT(*) FROM roasters"; let count_query = "SELECT COUNT(*) FROM roasters";
let sf =
search.and_then(|t| SearchFilter::new(t, vec!["name", "country", "COALESCE(city,'')"]));
crate::infrastructure::repositories::pagination::paginate( crate::infrastructure::repositories::pagination::paginate(
&self.pool, &self.pool,
@ -202,6 +207,7 @@ impl RoasterRepository for SqlRoasterRepository {
base_query, base_query,
count_query, count_query,
&order_clause, &order_clause,
sf.as_ref(),
|record| Ok(Self::into_domain(record)), |record| Ok(Self::into_domain(record)),
) )
.await .await

View file

@ -249,10 +249,24 @@ impl RoastRepository for SqlRoastRepository {
async fn list( async fn list(
&self, &self,
request: &ListRequest<RoastSortKey>, request: &ListRequest<RoastSortKey>,
search: Option<&str>,
) -> Result<Page<RoastWithRoaster>, RepositoryError> { ) -> Result<Page<RoastWithRoaster>, RepositoryError> {
use crate::infrastructure::repositories::pagination::SearchFilter;
let order_clause = Self::order_clause(request); let order_clause = Self::order_clause(request);
let base_query = "SELECT r.id, r.roaster_id, r.name, r.slug, r.origin, r.region, r.producer, r.process, r.tasting_notes, r.created_at, ro.name AS roaster_name, ro.slug AS roaster_slug \n FROM roasts r \n JOIN roasters ro ON ro.id = r.roaster_id"; let base_query = "SELECT r.id, r.roaster_id, r.name, r.slug, r.origin, r.region, r.producer, r.process, r.tasting_notes, r.created_at, ro.name AS roaster_name, ro.slug AS roaster_slug \n FROM roasts r \n JOIN roasters ro ON ro.id = r.roaster_id";
let count_query = "SELECT COUNT(*) FROM roasts"; let count_query = "SELECT COUNT(*) FROM roasts r JOIN roasters ro ON ro.id = r.roaster_id";
let sf = search.and_then(|t| {
SearchFilter::new(
t,
vec![
"r.name",
"ro.name",
"COALESCE(r.origin,'')",
"COALESCE(r.producer,'')",
],
)
});
crate::infrastructure::repositories::pagination::paginate( crate::infrastructure::repositories::pagination::paginate(
&self.pool, &self.pool,
@ -260,6 +274,7 @@ impl RoastRepository for SqlRoastRepository {
base_query, base_query,
count_query, count_query,
&order_clause, &order_clause,
sf.as_ref(),
|record: RoastWithRoasterRecord| record.into_with_roaster(), |record: RoastWithRoasterRecord| record.into_with_roaster(),
) )
.await .await

View file

@ -92,6 +92,7 @@ impl TimelineEventRepository for SqlTimelineEventRepository {
base_query, base_query,
count_query, count_query,
&order_clause, &order_clause,
None,
|record: TimelineEventRecord| record.into_domain(), |record: TimelineEventRecord| record.into_domain(),
) )
.await .await