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}