feat(fase-18): migration 027 — list title catalog + per-collective default
Adds `collectives.default_list_title` (nullable text) plus a new `collective_list_titles (collective_id, title)` table — admins curate via two SECURITY DEFINER RPCs (`add_list_title` / `remove_list_title`, P0002 on non-admin, idempotent on conflict). Members read; direct writes are all-deny RLS, matching the item_frequency pattern. Table is added to the `supabase_realtime` publication with REPLICA IDENTITY FULL so the manage UI sees admin edits live across browsers. Numbering rules (#N suffix) stay client-side intentionally — the DB is agnostic about the format so future "YYYY-MM" or other prefixes don't require a schema migration. The two-clients-race-on-#6 case is accepted (documented in migration header); a unique constraint would block deliberate duplicates. Wires `default_list_title` through every Collective construction site: loadUserCollectives in root layout, onboarding, CreateCollectiveModal, features.ts realtime subscriber, and the manage-collective patcher. The hand-curated `database.ts` gets the new column, table, and two RPC signatures. pgTAP `019_list_title_catalog.sql` (13 assertions): schema invariants on the column + table, PK shape, RLS posture, two-policies count, RPC existence + SECURITY DEFINER flag, realtime publication membership, and the default-NULL behaviour. Role-gate denial is covered by the upcoming list-title-flow integration suite (postgres bypasses auth.uid inside pgTAP, same caveat as Fase 15's 018_item_frequency_weight.sql). Tests: 191 pgTAP (was 178; +13). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
123
supabase/tests/019_list_title_catalog.sql
Normal file
123
supabase/tests/019_list_title_catalog.sql
Normal file
@@ -0,0 +1,123 @@
|
||||
-- pgTAP: collectives.default_list_title + collective_list_titles +
|
||||
-- add_list_title / remove_list_title RPCs (Fase 18.1).
|
||||
-- Run with: psql -U postgres -d postgres -f supabase/tests/019_list_title_catalog.sql
|
||||
--
|
||||
-- Schema invariants for the new column and table, plus existence + SECURITY
|
||||
-- DEFINER posture of the two write RPCs. As with the Fase 15 weight RPCs,
|
||||
-- behavioural denial-for-non-admin runs through the postgres superuser here,
|
||||
-- so we cannot exercise the role gate from inside pgTAP (postgres bypasses
|
||||
-- the function's `auth.uid()` check by returning NULL). Role-gate behaviour
|
||||
-- for member / guest is covered by the Vitest integration suite
|
||||
-- (list-title-flow.test.ts).
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgtap;
|
||||
|
||||
BEGIN;
|
||||
SELECT plan(13);
|
||||
|
||||
-- ── collectives.default_list_title column ───────────────────────────────────
|
||||
|
||||
SELECT has_column(
|
||||
'public', 'collectives', 'default_list_title',
|
||||
'LTC-T01: collectives.default_list_title column exists'
|
||||
);
|
||||
|
||||
SELECT col_type_is(
|
||||
'public', 'collectives', 'default_list_title', 'text',
|
||||
'LTC-T02: collectives.default_list_title is text'
|
||||
);
|
||||
|
||||
SELECT col_is_null(
|
||||
'public', 'collectives', 'default_list_title',
|
||||
'LTC-T03: collectives.default_list_title is nullable'
|
||||
);
|
||||
|
||||
-- ── collective_list_titles table ────────────────────────────────────────────
|
||||
|
||||
SELECT has_table(
|
||||
'public', 'collective_list_titles',
|
||||
'LTC-T04: collective_list_titles table exists'
|
||||
);
|
||||
|
||||
SELECT col_is_pk(
|
||||
'public', 'collective_list_titles', ARRAY['collective_id', 'title'],
|
||||
'LTC-T05: collective_list_titles PK is (collective_id, title)'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT relrowsecurity FROM pg_class
|
||||
WHERE relname = 'collective_list_titles'
|
||||
AND relnamespace = 'public'::regnamespace),
|
||||
'LTC-T06: collective_list_titles has RLS enabled'
|
||||
);
|
||||
|
||||
-- The select policy lets members read; the catch-all deny blocks direct
|
||||
-- writes (admins write via the RPCs below). Two policies expected.
|
||||
SELECT is(
|
||||
(SELECT count(*)::int FROM pg_policies
|
||||
WHERE schemaname = 'public' AND tablename = 'collective_list_titles'),
|
||||
2,
|
||||
'LTC-T07: collective_list_titles has exactly 2 policies (select + deny)'
|
||||
);
|
||||
|
||||
-- ── RPC existence + SECURITY DEFINER posture ────────────────────────────────
|
||||
|
||||
SELECT has_function(
|
||||
'public', 'add_list_title', ARRAY['uuid', 'text'],
|
||||
'LTC-T08: add_list_title(uuid, text) exists'
|
||||
);
|
||||
|
||||
SELECT has_function(
|
||||
'public', 'remove_list_title', ARRAY['uuid', 'text'],
|
||||
'LTC-T09: remove_list_title(uuid, text) exists'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT prosecdef FROM pg_proc
|
||||
WHERE proname = 'add_list_title'
|
||||
AND pronamespace = 'public'::regnamespace),
|
||||
'LTC-T10: add_list_title is SECURITY DEFINER'
|
||||
);
|
||||
|
||||
SELECT ok(
|
||||
(SELECT prosecdef FROM pg_proc
|
||||
WHERE proname = 'remove_list_title'
|
||||
AND pronamespace = 'public'::regnamespace),
|
||||
'LTC-T11: remove_list_title is SECURITY DEFINER'
|
||||
);
|
||||
|
||||
-- ── Realtime publication membership ─────────────────────────────────────────
|
||||
-- The /collective/manage UI subscribes to collective_list_titles so member
|
||||
-- clients see admin edits live without a page reload. Add to the publication
|
||||
-- in the same migration (documented in CLAUDE.md "Realtime publication"
|
||||
-- gotcha).
|
||||
|
||||
SELECT ok(
|
||||
EXISTS (
|
||||
SELECT 1 FROM pg_publication_tables
|
||||
WHERE pubname = 'supabase_realtime'
|
||||
AND schemaname = 'public'
|
||||
AND tablename = 'collective_list_titles'
|
||||
),
|
||||
'LTC-T12: collective_list_titles is in supabase_realtime publication'
|
||||
);
|
||||
|
||||
-- ── Behaviour: default_list_title defaults to NULL on a fresh collective ────
|
||||
|
||||
INSERT INTO public.collectives (id, name, emoji, created_by)
|
||||
VALUES (
|
||||
'99999999-9999-9999-9999-999999999999',
|
||||
'LTC-test-collective',
|
||||
'🏠',
|
||||
'11111111-1111-1111-1111-111111111111'
|
||||
);
|
||||
|
||||
SELECT is(
|
||||
(SELECT default_list_title FROM public.collectives
|
||||
WHERE id = '99999999-9999-9999-9999-999999999999'),
|
||||
NULL,
|
||||
'LTC-T13: default_list_title defaults to NULL on insert'
|
||||
);
|
||||
|
||||
SELECT * FROM finish();
|
||||
ROLLBACK;
|
||||
Reference in New Issue
Block a user