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}