feat(modules): author group trigger templates (event kinds) (§11 tail T2)
A [group] node may now declare EVENT trigger templates (kv/docs/files/ pubsub) that bind a group-owned handler. cron/queue/email stay app-only (rejected on a group at both the manifest and the reconcile layer). - Trigger is now owner-polymorphic (app_id: Option, group_id: Option), mirroring Script's Phase-4 reshape; TriggerRow carries group_id (#[sqlx(default)] so interactive RETURNING clauses still hydrate). - insert_trigger_tx takes a ScriptOwner (App XOR Group) and writes the group_id column; the queue advisory-lock branch is app-only. - TriggerRepo::list_for_group loads a group's templates; load_current's Group arm uses it so the diff is NoOp on re-apply and prune-able. - apply_service reconcile drops the "triggers are app-only" assumption; validate_bundle_for rejects non-event kinds on a group. The semantic- identity diff is already per-owner. - CLI manifest allows event-kind [[triggers]] on [group], rejects cron/queue/email there. Templates don't fire yet — the live dispatch union lands in T3. All trigger/apply/manifest unit tests green; clippy -D clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -698,7 +698,9 @@ async fn delete_trigger(
|
||||
.get(trigger_id)
|
||||
.await?
|
||||
.ok_or(TriggersApiError::NotFound(trigger_id))?;
|
||||
if trigger.app_id != app_id {
|
||||
// Interactive trigger management is app-scoped; a group TEMPLATE
|
||||
// (app_id NULL) is never addressable here, so a mismatch (incl. None) 404s.
|
||||
if trigger.app_id != Some(app_id) {
|
||||
return Err(TriggersApiError::NotFound(trigger_id));
|
||||
}
|
||||
if !s.triggers.delete(trigger_id).await? {
|
||||
@@ -900,7 +902,8 @@ mod tests {
|
||||
let id = TriggerId::new();
|
||||
let trigger = Trigger {
|
||||
id,
|
||||
app_id,
|
||||
app_id: Some(app_id),
|
||||
group_id: None,
|
||||
script_id: req.script_id,
|
||||
name: "mock".into(),
|
||||
kind: crate::trigger_repo::TriggerKind::Kv,
|
||||
@@ -929,7 +932,8 @@ mod tests {
|
||||
let id = TriggerId::new();
|
||||
let trigger = Trigger {
|
||||
id,
|
||||
app_id,
|
||||
app_id: Some(app_id),
|
||||
group_id: None,
|
||||
script_id: req.script_id,
|
||||
name: "mock".into(),
|
||||
kind: crate::trigger_repo::TriggerKind::Docs,
|
||||
@@ -958,7 +962,8 @@ mod tests {
|
||||
let id = TriggerId::new();
|
||||
let trigger = Trigger {
|
||||
id,
|
||||
app_id,
|
||||
app_id: Some(app_id),
|
||||
group_id: None,
|
||||
script_id: req.script_id,
|
||||
name: "mock".into(),
|
||||
kind: crate::trigger_repo::TriggerKind::DeadLetter,
|
||||
@@ -988,7 +993,8 @@ mod tests {
|
||||
let id = TriggerId::new();
|
||||
let trigger = Trigger {
|
||||
id,
|
||||
app_id,
|
||||
app_id: Some(app_id),
|
||||
group_id: None,
|
||||
script_id: req.script_id,
|
||||
name: "mock".into(),
|
||||
kind: TriggerKind::Email,
|
||||
@@ -1015,7 +1021,7 @@ mod tests {
|
||||
Ok(g.get(&trigger_id)
|
||||
.filter(|t| t.kind == TriggerKind::Email)
|
||||
.map(|t| EmailInboundTarget {
|
||||
app_id: t.app_id,
|
||||
app_id: t.app_id.expect("email trigger is app-owned"),
|
||||
script_id: t.script_id,
|
||||
enabled: t.enabled,
|
||||
dispatch_mode: t.dispatch_mode,
|
||||
@@ -1033,7 +1039,8 @@ mod tests {
|
||||
let id = TriggerId::new();
|
||||
let trigger = Trigger {
|
||||
id,
|
||||
app_id,
|
||||
app_id: Some(app_id),
|
||||
group_id: None,
|
||||
script_id: req.script_id,
|
||||
name: "mock".into(),
|
||||
kind: crate::trigger_repo::TriggerKind::Cron,
|
||||
@@ -1063,7 +1070,8 @@ mod tests {
|
||||
let id = TriggerId::new();
|
||||
let trigger = Trigger {
|
||||
id,
|
||||
app_id,
|
||||
app_id: Some(app_id),
|
||||
group_id: None,
|
||||
script_id: req.script_id,
|
||||
name: "mock".into(),
|
||||
kind: crate::trigger_repo::TriggerKind::Files,
|
||||
@@ -1092,7 +1100,8 @@ mod tests {
|
||||
let id = TriggerId::new();
|
||||
let trigger = Trigger {
|
||||
id,
|
||||
app_id,
|
||||
app_id: Some(app_id),
|
||||
group_id: None,
|
||||
script_id: req.script_id,
|
||||
name: "mock".into(),
|
||||
kind: crate::trigger_repo::TriggerKind::Pubsub,
|
||||
@@ -1117,7 +1126,7 @@ mod tests {
|
||||
.lock()
|
||||
.await
|
||||
.values()
|
||||
.filter(|t| t.app_id == app_id)
|
||||
.filter(|t| t.app_id == Some(app_id))
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
@@ -1169,7 +1178,8 @@ mod tests {
|
||||
let id = TriggerId::new();
|
||||
let trigger = Trigger {
|
||||
id,
|
||||
app_id,
|
||||
app_id: Some(app_id),
|
||||
group_id: None,
|
||||
script_id: req.script_id,
|
||||
name: "mock".into(),
|
||||
kind: TriggerKind::Queue,
|
||||
|
||||
Reference in New Issue
Block a user