feat(v1.1.5): pubsub::publish_durable SDK + pubsub:* triggers

Durable pub/sub through the universal outbox — the sixth trigger kind.

- `pubsub::publish_durable(topic, message)` Rhai SDK (no handle; topics
  ARE the grouping unit). Message JSON-encoded; Blobs base64 at any
  depth.
- `PubsubService` trait in picloud-shared with the topic matcher +
  validator (exact / `<prefix>.*` / `*`; mid-pattern wildcards
  rejected). `PostgresPubsubRepo` + `PubsubServiceImpl` in manager-core.
- Publish-time fan-out: one outbox row per matching enabled pubsub
  trigger, all in ONE transaction (no half-fan-out on crash). No
  matching trigger → publish succeeds silently, zero rows.
- `pubsub:*` trigger kind via Layout-E (0020: widen both CHECKs +
  pubsub_trigger_details + partial index), TriggerEvent::Pubsub +
  ctx.event.pubsub, dispatcher arm, admin endpoint POST /triggers/pubsub
  (validates topic pattern + reuses validate_trigger_target).
- AppPubsubPublish capability → script:write (seven-scope held).
- Dashboard Pub/Sub trigger form on the Triggers tab + list rendering.

publish_ephemeral stays deferred to v1.2. ~18 new tests (service
in-memory incl. transactional-rollback, shared matcher, bridge
encoding). No DB required for the suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-03 21:37:06 +02:00
parent 6e132b6ee0
commit 834c787ee1
25 changed files with 1240 additions and 16 deletions

View File

@@ -177,6 +177,15 @@ pub enum TriggerEvent {
prev: Option<serde_json::Value>,
},
/// A durable pub/sub publish fired this handler. v1.1.5. Carries
/// the topic, the JSON-decoded message, and the publish instant.
/// Surfaced to scripts as `ctx.event.pubsub`.
Pubsub {
topic: String,
message: serde_json::Value,
published_at: DateTime<Utc>,
},
/// A dead-letter row fired this handler. The original event is
/// nested verbatim plus the dead-letter metadata the design notes
/// §4 require.
@@ -203,6 +212,7 @@ impl TriggerEvent {
Self::Docs { .. } => "docs",
Self::Cron { .. } => "cron",
Self::Files { .. } => "files",
Self::Pubsub { .. } => "pubsub",
Self::DeadLetter { .. } => "dead_letter",
}
}