backend/model/entity/
seed_impl.rs

1//! Contains the implementation of [`Seed`].
2
3use diesel::pg::Pg;
4use diesel::{debug_query, BoolExpressionMethods, ExpressionMethods, QueryDsl, QueryResult};
5use diesel_async::{AsyncPgConnection, RunQueryDsl};
6use log::debug;
7use uuid::Uuid;
8
9use crate::db::{
10    function::{array_to_string, greatest4, similarity},
11    pagination::Paginate,
12};
13use crate::model::dto::{Page, PageParameters, SeedSearchParameters};
14use crate::{
15    model::dto::SeedDto,
16    schema::{
17        plants::{self, common_name_de, common_name_en, unique_name},
18        seeds::{self, all_columns, created_by, harvest_year, name, use_by},
19    },
20};
21
22use crate::model::r#enum::include_archived_seeds::IncludeArchivedSeeds;
23use chrono::NaiveDateTime;
24use diesel::dsl::sql;
25use diesel::sql_types::Float;
26
27use super::{NewSeed, Seed};
28
29impl Seed {
30    /// Get a page of seeds.
31    ///
32    /// `search_parameters.name` filters seeds by their full names (as defined in the documentation).
33    /// `search_parameters.harvest_year` will only include seeds with a specific harvest year.
34    /// `search_parameters.archived` specifies if archived seeds, non archived seeds or both kinds
35    /// should be part of the results.
36    /// By default, archived seeds will not be returned.
37    ///
38    /// If `search_parameters.name` is set, seeds will be ordered by how similar they are to the
39    /// `search_parameters.name`.
40    /// Otherwise, seeds are returned in ascending order of their `use_by` and `harvest_year` dates.
41    ///
42    ///
43    /// # Errors
44    /// * Unknown, diesel doesn't say why it might error.
45    pub async fn find(
46        search_parameters: SeedSearchParameters,
47        user_id: Uuid,
48        page_parameters: PageParameters,
49        conn: &mut AsyncPgConnection,
50    ) -> QueryResult<Page<SeedDto>> {
51        // Diesel allows only one select call per query.
52        // We therefore always include similarity measures for the complete name,
53        // even if we don't need them.
54        let search_query = search_parameters.name.as_deref().unwrap_or("");
55
56        let mut query = seeds::table
57            .inner_join(plants::table)
58            .select((
59                greatest4(
60                    similarity(name, search_query),
61                    similarity(unique_name, search_query),
62                    similarity(array_to_string(common_name_de, " "), search_query),
63                    similarity(array_to_string(common_name_en, " "), search_query),
64                ),
65                all_columns,
66            ))
67            .into_boxed();
68
69        if let Some(harvest_year_search) = search_parameters.harvest_year {
70            query = query.filter(harvest_year.eq(harvest_year_search));
71        }
72
73        if search_parameters.name.is_some() {
74            query = query
75                .filter(
76                    similarity(name, search_query)
77                        .gt(0.1)
78                        .or(similarity(array_to_string(common_name_de, " "), search_query).gt(0.1))
79                        .or(similarity(array_to_string(common_name_en, " "), search_query).gt(0.1))
80                        .or(similarity(unique_name, search_query).gt(0.1)),
81                )
82                // Order seeds by how similar they are to the search term,
83                // if one was set.
84                .order(sql::<Float>("1").desc());
85        } else {
86            // By default, seeds should be ordered by either use_by date or harvest year.
87            query = query.order((harvest_year.asc(), use_by.asc()));
88        }
89
90        let include_archived = search_parameters
91            .archived
92            .unwrap_or(IncludeArchivedSeeds::NotArchived);
93
94        // Don't filter the query if IncludeArchivedSeeds::Both is selected.
95        if include_archived == IncludeArchivedSeeds::Archived {
96            query = query.filter(seeds::archived_at.is_not_null());
97        } else if include_archived == IncludeArchivedSeeds::NotArchived {
98            query = query.filter(seeds::archived_at.is_null());
99        }
100
101        // Only return seeds that belong to the user.
102        query = query.filter(created_by.eq(user_id));
103
104        let query = query
105            .paginate(page_parameters.page)
106            .per_page(page_parameters.per_page);
107        debug!("{}", debug_query::<Pg, _>(&query));
108        query
109            .load_page::<(f32, Self)>(conn)
110            .await
111            .map(Page::from_entity)
112    }
113
114    /// Fetch seed by id from the database.
115    ///
116    /// # Errors
117    /// * Unknown, diesel doesn't say why it might error.
118    pub async fn find_by_id(
119        id: i64,
120        user_id: Uuid,
121        conn: &mut AsyncPgConnection,
122    ) -> QueryResult<SeedDto> {
123        let mut query = seeds::table.select(all_columns).into_boxed();
124
125        // Only return seeds that belong to the user.
126        query = query.filter(created_by.eq(user_id).and(seeds::id.eq(id)));
127
128        debug!("{}", debug_query::<Pg, _>(&query));
129        query.first::<Self>(conn).await.map(Into::into)
130    }
131
132    /// Create a new seed in the database.
133    ///
134    /// # Errors
135    /// * Unknown, diesel doesn't say why it might error.
136    pub async fn create(new_seed: NewSeed, conn: &mut AsyncPgConnection) -> QueryResult<SeedDto> {
137        let query = diesel::insert_into(seeds::table).values(&new_seed);
138        debug!("{}", debug_query::<Pg, _>(&query));
139        query.get_result::<Self>(conn).await.map(Into::into)
140    }
141
142    /// Edits a seed in the database.
143    ///
144    /// # Errors
145    /// * Unknown, diesel doesn't say why it might error.
146    pub async fn edit(
147        id: i64,
148        new_seed: NewSeed,
149        conn: &mut AsyncPgConnection,
150    ) -> QueryResult<SeedDto> {
151        let query_result = diesel::update(seeds::table.filter(seeds::id.eq(id)))
152            .set((
153                seeds::name.eq(new_seed.name),
154                seeds::harvest_year.eq(new_seed.harvest_year),
155                seeds::plant_id.eq(new_seed.plant_id),
156                seeds::use_by.eq(new_seed.use_by),
157                seeds::origin.eq(new_seed.origin),
158                seeds::taste.eq(new_seed.taste),
159                seeds::yield_.eq(new_seed.yield_),
160                seeds::generation.eq(new_seed.generation),
161                seeds::price.eq(new_seed.price),
162                seeds::notes.eq(new_seed.notes),
163                seeds::quantity.eq(new_seed.quantity),
164                seeds::quality.eq(new_seed.quality),
165            ))
166            .get_result::<Self>(conn)
167            .await;
168        query_result.map(Into::into)
169    }
170
171    /// Delete the seed from the database.
172    ///
173    /// # Errors
174    /// * Unknown, diesel doesn't say why it might error.
175    pub async fn delete_by_id(
176        id: i64,
177        user_id: Uuid,
178        conn: &mut AsyncPgConnection,
179    ) -> QueryResult<usize> {
180        // Only delete seeds that belong to the user.
181        let source = seeds::table.filter(created_by.eq(user_id).and(seeds::id.eq(id)));
182
183        let query = diesel::delete(source);
184        debug!("{}", debug_query::<Pg, _>(&query));
185        query.execute(conn).await
186    }
187
188    /// Archive or unarchive a seed in the database.
189    ///
190    /// # Errors
191    /// If the connection to the database could not be established.
192    pub async fn archive(
193        id: i64,
194        archived_at: Option<NaiveDateTime>,
195        user_id: Uuid,
196        conn: &mut AsyncPgConnection,
197    ) -> QueryResult<SeedDto> {
198        let source = seeds::table.filter(created_by.eq(user_id).and(seeds::id.eq(id)));
199
200        let query_result = diesel::update(source)
201            .set(seeds::archived_at.eq(archived_at))
202            .get_result::<Self>(conn)
203            .await;
204        query_result.map(Into::into)
205    }
206}