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:
2026-05-19 02:03:41 +02:00
parent 4832981e25
commit bd9a0a6d3f
9 changed files with 336 additions and 9 deletions

View File

@@ -72,6 +72,7 @@ export interface Database {
emoji: string;
created_by: string;
feature_flags: Json;
default_list_title: string | null;
created_at: string;
deleted_at: string | null;
};
@@ -81,6 +82,7 @@ export interface Database {
emoji?: string;
created_by: string;
feature_flags?: Json;
default_list_title?: string | null;
created_at?: string;
deleted_at?: string | null;
};
@@ -90,6 +92,7 @@ export interface Database {
emoji?: string;
created_by?: string;
feature_flags?: Json;
default_list_title?: string | null;
created_at?: string;
deleted_at?: string | null;
};
@@ -136,6 +139,31 @@ export interface Database {
}
];
};
collective_list_titles: {
Row: {
collective_id: string;
title: string;
created_at: string;
};
Insert: {
collective_id: string;
title: string;
created_at?: string;
};
Update: {
collective_id?: string;
title?: string;
created_at?: string;
};
Relationships: [
{
foreignKeyName: 'collective_list_titles_collective_id_fkey';
columns: ['collective_id'];
referencedRelation: 'collectives';
referencedColumns: ['id'];
}
];
};
collective_invitations: {
Row: {
id: string;
@@ -629,6 +657,14 @@ export interface Database {
Args: { p_collective_id: string; p_name: string };
Returns: void;
};
add_list_title: {
Args: { p_collective_id: string; p_title: string };
Returns: void;
};
remove_list_title: {
Args: { p_collective_id: string; p_title: string };
Returns: void;
};
};
Enums: {
language_code: LanguageCode;