]> Untitled Git - lemmy.git/blob - crates/db_queries/src/lib.rs
Clean up reporting (#1776)
[lemmy.git] / crates / db_queries / src / lib.rs
1 #[macro_use]
2 extern crate diesel;
3 #[macro_use]
4 extern crate strum_macros;
5 #[macro_use]
6 extern crate lazy_static;
7 // this is used in tests
8 #[allow(unused_imports)]
9 #[macro_use]
10 extern crate diesel_migrations;
11
12 #[cfg(test)]
13 extern crate serial_test;
14
15 use chrono::NaiveDateTime;
16 use diesel::{result::Error, *};
17 use lemmy_db_schema::{CommunityId, DbUrl, PersonId};
18 use lemmy_utils::ApiError;
19 use regex::Regex;
20 use serde::{Deserialize, Serialize};
21 use std::{env, env::VarError};
22 use url::Url;
23
24 pub mod aggregates;
25 pub mod source;
26
27 pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
28
29 pub trait Crud {
30   type Form;
31   type IdType;
32   fn create(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error>
33   where
34     Self: Sized;
35   fn read(conn: &PgConnection, id: Self::IdType) -> Result<Self, Error>
36   where
37     Self: Sized;
38   fn update(conn: &PgConnection, id: Self::IdType, form: &Self::Form) -> Result<Self, Error>
39   where
40     Self: Sized;
41   fn delete(_conn: &PgConnection, _id: Self::IdType) -> Result<usize, Error>
42   where
43     Self: Sized,
44   {
45     unimplemented!()
46   }
47 }
48
49 pub trait Followable {
50   type Form;
51   fn follow(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error>
52   where
53     Self: Sized;
54   fn follow_accepted(
55     conn: &PgConnection,
56     community_id: CommunityId,
57     person_id: PersonId,
58   ) -> Result<Self, Error>
59   where
60     Self: Sized;
61   fn unfollow(conn: &PgConnection, form: &Self::Form) -> Result<usize, Error>
62   where
63     Self: Sized;
64   fn has_local_followers(conn: &PgConnection, community_id: CommunityId) -> Result<bool, Error>;
65 }
66
67 pub trait Joinable {
68   type Form;
69   fn join(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error>
70   where
71     Self: Sized;
72   fn leave(conn: &PgConnection, form: &Self::Form) -> Result<usize, Error>
73   where
74     Self: Sized;
75 }
76
77 pub trait Likeable {
78   type Form;
79   type IdType;
80   fn like(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error>
81   where
82     Self: Sized;
83   fn remove(
84     conn: &PgConnection,
85     person_id: PersonId,
86     item_id: Self::IdType,
87   ) -> Result<usize, Error>
88   where
89     Self: Sized;
90 }
91
92 pub trait Bannable {
93   type Form;
94   fn ban(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error>
95   where
96     Self: Sized;
97   fn unban(conn: &PgConnection, form: &Self::Form) -> Result<usize, Error>
98   where
99     Self: Sized;
100 }
101
102 pub trait Saveable {
103   type Form;
104   fn save(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error>
105   where
106     Self: Sized;
107   fn unsave(conn: &PgConnection, form: &Self::Form) -> Result<usize, Error>
108   where
109     Self: Sized;
110 }
111
112 pub trait Blockable {
113   type Form;
114   fn block(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error>
115   where
116     Self: Sized;
117   fn unblock(conn: &PgConnection, form: &Self::Form) -> Result<usize, Error>
118   where
119     Self: Sized;
120 }
121
122 pub trait Readable {
123   type Form;
124   fn mark_as_read(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error>
125   where
126     Self: Sized;
127   fn mark_as_unread(conn: &PgConnection, form: &Self::Form) -> Result<usize, Error>
128   where
129     Self: Sized;
130 }
131
132 pub trait Reportable {
133   type Form;
134   type IdType;
135   fn report(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error>
136   where
137     Self: Sized;
138   fn resolve(
139     conn: &PgConnection,
140     report_id: Self::IdType,
141     resolver_id: PersonId,
142   ) -> Result<usize, Error>
143   where
144     Self: Sized;
145   fn unresolve(
146     conn: &PgConnection,
147     report_id: Self::IdType,
148     resolver_id: PersonId,
149   ) -> Result<usize, Error>
150   where
151     Self: Sized;
152 }
153
154 pub trait DeleteableOrRemoveable {
155   fn blank_out_deleted_or_removed_info(self) -> Self;
156 }
157
158 // TODO: move this to apub lib
159 pub trait ApubObject {
160   /// If this object should be refetched after a certain interval, it should return the last refresh
161   /// time here. This is mainly used to update remote actors.
162   fn last_refreshed_at(&self) -> Option<NaiveDateTime>;
163   fn read_from_apub_id(conn: &PgConnection, object_id: &DbUrl) -> Result<Self, Error>
164   where
165     Self: Sized;
166 }
167
168 pub trait MaybeOptional<T> {
169   fn get_optional(self) -> Option<T>;
170 }
171
172 impl<T> MaybeOptional<T> for T {
173   fn get_optional(self) -> Option<T> {
174     Some(self)
175   }
176 }
177
178 impl<T> MaybeOptional<T> for Option<T> {
179   fn get_optional(self) -> Option<T> {
180     self
181   }
182 }
183
184 pub trait ToSafe {
185   type SafeColumns;
186   fn safe_columns_tuple() -> Self::SafeColumns;
187 }
188
189 pub trait ToSafeSettings {
190   type SafeSettingsColumns;
191   fn safe_settings_columns_tuple() -> Self::SafeSettingsColumns;
192 }
193
194 pub trait ViewToVec {
195   type DbTuple;
196   fn from_tuple_to_vec(tuple: Vec<Self::DbTuple>) -> Vec<Self>
197   where
198     Self: Sized;
199 }
200
201 pub fn get_database_url_from_env() -> Result<String, VarError> {
202   env::var("LEMMY_DATABASE_URL")
203 }
204
205 #[derive(EnumString, ToString, Debug, Serialize, Deserialize, Clone, Copy)]
206 pub enum SortType {
207   Active,
208   Hot,
209   New,
210   TopDay,
211   TopWeek,
212   TopMonth,
213   TopYear,
214   TopAll,
215   MostComments,
216   NewComments,
217 }
218
219 #[derive(EnumString, ToString, Debug, Serialize, Deserialize, Clone, Copy)]
220 pub enum ListingType {
221   All,
222   Local,
223   Subscribed,
224   Community,
225 }
226
227 #[derive(EnumString, ToString, Debug, Serialize, Deserialize, Clone, Copy)]
228 pub enum SearchType {
229   All,
230   Comments,
231   Posts,
232   Communities,
233   Users,
234   Url,
235 }
236
237 pub fn from_opt_str_to_opt_enum<T: std::str::FromStr>(opt: &Option<String>) -> Option<T> {
238   opt.as_ref().map(|t| T::from_str(t).ok()).flatten()
239 }
240
241 pub fn fuzzy_search(q: &str) -> String {
242   let replaced = q.replace(" ", "%");
243   format!("%{}%", replaced)
244 }
245
246 pub fn limit_and_offset(page: Option<i64>, limit: Option<i64>) -> (i64, i64) {
247   let page = page.unwrap_or(1);
248   let limit = limit.unwrap_or(10);
249   let offset = limit * (page - 1);
250   (limit, offset)
251 }
252
253 pub fn is_email_regex(test: &str) -> bool {
254   EMAIL_REGEX.is_match(test)
255 }
256
257 pub fn diesel_option_overwrite(opt: &Option<String>) -> Option<Option<String>> {
258   match opt {
259     // An empty string is an erase
260     Some(unwrapped) => {
261       if !unwrapped.eq("") {
262         Some(Some(unwrapped.to_owned()))
263       } else {
264         Some(None)
265       }
266     }
267     None => None,
268   }
269 }
270
271 pub fn diesel_option_overwrite_to_url(
272   opt: &Option<String>,
273 ) -> Result<Option<Option<DbUrl>>, ApiError> {
274   match opt.as_ref().map(|s| s.as_str()) {
275     // An empty string is an erase
276     Some("") => Ok(Some(None)),
277     Some(str_url) => match Url::parse(str_url) {
278       Ok(url) => Ok(Some(Some(url.into()))),
279       Err(_) => Err(ApiError::err("invalid_url")),
280     },
281     None => Ok(None),
282   }
283 }
284
285 embed_migrations!();
286
287 pub fn establish_unpooled_connection() -> PgConnection {
288   let db_url = match get_database_url_from_env() {
289     Ok(url) => url,
290     Err(e) => panic!(
291       "Failed to read database URL from env var LEMMY_DATABASE_URL: {}",
292       e
293     ),
294   };
295   let conn =
296     PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url));
297   embedded_migrations::run(&conn).expect("load migrations");
298   conn
299 }
300
301 lazy_static! {
302   static ref EMAIL_REGEX: Regex =
303     Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
304       .expect("compile email regex");
305 }
306
307 pub mod functions {
308   use diesel::sql_types::*;
309
310   sql_function! {
311     fn hot_rank(score: BigInt, time: Timestamp) -> Integer;
312   }
313 }
314
315 #[cfg(test)]
316 mod tests {
317   use super::{fuzzy_search, *};
318   use crate::is_email_regex;
319
320   #[test]
321   fn test_fuzzy_search() {
322     let test = "This is a fuzzy search";
323     assert_eq!(fuzzy_search(test), "%This%is%a%fuzzy%search%".to_string());
324   }
325
326   #[test]
327   fn test_email() {
328     assert!(is_email_regex("gush@gmail.com"));
329     assert!(!is_email_regex("nada_neutho"));
330   }
331
332   #[test]
333   fn test_diesel_option_overwrite() {
334     assert_eq!(diesel_option_overwrite(&None), None);
335     assert_eq!(diesel_option_overwrite(&Some("".to_string())), Some(None));
336     assert_eq!(
337       diesel_option_overwrite(&Some("test".to_string())),
338       Some(Some("test".to_string()))
339     );
340   }
341
342   #[test]
343   fn test_diesel_option_overwrite_to_url() {
344     assert!(matches!(diesel_option_overwrite_to_url(&None), Ok(None)));
345     assert!(matches!(
346       diesel_option_overwrite_to_url(&Some("".to_string())),
347       Ok(Some(None))
348     ));
349     assert!(matches!(
350       diesel_option_overwrite_to_url(&Some("invalid_url".to_string())),
351       Err(_)
352     ));
353     let example_url = "https://example.com";
354     assert!(matches!(
355       diesel_option_overwrite_to_url(&Some(example_url.to_string())),
356       Ok(Some(Some(url))) if url == Url::parse(example_url).unwrap().into()
357     ));
358   }
359 }