
- Add event categories config endpoint (/api/config/event-categories) - Fix bulletin processing N+1: batch hymn lookups for single bulletins - Optimize V2 events list endpoint: replace memory pagination with database LIMIT/OFFSET - Add missing SQL functions: list_events_paginated() and count_events() - Preserve HTTP response compatibility for iOS app
297 lines
11 KiB
Rust
297 lines
11 KiB
Rust
// SHARED PROCESSING FUNCTIONS for bulletins handler
|
|
use crate::{
|
|
error::Result,
|
|
models::Bulletin,
|
|
services::HymnalService,
|
|
};
|
|
use regex::Regex;
|
|
|
|
/// Process multiple bulletins with shared logic (optimized to prevent N+1 queries)
|
|
pub async fn process_bulletins_batch(
|
|
pool: &sqlx::PgPool,
|
|
bulletins: &mut [Bulletin]
|
|
) -> Result<()> {
|
|
// Collect all hymn numbers across all bulletins first for batch lookup
|
|
let mut all_hymn_numbers = std::collections::HashSet::new();
|
|
|
|
for bulletin in bulletins.iter() {
|
|
// Extract hymn numbers from divine_worship content
|
|
if let Some(ref worship_content) = bulletin.divine_worship {
|
|
let hymn_numbers = extract_hymn_numbers_from_content(worship_content);
|
|
all_hymn_numbers.extend(hymn_numbers);
|
|
}
|
|
|
|
// Extract hymn numbers from sabbath_school content
|
|
if let Some(ref ss_content) = bulletin.sabbath_school {
|
|
let hymn_numbers = extract_hymn_numbers_from_content(ss_content);
|
|
all_hymn_numbers.extend(hymn_numbers);
|
|
}
|
|
}
|
|
|
|
// Batch query all hymns from both hymnals
|
|
let hymn_numbers_vec: Vec<i32> = all_hymn_numbers.into_iter().collect();
|
|
let hymnal_codes = vec!["sda-1985".to_string(), "sda-1941".to_string()];
|
|
let hymn_map = crate::sql::hymnal::get_hymns_by_numbers_batch(pool, &hymnal_codes, &hymn_numbers_vec).await?;
|
|
|
|
// Now process each bulletin using the cached hymn data
|
|
for bulletin in bulletins.iter_mut() {
|
|
process_single_bulletin_with_cache(pool, bulletin, &hymn_map).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Process a single bulletin with all required transformations (optimized to prevent N+1)
|
|
pub async fn process_single_bulletin(
|
|
pool: &sqlx::PgPool,
|
|
bulletin: &mut Bulletin
|
|
) -> Result<()> {
|
|
// Process scripture reading to include full verse text
|
|
bulletin.scripture_reading = process_scripture_reading(pool, &bulletin.scripture_reading).await?;
|
|
|
|
// Collect all hymn numbers from this bulletin first for batch lookup
|
|
let mut all_hymn_numbers = std::collections::HashSet::new();
|
|
|
|
// Extract hymn numbers from divine_worship content
|
|
if let Some(ref worship_content) = bulletin.divine_worship {
|
|
let hymn_numbers = extract_hymn_numbers_from_content(worship_content);
|
|
all_hymn_numbers.extend(hymn_numbers);
|
|
}
|
|
|
|
// Extract hymn numbers from sabbath_school content
|
|
if let Some(ref ss_content) = bulletin.sabbath_school {
|
|
let hymn_numbers = extract_hymn_numbers_from_content(ss_content);
|
|
all_hymn_numbers.extend(hymn_numbers);
|
|
}
|
|
|
|
// Batch query all hymns from both hymnals for this single bulletin
|
|
let hymn_numbers_vec: Vec<i32> = all_hymn_numbers.into_iter().collect();
|
|
let hymnal_codes = vec!["sda-1985".to_string(), "sda-1941".to_string()];
|
|
let hymn_map = crate::sql::hymnal::get_hymns_by_numbers_batch(pool, &hymnal_codes, &hymn_numbers_vec).await?;
|
|
|
|
// Process hymn references using cached data
|
|
if let Some(ref worship_content) = bulletin.divine_worship {
|
|
bulletin.divine_worship = Some(process_hymn_references_with_cache(worship_content, &hymn_map));
|
|
}
|
|
|
|
if let Some(ref ss_content) = bulletin.sabbath_school {
|
|
bulletin.sabbath_school = Some(process_hymn_references_with_cache(ss_content, &hymn_map));
|
|
}
|
|
|
|
// Ensure sunset field compatibility
|
|
if bulletin.sunset.is_none() {
|
|
bulletin.sunset = Some("TBA".to_string());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Process scripture reading field to lookup and include full verse text
|
|
async fn process_scripture_reading(
|
|
pool: &sqlx::PgPool,
|
|
scripture: &Option<String>,
|
|
) -> Result<Option<String>> {
|
|
let Some(scripture_text) = scripture else {
|
|
return Ok(None);
|
|
};
|
|
|
|
// If it already looks like it has full verse text (long), return as-is
|
|
if scripture_text.len() > 50 {
|
|
return Ok(Some(scripture_text.clone()));
|
|
}
|
|
|
|
// Try to find the verse(s) using direct SQL search
|
|
// Allow up to 10 verses for ranges like "Matt 1:21-23"
|
|
let verses = sqlx::query_as!(
|
|
crate::models::BibleVerse,
|
|
"SELECT * FROM bible_verses WHERE is_active = true AND (reference ILIKE $1 OR text ILIKE $1) ORDER BY reference LIMIT $2",
|
|
format!("%{}%", scripture_text),
|
|
10i64
|
|
)
|
|
.fetch_all(pool)
|
|
.await?;
|
|
|
|
if !verses.is_empty() {
|
|
if verses.len() == 1 {
|
|
// Single verse - format as before
|
|
let verse = &verses[0];
|
|
Ok(Some(format!("{} - {}", verse.text, scripture_text)))
|
|
} else {
|
|
// Multiple verses - combine them
|
|
let combined_text = verses
|
|
.iter()
|
|
.map(|v| v.text.as_str())
|
|
.collect::<Vec<&str>>()
|
|
.join(" ");
|
|
Ok(Some(format!("{} - {}", combined_text, scripture_text)))
|
|
}
|
|
} else {
|
|
// If no match found, return original text
|
|
Ok(Some(scripture_text.clone()))
|
|
}
|
|
}
|
|
|
|
/// Process hymn references in bulletin content to include titles
|
|
/// Looks for patterns like #319, Hymn 319, No. 319 and adds hymn titles
|
|
pub async fn process_hymn_references(
|
|
pool: &sqlx::PgPool,
|
|
content: &str,
|
|
) -> Result<String> {
|
|
// Create regex patterns to match hymn references
|
|
let hymn_patterns = vec![
|
|
// #319
|
|
Regex::new(r"#(\d{1,3})").unwrap(),
|
|
// Hymn 319 (case insensitive)
|
|
Regex::new(r"(?i)hymn\s+(\d{1,3})").unwrap(),
|
|
// No. 319
|
|
Regex::new(r"(?i)no\.\s*(\d{1,3})").unwrap(),
|
|
// Number 319
|
|
Regex::new(r"(?i)number\s+(\d{1,3})").unwrap(),
|
|
];
|
|
|
|
let mut result = content.to_string();
|
|
|
|
// Default to 1985 hymnal (most common)
|
|
let default_hymnal = "sda-1985";
|
|
|
|
// Process each pattern
|
|
for pattern in hymn_patterns {
|
|
let mut matches_to_replace = Vec::new();
|
|
|
|
// Find all matches for this pattern
|
|
for capture in pattern.captures_iter(&result) {
|
|
if let Some(number_match) = capture.get(1) {
|
|
if let Ok(hymn_number) = number_match.as_str().parse::<i32>() {
|
|
// Try to get hymn from 1985 hymnal first, then 1941
|
|
let hymn_title = match HymnalService::get_hymn_by_number(pool, default_hymnal, hymn_number).await {
|
|
Ok(Some(hymn)) => Some(hymn.title),
|
|
_ => {
|
|
// Try 1941 hymnal as fallback
|
|
match HymnalService::get_hymn_by_number(pool, "sda-1941", hymn_number).await {
|
|
Ok(Some(hymn)) => Some(hymn.title),
|
|
_ => None,
|
|
}
|
|
}
|
|
};
|
|
|
|
if let Some(title) = hymn_title {
|
|
let full_match = capture.get(0).unwrap();
|
|
matches_to_replace.push((
|
|
full_match.start(),
|
|
full_match.end(),
|
|
format!("{} - {}", full_match.as_str(), title)
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply replacements in reverse order to maintain string indices
|
|
matches_to_replace.reverse();
|
|
for (start, end, replacement) in matches_to_replace {
|
|
result.replace_range(start..end, &replacement);
|
|
}
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
/// Extract hymn numbers from content without making database calls
|
|
fn extract_hymn_numbers_from_content(content: &str) -> Vec<i32> {
|
|
let hymn_patterns = vec![
|
|
Regex::new(r"#(\d{1,3})").unwrap(),
|
|
Regex::new(r"(?i)hymn\s+(\d{1,3})").unwrap(),
|
|
Regex::new(r"(?i)no\.\s*(\d{1,3})").unwrap(),
|
|
Regex::new(r"(?i)number\s+(\d{1,3})").unwrap(),
|
|
];
|
|
|
|
let mut hymn_numbers = Vec::new();
|
|
|
|
for pattern in hymn_patterns {
|
|
for capture in pattern.captures_iter(content) {
|
|
if let Some(number_match) = capture.get(1) {
|
|
if let Ok(hymn_number) = number_match.as_str().parse::<i32>() {
|
|
hymn_numbers.push(hymn_number);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
hymn_numbers
|
|
}
|
|
|
|
/// Process single bulletin using cached hymn data (no database calls for hymns)
|
|
async fn process_single_bulletin_with_cache(
|
|
pool: &sqlx::PgPool,
|
|
bulletin: &mut Bulletin,
|
|
hymn_cache: &std::collections::HashMap<(String, i32), crate::models::HymnWithHymnal>
|
|
) -> Result<()> {
|
|
// Process scripture reading to include full verse text (still needs DB for bible verses)
|
|
bulletin.scripture_reading = process_scripture_reading(pool, &bulletin.scripture_reading).await?;
|
|
|
|
// Process hymn references in worship content using cache
|
|
if let Some(ref worship_content) = bulletin.divine_worship {
|
|
bulletin.divine_worship = Some(process_hymn_references_with_cache(worship_content, hymn_cache));
|
|
}
|
|
|
|
// Process hymn references in sabbath school content using cache
|
|
if let Some(ref ss_content) = bulletin.sabbath_school {
|
|
bulletin.sabbath_school = Some(process_hymn_references_with_cache(ss_content, hymn_cache));
|
|
}
|
|
|
|
// Ensure sunset field compatibility
|
|
if bulletin.sunset.is_none() {
|
|
bulletin.sunset = Some("TBA".to_string());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Process hymn references using cached hymn data (no database calls)
|
|
fn process_hymn_references_with_cache(
|
|
content: &str,
|
|
hymn_cache: &std::collections::HashMap<(String, i32), crate::models::HymnWithHymnal>
|
|
) -> String {
|
|
let hymn_patterns = vec![
|
|
Regex::new(r"#(\d{1,3})").unwrap(),
|
|
Regex::new(r"(?i)hymn\s+(\d{1,3})").unwrap(),
|
|
Regex::new(r"(?i)no\.\s*(\d{1,3})").unwrap(),
|
|
Regex::new(r"(?i)number\s+(\d{1,3})").unwrap(),
|
|
];
|
|
|
|
let mut result = content.to_string();
|
|
|
|
// Process each pattern
|
|
for pattern in hymn_patterns {
|
|
let mut matches_to_replace = Vec::new();
|
|
|
|
// Find all matches for this pattern
|
|
for capture in pattern.captures_iter(&result) {
|
|
if let Some(number_match) = capture.get(1) {
|
|
if let Ok(hymn_number) = number_match.as_str().parse::<i32>() {
|
|
// Look for hymn in 1985 hymnal first, then 1941
|
|
let hymn_title = hymn_cache.get(&("sda-1985".to_string(), hymn_number))
|
|
.or_else(|| hymn_cache.get(&("sda-1941".to_string(), hymn_number)))
|
|
.map(|hymn| &hymn.title);
|
|
|
|
if let Some(title) = hymn_title {
|
|
let full_match = capture.get(0).unwrap();
|
|
matches_to_replace.push((
|
|
full_match.start(),
|
|
full_match.end(),
|
|
format!("{} - {}", full_match.as_str(), title)
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply replacements in reverse order to maintain string indices
|
|
matches_to_replace.reverse();
|
|
for (start, end, replacement) in matches_to_replace {
|
|
result.replace_range(start..end, &replacement);
|
|
}
|
|
}
|
|
|
|
result
|
|
} |