diff --git a/backend/sqlx-data.json b/backend/sqlx-data.json index 872882cac7..e9f8d3d265 100644 --- a/backend/sqlx-data.json +++ b/backend/sqlx-data.json @@ -167,6 +167,27 @@ "nullable": [] } }, + "0d6412bc3ebb1d58bdd9cbcef774dacf9016fa402af5c1b4e339b9a3d7163d5e": { + "query": "SELECT EXISTS (SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2 AND path != script_path)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, "0dd3fe3ddf9cb72760687d2ee0950afdcce2d54721bfe8dba008b15e4b581956": { "query": "DELETE FROM account WHERE id = $1 AND workspace_id = $2", "describe": { @@ -2909,6 +2930,27 @@ ] } }, + "e262f83b672a558092dc959b28a919f18e44b2a0d03b27b0ddba99896940c6d3": { + "query": "SELECT EXISTS (SELECT 1 FROM flow WHERE path = $1 AND workspace_id = $2)", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + null + ] + } + }, "e3eeda2e19bfbfd5aadd71c40774f7e93c8479a777fdb2828607b1db36361726": { "query": "INSERT INTO usr_to_group\n VALUES ($1, 'all', $2)", "describe": { diff --git a/backend/src/flow.rs b/backend/src/flow.rs index 7b9d322c99..f5a32625ed 100644 --- a/backend/src/flow.rs +++ b/backend/src/flow.rs @@ -16,12 +16,12 @@ use axum::{ }; use serde::{Deserialize, Serialize}; use sql_builder::SqlBuilder; -use sqlx::FromRow; +use sqlx::{FromRow, Postgres, Transaction}; use crate::{ audit::{audit_log, ActionKind}, db::{UserDB, DB}, - error::{Error, JsonResult, Result}, + error::{self, Error, JsonResult, Result}, jobs::RawCode, scripts::Schema, users::Authed, @@ -171,6 +171,8 @@ async fn create_flow( // cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?; let mut tx = user_db.begin(&authed).await?; + check_schedule_conflict(&mut tx, &w_id, &nf.path).await?; + sqlx::query!( "INSERT INTO flow (workspace_id, path, summary, description, value, edited_by, edited_at, schema) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::text::json)", w_id, @@ -205,6 +207,27 @@ async fn create_flow( Ok(nf.path.to_string()) } +async fn check_schedule_conflict<'c>( + tx: &mut Transaction<'c, Postgres>, + w_id: &str, + path: &str, +) -> error::Result<()> { + let exists_flow = sqlx::query_scalar!( + "SELECT EXISTS (SELECT 1 FROM schedule WHERE path = $1 AND workspace_id = $2 AND path != script_path)", + path, + w_id + ) + .fetch_one(tx) + .await? + .unwrap_or(false); + if exists_flow { + return Err(error::Error::BadConfig(format!( + "A flow cannot have the same path as a schedule if the schedule does not trigger that same flow: {path}", + ))); + }; + Ok(()) +} + async fn update_flow( authed: Authed, Extension(user_db): Extension, @@ -214,6 +237,8 @@ async fn update_flow( let mut tx = user_db.begin(&authed).await?; let flow_path = flow_path.to_path(); + check_schedule_conflict(&mut tx, &w_id, flow_path).await?; + let schema = nf.schema.map(|x| x.0); let flow = sqlx::query_scalar!( "UPDATE flow SET path = $1, summary = $2, description = $3, value = $4, edited_by = $5, edited_at = $6, schema = $7 WHERE path = $8 AND workspace_id = $9 RETURNING path", diff --git a/backend/src/schedule.rs b/backend/src/schedule.rs index a56aea8b82..8e32977aa1 100644 --- a/backend/src/schedule.rs +++ b/backend/src/schedule.rs @@ -129,6 +129,8 @@ async fn create_schedule( cron::Schedule::from_str(&ns.schedule).map_err(|e| error::Error::BadRequest(e.to_string()))?; let mut tx = user_db.begin(&authed).await?; + check_flow_conflict(&mut tx, &w_id, &ns.path, ns.is_flow, &ns.script_path).await?; + let schedule = sqlx::query_as!(Schedule, "INSERT INTO schedule (workspace_id, path, schedule, offset_, edited_by, script_path, is_flow, args) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *", w_id, @@ -167,6 +169,32 @@ async fn create_schedule( Ok(ns.path.to_string()) } +async fn check_flow_conflict<'c>( + tx: &mut Transaction<'c, Postgres>, + w_id: &str, + path: &str, + is_flow: bool, + script_path: &str, +) -> error::Result<()> { + if path != script_path || !is_flow { + let exists_flow = sqlx::query_scalar!( + "SELECT EXISTS (SELECT 1 FROM flow WHERE path = $1 AND workspace_id = $2)", + path, + w_id + ) + .fetch_one(tx) + .await? + .unwrap_or(false); + if exists_flow { + return Err(error::Error::BadConfig(format!( + "If a schedule has the same path as or a flow, it must be its primary schedule and hence can only trigger it. + However the provided path is: {script_path} and is_flow is: {is_flow}", + ))); + }; + } + Ok(()) +} + #[derive(Deserialize)] pub struct EditSchedule { pub schedule: String, @@ -194,6 +222,8 @@ async fn edit_schedule( let mut tx = user_db.begin(&authed).await?; + check_flow_conflict(&mut tx, &w_id, &path, es.is_flow, &es.script_path).await?; + clear_schedule(&mut tx, path).await?; let schedule = sqlx::query_as!(Schedule, "UPDATE schedule SET schedule = $1, script_path = $2, is_flow = $3, args = $4 WHERE path = $5 AND workspace_id = $6 RETURNING *",