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