airlab_lib/model/
tag.rs

1use crate::ctx::Ctx;
2use crate::model::ModelManager;
3use crate::model::Result;
4use crate::model::base::{self, DbBmc};
5use modql::field::Fields;
6use modql::filter::{FilterNodes, ListOptions, OpValsInt64, OpValsString};
7use serde::{Deserialize, Serialize};
8use sqlx::FromRow;
9
10impl TagBmc {
11    #[must_use]
12    pub fn get_create_sql(drop_table: bool) -> String {
13        let table = Self::TABLE;
14        format!(
15            r##"{}
16create table if not exists "{table}" (
17  id serial primary key,
18  group_id integer NOT NULL,
19  name character varying NOT NULL,
20  description character varying,
21  is_metal boolean DEFAULT false NOT NULL,
22  is_fluorophore boolean DEFAULT false NOT NULL,
23  is_enzyme boolean DEFAULT false NOT NULL,
24  is_biotin boolean DEFAULT false NOT NULL,
25  is_other boolean DEFAULT false NOT NULL,
26  mw smallint,
27  emission smallint,
28  excitation smallint,
29  status smallint DEFAULT 0 NOT NULL,
30  meta jsonb,
31  created_at timestamp with time zone DEFAULT now() NOT NULL
32);
33ALTER TABLE ONLY tag
34  ADD CONSTRAINT "UQ_tag_group_id_and_name_and_mw" UNIQUE (group_id, name, mw);
35CREATE INDEX "IDX_tag_group_id" ON tag USING btree (group_id);
36        "##,
37            if drop_table {
38                format!("drop table if exists {table};")
39            } else {
40                String::new()
41            }
42        )
43    }
44}
45
46#[allow(clippy::struct_excessive_bools)]
47#[derive(Debug, Clone, Fields, FromRow, Serialize, Deserialize, Default)]
48pub struct Tag {
49    pub id: i32,
50    #[serde(rename = "groupId")]
51    pub group_id: i32,
52    pub name: String,
53    pub description: Option<String>,
54    #[serde(rename = "isMetal")]
55    pub is_metal: bool,
56    #[serde(rename = "isFluorophore")]
57    pub is_fluorophore: bool,
58    #[serde(rename = "isEnzyme")]
59    pub is_enzyme: bool,
60    #[serde(rename = "isBiotin")]
61    pub is_biotin: bool,
62    #[serde(rename = "isOther")]
63    pub is_other: bool,
64    pub mw: Option<i16>,
65    pub emission: Option<i16>,
66    pub excitation: Option<i16>,
67    pub status: Option<i16>,
68    pub meta: Option<serde_json::Value>,
69    #[serde(rename = "createdAt")]
70    pub created_at: chrono::DateTime<chrono::Utc>,
71}
72
73#[allow(clippy::struct_excessive_bools)]
74#[derive(Fields, Deserialize, Clone, Debug)]
75pub struct TagForCreate {
76    pub name: String,
77    #[serde(rename = "groupId")]
78    pub group_id: i32,
79    pub description: Option<String>,
80    #[serde(rename = "isMetal")]
81    pub is_metal: bool,
82    #[serde(rename = "isFluorophore")]
83    pub is_fluorophore: bool,
84    #[serde(rename = "isEnzyme")]
85    pub is_enzyme: bool,
86    #[serde(rename = "isBiotin")]
87    pub is_biotin: bool,
88    #[serde(rename = "isOther")]
89    pub is_other: bool,
90    pub mw: Option<i16>,
91    pub emission: Option<i16>,
92    pub excitation: Option<i16>,
93    pub status: Option<i16>,
94}
95
96#[allow(clippy::struct_excessive_bools)]
97#[derive(Fields, Default, Deserialize, Debug)]
98pub struct TagForUpdate {
99    pub name: String,
100    pub description: Option<String>,
101    #[serde(rename = "isMetal")]
102    pub is_metal: bool,
103    #[serde(rename = "isFluorophore")]
104    pub is_fluorophore: bool,
105    #[serde(rename = "isEnzyme")]
106    pub is_enzyme: bool,
107    #[serde(rename = "isBiotin")]
108    pub is_biotin: bool,
109    #[serde(rename = "isOther")]
110    pub is_other: bool,
111    pub mw: Option<i16>,
112    pub emission: Option<i16>,
113    pub excitation: Option<i16>,
114    pub status: Option<i16>,
115}
116
117#[derive(FilterNodes, Deserialize, Default, Debug)]
118pub struct TagFilter {
119    id: Option<OpValsInt64>,
120    group_id: Option<OpValsInt64>,
121
122    name: Option<OpValsString>,
123}
124
125pub struct TagBmc;
126
127impl DbBmc for TagBmc {
128    const TABLE: &'static str = "tag";
129}
130
131impl TagBmc {
132    pub async fn create(ctx: &Ctx, mm: &ModelManager, tag_c: TagForCreate) -> Result<i32> {
133        base::create::<Self, _>(ctx, mm, tag_c).await
134    }
135    pub async fn create_full(ctx: &Ctx, mm: &ModelManager, tag_c: Tag) -> Result<i32> {
136        base::create::<Self, _>(ctx, mm, tag_c).await
137    }
138
139    pub async fn get(ctx: &Ctx, mm: &ModelManager, id: i32) -> Result<Tag> {
140        base::get::<Self, _>(ctx, mm, id).await
141    }
142
143    pub async fn list(
144        ctx: &Ctx,
145        mm: &ModelManager,
146        filters: Option<Vec<TagFilter>>,
147        list_options: Option<ListOptions>,
148    ) -> Result<Vec<Tag>> {
149        base::list::<Self, _, _>(ctx, mm, filters, list_options).await
150    }
151
152    pub async fn update(ctx: &Ctx, mm: &ModelManager, id: i32, tag_u: TagForUpdate) -> Result<()> {
153        base::update::<Self, _>(ctx, mm, id, tag_u).await
154    }
155
156    pub async fn delete(ctx: &Ctx, mm: &ModelManager, id: i32) -> Result<()> {
157        base::delete::<Self>(ctx, mm, id).await
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::_dev_utils;
165    use crate::model::Error;
166    use anyhow::Result;
167    use serde_json::json;
168
169    #[ignore]
170    #[tokio::test]
171    async fn test_tag_create_ok() -> Result<()> {
172        let mm = ModelManager::new().await?;
173        let ctx = Ctx::root_ctx();
174        let fx_name = "test_create_ok name";
175
176        let tag_c = TagForCreate {
177            name: fx_name.to_string(),
178            group_id: 1,
179            description: None,
180            is_metal: false,
181            is_fluorophore: false,
182            is_enzyme: false,
183            is_biotin: false,
184            is_other: false,
185            mw: None,
186            emission: None,
187            excitation: None,
188            status: Some(0),
189        };
190        let id = TagBmc::create(&ctx, &mm, tag_c).await?;
191
192        let tag = TagBmc::get(&ctx, &mm, id).await?;
193        assert_eq!(tag.name, fx_name);
194
195        TagBmc::delete(&ctx, &mm, id).await?;
196
197        Ok(())
198    }
199
200    #[ignore]
201    #[tokio::test]
202    async fn test_tag_get_err_not_found() -> Result<()> {
203        let mm = ModelManager::new().await?;
204        let ctx = Ctx::root_ctx();
205        let fx_id = 100;
206
207        let res = TagBmc::get(&ctx, &mm, fx_id).await;
208
209        assert!(
210            matches!(
211                res,
212                Err(Error::EntityNotFound {
213                    entity: "tag",
214                    id: 100
215                })
216            ),
217            "EntityNotFound not matching"
218        );
219
220        Ok(())
221    }
222
223    #[ignore]
224    #[tokio::test]
225    async fn test_tag_list_all_ok() -> Result<()> {
226        let mm = ModelManager::new().await?;
227        let ctx = Ctx::root_ctx();
228        let tname = "test_tag_list_all_ok";
229        let seeds = _dev_utils::get_tag_seed(tname);
230        _dev_utils::seed_tags(&ctx, &mm, &seeds).await?;
231
232        let tags = TagBmc::list(&ctx, &mm, None, None).await?;
233
234        let tags: Vec<Tag> = tags
235            .into_iter()
236            .filter(|t| t.name.starts_with("test_list_all_ok-tag"))
237            .collect();
238        assert_eq!(tags.len(), 4, "number of seeded tags.");
239
240        if false {
241            for tag in tags.iter() {
242                TagBmc::delete(&ctx, &mm, tag.id).await?;
243            }
244        }
245
246        Ok(())
247    }
248
249    #[ignore]
250    #[tokio::test]
251    async fn test_tag_list_by_filter_ok() -> Result<()> {
252        let mm = ModelManager::new().await?;
253        let ctx = Ctx::root_ctx();
254        let tname = "test_tag_list_by_filter_ok";
255        let seeds = _dev_utils::get_tag_seed(tname);
256        _dev_utils::seed_tags(&ctx, &mm, &seeds).await?;
257
258        let filters: Vec<TagFilter> = serde_json::from_value(json!([
259            {
260                "name": {
261                    "$endsWith": ".a",
262                    "$containsAny": ["01", "02"]
263                }
264            },
265            {
266                "name": {"$contains": "03"}
267            }
268        ]))?;
269        let list_options = serde_json::from_value(json!({
270            "order_bys": "!id"
271        }))?;
272        let tags = TagBmc::list(&ctx, &mm, Some(filters), Some(list_options)).await?;
273
274        assert_eq!(tags.len(), 3);
275        assert!(tags[0].name.ends_with("03"));
276        assert!(tags[1].name.ends_with("02.a"));
277        assert!(tags[2].name.ends_with("01.a"));
278
279        if false {
280            let tags = TagBmc::list(
281                &ctx,
282                &mm,
283                Some(serde_json::from_value(json!([{
284                    "name": {"$startsWith": "test_list_by_filter_ok"}
285                }]))?),
286                None,
287            )
288            .await?;
289            assert_eq!(tags.len(), 5);
290            for tag in tags.iter() {
291                TagBmc::delete(&ctx, &mm, tag.id).await?;
292            }
293        }
294
295        Ok(())
296    }
297
298    #[ignore]
299    #[tokio::test]
300    async fn test_tag_update_ok() -> Result<()> {
301        let mm = ModelManager::new().await?;
302        let ctx = Ctx::root_ctx();
303        let tname = "test_tag_update_ok";
304        let seeds = _dev_utils::get_tag_seed(tname);
305        let fx_tag = _dev_utils::seed_tags(&ctx, &mm, &seeds).await?.remove(0);
306
307        TagBmc::update(
308            &ctx,
309            &mm,
310            fx_tag.id,
311            TagForUpdate {
312                name: tname.to_string(),
313                ..Default::default()
314            },
315        )
316        .await?;
317
318        let tag = TagBmc::get(&ctx, &mm, fx_tag.id).await?;
319        assert_eq!(tag.name, tname);
320
321        Ok(())
322    }
323
324    #[ignore]
325    #[tokio::test]
326    async fn test_tag_delete_err_not_found() -> Result<()> {
327        let mm = ModelManager::new().await?;
328        let ctx = Ctx::root_ctx();
329        let fx_id = 100;
330
331        let res = TagBmc::delete(&ctx, &mm, fx_id).await;
332
333        assert!(
334            matches!(
335                res,
336                Err(Error::EntityNotFound {
337                    entity: "tag",
338                    id: 100
339                })
340            ),
341            "EntityNotFound not matching"
342        );
343
344        Ok(())
345    }
346}