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}