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    fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
204        vec![compact_str::format_compact!(
205            "{}::{}",
206            Self::NAME,
207            self.uuid
208        )]
209    }
210}
211
212impl DatabaseAgentTemplate {
213    pub async fn all_deployment_enabled(
214        database: &crate::database::Database,
215    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
216        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
217            r#"
218            SELECT {}
219            FROM database_agent_templates
220            WHERE database_agent_templates.deployment_enabled
221            ORDER BY database_agent_templates.created, database_agent_templates.uuid
222            "#,
223            Self::columns_sql(None)
224        )))
225        .fetch_all(database.read())
226        .await?;
227
228        rows.into_iter()
229            .map(|row| Self::map(None, &row))
230            .try_collect_vec()
231    }
232
233    pub async fn all_with_pagination(
234        database: &crate::database::Database,
235        page: i64,
236        per_page: i64,
237        search: Option<&str>,
238    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
239        super::PaginatedQueryBuilder::new(
240            Self::columns_sql(None),
241            format!(
242                r#"
243                FROM database_agent_templates
244                WHERE {search}
245                "#,
246                search = super::search_sql(
247                    1,
248                    &["database_agent_templates.name"],
249                    &["database_agent_templates.uuid"]
250                ),
251            ),
252            "database_agent_templates.created, database_agent_templates.uuid",
253        )
254        .bind(search)
255        .fetch(database, page, per_page)
256        .await
257    }
258}
259
260#[async_trait::async_trait]
261impl IntoAdminApiObject for DatabaseAgentTemplate {
262    type AdminApiObject = AdminApiDatabaseAgentTemplate;
263    type ExtraArgs<'a> = ();
264
265    async fn into_admin_api_object<'a>(
266        self,
267        state: &crate::State,
268        _args: Self::ExtraArgs<'a>,
269    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
270        let api_object = AdminApiDatabaseAgentTemplate::init_hooks(&self, state).await?;
271
272        let api_object = finish_extendible!(
273            AdminApiDatabaseAgentTemplate {
274                uuid: self.uuid,
275                name: self.name,
276                description: self.description,
277                r#type: self.r#type,
278                deployment_enabled: self.deployment_enabled,
279                docker_images: self.docker_images,
280                env: self.env,
281                image_uid: self.image_uid,
282                image_gid: self.image_gid,
283                cmd: self.cmd,
284                volumes: self.volumes,
285                socket_path: self.socket_path,
286                memory: self.memory,
287                swap: self.swap,
288                disk: self.disk,
289                io_weight: self.io_weight,
290                cpu: self.cpu,
291                version: self.version,
292                created: self.created.and_utc(),
293            },
294            api_object,
295            state
296        )?;
297
298        Ok(api_object)
299    }
300}
301
302#[async_trait::async_trait]
303impl IntoApiObject for DatabaseAgentTemplate {
304    type ApiObject = ApiDatabaseAgentTemplate;
305    type ExtraArgs<'a> = ();
306
307    async fn into_api_object<'a>(
308        self,
309        state: &crate::State,
310        _args: Self::ExtraArgs<'a>,
311    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
312        let api_object = ApiDatabaseAgentTemplate::init_hooks(&self, state).await?;
313
314        let api_object = finish_extendible!(
315            ApiDatabaseAgentTemplate {
316                uuid: self.uuid,
317                name: self.name,
318                description: self.description,
319                r#type: self.r#type,
320                docker_images: self.docker_images,
321                memory: self.memory,
322                swap: self.swap,
323                disk: self.disk,
324                io_weight: self.io_weight,
325                cpu: self.cpu,
326                version: self.version,
327                created: self.created.and_utc(),
328            },
329            api_object,
330            state
331        )?;
332
333        Ok(api_object)
334    }
335}
336
337#[async_trait::async_trait]
338impl ByUuid for DatabaseAgentTemplate {
339    async fn by_uuid(
340        database: &crate::database::Database,
341        uuid: uuid::Uuid,
342    ) -> Result<Self, crate::database::DatabaseError> {
343        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
344            r#"
345            SELECT {}
346            FROM database_agent_templates
347            WHERE database_agent_templates.uuid = $1
348            "#,
349            Self::columns_sql(None)
350        )))
351        .bind(uuid)
352        .fetch_one(database.read())
353        .await?;
354
355        Self::map(None, &row)
356    }
357
358    async fn by_uuid_with_transaction(
359        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
360        uuid: uuid::Uuid,
361    ) -> Result<Self, crate::database::DatabaseError> {
362        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
363            r#"
364            SELECT {}
365            FROM database_agent_templates
366            WHERE database_agent_templates.uuid = $1
367            "#,
368            Self::columns_sql(None)
369        )))
370        .bind(uuid)
371        .fetch_one(&mut **transaction)
372        .await?;
373
374        Self::map(None, &row)
375    }
376}
377
378#[derive(ToSchema, Deserialize, Validate)]
379pub struct CreateDatabaseAgentTemplateOptions {
380    #[garde(length(chars, min = 1, max = 255))]
381    #[schema(min_length = 1, max_length = 255)]
382    pub name: compact_str::CompactString,
383    #[garde(length(chars, min = 1, max = 1024))]
384    #[schema(min_length = 1, max_length = 1024)]
385    pub description: Option<compact_str::CompactString>,
386
387    #[garde(skip)]
388    pub r#type: db_agent_api::DatabaseAgentType,
389    #[garde(skip)]
390    pub deployment_enabled: bool,
391
392    #[garde(custom(validate_docker_images))]
393    pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
394    #[garde(skip)]
395    pub env: IndexMap<compact_str::CompactString, compact_str::CompactString>,
396
397    #[garde(range(min = 0))]
398    #[schema(minimum = 0)]
399    pub image_uid: i32,
400    #[garde(range(min = 0))]
401    #[schema(minimum = 0)]
402    pub image_gid: i32,
403    #[garde(skip)]
404    pub cmd: Option<Vec<compact_str::CompactString>>,
405    #[garde(skip)]
406    pub volumes: IndexMap<compact_str::CompactString, compact_str::CompactString>,
407    #[garde(length(chars, min = 1, max = 255))]
408    #[schema(min_length = 1, max_length = 255)]
409    pub socket_path: compact_str::CompactString,
410
411    #[garde(range(min = 0))]
412    #[schema(minimum = 0)]
413    pub memory: i64,
414    #[garde(range(min = -1))]
415    #[schema(minimum = -1)]
416    pub swap: i64,
417    #[garde(range(min = 0))]
418    #[schema(minimum = 0)]
419    pub disk: i64,
420    #[garde(range(min = 0, max = 1000))]
421    #[schema(minimum = 0, maximum = 1000)]
422    pub io_weight: Option<i16>,
423    #[garde(range(min = 0))]
424    #[schema(minimum = 0)]
425    pub cpu: i32,
426}
427
428#[async_trait::async_trait]
429impl CreatableModel for DatabaseAgentTemplate {
430    type CreateOptions<'a> = CreateDatabaseAgentTemplateOptions;
431    type CreateResult = Self;
432
433    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
434        static CREATE_LISTENERS: LazyLock<CreateListenerList<DatabaseAgentTemplate>> =
435            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
436
437        &CREATE_LISTENERS
438    }
439
440    async fn create_with_transaction(
441        state: &crate::State,
442        mut options: Self::CreateOptions<'_>,
443        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
444    ) -> Result<Self, crate::database::DatabaseError> {
445        options.validate()?;
446
447        let mut query_builder = InsertQueryBuilder::new("database_agent_templates");
448
449        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
450
451        query_builder
452            .set("name", &options.name)
453            .set("description", &options.description)
454            .set("type", options.r#type)
455            .set("deployment_enabled", options.deployment_enabled)
456            .set("docker_images", OrderedJson(&options.docker_images))
457            .set("env", OrderedJson(&options.env))
458            .set("image_uid", options.image_uid)
459            .set("image_gid", options.image_gid)
460            .set("cmd", options.cmd.as_ref())
461            .set("volumes", OrderedJson(&options.volumes))
462            .set("socket_path", &options.socket_path)
463            .set("memory", options.memory)
464            .set("swap", options.swap)
465            .set("disk", options.disk)
466            .set("io_weight", options.io_weight)
467            .set("cpu", options.cpu);
468
469        let row = query_builder
470            .returning(&Self::columns_sql(None))
471            .fetch_one(&mut **transaction)
472            .await?;
473        let mut database_agent_template = Self::map(None, &row)?;
474
475        Self::run_after_create_handlers(&mut database_agent_template, &options, state, transaction)
476            .await?;
477
478        Ok(database_agent_template)
479    }
480}
481
482#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
483pub struct UpdateDatabaseAgentTemplateOptions {
484    #[garde(length(chars, min = 1, max = 255))]
485    #[schema(min_length = 1, max_length = 255)]
486    pub name: Option<compact_str::CompactString>,
487    #[garde(length(chars, min = 1, max = 1024))]
488    #[schema(min_length = 1, max_length = 1024)]
489    #[serde(
490        default,
491        skip_serializing_if = "Option::is_none",
492        with = "::serde_with::rust::double_option"
493    )]
494    pub description: Option<Option<compact_str::CompactString>>,
495
496    #[garde(skip)]
497    pub deployment_enabled: Option<bool>,
498
499    #[garde(inner(custom(validate_docker_images)))]
500    pub docker_images: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
501    #[garde(skip)]
502    pub env: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
503
504    #[garde(range(min = 0))]
505    #[schema(minimum = 0)]
506    pub image_uid: Option<i32>,
507    #[garde(range(min = 0))]
508    #[schema(minimum = 0)]
509    pub image_gid: Option<i32>,
510    #[garde(skip)]
511    #[serde(
512        default,
513        skip_serializing_if = "Option::is_none",
514        with = "::serde_with::rust::double_option"
515    )]
516    pub cmd: Option<Option<Vec<compact_str::CompactString>>>,
517    #[garde(skip)]
518    pub volumes: Option<IndexMap<compact_str::CompactString, compact_str::CompactString>>,
519    #[garde(length(chars, min = 1, max = 255))]
520    #[schema(min_length = 1, max_length = 255)]
521    pub socket_path: Option<compact_str::CompactString>,
522
523    #[garde(range(min = 0))]
524    #[schema(minimum = 0)]
525    pub memory: Option<i64>,
526    #[garde(range(min = -1))]
527    #[schema(minimum = -1)]
528    pub swap: Option<i64>,
529    #[garde(range(min = 0))]
530    #[schema(minimum = 0)]
531    pub disk: Option<i64>,
532    #[garde(skip)]
533    #[serde(
534        default,
535        skip_serializing_if = "Option::is_none",
536        with = "::serde_with::rust::double_option"
537    )]
538    pub io_weight: Option<Option<i16>>,
539    #[garde(range(min = 0))]
540    #[schema(minimum = 0)]
541    pub cpu: Option<i32>,
542}
543
544#[async_trait::async_trait]
545impl UpdatableModel for DatabaseAgentTemplate {
546    type UpdateOptions = UpdateDatabaseAgentTemplateOptions;
547
548    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
549        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<DatabaseAgentTemplate>> =
550            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
551
552        &UPDATE_LISTENERS
553    }
554
555    async fn update_with_transaction(
556        &mut self,
557        state: &crate::State,
558        mut options: Self::UpdateOptions,
559        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
560    ) -> Result<(), crate::database::DatabaseError> {
561        options.validate()?;
562
563        let spec_changed = options
564            .docker_images
565            .as_ref()
566            .is_some_and(|v| *v != self.docker_images)
567            || options.env.as_ref().is_some_and(|v| *v != self.env)
568            || options.image_uid.is_some_and(|v| v != self.image_uid)
569            || options.image_gid.is_some_and(|v| v != self.image_gid)
570            || options.cmd.as_ref().is_some_and(|v| *v != self.cmd)
571            || options.volumes.as_ref().is_some_and(|v| *v != self.volumes)
572            || options
573                .socket_path
574                .as_ref()
575                .is_some_and(|v| *v != self.socket_path)
576            || options.memory.is_some_and(|v| v != self.memory)
577            || options.swap.is_some_and(|v| v != self.swap)
578            || options.disk.is_some_and(|v| v != self.disk)
579            || options.io_weight.is_some_and(|v| v != self.io_weight)
580            || options.cpu.is_some_and(|v| v != self.cpu);
581
582        let mut query_builder = UpdateQueryBuilder::new("database_agent_templates");
583
584        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
585            .await?;
586
587        query_builder
588            .set("name", options.name.as_ref())
589            .set(
590                "description",
591                options.description.as_ref().map(|d| d.as_ref()),
592            )
593            .set("deployment_enabled", options.deployment_enabled)
594            .set(
595                "docker_images",
596                options.docker_images.as_ref().map(OrderedJson),
597            )
598            .set("env", options.env.as_ref().map(OrderedJson))
599            .set("image_uid", options.image_uid)
600            .set("image_gid", options.image_gid)
601            .set("cmd", options.cmd.as_ref().map(|c| c.as_ref()))
602            .set("volumes", options.volumes.as_ref().map(OrderedJson))
603            .set("socket_path", options.socket_path.as_ref())
604            .set("memory", options.memory)
605            .set("swap", options.swap)
606            .set("disk", options.disk)
607            .set("io_weight", options.io_weight.as_ref().map(|w| w.as_ref()))
608            .set("cpu", options.cpu)
609            .where_eq("uuid", self.uuid);
610
611        query_builder.execute(&mut **transaction).await?;
612
613        if spec_changed {
614            self.version = sqlx::query_scalar(
615                "UPDATE database_agent_templates SET version = version + 1 WHERE uuid = $1 RETURNING version",
616            )
617            .bind(self.uuid)
618            .fetch_one(&mut **transaction)
619            .await?;
620        }
621
622        if let Some(name) = options.name {
623            self.name = name;
624        }
625        if let Some(description) = options.description {
626            self.description = description;
627        }
628        if let Some(deployment_enabled) = options.deployment_enabled {
629            self.deployment_enabled = deployment_enabled;
630        }
631        if let Some(docker_images) = options.docker_images {
632            self.docker_images = docker_images;
633        }
634        if let Some(env) = options.env {
635            self.env = env;
636        }
637        if let Some(image_uid) = options.image_uid {
638            self.image_uid = image_uid;
639        }
640        if let Some(image_gid) = options.image_gid {
641            self.image_gid = image_gid;
642        }
643        if let Some(cmd) = options.cmd {
644            self.cmd = cmd;
645        }
646        if let Some(volumes) = options.volumes {
647            self.volumes = volumes;
648        }
649        if let Some(socket_path) = options.socket_path {
650            self.socket_path = socket_path;
651        }
652        if let Some(memory) = options.memory {
653            self.memory = memory;
654        }
655        if let Some(swap) = options.swap {
656            self.swap = swap;
657        }
658        if let Some(disk) = options.disk {
659            self.disk = disk;
660        }
661        if let Some(io_weight) = options.io_weight {
662            self.io_weight = io_weight;
663        }
664        if let Some(cpu) = options.cpu {
665            self.cpu = cpu;
666        }
667
668        self.run_after_update_handlers(state, transaction).await?;
669
670        Ok(())
671    }
672}
673
674#[async_trait::async_trait]
675impl DeletableModel for DatabaseAgentTemplate {
676    type DeleteOptions = ();
677
678    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
679        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<DatabaseAgentTemplate>> =
680            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
681
682        &DELETE_LISTENERS
683    }
684
685    async fn delete_with_transaction(
686        &self,
687        state: &crate::State,
688        options: Self::DeleteOptions,
689        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
690    ) -> Result<(), anyhow::Error> {
691        self.run_delete_handlers(&options, state, transaction)
692            .await?;
693
694        sqlx::query(
695            r#"
696            UPDATE server_database_instances SET
697                memory = COALESCE(memory, $2),
698                swap = COALESCE(swap, $3),
699                disk = COALESCE(disk, $4),
700                io_weight = COALESCE(io_weight, $5),
701                cpu = COALESCE(cpu, $6),
702                image = COALESCE(image, $7),
703                template_version = NULL
704            WHERE server_database_instances.database_agent_template_uuid = $1
705            "#,
706        )
707        .bind(self.uuid)
708        .bind(self.memory)
709        .bind(self.swap)
710        .bind(self.disk)
711        .bind(self.io_weight)
712        .bind(self.cpu)
713        .bind(self.docker_images.values().next())
714        .execute(&mut **transaction)
715        .await?;
716
717        let instance_envs = sqlx::query(
718            r#"
719            SELECT server_database_instances.uuid, server_database_instances.env
720            FROM server_database_instances
721            WHERE server_database_instances.database_agent_template_uuid = $1
722            "#,
723        )
724        .bind(self.uuid)
725        .fetch_all(&mut **transaction)
726        .await?;
727
728        for row in instance_envs {
729            let uuid: uuid::Uuid = row.try_get("uuid")?;
730            let env_overrides: Option<serde_json::Value> = row.try_get("env")?;
731
732            let mut env = self.env.clone();
733            if let Some(env_overrides) = env_overrides {
734                env.extend(serde_json::from_value::<
735                    IndexMap<compact_str::CompactString, compact_str::CompactString>,
736                >(env_overrides)?);
737            }
738
739            sqlx::query(
740                r#"
741                UPDATE server_database_instances
742                SET env = $2
743                WHERE server_database_instances.uuid = $1
744                "#,
745            )
746            .bind(uuid)
747            .bind(OrderedJson(&env))
748            .execute(&mut **transaction)
749            .await?;
750        }
751
752        sqlx::query(
753            r#"
754            DELETE FROM database_agent_templates
755            WHERE database_agent_templates.uuid = $1
756            "#,
757        )
758        .bind(self.uuid)
759        .execute(&mut **transaction)
760        .await?;
761
762        self.run_after_delete_handlers(&options, state, transaction)
763            .await?;
764
765        Ok(())
766    }
767}
768
769#[derive(Validate)]
770pub struct DuplicateDatabaseAgentTemplateOptions {
771    #[garde(length(chars, min = 1, max = 255))]
772    pub name: compact_str::CompactString,
773}
774
775#[async_trait::async_trait]
776impl DuplicableModel for DatabaseAgentTemplate {
777    type DuplicateOptions<'a> = DuplicateDatabaseAgentTemplateOptions;
778    type DuplicateResult = Self;
779
780    fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
781        static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<DatabaseAgentTemplate>> =
782            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
783
784        &DUPLICATE_LISTENERS
785    }
786
787    async fn duplicate_with_transaction(
788        &self,
789        state: &crate::State,
790        options: Self::DuplicateOptions<'_>,
791        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
792    ) -> Result<Self, crate::database::DatabaseError> {
793        options.validate()?;
794
795        self.run_duplicate_handlers(&options, state, transaction)
796            .await?;
797
798        let mut query_builder = InsertQueryBuilder::new("database_agent_templates");
799
800        query_builder
801            .set("name", &options.name)
802            .set("description", &self.description)
803            .set("type", self.r#type)
804            .set("deployment_enabled", self.deployment_enabled)
805            .set("docker_images", OrderedJson(&self.docker_images))
806            .set("env", OrderedJson(&self.env))
807            .set("image_uid", self.image_uid)
808            .set("image_gid", self.image_gid)
809            .set("cmd", self.cmd.as_ref())
810            .set("volumes", OrderedJson(&self.volumes))
811            .set("socket_path", &self.socket_path)
812            .set("memory", self.memory)
813            .set("swap", self.swap)
814            .set("disk", self.disk)
815            .set("io_weight", self.io_weight)
816            .set("cpu", self.cpu);
817
818        let row = query_builder
819            .returning(&Self::columns_sql(None))
820            .fetch_one(&mut **transaction)
821            .await?;
822        let mut database_agent_template = Self::map(None, &row)?;
823
824        self.run_after_duplicate_handlers(
825            &mut database_agent_template,
826            &options,
827            state,
828            transaction,
829        )
830        .await?;
831
832        Ok(database_agent_template)
833    }
834}
835
836#[schema_extension_derive::extendible]
837#[init_args(DatabaseAgentTemplate, crate::State)]
838#[hook_args(crate::State)]
839#[derive(ToSchema, Serialize)]
840#[schema(title = "AdminDatabaseAgentTemplate")]
841pub struct AdminApiDatabaseAgentTemplate {
842    pub uuid: uuid::Uuid,
843
844    pub name: compact_str::CompactString,
845    pub description: Option<compact_str::CompactString>,
846
847    pub r#type: db_agent_api::DatabaseAgentType,
848    pub deployment_enabled: bool,
849
850    pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
851    pub env: IndexMap<compact_str::CompactString, compact_str::CompactString>,
852
853    pub image_uid: i32,
854    pub image_gid: i32,
855    pub cmd: Option<Vec<compact_str::CompactString>>,
856    pub volumes: IndexMap<compact_str::CompactString, compact_str::CompactString>,
857    pub socket_path: compact_str::CompactString,
858
859    pub memory: i64,
860    pub swap: i64,
861    pub disk: i64,
862    pub io_weight: Option<i16>,
863    pub cpu: i32,
864
865    pub version: i32,
866
867    pub created: chrono::DateTime<chrono::Utc>,
868}
869
870#[schema_extension_derive::extendible]
871#[init_args(DatabaseAgentTemplate, crate::State)]
872#[hook_args(crate::State)]
873#[derive(ToSchema, Serialize)]
874#[schema(title = "DatabaseAgentTemplate")]
875pub struct ApiDatabaseAgentTemplate {
876    pub uuid: uuid::Uuid,
877
878    pub name: compact_str::CompactString,
879    pub description: Option<compact_str::CompactString>,
880
881    pub r#type: db_agent_api::DatabaseAgentType,
882
883    pub docker_images: IndexMap<compact_str::CompactString, compact_str::CompactString>,
884
885    pub memory: i64,
886    pub swap: i64,
887    pub disk: i64,
888    pub io_weight: Option<i16>,
889    pub cpu: i32,
890
891    pub version: i32,
892
893    pub created: chrono::DateTime<chrono::Utc>,
894}