]> Untitled Git - lemmy.git/blob - crates/db_schema/src/utils.rs
Moving settings to Database. (#2492)
[lemmy.git] / crates / db_schema / src / utils.rs
1 use crate::{diesel_migrations::MigrationHarness, newtypes::DbUrl, CommentSortType, SortType};
2 use activitypub_federation::{core::object_id::ObjectId, traits::ApubObject};
3 use chrono::NaiveDateTime;
4 use diesel::{
5   backend::Backend,
6   deserialize::FromSql,
7   pg::Pg,
8   result::Error::QueryBuilderError,
9   serialize::{Output, ToSql},
10   sql_types::Text,
11   Connection,
12   PgConnection,
13 };
14 use diesel_migrations::EmbeddedMigrations;
15 use lemmy_utils::error::LemmyError;
16 use once_cell::sync::Lazy;
17 use regex::Regex;
18 use std::{env, env::VarError};
19 use url::Url;
20
21 const FETCH_LIMIT_DEFAULT: i64 = 10;
22 pub const FETCH_LIMIT_MAX: i64 = 50;
23
24 pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
25
26 pub fn get_database_url_from_env() -> Result<String, VarError> {
27   env::var("LEMMY_DATABASE_URL")
28 }
29
30 pub fn fuzzy_search(q: &str) -> String {
31   let replaced = q.replace('%', "\\%").replace('_', "\\_").replace(' ', "%");
32   format!("%{}%", replaced)
33 }
34
35 pub fn limit_and_offset(
36   page: Option<i64>,
37   limit: Option<i64>,
38 ) -> Result<(i64, i64), diesel::result::Error> {
39   let page = match page {
40     Some(page) => {
41       if page < 1 {
42         return Err(QueryBuilderError("Page is < 1".into()));
43       } else {
44         page
45       }
46     }
47     None => 1,
48   };
49   let limit = match limit {
50     Some(limit) => {
51       if !(1..=FETCH_LIMIT_MAX).contains(&limit) {
52         return Err(QueryBuilderError(
53           format!("Fetch limit is > {}", FETCH_LIMIT_MAX).into(),
54         ));
55       } else {
56         limit
57       }
58     }
59     None => FETCH_LIMIT_DEFAULT,
60   };
61   let offset = limit * (page - 1);
62   Ok((limit, offset))
63 }
64
65 pub fn limit_and_offset_unlimited(page: Option<i64>, limit: Option<i64>) -> (i64, i64) {
66   let limit = limit.unwrap_or(FETCH_LIMIT_DEFAULT);
67   let offset = limit * (page.unwrap_or(1) - 1);
68   (limit, offset)
69 }
70
71 pub fn is_email_regex(test: &str) -> bool {
72   EMAIL_REGEX.is_match(test)
73 }
74
75 pub fn diesel_option_overwrite(opt: &Option<String>) -> Option<Option<String>> {
76   match opt {
77     // An empty string is an erase
78     Some(unwrapped) => {
79       if !unwrapped.eq("") {
80         Some(Some(unwrapped.to_owned()))
81       } else {
82         Some(None)
83       }
84     }
85     None => None,
86   }
87 }
88
89 pub fn diesel_option_overwrite_to_url(
90   opt: &Option<String>,
91 ) -> Result<Option<Option<DbUrl>>, LemmyError> {
92   match opt.as_ref().map(|s| s.as_str()) {
93     // An empty string is an erase
94     Some("") => Ok(Some(None)),
95     Some(str_url) => match Url::parse(str_url) {
96       Ok(url) => Ok(Some(Some(url.into()))),
97       Err(e) => Err(LemmyError::from_error_message(e, "invalid_url")),
98     },
99     None => Ok(None),
100   }
101 }
102
103 pub fn diesel_option_overwrite_to_url_create(
104   opt: &Option<String>,
105 ) -> Result<Option<DbUrl>, LemmyError> {
106   match opt.as_ref().map(|s| s.as_str()) {
107     // An empty string is nothing
108     Some("") => Ok(None),
109     Some(str_url) => match Url::parse(str_url) {
110       Ok(url) => Ok(Some(url.into())),
111       Err(e) => Err(LemmyError::from_error_message(e, "invalid_url")),
112     },
113     None => Ok(None),
114   }
115 }
116
117 pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
118
119 pub fn establish_unpooled_connection() -> PgConnection {
120   let db_url = match get_database_url_from_env() {
121     Ok(url) => url,
122     Err(e) => panic!(
123       "Failed to read database URL from env var LEMMY_DATABASE_URL: {}",
124       e
125     ),
126   };
127   let mut conn =
128     PgConnection::establish(&db_url).unwrap_or_else(|_| panic!("Error connecting to {}", db_url));
129   let _ = &mut conn
130     .run_pending_migrations(MIGRATIONS)
131     .unwrap_or_else(|_| panic!("Couldn't run DB Migrations"));
132   conn
133 }
134
135 pub fn naive_now() -> NaiveDateTime {
136   chrono::prelude::Utc::now().naive_utc()
137 }
138
139 pub fn post_to_comment_sort_type(sort: SortType) -> CommentSortType {
140   match sort {
141     SortType::Active | SortType::Hot => CommentSortType::Hot,
142     SortType::New | SortType::NewComments | SortType::MostComments => CommentSortType::New,
143     SortType::Old => CommentSortType::Old,
144     SortType::TopDay
145     | SortType::TopAll
146     | SortType::TopWeek
147     | SortType::TopYear
148     | SortType::TopMonth => CommentSortType::Top,
149   }
150 }
151
152 static EMAIL_REGEX: Lazy<Regex> = Lazy::new(|| {
153   Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
154     .expect("compile email regex")
155 });
156
157 pub mod functions {
158   use diesel::sql_types::*;
159
160   sql_function! {
161     fn hot_rank(score: BigInt, time: Timestamp) -> Integer;
162   }
163
164   sql_function!(fn lower(x: Text) -> Text);
165 }
166
167 impl ToSql<Text, Pg> for DbUrl {
168   fn to_sql(&self, out: &mut Output<Pg>) -> diesel::serialize::Result {
169     <std::string::String as ToSql<Text, Pg>>::to_sql(&self.0.to_string(), &mut out.reborrow())
170   }
171 }
172
173 impl<DB: Backend> FromSql<Text, DB> for DbUrl
174 where
175   String: FromSql<Text, DB>,
176 {
177   fn from_sql(value: diesel::backend::RawValue<'_, DB>) -> diesel::deserialize::Result<Self> {
178     let str = String::from_sql(value)?;
179     Ok(DbUrl(Url::parse(&str)?))
180   }
181 }
182
183 impl<Kind> From<ObjectId<Kind>> for DbUrl
184 where
185   Kind: ApubObject + Send + 'static,
186   for<'de2> <Kind as ApubObject>::ApubType: serde::Deserialize<'de2>,
187 {
188   fn from(id: ObjectId<Kind>) -> Self {
189     DbUrl(id.into())
190   }
191 }
192
193 #[cfg(test)]
194 mod tests {
195   use super::{fuzzy_search, *};
196   use crate::utils::is_email_regex;
197
198   #[test]
199   fn test_fuzzy_search() {
200     let test = "This %is% _a_ fuzzy search";
201     assert_eq!(
202       fuzzy_search(test),
203       "%This%\\%is\\%%\\_a\\_%fuzzy%search%".to_string()
204     );
205   }
206
207   #[test]
208   fn test_email() {
209     assert!(is_email_regex("gush@gmail.com"));
210     assert!(!is_email_regex("nada_neutho"));
211   }
212
213   #[test]
214   fn test_diesel_option_overwrite() {
215     assert_eq!(diesel_option_overwrite(&None), None);
216     assert_eq!(diesel_option_overwrite(&Some("".to_string())), Some(None));
217     assert_eq!(
218       diesel_option_overwrite(&Some("test".to_string())),
219       Some(Some("test".to_string()))
220     );
221   }
222
223   #[test]
224   fn test_diesel_option_overwrite_to_url() {
225     assert!(matches!(diesel_option_overwrite_to_url(&None), Ok(None)));
226     assert!(matches!(
227       diesel_option_overwrite_to_url(&Some("".to_string())),
228       Ok(Some(None))
229     ));
230     assert!(matches!(
231       diesel_option_overwrite_to_url(&Some("invalid_url".to_string())),
232       Err(_)
233     ));
234     let example_url = "https://example.com";
235     assert!(matches!(
236       diesel_option_overwrite_to_url(&Some(example_url.to_string())),
237       Ok(Some(Some(url))) if url == Url::parse(example_url).unwrap().into()
238     ));
239   }
240 }