1use crate::{
2 models::{InsertQueryBuilder, UpdateQueryBuilder},
3 prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow};
8use std::{
9 collections::BTreeMap,
10 sync::{Arc, LazyLock},
11};
12use utoipa::ToSchema;
13
14#[derive(Serialize, Deserialize, Clone)]
15pub struct Location {
16 pub uuid: uuid::Uuid,
17 pub backup_configuration: Option<Fetchable<super::backup_configuration::BackupConfiguration>>,
18
19 pub name: compact_str::CompactString,
20 pub description: Option<compact_str::CompactString>,
21
22 pub flag: Option<compact_str::CompactString>,
23
24 pub created: chrono::NaiveDateTime,
25
26 extension_data: super::ModelExtensionData,
27}
28
29impl BaseModel for Location {
30 const NAME: &'static str = "location";
31
32 fn get_extension_list() -> &'static super::ModelExtensionList {
33 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
34 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
35
36 &EXTENSIONS
37 }
38
39 fn get_extension_data(&self) -> &super::ModelExtensionData {
40 &self.extension_data
41 }
42
43 #[inline]
44 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
45 let prefix = prefix.unwrap_or_default();
46
47 BTreeMap::from([
48 (
49 "locations.uuid",
50 compact_str::format_compact!("{prefix}uuid"),
51 ),
52 (
53 "locations.backup_configuration_uuid",
54 compact_str::format_compact!("{prefix}location_backup_configuration_uuid"),
55 ),
56 (
57 "locations.name",
58 compact_str::format_compact!("{prefix}name"),
59 ),
60 (
61 "locations.description",
62 compact_str::format_compact!("{prefix}description"),
63 ),
64 (
65 "locations.flag",
66 compact_str::format_compact!("{prefix}flag"),
67 ),
68 (
69 "locations.created",
70 compact_str::format_compact!("{prefix}created"),
71 ),
72 ])
73 }
74
75 #[inline]
76 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
77 let prefix = prefix.unwrap_or_default();
78
79 Ok(Self {
80 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
81 backup_configuration:
82 super::backup_configuration::BackupConfiguration::get_fetchable_from_row(
83 row,
84 compact_str::format_compact!("{prefix}location_backup_configuration_uuid"),
85 ),
86 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
87 description: row
88 .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
89 flag: row.try_get(compact_str::format_compact!("{prefix}flag").as_str())?,
90 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
91 extension_data: Self::map_extensions(prefix, row)?,
92 })
93 }
94
95 fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
96 vec![compact_str::format_compact!(
97 "{}::{}",
98 Self::NAME,
99 self.uuid
100 )]
101 }
102}
103
104impl Location {
105 pub async fn by_backup_configuration_uuid_with_pagination(
106 database: &crate::database::Database,
107 backup_configuration_uuid: uuid::Uuid,
108 page: i64,
109 per_page: i64,
110 search: Option<&str>,
111 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
112 super::PaginatedQueryBuilder::new(
113 Self::columns_sql(None),
114 format!(
115 r#"
116 FROM locations
117 WHERE locations.backup_configuration_uuid = $1 AND {search}
118 "#,
119 search = super::search_sql(2, &["locations.name"], &["locations.uuid"]),
120 ),
121 "locations.created, locations.uuid",
122 )
123 .bind(backup_configuration_uuid)
124 .bind(search)
125 .fetch(database, page, per_page)
126 .await
127 }
128
129 pub async fn all_with_pagination(
130 database: &crate::database::Database,
131 page: i64,
132 per_page: i64,
133 search: Option<&str>,
134 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
135 super::PaginatedQueryBuilder::new(
136 Self::columns_sql(None),
137 format!(
138 r#"
139 FROM locations
140 WHERE {search}
141 "#,
142 search = super::search_sql(1, &["locations.name"], &["locations.uuid"]),
143 ),
144 "locations.created, locations.uuid",
145 )
146 .bind(search)
147 .fetch(database, page, per_page)
148 .await
149 }
150}
151
152#[async_trait::async_trait]
153impl IntoAdminApiObject for Location {
154 type AdminApiObject = AdminApiLocation;
155 type ExtraArgs<'a> = ();
156
157 async fn into_admin_api_object<'a>(
158 self,
159 state: &crate::State,
160 _args: Self::ExtraArgs<'a>,
161 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
162 let api_object = AdminApiLocation::init_hooks(&self, state).await?;
163
164 let api_object = finish_extendible!(
165 AdminApiLocation {
166 uuid: self.uuid,
167 backup_configuration: if let Some(backup_configuration) = self.backup_configuration
168 {
169 if let Ok(backup_configuration) =
170 backup_configuration.fetch_cached(&state.database).await
171 {
172 backup_configuration
173 .into_admin_api_object(state, ())
174 .await
175 .ok()
176 } else {
177 None
178 }
179 } else {
180 None
181 },
182 name: self.name,
183 description: self.description,
184 flag: self.flag,
185 created: self.created.and_utc(),
186 },
187 api_object,
188 state
189 )?;
190
191 Ok(api_object)
192 }
193}
194
195#[async_trait::async_trait]
196impl ByUuid for Location {
197 async fn by_uuid(
198 database: &crate::database::Database,
199 uuid: uuid::Uuid,
200 ) -> Result<Self, crate::database::DatabaseError> {
201 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
202 r#"
203 SELECT {}
204 FROM locations
205 WHERE locations.uuid = $1
206 "#,
207 Self::columns_sql(None)
208 )))
209 .bind(uuid)
210 .fetch_one(database.read())
211 .await?;
212
213 Self::map(None, &row)
214 }
215
216 async fn by_uuid_with_transaction(
217 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
218 uuid: uuid::Uuid,
219 ) -> Result<Self, crate::database::DatabaseError> {
220 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
221 r#"
222 SELECT {}
223 FROM locations
224 WHERE locations.uuid = $1
225 "#,
226 Self::columns_sql(None)
227 )))
228 .bind(uuid)
229 .fetch_one(&mut **transaction)
230 .await?;
231
232 Self::map(None, &row)
233 }
234}
235
236#[derive(ToSchema, Deserialize, Validate)]
237pub struct CreateLocationOptions {
238 #[garde(skip)]
239 pub backup_configuration_uuid: Option<uuid::Uuid>,
240 #[garde(length(chars, min = 1, max = 255))]
241 #[schema(min_length = 1, max_length = 255)]
242 pub name: compact_str::CompactString,
243 #[garde(length(chars, min = 1, max = 1024))]
244 #[schema(min_length = 1, max_length = 1024)]
245 pub description: Option<compact_str::CompactString>,
246 #[garde(length(chars, min = 2, max = 2))]
247 #[schema(min_length = 2, max_length = 2)]
248 pub flag: Option<compact_str::CompactString>,
249}
250
251#[async_trait::async_trait]
252impl CreatableModel for Location {
253 type CreateOptions<'a> = CreateLocationOptions;
254 type CreateResult = Self;
255
256 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
257 static CREATE_LISTENERS: LazyLock<CreateListenerList<Location>> =
258 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
259
260 &CREATE_LISTENERS
261 }
262
263 async fn create_with_transaction(
264 state: &crate::State,
265 mut options: Self::CreateOptions<'_>,
266 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
267 ) -> Result<Self, crate::database::DatabaseError> {
268 options.validate()?;
269
270 if let Some(backup_configuration_uuid) = &options.backup_configuration_uuid {
271 super::backup_configuration::BackupConfiguration::by_uuid_optional_cached(
272 &state.database,
273 *backup_configuration_uuid,
274 )
275 .await?
276 .ok_or(crate::database::InvalidRelationError(
277 "backup_configuration",
278 ))?;
279 }
280
281 let mut query_builder = InsertQueryBuilder::new("locations");
282
283 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
284
285 query_builder
286 .set(
287 "backup_configuration_uuid",
288 options.backup_configuration_uuid,
289 )
290 .set("name", &options.name)
291 .set("description", &options.description)
292 .set("flag", &options.flag);
293
294 let row = query_builder
295 .returning(&Self::columns_sql(None))
296 .fetch_one(&mut **transaction)
297 .await?;
298 let mut location = Self::map(None, &row)?;
299
300 Self::run_after_create_handlers(&mut location, &options, state, transaction).await?;
301
302 Ok(location)
303 }
304}
305
306#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
307pub struct UpdateLocationOptions {
308 #[garde(skip)]
309 #[serde(
310 default,
311 skip_serializing_if = "Option::is_none",
312 with = "::serde_with::rust::double_option"
313 )]
314 pub backup_configuration_uuid: Option<Option<uuid::Uuid>>,
315 #[garde(length(chars, min = 1, max = 255))]
316 #[schema(min_length = 1, max_length = 255)]
317 pub name: Option<compact_str::CompactString>,
318 #[garde(length(chars, min = 1, max = 1024))]
319 #[schema(min_length = 1, max_length = 1024)]
320 #[serde(
321 default,
322 skip_serializing_if = "Option::is_none",
323 with = "::serde_with::rust::double_option"
324 )]
325 pub description: Option<Option<compact_str::CompactString>>,
326 #[garde(length(chars, min = 2, max = 2))]
327 #[schema(min_length = 2, max_length = 2)]
328 #[serde(
329 default,
330 skip_serializing_if = "Option::is_none",
331 with = "::serde_with::rust::double_option"
332 )]
333 pub flag: Option<Option<compact_str::CompactString>>,
334}
335
336#[async_trait::async_trait]
337impl UpdatableModel for Location {
338 type UpdateOptions = UpdateLocationOptions;
339
340 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
341 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Location>> =
342 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
343
344 &UPDATE_LISTENERS
345 }
346
347 async fn update_with_transaction(
348 &mut self,
349 state: &crate::State,
350 mut options: Self::UpdateOptions,
351 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
352 ) -> Result<(), crate::database::DatabaseError> {
353 options.validate()?;
354
355 let backup_configuration =
356 if let Some(backup_configuration_uuid) = &options.backup_configuration_uuid {
357 match backup_configuration_uuid {
358 Some(uuid) => {
359 super::backup_configuration::BackupConfiguration::by_uuid_optional_cached(
360 &state.database,
361 *uuid,
362 )
363 .await?
364 .ok_or(crate::database::InvalidRelationError(
365 "backup_configuration",
366 ))?;
367
368 Some(Some(
369 super::backup_configuration::BackupConfiguration::get_fetchable(*uuid),
370 ))
371 }
372 None => Some(None),
373 }
374 } else {
375 None
376 };
377
378 let mut query_builder = UpdateQueryBuilder::new("locations");
379
380 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
381 .await?;
382
383 query_builder
384 .set(
385 "backup_configuration_uuid",
386 options.backup_configuration_uuid.as_ref(),
387 )
388 .set("name", options.name.as_ref())
389 .set("description", options.description.as_ref())
390 .set("flag", options.flag.as_ref())
391 .where_eq("uuid", self.uuid);
392
393 query_builder.execute(&mut **transaction).await?;
394
395 if let Some(backup_configuration) = backup_configuration {
396 self.backup_configuration = backup_configuration;
397 }
398 if let Some(name) = options.name {
399 self.name = name;
400 }
401 if let Some(description) = options.description {
402 self.description = description;
403 }
404 if let Some(flag) = options.flag {
405 self.flag = flag;
406 }
407
408 self.run_after_update_handlers(state, transaction).await?;
409
410 Ok(())
411 }
412}
413
414#[async_trait::async_trait]
415impl DeletableModel for Location {
416 type DeleteOptions = ();
417
418 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
419 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Location>> =
420 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
421
422 &DELETE_LISTENERS
423 }
424
425 async fn delete_with_transaction(
426 &self,
427 state: &crate::State,
428 options: Self::DeleteOptions,
429 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
430 ) -> Result<(), anyhow::Error> {
431 self.run_delete_handlers(&options, state, transaction)
432 .await?;
433
434 sqlx::query(
435 r#"
436 DELETE FROM locations
437 WHERE locations.uuid = $1
438 "#,
439 )
440 .bind(self.uuid)
441 .execute(&mut **transaction)
442 .await?;
443
444 self.run_after_delete_handlers(&options, state, transaction)
445 .await?;
446
447 Ok(())
448 }
449}
450
451#[derive(Validate)]
452pub struct DuplicateLocationOptions {
453 #[garde(length(chars, min = 1, max = 255))]
454 pub name: compact_str::CompactString,
455}
456
457#[async_trait::async_trait]
458impl DuplicableModel for Location {
459 type DuplicateOptions<'a> = DuplicateLocationOptions;
460 type DuplicateResult = Self;
461
462 fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
463 static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<Location>> =
464 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
465
466 &DUPLICATE_LISTENERS
467 }
468
469 async fn duplicate_with_transaction(
470 &self,
471 state: &crate::State,
472 options: Self::DuplicateOptions<'_>,
473 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
474 ) -> Result<Self, crate::database::DatabaseError> {
475 options.validate()?;
476
477 self.run_duplicate_handlers(&options, state, transaction)
478 .await?;
479
480 let mut query_builder = InsertQueryBuilder::new("locations");
481
482 query_builder
483 .set(
484 "backup_configuration_uuid",
485 self.backup_configuration.as_ref().map(|c| c.uuid),
486 )
487 .set("name", &options.name)
488 .set("description", &self.description)
489 .set("flag", &self.flag);
490
491 let row = query_builder
492 .returning(&Self::columns_sql(None))
493 .fetch_one(&mut **transaction)
494 .await?;
495 let mut location = Self::map(None, &row)?;
496
497 sqlx::query!(
498 "INSERT INTO location_database_hosts (location_uuid, database_host_uuid)
499 SELECT $1, location_database_hosts.database_host_uuid
500 FROM location_database_hosts
501 WHERE location_database_hosts.location_uuid = $2",
502 location.uuid,
503 self.uuid,
504 )
505 .execute(&mut **transaction)
506 .await?;
507
508 sqlx::query!(
509 "INSERT INTO location_database_agent_hosts (location_uuid, database_agent_host_uuid)
510 SELECT $1, location_database_agent_hosts.database_agent_host_uuid
511 FROM location_database_agent_hosts
512 WHERE location_database_agent_hosts.location_uuid = $2",
513 location.uuid,
514 self.uuid,
515 )
516 .execute(&mut **transaction)
517 .await?;
518
519 self.run_after_duplicate_handlers(&mut location, &options, state, transaction)
520 .await?;
521
522 Ok(location)
523 }
524}
525
526#[schema_extension_derive::extendible]
527#[init_args(Location, crate::State)]
528#[hook_args(crate::State)]
529#[derive(ToSchema, Serialize)]
530#[schema(title = "Location")]
531pub struct AdminApiLocation {
532 pub uuid: uuid::Uuid,
533 pub backup_configuration: Option<super::backup_configuration::AdminApiBackupConfiguration>,
534
535 pub name: compact_str::CompactString,
536 pub description: Option<compact_str::CompactString>,
537
538 pub flag: Option<compact_str::CompactString>,
539
540 pub created: chrono::DateTime<chrono::Utc>,
541}