-- Migration : fonction `create_event` (public, service_role uniquement) -- Réf. : docs/decisions.md ("2026-09-18 (bis)", création d'événement via un -- formulaire n8n + Edge Function `create-event`), docs/modele-donnees.md -- (table `events`), 20260916090400_create_public_functions.sql (pattern -- des fonctions SECURITY DEFINER réservées à service_role). -- -- Avant cette migration, la création d'un event se faisait uniquement en SQL -- manuel (cf. docs/cahier-des-charges.md, livrable non coché "Documentation -- courte : comment créer un nouvel événement manuellement"). Cette fonction -- devient le point d'entrée unique, appelée par la nouvelle Edge Function -- `supabase/functions/create-event/index.ts` (elle-même appelée par un -- workflow n8n serveur-à-serveur, jamais depuis le front). -- -- ⚠️ Ne pas appliquer depuis cet agent (Kévin applique lui-même). -- unaccent est nécessaire pour dériver un slug lisible à partir de `nom` -- (ex: "Mariage Julie & Marc" -> "mariage-julie-marc"), même logique que -- `sanitizeFilenamePart` côté admin-gallery/index.ts mais en SQL (extension -- contrib standard, pas de dépendance tierce). create extension if not exists unaccent with schema extensions; -- ============================================================================= -- photobooth.generate_unique_slug — interne (pas un point de contrat public) -- ============================================================================= -- Dérive un slug lisible à partir d'un texte libre, et garantit son unicité -- contre photobooth.events.slug en ajoutant un suffixe numérique en cas de -- collision ("mariage-julie-marc", puis "mariage-julie-marc-2", etc.). -- Fonction interne au schéma photobooth : ce n'est pas un point de contrat -- public (comme photobooth.events_set_expire_at()), pas besoin d'être dans -- public ni d'avoir des grants dédiés (appelée uniquement par -- public.create_event, SECURITY DEFINER, propriétaire postgres). create or replace function photobooth.generate_unique_slug(p_source text) returns text language plpgsql as $$ declare v_base_slug text; v_candidate text; v_suffix integer := 1; -- Garde-fou défensif : en pratique la boucle se termine toujours au -- premier ou deuxième tour (collision rarissime), mais on évite une -- boucle infinie en cas de bug/donnée pathologique. v_max_attempts constant integer := 1000; begin v_base_slug := lower(trim(both '-' from regexp_replace(extensions.unaccent(coalesce(p_source, '')), '[^a-zA-Z0-9]+', '-', 'g') )); if v_base_slug = '' then v_base_slug := 'evenement'; end if; v_candidate := v_base_slug; while exists (select 1 from photobooth.events where slug = v_candidate) loop v_suffix := v_suffix + 1; v_candidate := v_base_slug || '-' || v_suffix; if v_suffix > v_max_attempts then raise exception 'slug_generation_failed' using errcode = 'P0001', detail = format('Impossible de générer un slug unique pour "%s" après %s tentatives.', p_source, v_max_attempts); end if; end loop; return v_candidate; end; $$; comment on function photobooth.generate_unique_slug(text) is 'Dérive un slug lisible et unique (photobooth.events.slug) à partir d''un texte libre (typiquement events.nom) : normalise via unaccent, remplace tout ce qui n''est pas [a-zA-Z0-9] par un tiret, ajoute un suffixe numérique en cas de collision. Fonction interne, appelée uniquement par public.create_event.'; -- ============================================================================= -- create_event — service_role UNIQUEMENT (NOUVEAU) -- ============================================================================= -- Point d'entrée unique pour créer un événement. Réservée à service_role : -- appelée par l'Edge Function `create-event`, elle-même appelée uniquement -- par le workflow n8n de création d'événement (jamais depuis le front, pas -- de formulaire de création dans l'appli photobooth-qr, cf. décision -- "2026-09-18 (bis)"). -- -- Ne calcule NI admin_token NI expire_at explicitement : ces deux valeurs -- sont déjà garanties par la table elle-même (default -- `encode(extensions.gen_random_bytes(32), 'hex')` pour admin_token, trigger -- `trg_events_set_expire_at` pour expire_at, cf. -- 20260916090100_create_events_table.sql) — dupliquer cette logique ici -- créerait deux sources de vérité pour le même comportement. create or replace function public.create_event( p_nom text, p_date_evenement date, p_client_nom text default null, p_max_photos_per_guest integer default null ) returns table ( id uuid, slug text, nom text, date_evenement date, expire_at timestamptz, admin_token text, client_nom text, max_photos_per_guest integer ) language plpgsql security definer set search_path = public, photobooth, extensions, pg_temp as $$ declare v_slug text; begin -- Revalidation défensive côté SQL (l'Edge Function valide déjà ces mêmes -- règles avant l'appel RPC, cf. supabase/functions/create-event/index.ts) : -- même principe que public.insert_public_photo(), qui ne fait pas confiance -- à un appelant "déjà propre". if p_nom is null or trim(p_nom) = '' then raise exception 'nom_required' using errcode = 'P0001', detail = 'Le nom de l''événement est requis.'; end if; if p_date_evenement is null then raise exception 'date_evenement_required' using errcode = 'P0001', detail = 'La date de l''événement est requise.'; end if; if p_max_photos_per_guest is not null and p_max_photos_per_guest <= 0 then raise exception 'max_photos_per_guest_invalid' using errcode = 'P0001', detail = 'max_photos_per_guest doit être un entier strictement positif si fourni.'; end if; v_slug := photobooth.generate_unique_slug(p_nom); return query insert into photobooth.events (slug, nom, date_evenement, client_nom, max_photos_per_guest) values (v_slug, p_nom, p_date_evenement, p_client_nom, p_max_photos_per_guest) returning events.id, events.slug, events.nom, events.date_evenement, events.expire_at, events.admin_token, events.client_nom, events.max_photos_per_guest; end; $$; comment on function public.create_event(text, date, text, integer) is 'Crée un événement (photobooth.events) : génère un slug unique à partir de `nom`, laisse la table calculer admin_token (default) et expire_at (trigger). Réservée à service_role (Edge Function create-event) : ne JAMAIS accorder EXECUTE à anon/authenticated.'; revoke all on function public.create_event(text, date, text, integer) from public, anon, authenticated; grant execute on function public.create_event(text, date, text, integer) to service_role;