Skip to main content

server/
artifact_mgmt.rs

1pub mod user {
2    use crate::db::DBError;
3    use crate::error::ServerError;
4    use crate::user::User;
5    use finance::tag::Tag;
6    use sqlx::Connection;
7    use sqlx::types::Uuid;
8
9    const KIND_AUTOMATION: &str = "automation";
10    const KIND_TEMPLATE: &str = "template";
11
12    pub struct ScriptInfo {
13        pub id: Uuid,
14        pub name: Option<String>,
15        pub size: i32,
16        pub enabled: Option<String>,
17    }
18
19    pub struct ScriptDetail {
20        pub id: Uuid,
21        pub name: Option<String>,
22        pub bytecode: Vec<u8>,
23        pub enabled: Option<String>,
24    }
25
26    pub struct TemplateInfo {
27        pub id: Uuid,
28        pub name: Option<String>,
29        pub size: i32,
30        pub enabled: Option<String>,
31    }
32
33    pub struct TemplateDetail {
34        pub id: Uuid,
35        pub name: Option<String>,
36        pub source: String,
37        pub enabled: Option<String>,
38    }
39
40    fn db_err(err: sqlx::Error) -> ServerError {
41        log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
42        ServerError::DB(DBError::Sqlx(err))
43    }
44
45    impl User {
46        /// Upsert a canonical `(name, value)` tag and link it to `artifact_id`
47        /// using the id the upsert RETURNS — never a locally minted uuid, which
48        /// would dangle when the canonical row already exists.
49        async fn link_artifact_tag(
50            conn: &mut sqlx::PgConnection,
51            artifact_id: Uuid,
52            name: &str,
53            value: &str,
54        ) -> Result<(), ServerError> {
55            let canonical_id = Tag {
56                id: Uuid::new_v4(),
57                tag_name: name.to_string(),
58                tag_value: value.to_string(),
59                description: None,
60            }
61            .commit(&mut *conn)
62            .await
63            .map_err(|err| {
64                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
65                ServerError::Finance(err)
66            })?;
67
68            sqlx::query_file!(
69                "sql/insert/artifact_tags/artifact_tag.sql",
70                &artifact_id,
71                &canonical_id
72            )
73            .execute(&mut *conn)
74            .await
75            .map_err(db_err)?;
76
77            Ok(())
78        }
79
80        pub async fn list_scripts(&self) -> Result<Vec<ScriptInfo>, ServerError> {
81            let mut conn = self.get_connection().await?;
82
83            let rows = sqlx::query_file!("sql/select/artifacts/by_kind.sql", KIND_AUTOMATION)
84                .fetch_all(&mut *conn)
85                .await
86                .map_err(db_err)?;
87
88            Ok(rows
89                .into_iter()
90                .map(|r| ScriptInfo {
91                    id: r.id,
92                    name: r.artifact_name,
93                    size: r.size.unwrap_or(0),
94                    enabled: r.enabled,
95                })
96                .collect())
97        }
98
99        pub async fn get_script(&self, id: Uuid) -> Result<ScriptDetail, ServerError> {
100            let mut conn = self.get_connection().await?;
101
102            let row = sqlx::query_file!("sql/select/artifacts/by_id.sql", &id, KIND_AUTOMATION)
103                .fetch_one(&mut *conn)
104                .await
105                .map_err(db_err)?;
106
107            Ok(ScriptDetail {
108                id: row.id,
109                name: row.artifact_name,
110                bytecode: row.bytecode.unwrap_or_default(),
111                enabled: row.enabled,
112            })
113        }
114
115        pub async fn create_script(
116            &self,
117            bytecode: Vec<u8>,
118            name: Option<String>,
119        ) -> Result<Uuid, ServerError> {
120            let mut conn = self.get_connection().await?;
121            let mut tx = conn.begin().await.map_err(db_err)?;
122
123            let id = Uuid::new_v4();
124            sqlx::query_file!(
125                "sql/insert/artifacts/artifact.sql",
126                &id,
127                Some(&bytecode[..]),
128                None::<&str>
129            )
130            .execute(&mut *tx)
131            .await
132            .map_err(db_err)?;
133
134            User::link_artifact_tag(&mut tx, id, "kind", KIND_AUTOMATION).await?;
135            if let Some(name) = name {
136                User::link_artifact_tag(&mut tx, id, "name", &name).await?;
137            }
138
139            tx.commit().await.map_err(db_err)?;
140            Ok(id)
141        }
142
143        pub async fn update_script_bytecode(
144            &self,
145            id: Uuid,
146            bytecode: Vec<u8>,
147        ) -> Result<(), ServerError> {
148            let mut conn = self.get_connection().await?;
149
150            sqlx::query_file!("sql/update/artifacts/bytecode.sql", &id, &bytecode)
151                .execute(&mut *conn)
152                .await
153                .map_err(db_err)?;
154
155            Ok(())
156        }
157
158        pub async fn delete_script(&self, id: Uuid) -> Result<(), ServerError> {
159            self.delete_artifact(id).await
160        }
161
162        pub async fn set_script_enabled(&self, id: Uuid, enabled: bool) -> Result<(), ServerError> {
163            self.set_artifact_enabled(id, enabled).await
164        }
165
166        pub async fn update_script_name(
167            &self,
168            id: Uuid,
169            name: Option<String>,
170        ) -> Result<(), ServerError> {
171            self.update_artifact_name(id, name).await
172        }
173
174        pub async fn list_templates(&self) -> Result<Vec<TemplateInfo>, ServerError> {
175            let mut conn = self.get_connection().await?;
176
177            let rows = sqlx::query_file!("sql/select/artifacts/by_kind.sql", KIND_TEMPLATE)
178                .fetch_all(&mut *conn)
179                .await
180                .map_err(db_err)?;
181
182            Ok(rows
183                .into_iter()
184                .map(|r| TemplateInfo {
185                    id: r.id,
186                    name: r.artifact_name,
187                    size: r.size.unwrap_or(0),
188                    enabled: r.enabled,
189                })
190                .collect())
191        }
192
193        pub async fn get_template(&self, id: Uuid) -> Result<TemplateDetail, ServerError> {
194            let mut conn = self.get_connection().await?;
195
196            let row = sqlx::query_file!("sql/select/artifacts/by_id.sql", &id, KIND_TEMPLATE)
197                .fetch_one(&mut *conn)
198                .await
199                .map_err(db_err)?;
200
201            Ok(TemplateDetail {
202                id: row.id,
203                name: row.artifact_name,
204                source: row.source.unwrap_or_default(),
205                enabled: row.enabled,
206            })
207        }
208
209        pub async fn create_template(
210            &self,
211            source: String,
212            name: Option<String>,
213        ) -> Result<Uuid, ServerError> {
214            let mut conn = self.get_connection().await?;
215            let mut tx = conn.begin().await.map_err(db_err)?;
216
217            let id = Uuid::new_v4();
218            sqlx::query_file!(
219                "sql/insert/artifacts/artifact.sql",
220                &id,
221                None::<&[u8]>,
222                Some(source.as_str())
223            )
224            .execute(&mut *tx)
225            .await
226            .map_err(db_err)?;
227
228            User::link_artifact_tag(&mut tx, id, "kind", KIND_TEMPLATE).await?;
229            if let Some(name) = name {
230                User::link_artifact_tag(&mut tx, id, "name", &name).await?;
231            }
232
233            tx.commit().await.map_err(db_err)?;
234            Ok(id)
235        }
236
237        pub async fn update_template_source(
238            &self,
239            id: Uuid,
240            source: String,
241        ) -> Result<(), ServerError> {
242            let mut conn = self.get_connection().await?;
243
244            sqlx::query_file!("sql/update/artifacts/source.sql", &id, &source)
245                .execute(&mut *conn)
246                .await
247                .map_err(db_err)?;
248
249            Ok(())
250        }
251
252        pub async fn delete_template(&self, id: Uuid) -> Result<(), ServerError> {
253            self.delete_artifact(id).await
254        }
255
256        pub async fn update_template_name(
257            &self,
258            id: Uuid,
259            name: Option<String>,
260        ) -> Result<(), ServerError> {
261            self.update_artifact_name(id, name).await
262        }
263
264        /// Lifecycle delete: detach links and drop the row in ONE transaction so
265        /// the deferred kind-tag DELETE guard sees the parent gone at commit.
266        async fn delete_artifact(&self, id: Uuid) -> Result<(), ServerError> {
267            let mut conn = self.get_connection().await?;
268            let mut tx = conn.begin().await.map_err(db_err)?;
269
270            sqlx::query_file!("sql/delete/artifact_tags/by_artifact.sql", &id)
271                .execute(&mut *tx)
272                .await
273                .map_err(db_err)?;
274
275            sqlx::query_file!("sql/delete/artifacts/by_id.sql", &id)
276                .execute(&mut *tx)
277                .await
278                .map_err(db_err)?;
279
280            tx.commit().await.map_err(db_err)?;
281            Ok(())
282        }
283
284        async fn set_artifact_enabled(&self, id: Uuid, enabled: bool) -> Result<(), ServerError> {
285            let mut conn = self.get_connection().await?;
286            let mut tx = conn.begin().await.map_err(db_err)?;
287
288            sqlx::query_file!(
289                "sql/delete/artifact_tags/by_artifact_and_name.sql",
290                &id,
291                "enabled"
292            )
293            .execute(&mut *tx)
294            .await
295            .map_err(db_err)?;
296
297            if !enabled {
298                User::link_artifact_tag(&mut tx, id, "enabled", "false").await?;
299            }
300
301            tx.commit().await.map_err(db_err)?;
302            Ok(())
303        }
304
305        async fn update_artifact_name(
306            &self,
307            id: Uuid,
308            name: Option<String>,
309        ) -> Result<(), ServerError> {
310            let mut conn = self.get_connection().await?;
311            let mut tx = conn.begin().await.map_err(db_err)?;
312
313            sqlx::query_file!(
314                "sql/delete/artifact_tags/by_artifact_and_name.sql",
315                &id,
316                "name"
317            )
318            .execute(&mut *tx)
319            .await
320            .map_err(db_err)?;
321
322            if let Some(name) = name {
323                User::link_artifact_tag(&mut tx, id, "name", &name).await?;
324            }
325
326            tx.commit().await.map_err(db_err)?;
327            Ok(())
328        }
329    }
330}
331
332#[cfg(test)]
333mod artifact_mgmt_tests;