1
pub 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
2
    fn db_err(err: sqlx::Error) -> ServerError {
41
2
        log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
42
2
        ServerError::DB(DBError::Sqlx(err))
43
2
    }
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
45
        async fn link_artifact_tag(
50
45
            conn: &mut sqlx::PgConnection,
51
45
            artifact_id: Uuid,
52
45
            name: &str,
53
45
            value: &str,
54
45
        ) -> Result<(), ServerError> {
55
45
            let canonical_id = Tag {
56
45
                id: Uuid::new_v4(),
57
45
                tag_name: name.to_string(),
58
45
                tag_value: value.to_string(),
59
45
                description: None,
60
45
            }
61
45
            .commit(&mut *conn)
62
45
            .await
63
45
            .map_err(|err| {
64
                log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
65
                ServerError::Finance(err)
66
            })?;
67

            
68
45
            sqlx::query_file!(
69
                "sql/insert/artifact_tags/artifact_tag.sql",
70
                &artifact_id,
71
                &canonical_id
72
            )
73
45
            .execute(&mut *conn)
74
45
            .await
75
45
            .map_err(db_err)?;
76

            
77
45
            Ok(())
78
45
        }
79

            
80
9
        pub async fn list_scripts(&self) -> Result<Vec<ScriptInfo>, ServerError> {
81
9
            let mut conn = self.get_connection().await?;
82

            
83
9
            let rows = sqlx::query_file!("sql/select/artifacts/by_kind.sql", KIND_AUTOMATION)
84
9
                .fetch_all(&mut *conn)
85
9
                .await
86
9
                .map_err(db_err)?;
87

            
88
9
            Ok(rows
89
9
                .into_iter()
90
9
                .map(|r| ScriptInfo {
91
12
                    id: r.id,
92
12
                    name: r.artifact_name,
93
12
                    size: r.size.unwrap_or(0),
94
12
                    enabled: r.enabled,
95
12
                })
96
9
                .collect())
97
9
        }
98

            
99
9
        pub async fn get_script(&self, id: Uuid) -> Result<ScriptDetail, ServerError> {
100
9
            let mut conn = self.get_connection().await?;
101

            
102
9
            let row = sqlx::query_file!("sql/select/artifacts/by_id.sql", &id, KIND_AUTOMATION)
103
9
                .fetch_one(&mut *conn)
104
9
                .await
105
9
                .map_err(db_err)?;
106

            
107
8
            Ok(ScriptDetail {
108
8
                id: row.id,
109
8
                name: row.artifact_name,
110
8
                bytecode: row.bytecode.unwrap_or_default(),
111
8
                enabled: row.enabled,
112
8
            })
113
9
        }
114

            
115
27
        pub async fn create_script(
116
27
            &self,
117
27
            bytecode: Vec<u8>,
118
27
            name: Option<String>,
119
27
        ) -> Result<Uuid, ServerError> {
120
27
            let mut conn = self.get_connection().await?;
121
27
            let mut tx = conn.begin().await.map_err(db_err)?;
122

            
123
27
            let id = Uuid::new_v4();
124
27
            sqlx::query_file!(
125
                "sql/insert/artifacts/artifact.sql",
126
                &id,
127
27
                Some(&bytecode[..]),
128
                None::<&str>
129
            )
130
27
            .execute(&mut *tx)
131
27
            .await
132
27
            .map_err(db_err)?;
133

            
134
27
            User::link_artifact_tag(&mut tx, id, "kind", KIND_AUTOMATION).await?;
135
27
            if let Some(name) = name {
136
8
                User::link_artifact_tag(&mut tx, id, "name", &name).await?;
137
19
            }
138

            
139
27
            tx.commit().await.map_err(db_err)?;
140
27
            Ok(id)
141
27
        }
142

            
143
1
        pub async fn update_script_bytecode(
144
1
            &self,
145
1
            id: Uuid,
146
1
            bytecode: Vec<u8>,
147
1
        ) -> Result<(), ServerError> {
148
1
            let mut conn = self.get_connection().await?;
149

            
150
1
            sqlx::query_file!("sql/update/artifacts/bytecode.sql", &id, &bytecode)
151
1
                .execute(&mut *conn)
152
1
                .await
153
1
                .map_err(db_err)?;
154

            
155
1
            Ok(())
156
1
        }
157

            
158
7
        pub async fn delete_script(&self, id: Uuid) -> Result<(), ServerError> {
159
7
            self.delete_artifact(id).await
160
7
        }
161

            
162
4
        pub async fn set_script_enabled(&self, id: Uuid, enabled: bool) -> Result<(), ServerError> {
163
4
            self.set_artifact_enabled(id, enabled).await
164
4
        }
165

            
166
2
        pub async fn update_script_name(
167
2
            &self,
168
2
            id: Uuid,
169
2
            name: Option<String>,
170
2
        ) -> Result<(), ServerError> {
171
2
            self.update_artifact_name(id, name).await
172
2
        }
173

            
174
3
        pub async fn list_templates(&self) -> Result<Vec<TemplateInfo>, ServerError> {
175
3
            let mut conn = self.get_connection().await?;
176

            
177
3
            let rows = sqlx::query_file!("sql/select/artifacts/by_kind.sql", KIND_TEMPLATE)
178
3
                .fetch_all(&mut *conn)
179
3
                .await
180
3
                .map_err(db_err)?;
181

            
182
3
            Ok(rows
183
3
                .into_iter()
184
3
                .map(|r| TemplateInfo {
185
2
                    id: r.id,
186
2
                    name: r.artifact_name,
187
2
                    size: r.size.unwrap_or(0),
188
2
                    enabled: r.enabled,
189
2
                })
190
3
                .collect())
191
3
        }
192

            
193
4
        pub async fn get_template(&self, id: Uuid) -> Result<TemplateDetail, ServerError> {
194
4
            let mut conn = self.get_connection().await?;
195

            
196
4
            let row = sqlx::query_file!("sql/select/artifacts/by_id.sql", &id, KIND_TEMPLATE)
197
4
                .fetch_one(&mut *conn)
198
4
                .await
199
4
                .map_err(db_err)?;
200

            
201
3
            Ok(TemplateDetail {
202
3
                id: row.id,
203
3
                name: row.artifact_name,
204
3
                source: row.source.unwrap_or_default(),
205
3
                enabled: row.enabled,
206
3
            })
207
4
        }
208

            
209
4
        pub async fn create_template(
210
4
            &self,
211
4
            source: String,
212
4
            name: Option<String>,
213
4
        ) -> Result<Uuid, ServerError> {
214
4
            let mut conn = self.get_connection().await?;
215
4
            let mut tx = conn.begin().await.map_err(db_err)?;
216

            
217
4
            let id = Uuid::new_v4();
218
4
            sqlx::query_file!(
219
                "sql/insert/artifacts/artifact.sql",
220
                &id,
221
                None::<&[u8]>,
222
4
                Some(source.as_str())
223
            )
224
4
            .execute(&mut *tx)
225
4
            .await
226
4
            .map_err(db_err)?;
227

            
228
4
            User::link_artifact_tag(&mut tx, id, "kind", KIND_TEMPLATE).await?;
229
4
            if let Some(name) = name {
230
1
                User::link_artifact_tag(&mut tx, id, "name", &name).await?;
231
3
            }
232

            
233
4
            tx.commit().await.map_err(db_err)?;
234
4
            Ok(id)
235
4
        }
236

            
237
1
        pub async fn update_template_source(
238
1
            &self,
239
1
            id: Uuid,
240
1
            source: String,
241
1
        ) -> Result<(), ServerError> {
242
1
            let mut conn = self.get_connection().await?;
243

            
244
1
            sqlx::query_file!("sql/update/artifacts/source.sql", &id, &source)
245
1
                .execute(&mut *conn)
246
1
                .await
247
1
                .map_err(db_err)?;
248

            
249
1
            Ok(())
250
1
        }
251

            
252
1
        pub async fn delete_template(&self, id: Uuid) -> Result<(), ServerError> {
253
1
            self.delete_artifact(id).await
254
1
        }
255

            
256
1
        pub async fn update_template_name(
257
1
            &self,
258
1
            id: Uuid,
259
1
            name: Option<String>,
260
1
        ) -> Result<(), ServerError> {
261
1
            self.update_artifact_name(id, name).await
262
1
        }
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
8
        async fn delete_artifact(&self, id: Uuid) -> Result<(), ServerError> {
267
8
            let mut conn = self.get_connection().await?;
268
8
            let mut tx = conn.begin().await.map_err(db_err)?;
269

            
270
8
            sqlx::query_file!("sql/delete/artifact_tags/by_artifact.sql", &id)
271
8
                .execute(&mut *tx)
272
8
                .await
273
8
                .map_err(db_err)?;
274

            
275
8
            sqlx::query_file!("sql/delete/artifacts/by_id.sql", &id)
276
8
                .execute(&mut *tx)
277
8
                .await
278
8
                .map_err(db_err)?;
279

            
280
8
            tx.commit().await.map_err(db_err)?;
281
8
            Ok(())
282
8
        }
283

            
284
4
        async fn set_artifact_enabled(&self, id: Uuid, enabled: bool) -> Result<(), ServerError> {
285
4
            let mut conn = self.get_connection().await?;
286
4
            let mut tx = conn.begin().await.map_err(db_err)?;
287

            
288
4
            sqlx::query_file!(
289
                "sql/delete/artifact_tags/by_artifact_and_name.sql",
290
                &id,
291
                "enabled"
292
            )
293
4
            .execute(&mut *tx)
294
4
            .await
295
4
            .map_err(db_err)?;
296

            
297
4
            if !enabled {
298
3
                User::link_artifact_tag(&mut tx, id, "enabled", "false").await?;
299
1
            }
300

            
301
4
            tx.commit().await.map_err(db_err)?;
302
4
            Ok(())
303
4
        }
304

            
305
3
        async fn update_artifact_name(
306
3
            &self,
307
3
            id: Uuid,
308
3
            name: Option<String>,
309
3
        ) -> Result<(), ServerError> {
310
3
            let mut conn = self.get_connection().await?;
311
3
            let mut tx = conn.begin().await.map_err(db_err)?;
312

            
313
3
            sqlx::query_file!(
314
                "sql/delete/artifact_tags/by_artifact_and_name.sql",
315
                &id,
316
                "name"
317
            )
318
3
            .execute(&mut *tx)
319
3
            .await
320
3
            .map_err(db_err)?;
321

            
322
3
            if let Some(name) = name {
323
2
                User::link_artifact_tag(&mut tx, id, "name", &name).await?;
324
1
            }
325

            
326
3
            tx.commit().await.map_err(db_err)?;
327
3
            Ok(())
328
3
        }
329
    }
330
}
331

            
332
#[cfg(test)]
333
mod artifact_mgmt_tests;