feat(triggers): show a materialized column in pic triggers ls --app (D1)

A materialized copy of an M5 group stateful template (cron/queue/email) now
reads as `materialized = true` — inherited, read-only — distinct from a
hand-authored trigger. Threads a derived `materialized` bool (=
`materialized_from IS NOT NULL`) row → domain → API → CLI, mirroring the
`sealed`/`shared` columns; `list_for_app` SELECTs `materialized_from`. No
migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-02 21:30:32 +02:00
parent 849bd367b9
commit 137103a429
7 changed files with 69 additions and 2 deletions

View File

@@ -4121,6 +4121,7 @@ mod tests {
enabled: true,
sealed: false,
shared: false,
materialized: false,
dispatch_mode: TriggerDispatchMode::Async,
retry_max_attempts: 3,
retry_backoff: BackoffShape::Exponential,

View File

@@ -50,6 +50,9 @@ pub enum TriggerRepoError {
/// Parent-table row plus the per-kind detail merged in. Serialized
/// back to admin clients via the JSON API.
// enabled + sealed + shared + materialized are distinct trigger facets, not a
// bit-packed config — a struct is the right shape.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trigger {
pub id: TriggerId,
@@ -71,6 +74,11 @@ pub struct Trigger {
/// collection, not per-app ones). Also part of the apply diff identity.
#[serde(default)]
pub shared: bool,
/// §4.5 M5: `true` for a materialized copy of a group stateful template
/// (`materialized_from` is set). Read-only — inherited, not app-authored.
/// Derived, not stored directly (see `TriggerRow::materialized_from`).
#[serde(default)]
pub materialized: bool,
pub dispatch_mode: TriggerDispatchMode,
pub retry_max_attempts: u32,
pub retry_backoff: BackoffShape,
@@ -907,6 +915,7 @@ impl TriggerRepo for PostgresTriggerRepo {
enabled: parent.enabled,
sealed: parent.sealed,
shared: parent.shared,
materialized: parent.materialized_from.is_some(),
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)
@@ -976,6 +985,7 @@ impl TriggerRepo for PostgresTriggerRepo {
enabled: parent.enabled,
sealed: parent.sealed,
shared: parent.shared,
materialized: parent.materialized_from.is_some(),
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)
@@ -1040,6 +1050,7 @@ impl TriggerRepo for PostgresTriggerRepo {
enabled: parent.enabled,
sealed: parent.sealed,
shared: parent.shared,
materialized: parent.materialized_from.is_some(),
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)
@@ -1110,6 +1121,7 @@ impl TriggerRepo for PostgresTriggerRepo {
enabled: parent.enabled,
sealed: parent.sealed,
shared: parent.shared,
materialized: parent.materialized_from.is_some(),
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)
@@ -1180,6 +1192,7 @@ impl TriggerRepo for PostgresTriggerRepo {
enabled: parent.enabled,
sealed: parent.sealed,
shared: parent.shared,
materialized: parent.materialized_from.is_some(),
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)
@@ -1245,6 +1258,7 @@ impl TriggerRepo for PostgresTriggerRepo {
enabled: parent.enabled,
sealed: parent.sealed,
shared: parent.shared,
materialized: parent.materialized_from.is_some(),
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)
@@ -1308,6 +1322,7 @@ impl TriggerRepo for PostgresTriggerRepo {
enabled: parent.enabled,
sealed: parent.sealed,
shared: parent.shared,
materialized: parent.materialized_from.is_some(),
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)
@@ -1352,8 +1367,8 @@ 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, sealed, shared, dispatch_mode, \
retry_max_attempts, retry_backoff, retry_base_ms, \
"SELECT id, app_id, script_id, name, kind, enabled, sealed, shared, materialized_from, \
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",
)
@@ -1756,6 +1771,7 @@ impl TriggerRepo for PostgresTriggerRepo {
enabled: parent.enabled,
sealed: parent.sealed,
shared: parent.shared,
materialized: parent.materialized_from.is_some(),
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)
@@ -1968,6 +1984,7 @@ async fn hydrate_one(pool: &PgPool, parent: TriggerRow) -> Result<Trigger, Trigg
enabled: parent.enabled,
sealed: parent.sealed,
shared: parent.shared,
materialized: parent.materialized_from.is_some(),
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)
@@ -2024,6 +2041,11 @@ struct TriggerRow {
// §11.6: same `default` treatment as `sealed`.
#[sqlx(default)]
shared: bool,
// §4.5 M5: set on a materialized copy of a group stateful template; NULL on
// a hand-authored trigger. `default` like the flags above — only the app/
// group list queries SELECT it.
#[sqlx(default)]
materialized_from: Option<Uuid>,
dispatch_mode: String,
retry_max_attempts: i32,
retry_backoff: String,

View File

@@ -910,6 +910,7 @@ mod tests {
enabled: true,
sealed: false,
shared: false,
materialized: false,
dispatch_mode: req.dispatch_mode,
retry_max_attempts: req.retry_max_attempts,
retry_backoff: req.retry_backoff,
@@ -942,6 +943,7 @@ mod tests {
enabled: true,
sealed: false,
shared: false,
materialized: false,
dispatch_mode: req.dispatch_mode,
retry_max_attempts: req.retry_max_attempts,
retry_backoff: req.retry_backoff,
@@ -974,6 +976,7 @@ mod tests {
enabled: true,
sealed: false,
shared: false,
materialized: false,
dispatch_mode: TriggerDispatchMode::Async,
retry_max_attempts: 1,
retry_backoff: BackoffShape::Constant,
@@ -1007,6 +1010,7 @@ mod tests {
enabled: true,
sealed: false,
shared: false,
materialized: false,
dispatch_mode: TriggerDispatchMode::Async,
retry_max_attempts: 3,
retry_backoff: BackoffShape::Exponential,
@@ -1058,6 +1062,7 @@ mod tests {
enabled: true,
sealed: false,
shared: false,
materialized: false,
dispatch_mode: req.dispatch_mode,
retry_max_attempts: req.retry_max_attempts,
retry_backoff: req.retry_backoff,
@@ -1091,6 +1096,7 @@ mod tests {
enabled: true,
sealed: false,
shared: false,
materialized: false,
dispatch_mode: req.dispatch_mode,
retry_max_attempts: req.retry_max_attempts,
retry_backoff: req.retry_backoff,
@@ -1123,6 +1129,7 @@ mod tests {
enabled: true,
sealed: false,
shared: false,
materialized: false,
dispatch_mode: req.dispatch_mode,
retry_max_attempts: req.retry_max_attempts,
retry_backoff: req.retry_backoff,
@@ -1203,6 +1210,7 @@ mod tests {
enabled: true,
sealed: false,
shared: false,
materialized: false,
dispatch_mode: req.dispatch_mode,
retry_max_attempts: req.retry_max_attempts,
retry_backoff: req.retry_backoff,