yggdrasil/api/posts/
stats.rs1use crate::api::error::ApiError;
8use leptos::prelude::*;
9
10#[cfg(feature = "server")]
11use super::helpers::get_current_admin_user;
12use super::types::PostStatsResponse;
13#[cfg(feature = "server")]
14use crate::api::error::AppError;
15#[cfg(feature = "server")]
16use crate::db::pool::get_conn;
17#[cfg(feature = "server")]
18use crate::models::post::{MonthCount, PostStats};
19
20#[server(GetPostStats, endpoint = "get_post_stats", input = leptos::server_fn::codec::Json)]
25pub async fn get_post_stats() -> Result<PostStatsResponse, ApiError> {
26 let _user = get_current_admin_user().await?;
27
28 #[cfg(feature = "server")]
29 {
30 let stats = load_post_stats().await?;
31 Ok(PostStatsResponse { stats })
32 }
33
34 #[cfg(not(feature = "server"))]
35 {
36 Ok(PostStatsResponse {
37 stats: PostStats {
38 total: 0,
39 drafts: 0,
40 published: 0,
41 trash: 0,
42 recent_30d: 0,
43 monthly_published: Vec::new(),
44 },
45 })
46 }
47}
48
49#[cfg(feature = "server")]
52pub(crate) async fn load_post_stats() -> Result<PostStats, AppError> {
53 if let Some(cached) = crate::cache::get_post_stats().await {
54 return Ok(cached);
55 }
56
57 let client = get_conn().await.map_err(AppError::db_conn)?;
58
59 let row = client
61 .query_one(
62 "SELECT
63 COUNT(*) FILTER (WHERE deleted_at IS NULL) AS total,
64 COUNT(*) FILTER (WHERE deleted_at IS NULL AND status = 'draft') AS drafts,
65 COUNT(*) FILTER (WHERE deleted_at IS NULL AND status = 'published') AS published,
66 COUNT(*) FILTER (WHERE deleted_at IS NOT NULL) AS trash,
67 COUNT(*) FILTER (WHERE deleted_at IS NULL AND created_at > now() - interval '30 days') AS recent_30d
68 FROM posts",
69 &[],
70 )
71 .await
72 .map_err(AppError::query)?;
73
74 let stats = PostStats {
75 total: row.get("total"),
76 drafts: row.get("drafts"),
77 published: row.get("published"),
78 trash: row.get("trash"),
79 recent_30d: row.get("recent_30d"),
80 monthly_published: Vec::new(),
81 };
82
83 let month_rows = client
86 .query(
87 "SELECT EXTRACT(YEAR FROM gs.m)::int AS year, EXTRACT(MONTH FROM gs.m)::int AS month,
88 COUNT(p.id) AS cnt
89 FROM generate_series(
90 date_trunc('month', now() AT TIME ZONE 'UTC') - interval '11 months',
91 date_trunc('month', now() AT TIME ZONE 'UTC'),
92 interval '1 month'
93 ) AS gs(m)
94 LEFT JOIN posts p
95 ON p.status = 'published'
96 AND p.deleted_at IS NULL
97 AND date_trunc('month', p.published_at AT TIME ZONE 'UTC') = gs.m
98 GROUP BY gs.m
99 ORDER BY gs.m",
100 &[],
101 )
102 .await
103 .map_err(AppError::query)?;
104 let monthly_published = month_rows
105 .iter()
106 .map(|r| MonthCount {
107 year: r.get("year"),
108 month: r.get::<_, i32>("month") as u32,
109 count: r.get("cnt"),
110 })
111 .collect();
112
113 let stats = PostStats {
114 monthly_published,
115 ..stats
116 };
117 crate::cache::set_post_stats(stats.clone()).await;
118 Ok(stats)
119}