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)]
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    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(|| std::sync::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
125impl ServerSchedule {
126    pub async fn by_server_uuid_uuid(
127        database: &crate::database::Database,
128        server_uuid: uuid::Uuid,
129        uuid: uuid::Uuid,
130    ) -> Result<Option<Self>, crate::database::DatabaseError> {
131        let row = sqlx::query(&format!(
132            r#"
133            SELECT {}
134            FROM server_schedules
135            WHERE server_schedules.server_uuid = $1 AND server_schedules.uuid = $2
136            "#,
137            Self::columns_sql(None)
138        ))
139        .bind(server_uuid)
140        .bind(uuid)
141        .fetch_optional(database.read())
142        .await?;
143
144        row.try_map(|row| Self::map(None, &row))
145    }
146
147    pub async fn by_server_uuid_with_pagination(
148        database: &crate::database::Database,
149        server_uuid: uuid::Uuid,
150        page: i64,
151        per_page: i64,
152        search: Option<&str>,
153    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
154        let offset = (page - 1) * per_page;
155
156        let rows = sqlx::query(&format!(
157            r#"
158            SELECT {}, COUNT(*) OVER() AS total_count
159            FROM server_schedules
160            WHERE server_schedules.server_uuid = $1 AND ($2 IS NULL OR server_schedules.name ILIKE '%' || $2 || '%')
161            ORDER BY server_schedules.created
162            LIMIT $3 OFFSET $4
163            "#,
164            Self::columns_sql(None)
165        ))
166        .bind(server_uuid)
167        .bind(search)
168        .bind(per_page)
169        .bind(offset)
170        .fetch_all(database.read())
171        .await?;
172
173        Ok(super::Pagination {
174            total: rows
175                .first()
176                .map_or(Ok(0), |row| row.try_get("total_count"))?,
177            per_page,
178            page,
179            data: rows
180                .into_iter()
181                .map(|row| Self::map(None, &row))
182                .try_collect_vec()?,
183        })
184    }
185
186    pub async fn count_by_server_uuid(
187        database: &crate::database::Database,
188        server_uuid: uuid::Uuid,
189    ) -> Result<i64, sqlx::Error> {
190        sqlx::query_scalar(
191            r#"
192            SELECT COUNT(*)
193            FROM server_schedules
194            WHERE server_schedules.server_uuid = $1
195            "#,
196        )
197        .bind(server_uuid)
198        .fetch_one(database.read())
199        .await
200    }
201
202    #[inline]
203    pub async fn into_exported(
204        self,
205        database: &crate::database::Database,
206    ) -> Result<ExportedServerSchedule, crate::database::DatabaseError> {
207        Ok(ExportedServerSchedule {
208            name: self.name,
209            enabled: self.enabled,
210            triggers: self.triggers,
211            condition: self.condition,
212            steps: super::server_schedule_step::ServerScheduleStep::all_by_schedule_uuid(
213                database, self.uuid,
214            )
215            .await?
216            .into_iter()
217            .map(|step| step.into_exported())
218            .collect(),
219        })
220    }
221}
222
223#[async_trait::async_trait]
224impl IntoApiObject for ServerSchedule {
225    type ApiObject = ApiServerSchedule;
226    type ExtraArgs<'a> = ();
227
228    async fn into_api_object<'a>(
229        self,
230        state: &crate::State,
231        _args: Self::ExtraArgs<'a>,
232    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
233        let api_object = ApiServerSchedule::init_hooks(&self, state).await?;
234
235        let api_object = finish_extendible!(
236            ApiServerSchedule {
237                uuid: self.uuid,
238                name: self.name,
239                enabled: self.enabled,
240                triggers: self.triggers,
241                condition: self.condition,
242                last_run: self.last_run.map(|dt| dt.and_utc()),
243                last_failure: self.last_failure.map(|dt| dt.and_utc()),
244                created: self.created.and_utc(),
245            },
246            api_object,
247            state
248        )?;
249
250        Ok(api_object)
251    }
252}
253
254#[derive(ToSchema, Deserialize, Validate)]
255pub struct CreateServerScheduleOptions {
256    #[garde(skip)]
257    pub server_uuid: uuid::Uuid,
258    #[garde(length(chars, min = 1, max = 255))]
259    #[schema(min_length = 1, max_length = 255)]
260    pub name: compact_str::CompactString,
261    #[garde(skip)]
262    pub enabled: bool,
263    #[garde(dive)]
264    pub triggers: Vec<wings_api::ScheduleTrigger>,
265    #[garde(dive)]
266    pub condition: wings_api::SchedulePreCondition,
267}
268
269#[async_trait::async_trait]
270impl CreatableModel for ServerSchedule {
271    type CreateOptions<'a> = CreateServerScheduleOptions;
272    type CreateResult = Self;
273
274    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
275        static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerSchedule>> =
276            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
277
278        &CREATE_LISTENERS
279    }
280
281    async fn create_with_transaction(
282        state: &crate::State,
283        mut options: Self::CreateOptions<'_>,
284        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
285    ) -> Result<Self, crate::database::DatabaseError> {
286        options.validate()?;
287
288        let mut query_builder = InsertQueryBuilder::new("server_schedules");
289
290        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
291
292        query_builder
293            .set("server_uuid", options.server_uuid)
294            .set("name", &options.name)
295            .set("enabled", options.enabled)
296            .set("triggers", serde_json::to_value(&options.triggers)?)
297            .set("condition", serde_json::to_value(&options.condition)?);
298
299        let row = query_builder
300            .returning(&Self::columns_sql(None))
301            .fetch_one(&mut **transaction)
302            .await?;
303        let mut schedule = Self::map(None, &row)?;
304
305        Self::run_after_create_handlers(&mut schedule, &options, state, transaction).await?;
306
307        Ok(schedule)
308    }
309}
310
311#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
312pub struct UpdateServerScheduleOptions {
313    #[garde(length(chars, min = 1, max = 255))]
314    #[schema(min_length = 1, max_length = 255)]
315    pub name: Option<compact_str::CompactString>,
316    #[garde(skip)]
317    pub enabled: Option<bool>,
318    #[garde(dive)]
319    pub triggers: Option<Vec<wings_api::ScheduleTrigger>>,
320    #[garde(dive)]
321    pub condition: Option<wings_api::SchedulePreCondition>,
322}
323
324#[async_trait::async_trait]
325impl UpdatableModel for ServerSchedule {
326    type UpdateOptions = UpdateServerScheduleOptions;
327
328    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
329        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<ServerSchedule>> =
330            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
331
332        &UPDATE_LISTENERS
333    }
334
335    async fn update_with_transaction(
336        &mut self,
337        state: &crate::State,
338        mut options: Self::UpdateOptions,
339        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
340    ) -> Result<(), crate::database::DatabaseError> {
341        options.validate()?;
342
343        let mut query_builder = UpdateQueryBuilder::new("server_schedules");
344
345        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
346            .await?;
347
348        query_builder
349            .set("name", options.name.as_ref())
350            .set("enabled", options.enabled)
351            .set(
352                "triggers",
353                if let Some(triggers) = &options.triggers {
354                    Some(serde_json::to_value(triggers)?)
355                } else {
356                    None
357                },
358            )
359            .set(
360                "condition",
361                if let Some(condition) = &options.condition {
362                    Some(serde_json::to_value(condition)?)
363                } else {
364                    None
365                },
366            )
367            .where_eq("uuid", self.uuid);
368
369        query_builder.execute(&mut **transaction).await?;
370
371        if let Some(name) = options.name {
372            self.name = name;
373        }
374        if let Some(enabled) = options.enabled {
375            self.enabled = enabled;
376        }
377        if let Some(triggers) = options.triggers {
378            self.triggers = triggers;
379        }
380        if let Some(condition) = options.condition {
381            self.condition = condition;
382        }
383
384        self.run_after_update_handlers(state, transaction).await?;
385
386        Ok(())
387    }
388}
389
390#[async_trait::async_trait]
391impl ByUuid for ServerSchedule {
392    async fn by_uuid(
393        database: &crate::database::Database,
394        uuid: uuid::Uuid,
395    ) -> Result<Self, crate::database::DatabaseError> {
396        let row = sqlx::query(&format!(
397            r#"
398            SELECT {}
399            FROM server_schedules
400            WHERE server_schedules.uuid = $1
401            "#,
402            Self::columns_sql(None)
403        ))
404        .bind(uuid)
405        .fetch_one(database.read())
406        .await?;
407
408        Self::map(None, &row)
409    }
410
411    async fn by_uuid_with_transaction(
412        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
413        uuid: uuid::Uuid,
414    ) -> Result<Self, crate::database::DatabaseError> {
415        let row = sqlx::query(&format!(
416            r#"
417            SELECT {}
418            FROM server_schedules
419            WHERE server_schedules.uuid = $1
420            "#,
421            Self::columns_sql(None)
422        ))
423        .bind(uuid)
424        .fetch_one(&mut **transaction)
425        .await?;
426
427        Self::map(None, &row)
428    }
429}
430
431#[async_trait::async_trait]
432impl DeletableModel for ServerSchedule {
433    type DeleteOptions = ();
434
435    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
436        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<ServerSchedule>> =
437            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
438
439        &DELETE_LISTENERS
440    }
441
442    async fn delete_with_transaction(
443        &self,
444        state: &crate::State,
445        options: Self::DeleteOptions,
446        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
447    ) -> Result<(), anyhow::Error> {
448        self.run_delete_handlers(&options, state, transaction)
449            .await?;
450
451        sqlx::query(
452            r#"
453            DELETE FROM server_schedules
454            WHERE server_schedules.uuid = $1
455            "#,
456        )
457        .bind(self.uuid)
458        .execute(&mut **transaction)
459        .await?;
460
461        self.run_after_delete_handlers(&options, state, transaction)
462            .await?;
463
464        Ok(())
465    }
466}
467
468#[schema_extension_derive::extendible]
469#[init_args(ServerSchedule, crate::State)]
470#[hook_args(crate::State)]
471#[derive(ToSchema, Serialize)]
472#[schema(title = "ServerSchedule")]
473pub struct ApiServerSchedule {
474    pub uuid: uuid::Uuid,
475
476    pub name: compact_str::CompactString,
477    pub enabled: bool,
478
479    pub triggers: Vec<wings_api::ScheduleTrigger>,
480    pub condition: wings_api::SchedulePreCondition,
481
482    pub last_run: Option<chrono::DateTime<chrono::Utc>>,
483    pub last_failure: Option<chrono::DateTime<chrono::Utc>>,
484    pub created: chrono::DateTime<chrono::Utc>,
485}