1use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use sqlx::{
9 postgres::{PgPool, PgRow},
10 Error as SqlxError, FromRow, Row,
11};
12use std::collections::HashMap;
13use uuid::Uuid;
14
15use crate::postgres_repository::PostgresRepository;
16
17pub struct DbArticle {
21 pub id: Option<Uuid>,
22 pub title: String,
23 pub hero_image: String,
24 pub slug: String,
25 pub description: String,
26 pub author: String,
27 pub status: String,
28 pub created: DateTime<Utc>,
29 pub content: String,
30 pub source: String,
31 pub published: Option<DateTime<Utc>>,
32 pub kind: String,
33}
34
35
36impl DbArticle {
37 pub fn new(title: &str, slug: &str, description: &str, author: &str) -> Self {
45 let now = Utc::now();
46 DbArticle {
47 id: None,
48 title: title.to_string(),
49 hero_image: "".to_string(),
50 slug: slug.to_string(),
51 description: description.to_string(),
52 author: author.to_string(),
53 status: "NEW".to_string(),
54 created: now,
55 content: String::new(),
56 source: String::new(),
57 published: None,
58 kind: "POST".to_string(),
59 }
60 }
61}
62
63impl<'r> FromRow<'r, PgRow> for DbArticle {
65 fn from_row(row: &'r PgRow) -> Result<Self, sqlx::Error> {
66 let row_id = row.try_get("id").ok();
67 let hero: String = row.try_get("hero_image").unwrap_or_else(|_| String::new());
68 let published = row.try_get::<chrono::NaiveDateTime, _>("published")
69 .ok()
70 .map(|naive_date| DateTime::<Utc>::from_naive_utc_and_offset(naive_date, Utc));
71 Ok(DbArticle {
72 id: row_id,
73 title: row.try_get("title")?,
74 hero_image: hero,
75 slug: row.try_get("slug")?,
76 description: row.try_get("description")?,
77 author: row.try_get("author")?,
78 status: row.try_get("status")?,
79 created: row
80 .try_get::<DateTime<Utc>, _>("created")
81 .unwrap_or_else(|_| {
82 let naive_date: chrono::NaiveDateTime = row.try_get("created").unwrap();
84 DateTime::<Utc>::from_naive_utc_and_offset(naive_date, Utc)
85 }),
86 content: row.try_get("content")?,
87 source: row.try_get("source").unwrap_or_default(),
88 published: published,
89 kind: row.try_get("kind").unwrap_or_else(|_| "POST".to_string()),
90 })
91 }
92}
93
94pub struct ArticleRepository {
96 pool: PgPool,
98}
99
100impl ArticleRepository {
101 pub fn new(pool: PgPool) -> Self {
106 Self { pool }
107 }
108}
109
110#[async_trait]
112impl PostgresRepository for ArticleRepository {
113 type Error = SqlxError;
114 type Item = DbArticle;
115
116 async fn create(&self, article: &Self::Item) -> Result<Uuid, Self::Error> {
124 let row = sqlx::query(
125 r#"
126 INSERT INTO articles (title, slug, source, description, author, status, created, content, published, kind)
127 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
128 RETURNING id
129 "#,
130 )
131 .bind(&article.title)
132 .bind(&article.slug)
133 .bind(&article.source)
134 .bind(&article.description)
135 .bind(&article.author)
136 .bind(&article.status)
137 .bind(article.created)
138 .bind(&article.content)
139 .bind(article.published)
140 .bind(&article.kind)
141 .fetch_one(&self.pool)
142 .await?;
143
144 let id: Uuid = row.try_get("id")?;
145 Ok(id)
146 }
147
148 async fn update(&self, article: &Self::Item) -> Result<(), Self::Error> {
156 let affected_rows = sqlx::query(
157 r#"
158 UPDATE articles
159 SET title = $1, slug = $2, author = $3, status = $4, created = $5, content = $6, description = $7, source = $8, published = $9, kind = $10
160 WHERE id = $11
161 "#,
162 )
163 .bind(&article.title)
164 .bind(&article.slug)
165 .bind(&article.author)
166 .bind(&article.status)
167 .bind(article.created)
168 .bind(&article.content)
169 .bind(&article.description)
170 .bind(&article.source)
171 .bind(article.published)
172 .bind(&article.kind)
173 .bind(article.id)
174 .execute(&self.pool)
175 .await?
176 .rows_affected();
177
178 if affected_rows == 0 {
179 return Err(SqlxError::RowNotFound);
180 }
181
182 Ok(())
183 }
184
185 async fn delete(&self, id: Uuid) -> Result<(), Self::Error> {
193 let affected_rows = sqlx::query(
194 r#"
195 DELETE FROM articles
196 WHERE id = $1
197 "#,
198 )
199 .bind(id)
200 .execute(&self.pool)
201 .await?
202 .rows_affected();
203
204 if affected_rows == 0 {
205 return Err(SqlxError::RowNotFound);
206 }
207
208 Ok(())
209 }
210
211 async fn delete_all(
215 &self,
216 _filters: Option<HashMap<String, String>>,
217 ) -> Result<(), Self::Error> {
218 tracing::warn!("delete_all not implemented");
219 return Err(SqlxError::RowNotFound);
220 }
221
222 async fn find_by_id(&self, id: Uuid) -> Result<Option<Self::Item>, Self::Error> {
230 let row_result = sqlx::query(
231 "SELECT articles.id, title, slug, description, author, status, created, content,
232 (SELECT image_path FROM article_images WHERE article_images.article_id = articles.id LIMIT 1) as hero_image,
233 source,
234 published,
235 kind
236 FROM articles WHERE articles.id = $1",
237 )
238 .bind(id)
239 .fetch_optional(&self.pool)
240 .await?;
241
242 match row_result {
243 Some(row) => {
244 let article = DbArticle::from_row(&row)?;
245 Ok(Some(article))
246 }
247 None => Ok(None),
248 }
249 }
250
251 async fn find_by_name(&self, slug: String) -> Result<Option<Self::Item>, Self::Error> {
259 let sql = "SELECT articles.id, title, slug, description, author, status, created, content,
260 (SELECT image_path FROM article_images WHERE article_images.article_id = articles.id LIMIT 1) as hero_image,
261 source,
262 published,
263 kind
264 FROM articles WHERE articles.slug = $1";
265 let row_result = sqlx::query(sql)
266 .bind(slug.clone())
267 .fetch_optional(&self.pool)
268 .await?;
269
270 match row_result {
271 Some(row) => {
272 let article = DbArticle::from_row(&row)?;
273 Ok(Some(article))
274 }
275 None => Ok(None),
276 }
277 }
278
279 async fn list(
287 &self,
288 filters: Option<HashMap<String, String>>,
289 ) -> Result<Vec<Self::Item>, Self::Error> {
290 let mut query = String::from(
292 "SELECT articles.id, title, slug, description, author, status, created, content, source, published, kind,
293 (SELECT image_path FROM article_images WHERE article_images.article_id = articles.id LIMIT 1) as hero_image
294 FROM articles",
295 );
296 let mut params: Vec<String> = Vec::new();
297 let mut param_count = 1;
298
299 if let Some(filter_map) = filters {
301 if !filter_map.is_empty() {
302 query.push_str(" WHERE ");
303 for (column, value) in filter_map {
304 if param_count > 1 {
305 query.push_str(" AND ");
306 }
307 query.push_str(&format!("{} = ${}", column, param_count));
308 params.push(value);
309 param_count += 1;
310 }
311 }
312 }
313
314 query.push_str(" ORDER BY created DESC");
316 let mut query_builder = sqlx::query(&query);
317
318 for param in params {
320 if let Ok(uuid) = sqlx::types::Uuid::parse_str(¶m) {
322 query_builder = query_builder.bind(uuid);
323 } else {
324 query_builder = query_builder.bind(param);
325 }
326 }
327
328 let articles = query_builder
330 .fetch_all(&self.pool)
331 .await?
332 .into_iter()
333 .map(|row| DbArticle::from_row(&row))
334 .collect::<Result<Vec<_>, _>>()?;
335
336 Ok(articles)
337 }
338}