Files
collective-lists/supabase/migrations/023_section_visibility.sql
Oier Bravo Urtasun 59a32f80f7 feat(fase-12): section visibility model — feature_flags + section_enabled()
Adds JSONB `feature_flags` columns on public.users and public.collectives
(default '{}') plus two SQL helpers:

  * public.known_sections() — IMMUTABLE, returns the canonical 4 sections
    (lists, tasks, notes, search). Adding a section is a fresh migration.
  * public.section_enabled(section, user, collective) — STABLE, returns
    boolean. Precedence is COLLECTIVE > USER > default TRUE, evaluated as
    a single COALESCE so Fase 13 can prepend a server layer as a one-line
    diff. Unknown section keys default TRUE (forward-compat).

No new RLS policy: writes to feature_flags piggyback on users_update_own
and collectives_update (migration 003). 16 new pgTAP assertions verify
column shape, function signatures, volatility, and precedence semantics.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 03:47:52 +02:00

98 lines
5.0 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Migration 023: section visibility / feature flags (Fase 12)
--
-- Lets each user hide top-level sections they don't use, and lets admins of a
-- collective hide them for every member. The control surface is intentionally
-- typed-rigid via `public.known_sections()` instead of being a free-form bag
-- of strings — adding a new section requires a migration (which is the same
-- diff that adds the section's routes anyway, so no extra friction).
--
-- ── Precedence ───────────────────────────────────────────────────────────────
-- Effective layers, MOST restrictive wins:
-- 1. (Fase 13) server — server_settings.default_sections jsonb
-- 2. collective — collectives.feature_flags jsonb (admin override)
-- 3. user — users.feature_flags jsonb (per-user override)
-- 4. default — true (ON)
-- Fase 12 implements layers 24. Fase 13 will extend `public.section_enabled()`
-- with a server layer above layer 2. The function body below is structured as
-- a single `coalesce(...)` so that adding the server layer is a one-line diff
-- (prepend the new SELECT to the coalesce) — no callers change shape.
--
-- ── Data shape ───────────────────────────────────────────────────────────────
-- `feature_flags jsonb` (object) where each key is a section name (matching
-- `known_sections()`) and the value is the boolean override. Missing key →
-- "no opinion" → defer to the next layer. Default for both columns is `{}`
-- so existing rows opt in to nothing and behave exactly as before.
--
-- RLS: write-paths on `users.feature_flags` and `collectives.feature_flags`
-- piggyback on the existing `users_update_own` and `collectives_update`
-- policies (migration 003). No new policy is required — the JSONB column is
-- covered by the row-level UPDATE policy automatically.
ALTER TABLE public.users
ADD COLUMN feature_flags jsonb NOT NULL DEFAULT '{}'::jsonb;
ALTER TABLE public.collectives
ADD COLUMN feature_flags jsonb NOT NULL DEFAULT '{}'::jsonb;
COMMENT ON COLUMN public.users.feature_flags IS
'Per-user section visibility overrides. Keys must be in public.known_sections().';
COMMENT ON COLUMN public.collectives.feature_flags IS
'Per-collective section visibility overrides set by an admin. Outranks the user layer.';
-- ── known_sections() ────────────────────────────────────────────────────────
-- IMMUTABLE: the result is a compile-time literal of the source rows in this
-- migration. Adding/removing a section is a fresh migration, not a runtime
-- change, so it's safe to inline as IMMUTABLE.
CREATE OR REPLACE FUNCTION public.known_sections()
RETURNS text[]
LANGUAGE sql
IMMUTABLE
AS $$
SELECT ARRAY['lists', 'tasks', 'notes', 'search']::text[]
$$;
COMMENT ON FUNCTION public.known_sections() IS
'Canonical list of top-level sections that participate in feature_flags. '
'Each new section requires a migration that updates this function + a '
'toggle row in /settings + /collective/manage.';
-- ── section_enabled() ───────────────────────────────────────────────────────
-- Resolves the effective ON/OFF state for one section, in one statement, with
-- the layered precedence documented at the top of this file.
--
-- The `coalesce(NULL, NULL, default)` pattern relies on `(jsonb ->> key)`
-- returning NULL when the key is missing — so a layer with "no opinion"
-- transparently falls through. An unknown section key returns NULL from every
-- layer and the trailing `true` wins, matching the documented default-ON
-- behaviour (a new feature is visible by default until someone opts out).
CREATE OR REPLACE FUNCTION public.section_enabled(
p_section text,
p_user uuid,
p_collective uuid
)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
SELECT COALESCE(
-- Fase 13 will prepend a server layer here:
-- (SELECT (value ->> p_section)::boolean
-- FROM public.server_settings
-- WHERE key = 'default_sections'),
(SELECT (feature_flags ->> p_section)::boolean
FROM public.collectives
WHERE id = p_collective),
(SELECT (feature_flags ->> p_section)::boolean
FROM public.users
WHERE id = p_user),
true
);
$$;
COMMENT ON FUNCTION public.section_enabled(text, uuid, uuid) IS
'Effective ON/OFF of a top-level section for (user, collective). '
'Precedence: collective override > user override > default true. '
'Fase 13 will prepend a server-settings layer above the collective.';