Files
PiCloud/crates/manager-core/src/suppression_repo.rs
MechaCat02 86c57e2e43 feat(suppress): owner-polymorphic suppression repo + reconcile (M1.2)
suppression_repo takes a ScriptOwner (app or group), binding the matching
owner column. apply_service load_current / create / prune / suppression_report
all thread owner.as_script_owner(); the validate_bundle_for group-suppress
rejection and the manifest parse guard are dropped — a [group] may now declare
[suppress] to decline a template it inherits from a higher ancestor. No
runtime consumption effect yet (the chain filters land in M1.3/M1.4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:04:32 +02:00

109 lines
3.8 KiB
Rust

//! Template-suppression markers (§11 tail) — the `template_suppressions` table
//! (0058, made owner-polymorphic in 0060). A marker `(owner, target_kind,
//! reference)` records that an OWNER opts OUT of an inherited group template: a
//! handler script name it declines (`target_kind='trigger'`) or a path it
//! declines (`target_kind='route'`).
//!
//! Owner-polymorphic (like `extension_points`): an **app** declines for itself;
//! a **group** declines for its whole subtree. The functions take a
//! `ScriptOwner` and bind the matching owner column. Same tx-function style:
//! read over `&PgPool`, write over a `&mut Transaction`.
use picloud_shared::ScriptOwner;
use sqlx::{PgPool, Postgres, Transaction};
/// The two suppressible template kinds, as stored in `target_kind`.
pub const TARGET_TRIGGER: &str = "trigger";
pub const TARGET_ROUTE: &str = "route";
/// List the suppression markers declared at `owner`, as `(target_kind,
/// reference)` pairs, stably ordered. Backs `load_current` (the apply diff) and
/// the read-only `suppress ls`.
pub async fn list_for_owner(
pool: &PgPool,
owner: ScriptOwner,
) -> Result<Vec<(String, String)>, sqlx::Error> {
let sql = match owner {
ScriptOwner::App(_) => {
"SELECT target_kind, reference FROM template_suppressions \
WHERE app_id = $1 ORDER BY target_kind, LOWER(reference)"
}
ScriptOwner::Group(_) => {
"SELECT target_kind, reference FROM template_suppressions \
WHERE group_id = $1 ORDER BY target_kind, LOWER(reference)"
}
};
sqlx::query_as(sql)
.bind(owner_uuid(owner))
.fetch_all(pool)
.await
}
/// Insert a suppression marker in the apply transaction. Idempotent: re-apply
/// of an already-declared `(kind, reference)` is a no-op (`ON CONFLICT DO
/// NOTHING` against the per-owner partial unique index), so the marker survives
/// without a spurious version bump.
pub async fn insert_suppression_tx(
tx: &mut Transaction<'_, Postgres>,
owner: ScriptOwner,
target_kind: &str,
reference: &str,
) -> Result<(), sqlx::Error> {
let (app_id, group_id) = owner_columns(owner);
sqlx::query(
"INSERT INTO template_suppressions (app_id, group_id, target_kind, reference) \
VALUES ($1, $2, $3, $4) \
ON CONFLICT DO NOTHING",
)
.bind(app_id)
.bind(group_id)
.bind(target_kind)
.bind(reference)
.execute(&mut **tx)
.await?;
Ok(())
}
/// Delete a suppression marker in the apply transaction. Used by `--prune` when
/// the manifest stops declaring the reference → the template re-inherits.
pub async fn delete_suppression_tx(
tx: &mut Transaction<'_, Postgres>,
owner: ScriptOwner,
target_kind: &str,
reference: &str,
) -> Result<(), sqlx::Error> {
let sql = match owner {
ScriptOwner::App(_) => {
"DELETE FROM template_suppressions \
WHERE app_id = $1 AND target_kind = $2 AND reference = $3"
}
ScriptOwner::Group(_) => {
"DELETE FROM template_suppressions \
WHERE group_id = $1 AND target_kind = $2 AND reference = $3"
}
};
sqlx::query(sql)
.bind(owner_uuid(owner))
.bind(target_kind)
.bind(reference)
.execute(&mut **tx)
.await?;
Ok(())
}
/// `(app_id, group_id)` bind pair for the polymorphic owner — exactly one set.
fn owner_columns(owner: ScriptOwner) -> (Option<uuid::Uuid>, Option<uuid::Uuid>) {
match owner {
ScriptOwner::App(a) => (Some(a.into_inner()), None),
ScriptOwner::Group(g) => (None, Some(g.into_inner())),
}
}
/// The single owner UUID (app or group), for the WHERE-on-one-column queries.
fn owner_uuid(owner: ScriptOwner) -> uuid::Uuid {
match owner {
ScriptOwner::App(a) => a.into_inner(),
ScriptOwner::Group(g) => g.into_inner(),
}
}