Skip to main content

shared/models/
server_schedule.rs

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}