use async_trait::async_trait;
use chrono::{DateTime, Utc};
use sqlx::{
postgres::{PgPool, PgRow},
Error as SqlxError, FromRow, Row,
};
use std::collections::HashMap;
use uuid::Uuid;
use crate::postgres_repository::PostgresRepository;
pub struct DbArticle {
pub id: Option<Uuid>,
pub title: String,
pub hero_image: String,
pub slug: String,
pub description: String,
pub author: String,
pub status: String,
pub date: DateTime<Utc>,
pub content: String,
}
impl DbArticle {
pub fn new(title: &str, slug: &str, description: &str, author: &str) -> Self {
let now = Utc::now();
DbArticle {
id: None,
title: title.to_string(),
hero_image: "".to_string(),
slug: slug.to_string(),
description: description.to_string(),
author: author.to_string(),
status: "NEW".to_string(),
date: now,
content: String::new(),
}
}
}
impl<'r> FromRow<'r, PgRow> for DbArticle {
fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
let row_id = row.try_get("id").ok();
let hero: String = row.try_get("hero_image").unwrap_or_else(|_| String::new());
Ok(DbArticle {
id: row_id,
title: row.try_get("title")?,
hero_image: hero,
slug: row.try_get("slug")?,
description: row.try_get("description")?,
author: row.try_get("author")?,
status: row.try_get("status")?,
date: row.try_get::<DateTime<Utc>, _>("date").unwrap_or_else(|_| {
let naive_date: chrono::NaiveDateTime = row.try_get("date").unwrap();
DateTime::<Utc>::from_naive_utc_and_offset(naive_date, Utc)
}),
content: row.try_get("content")?,
})
}
}
pub struct ArticleRepository {
pool: PgPool,
}
impl ArticleRepository {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
}
#[async_trait]
impl PostgresRepository for ArticleRepository {
type Error = SqlxError;
type Item = DbArticle;
async fn create(&self, article: &Self::Item) -> Result<Uuid, Self::Error> {
let row = sqlx::query(
r#"
INSERT INTO articles (title, slug, description, author, status, date, content)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id
"#,
)
.bind(&article.title)
.bind(&article.slug)
.bind(&article.description)
.bind(&article.author)
.bind(&article.status)
.bind(article.date)
.bind(&article.content)
.fetch_one(&self.pool)
.await?;
let id: Uuid = row.try_get("id")?;
Ok(id)
}
async fn update(&self, article: &Self::Item) -> Result<(), Self::Error> {
let affected_rows = sqlx::query(
r#"
UPDATE articles
SET title = $1, slug = $2, author = $3, status = $4, date = $5, content = $6, description = $7
WHERE id = $8
"#,
)
.bind(&article.title)
.bind(&article.slug)
.bind(&article.author)
.bind(&article.status)
.bind(article.date)
.bind(&article.content)
.bind(&article.description)
.bind(article.id)
.execute(&self.pool)
.await?
.rows_affected();
if affected_rows == 0 {
return Err(SqlxError::RowNotFound);
}
Ok(())
}
async fn delete(&self, id: Uuid) -> Result<(), Self::Error> {
let affected_rows = sqlx::query(
r#"
DELETE FROM articles
WHERE id = $1
"#,
)
.bind(id)
.execute(&self.pool)
.await?
.rows_affected();
if affected_rows == 0 {
return Err(SqlxError::RowNotFound);
}
Ok(())
}
async fn delete_all(
&self,
_filters: Option<HashMap<String, String>>,
) -> Result<(), Self::Error> {
return Err(SqlxError::RowNotFound);
}
async fn find_by_id(&self, id: Uuid) -> Result<Option<Self::Item>, Self::Error> {
let row_result = sqlx::query(
"SELECT articles.id, title, slug, description, author, status, date, content,
(SELECT image_path FROM article_images WHERE article_images.article_id = articles.id LIMIT 1) as hero_image
FROM articles WHERE articles.id = $1",
)
.bind(id)
.fetch_optional(&self.pool)
.await?;
match row_result {
Some(row) => {
let article = DbArticle::from_row(&row)?;
Ok(Some(article))
}
None => Ok(None),
}
}
async fn find_by_name(&self, slug: String) -> Result<Option<Self::Item>, Self::Error> {
let sql = "SELECT articles.id, title, slug, description, author, status, date, content,
(SELECT image_path FROM article_images WHERE article_images.article_id = articles.id LIMIT 1) as hero_image
FROM articles WHERE articles.slug = $1";
let row_result = sqlx::query(sql)
.bind(slug.clone())
.fetch_optional(&self.pool)
.await?;
match row_result {
Some(row) => {
let article = DbArticle::from_row(&row)?;
Ok(Some(article))
}
None => Ok(None),
}
}
async fn list(
&self,
filters: Option<HashMap<String, String>>,
) -> Result<Vec<Self::Item>, Self::Error> {
let mut query = String::from(
"SELECT articles.id, title, slug, description, author, status, date, content,
(SELECT image_path FROM article_images WHERE article_images.article_id = articles.id LIMIT 1) as hero_image
FROM articles",
);
let mut params: Vec<String> = Vec::new();
let mut param_count = 1;
if let Some(filter_map) = filters {
if !filter_map.is_empty() {
query.push_str(" WHERE ");
for (column, value) in filter_map {
if param_count > 1 {
query.push_str(" AND ");
}
query.push_str(&format!("{} = ${}", column, param_count));
params.push(value);
param_count += 1;
}
}
}
query.push_str(" ORDER BY date DESC");
let mut query_builder = sqlx::query(&query);
for param in params {
if let Ok(uuid) = sqlx::types::Uuid::parse_str(¶m) {
query_builder = query_builder.bind(uuid);
} else {
query_builder = query_builder.bind(param);
}
}
let articles = query_builder
.fetch_all(&self.pool)
.await?
.into_iter()
.map(|row| DbArticle::from_row(&row))
.collect::<Result<Vec<_>, _>>()?;
Ok(articles)
}
}