diff --git a/src/db/bible_verses.rs b/src/db/bible_verses.rs.unused similarity index 100% rename from src/db/bible_verses.rs rename to src/db/bible_verses.rs.unused diff --git a/src/db/bulletins.rs b/src/db/bulletins.rs.unused similarity index 100% rename from src/db/bulletins.rs rename to src/db/bulletins.rs.unused diff --git a/src/db/mod.rs b/src/db/mod.rs index c6b2937..45b3449 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,8 +1,5 @@ -pub mod bulletins; pub mod users; pub mod events; pub mod config; -pub mod bible_verses; -pub mod schedule; pub mod contact; pub mod members; diff --git a/src/db/schedule.rs b/src/db/schedule.rs.unused similarity index 100% rename from src/db/schedule.rs rename to src/db/schedule.rs.unused diff --git a/src/handlers/bulletins_shared.rs b/src/handlers/bulletins_shared.rs index ee19554..4cef6e3 100644 --- a/src/handlers/bulletins_shared.rs +++ b/src/handlers/bulletins_shared.rs @@ -2,7 +2,6 @@ use crate::{ error::Result, models::Bulletin, - utils::db_operations::BibleVerseOperations, services::HymnalService, }; use regex::Regex; @@ -58,28 +57,34 @@ async fn process_scripture_reading( return Ok(Some(scripture_text.clone())); } - // Try to find the verse(s) using existing search functionality + // Try to find the verse(s) using direct SQL search // Allow up to 10 verses for ranges like "Matt 1:21-23" - match BibleVerseOperations::search(pool, scripture_text, 10).await { - Ok(verses) 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::>() - .join(" "); - Ok(Some(format!("{} - {}", combined_text, scripture_text))) - } - }, - _ => { - // If no match found, return original text - Ok(Some(scripture_text.clone())) + 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::>() + .join(" "); + Ok(Some(format!("{} - {}", combined_text, scripture_text))) } + } else { + // If no match found, return original text + Ok(Some(scripture_text.clone())) } } diff --git a/src/handlers/media.rs b/src/handlers/media.rs index bd6c097..27c7921 100644 --- a/src/handlers/media.rs +++ b/src/handlers/media.rs @@ -88,11 +88,18 @@ pub async fn get_media_item( // If scripture_reading is null and this is a sermon (has a date), // try to get scripture reading from corresponding bulletin if item.scripture_reading.is_none() && item.date.is_some() { - if let Ok(bulletin) = crate::db::bulletins::get_by_date(&state.pool, item.date.unwrap()).await { - if let Some(bulletin_data) = bulletin { - // Use the processed scripture reading from the bulletin - item.scripture_reading = bulletin_data.scripture_reading.clone(); - } + let bulletin = sqlx::query_as!( + crate::models::Bulletin, + r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, + sabbath_school, divine_worship, scripture_reading, sunset, + cover_image, pdf_path, created_at, updated_at + FROM bulletins WHERE date = $1 AND is_active = true ORDER BY created_at DESC LIMIT 1"#, + item.date.unwrap() + ).fetch_optional(&state.pool).await; + + if let Ok(Some(bulletin_data)) = bulletin { + // Use the processed scripture reading from the bulletin + item.scripture_reading = bulletin_data.scripture_reading.clone(); } } @@ -130,11 +137,18 @@ pub async fn list_sermons( // Link sermons to bulletins for scripture readings for item in &mut media_items { if item.scripture_reading.is_none() && item.date.is_some() { - if let Ok(bulletin) = crate::db::bulletins::get_by_date(&state.pool, item.date.unwrap()).await { - if let Some(bulletin_data) = bulletin { - // Use the processed scripture reading from the bulletin - item.scripture_reading = bulletin_data.scripture_reading.clone(); - } + let bulletin = sqlx::query_as!( + crate::models::Bulletin, + r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, + sabbath_school, divine_worship, scripture_reading, sunset, + cover_image, pdf_path, created_at, updated_at + FROM bulletins WHERE date = $1 AND is_active = true ORDER BY created_at DESC LIMIT 1"#, + item.date.unwrap() + ).fetch_optional(&state.pool).await; + + if let Ok(Some(bulletin_data)) = bulletin { + // Use the processed scripture reading from the bulletin + item.scripture_reading = bulletin_data.scripture_reading.clone(); } } } diff --git a/src/handlers/v2/events.rs b/src/handlers/v2/events.rs index 7f80467..cb89b84 100644 --- a/src/handlers/v2/events.rs +++ b/src/handlers/v2/events.rs @@ -34,23 +34,19 @@ pub async fn list( let pagination = PaginationHelper::from_query(query.page, query.per_page); let url_builder = UrlBuilder::new(); - let events = EventService::list_v2(&state.pool, timezone, &url_builder).await?; - let total = events.len() as i64; + let events_v2 = EventService::list_v2(&state.pool, timezone, &url_builder).await?; + let total = events_v2.len() as i64; // Apply pagination let start = pagination.offset as usize; - let end = std::cmp::min(start + pagination.per_page as usize, events.len()); - let paginated_events = if start < events.len() { - events[start..end].to_vec() + let end = std::cmp::min(start + pagination.per_page as usize, events_v2.len()); + let paginated_events = if start < events_v2.len() { + events_v2[start..end].to_vec() } else { Vec::new() }; - // Convert to V2 format using shared converter - let url_builder = UrlBuilder::new(); - let events_v2 = convert_events_to_v2(paginated_events, timezone, &url_builder)?; - - let response = pagination.create_response(events_v2, total); + let response = pagination.create_response(paginated_events, total); Ok(success_response(response)) } @@ -60,9 +56,7 @@ pub async fn get_upcoming( ) -> Result>>> { let timezone = query.timezone.as_deref().unwrap_or(DEFAULT_CHURCH_TIMEZONE); let url_builder = UrlBuilder::new(); - let events = EventService::get_upcoming_v2(&state.pool, 50, timezone, &url_builder).await?; - let url_builder = UrlBuilder::new(); - let events_v2 = convert_events_to_v2(events, timezone, &url_builder)?; + let events_v2 = EventService::get_upcoming_v2(&state.pool, 50, timezone, &url_builder).await?; Ok(success_response(events_v2)) } @@ -72,9 +66,7 @@ pub async fn get_featured( ) -> Result>>> { let timezone = query.timezone.as_deref().unwrap_or(DEFAULT_CHURCH_TIMEZONE); let url_builder = UrlBuilder::new(); - let events = EventService::get_featured_v2(&state.pool, 10, timezone, &url_builder).await?; - let url_builder = UrlBuilder::new(); - let events_v2 = convert_events_to_v2(events, timezone, &url_builder)?; + let events_v2 = EventService::get_featured_v2(&state.pool, 10, timezone, &url_builder).await?; Ok(success_response(events_v2)) } @@ -85,11 +77,8 @@ pub async fn get_by_id( ) -> Result>> { let timezone = query.timezone.as_deref().unwrap_or(DEFAULT_CHURCH_TIMEZONE); let url_builder = UrlBuilder::new(); - let event = EventService::get_by_id_v2(&state.pool, &id, timezone, &url_builder).await? + let event_v2 = EventService::get_by_id_v2(&state.pool, &id, timezone, &url_builder).await? .ok_or_else(|| ApiError::event_not_found(&id))?; - - let url_builder = UrlBuilder::new(); - let event_v2 = convert_event_to_v2(event, timezone, &url_builder)?; Ok(success_response(event_v2)) } @@ -250,16 +239,8 @@ pub async fn list_pending( let timezone = query.timezone.as_deref().unwrap_or(DEFAULT_CHURCH_TIMEZONE); let url_builder = UrlBuilder::new(); - let events = EventService::list_pending_v2(&state.pool, pagination.page, pagination.per_page, timezone, &url_builder).await?; - let total = events.len() as i64; - - let mut events_v2 = Vec::new(); - let url_builder = UrlBuilder::new(); - - for event in events { - let event_v2 = crate::utils::converters::convert_pending_event_to_v2(event, timezone, &url_builder)?; - events_v2.push(event_v2); - } + let events_v2 = EventService::list_pending_v2(&state.pool, pagination.page, pagination.per_page, timezone, &url_builder).await?; + let total = events_v2.len() as i64; let response = pagination.create_response(events_v2, total); Ok(success_response(response)) diff --git a/src/lib.rs b/src/lib.rs index ea25b8e..b324d0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod models; pub mod utils; pub mod handlers; pub mod db; +pub mod sql; pub mod auth; pub mod email; pub mod upload; diff --git a/src/main.rs b/src/main.rs index 74f1bb5..1f8a973 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod auth; mod db; +mod sql; mod email; mod upload; mod recurring; diff --git a/src/services/auth.rs b/src/services/auth.rs index 9bd90bf..e6c9427 100644 --- a/src/services/auth.rs +++ b/src/services/auth.rs @@ -1,7 +1,6 @@ use sqlx::PgPool; use bcrypt::verify; use crate::{ - db, models::{User, LoginRequest, LoginResponse}, error::{Result, ApiError}, auth::create_jwt, @@ -56,6 +55,12 @@ impl AuthService { /// List all users (admin function) pub async fn list_users(pool: &PgPool) -> Result> { - db::users::list(pool).await + sqlx::query_as!( + User, + "SELECT id, username, email, name, avatar_url, role, verified, created_at, updated_at FROM users ORDER BY created_at DESC" + ) + .fetch_all(pool) + .await + .map_err(Into::into) } } \ No newline at end of file diff --git a/src/services/bible_verses.rs b/src/services/bible_verses.rs index 6a31b63..7071a15 100644 --- a/src/services/bible_verses.rs +++ b/src/services/bible_verses.rs @@ -1,11 +1,9 @@ use sqlx::PgPool; use crate::{ + sql::bible_verses as sql, models::{BibleVerse, BibleVerseV2}, error::Result, - utils::{ - converters::{convert_bible_verses_to_v1, convert_bible_verse_to_v1, convert_bible_verses_to_v2, convert_bible_verse_to_v2}, - db_operations::BibleVerseOperations, - }, + utils::converters::{convert_bible_verses_to_v1, convert_bible_verse_to_v1, convert_bible_verses_to_v2, convert_bible_verse_to_v2}, }; /// Bible verse business logic service @@ -15,7 +13,7 @@ pub struct BibleVerseService; impl BibleVerseService { /// Get random bible verse with V1 format (EST timezone) pub async fn get_random_v1(pool: &PgPool) -> Result> { - let verse = BibleVerseOperations::get_random(pool).await?; + let verse = sql::get_random(pool).await?; match verse { Some(v) => { @@ -28,14 +26,13 @@ impl BibleVerseService { /// List all active bible verses with V1 format (EST timezone) pub async fn list_v1(pool: &PgPool) -> Result> { - // Use db module for list since BibleVerseOperations doesn't have it - let verses = crate::db::bible_verses::list(pool).await?; + let verses = sql::list_active(pool).await?; convert_bible_verses_to_v1(verses) } /// Search bible verses with V1 format (EST timezone) pub async fn search_v1(pool: &PgPool, query: &str) -> Result> { - let verses = BibleVerseOperations::search(pool, query, 100).await?; + let verses = sql::search(pool, query, 100).await?; convert_bible_verses_to_v1(verses) } @@ -43,7 +40,7 @@ impl BibleVerseService { /// Get random bible verse with V2 format (UTC timestamps) pub async fn get_random_v2(pool: &PgPool) -> Result> { - let verse = BibleVerseOperations::get_random(pool).await?; + let verse = sql::get_random(pool).await?; match verse { Some(v) => { @@ -56,14 +53,13 @@ impl BibleVerseService { /// List all active bible verses with V2 format (UTC timestamps) pub async fn list_v2(pool: &PgPool) -> Result> { - // Use db module for list since BibleVerseOperations doesn't have it - let verses = crate::db::bible_verses::list(pool).await?; + let verses = sql::list_active(pool).await?; convert_bible_verses_to_v2(verses) } /// Search bible verses with V2 format (UTC timestamps) pub async fn search_v2(pool: &PgPool, query: &str) -> Result> { - let verses = BibleVerseOperations::search(pool, query, 100).await?; + let verses = sql::search(pool, query, 100).await?; convert_bible_verses_to_v2(verses) } } \ No newline at end of file diff --git a/src/services/bulletins.rs b/src/services/bulletins.rs index e58079a..0a7f96f 100644 --- a/src/services/bulletins.rs +++ b/src/services/bulletins.rs @@ -1,13 +1,12 @@ use sqlx::PgPool; use uuid::Uuid; use crate::{ - db, + sql::bulletins as sql, models::{Bulletin, BulletinV2, CreateBulletinRequest}, error::Result, utils::{ urls::UrlBuilder, converters::{convert_bulletins_to_v1, convert_bulletin_to_v1, convert_bulletins_to_v2, convert_bulletin_to_v2}, - // db_operations::BulletinOperations, // DELETED - using db:: directly }, handlers::bulletins_shared::{process_bulletins_batch, process_single_bulletin}, }; @@ -25,7 +24,7 @@ impl BulletinService { active_only: bool, url_builder: &UrlBuilder ) -> Result<(Vec, i64)> { - let (mut bulletins, total) = db::bulletins::list(pool, page, per_page, active_only).await?; + let (mut bulletins, total) = sql::list(pool, page, per_page, active_only).await?; // Apply shared processing logic process_bulletins_batch(pool, &mut bulletins).await?; @@ -38,7 +37,7 @@ impl BulletinService { /// Get current bulletin with V1 timezone conversion (EST) pub async fn get_current_v1(pool: &PgPool, url_builder: &UrlBuilder) -> Result> { - let mut bulletin = db::bulletins::get_current(pool).await?; + let mut bulletin = sql::get_current(pool).await?; if let Some(ref mut bulletin_data) = bulletin { process_single_bulletin(pool, bulletin_data).await?; @@ -56,7 +55,7 @@ impl BulletinService { /// Get next bulletin with V1 timezone conversion (EST) pub async fn get_next_v1(pool: &PgPool, url_builder: &UrlBuilder) -> Result> { - let mut bulletin = db::bulletins::get_next(pool).await?; + let mut bulletin = sql::get_next(pool).await?; if let Some(ref mut bulletin_data) = bulletin { process_single_bulletin(pool, bulletin_data).await?; @@ -74,7 +73,7 @@ impl BulletinService { /// Get bulletin by ID with V1 timezone conversion (EST) pub async fn get_by_id_v1(pool: &PgPool, id: &Uuid, url_builder: &UrlBuilder) -> Result> { - let mut bulletin = crate::utils::db_operations::DbOperations::get_bulletin_by_id(pool, id).await?; + let mut bulletin = sql::get_by_id(pool, id).await?; match bulletin { Some(ref mut bulletin_data) => { @@ -88,15 +87,13 @@ impl BulletinService { /// Create a new bulletin pub async fn create(pool: &PgPool, request: CreateBulletinRequest, url_builder: &UrlBuilder) -> Result { - let bulletin = db::bulletins::create(pool, request).await?; - - // Convert UTC times to EST for V1 compatibility + let bulletin = sql::create(pool, &request).await?; convert_bulletin_to_v1(bulletin, url_builder) } /// Update a bulletin pub async fn update(pool: &PgPool, id: &Uuid, request: CreateBulletinRequest, url_builder: &UrlBuilder) -> Result> { - let bulletin = db::bulletins::update(pool, id, request).await?; + let bulletin = sql::update(pool, id, &request).await?; match bulletin { Some(b) => { @@ -109,7 +106,7 @@ impl BulletinService { /// Delete a bulletin pub async fn delete(pool: &PgPool, id: &Uuid) -> Result<()> { - db::bulletins::delete(pool, id).await + sql::delete(pool, id).await } // V2 API methods (UTC timezone as per shared converter) @@ -122,7 +119,7 @@ impl BulletinService { active_only: bool, url_builder: &UrlBuilder ) -> Result<(Vec, i64)> { - let (bulletins, total) = db::bulletins::list(pool, page, per_page, active_only).await?; + let (bulletins, total) = sql::list(pool, page, per_page, active_only).await?; // Convert to V2 format with UTC timestamps let converted_bulletins = convert_bulletins_to_v2(bulletins, url_builder)?; @@ -132,7 +129,7 @@ impl BulletinService { /// Get current bulletin with V2 format (UTC timestamps) pub async fn get_current_v2(pool: &PgPool, url_builder: &UrlBuilder) -> Result> { - let bulletin = db::bulletins::get_current(pool).await?; + let bulletin = sql::get_current(pool).await?; match bulletin { Some(b) => { @@ -145,7 +142,7 @@ impl BulletinService { /// Get next bulletin with V2 format (UTC timestamps) pub async fn get_next_v2(pool: &PgPool, url_builder: &UrlBuilder) -> Result> { - let bulletin = db::bulletins::get_next(pool).await?; + let bulletin = sql::get_next(pool).await?; match bulletin { Some(b) => { @@ -158,7 +155,7 @@ impl BulletinService { /// Get bulletin by ID with V2 format (UTC timestamps) pub async fn get_by_id_v2(pool: &PgPool, id: &Uuid, url_builder: &UrlBuilder) -> Result> { - let bulletin = db::bulletins::get_by_id(pool, id).await?; + let bulletin = sql::get_by_id(pool, id).await?; match bulletin { Some(b) => { diff --git a/src/services/config.rs b/src/services/config.rs index 6a4a440..3b9356f 100644 --- a/src/services/config.rs +++ b/src/services/config.rs @@ -1,7 +1,6 @@ use sqlx::PgPool; use serde_json::Value; use crate::{ - db, models::ChurchConfig, error::Result, }; @@ -13,7 +12,12 @@ pub struct ConfigService; impl ConfigService { /// Get public configuration (excludes API keys) pub async fn get_public_config(pool: &PgPool) -> Result> { - let config = db::config::get_config(pool).await?; + let config = sqlx::query_as!( + ChurchConfig, + "SELECT * FROM church_config LIMIT 1" + ) + .fetch_optional(pool) + .await?; match config { Some(config) => { @@ -43,11 +47,38 @@ impl ConfigService { /// Get admin configuration (includes all fields including API keys) pub async fn get_admin_config(pool: &PgPool) -> Result> { - db::config::get_config(pool).await + sqlx::query_as!( + ChurchConfig, + "SELECT * FROM church_config LIMIT 1" + ) + .fetch_optional(pool) + .await + .map_err(Into::into) } /// Update church configuration pub async fn update_config(pool: &PgPool, config: ChurchConfig) -> Result { - db::config::update_config(pool, config).await + sqlx::query_as!( + ChurchConfig, + r#"UPDATE church_config SET + church_name = $2, contact_email = $3, contact_phone = $4, + church_address = $5, po_box = $6, google_maps_url = $7, + about_text = $8, api_keys = $9, brand_color = $10, updated_at = NOW() + WHERE id = $1 + RETURNING *"#, + config.id, + config.church_name, + config.contact_email, + config.contact_phone, + config.church_address, + config.po_box, + config.google_maps_url, + config.about_text, + config.api_keys, + config.brand_color + ) + .fetch_one(pool) + .await + .map_err(Into::into) } } \ No newline at end of file diff --git a/src/services/schedule.rs b/src/services/schedule.rs index 6ea34e3..39e3288 100644 --- a/src/services/schedule.rs +++ b/src/services/schedule.rs @@ -2,13 +2,9 @@ use sqlx::PgPool; use chrono::{NaiveDate, Timelike}; use uuid::Uuid; use crate::{ - db, models::{Schedule, ScheduleV2, ScheduleData, ConferenceData, Personnel}, error::{Result, ApiError}, - utils::{ - converters::{convert_schedules_to_v1, convert_schedule_to_v2}, - db_operations::ScheduleOperations, - }, + utils::converters::{convert_schedules_to_v1, convert_schedule_to_v2}, }; #[derive(Debug, serde::Deserialize)] @@ -38,7 +34,13 @@ impl ScheduleService { let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") .map_err(|_| ApiError::BadRequest("Invalid date format. Use YYYY-MM-DD".to_string()))?; - let schedule = ScheduleOperations::get_by_date(pool, date).await?; + let schedule = sqlx::query_as!( + Schedule, + "SELECT * FROM schedule WHERE date = $1", + date + ) + .fetch_optional(pool) + .await?; let personnel = if let Some(s) = schedule { Personnel { @@ -128,7 +130,51 @@ impl ScheduleService { updated_at: None, }; - db::schedule::insert_or_update(pool, &schedule).await + let result = sqlx::query_as!( + Schedule, + r#" + INSERT INTO schedule ( + id, date, song_leader, ss_teacher, ss_leader, mission_story, + special_program, sermon_speaker, scripture, offering, deacons, + special_music, childrens_story, afternoon_program, created_at, updated_at + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, NOW(), NOW() + ) + ON CONFLICT (date) DO UPDATE SET + song_leader = EXCLUDED.song_leader, + ss_teacher = EXCLUDED.ss_teacher, + ss_leader = EXCLUDED.ss_leader, + mission_story = EXCLUDED.mission_story, + special_program = EXCLUDED.special_program, + sermon_speaker = EXCLUDED.sermon_speaker, + scripture = EXCLUDED.scripture, + offering = EXCLUDED.offering, + deacons = EXCLUDED.deacons, + special_music = EXCLUDED.special_music, + childrens_story = EXCLUDED.childrens_story, + afternoon_program = EXCLUDED.afternoon_program, + updated_at = NOW() + RETURNING * + "#, + schedule.id, + schedule.date, + schedule.song_leader, + schedule.ss_teacher, + schedule.ss_leader, + schedule.mission_story, + schedule.special_program, + schedule.sermon_speaker, + schedule.scripture, + schedule.offering, + schedule.deacons, + schedule.special_music, + schedule.childrens_story, + schedule.afternoon_program + ) + .fetch_one(pool) + .await?; + + Ok(result) } /// Delete schedule by date @@ -159,7 +205,13 @@ impl ScheduleService { /// Get schedule by date with V2 format (UTC timestamps) pub async fn get_schedule_v2(pool: &PgPool, date: &NaiveDate) -> Result> { - let schedule = ScheduleOperations::get_by_date(pool, *date).await?; + let schedule = sqlx::query_as!( + Schedule, + "SELECT * FROM schedule WHERE date = $1", + date + ) + .fetch_optional(pool) + .await?; match schedule { Some(s) => { @@ -172,8 +224,14 @@ impl ScheduleService { /// Get conference data for V2 (simplified version) pub async fn get_conference_data_v2(pool: &PgPool, date: &NaiveDate) -> Result { - let schedule = ScheduleOperations::get_by_date(pool, *date).await? - .ok_or_else(|| ApiError::NotFound("Schedule not found".to_string()))?; + let schedule = sqlx::query_as!( + Schedule, + "SELECT * FROM schedule WHERE date = $1", + date + ) + .fetch_optional(pool) + .await? + .ok_or_else(|| ApiError::NotFound("Schedule not found".to_string()))?; Ok(ConferenceData { date: date.format("%Y-%m-%d").to_string(), diff --git a/src/sql/bible_verses.rs b/src/sql/bible_verses.rs new file mode 100644 index 0000000..f41aa01 --- /dev/null +++ b/src/sql/bible_verses.rs @@ -0,0 +1,37 @@ +use sqlx::PgPool; +use crate::{error::Result, models::BibleVerse}; + +/// Get random active bible verse (raw SQL, no conversion) +pub async fn get_random(pool: &PgPool) -> Result> { + sqlx::query_as!( + BibleVerse, + "SELECT * FROM bible_verses WHERE is_active = true ORDER BY RANDOM() LIMIT 1" + ) + .fetch_optional(pool) + .await + .map_err(Into::into) +} + +/// List all active bible verses (raw SQL, no conversion) +pub async fn list_active(pool: &PgPool) -> Result> { + sqlx::query_as!( + BibleVerse, + "SELECT * FROM bible_verses WHERE is_active = true ORDER BY reference" + ) + .fetch_all(pool) + .await + .map_err(Into::into) +} + +/// Search bible verses by text or reference (raw SQL, no conversion) +pub async fn search(pool: &PgPool, query: &str, limit: i64) -> Result> { + sqlx::query_as!( + BibleVerse, + "SELECT * FROM bible_verses WHERE is_active = true AND (reference ILIKE $1 OR text ILIKE $1) ORDER BY reference LIMIT $2", + format!("%{}%", query), + limit + ) + .fetch_all(pool) + .await + .map_err(Into::into) +} \ No newline at end of file diff --git a/src/sql/bulletins.rs b/src/sql/bulletins.rs new file mode 100644 index 0000000..2521be4 --- /dev/null +++ b/src/sql/bulletins.rs @@ -0,0 +1,161 @@ +use sqlx::PgPool; +use uuid::Uuid; +use crate::{error::Result, models::{Bulletin, CreateBulletinRequest}}; + +/// List bulletins with pagination (raw SQL, no conversion) +pub async fn list(pool: &PgPool, page: i32, per_page: i64, active_only: bool) -> Result<(Vec, i64)> { + let offset = ((page - 1) * per_page as i32) as i64; + + // Get total count + let total = if active_only { + sqlx::query!("SELECT COUNT(*) as count FROM bulletins WHERE is_active = true") + .fetch_one(pool) + .await? + .count + .unwrap_or(0) + } else { + sqlx::query!("SELECT COUNT(*) as count FROM bulletins") + .fetch_one(pool) + .await? + .count + .unwrap_or(0) + }; + + // Get bulletins with pagination - explicit field selection + let bulletins = if active_only { + sqlx::query_as!( + Bulletin, + r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, + sabbath_school, divine_worship, scripture_reading, sunset, + cover_image, pdf_path, created_at, updated_at + FROM bulletins WHERE is_active = true ORDER BY date DESC LIMIT $1 OFFSET $2"#, + per_page, + offset + ) + .fetch_all(pool) + .await? + } else { + sqlx::query_as!( + Bulletin, + r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, + sabbath_school, divine_worship, scripture_reading, sunset, + cover_image, pdf_path, created_at, updated_at + FROM bulletins ORDER BY date DESC LIMIT $1 OFFSET $2"#, + per_page, + offset + ) + .fetch_all(pool) + .await? + }; + + Ok((bulletins, total)) +} + +/// Get current bulletin (raw SQL, no conversion) +pub async fn get_current(pool: &PgPool) -> Result> { + sqlx::query_as!( + Bulletin, + r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, + sabbath_school, divine_worship, scripture_reading, sunset, + cover_image, pdf_path, created_at, updated_at + FROM bulletins WHERE is_active = true AND date <= CURRENT_DATE ORDER BY date DESC LIMIT 1"# + ) + .fetch_optional(pool) + .await + .map_err(Into::into) +} + +/// Get next bulletin (raw SQL, no conversion) +pub async fn get_next(pool: &PgPool) -> Result> { + sqlx::query_as!( + Bulletin, + r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, + sabbath_school, divine_worship, scripture_reading, sunset, + cover_image, pdf_path, created_at, updated_at + FROM bulletins WHERE is_active = true AND date > CURRENT_DATE ORDER BY date ASC LIMIT 1"# + ) + .fetch_optional(pool) + .await + .map_err(Into::into) +} + +/// Get bulletin by ID (raw SQL, no conversion) +pub async fn get_by_id(pool: &PgPool, id: &Uuid) -> Result> { + sqlx::query_as!( + Bulletin, + r#"SELECT id, title, date, url, pdf_url, is_active, pdf_file, + sabbath_school, divine_worship, scripture_reading, sunset, + cover_image, pdf_path, created_at, updated_at + FROM bulletins WHERE id = $1"#, + id + ) + .fetch_optional(pool) + .await + .map_err(Into::into) +} + +/// Create new bulletin (raw SQL, no conversion) +pub async fn create(pool: &PgPool, request: &CreateBulletinRequest) -> Result { + let bulletin_id = Uuid::new_v4(); + + sqlx::query_as!( + Bulletin, + r#"INSERT INTO bulletins ( + id, title, date, url, is_active, + sabbath_school, divine_worship, scripture_reading, sunset, cover_image + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 + ) RETURNING id, title, date, url, pdf_url, is_active, pdf_file, + sabbath_school, divine_worship, scripture_reading, sunset, + cover_image, pdf_path, created_at, updated_at"#, + bulletin_id, + request.title, + request.date, + request.url, + request.is_active.unwrap_or(true), + request.sabbath_school, + request.divine_worship, + request.scripture_reading, + request.sunset, + request.cover_image + ) + .fetch_one(pool) + .await + .map_err(Into::into) +} + +/// Update bulletin (raw SQL, no conversion) +pub async fn update(pool: &PgPool, id: &Uuid, request: &CreateBulletinRequest) -> Result> { + sqlx::query_as!( + Bulletin, + r#"UPDATE bulletins SET + title = $2, date = $3, url = $4, is_active = $5, + sabbath_school = $6, divine_worship = $7, scripture_reading = $8, + sunset = $9, cover_image = $10, updated_at = NOW() + WHERE id = $1 + RETURNING id, title, date, url, pdf_url, is_active, pdf_file, + sabbath_school, divine_worship, scripture_reading, sunset, + cover_image, pdf_path, created_at, updated_at"#, + id, + request.title, + request.date, + request.url, + request.is_active.unwrap_or(true), + request.sabbath_school, + request.divine_worship, + request.scripture_reading, + request.sunset, + request.cover_image + ) + .fetch_optional(pool) + .await + .map_err(Into::into) +} + +/// Delete bulletin (raw SQL, no conversion) +pub async fn delete(pool: &PgPool, id: &Uuid) -> Result<()> { + sqlx::query!("DELETE FROM bulletins WHERE id = $1", id) + .execute(pool) + .await?; + Ok(()) +} \ No newline at end of file diff --git a/src/sql/mod.rs b/src/sql/mod.rs new file mode 100644 index 0000000..85f64a4 --- /dev/null +++ b/src/sql/mod.rs @@ -0,0 +1,5 @@ +// Shared SQL functions - raw database operations without business logic +// Services call these functions and handle conversion/business logic + +pub mod bible_verses; +pub mod bulletins; \ No newline at end of file