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::db::function::{similarity, PgTrgmExpressionMethods};
16use crate::db::pagination::Paginate;
17use crate::model::dto::{
18    MapSearchParameters, Page, PageParameters, UpdateMapDto, UpdateMapGeometryDto,
19};
20use crate::model::entity::{UpdateMap, UpdateMapGeometry};
21use crate::{
22    model::dto::{MapDto, NewMapDto},
23    schema::maps::{self, all_columns, created_by, deletion_date, is_inactive, name, privacy},
24};
25
26use super::{Map, NewMap};
27
28impl Map {
29    /// Get the top maps matching the search query.
30    ///
31    /// Can be filtered by `is_inactive` and `created_by` if provided in `search_parameters`.
32    /// This will be done with equals and is additional functionality for maps (when compared to plant search).
33    ///
34    /// Uses `pg_trgm` to find matches in `name`.
35    /// Ranks using the `pg_trgm` function `similarity()`.
36    ///
37    /// # Errors
38    /// * Unknown, diesel doesn't say why it might error.
39    pub async fn find(
40        search_parameters: MapSearchParameters,
41        page_parameters: PageParameters,
42        conn: &mut AsyncPgConnection,
43    ) -> QueryResult<Page<MapDto>> {
44        let mut query = maps::table
45            .select((
46                similarity(name, search_parameters.name.clone().unwrap_or_default()),
47                all_columns,
48            ))
49            .into_boxed();
50
51        if let Some(search_query) = &search_parameters.name {
52            if !search_query.is_empty() {
53                query = query.filter(
54                    name.fuzzy(search_query)
55                        .or(name.ilike(format!("%{search_query}%"))),
56                );
57            }
58        }
59        if let Some(is_inactive_search) = search_parameters.is_inactive {
60            query = query.filter(is_inactive.eq(is_inactive_search));
61        }
62        if let Some(privacy_search) = search_parameters.privacy {
63            query = query.filter(privacy.eq(privacy_search));
64        }
65        if let Some(created_by_search) = search_parameters.created_by {
66            query = query.filter(created_by.eq(created_by_search));
67        }
68
69        let query = query
70            .filter(deletion_date.is_null())
71            .order(sql::<Float>("1").desc())
72            .paginate(page_parameters.page)
73            .per_page(page_parameters.per_page);
74        debug!("{}", debug_query::<Pg, _>(&query));
75        query
76            .load_page::<(f32, Self)>(conn)
77            .await
78            .map(Page::from_entity)
79    }
80
81    /// Fetch map by id from the database.
82    ///
83    /// # Errors
84    /// * Unknown, diesel doesn't say why it might error.
85    pub async fn find_by_id(id: i32, conn: &mut AsyncPgConnection) -> QueryResult<MapDto> {
86        let query = maps::table.find(id).filter(deletion_date.is_null());
87        debug!("{}", debug_query::<Pg, _>(&query));
88        query.first::<Self>(conn).await.map(Into::into)
89    }
90
91    /// Checks if a map with this name already exists in the database.
92    ///
93    /// # Errors
94    /// * Unknown, diesel doesn't say why it might error.
95    pub async fn is_name_taken(map_name: &str, conn: &mut AsyncPgConnection) -> QueryResult<bool> {
96        let query = select(exists(maps::table.filter(name.eq(map_name))));
97        debug!("{}", debug_query::<Pg, _>(&query));
98        query.get_result(conn).await
99    }
100
101    /// Create a new map in the database.
102    ///
103    /// # Errors
104    /// * Unknown, diesel doesn't say why it might error.
105    pub async fn create(
106        new_map: NewMapDto,
107        user_id: Uuid,
108        conn: &mut AsyncPgConnection,
109    ) -> QueryResult<MapDto> {
110        let new_map = NewMap::from((new_map, user_id));
111        let query = diesel::insert_into(maps::table).values(&new_map);
112        debug!("{}", debug_query::<Pg, _>(&query));
113        query.get_result::<Self>(conn).await.map(Into::into)
114    }
115
116    /// Update a map in the database.
117    ///
118    /// # Errors
119    /// * Unknown, diesel doesn't say why it might error.
120    pub async fn update(
121        map_update: UpdateMapDto,
122        id: i32,
123        conn: &mut AsyncPgConnection,
124    ) -> QueryResult<MapDto> {
125        let map_update = UpdateMap::from(map_update);
126        let query = diesel::update(maps::table.find(id)).set(&map_update);
127        debug!("{}", debug_query::<Pg, _>(&query));
128        query.get_result::<Self>(conn).await.map(Into::into)
129    }
130
131    /// Marks a map for deletion using the systems current date and time.
132    ///
133    /// # Errors
134    /// * Unknown, diesel doesn't say why it might error.
135    pub async fn mark_for_deletion(id: i32, conn: &mut AsyncPgConnection) -> QueryResult<MapDto> {
136        let now = Utc::now().naive_utc();
137        let in_one_month = now + Months::new(1);
138        // Indeed, there is no automatic conversion from NaiveDateTime to NaiveDate for some reason.
139        let in_one_month_as_naive_date = NaiveDate::from_ymd_opt(
140            in_one_month.year(),
141            in_one_month.month(),
142            in_one_month.day(),
143        );
144
145        let map = Self::find_by_id(id, conn).await?;
146
147        let query = diesel::update(maps::table.find(id)).set((
148            deletion_date.eq(in_one_month_as_naive_date),
149            // Prevent deleted maps causing issues because their name still exists.
150            name.eq(format!("{} __DELETED {}", map.name, now)),
151        ));
152        debug!("{}", debug_query::<Pg, _>(&query));
153        query.get_result::<Self>(conn).await.map(Into::into)
154    }
155
156    /// Update a maps bounds in the database.
157    ///
158    /// # Errors
159    /// * Unknown, diesel doesn't say why it might error.
160    pub async fn update_geometry(
161        map_update_bounds: UpdateMapGeometryDto,
162        id: i32,
163        conn: &mut AsyncPgConnection,
164    ) -> QueryResult<MapDto> {
165        let map_update = UpdateMapGeometry::from(map_update_bounds);
166        let query = diesel::update(maps::table.find(id)).set(&map_update);
167        debug!("{}", debug_query::<Pg, _>(&query));
168        query.get_result::<Self>(conn).await.map(Into::into)
169    }
170
171    /// Update modified metadata (`modified_at`, `modified_by`) of the map.
172    ///
173    /// # Errors
174    /// * Unknown, diesel doesn't say why it might error.
175    pub async fn update_modified_metadata(
176        id: i32,
177        user_id: Uuid,
178        time: NaiveDateTime,
179        conn: &mut AsyncPgConnection,
180    ) -> QueryResult<()> {
181        diesel::update(maps::table.find(id))
182            .set((maps::modified_at.eq(time), maps::modified_by.eq(user_id)))
183            .execute(conn)
184            .await?;
185        Ok(())
186    }
187}