Skip to main content

shared/models/
database_agent_template.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder},
3    prelude::*,
4};
5use garde::Validate;
6use indexmap::IndexMap;
7use serde::{Deserialize, Serialize};
8use sqlx::{Row, postgres::PgRow};
9use std::{
10    collections::{BTreeMap, HashSet},
11    sync::{Arc, LazyLock},
12};
13use utoipa::ToSchema;
14
15pub fn validate_docker_images(
16    docker_images: &IndexMap<compact_str::CompactString, compact_str::CompactString>,
17    _context: &(),
18) -> Result<(), garde::Error> {
19    if docker_images.is_empty() {
20        return Err(garde::Error::new("at least one docker image is required"));
21    }
22
23    let mut seen_images = HashSet::new();
24    for image in docker_images.values() {
25        if !seen_images.insert(image) {
26            return Err(garde::Error::new(compact_str::format_compact!(
27                "duplicate docker image: {}",
28                image
29            )));
30        }
31    }
32
33    Ok(())
34}
35
36#[derive(Serialize, Deserialize, Clone)]
37pub struct DatabaseAgentTemplate {
38    pub uuid: uuid::Uuid,
39
40    pub name: compact_str::CompactString,
41    pub description: Option<compact_str::CompactString>,
42
43    pub r#type: db_agent_api::DatabaseAgentType,
44    pub deployment_enabled: bool,
45
46    pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
47    pub env: IndexMap<compact_str::CompactString, compact_str::CompactString>,
48
49    pub image_uid: i32,
50    pub image_gid: i32,
51    pub cmd: Option<Vec<compact_str::CompactString>>,
52    pub volumes: IndexMap<compact_str::CompactString, compact_str::CompactString>,
53    pub socket_path: compact_str::CompactString,
54
55    pub memory: i64,
56    pub swap: i64,
57    pub disk: i64,
58    pub io_weight: Option<i16>,
59    pub cpu: i32,
60
61    pub version: i32,
62
63    pub created: chrono::NaiveDateTime,
64
65    extension_data: super::ModelExtensionData,
66}
67
68impl BaseModel for DatabaseAgentTemplate {
69    const NAME: &'static str = "database_agent_template";
70
71    fn get_extension_list() -> &'static super::ModelExtensionList {
72        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
73            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
74
75        &EXTENSIONS
76    }
77
78    fn get_extension_data(&self) -> &super::ModelExtensionData {
79        &self.extension_data
80    }
81
82    #[inline]
83    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
84        let prefix = prefix.unwrap_or_default();
85
86        BTreeMap::from([
87            (
88                "database_agent_templates.uuid",
89                compact_str::format_compact!("{prefix}uuid"),
90            ),
91            (
92                "database_agent_templates.name",
93                compact_str::format_compact!("{prefix}name"),
94            ),
95            (
96                "database_agent_templates.description",
97                compact_str::format_compact!("{prefix}description"),
98            ),
99            (
100                "database_agent_templates.type",
101                compact_str::format_compact!("{prefix}type"),
102            ),
103            (
104                "database_agent_templates.deployment_enabled",
105                compact_str::format_compact!("{prefix}deployment_enabled"),
106            ),
107            (
108                "database_agent_templates.docker_images",
109                compact_str::format_compact!("{prefix}docker_images"),
110            ),
111            (
112                "database_agent_templates.env",
113                compact_str::format_compact!("{prefix}env"),
114            ),
115            (
116                "database_agent_templates.image_uid",
117                compact_str::format_compact!("{prefix}image_uid"),
118            ),
119            (
120                "database_agent_templates.image_gid",
121                compact_str::format_compact!("{prefix}image_gid"),
122            ),
123            (
124                "database_agent_templates.cmd",
125                compact_str::format_compact!("{prefix}cmd"),
126            ),
127            (
128                "database_agent_templates.volumes",
129                compact_str::format_compact!("{prefix}volumes"),
130            ),
131            (
132                "database_agent_templates.socket_path",
133                compact_str::format_compact!("{prefix}socket_path"),
134            ),
135            (
136                "database_agent_templates.memory",
137                compact_str::format_compact!("{prefix}memory"),
138            ),
139            (
140                "database_agent_templates.swap",
141                compact_str::format_compact!("{prefix}swap"),
142            ),
143            (
144                "database_agent_templates.disk",
145                compact_str::format_compact!("{prefix}disk"),
146            ),
147            (
148                "database_agent_templates.io_weight",
149                compact_str::format_compact!("{prefix}io_weight"),
150            ),
151            (
152                "database_agent_templates.cpu",
153                compact_str::format_compact!("{prefix}cpu"),
154            ),
155            (
156                "database_agent_templates.version",
157                compact_str::format_compact!("{prefix}version"),
158            ),
159            (
160                "database_agent_templates.created",
161                compact_str::format_compact!("{prefix}created"),
162            ),
163        ])
164    }
165
166    #[inline]
167    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
168        let prefix = prefix.unwrap_or_default();
169
170        Ok(Self {
171            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
172            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
173            description: row
174                .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
175            r#type: row.try_get(compact_str::format_compact!("{prefix}type").as_str())?,
176            deployment_enabled: row
177                .try_get(compact_str::format_compact!("{prefix}deployment_enabled").as_str())?,
178            docker_images: serde_json::from_value(
179                row.try_get(compact_str::format_compact!("{prefix}docker_images").as_str())?,
180            )?,
181            env: serde_json::from_value(
182                row.try_get(compact_str::format_compact!("{prefix}env").as_str())?,
183            )?,
184            image_uid: row.try_get(compact_str::format_compact!("{prefix}image_uid").as_str())?,
185            image_gid: row.try_get(compact_str::format_compact!("{prefix}image_gid").as_str())?,
186            cmd: row.try_get(compact_str::format_compact!("{prefix}cmd").as_str())?,
187            volumes: serde_json::from_value(
188                row.try_get(compact_str::format_compact!("{prefix}volumes").as_str())?,
189            )?,
190            socket_path: row
191                .try_get(compact_str::format_compact!("{prefix}socket_path").as_str())?,
192            memory: row.try_get(compact_str::format_compact!("{prefix}memory").as_str())?,
193            swap: row.try_get(compact_str::format_compact!("{prefix}swap").as_str())?,
194            disk: row.try_get(compact_str::format_compact!("{prefix}disk").as_str())?,
195            io_weight: row.try_get(compact_str::format_compact!("{prefix}io_weight").as_str())?,
196            cpu: row.try_get(compact_str::format_compact!("{prefix}cpu").as_str())?,
197            version: row.try_get(compact_str::format_compact!("{prefix}version").as_str())?,
198            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
199            extension_data: Self::map_extensions(prefix, row)?,
200        })
201    }
202}
203
204impl DatabaseAgentTemplate {
205    pub async fn all_deployment_enabled(
206        database: &crate::database::Database,
207    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
208        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
209            r#"
210            SELECT {}
211            FROM database_agent_templates
212            WHERE database_agent_templates.deployment_enabled
213            ORDER BY database_agent_templates.created
214            "#,
215            Self::columns_sql(None)
216        )))
217        .fetch_all(database.read())
218        .await?;
219
220        rows.into_iter()
221            .map(|row| Self::map(None, &row))
222            .try_collect_vec()
223    }
224
225    pub async fn all_with_pagination(
226        database: &crate::database::Database,
227        page: i64,
228        per_page: i64,
229        search: Option<&str>,
230    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
231        let offset = (page - 1) * per_page;
232
233        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
234            r#"
235            SELECT {}, COUNT(*) OVER() AS total_count
236            FROM database_agent_templates
237            WHERE ($1 IS NULL OR database_agent_templates.name ILIKE '%' || $1 || '%')
238            ORDER BY database_agent_templates.created
239            LIMIT $2 OFFSET $3
240            "#,
241            Self::columns_sql(None)
242        )))
243        .bind(search)
244        .bind(per_page)
245        .bind(offset)
246        .fetch_all(database.read())
247        .await?;
248
249        Ok(super::Pagination {
250            total: rows
251                .first()
252                .map_or(Ok(0), |row| row.try_get("total_count"))?,
253            per_page,
254            page,
255            data: rows
256                .into_iter()
257                .map(|row| Self::map(None, &row))
258                .try_collect_vec()?,
259        })
260    }
261}
262
263#[async_trait::async_trait]
264impl IntoAdminApiObject for DatabaseAgentTemplate {
265    type AdminApiObject = AdminApiDatabaseAgentTemplate;
266    type ExtraArgs<'a> = ();
267
268    async fn into_admin_api_object<'a>(
269        self,
270        state: &crate::State,
271        _args: Self::ExtraArgs<'a>,
272    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
273        let api_object = AdminApiDatabaseAgentTemplate::init_hooks(&self, state).await?;
274
275        let api_object = finish_extendible!(
276            AdminApiDatabaseAgentTemplate {
277                uuid: self.uuid,
278                name: self.name,
279                description: self.description,
280                r#type: self.r#type,
281                deployment_enabled: self.deployment_enabled,
282                docker_images: self.docker_images,
283                env: self.env,
284                image_uid: self.image_uid,
285                image_gid: self.image_gid,
286                cmd: self.cmd,
287                volumes: self.volumes,
288                socket_path: self.socket_path,
289                memory: self.memory,
290                swap: self.swap,
291                disk: self.disk,
292                io_weight: self.io_weight,
293                cpu: self.cpu,
294                version: self.version,
295                created: self.created.and_utc(),
296            },
297            api_object,
298            state
299        )?;
300
301        Ok(api_object)
302    }
303}
304
305#[async_trait::async_trait]
306impl IntoApiObject for DatabaseAgentTemplate {
307    type ApiObject = ApiDatabaseAgentTemplate;
308    type ExtraArgs<'a> = ();
309
310    async fn into_api_object<'a>(
311        self,
312        state: &crate::State,
313        _args: Self::ExtraArgs<'a>,
314    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
315        let api_object = ApiDatabaseAgentTemplate::init_hooks(&self, state).await?;
316
317        let api_object = finish_extendible!(
318            ApiDatabaseAgentTemplate {
319                uuid: self.uuid,
320                name: self.name,
321                description: self.description,
322                r#type: self.r#type,
323                docker_images: self.docker_images,
324                memory: self.memory,
325                swap: self.swap,
326                disk: self.disk,
327                io_weight: self.io_weight,
328                cpu: self.cpu,
329                version: self.version,
330                created: self.created.and_utc(),
331            },
332            api_object,
333            state
334        )?;
335
336        Ok(api_object)
337    }
338}
339
340#[async_trait::async_trait]
341impl ByUuid for DatabaseAgentTemplate {
342    async fn by_uuid(
343        database: &crate::database::Database,
344        uuid: uuid::Uuid,
345    ) -> Result<Self, crate::database::DatabaseError> {
346        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
347            r#"
348            SELECT {}
349            FROM database_agent_templates
350            WHERE database_agent_templates.uuid = $1
351            "#,
352            Self::columns_sql(None)
353        )))
354        .bind(uuid)
355        .fetch_one(database.read())
356        .await?;
357
358        Self::map(None, &row)
359    }
360
361    async fn by_uuid_with_transaction(
362        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
363        uuid: uuid::Uuid,
364    ) -> Result<Self, crate::database::DatabaseError> {
365        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
366            r#"
367            SELECT {}
368            FROM database_agent_templates
369            WHERE database_agent_templates.uuid = $1
370            "#,
371            Self::columns_sql(None)
372        )))
373        .bind(uuid)
374        .fetch_one(&mut **transaction)
375        .await?;
376
377        Self::map(None, &row)
378    }
379}
380
381#[derive(ToSchema, Deserialize, Validate)]
382pub struct CreateDatabaseAgentTemplateOptions {
383    #[garde(length(chars, min = 1, max = 255))]
384    #[schema(min_length = 1, max_length = 255)]
385    pub name: compact_str::CompactString,
386    #[garde(length(chars, min = 1, max = 1024))]
387    #[schema(min_length = 1, max_length = 1024)]
388    pub description: Option<compact_str::CompactString>,
389
390    #[garde(skip)]
391    pub r#type: db_agent_api::DatabaseAgentType,
392    #[garde(skip)]
393    pub deployment_enabled: bool,
394
395    #[garde(custom(validate_docker_images))]
396    pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
397    #[garde(skip)]
398    pub env: IndexMap<compact_str::CompactString, compact_str::CompactString>,
399
400    #[garde(range(min = 0))]
401    #[schema(minimum = 0)]
402    pub image_uid: i32,
403    #[garde(range(min = 0))]
404    #[schema(minimum = 0)]
405    pub image_gid: i32,
406    #[garde(skip)]
407    pub cmd: Option<Vec<compact_str::CompactString>>,
408    #[garde(skip)]
409    pub volumes: IndexMap<compact_str::CompactString, compact_str::CompactString>,
410    #[garde(length(chars, min = 1, max = 255))]
411    #[schema(min_length = 1, max_length = 255)]
412    pub socket_path: compact_str::CompactString,
413
414    #[garde(range(min = 0))]
415    #[schema(minimum = 0)]
416    pub memory: i64,
417    #[garde(range(min = -1))]
418    #[schema(minimum = -1)]
419    pub swap: i64,
420    #[garde(range(min = 0))]
421    #[schema(minimum = 0)]
422    pub disk: i64,
423    #[garde(range(min = 0, max = 1000))]
424    #[schema(minimum = 0, maximum = 1000)]
425    pub io_weight: Option<i16>,
426    #[garde(range(min = 0))]
427    #[schema(minimum = 0)]
428    pub cpu: i32,
429}
430
431#[async_trait::async_trait]
432impl CreatableModel for DatabaseAgentTemplate {
433    type CreateOptions<'a> = CreateDatabaseAgentTemplateOptions;
434    type CreateResult = Self;
435
436    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
437        static CREATE_LISTENERS: LazyLock<CreateListenerList<DatabaseAgentTemplate>> =
438            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
439
440        &CREATE_LISTENERS
441    }
442
443    async fn create_with_transaction(
444        state: &crate::State,
445        mut options: Self::CreateOptions<'_>,
446        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
447    ) -> Result<Self, crate::database::DatabaseError> {
448        options.validate()?;
449
450        let mut query_builder = InsertQueryBuilder::new("database_agent_templates");
451
452        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
453
454        query_builder
455            .set("name", &options.name)
456            .set("description", &options.description)
457            .set("type", options.r#type)
458            .set("deployment_enabled", options.deployment_enabled)
459            .set("docker_images", OrderedJson(&options.docker_images))
460            .set("env", OrderedJson(&options.env))
461            .set("image_uid", options.image_uid)
462            .set("image_gid", options.image_gid)
463            .set("cmd", options.cmd.as_ref())
464            .set("volumes", OrderedJson(&options.volumes))
465            .set("socket_path", &options.socket_path)
466            .set("memory", options.memory)
467            .set("swap", options.swap)
468            .set("disk", options.disk)
469            .set("io_weight", options.io_weight)
470            .set("cpu", options.cpu);
471
472        let row = query_builder
473            .returning(&Self::columns_sql(None))
474            .fetch_one(&mut **transaction)
475            .await?;
476        let mut database_agent_template = Self::map(None, &row)?;
477
478        Self::run_after_create_handlers(&mut database_agent_template, &options, state, transaction)
479            .await?;
480
481        Ok(database_agent_template)
482    }
483}
484
485#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
486pub struct UpdateDatabaseAgentTemplateOptions {
487    #[garde(length(chars, min = 1, max = 255))]
488    #[schema(min_length = 1, max_length = 255)]
489    pub name: Option<compact_str::CompactString>,
490    #[garde(length(chars, min = 1, max = 1024))]
491    #[schema(min_length = 1, max_length = 1024)]
492    #[serde(
493        default,
494        skip_serializing_if = "Option::is_none",
495        with = "::serde_with::rust::double_option"
496    )]
497    pub description: Option<Option<compact_str::CompactString>>,
498
499    #[garde(skip)]
500    pub deployment_enabled: Option<bool>,
501
502    #[garde(inner(custom(validate_docker_images)))]
503    pub docker_images: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
504    #[garde(skip)]
505    pub env: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
506
507    #[garde(range(min = 0))]
508    #[schema(minimum = 0)]
509    pub image_uid: Option<i32>,
510    #[garde(range(min = 0))]
511    #[schema(minimum = 0)]
512    pub image_gid: Option<i32>,
513    #[garde(skip)]
514    #[serde(
515        default,
516        skip_serializing_if = "Option::is_none",
517        with = "::serde_with::rust::double_option"
518    )]
519    pub cmd: Option<Option<Vec<compact_str::CompactString>>>,
520    #[garde(skip)]
521    pub volumes: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
522    #[garde(length(chars, min = 1, max = 255))]
523    #[schema(min_length = 1, max_length = 255)]
524    pub socket_path: Option<compact_str::CompactString>,
525
526    #[garde(range(min = 0))]
527    #[schema(minimum = 0)]
528    pub memory: Option<i64>,
529    #[garde(range(min = -1))]
530    #[schema(minimum = -1)]
531    pub swap: Option<i64>,
532    #[garde(range(min = 0))]
533    #[schema(minimum = 0)]
534    pub disk: Option<i64>,
535    #[garde(skip)]
536    #[serde(
537        default,
538        skip_serializing_if = "Option::is_none",
539        with = "::serde_with::rust::double_option"
540    )]
541    pub io_weight: Option<Option<i16>>,
542    #[garde(range(min = 0))]
543    #[schema(minimum = 0)]
544    pub cpu: Option<i32>,
545}
546
547#[async_trait::async_trait]
548impl UpdatableModel for DatabaseAgentTemplate {
549    type UpdateOptions = UpdateDatabaseAgentTemplateOptions;
550
551    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
552        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<DatabaseAgentTemplate>> =
553            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
554
555        &UPDATE_LISTENERS
556    }
557
558    async fn update_with_transaction(
559        &mut self,
560        state: &crate::State,
561        mut options: Self::UpdateOptions,
562        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
563    ) -> Result<(), crate::database::DatabaseError> {
564        options.validate()?;
565
566        let spec_changed = options
567            .docker_images
568            .as_ref()
569            .is_some_and(|v| *v != self.docker_images)
570            || options.env.as_ref().is_some_and(|v| *v != self.env)
571            || options.image_uid.is_some_and(|v| v != self.image_uid)
572            || options.image_gid.is_some_and(|v| v != self.image_gid)
573            || options.cmd.as_ref().is_some_and(|v| *v != self.cmd)
574            || options.volumes.as_ref().is_some_and(|v| *v != self.volumes)
575            || options
576                .socket_path
577                .as_ref()
578                .is_some_and(|v| *v != self.socket_path)
579            || options.memory.is_some_and(|v| v != self.memory)
580            || options.swap.is_some_and(|v| v != self.swap)
581            || options.disk.is_some_and(|v| v != self.disk)
582            || options.io_weight.is_some_and(|v| v != self.io_weight)
583            || options.cpu.is_some_and(|v| v != self.cpu);
584
585        let mut query_builder = UpdateQueryBuilder::new("database_agent_templates");
586
587        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
588            .await?;
589
590        query_builder
591            .set("name", options.name.as_ref())
592            .set(
593                "description",
594                options.description.as_ref().map(|d| d.as_ref()),
595            )
596            .set("deployment_enabled", options.deployment_enabled)
597            .set(
598                "docker_images",
599                options.docker_images.as_ref().map(OrderedJson),
600            )
601            .set("env", options.env.as_ref().map(OrderedJson))
602            .set("image_uid", options.image_uid)
603            .set("image_gid", options.image_gid)
604            .set("cmd", options.cmd.as_ref().map(|c| c.as_ref()))
605            .set("volumes", options.volumes.as_ref().map(OrderedJson))
606            .set("socket_path", options.socket_path.as_ref())
607            .set("memory", options.memory)
608            .set("swap", options.swap)
609            .set("disk", options.disk)
610            .set("io_weight", options.io_weight.as_ref().map(|w| w.as_ref()))
611            .set("cpu", options.cpu)
612            .where_eq("uuid", self.uuid);
613
614        query_builder.execute(&mut **transaction).await?;
615
616        if spec_changed {
617            self.version = sqlx::query_scalar(
618                "UPDATE database_agent_templates SET version = version + 1 WHERE uuid = $1 RETURNING version",
619            )
620            .bind(self.uuid)
621            .fetch_one(&mut **transaction)
622            .await?;
623        }
624
625        if let Some(name) = options.name {
626            self.name = name;
627        }
628        if let Some(description) = options.description {
629            self.description = description;
630        }
631        if let Some(deployment_enabled) = options.deployment_enabled {
632            self.deployment_enabled = deployment_enabled;
633        }
634        if let Some(docker_images) = options.docker_images {
635            self.docker_images = docker_images;
636        }
637        if let Some(env) = options.env {
638            self.env = env;
639        }
640        if let Some(image_uid) = options.image_uid {
641            self.image_uid = image_uid;
642        }
643        if let Some(image_gid) = options.image_gid {
644            self.image_gid = image_gid;
645        }
646        if let Some(cmd) = options.cmd {
647            self.cmd = cmd;
648        }
649        if let Some(volumes) = options.volumes {
650            self.volumes = volumes;
651        }
652        if let Some(socket_path) = options.socket_path {
653            self.socket_path = socket_path;
654        }
655        if let Some(memory) = options.memory {
656            self.memory = memory;
657        }
658        if let Some(swap) = options.swap {
659            self.swap = swap;
660        }
661        if let Some(disk) = options.disk {
662            self.disk = disk;
663        }
664        if let Some(io_weight) = options.io_weight {
665            self.io_weight = io_weight;
666        }
667        if let Some(cpu) = options.cpu {
668            self.cpu = cpu;
669        }
670
671        self.run_after_update_handlers(state, transaction).await?;
672
673        Ok(())
674    }
675}
676
677#[async_trait::async_trait]
678impl DeletableModel for DatabaseAgentTemplate {
679    type DeleteOptions = ();
680
681    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
682        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<DatabaseAgentTemplate>> =
683            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
684
685        &DELETE_LISTENERS
686    }
687
688    async fn delete_with_transaction(
689        &self,
690        state: &crate::State,
691        options: Self::DeleteOptions,
692        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
693    ) -> Result<(), anyhow::Error> {
694        self.run_delete_handlers(&options, state, transaction)
695            .await?;
696
697        sqlx::query(
698            r#"
699            UPDATE server_database_instances SET
700                memory = COALESCE(memory, $2),
701                swap = COALESCE(swap, $3),
702                disk = COALESCE(disk, $4),
703                io_weight = COALESCE(io_weight, $5),
704                cpu = COALESCE(cpu, $6),
705                image = COALESCE(image, $7),
706                template_version = NULL
707            WHERE server_database_instances.database_agent_template_uuid = $1
708            "#,
709        )
710        .bind(self.uuid)
711        .bind(self.memory)
712        .bind(self.swap)
713        .bind(self.disk)
714        .bind(self.io_weight)
715        .bind(self.cpu)
716        .bind(self.docker_images.values().next())
717        .execute(&mut **transaction)
718        .await?;
719
720        let instance_envs = sqlx::query(
721            r#"
722            SELECT server_database_instances.uuid, server_database_instances.env
723            FROM server_database_instances
724            WHERE server_database_instances.database_agent_template_uuid = $1
725            "#,
726        )
727        .bind(self.uuid)
728        .fetch_all(&mut **transaction)
729        .await?;
730
731        for row in instance_envs {
732            let uuid: uuid::Uuid = row.try_get("uuid")?;
733            let env_overrides: Option<serde_json::Value> = row.try_get("env")?;
734
735            let mut env = self.env.clone();
736            if let Some(env_overrides) = env_overrides {
737                env.extend(serde_json::from_value::<
738                    IndexMap<compact_str::CompactString, compact_str::CompactString>,
739                >(env_overrides)?);
740            }
741
742            sqlx::query(
743                r#"
744                UPDATE server_database_instances
745                SET env = $2
746                WHERE server_database_instances.uuid = $1
747                "#,
748            )
749            .bind(uuid)
750            .bind(OrderedJson(&env))
751            .execute(&mut **transaction)
752            .await?;
753        }
754
755        sqlx::query(
756            r#"
757            DELETE FROM database_agent_templates
758            WHERE database_agent_templates.uuid = $1
759            "#,
760        )
761        .bind(self.uuid)
762        .execute(&mut **transaction)
763        .await?;
764
765        self.run_after_delete_handlers(&options, state, transaction)
766            .await?;
767
768        Ok(())
769    }
770}
771
772#[derive(Validate)]
773pub struct DuplicateDatabaseAgentTemplateOptions {
774    #[garde(length(chars, min = 1, max = 255))]
775    pub name: compact_str::CompactString,
776}
777
778#[async_trait::async_trait]
779impl DuplicableModel for DatabaseAgentTemplate {
780    type DuplicateOptions<'a> = DuplicateDatabaseAgentTemplateOptions;
781
782    fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
783        static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<DatabaseAgentTemplate>> =
784            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
785
786        &DUPLICATE_LISTENERS
787    }
788
789    async fn duplicate_with_transaction(
790        &self,
791        state: &crate::State,
792        options: Self::DuplicateOptions<'_>,
793        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
794    ) -> Result<Self, crate::database::DatabaseError> {
795        options.validate()?;
796
797        self.run_duplicate_handlers(&options, state, transaction)
798            .await?;
799
800        let mut query_builder = InsertQueryBuilder::new("database_agent_templates");
801
802        query_builder
803            .set("name", &options.name)
804            .set("description", &self.description)
805            .set("type", self.r#type)
806            .set("deployment_enabled", self.deployment_enabled)
807            .set("docker_images", OrderedJson(&self.docker_images))
808            .set("env", OrderedJson(&self.env))
809            .set("image_uid", self.image_uid)
810            .set("image_gid", self.image_gid)
811            .set("cmd", self.cmd.as_ref())
812            .set("volumes", OrderedJson(&self.volumes))
813            .set("socket_path", &self.socket_path)
814            .set("memory", self.memory)
815            .set("swap", self.swap)
816            .set("disk", self.disk)
817            .set("io_weight", self.io_weight)
818            .set("cpu", self.cpu);
819
820        let row = query_builder
821            .returning(&Self::columns_sql(None))
822            .fetch_one(&mut **transaction)
823            .await?;
824        let mut database_agent_template = Self::map(None, &row)?;
825
826        self.run_after_duplicate_handlers(
827            &mut database_agent_template,
828            &options,
829            state,
830            transaction,
831        )
832        .await?;
833
834        Ok(database_agent_template)
835    }
836}
837
838#[schema_extension_derive::extendible]
839#[init_args(DatabaseAgentTemplate, crate::State)]
840#[hook_args(crate::State)]
841#[derive(ToSchema, Serialize)]
842#[schema(title = "AdminDatabaseAgentTemplate")]
843pub struct AdminApiDatabaseAgentTemplate {
844    pub uuid: uuid::Uuid,
845
846    pub name: compact_str::CompactString,
847    pub description: Option<compact_str::CompactString>,
848
849    pub r#type: db_agent_api::DatabaseAgentType,
850    pub deployment_enabled: bool,
851
852    pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
853    pub env: IndexMap<compact_str::CompactString, compact_str::CompactString>,
854
855    pub image_uid: i32,
856    pub image_gid: i32,
857    pub cmd: Option<Vec<compact_str::CompactString>>,
858    pub volumes: IndexMap<compact_str::CompactString, compact_str::CompactString>,
859    pub socket_path: compact_str::CompactString,
860
861    pub memory: i64,
862    pub swap: i64,
863    pub disk: i64,
864    pub io_weight: Option<i16>,
865    pub cpu: i32,
866
867    pub version: i32,
868
869    pub created: chrono::DateTime<chrono::Utc>,
870}
871
872#[schema_extension_derive::extendible]
873#[init_args(DatabaseAgentTemplate, crate::State)]
874#[hook_args(crate::State)]
875#[derive(ToSchema, Serialize)]
876#[schema(title = "DatabaseAgentTemplate")]
877pub struct ApiDatabaseAgentTemplate {
878    pub uuid: uuid::Uuid,
879
880    pub name: compact_str::CompactString,
881    pub description: Option<compact_str::CompactString>,
882
883    pub r#type: db_agent_api::DatabaseAgentType,
884
885    pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
886
887    pub memory: i64,
888    pub swap: i64,
889    pub disk: i64,
890    pub io_weight: Option<i16>,
891    pub cpu: i32,
892
893    pub version: i32,
894
895    pub created: chrono::DateTime<chrono::Utc>,
896}