backend/model/entity/
map_impl.rs

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