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::{NewSeedDto, 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 mut search_query = &String::new();
55        if let Some(name_search) = &search_parameters.name {
56            search_query = name_search;
57        }
58
59        let mut query = seeds::table
60            .inner_join(plants::table)
61            .select((
62                greatest4(
63                    similarity(name, search_query),
64                    similarity(unique_name, search_query),
65                    similarity(array_to_string(common_name_de, " "), search_query),
66                    similarity(array_to_string(common_name_en, " "), search_query),
67                ),
68                all_columns,
69            ))
70            .into_boxed();
71
72        if let Some(harvest_year_search) = search_parameters.harvest_year {
73            query = query.filter(harvest_year.eq(harvest_year_search));
74        }
75
76        if search_parameters.name.is_some() {
77            query = query
78                .filter(
79                    similarity(name, search_query)
80                        .gt(0.1)
81                        .or(similarity(array_to_string(common_name_de, " "), search_query).gt(0.1))
82                        .or(similarity(array_to_string(common_name_en, " "), search_query).gt(0.1))
83                        .or(similarity(unique_name, search_query).gt(0.1)),
84                )
85                // Order seeds by how similar they are to the search term,
86                // if one was set.
87                .order(sql::<Float>("1").desc());
88        } else {
89            // By default, seeds should be ordered by either use_by date or harvest year.
90            query = query.order((harvest_year.asc(), use_by.asc()));
91        }
92
93        let mut include_archived = IncludeArchivedSeeds::NotArchived;
94        if let Some(include_archived_) = search_parameters.archived {
95            include_archived = include_archived_;
96        }
97
98        // Don't filter the query if IncludeArchivedSeeds::Both is selected.
99        if include_archived == IncludeArchivedSeeds::Archived {
100            query = query.filter(seeds::archived_at.is_not_null());
101        } else if include_archived == IncludeArchivedSeeds::NotArchived {
102            query = query.filter(seeds::archived_at.is_null());
103        }
104
105        // Only return seeds that belong to the user.
106        query = query.filter(created_by.eq(user_id));
107
108        let query = query
109            .paginate(page_parameters.page)
110            .per_page(page_parameters.per_page);
111        debug!("{}", debug_query::<Pg, _>(&query));
112        query
113            .load_page::<(f32, Self)>(conn)
114            .await
115            .map(Page::from_entity)
116    }
117
118    /// Fetch seed by id from the database.
119    ///
120    /// # Errors
121    /// * Unknown, diesel doesn't say why it might error.
122    pub async fn find_by_id(
123        id: i32,
124        user_id: Uuid,
125        conn: &mut AsyncPgConnection,
126    ) -> QueryResult<SeedDto> {
127        let mut query = seeds::table.select(all_columns).into_boxed();
128
129        // Only return seeds that belong to the user.
130        query = query.filter(created_by.eq(user_id).and(seeds::id.eq(id)));
131
132        debug!("{}", debug_query::<Pg, _>(&query));
133        query.first::<Self>(conn).await.map(Into::into)
134    }
135
136    /// Create a new seed in the database.
137    ///
138    /// # Errors
139    /// * Unknown, diesel doesn't say why it might error.
140    pub async fn create(
141        new_seed: NewSeedDto,
142        user_id: Uuid,
143        conn: &mut AsyncPgConnection,
144    ) -> QueryResult<SeedDto> {
145        let new_seed = NewSeed::from((new_seed, user_id));
146        let query = diesel::insert_into(seeds::table).values(&new_seed);
147        debug!("{}", debug_query::<Pg, _>(&query));
148        query.get_result::<Self>(conn).await.map(Into::into)
149    }
150
151    /// Edits a seed in the database.
152    ///
153    /// # Errors
154    /// * Unknown, diesel doesn't say why it might error.
155    pub async fn edit(
156        id: i32,
157        user_id: Uuid,
158        new_seed: NewSeedDto,
159        conn: &mut AsyncPgConnection,
160    ) -> QueryResult<SeedDto> {
161        let new_seed = NewSeed::from((new_seed, user_id));
162        let query_result = diesel::update(seeds::table.filter(seeds::id.eq(id)))
163            .set((
164                seeds::name.eq(new_seed.name),
165                seeds::harvest_year.eq(new_seed.harvest_year),
166                seeds::plant_id.eq(new_seed.plant_id),
167                seeds::use_by.eq(new_seed.use_by),
168                seeds::origin.eq(new_seed.origin),
169                seeds::taste.eq(new_seed.taste),
170                seeds::yield_.eq(new_seed.yield_),
171                seeds::generation.eq(new_seed.generation),
172                seeds::price.eq(new_seed.price),
173                seeds::notes.eq(new_seed.notes),
174                seeds::quantity.eq(new_seed.quantity),
175                seeds::quality.eq(new_seed.quality),
176            ))
177            .get_result::<Self>(conn)
178            .await;
179        query_result.map(Into::into)
180    }
181
182    /// Delete the seed from the database.
183    ///
184    /// # Errors
185    /// * Unknown, diesel doesn't say why it might error.
186    pub async fn delete_by_id(
187        id: i32,
188        user_id: Uuid,
189        conn: &mut AsyncPgConnection,
190    ) -> QueryResult<usize> {
191        // Only delete seeds that belong to the user.
192        let source = seeds::table.filter(created_by.eq(user_id).and(seeds::id.eq(id)));
193
194        let query = diesel::delete(source);
195        debug!("{}", debug_query::<Pg, _>(&query));
196        query.execute(conn).await
197    }
198
199    /// Archive or unarchive a seed in the database.
200    ///
201    /// # Errors
202    /// If the connection to the database could not be established.
203    pub async fn archive(
204        id: i32,
205        archived_at: Option<NaiveDateTime>,
206        user_id: Uuid,
207        conn: &mut AsyncPgConnection,
208    ) -> QueryResult<SeedDto> {
209        let source = seeds::table.filter(created_by.eq(user_id).and(seeds::id.eq(id)));
210
211        let query_result = diesel::update(source)
212            .set(seeds::archived_at.eq(archived_at))
213            .get_result::<Self>(conn)
214            .await;
215        query_result.map(Into::into)
216    }
217}