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