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>
This commit is contained in:
@@ -1,52 +1,62 @@
|
||||
//! Template-suppression markers (§11 tail) — the `template_suppressions` table
|
||||
//! (0058). A marker `(app_id, target_kind, reference)` records that an app opts
|
||||
//! OUT of an inherited group template: a handler script name it declines
|
||||
//! (`target_kind='trigger'`) or a path it declines (`target_kind='route'`).
|
||||
//! (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'`).
|
||||
//!
|
||||
//! App-only (a group would just not declare the template), so — unlike the
|
||||
//! owner-polymorphic `extension_points` — these free functions take a plain
|
||||
//! `AppId`. Same tx-function style: read over `&PgPool`, write over a
|
||||
//! `&mut Transaction`.
|
||||
//! 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::AppId;
|
||||
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 `app_id`, as `(target_kind,
|
||||
/// 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_app(
|
||||
pub async fn list_for_owner(
|
||||
pool: &PgPool,
|
||||
app_id: AppId,
|
||||
owner: ScriptOwner,
|
||||
) -> Result<Vec<(String, String)>, sqlx::Error> {
|
||||
let rows: Vec<(String, String)> = sqlx::query_as(
|
||||
"SELECT target_kind, reference FROM template_suppressions \
|
||||
WHERE app_id = $1 ORDER BY target_kind, LOWER(reference)",
|
||||
)
|
||||
.bind(app_id.into_inner())
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows)
|
||||
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`), so the marker survives without a spurious version bump.
|
||||
/// 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>,
|
||||
app_id: AppId,
|
||||
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, target_kind, reference) \
|
||||
VALUES ($1, $2, $3) \
|
||||
ON CONFLICT (app_id, target_kind, reference) DO NOTHING",
|
||||
"INSERT INTO template_suppressions (app_id, group_id, target_kind, reference) \
|
||||
VALUES ($1, $2, $3, $4) \
|
||||
ON CONFLICT DO NOTHING",
|
||||
)
|
||||
.bind(app_id.into_inner())
|
||||
.bind(app_id)
|
||||
.bind(group_id)
|
||||
.bind(target_kind)
|
||||
.bind(reference)
|
||||
.execute(&mut **tx)
|
||||
@@ -58,18 +68,41 @@ pub async fn insert_suppression_tx(
|
||||
/// the manifest stops declaring the reference → the template re-inherits.
|
||||
pub async fn delete_suppression_tx(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
app_id: AppId,
|
||||
owner: ScriptOwner,
|
||||
target_kind: &str,
|
||||
reference: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query(
|
||||
"DELETE FROM template_suppressions \
|
||||
WHERE app_id = $1 AND target_kind = $2 AND reference = $3",
|
||||
)
|
||||
.bind(app_id.into_inner())
|
||||
.bind(target_kind)
|
||||
.bind(reference)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user