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}