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 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: i32,
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(
137        new_seed: NewSeedDto,
138        user_id: Uuid,
139        conn: &mut AsyncPgConnection,
140    ) -> QueryResult<SeedDto> {
141        let new_seed = NewSeed::from((new_seed, user_id));
142        let query = diesel::insert_into(seeds::table).values(&new_seed);
143        debug!("{}", debug_query::<Pg, _>(&query));
144        query.get_result::<Self>(conn).await.map(Into::into)
145    }
146
147    /// Edits a seed in the database.
148    ///
149    /// # Errors
150    /// * Unknown, diesel doesn't say why it might error.
151    pub async fn edit(
152        id: i32,
153        user_id: Uuid,
154        new_seed: NewSeedDto,
155        conn: &mut AsyncPgConnection,
156    ) -> QueryResult<SeedDto> {
157        let new_seed = NewSeed::from((new_seed, user_id));
158        let query_result = diesel::update(seeds::table.filter(seeds::id.eq(id)))
159            .set((
160                seeds::name.eq(new_seed.name),
161                seeds::harvest_year.eq(new_seed.harvest_year),
162                seeds::plant_id.eq(new_seed.plant_id),
163                seeds::use_by.eq(new_seed.use_by),
164                seeds::origin.eq(new_seed.origin),
165                seeds::taste.eq(new_seed.taste),
166                seeds::yield_.eq(new_seed.yield_),
167                seeds::generation.eq(new_seed.generation),
168                seeds::price.eq(new_seed.price),
169                seeds::notes.eq(new_seed.notes),
170                seeds::quantity.eq(new_seed.quantity),
171                seeds::quality.eq(new_seed.quality),
172            ))
173            .get_result::<Self>(conn)
174            .await;
175        query_result.map(Into::into)
176    }
177
178    /// Delete the seed from the database.
179    ///
180    /// # Errors
181    /// * Unknown, diesel doesn't say why it might error.
182    pub async fn delete_by_id(
183        id: i32,
184        user_id: Uuid,
185        conn: &mut AsyncPgConnection,
186    ) -> QueryResult<usize> {
187        // Only delete seeds that belong to the user.
188        let source = seeds::table.filter(created_by.eq(user_id).and(seeds::id.eq(id)));
189
190        let query = diesel::delete(source);
191        debug!("{}", debug_query::<Pg, _>(&query));
192        query.execute(conn).await
193    }
194
195    /// Archive or unarchive a seed in the database.
196    ///
197    /// # Errors
198    /// If the connection to the database could not be established.
199    pub async fn archive(
200        id: i32,
201        archived_at: Option<NaiveDateTime>,
202        user_id: Uuid,
203        conn: &mut AsyncPgConnection,
204    ) -> QueryResult<SeedDto> {
205        let source = seeds::table.filter(created_by.eq(user_id).and(seeds::id.eq(id)));
206
207        let query_result = diesel::update(source)
208            .set(seeds::archived_at.eq(archived_at))
209            .get_result::<Self>(conn)
210            .await;
211        query_result.map(Into::into)
212    }
213}