Apply DRY/KISS: Replace db wrapper layer with shared SQL functions
- Create sql:: module with raw SQL queries (no business logic) - Services now call sql:: functions + handle conversion logic only - Eliminate SQL duplication across V1/V2 service methods - Remove redundant db:: wrapper functions for bulletins, bible_verses, schedule - Clean Handler → Service → Shared SQL → DB architecture - Maintain API compatibility, zero downtime
This commit is contained in:
parent
ef7e077ae2
commit
6bee94c311
|
@ -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;
|
||||
|
|
|
@ -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::<Vec<&str>>()
|
||||
.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::<Vec<&str>>()
|
||||
.join(" ");
|
||||
Ok(Some(format!("{} - {}", combined_text, scripture_text)))
|
||||
}
|
||||
} else {
|
||||
// If no match found, return original text
|
||||
Ok(Some(scripture_text.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Json<ApiResponse<Vec<EventV2>>>> {
|
||||
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<Json<ApiResponse<Vec<EventV2>>>> {
|
||||
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<Json<ApiResponse<EventV2>>> {
|
||||
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))
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -17,6 +17,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
|||
|
||||
mod auth;
|
||||
mod db;
|
||||
mod sql;
|
||||
mod email;
|
||||
mod upload;
|
||||
mod recurring;
|
||||
|
|
|
@ -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<Vec<User>> {
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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<Option<BibleVerse>> {
|
||||
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<Vec<BibleVerse>> {
|
||||
// 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<Vec<BibleVerse>> {
|
||||
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<Option<BibleVerseV2>> {
|
||||
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<Vec<BibleVerseV2>> {
|
||||
// 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<Vec<BibleVerseV2>> {
|
||||
let verses = BibleVerseOperations::search(pool, query, 100).await?;
|
||||
let verses = sql::search(pool, query, 100).await?;
|
||||
convert_bible_verses_to_v2(verses)
|
||||
}
|
||||
}
|
|
@ -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<Bulletin>, 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<Option<Bulletin>> {
|
||||
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<Option<Bulletin>> {
|
||||
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<Option<Bulletin>> {
|
||||
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<Bulletin> {
|
||||
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<Option<Bulletin>> {
|
||||
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<BulletinV2>, 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<Option<BulletinV2>> {
|
||||
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<Option<BulletinV2>> {
|
||||
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<Option<BulletinV2>> {
|
||||
let bulletin = db::bulletins::get_by_id(pool, id).await?;
|
||||
let bulletin = sql::get_by_id(pool, id).await?;
|
||||
|
||||
match bulletin {
|
||||
Some(b) => {
|
||||
|
|
|
@ -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<Option<Value>> {
|
||||
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<Option<ChurchConfig>> {
|
||||
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<ChurchConfig> {
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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<Option<ScheduleV2>> {
|
||||
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<ConferenceData> {
|
||||
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(),
|
||||
|
|
37
src/sql/bible_verses.rs
Normal file
37
src/sql/bible_verses.rs
Normal file
|
@ -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<Option<BibleVerse>> {
|
||||
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<Vec<BibleVerse>> {
|
||||
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<Vec<BibleVerse>> {
|
||||
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)
|
||||
}
|
161
src/sql/bulletins.rs
Normal file
161
src/sql/bulletins.rs
Normal file
|
@ -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<Bulletin>, 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<Option<Bulletin>> {
|
||||
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<Option<Bulletin>> {
|
||||
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<Option<Bulletin>> {
|
||||
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<Bulletin> {
|
||||
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<Option<Bulletin>> {
|
||||
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(())
|
||||
}
|
5
src/sql/mod.rs
Normal file
5
src/sql/mod.rs
Normal file
|
@ -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;
|
Loading…
Reference in a new issue