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