mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-29 08:12:56 +00:00
feat: add trigger_history table with source tracking (#10696)
* feat: add trigger_history table with source tracking Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: gate trigger history reads on scopes and harden its writers Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: filter trigger history scopes in SQL and match the cleared-handler diff Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: record a trigger restore from the trashbin in its history Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: record bulk http trigger creates and document the recording boundary Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: lock the trigger row when capturing its history preimage Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: only record an auto-disable that actually flipped the schedule Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: state the auto-disable invariant once instead of at four call sites Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: render trigger history changes as a structured field diff Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: make a server-initiated disable atomic with its history row Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: note that the auto-disable savepoint takes no pool connection Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: note the flow fallback is the last chance to disable Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: never leave a trigger enabled because its history row failed Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: retry the disable history row instead of dropping it on first failure Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: use the design-system Button for the change-value expander Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: hold the trigger row lock across its disable history row Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep the history-loss alert out of the listener cancellation race Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: read the history workspace through the trigger-workspace seam Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6d03784d4b
commit
633d7bcb2e
@@ -16,6 +16,7 @@ use windmill_api_auth::{build_scope_path_predicate, check_scopes, ApiAuthed};
|
||||
use windmill_common::{
|
||||
db::UserDB,
|
||||
error::{Error, JsonResult, Result},
|
||||
trigger_history::{self, TriggerHistoryEvent, TriggerOperation, TriggerSource},
|
||||
user_drafts::{
|
||||
delete_all_drafts_for_path, delete_own_draft_for_path, fetch_draft_only_list_rows,
|
||||
overlay_or_draft_only, UserDraftItemKind, WithDraftOverlay, WithDraftQuery,
|
||||
@@ -458,6 +459,45 @@ pub trait TriggerCrud: Send + Sync + 'static {
|
||||
}
|
||||
}
|
||||
|
||||
/// Append this mutation to `trigger_history`, diffing the row at `path` against
|
||||
/// `before`.
|
||||
///
|
||||
/// Call it on the transaction that made the change, after the change: the
|
||||
/// snapshot it takes is the "after" side of the diff, and the two commit or roll
|
||||
/// back together.
|
||||
///
|
||||
/// Records nothing when the snapshots say no row was written. `TriggerCrud::update_trigger`
|
||||
/// returns `Result<()>` and several impls do not check `rows_affected`, so an
|
||||
/// update aimed at a path that does not exist — or that RLS hides from the
|
||||
/// caller — reaches here having changed nothing; without this the caller could
|
||||
/// forge history rows at any path, since the insert policy is `WITH CHECK (true)`.
|
||||
async fn record_trigger_history<T: TriggerCrud>(
|
||||
tx: &mut PgConnection,
|
||||
authed: &ApiAuthed,
|
||||
workspace_id: &str,
|
||||
path: &str,
|
||||
operation: TriggerOperation,
|
||||
before: Option<serde_json::Value>,
|
||||
) -> Result<()> {
|
||||
let after = trigger_history::snapshot_row(&mut *tx, T::TABLE_NAME, workspace_id, path).await?;
|
||||
if after.is_none() || (operation == TriggerOperation::Update && before.is_none()) {
|
||||
return Ok(());
|
||||
}
|
||||
trigger_history::record(
|
||||
&mut *tx,
|
||||
TriggerHistoryEvent {
|
||||
workspace_id,
|
||||
trigger_kind: T::TRIGGER_TYPE,
|
||||
path,
|
||||
operation,
|
||||
source: TriggerSource::of_request(authed.is_session_token),
|
||||
username: Some(&authed.username),
|
||||
changes: trigger_history::summarize_changes(before.as_ref(), after.as_ref()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn trigger_routes<T: TriggerCrud + 'static>() -> Router {
|
||||
let mut router = Router::new()
|
||||
.route("/create", post(create_trigger::<T>))
|
||||
@@ -556,6 +596,16 @@ async fn create_trigger<T: TriggerCrud>(
|
||||
.await?;
|
||||
}
|
||||
|
||||
record_trigger_history::<T>(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
&workspace_id,
|
||||
&new_path,
|
||||
TriggerOperation::Create,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
@@ -782,6 +832,9 @@ async fn update_trigger<T: TriggerCrud>(
|
||||
&authed.username,
|
||||
);
|
||||
|
||||
let before =
|
||||
trigger_history::snapshot_row(&mut *tx, T::TABLE_NAME, &workspace_id, path).await?;
|
||||
|
||||
handler
|
||||
.update_trigger(&db, &mut *tx, &authed, &workspace_id, path, edit_trigger)
|
||||
.await?;
|
||||
@@ -799,6 +852,18 @@ async fn update_trigger<T: TriggerCrud>(
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Recorded at the new path, so a rename reads as one event there with
|
||||
// `path` among the changed fields rather than a delete plus a create.
|
||||
record_trigger_history::<T>(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
&workspace_id,
|
||||
&new_path,
|
||||
TriggerOperation::Update,
|
||||
before,
|
||||
)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
@@ -913,6 +978,22 @@ async fn delete_trigger<T: TriggerCrud>(
|
||||
.await?;
|
||||
}
|
||||
|
||||
// No diff: the row is gone, and the trashbin above already keeps its full
|
||||
// contents for a restore.
|
||||
trigger_history::record(
|
||||
&mut *tx,
|
||||
TriggerHistoryEvent {
|
||||
workspace_id: &workspace_id,
|
||||
trigger_kind: T::TRIGGER_TYPE,
|
||||
path,
|
||||
operation: TriggerOperation::Delete,
|
||||
source: TriggerSource::of_request(authed.is_session_token),
|
||||
username: Some(&authed.username),
|
||||
changes: None,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
audit_log(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
@@ -1052,6 +1133,9 @@ async fn set_trigger_mode<T: TriggerCrud>(
|
||||
}
|
||||
}
|
||||
|
||||
let before =
|
||||
trigger_history::snapshot_row(&mut *tx, T::TABLE_NAME, &workspace_id, path).await?;
|
||||
|
||||
let updated = handler
|
||||
.set_trigger_mode(&authed, &mut *tx, &workspace_id, path, &payload.mode)
|
||||
.await?;
|
||||
@@ -1063,6 +1147,20 @@ async fn set_trigger_mode<T: TriggerCrud>(
|
||||
)));
|
||||
}
|
||||
|
||||
record_trigger_history::<T>(
|
||||
&mut *tx,
|
||||
&authed,
|
||||
&workspace_id,
|
||||
path,
|
||||
match payload.mode {
|
||||
TriggerMode::Enabled => TriggerOperation::Enable,
|
||||
TriggerMode::Disabled => TriggerOperation::Disable,
|
||||
TriggerMode::Suspended => TriggerOperation::Suspend,
|
||||
},
|
||||
before,
|
||||
)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
handle_deployment_metadata(
|
||||
|
||||
Reference in New Issue
Block a user