1use color_eyre::eyre::eyre;
4use models::DBAdminDashboardQP;
5use sqlx::{postgres::PgPoolOptions, prelude::FromRow, PgPool, Postgres, Transaction};
6use std::time::Duration;
7
8use crate::{
9 env::EnvVars,
10 pathutils::{PaperCategory, Paths},
11 qp::{self, AdminDashboardQP, Exam, LibraryQP, Semester},
12 routing::{EditReq, FileDetails},
13};
14
15mod models;
16mod queries;
17
18#[derive(Clone)]
19pub struct Database {
21 connection: PgPool,
22}
23
24#[derive(FromRow)]
25struct Breh {
27 id: i32,
28}
29
30impl Database {
31 pub async fn new(env_vars: &EnvVars) -> Result<Self, sqlx::Error> {
33 let database_url = format!(
34 "postgres://{}:{}@{}:{}/{}",
35 env_vars.db_user,
36 env_vars.db_password,
37 env_vars.db_host,
38 env_vars.db_port,
39 env_vars.db_name
40 );
41
42 let conn_pool = PgPoolOptions::new()
43 .max_connections(5)
44 .acquire_timeout(Duration::from_secs(3))
45 .connect(&database_url)
46 .await?;
47
48 Ok(Self {
49 connection: conn_pool,
50 })
51 }
52
53 pub async fn get_unapproved_papers(&self) -> Result<Vec<qp::AdminDashboardQP>, sqlx::Error> {
55 let query_sql = queries::get_all_unapproved_query();
56 let papers: Vec<models::DBAdminDashboardQP> = sqlx::query_as(&query_sql)
57 .fetch_all(&self.connection)
58 .await?;
59
60 Ok(papers
61 .iter()
62 .map(|qp| qp::AdminDashboardQP::from(qp.clone()))
63 .collect())
64 }
65
66 pub async fn get_unapproved_papers_count(
68 &self,
69 ) -> Result<i64, sqlx::Error> {
70 let count: (i64,) = sqlx::query_as(queries::GET_UNAPPROVED_COUNT)
71 .fetch_one(&self.connection)
72 .await?;
73
74 Ok(count.0)
75 }
76
77 pub async fn search_papers(
79 &self,
80 query: &str,
81 exam_filter: Vec<Exam>,
82 ) -> Result<Vec<qp::BaseQP>, sqlx::Error> {
83 let query_sql = queries::get_qp_search_query(exam_filter);
84 let query = sqlx::query_as(&query_sql).bind(query);
85
86 let papers: Vec<models::DBBaseQP> = query.fetch_all(&self.connection).await?;
87
88 Ok(papers
89 .iter()
90 .map(|qp| qp::BaseQP::from(qp.clone()))
91 .collect())
92 }
93
94 pub async fn get_paper_by_id(&self, id: i32) -> Result<qp::AdminDashboardQP, sqlx::Error> {
95 let query_sql = queries::get_get_paper_by_id_query();
96 let query = sqlx::query_as(&query_sql).bind(id);
97
98 let paper: models::DBAdminDashboardQP = query.fetch_one(&self.connection).await?;
99
100 Ok(paper.into())
101 }
102
103 pub async fn edit_paper<'c>(
113 &self,
114 edit_req: EditReq,
115 username: &str,
116 env_vars: &EnvVars,
117 ) -> Result<(Transaction<'c, Postgres>, String, AdminDashboardQP), color_eyre::eyre::Error>
118 {
119 let EditReq {
120 id,
121 course_code,
122 course_name,
123 year,
124 semester,
125 exam,
126 approve_status,
127 note,
128 replace,
129 } = edit_req;
130
131 let current_details = self.get_paper_by_id(id).await?;
132
133 let course_code = course_code.unwrap_or(current_details.qp.course_code);
135 let course_name = course_name.unwrap_or(current_details.qp.course_name);
136 let year = year.unwrap_or(current_details.qp.year);
137 let semester: String = semester
138 .map(|sem| Semester::try_from(&sem))
139 .transpose()?
140 .unwrap_or(current_details.qp.semester)
141 .into();
142 let exam: String = exam
143 .map(|exam| Exam::try_from(&exam))
144 .transpose()?
145 .unwrap_or(current_details.qp.exam)
146 .into();
147 let approve_status = approve_status.unwrap_or(current_details.approve_status);
148
149 let old_filelink = current_details.qp.filelink;
151 let new_filelink = if current_details.qp.from_library {
152 old_filelink.clone() } else if approve_status {
154 env_vars.paths.get_slug(
155 &format!(
156 "{}.pdf",
157 Paths::sanitize_path(&format!(
158 "{}_{}_{}_{}_{}_{}",
159 id, course_code, course_name, year, semester, exam
160 ))
161 ),
162 PaperCategory::Approved,
163 )
164 } else {
165 env_vars
166 .paths
167 .get_slug(&format!("{}.pdf", id), PaperCategory::Unapproved)
168 };
169
170 let mut tx = self.connection.begin().await?;
171
172 let query_sql = queries::get_edit_paper_query(approve_status);
173 let query = sqlx::query_as(&query_sql)
174 .bind(id)
175 .bind(&course_code)
176 .bind(&course_name)
177 .bind(year)
178 .bind(&semester)
179 .bind(&exam)
180 .bind(¬e)
181 .bind(approve_status)
182 .bind(&new_filelink);
183
184 let query = if approve_status {
185 query.bind(username)
186 } else {
187 query
188 };
189
190 let new_qp: DBAdminDashboardQP = query.fetch_one(&mut *tx).await?;
191 let new_qp = AdminDashboardQP::from(new_qp);
192
193 for replace_id in replace {
195 let rows_affected = sqlx::query(queries::SOFT_DELETE_ANY_BY_ID)
196 .bind(replace_id)
197 .execute(&mut *tx)
198 .await?
199 .rows_affected();
200
201 if rows_affected > 1 {
202 tx.rollback().await?;
203 return Err(eyre!(
204 "Error: {} (> 1) papers were deleted. Rolling back.",
205 rows_affected
206 ));
207 }
208 }
209
210 Ok((tx, old_filelink, new_qp))
211 }
212
213 pub async fn soft_delete(&self, id: i32) -> Result<bool, color_eyre::eyre::Error> {
226 let mut tx = self.connection.begin().await?;
227
228 let rows_affected = sqlx::query(queries::SOFT_DELETE_BY_ID)
229 .bind(id)
230 .execute(&mut *tx)
231 .await?
232 .rows_affected();
233
234 if rows_affected > 1 {
235 tx.rollback().await?;
236 Err(eyre!(
237 "Error: {} (> 1) papers were deleted. Rolling back.",
238 rows_affected
239 ))
240 } else {
241 tx.commit().await?;
242 Ok(rows_affected == 1)
243 }
244 }
245
246 pub async fn get_similar_papers(
248 &self,
249 course_code: &str,
250 year: Option<i32>,
251 semester: Option<&String>,
252 exam: Option<&String>,
253 ) -> Result<Vec<AdminDashboardQP>, sqlx::Error> {
254 let query_sql =
255 queries::get_similar_papers_query(year.is_some(), semester.is_some(), exam.is_some());
256 let query = sqlx::query_as(&query_sql).bind(course_code);
257
258 let query = query.bind(year);
259 let query = query.bind(semester);
260 let query = query.bind(exam);
261
262 let papers: Vec<models::DBAdminDashboardQP> = query.fetch_all(&self.connection).await?;
263
264 Ok(papers
265 .iter()
266 .map(|qp| qp::AdminDashboardQP::from(qp.clone()))
267 .collect())
268 }
269
270 pub async fn insert_new_uploaded_qp<'c>(
274 &self,
275 file_details: &FileDetails,
276 ) -> Result<(Transaction<'c, Postgres>, i32), color_eyre::eyre::Error> {
277 let mut tx = self.connection.begin().await?;
278
279 let FileDetails {
280 course_code,
281 course_name,
282 year,
283 exam,
284 semester,
285 note,
286 ..
287 } = file_details;
288
289 let query = sqlx::query_as(queries::INSERT_NEW_QP)
290 .bind(course_code)
291 .bind(course_name)
292 .bind(year)
293 .bind(exam)
294 .bind(semester)
295 .bind(note)
296 .bind("placeholder_filelink")
297 .bind(false);
298
299 let Breh { id } = query.fetch_one(&mut *tx).await?;
300
301 Ok((tx, id))
302 }
303
304 #[allow(unused)]
305 pub async fn insert_new_library_qp<'c>(
309 &self,
310 paper: &LibraryQP,
311 ) -> Result<(Transaction<'c, Postgres>, i32), color_eyre::eyre::Error> {
312 let mut tx = self.connection.begin().await?;
313
314 let LibraryQP {
315 course_code,
316 course_name,
317 year,
318 exam,
319 semester,
320 approve_status,
321 ..
322 } = paper;
323
324 let query = sqlx::query_as(queries::INSERT_NEW_LIBRARY_QP)
325 .bind(course_code)
326 .bind(course_name)
327 .bind(year)
328 .bind(exam)
329 .bind(semester)
330 .bind("")
331 .bind("placeholder_filelink")
332 .bind(approve_status);
333
334 let Breh { id } = query.fetch_one(&mut *tx).await?;
335
336 Ok((tx, id))
337 }
338
339 pub async fn update_filelink(
341 &self,
342 tx: &mut Transaction<'_, Postgres>,
343 id: i32,
344 file_link: &str,
345 ) -> Result<(), color_eyre::eyre::Error> {
346 let query = sqlx::query(queries::UPDATE_FILELINK)
347 .bind(id)
348 .bind(file_link);
349
350 query.execute(&mut **tx).await?;
351
352 Ok(())
353 }
354}