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