Implements search for custom fields closes #196 #266

Merged
moritz merged 11 commits from feature/196_search_custom_fields into main 2025-12-11 14:07:42 +01:00
Showing only changes of commit ca5fad0dcc - Show all commits

View file

@ -156,12 +156,15 @@ defmodule Mv.Membership.Member do
if is_binary(q) and String.trim(q) != "" do
q2 = String.trim(q)
pat = "%" <> q2 <> "%"
# Sanitize for LIKE patterns (escape % and _), limit length to 100 chars
q2_sanitized = sanitize_search_query(q2)
pat = "%" <> q2_sanitized <> "%"
# Build search filters grouped by search type for maintainability
# Priority: FTS > Substring > Custom Fields > Fuzzy Matching
# Note: FTS and fuzzy use q2 (unsanitized), LIKE-based filters use pat (sanitized)
fts_match = build_fts_filter(q2)
substring_match = build_substring_filter(q2, pat)
substring_match = build_substring_filter(q2_sanitized, pat)
custom_field_match = build_custom_field_filter(pat)
fuzzy_match = build_fuzzy_filter(q2, threshold)
@ -505,6 +508,31 @@ defmodule Mv.Membership.Member do
end
end
# ============================================================================
# Search Input Sanitization
# ============================================================================
# Sanitizes search input to prevent LIKE pattern injection.
# Escapes SQL LIKE wildcards (% and _) and limits query length.
#
# ## Examples
#
# iex> sanitize_search_query("test%injection")
# "test\\%injection"
#
# iex> sanitize_search_query("very_long_search")
# "very\\_long\\_search"
#
defp sanitize_search_query(query) when is_binary(query) do
query
|> String.slice(0, 100)
|> String.replace("\\", "\\\\")
|> String.replace("%", "\\%")
|> String.replace("_", "\\_")
end
defp sanitize_search_query(_), do: ""
# ============================================================================
# Search Filter Builders
# ============================================================================
@ -590,11 +618,13 @@ defmodule Mv.Membership.Member do
if has_search do
# Search query provided: return email-match OR fuzzy-search candidates
trimmed_search = String.trim(search_query)
# Sanitize for LIKE patterns (contains uses ILIKE internally)
sanitized_search = sanitize_search_query(trimmed_search)
# Build search filters - excluding custom_field_filter for performance
fts_match = build_fts_filter(trimmed_search)
fuzzy_match = build_fuzzy_filter(trimmed_search, @default_similarity_threshold)
email_substring_match = expr(contains(email, ^trimmed_search))
email_substring_match = expr(contains(email, ^sanitized_search))
query
|> Ash.Query.filter(