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 users;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod bible_verses;
|
|
||||||
pub mod schedule;
|
|
||||||
pub mod contact;
|
pub mod contact;
|
||||||
pub mod members;
|
pub mod members;
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Result,
|
error::Result,
|
||||||
models::Bulletin,
|
models::Bulletin,
|
||||||
utils::db_operations::BibleVerseOperations,
|
|
||||||
services::HymnalService,
|
services::HymnalService,
|
||||||
};
|
};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
@ -58,10 +57,18 @@ async fn process_scripture_reading(
|
||||||
return Ok(Some(scripture_text.clone()));
|
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"
|
// Allow up to 10 verses for ranges like "Matt 1:21-23"
|
||||||
match BibleVerseOperations::search(pool, scripture_text, 10).await {
|
let verses = sqlx::query_as!(
|
||||||
Ok(verses) if !verses.is_empty() => {
|
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 {
|
if verses.len() == 1 {
|
||||||
// Single verse - format as before
|
// Single verse - format as before
|
||||||
let verse = &verses[0];
|
let verse = &verses[0];
|
||||||
|
@ -75,13 +82,11 @@ async fn process_scripture_reading(
|
||||||
.join(" ");
|
.join(" ");
|
||||||
Ok(Some(format!("{} - {}", combined_text, scripture_text)))
|
Ok(Some(format!("{} - {}", combined_text, scripture_text)))
|
||||||
}
|
}
|
||||||
},
|
} else {
|
||||||
_ => {
|
|
||||||
// If no match found, return original text
|
// If no match found, return original text
|
||||||
Ok(Some(scripture_text.clone()))
|
Ok(Some(scripture_text.clone()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Process hymn references in bulletin content to include titles
|
/// Process hymn references in bulletin content to include titles
|
||||||
/// Looks for patterns like #319, Hymn 319, No. 319 and adds hymn titles
|
/// Looks for patterns like #319, Hymn 319, No. 319 and adds hymn titles
|
||||||
|
|
|
@ -88,13 +88,20 @@ pub async fn get_media_item(
|
||||||
// If scripture_reading is null and this is a sermon (has a date),
|
// If scripture_reading is null and this is a sermon (has a date),
|
||||||
// try to get scripture reading from corresponding bulletin
|
// try to get scripture reading from corresponding bulletin
|
||||||
if item.scripture_reading.is_none() && item.date.is_some() {
|
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 {
|
let bulletin = sqlx::query_as!(
|
||||||
if let Some(bulletin_data) = bulletin {
|
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
|
// Use the processed scripture reading from the bulletin
|
||||||
item.scripture_reading = bulletin_data.scripture_reading.clone();
|
item.scripture_reading = bulletin_data.scripture_reading.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let base_url = get_base_url(&headers);
|
let base_url = get_base_url(&headers);
|
||||||
Ok(success_response(item.to_response(&base_url)))
|
Ok(success_response(item.to_response(&base_url)))
|
||||||
|
@ -130,14 +137,21 @@ pub async fn list_sermons(
|
||||||
// Link sermons to bulletins for scripture readings
|
// Link sermons to bulletins for scripture readings
|
||||||
for item in &mut media_items {
|
for item in &mut media_items {
|
||||||
if item.scripture_reading.is_none() && item.date.is_some() {
|
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 {
|
let bulletin = sqlx::query_as!(
|
||||||
if let Some(bulletin_data) = bulletin {
|
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
|
// Use the processed scripture reading from the bulletin
|
||||||
item.scripture_reading = bulletin_data.scripture_reading.clone();
|
item.scripture_reading = bulletin_data.scripture_reading.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let base_url = get_base_url(&headers);
|
let base_url = get_base_url(&headers);
|
||||||
let responses: Vec<MediaItemResponse> = media_items
|
let responses: Vec<MediaItemResponse> = media_items
|
||||||
|
|
|
@ -34,23 +34,19 @@ pub async fn list(
|
||||||
let pagination = PaginationHelper::from_query(query.page, query.per_page);
|
let pagination = PaginationHelper::from_query(query.page, query.per_page);
|
||||||
|
|
||||||
let url_builder = UrlBuilder::new();
|
let url_builder = UrlBuilder::new();
|
||||||
let events = EventService::list_v2(&state.pool, timezone, &url_builder).await?;
|
let events_v2 = EventService::list_v2(&state.pool, timezone, &url_builder).await?;
|
||||||
let total = events.len() as i64;
|
let total = events_v2.len() as i64;
|
||||||
|
|
||||||
// Apply pagination
|
// Apply pagination
|
||||||
let start = pagination.offset as usize;
|
let start = pagination.offset as usize;
|
||||||
let end = std::cmp::min(start + pagination.per_page as usize, events.len());
|
let end = std::cmp::min(start + pagination.per_page as usize, events_v2.len());
|
||||||
let paginated_events = if start < events.len() {
|
let paginated_events = if start < events_v2.len() {
|
||||||
events[start..end].to_vec()
|
events_v2[start..end].to_vec()
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert to V2 format using shared converter
|
let response = pagination.create_response(paginated_events, total);
|
||||||
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);
|
|
||||||
Ok(success_response(response))
|
Ok(success_response(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,9 +56,7 @@ pub async fn get_upcoming(
|
||||||
) -> Result<Json<ApiResponse<Vec<EventV2>>>> {
|
) -> Result<Json<ApiResponse<Vec<EventV2>>>> {
|
||||||
let timezone = query.timezone.as_deref().unwrap_or(DEFAULT_CHURCH_TIMEZONE);
|
let timezone = query.timezone.as_deref().unwrap_or(DEFAULT_CHURCH_TIMEZONE);
|
||||||
let url_builder = UrlBuilder::new();
|
let url_builder = UrlBuilder::new();
|
||||||
let events = EventService::get_upcoming_v2(&state.pool, 50, timezone, &url_builder).await?;
|
let events_v2 = 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)?;
|
|
||||||
Ok(success_response(events_v2))
|
Ok(success_response(events_v2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,9 +66,7 @@ pub async fn get_featured(
|
||||||
) -> Result<Json<ApiResponse<Vec<EventV2>>>> {
|
) -> Result<Json<ApiResponse<Vec<EventV2>>>> {
|
||||||
let timezone = query.timezone.as_deref().unwrap_or(DEFAULT_CHURCH_TIMEZONE);
|
let timezone = query.timezone.as_deref().unwrap_or(DEFAULT_CHURCH_TIMEZONE);
|
||||||
let url_builder = UrlBuilder::new();
|
let url_builder = UrlBuilder::new();
|
||||||
let events = EventService::get_featured_v2(&state.pool, 10, timezone, &url_builder).await?;
|
let events_v2 = 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)?;
|
|
||||||
Ok(success_response(events_v2))
|
Ok(success_response(events_v2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,11 +77,8 @@ pub async fn get_by_id(
|
||||||
) -> Result<Json<ApiResponse<EventV2>>> {
|
) -> Result<Json<ApiResponse<EventV2>>> {
|
||||||
let timezone = query.timezone.as_deref().unwrap_or(DEFAULT_CHURCH_TIMEZONE);
|
let timezone = query.timezone.as_deref().unwrap_or(DEFAULT_CHURCH_TIMEZONE);
|
||||||
let url_builder = UrlBuilder::new();
|
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))?;
|
.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))
|
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 timezone = query.timezone.as_deref().unwrap_or(DEFAULT_CHURCH_TIMEZONE);
|
||||||
|
|
||||||
let url_builder = UrlBuilder::new();
|
let url_builder = UrlBuilder::new();
|
||||||
let events = EventService::list_pending_v2(&state.pool, pagination.page, pagination.per_page, timezone, &url_builder).await?;
|
let events_v2 = EventService::list_pending_v2(&state.pool, pagination.page, pagination.per_page, timezone, &url_builder).await?;
|
||||||
let total = events.len() as i64;
|
let total = events_v2.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 response = pagination.create_response(events_v2, total);
|
let response = pagination.create_response(events_v2, total);
|
||||||
Ok(success_response(response))
|
Ok(success_response(response))
|
||||||
|
|
|
@ -4,6 +4,7 @@ pub mod models;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
pub mod sql;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod email;
|
pub mod email;
|
||||||
pub mod upload;
|
pub mod upload;
|
||||||
|
|
|
@ -17,6 +17,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
mod db;
|
mod db;
|
||||||
|
mod sql;
|
||||||
mod email;
|
mod email;
|
||||||
mod upload;
|
mod upload;
|
||||||
mod recurring;
|
mod recurring;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use bcrypt::verify;
|
use bcrypt::verify;
|
||||||
use crate::{
|
use crate::{
|
||||||
db,
|
|
||||||
models::{User, LoginRequest, LoginResponse},
|
models::{User, LoginRequest, LoginResponse},
|
||||||
error::{Result, ApiError},
|
error::{Result, ApiError},
|
||||||
auth::create_jwt,
|
auth::create_jwt,
|
||||||
|
@ -56,6 +55,12 @@ impl AuthService {
|
||||||
|
|
||||||
/// List all users (admin function)
|
/// List all users (admin function)
|
||||||
pub async fn list_users(pool: &PgPool) -> Result<Vec<User>> {
|
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 sqlx::PgPool;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
sql::bible_verses as sql,
|
||||||
models::{BibleVerse, BibleVerseV2},
|
models::{BibleVerse, BibleVerseV2},
|
||||||
error::Result,
|
error::Result,
|
||||||
utils::{
|
utils::converters::{convert_bible_verses_to_v1, convert_bible_verse_to_v1, convert_bible_verses_to_v2, convert_bible_verse_to_v2},
|
||||||
converters::{convert_bible_verses_to_v1, convert_bible_verse_to_v1, convert_bible_verses_to_v2, convert_bible_verse_to_v2},
|
|
||||||
db_operations::BibleVerseOperations,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Bible verse business logic service
|
/// Bible verse business logic service
|
||||||
|
@ -15,7 +13,7 @@ pub struct BibleVerseService;
|
||||||
impl BibleVerseService {
|
impl BibleVerseService {
|
||||||
/// Get random bible verse with V1 format (EST timezone)
|
/// Get random bible verse with V1 format (EST timezone)
|
||||||
pub async fn get_random_v1(pool: &PgPool) -> Result<Option<BibleVerse>> {
|
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 {
|
match verse {
|
||||||
Some(v) => {
|
Some(v) => {
|
||||||
|
@ -28,14 +26,13 @@ impl BibleVerseService {
|
||||||
|
|
||||||
/// List all active bible verses with V1 format (EST timezone)
|
/// List all active bible verses with V1 format (EST timezone)
|
||||||
pub async fn list_v1(pool: &PgPool) -> Result<Vec<BibleVerse>> {
|
pub async fn list_v1(pool: &PgPool) -> Result<Vec<BibleVerse>> {
|
||||||
// Use db module for list since BibleVerseOperations doesn't have it
|
let verses = sql::list_active(pool).await?;
|
||||||
let verses = crate::db::bible_verses::list(pool).await?;
|
|
||||||
convert_bible_verses_to_v1(verses)
|
convert_bible_verses_to_v1(verses)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search bible verses with V1 format (EST timezone)
|
/// Search bible verses with V1 format (EST timezone)
|
||||||
pub async fn search_v1(pool: &PgPool, query: &str) -> Result<Vec<BibleVerse>> {
|
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)
|
convert_bible_verses_to_v1(verses)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +40,7 @@ impl BibleVerseService {
|
||||||
|
|
||||||
/// Get random bible verse with V2 format (UTC timestamps)
|
/// Get random bible verse with V2 format (UTC timestamps)
|
||||||
pub async fn get_random_v2(pool: &PgPool) -> Result<Option<BibleVerseV2>> {
|
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 {
|
match verse {
|
||||||
Some(v) => {
|
Some(v) => {
|
||||||
|
@ -56,14 +53,13 @@ impl BibleVerseService {
|
||||||
|
|
||||||
/// List all active bible verses with V2 format (UTC timestamps)
|
/// List all active bible verses with V2 format (UTC timestamps)
|
||||||
pub async fn list_v2(pool: &PgPool) -> Result<Vec<BibleVerseV2>> {
|
pub async fn list_v2(pool: &PgPool) -> Result<Vec<BibleVerseV2>> {
|
||||||
// Use db module for list since BibleVerseOperations doesn't have it
|
let verses = sql::list_active(pool).await?;
|
||||||
let verses = crate::db::bible_verses::list(pool).await?;
|
|
||||||
convert_bible_verses_to_v2(verses)
|
convert_bible_verses_to_v2(verses)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search bible verses with V2 format (UTC timestamps)
|
/// Search bible verses with V2 format (UTC timestamps)
|
||||||
pub async fn search_v2(pool: &PgPool, query: &str) -> Result<Vec<BibleVerseV2>> {
|
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)
|
convert_bible_verses_to_v2(verses)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,12 @@
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use crate::{
|
use crate::{
|
||||||
db,
|
sql::bulletins as sql,
|
||||||
models::{Bulletin, BulletinV2, CreateBulletinRequest},
|
models::{Bulletin, BulletinV2, CreateBulletinRequest},
|
||||||
error::Result,
|
error::Result,
|
||||||
utils::{
|
utils::{
|
||||||
urls::UrlBuilder,
|
urls::UrlBuilder,
|
||||||
converters::{convert_bulletins_to_v1, convert_bulletin_to_v1, convert_bulletins_to_v2, convert_bulletin_to_v2},
|
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},
|
handlers::bulletins_shared::{process_bulletins_batch, process_single_bulletin},
|
||||||
};
|
};
|
||||||
|
@ -25,7 +24,7 @@ impl BulletinService {
|
||||||
active_only: bool,
|
active_only: bool,
|
||||||
url_builder: &UrlBuilder
|
url_builder: &UrlBuilder
|
||||||
) -> Result<(Vec<Bulletin>, i64)> {
|
) -> 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
|
// Apply shared processing logic
|
||||||
process_bulletins_batch(pool, &mut bulletins).await?;
|
process_bulletins_batch(pool, &mut bulletins).await?;
|
||||||
|
@ -38,7 +37,7 @@ impl BulletinService {
|
||||||
|
|
||||||
/// Get current bulletin with V1 timezone conversion (EST)
|
/// Get current bulletin with V1 timezone conversion (EST)
|
||||||
pub async fn get_current_v1(pool: &PgPool, url_builder: &UrlBuilder) -> Result<Option<Bulletin>> {
|
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 {
|
if let Some(ref mut bulletin_data) = bulletin {
|
||||||
process_single_bulletin(pool, bulletin_data).await?;
|
process_single_bulletin(pool, bulletin_data).await?;
|
||||||
|
@ -56,7 +55,7 @@ impl BulletinService {
|
||||||
|
|
||||||
/// Get next bulletin with V1 timezone conversion (EST)
|
/// Get next bulletin with V1 timezone conversion (EST)
|
||||||
pub async fn get_next_v1(pool: &PgPool, url_builder: &UrlBuilder) -> Result<Option<Bulletin>> {
|
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 {
|
if let Some(ref mut bulletin_data) = bulletin {
|
||||||
process_single_bulletin(pool, bulletin_data).await?;
|
process_single_bulletin(pool, bulletin_data).await?;
|
||||||
|
@ -74,7 +73,7 @@ impl BulletinService {
|
||||||
|
|
||||||
/// Get bulletin by ID with V1 timezone conversion (EST)
|
/// 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>> {
|
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 {
|
match bulletin {
|
||||||
Some(ref mut bulletin_data) => {
|
Some(ref mut bulletin_data) => {
|
||||||
|
@ -88,15 +87,13 @@ impl BulletinService {
|
||||||
|
|
||||||
/// Create a new bulletin
|
/// Create a new bulletin
|
||||||
pub async fn create(pool: &PgPool, request: CreateBulletinRequest, url_builder: &UrlBuilder) -> Result<Bulletin> {
|
pub async fn create(pool: &PgPool, request: CreateBulletinRequest, url_builder: &UrlBuilder) -> Result<Bulletin> {
|
||||||
let bulletin = db::bulletins::create(pool, request).await?;
|
let bulletin = sql::create(pool, &request).await?;
|
||||||
|
|
||||||
// Convert UTC times to EST for V1 compatibility
|
|
||||||
convert_bulletin_to_v1(bulletin, url_builder)
|
convert_bulletin_to_v1(bulletin, url_builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update a bulletin
|
/// Update a bulletin
|
||||||
pub async fn update(pool: &PgPool, id: &Uuid, request: CreateBulletinRequest, url_builder: &UrlBuilder) -> Result<Option<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 {
|
match bulletin {
|
||||||
Some(b) => {
|
Some(b) => {
|
||||||
|
@ -109,7 +106,7 @@ impl BulletinService {
|
||||||
|
|
||||||
/// Delete a bulletin
|
/// Delete a bulletin
|
||||||
pub async fn delete(pool: &PgPool, id: &Uuid) -> Result<()> {
|
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)
|
// V2 API methods (UTC timezone as per shared converter)
|
||||||
|
@ -122,7 +119,7 @@ impl BulletinService {
|
||||||
active_only: bool,
|
active_only: bool,
|
||||||
url_builder: &UrlBuilder
|
url_builder: &UrlBuilder
|
||||||
) -> Result<(Vec<BulletinV2>, i64)> {
|
) -> 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
|
// Convert to V2 format with UTC timestamps
|
||||||
let converted_bulletins = convert_bulletins_to_v2(bulletins, url_builder)?;
|
let converted_bulletins = convert_bulletins_to_v2(bulletins, url_builder)?;
|
||||||
|
@ -132,7 +129,7 @@ impl BulletinService {
|
||||||
|
|
||||||
/// Get current bulletin with V2 format (UTC timestamps)
|
/// Get current bulletin with V2 format (UTC timestamps)
|
||||||
pub async fn get_current_v2(pool: &PgPool, url_builder: &UrlBuilder) -> Result<Option<BulletinV2>> {
|
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 {
|
match bulletin {
|
||||||
Some(b) => {
|
Some(b) => {
|
||||||
|
@ -145,7 +142,7 @@ impl BulletinService {
|
||||||
|
|
||||||
/// Get next bulletin with V2 format (UTC timestamps)
|
/// Get next bulletin with V2 format (UTC timestamps)
|
||||||
pub async fn get_next_v2(pool: &PgPool, url_builder: &UrlBuilder) -> Result<Option<BulletinV2>> {
|
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 {
|
match bulletin {
|
||||||
Some(b) => {
|
Some(b) => {
|
||||||
|
@ -158,7 +155,7 @@ impl BulletinService {
|
||||||
|
|
||||||
/// Get bulletin by ID with V2 format (UTC timestamps)
|
/// 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>> {
|
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 {
|
match bulletin {
|
||||||
Some(b) => {
|
Some(b) => {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use crate::{
|
use crate::{
|
||||||
db,
|
|
||||||
models::ChurchConfig,
|
models::ChurchConfig,
|
||||||
error::Result,
|
error::Result,
|
||||||
};
|
};
|
||||||
|
@ -13,7 +12,12 @@ pub struct ConfigService;
|
||||||
impl ConfigService {
|
impl ConfigService {
|
||||||
/// Get public configuration (excludes API keys)
|
/// Get public configuration (excludes API keys)
|
||||||
pub async fn get_public_config(pool: &PgPool) -> Result<Option<Value>> {
|
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 {
|
match config {
|
||||||
Some(config) => {
|
Some(config) => {
|
||||||
|
@ -43,11 +47,38 @@ impl ConfigService {
|
||||||
|
|
||||||
/// Get admin configuration (includes all fields including API keys)
|
/// Get admin configuration (includes all fields including API keys)
|
||||||
pub async fn get_admin_config(pool: &PgPool) -> Result<Option<ChurchConfig>> {
|
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
|
/// Update church configuration
|
||||||
pub async fn update_config(pool: &PgPool, config: ChurchConfig) -> Result<ChurchConfig> {
|
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 chrono::{NaiveDate, Timelike};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use crate::{
|
use crate::{
|
||||||
db,
|
|
||||||
models::{Schedule, ScheduleV2, ScheduleData, ConferenceData, Personnel},
|
models::{Schedule, ScheduleV2, ScheduleData, ConferenceData, Personnel},
|
||||||
error::{Result, ApiError},
|
error::{Result, ApiError},
|
||||||
utils::{
|
utils::converters::{convert_schedules_to_v1, convert_schedule_to_v2},
|
||||||
converters::{convert_schedules_to_v1, convert_schedule_to_v2},
|
|
||||||
db_operations::ScheduleOperations,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
@ -38,7 +34,13 @@ impl ScheduleService {
|
||||||
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
|
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")
|
||||||
.map_err(|_| ApiError::BadRequest("Invalid date format. Use YYYY-MM-DD".to_string()))?;
|
.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 {
|
let personnel = if let Some(s) = schedule {
|
||||||
Personnel {
|
Personnel {
|
||||||
|
@ -128,7 +130,51 @@ impl ScheduleService {
|
||||||
updated_at: None,
|
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
|
/// Delete schedule by date
|
||||||
|
@ -159,7 +205,13 @@ impl ScheduleService {
|
||||||
|
|
||||||
/// Get schedule by date with V2 format (UTC timestamps)
|
/// Get schedule by date with V2 format (UTC timestamps)
|
||||||
pub async fn get_schedule_v2(pool: &PgPool, date: &NaiveDate) -> Result<Option<ScheduleV2>> {
|
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 {
|
match schedule {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
|
@ -172,7 +224,13 @@ impl ScheduleService {
|
||||||
|
|
||||||
/// Get conference data for V2 (simplified version)
|
/// Get conference data for V2 (simplified version)
|
||||||
pub async fn get_conference_data_v2(pool: &PgPool, date: &NaiveDate) -> Result<ConferenceData> {
|
pub async fn get_conference_data_v2(pool: &PgPool, date: &NaiveDate) -> Result<ConferenceData> {
|
||||||
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?
|
||||||
.ok_or_else(|| ApiError::NotFound("Schedule not found".to_string()))?;
|
.ok_or_else(|| ApiError::NotFound("Schedule not found".to_string()))?;
|
||||||
|
|
||||||
Ok(ConferenceData {
|
Ok(ConferenceData {
|
||||||
|
|
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