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
96impl Location {
97 pub async fn by_backup_configuration_uuid_with_pagination(
98 database: &crate::database::Database,
99 backup_configuration_uuid: uuid::Uuid,
100 page: i64,
101 per_page: i64,
102 search: Option<&str>,
103 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
104 let offset = (page - 1) * per_page;
105
106 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
107 r#"
108 SELECT {}, COUNT(*) OVER() AS total_count
109 FROM locations
110 WHERE locations.backup_configuration_uuid = $1 AND ($2 IS NULL OR locations.name ILIKE '%' || $2 || '%')
111 ORDER BY locations.created
112 LIMIT $3 OFFSET $4
113 "#,
114 Self::columns_sql(None)
115 )))
116 .bind(backup_configuration_uuid)
117 .bind(search)
118 .bind(per_page)
119 .bind(offset)
120 .fetch_all(database.read())
121 .await?;
122
123 Ok(super::Pagination {
124 total: rows
125 .first()
126 .map_or(Ok(0), |row| row.try_get("total_count"))?,
127 per_page,
128 page,
129 data: rows
130 .into_iter()
131 .map(|row| Self::map(None, &row))
132 .try_collect_vec()?,
133 })
134 }
135
136 pub async fn all_with_pagination(
137 database: &crate::database::Database,
138 page: i64,
139 per_page: i64,
140 search: Option<&str>,
141 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
142 let offset = (page - 1) * per_page;
143
144 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
145 r#"
146 SELECT {}, COUNT(*) OVER() AS total_count
147 FROM locations
148 WHERE $1 IS NULL OR locations.name ILIKE '%' || $1 || '%'
149 ORDER BY locations.created
150 LIMIT $2 OFFSET $3
151 "#,
152 Self::columns_sql(None)
153 )))
154 .bind(search)
155 .bind(per_page)
156 .bind(offset)
157 .fetch_all(database.read())
158 .await?;
159
160 Ok(super::Pagination {
161 total: rows
162 .first()
163 .map_or(Ok(0), |row| row.try_get("total_count"))?,
164 per_page,
165 page,
166 data: rows
167 .into_iter()
168 .map(|row| Self::map(None, &row))
169 .try_collect_vec()?,
170 })
171 }
172}
173
174#[async_trait::async_trait]
175impl IntoAdminApiObject for Location {
176 type AdminApiObject = AdminApiLocation;
177 type ExtraArgs<'a> = ();
178
179 async fn into_admin_api_object<'a>(
180 self,
181 state: &crate::State,
182 _args: Self::ExtraArgs<'a>,
183 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
184 let api_object = AdminApiLocation::init_hooks(&self, state).await?;
185
186 let api_object = finish_extendible!(
187 AdminApiLocation {
188 uuid: self.uuid,
189 backup_configuration: if let Some(backup_configuration) = self.backup_configuration
190 {
191 if let Ok(backup_configuration) =
192 backup_configuration.fetch_cached(&state.database).await
193 {
194 backup_configuration
195 .into_admin_api_object(state, ())
196 .await
197 .ok()
198 } else {
199 None
200 }
201 } else {
202 None
203 },
204 name: self.name,
205 description: self.description,
206 flag: self.flag,
207 created: self.created.and_utc(),
208 },
209 api_object,
210 state
211 )?;
212
213 Ok(api_object)
214 }
215}
216
217#[async_trait::async_trait]
218impl ByUuid for Location {
219 async fn by_uuid(
220 database: &crate::database::Database,
221 uuid: uuid::Uuid,
222 ) -> Result<Self, crate::database::DatabaseError> {
223 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
224 r#"
225 SELECT {}
226 FROM locations
227 WHERE locations.uuid = $1
228 "#,
229 Self::columns_sql(None)
230 )))
231 .bind(uuid)
232 .fetch_one(database.read())
233 .await?;
234
235 Self::map(None, &row)
236 }
237
238 async fn by_uuid_with_transaction(
239 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
240 uuid: uuid::Uuid,
241 ) -> Result<Self, crate::database::DatabaseError> {
242 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
243 r#"
244 SELECT {}
245 FROM locations
246 WHERE locations.uuid = $1
247 "#,
248 Self::columns_sql(None)
249 )))
250 .bind(uuid)
251 .fetch_one(&mut **transaction)
252 .await?;
253
254 Self::map(None, &row)
255 }
256}
257
258#[derive(ToSchema, Deserialize, Validate)]
259pub struct CreateLocationOptions {
260 #[garde(skip)]
261 pub backup_configuration_uuid: Option<uuid::Uuid>,
262 #[garde(length(chars, min = 1, max = 255))]
263 #[schema(min_length = 1, max_length = 255)]
264 pub name: compact_str::CompactString,
265 #[garde(length(chars, min = 1, max = 1024))]
266 #[schema(min_length = 1, max_length = 1024)]
267 pub description: Option<compact_str::CompactString>,
268 #[garde(length(chars, min = 2, max = 2))]
269 #[schema(min_length = 2, max_length = 2)]
270 pub flag: Option<compact_str::CompactString>,
271}
272
273#[async_trait::async_trait]
274impl CreatableModel for Location {
275 type CreateOptions<'a> = CreateLocationOptions;
276 type CreateResult = Self;
277
278 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
279 static CREATE_LISTENERS: LazyLock<CreateListenerList<Location>> =
280 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
281
282 &CREATE_LISTENERS
283 }
284
285 async fn create_with_transaction(
286 state: &crate::State,
287 mut options: Self::CreateOptions<'_>,
288 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
289 ) -> Result<Self, crate::database::DatabaseError> {
290 options.validate()?;
291
292 if let Some(backup_configuration_uuid) = &options.backup_configuration_uuid {
293 super::backup_configuration::BackupConfiguration::by_uuid_optional_cached(
294 &state.database,
295 *backup_configuration_uuid,
296 )
297 .await?
298 .ok_or(crate::database::InvalidRelationError(
299 "backup_configuration",
300 ))?;
301 }
302
303 let mut query_builder = InsertQueryBuilder::new("locations");
304
305 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
306
307 query_builder
308 .set(
309 "backup_configuration_uuid",
310 options.backup_configuration_uuid,
311 )
312 .set("name", &options.name)
313 .set("description", &options.description)
314 .set("flag", &options.flag);
315
316 let row = query_builder
317 .returning(&Self::columns_sql(None))
318 .fetch_one(&mut **transaction)
319 .await?;
320 let mut location = Self::map(None, &row)?;
321
322 Self::run_after_create_handlers(&mut location, &options, state, transaction).await?;
323
324 Ok(location)
325 }
326}
327
328#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
329pub struct UpdateLocationOptions {
330 #[garde(skip)]
331 #[serde(
332 default,
333 skip_serializing_if = "Option::is_none",
334 with = "::serde_with::rust::double_option"
335 )]
336 pub backup_configuration_uuid: Option<Option<uuid::Uuid>>,
337 #[garde(length(chars, min = 1, max = 255))]
338 #[schema(min_length = 1, max_length = 255)]
339 pub name: Option<compact_str::CompactString>,
340 #[garde(length(chars, min = 1, max = 1024))]
341 #[schema(min_length = 1, max_length = 1024)]
342 #[serde(
343 default,
344 skip_serializing_if = "Option::is_none",
345 with = "::serde_with::rust::double_option"
346 )]
347 pub description: Option<Option<compact_str::CompactString>>,
348 #[garde(length(chars, min = 2, max = 2))]
349 #[schema(min_length = 2, max_length = 2)]
350 #[serde(
351 default,
352 skip_serializing_if = "Option::is_none",
353 with = "::serde_with::rust::double_option"
354 )]
355 pub flag: Option<Option<compact_str::CompactString>>,
356}
357
358#[async_trait::async_trait]
359impl UpdatableModel for Location {
360 type UpdateOptions = UpdateLocationOptions;
361
362 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
363 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<Location>> =
364 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
365
366 &UPDATE_LISTENERS
367 }
368
369 async fn update_with_transaction(
370 &mut self,
371 state: &crate::State,
372 mut options: Self::UpdateOptions,
373 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
374 ) -> Result<(), crate::database::DatabaseError> {
375 options.validate()?;
376
377 let backup_configuration =
378 if let Some(backup_configuration_uuid) = &options.backup_configuration_uuid {
379 match backup_configuration_uuid {
380 Some(uuid) => {
381 super::backup_configuration::BackupConfiguration::by_uuid_optional_cached(
382 &state.database,
383 *uuid,
384 )
385 .await?
386 .ok_or(crate::database::InvalidRelationError(
387 "backup_configuration",
388 ))?;
389
390 Some(Some(
391 super::backup_configuration::BackupConfiguration::get_fetchable(*uuid),
392 ))
393 }
394 None => Some(None),
395 }
396 } else {
397 None
398 };
399
400 let mut query_builder = UpdateQueryBuilder::new("locations");
401
402 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
403 .await?;
404
405 query_builder
406 .set(
407 "backup_configuration_uuid",
408 options.backup_configuration_uuid.as_ref(),
409 )
410 .set("name", options.name.as_ref())
411 .set("description", options.description.as_ref())
412 .set("flag", options.flag.as_ref())
413 .where_eq("uuid", self.uuid);
414
415 query_builder.execute(&mut **transaction).await?;
416
417 if let Some(backup_configuration) = backup_configuration {
418 self.backup_configuration = backup_configuration;
419 }
420 if let Some(name) = options.name {
421 self.name = name;
422 }
423 if let Some(description) = options.description {
424 self.description = description;
425 }
426 if let Some(flag) = options.flag {
427 self.flag = flag;
428 }
429
430 self.run_after_update_handlers(state, transaction).await?;
431
432 Ok(())
433 }
434}
435
436#[async_trait::async_trait]
437impl DeletableModel for Location {
438 type DeleteOptions = ();
439
440 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
441 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<Location>> =
442 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
443
444 &DELETE_LISTENERS
445 }
446
447 async fn delete_with_transaction(
448 &self,
449 state: &crate::State,
450 options: Self::DeleteOptions,
451 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
452 ) -> Result<(), anyhow::Error> {
453 self.run_delete_handlers(&options, state, transaction)
454 .await?;
455
456 sqlx::query(
457 r#"
458 DELETE FROM locations
459 WHERE locations.uuid = $1
460 "#,
461 )
462 .bind(self.uuid)
463 .execute(&mut **transaction)
464 .await?;
465
466 self.run_after_delete_handlers(&options, state, transaction)
467 .await?;
468
469 Ok(())
470 }
471}
472
473#[derive(Validate)]
474pub struct DuplicateLocationOptions {
475 #[garde(length(chars, min = 1, max = 255))]
476 pub name: compact_str::CompactString,
477}
478
479#[async_trait::async_trait]
480impl DuplicableModel for Location {
481 type DuplicateOptions<'a> = DuplicateLocationOptions;
482
483 fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
484 static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<Location>> =
485 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
486
487 &DUPLICATE_LISTENERS
488 }
489
490 async fn duplicate_with_transaction(
491 &self,
492 state: &crate::State,
493 options: Self::DuplicateOptions<'_>,
494 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
495 ) -> Result<Self, crate::database::DatabaseError> {
496 options.validate()?;
497
498 self.run_duplicate_handlers(&options, state, transaction)
499 .await?;
500
501 let mut query_builder = InsertQueryBuilder::new("locations");
502
503 query_builder
504 .set(
505 "backup_configuration_uuid",
506 self.backup_configuration.as_ref().map(|c| c.uuid),
507 )
508 .set("name", &options.name)
509 .set("description", &self.description)
510 .set("flag", &self.flag);
511
512 let row = query_builder
513 .returning(&Self::columns_sql(None))
514 .fetch_one(&mut **transaction)
515 .await?;
516 let mut location = Self::map(None, &row)?;
517
518 sqlx::query!(
519 "INSERT INTO location_database_hosts (location_uuid, database_host_uuid)
520 SELECT $1, location_database_hosts.database_host_uuid
521 FROM location_database_hosts
522 WHERE location_database_hosts.location_uuid = $2",
523 location.uuid,
524 self.uuid,
525 )
526 .execute(&mut **transaction)
527 .await?;
528
529 self.run_after_duplicate_handlers(&mut location, &options, state, transaction)
530 .await?;
531
532 Ok(location)
533 }
534}
535
536#[schema_extension_derive::extendible]
537#[init_args(Location, crate::State)]
538#[hook_args(crate::State)]
539#[derive(ToSchema, Serialize)]
540#[schema(title = "Location")]
541pub struct AdminApiLocation {
542 pub uuid: uuid::Uuid,
543 pub backup_configuration: Option<super::backup_configuration::AdminApiBackupConfiguration>,
544
545 pub name: compact_str::CompactString,
546 pub description: Option<compact_str::CompactString>,
547
548 pub flag: Option<compact_str::CompactString>,
549
550 pub created: chrono::DateTime<chrono::Utc>,
551}