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}