backend/model/entity/
map_impl.rs

1//! Contains the implementation of [`Map`].
2
3use chrono::{Datelike, Months, NaiveDate, NaiveDateTime, Utc};
4use diesel::dsl::case_when;
5use diesel::dsl::{exists, sql};
6use diesel::pg::Pg;
7use diesel::sql_types::{Float, Integer};
8use diesel::{
9    debug_query, select, BoolExpressionMethods, ExpressionMethods, PgTextExpressionMethods,
10    QueryDsl, QueryResult,
11};
12use diesel_async::{AsyncPgConnection, RunQueryDsl};
13use log::debug;
14use uuid::Uuid;
15
16use crate::config::auth::user_info::UserInfo;
17use crate::db::function::{similarity, PgTrgmExpressionMethods};
18use crate::db::pagination::Paginate;
19use crate::model::dto::{
20    MapSearchParameters, Page, PageParameters, UpdateMapDto, UpdateMapGeometryDto,
21};
22use crate::model::entity::{UpdateMap, UpdateMapGeometry};
23use crate::{
24    model::dto::{MapDto, NewMapDto},
25    schema::maps::{self, all_columns, deletion_date, name},
26};
27
28use super::{Map, NewMap};
29
30impl Map {
31    #[cfg(not(feature = "access_control"))]
32    /// Get the maps matching the search query.
33    ///
34    /// Sorting is done in three steps:
35    /// 1. Priority order (Owned maps first, then collaborating maps and then everything else)
36    /// 2. Similarity to the search query (if provided).
37    /// 3. Modified Date
38    ///
39    /// The later criteria are only considered if the earlier criteria fail to establish an unambiguous order.
40    ///
41    /// Uses `pg_trgm` to find matches in `name`.
42    /// Ranks using the `pg_trgm` function `similarity()`.
43    ///
44    /// # Errors
45    /// * Unknown, diesel doesn't say why it might error.
46    pub async fn find(
47        search_parameters: MapSearchParameters,
48        page_parameters: PageParameters,
49        conn: &mut AsyncPgConnection,
50        _user_info: UserInfo,
51        _collaborating_in: Vec<i32>,
52    ) -> QueryResult<Page<MapDto>> {
53        let mut query = maps::table
54            .select((
55                similarity(name, search_parameters.name.clone().unwrap_or_default()),
56                all_columns,
57            ))
58            .into_boxed();
59
60        if let Some(search_query) = &search_parameters.name {
61            if !search_query.is_empty() {
62                query = query.filter(
63                    name.fuzzy(search_query)
64                        .or(name.ilike(format!("%{search_query}%"))),
65                );
66            }
67        }
68
69        // Labels the rows of the query by the relevancy of the map for the user.
70        let priority_order = case_when(maps::created_by.eq(&user_info.id), 0.into_sql::<Integer>())
71            .when(maps::id.eq_any(&collaborating_in), 1.into_sql::<Integer>())
72            .otherwise(2.into_sql::<Integer>());
73
74        let query = query
75            .filter(deletion_date.is_null())
76            .order_by(priority_order.asc())
77            .then_order_by(sql::<Float>("1").desc())
78            .then_order_by(map::modified_at.desc())
79            .paginate(page_parameters.page)
80            .per_page(page_parameters.per_page);
81        debug!("{}", debug_query::<Pg, _>(&query));
82        query
83            .load_page::<(f32, Self)>(conn)
84            .await
85            .map(Page::from_entity)
86    }
87
88    #[cfg(feature = "access_control")]
89    /// Get the maps matching the search query.
90    ///
91    /// Sorting is done in three steps:
92    /// 1. Priority order (Owned maps first, then collaborating maps and then everything else)
93    /// 2. Similarity to the search query (if provided).
94    /// 3. Modified Date
95    ///
96    /// The later criteria are only considered if the earlier criteria fail to establish an unambiguous order.
97    ///
98    /// Uses `pg_trgm` to find matches in `name`.
99    /// Ranks using the `pg_trgm` function `similarity()`.
100    ///
101    /// # Errors
102    /// * Unknown, diesel doesn't say why it might error.
103    pub async fn find(
104        search_parameters: MapSearchParameters,
105        page_parameters: PageParameters,
106        conn: &mut AsyncPgConnection,
107        user_info: UserInfo,
108        collaborating_in: Vec<i64>,
109    ) -> QueryResult<Page<MapDto>> {
110        use diesel::IntoSql;
111
112        use crate::model::r#enum::privacy_access_control::PrivacyAccessControl;
113        use crate::schema::maps::dsl as map;
114        let mut query = maps::table
115            .select((
116                similarity(name, search_parameters.name.clone().unwrap_or_default()),
117                all_columns,
118            ))
119            .into_boxed();
120
121        if let Some(search_query) = &search_parameters.name {
122            if !search_query.is_empty() {
123                query = query.filter(
124                    name.fuzzy(search_query)
125                        .or(name.ilike(format!("%{search_query}%"))),
126                );
127            }
128        }
129
130        if !user_info.is_admin() {
131            query = query.filter(
132                map::created_by
133                    .eq(user_info.id)
134                    .or(map::id.eq_any(collaborating_in.clone()))
135                    .or(map::privacy.eq(PrivacyAccessControl::Public)),
136            );
137            if user_info.is_member() {
138                query = query.or_filter(map::privacy.eq(PrivacyAccessControl::Protected));
139            }
140        }
141
142        // Labels the rows of the query by the relevancy of the map for the user.
143        let priority_order = case_when(maps::created_by.eq(&user_info.id), 0.into_sql::<Integer>())
144            .when(maps::id.eq_any(&collaborating_in), 1.into_sql::<Integer>())
145            .otherwise(2.into_sql::<Integer>());
146
147        let query = query
148            .filter(deletion_date.is_null())
149            .order_by(priority_order.asc())
150            .then_order_by(sql::<Float>("1").desc())
151            .then_order_by(map::modified_at.desc())
152            .paginate(page_parameters.page)
153            .per_page(page_parameters.per_page);
154        debug!("{}", debug_query::<Pg, _>(&query));
155        query
156            .load_page::<(f32, Self)>(conn)
157            .await
158            .map(Page::from_entity)
159    }
160
161    /// Fetch map by id from the database.
162    ///
163    /// # Errors
164    /// * Unknown, diesel doesn't say why it might error.
165    pub async fn find_by_id(id: i64, conn: &mut AsyncPgConnection) -> QueryResult<MapDto> {
166        let query = maps::table.find(id).filter(deletion_date.is_null());
167        debug!("{}", debug_query::<Pg, _>(&query));
168        query.first::<Self>(conn).await.map(Into::into)
169    }
170
171    /// Checks if a map with this name already exists in the database.
172    ///
173    /// # Errors
174    /// * Unknown, diesel doesn't say why it might error.
175    pub async fn is_name_taken(map_name: &str, conn: &mut AsyncPgConnection) -> QueryResult<bool> {
176        let query = select(exists(maps::table.filter(name.eq(map_name))));
177        debug!("{}", debug_query::<Pg, _>(&query));
178        query.get_result(conn).await
179    }
180
181    /// Create a new map in the database.
182    ///
183    /// # Errors
184    /// * Unknown, diesel doesn't say why it might error.
185    pub async fn create(
186        new_map: NewMapDto,
187        user_id: Uuid,
188        conn: &mut AsyncPgConnection,
189    ) -> QueryResult<MapDto> {
190        let new_map = NewMap::from((new_map, user_id));
191        let query = diesel::insert_into(maps::table).values(&new_map);
192        debug!("{}", debug_query::<Pg, _>(&query));
193        query.get_result::<Self>(conn).await.map(Into::into)
194    }
195
196    /// Update a map in the database.
197    ///
198    /// # Errors
199    /// * Unknown, diesel doesn't say why it might error.
200    pub async fn update(
201        map_update: UpdateMapDto,
202        id: i64,
203        conn: &mut AsyncPgConnection,
204    ) -> QueryResult<MapDto> {
205        let map_update = UpdateMap::from(map_update);
206        let query = diesel::update(maps::table.find(id)).set(&map_update);
207        debug!("{}", debug_query::<Pg, _>(&query));
208        query.get_result::<Self>(conn).await.map(Into::into)
209    }
210
211    /// Marks a map for deletion using the systems current date and time.
212    ///
213    /// # Errors
214    /// * Unknown, diesel doesn't say why it might error.
215    pub async fn mark_for_deletion(id: i64, conn: &mut AsyncPgConnection) -> QueryResult<MapDto> {
216        let now = Utc::now().naive_utc();
217        let in_one_month = now + Months::new(1);
218        // Indeed, there is no automatic conversion from NaiveDateTime to NaiveDate for some reason.
219        let in_one_month_as_naive_date = NaiveDate::from_ymd_opt(
220            in_one_month.year(),
221            in_one_month.month(),
222            in_one_month.day(),
223        );
224
225        let map = Self::find_by_id(id, conn).await?;
226
227        let query = diesel::update(maps::table.find(id)).set((
228            deletion_date.eq(in_one_month_as_naive_date),
229            // Prevent deleted maps causing issues because their name still exists.
230            name.eq(format!("{} __DELETED {}", map.name, now)),
231        ));
232        debug!("{}", debug_query::<Pg, _>(&query));
233        query.get_result::<Self>(conn).await.map(Into::into)
234    }
235
236    /// Update a maps bounds in the database.
237    ///
238    /// # Errors
239    /// * Unknown, diesel doesn't say why it might error.
240    pub async fn update_geometry(
241        map_update_bounds: UpdateMapGeometryDto,
242        id: i64,
243        conn: &mut AsyncPgConnection,
244    ) -> QueryResult<MapDto> {
245        let map_update = UpdateMapGeometry::from(map_update_bounds);
246        let query = diesel::update(maps::table.find(id)).set(&map_update);
247        debug!("{}", debug_query::<Pg, _>(&query));
248        query.get_result::<Self>(conn).await.map(Into::into)
249    }
250
251    /// Update modified metadata (`modified_at`, `modified_by`) of the map.
252    ///
253    /// # Errors
254    /// * Unknown, diesel doesn't say why it might error.
255    pub async fn update_modified_metadata(
256        id: i64,
257        user_id: Uuid,
258        time: NaiveDateTime,
259        conn: &mut AsyncPgConnection,
260    ) -> QueryResult<()> {
261        diesel::update(maps::table.find(id))
262            .set((maps::modified_at.eq(time), maps::modified_by.eq(user_id)))
263            .execute(conn)
264            .await?;
265        Ok(())
266    }
267}