fix: skip triggers moved since their scope check when disconnecting

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-09-28 09:01:28 +02:00
co-authored by Claude Opus 5.5
parent b552922036
commit 80a613b25f
3 changed files with 30 additions and 12 deletions
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM native_trigger\n WHERE workspace_id = $1 AND service_name = $2 AND ($3::text[] IS NULL OR external_id = ANY($3))\n RETURNING webhook_token_hash",
"query": "DELETE FROM native_trigger\n WHERE workspace_id = $1 AND service_name = $2 AND ($3::text[] IS NULL\n OR (external_id, script_path) IN (SELECT * FROM unnest($3::text[], $4::text[])))\n RETURNING webhook_token_hash",
"describe": {
"columns": [
{
@@ -24,6 +24,7 @@
}
}
},
"TextArray",
"TextArray"
]
},
@@ -31,5 +32,5 @@
false
]
},
"hash": "e4407cb73d41652088bb6dcef2503e91a190217cc7875ebb07928b25720ae20b"
"hash": "59e359ec5307da46b7fb75d0a2ada7abd1836ec53fdc8086aa160735843f62b9"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT external_id, connection_path FROM native_trigger\n WHERE workspace_id = $1 AND service_name = $2 AND ($3::text[] IS NULL OR external_id = ANY($3))",
"query": "SELECT external_id, connection_path FROM native_trigger\n WHERE workspace_id = $1 AND service_name = $2 AND ($3::text[] IS NULL\n OR (external_id, script_path) IN (SELECT * FROM unnest($3::text[], $4::text[])))",
"describe": {
"columns": [
{
@@ -29,6 +29,7 @@
}
}
},
"TextArray",
"TextArray"
]
},
@@ -37,5 +38,5 @@
true
]
},
"hash": "c0a782d90433c27c97eeb49cf620e993320337f046e95efbd5bc21adf69e3d7d"
"hash": "f59c0f10c2dc7c0b578c764545e2ceee71c5f0ed6aca603e7ec7aae47f7cdade"
}
@@ -348,22 +348,26 @@ async fn fetch_nextcloud_user_id(base_url: &str, access_token: &str) -> anyhow::
Ok(ocs.ocs.data.id)
}
/// Delete the native triggers of a workspace+service — only those in `external_ids` when given —
/// including remote webhook cleanup.
/// Delete the native triggers of a workspace+service, including remote webhook cleanup. When `only`
/// gives `(external_ids, script_paths)`, just the triggers still at those paths: a trigger moved
/// since its path was checked is left alone.
/// This is best-effort: errors during remote cleanup or token deletion are logged but ignored.
#[cfg(feature = "native_trigger")]
async fn delete_triggers_for_service(
db: &DB,
workspace_id: &str,
service_name: ServiceName,
external_ids: Option<&[String]>,
only: Option<(&[String], &[String])>,
) {
let (external_ids, script_paths) = only.unzip();
let triggers = sqlx::query!(
"SELECT external_id, connection_path FROM native_trigger
WHERE workspace_id = $1 AND service_name = $2 AND ($3::text[] IS NULL OR external_id = ANY($3))",
WHERE workspace_id = $1 AND service_name = $2 AND ($3::text[] IS NULL
OR (external_id, script_path) IN (SELECT * FROM unnest($3::text[], $4::text[])))",
workspace_id,
service_name as ServiceName,
external_ids,
script_paths,
)
.fetch_all(db)
.await;
@@ -423,11 +427,13 @@ async fn delete_triggers_for_service(
// disconnected integration keep starting jobs.
let deleted_token_hashes = sqlx::query_scalar!(
"DELETE FROM native_trigger
WHERE workspace_id = $1 AND service_name = $2 AND ($3::text[] IS NULL OR external_id = ANY($3))
WHERE workspace_id = $1 AND service_name = $2 AND ($3::text[] IS NULL
OR (external_id, script_path) IN (SELECT * FROM unnest($3::text[], $4::text[])))
RETURNING webhook_token_hash",
workspace_id,
service_name as ServiceName,
external_ids,
script_paths,
)
.fetch_all(db)
.await
@@ -1085,7 +1091,8 @@ async fn delete_connection(
}
// Disconnecting deletes these triggers, so a path-scoped token must cover each of them. Only
// the rows checked here are deleted: one created meanwhile is never removed unchecked.
// the rows checked here, at the paths checked, are deleted: one created or moved meanwhile is
// never removed unchecked.
let triggers = sqlx::query!(
"SELECT external_id, script_path FROM native_trigger
WHERE workspace_id = $1 AND service_name = $2 AND connection_path = $3",
@@ -1100,10 +1107,19 @@ async fn delete_connection(
format!("native_triggers:write:{}", trigger.script_path)
})?;
}
let external_ids: Vec<String> = triggers.into_iter().map(|t| t.external_id).collect();
let (external_ids, script_paths): (Vec<String>, Vec<String>) = triggers
.into_iter()
.map(|t| (t.external_id, t.script_path))
.unzip();
// Before the connection goes: removing the registrations needs its token.
delete_triggers_for_service(&db, &workspace_id, service_name, Some(&external_ids)).await;
delete_triggers_for_service(
&db,
&workspace_id,
service_name,
Some((&external_ids, &script_paths)),
)
.await;
let mut tx = user_db.begin(&authed).await?;
let account = cleanup_connection(&mut *tx, &workspace_id, &path).await?;