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