feat(hierarchies): trigger templates — per-app fan-out (M4b)
Completes §4.5 templates (M4a shipped routes). A group declares a trigger once;
the tree apply fans it out into one concrete per-app_id trigger on each
descendant app, reusing the M4a expansion engine over the trigger insert path.
- Migration 0054: `trigger_templates` table (group-owned; the whole
BundleTrigger wire object stored as `spec` JSONB, placeholders unresolved) +
`triggers.from_template` provenance column + index.
- `template_repo.rs`: trigger-template CRUD + chain-load.
- Manifest: `[group]` `[[trigger_templates]]` (flat: name, kind, script, + kind
params); build_bundle emits them; rejected on an app node.
- Apply: group nodes reconcile trigger-template rows; tree Phase B expands each
chain trigger template into a concrete trigger per descendant app —
placeholders resolved in every spec string leaf, then the typed trigger is
rebuilt and inserted through the shared `insert_bundle_trigger_tx` (email
secrets resolved + re-sealed per recipient app). Idempotent via from_template
(one trigger per template per app; semantic-identity compare → no-op /
delete+recreate / reap). Collision with a hand-declared trigger or between
templates is a hard error. Each resolved trigger is re-validated with the
per-kind shape check (cron/queue/etc.) so a {var:}-injected bad
schedule/timeout can't slip in.
- Authz: declaring → GroupScriptsWrite; receiving → AppManageTriggers, plus
AppSecretsRead for email-trigger recipients (parity with hand-declared email).
- Plan/token/report extended for trigger templates; blast radius covers both.
- Tests: tests/templates.rs trigger fan-out (kv + {app_slug}, stable-ids,
prune-reap); resolve_placeholders_in_json unit test; schema golden reblessed.
Reviewed (no CRITICAL; isolation + per-app email-secret sealing verified
sound). Closed a HIGH validation-parity gap (re-validate resolved triggers) and
a MEDIUM authz gap (AppSecretsRead for email expansions). v1.2 Hierarchies
template work (M4) complete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
33
crates/manager-core/migrations/0054_trigger_templates.sql
Normal file
33
crates/manager-core/migrations/0054_trigger_templates.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- Phase 5 / M4b (v1.2 Hierarchies): TRIGGER templates (§4.5).
|
||||
--
|
||||
-- The trigger half of templates (M4a shipped routes). A `[group]` manifest
|
||||
-- declares `[[trigger_templates.<kind>]]`; the apply fans each one out into a
|
||||
-- concrete per-`app_id` trigger on every descendant app, reusing the same
|
||||
-- expansion engine, placeholder set (`{app_slug}`/`{env}`/`{var:NAME}`), and
|
||||
-- `from_template` provenance as routes.
|
||||
--
|
||||
-- A trigger's parameters vary by kind (collection_glob/ops, schedule/timezone,
|
||||
-- topic_pattern, queue_name, inbound_secret_ref, …), so the template stores the
|
||||
-- whole `BundleTrigger` wire object as `spec` JSONB (kind tag included). The
|
||||
-- expansion resolves placeholders in the spec's string leaves, then rebuilds the
|
||||
-- typed trigger and inserts it through the normal trigger path (email secrets
|
||||
-- are resolved + re-sealed per app, exactly like a hand-declared email trigger).
|
||||
|
||||
CREATE TABLE trigger_templates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
||||
-- Identity/upsert key, unique per group (case-insensitive).
|
||||
name TEXT NOT NULL,
|
||||
-- The full `BundleTrigger` wire object (internally tagged by `kind`), with
|
||||
-- placeholders left unresolved. Rebuilt + resolved per descendant at apply.
|
||||
spec JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE UNIQUE INDEX trigger_templates_group_name_idx
|
||||
ON trigger_templates (group_id, LOWER(name));
|
||||
|
||||
-- Provenance on expanded triggers (mirrors routes.from_template, 0053). NULL =
|
||||
-- hand-declared; non-NULL = stamped from this template, managed by expansion.
|
||||
ALTER TABLE triggers ADD COLUMN from_template UUID;
|
||||
CREATE INDEX triggers_from_template_idx ON triggers (app_id, from_template);
|
||||
Reference in New Issue
Block a user