feat(sealed): author + persist sealed group templates (§11 tail M2)
Thread `sealed` from the manifest through to the DB column: - manifest: `sealed` on ManifestRoute + the four event-kind trigger specs (Kv/Docs/Files/Pubsub), flowing to Bundle via serde. - persist: NewRoute.sealed + insert_route_tx; insert_trigger_tx `sealed` param; the reconcile passes br.sealed / bt.sealed(). - validate_bundle_for rejects `sealed` on an app owner (meaningless — an app route/trigger is never inherited). - diff: `sealed` joins the route Update comparison and the trigger identity (both bundle + current sides), so toggling it re-applies rather than NoOp. `sealed` lives on shared Route + manager-core Trigger (both pure DTOs) so the diff can see the current value; RouteRow/TriggerRow default it so RETURNING clauses that omit it still hydrate. No runtime read effect yet — the two suppression gates land in M3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,6 +56,11 @@ pub struct Trigger {
|
||||
pub name: String,
|
||||
pub kind: TriggerKind,
|
||||
pub enabled: bool,
|
||||
/// §11 tail: `true` for a sealed (non-suppressible) group trigger template.
|
||||
/// Always `false` for an app-owned trigger. Part of the apply diff identity
|
||||
/// so toggling it re-materializes the row.
|
||||
#[serde(default)]
|
||||
pub sealed: bool,
|
||||
pub dispatch_mode: TriggerDispatchMode,
|
||||
pub retry_max_attempts: u32,
|
||||
pub retry_backoff: BackoffShape,
|
||||
@@ -543,6 +548,9 @@ pub(crate) async fn insert_trigger_tx(
|
||||
retry_max_attempts: u32,
|
||||
retry_backoff: BackoffShape,
|
||||
retry_base_ms: u32,
|
||||
// §11 tail: `true` for a sealed (non-suppressible) group trigger template.
|
||||
// Always `false` for an app-owned trigger.
|
||||
sealed: bool,
|
||||
details: &TriggerDetails,
|
||||
) -> Result<TriggerId, TriggerRepoError> {
|
||||
let kind = match details {
|
||||
@@ -595,8 +603,8 @@ pub(crate) async fn insert_trigger_tx(
|
||||
"INSERT INTO triggers ( \
|
||||
app_id, group_id, script_id, kind, enabled, dispatch_mode, \
|
||||
retry_max_attempts, retry_backoff, retry_base_ms, \
|
||||
registered_by_principal \
|
||||
) VALUES ($1, $2, $3, $4, TRUE, $5, $6, $7, $8, $9) RETURNING id",
|
||||
registered_by_principal, sealed \
|
||||
) VALUES ($1, $2, $3, $4, TRUE, $5, $6, $7, $8, $9, $10) RETURNING id",
|
||||
)
|
||||
.bind(owner_app_id)
|
||||
.bind(owner_group_id)
|
||||
@@ -607,6 +615,7 @@ pub(crate) async fn insert_trigger_tx(
|
||||
.bind(retry_backoff.as_str())
|
||||
.bind(i32::try_from(retry_base_ms).unwrap_or(1000))
|
||||
.bind(registered_by.into_inner())
|
||||
.bind(sealed)
|
||||
.fetch_one(&mut **tx)
|
||||
.await?;
|
||||
let tid = row.0;
|
||||
@@ -806,6 +815,7 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
name: parent.name.clone(),
|
||||
kind: TriggerKind::Kv,
|
||||
enabled: parent.enabled,
|
||||
sealed: parent.sealed,
|
||||
dispatch_mode: dispatch_from_str(&parent.dispatch_mode),
|
||||
retry_max_attempts: u32::try_from(parent.retry_max_attempts).unwrap_or(3),
|
||||
retry_backoff: BackoffShape::from_wire(&parent.retry_backoff)
|
||||
@@ -873,6 +883,7 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
name: parent.name.clone(),
|
||||
kind: TriggerKind::Docs,
|
||||
enabled: parent.enabled,
|
||||
sealed: parent.sealed,
|
||||
dispatch_mode: dispatch_from_str(&parent.dispatch_mode),
|
||||
retry_max_attempts: u32::try_from(parent.retry_max_attempts).unwrap_or(3),
|
||||
retry_backoff: BackoffShape::from_wire(&parent.retry_backoff)
|
||||
@@ -935,6 +946,7 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
name: parent.name.clone(),
|
||||
kind: TriggerKind::DeadLetter,
|
||||
enabled: parent.enabled,
|
||||
sealed: parent.sealed,
|
||||
dispatch_mode: dispatch_from_str(&parent.dispatch_mode),
|
||||
retry_max_attempts: u32::try_from(parent.retry_max_attempts).unwrap_or(1),
|
||||
retry_backoff: BackoffShape::from_wire(&parent.retry_backoff)
|
||||
@@ -1003,6 +1015,7 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
name: parent.name.clone(),
|
||||
kind: TriggerKind::Cron,
|
||||
enabled: parent.enabled,
|
||||
sealed: parent.sealed,
|
||||
dispatch_mode: dispatch_from_str(&parent.dispatch_mode),
|
||||
retry_max_attempts: u32::try_from(parent.retry_max_attempts).unwrap_or(3),
|
||||
retry_backoff: BackoffShape::from_wire(&parent.retry_backoff)
|
||||
@@ -1071,6 +1084,7 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
name: parent.name.clone(),
|
||||
kind: TriggerKind::Files,
|
||||
enabled: parent.enabled,
|
||||
sealed: parent.sealed,
|
||||
dispatch_mode: dispatch_from_str(&parent.dispatch_mode),
|
||||
retry_max_attempts: u32::try_from(parent.retry_max_attempts).unwrap_or(3),
|
||||
retry_backoff: BackoffShape::from_wire(&parent.retry_backoff)
|
||||
@@ -1134,6 +1148,7 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
name: parent.name.clone(),
|
||||
kind: TriggerKind::Pubsub,
|
||||
enabled: parent.enabled,
|
||||
sealed: parent.sealed,
|
||||
dispatch_mode: dispatch_from_str(&parent.dispatch_mode),
|
||||
retry_max_attempts: u32::try_from(parent.retry_max_attempts).unwrap_or(3),
|
||||
retry_backoff: BackoffShape::from_wire(&parent.retry_backoff)
|
||||
@@ -1195,6 +1210,7 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
name: parent.name.clone(),
|
||||
kind: TriggerKind::Email,
|
||||
enabled: parent.enabled,
|
||||
sealed: parent.sealed,
|
||||
dispatch_mode: dispatch_from_str(&parent.dispatch_mode),
|
||||
retry_max_attempts: u32::try_from(parent.retry_max_attempts).unwrap_or(3),
|
||||
retry_backoff: BackoffShape::from_wire(&parent.retry_backoff)
|
||||
@@ -1235,7 +1251,7 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
|
||||
async fn list_for_app(&self, app_id: AppId) -> Result<Vec<Trigger>, TriggerRepoError> {
|
||||
let parents: Vec<TriggerRow> = sqlx::query_as(
|
||||
"SELECT id, app_id, script_id, name, kind, enabled, dispatch_mode, \
|
||||
"SELECT id, app_id, script_id, name, kind, enabled, sealed, dispatch_mode, \
|
||||
retry_max_attempts, retry_backoff, retry_base_ms, \
|
||||
registered_by_principal, created_at, updated_at \
|
||||
FROM triggers WHERE app_id = $1 ORDER BY created_at DESC",
|
||||
@@ -1263,7 +1279,7 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
|
||||
async fn list_for_group(&self, group_id: GroupId) -> Result<Vec<Trigger>, TriggerRepoError> {
|
||||
let parents: Vec<TriggerRow> = sqlx::query_as(
|
||||
"SELECT id, app_id, group_id, script_id, name, kind, enabled, dispatch_mode, \
|
||||
"SELECT id, app_id, group_id, script_id, name, kind, enabled, sealed, dispatch_mode, \
|
||||
retry_max_attempts, retry_backoff, retry_base_ms, \
|
||||
registered_by_principal, created_at, updated_at \
|
||||
FROM triggers WHERE group_id = $1 ORDER BY created_at DESC",
|
||||
@@ -1289,7 +1305,7 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
// rows are all app-owned) — else a template would hydrate with both
|
||||
// owners None, breaking the exactly-one invariant in memory.
|
||||
let parent: Option<TriggerRow> = sqlx::query_as(
|
||||
"SELECT id, app_id, group_id, script_id, name, kind, enabled, dispatch_mode, \
|
||||
"SELECT id, app_id, group_id, script_id, name, kind, enabled, sealed, dispatch_mode, \
|
||||
retry_max_attempts, retry_backoff, retry_base_ms, \
|
||||
registered_by_principal, created_at, updated_at \
|
||||
FROM triggers WHERE id = $1",
|
||||
@@ -1580,6 +1596,7 @@ impl TriggerRepo for PostgresTriggerRepo {
|
||||
name: parent.name.clone(),
|
||||
kind: TriggerKind::Queue,
|
||||
enabled: parent.enabled,
|
||||
sealed: parent.sealed,
|
||||
dispatch_mode: dispatch_from_str(&parent.dispatch_mode),
|
||||
retry_max_attempts: u32::try_from(parent.retry_max_attempts).unwrap_or(3),
|
||||
retry_backoff: BackoffShape::from_wire(&parent.retry_backoff)
|
||||
@@ -1789,6 +1806,7 @@ async fn hydrate_one(pool: &PgPool, parent: TriggerRow) -> Result<Trigger, Trigg
|
||||
name: parent.name,
|
||||
kind,
|
||||
enabled: parent.enabled,
|
||||
sealed: parent.sealed,
|
||||
dispatch_mode: dispatch_from_str(&parent.dispatch_mode),
|
||||
retry_max_attempts: u32::try_from(parent.retry_max_attempts).unwrap_or(3),
|
||||
retry_backoff: BackoffShape::from_wire(&parent.retry_backoff)
|
||||
@@ -1837,6 +1855,11 @@ struct TriggerRow {
|
||||
name: String,
|
||||
kind: String,
|
||||
enabled: bool,
|
||||
// `default` (§11 tail) so the interactive-create RETURNING clauses (which
|
||||
// build app-owned rows, never sealed) still hydrate; the group/app list
|
||||
// queries that must see a true value SELECT it explicitly.
|
||||
#[sqlx(default)]
|
||||
sealed: bool,
|
||||
dispatch_mode: String,
|
||||
retry_max_attempts: i32,
|
||||
retry_backoff: String,
|
||||
|
||||
Reference in New Issue
Block a user