diff --git a/crates/manager-core/migrations/0058_template_suppressions.sql b/crates/manager-core/migrations/0058_template_suppressions.sql new file mode 100644 index 0000000..967178a --- /dev/null +++ b/crates/manager-core/migrations/0058_template_suppressions.sql @@ -0,0 +1,36 @@ +-- §11 tail (v1.2 Hierarchies): per-app opt-out of inherited group templates. +-- +-- A group TRIGGER or ROUTE template inherits to every descendant app. Until now +-- a descendant could SHADOW a template (declare its own identical binding) but +-- not DECLINE one. This marker records that a specific app opts OUT of a +-- specific inherited template — the template then stops resolving for that app +-- (and only that app; a sibling subtree is unaffected). +-- +-- Coarse by REFERENCE (not row id — template ids churn on re-apply): a +-- suppression names a handler SCRIPT NAME (target_kind='trigger') or a PATH +-- (target_kind='route'). A reference may decline more than one inherited +-- template (a group that bound several to the same script/path). The dispatch +-- filters gate to group-owned rows, so an app can only decline what it +-- INHERITS — never its own trigger/route, never a sibling's. +-- +-- App-only: only an app suppresses (a group would just not declare the +-- template). So `app_id NOT NULL` — no polymorphic owner. It is pure app config +-- (like `extension_points`), so ON DELETE CASCADE (not the code-owner RESTRICT). + +CREATE TABLE template_suppressions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + app_id UUID NOT NULL REFERENCES apps(id) ON DELETE CASCADE, + target_kind TEXT NOT NULL CHECK (target_kind IN ('trigger', 'route')), + -- A handler script name (trigger) or a path (route). Matched + -- case-insensitively for triggers (as script-name resolution is + -- elsewhere) and exactly for routes; stored as authored. + reference TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- One marker per (app, kind, reference) — re-apply is a NoOp. +CREATE UNIQUE INDEX template_suppressions_uidx + ON template_suppressions (app_id, target_kind, reference); + +-- The trigger anti-join / route rebuild both look up by app_id. +CREATE INDEX template_suppressions_app_idx ON template_suppressions (app_id); diff --git a/crates/manager-core/tests/expected_schema.txt b/crates/manager-core/tests/expected_schema.txt index b4299fd..0d1a31e 100644 --- a/crates/manager-core/tests/expected_schema.txt +++ b/crates/manager-core/tests/expected_schema.txt @@ -376,6 +376,13 @@ table: secrets group_id: uuid NULL environment_scope: text NOT NULL default='*'::text +table: template_suppressions + id: uuid NOT NULL default=gen_random_uuid() + app_id: uuid NOT NULL + target_kind: text NOT NULL + reference: text NOT NULL + created_at: timestamp with time zone NOT NULL default=now() + table: topics app_id: uuid NOT NULL name: text NOT NULL @@ -605,6 +612,11 @@ indexes on secrets: secrets_app_uidx: public.secrets USING btree (app_id, environment_scope, name) WHERE (app_id IS NOT NULL) secrets_group_uidx: public.secrets USING btree (group_id, environment_scope, name) WHERE (group_id IS NOT NULL) +indexes on template_suppressions: + template_suppressions_app_idx: public.template_suppressions USING btree (app_id) + template_suppressions_pkey: public.template_suppressions USING btree (id) + template_suppressions_uidx: public.template_suppressions USING btree (app_id, target_kind, reference) + indexes on topics: topics_pkey: public.topics USING btree (app_id, name) @@ -833,6 +845,11 @@ constraints on secrets: [FOREIGN KEY] secrets_app_id_fkey: FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE [FOREIGN KEY] secrets_group_id_fkey: FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE +constraints on template_suppressions: + [CHECK] template_suppressions_target_kind_check: CHECK ((target_kind = ANY (ARRAY['trigger'::text, 'route'::text]))) + [FOREIGN KEY] template_suppressions_app_id_fkey: FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE + [PRIMARY KEY] template_suppressions_pkey: PRIMARY KEY (id) + constraints on topics: [CHECK] topics_auth_mode_check: CHECK ((auth_mode = ANY (ARRAY['public'::text, 'token'::text, 'session'::text]))) [FOREIGN KEY] topics_app_id_fkey: FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE @@ -913,3 +930,4 @@ constraints on vars: 0055: group files 0056: group triggers 0057: group routes + 0058: template suppressions