]> Untitled Git - lemmy.git/blob - crates/db_queries/src/lib.rs
dbd470e4deb98be1b9c7c0d19b510863ffb4df5c
[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   fn report(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error>
135   where
136     Self: Sized;
137   fn resolve(conn: &PgConnection, report_id: i32, resolver_id: PersonId) -> Result<usize, Error>
138   where
139     Self: Sized;
140   fn unresolve(conn: &PgConnection, report_id: i32, resolver_id: PersonId) -> Result<usize, Error>
141   where
142     Self: Sized;
143 }
144
145 pub trait DeleteableOrRemoveable {
146   fn blank_out_deleted_or_removed_info(self) -> Self;
147 }
148
149 // TODO: move this to apub lib
150 pub trait ApubObject {
151   /// If this object should be refetched after a certain interval, it should return the last refresh
152   /// time here. This is mainly used to update remote actors.
153   fn last_refreshed_at(&self) -> Option<NaiveDateTime>;
154   fn read_from_apub_id(conn: &PgConnection, object_id: &DbUrl) -> Result<Self, Error>
155   where
156     Self: Sized;
157 }
158
159 pub trait MaybeOptional<T> {
160   fn get_optional(self) -> Option<T>;
161 }
162
163 impl<T> MaybeOptional<T> for T {
164   fn get_optional(self) -> Option<T> {
165     Some(self)
166   }
167 }
168
169 impl<T> MaybeOptional<T> for Option<T> {
170   fn get_optional(self) -> Option<T> {
171     self
172   }
173 }
174
175 pub trait ToSafe {
176   type SafeColumns;
177   fn safe_columns_tuple() -> Self::SafeColumns;
178 }
179
180 pub trait ToSafeSettings {
181   type SafeSettingsColumns;
182   fn safe_settings_columns_tuple() -> Self::SafeSettingsColumns;
183 }
184
185 pub trait ViewToVec {
186   type DbTuple;
187   fn from_tuple_to_vec(tuple: Vec<Self::DbTuple>) -> Vec<Self>
188   where
189     Self: Sized;
190 }
191
192 pub fn get_database_url_from_env() -> Result<String, VarError> {
193   env::var("LEMMY_DATABASE_URL")
194 }
195
196 #[derive(EnumString, ToString, Debug, Serialize, Deserialize, Clone, Copy)]
197 pub enum SortType {
198   Active,
199   Hot,
200   New,
201   TopDay,
202   TopWeek,
203   TopMonth,
204   TopYear,
205   TopAll,
206   MostComments,
207   NewComments,
208 }
209
210 #[derive(EnumString, ToString, Debug, Serialize, Deserialize, Clone, Copy)]
211 pub enum ListingType {
212   All,
213   Local,
214   Subscribed,
215   Community,
216 }
217
218 #[derive(EnumString, ToString, Debug, Serialize, Deserialize, Clone, Copy)]
219 pub enum SearchType {
220   All,
221   Comments,
222   Posts,
223   Communities,
224   Users,
225   Url,
226 }
227
228 pub fn from_opt_str_to_opt_enum<T: std::str::FromStr>(opt: &Option<String>) -> Option<T> {
229   opt.as_ref().map(|t| T::from_str(t).ok()).flatten()
230 }
231
232 pub fn fuzzy_search(q: &str) -> String {
233   let replaced = q.replace(" ", "%");
234   format!("%{}%", replaced)
235 }
236
237 pub fn limit_and_offset(page: Option<i64>, limit: Option<i64>) -> (i64, i64) {
238   let page = page.unwrap_or(1);
239   let limit = limit.unwrap_or(10);
240   let offset = limit * (page - 1);
241   (limit, offset)
242 }
243
244 pub fn is_email_regex(test: &str) -> bool {
245   EMAIL_REGEX.is_match(test)
246 }
247
248 pub fn diesel_option_overwrite(opt: &Option<String>) -> Option<Option<String>> {
249   match opt {
250     // An empty string is an erase
251     Some(unwrapped) => {
252       if !unwrapped.eq("") {
253         Some(Some(unwrapped.to_owned()))
254       } else {
255         Some(None)
256       }
257     }
258     None => None,
259   }
260 }
261
262 pub fn diesel_option_overwrite_to_url(
263   opt: &Option<String>,
264 ) -> Result<Option<Option<DbUrl>>, ApiError> {
265   match opt.as_ref().map(|s| s.as_str()) {
266     // An empty string is an erase
267     Some("") => Ok(Some(None)),
268     Some(str_url) => match Url::parse(str_url) {
269       Ok(url) => Ok(Some(Some(url.into()))),
270       Err(_) => Err(ApiError::err("invalid_url")),
271     },
272     None => Ok(None),
273   }
274 }
275
276 embed_migrations!();
277
278 pub fn establish_unpooled_connection() -> PgConnection {
279   let db_url = match get_database_url_from_env() {
280     Ok(url) => url,
281     Err(e) => panic!(
282       "Failed to read database URL from env var LEMMY_DATABASE_URL: {}",
283       e
284     ),
285   };
286   let conn =
287     PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url));
288   embedded_migrations::run(&conn).expect("load migrations");
289   conn
290 }
291
292 lazy_static! {
293   static ref EMAIL_REGEX: Regex =
294     Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
295       .expect("compile email regex");
296 }
297
298 pub mod functions {
299   use diesel::sql_types::*;
300
301   sql_function! {
302     fn hot_rank(score: BigInt, time: Timestamp) -> Integer;
303   }
304 }
305
306 #[cfg(test)]
307 mod tests {
308   use super::{fuzzy_search, *};
309   use crate::is_email_regex;
310
311   #[test]
312   fn test_fuzzy_search() {
313     let test = "This is a fuzzy search";
314     assert_eq!(fuzzy_search(test), "%This%is%a%fuzzy%search%".to_string());
315   }
316
317   #[test]
318   fn test_email() {
319     assert!(is_email_regex("gush@gmail.com"));
320     assert!(!is_email_regex("nada_neutho"));
321   }
322
323   #[test]
324   fn test_diesel_option_overwrite() {
325     assert_eq!(diesel_option_overwrite(&None), None);
326     assert_eq!(diesel_option_overwrite(&Some("".to_string())), Some(None));
327     assert_eq!(
328       diesel_option_overwrite(&Some("test".to_string())),
329       Some(Some("test".to_string()))
330     );
331   }
332
333   #[test]
334   fn test_diesel_option_overwrite_to_url() {
335     assert!(matches!(diesel_option_overwrite_to_url(&None), Ok(None)));
336     assert!(matches!(
337       diesel_option_overwrite_to_url(&Some("".to_string())),
338       Ok(Some(None))
339     ));
340     assert!(matches!(
341       diesel_option_overwrite_to_url(&Some("invalid_url".to_string())),
342       Err(_)
343     ));
344     let example_url = "https://example.com";
345     assert!(matches!(
346       diesel_option_overwrite_to_url(&Some(example_url.to_string())),
347       Ok(Some(Some(url))) if url == Url::parse(example_url).unwrap().into()
348     ));
349   }
350 }