Files
2026-08-10 22:06:49 +02:00

564 lines
26 KiB
PL/PgSQL

-- =====================================================================
-- GameDev Tracker — Mise à jour complète (v2 + v3 + v4)
-- À coller dans : Supabase > SQL Editor > New query > Run
-- Sans danger si une partie a déjà été appliquée (tout est idempotent).
-- =====================================================================
-- ---------------------------------------------------------------------
-- v2 — Pôles, assignation multiple, notifications, suppression jalons
-- ---------------------------------------------------------------------
alter table public.tasks add column if not exists pole member_role;
alter table public.bugs add column if not exists pole member_role;
alter table public.tasks
add column if not exists assignee_ids uuid[] not null default '{}';
-- Reprend l'assigné unique existant dans le tableau (si la colonne existe encore).
do $$ begin
if exists (select 1 from information_schema.columns
where table_schema='public' and table_name='tasks' and column_name='assignee_id') then
update public.tasks
set assignee_ids = array[assignee_id]
where assignee_id is not null and (assignee_ids is null or assignee_ids = '{}');
alter table public.tasks drop column assignee_id;
end if;
end $$;
alter table public.tasks drop column if exists milestone_id;
drop table if exists public.milestones cascade;
drop type if exists milestone_status;
create table if not exists public.notifications (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references public.profiles(id) on delete cascade,
project_id uuid references public.projects(id) on delete cascade,
kind text not null,
title text not null,
body text,
entity_id uuid,
pole member_role,
read boolean not null default false,
created_at timestamptz not null default now()
);
create index if not exists idx_notifications_user on public.notifications(user_id);
alter table public.notifications enable row level security;
drop policy if exists notifications_select on public.notifications;
drop policy if exists notifications_insert on public.notifications;
drop policy if exists notifications_update on public.notifications;
drop policy if exists notifications_delete on public.notifications;
create policy notifications_select on public.notifications
for select to authenticated using (user_id = auth.uid());
create policy notifications_insert on public.notifications
for insert to authenticated with check (true);
create policy notifications_update on public.notifications
for update to authenticated using (user_id = auth.uid()) with check (user_id = auth.uid());
create policy notifications_delete on public.notifications
for delete to authenticated using (user_id = auth.uid());
do $$ begin
alter publication supabase_realtime add table public.notifications;
exception when duplicate_object then null; end $$;
-- ---------------------------------------------------------------------
-- v3 — Feuille de temps : entrées libres (réunion, etc.)
-- ---------------------------------------------------------------------
-- Crée la table si elle n'a jamais existé (ancienne base sans suivi du temps).
create table if not exists public.time_logs (
id uuid primary key default gen_random_uuid(),
project_id uuid references public.projects(id) on delete cascade,
task_id uuid references public.tasks(id) on delete cascade,
label text,
user_id uuid references public.profiles(id) on delete set null,
minutes integer not null check (minutes > 0),
note text,
logged_at date not null default current_date,
created_at timestamptz not null default now()
);
-- Si la table existait déjà (ancienne version), on l'aligne sur le nouveau format.
alter table public.time_logs alter column task_id drop not null;
alter table public.time_logs add column if not exists label text;
alter table public.time_logs
add column if not exists project_id uuid references public.projects(id) on delete cascade;
update public.time_logs tl
set project_id = t.project_id
from public.tasks t
where tl.task_id = t.id and tl.project_id is null;
create index if not exists idx_time_logs_task on public.time_logs(task_id);
create index if not exists idx_time_logs_project on public.time_logs(project_id);
create index if not exists idx_time_logs_user on public.time_logs(user_id);
alter table public.time_logs enable row level security;
drop policy if exists time_logs_all on public.time_logs;
create policy time_logs_all on public.time_logs
for all to authenticated using (true) with check (true);
do $$ begin
alter publication supabase_realtime add table public.time_logs;
exception when duplicate_object then null; end $$;
-- ---------------------------------------------------------------------
-- v4 — Plusieurs pôles (rôles) par membre
-- ---------------------------------------------------------------------
alter table public.profiles
add column if not exists roles member_role[] not null default '{}';
update public.profiles
set roles = array[role]
where roles = '{}';
-- ---------------------------------------------------------------------
-- v5 — Accueil (onboarding) des nouveaux membres
-- ---------------------------------------------------------------------
alter table public.profiles
add column if not exists onboarded boolean not null default false;
-- Les membres déjà présents ne sont pas ré-embarqués : on les marque « fait ».
update public.profiles set onboarded = true where onboarded = false;
-- ---------------------------------------------------------------------
-- v6 — Photo de profil (avatar image) : colonne + bucket de stockage
-- ---------------------------------------------------------------------
alter table public.profiles add column if not exists avatar_url text;
-- Bucket public "avatars" pour héberger les photos de profil.
insert into storage.buckets (id, name, public)
values ('avatars', 'avatars', true)
on conflict (id) do nothing;
-- Lecture publique ; écriture réservée aux membres connectés.
drop policy if exists avatars_read on storage.objects;
drop policy if exists avatars_insert on storage.objects;
drop policy if exists avatars_update on storage.objects;
drop policy if exists avatars_delete on storage.objects;
create policy avatars_read on storage.objects
for select using (bucket_id = 'avatars');
create policy avatars_insert on storage.objects
for insert to authenticated with check (bucket_id = 'avatars');
create policy avatars_update on storage.objects
for update to authenticated using (bucket_id = 'avatars');
create policy avatars_delete on storage.objects
for delete to authenticated using (bucket_id = 'avatars');
-- ---------------------------------------------------------------------
-- v7 — Plusieurs pôles par tâche
-- ---------------------------------------------------------------------
alter table public.tasks
add column if not exists poles member_role[] not null default '{}';
-- Reprend le pôle unique existant dans le tableau (si présent).
update public.tasks
set poles = array[pole]
where pole is not null and poles = '{}';
-- ---------------------------------------------------------------------
-- v8 — Checklists (cases à cocher) sur les tâches
-- ---------------------------------------------------------------------
-- Liste d'éléments [{ "id": "...", "text": "...", "done": false }, ...]
alter table public.tasks
add column if not exists checklist jsonb not null default '[]'::jsonb;
-- ---------------------------------------------------------------------
-- v9 — Assignation MULTIPLE des bugs (parité avec les tâches)
-- ---------------------------------------------------------------------
alter table public.bugs
add column if not exists assignee_ids uuid[] not null default '{}';
-- Reprend l'assigné unique existant dans le tableau (si présent).
update public.bugs
set assignee_ids = array[assignee_id]
where assignee_id is not null and assignee_ids = '{}';
-- =====================================================================
-- GameDev Tracker — Migration v10 « Refonte »
-- À coller dans : Supabase > SQL Editor > New query > Run
-- Idempotent : sans danger si déjà appliquée en tout ou partie.
--
-- Ce que fait cette migration :
-- 0. Colonnes « tableau » manquantes (dont tasks.tags) et non-NULL
-- 1. Commentaires sur les tâches et les bugs
-- 2. Pièces jointes (captures d'écran) sur les tâches et les bugs
-- 3. Parité tâches/bugs : plusieurs pôles par bug
-- 4. Suppression des colonnes « de compatibilité » devenues des pièges
-- (profiles.role, tasks.pole, bugs.assignee_id)
-- 5. Réordonnancement du Kanban en UNE requête transactionnelle
-- 6. Suppression d'un projet réservée à son créateur ou à un admin
-- 7. Temps réel sur les projets
-- =====================================================================
-- ---------------------------------------------------------------------
-- 0) Colonnes « tableau » garanties — À EXÉCUTER EN PREMIER
--
-- `tasks.tags` n'existait que dans schema.sql (installations neuves) et
-- n'a jamais été ajoutée par les migrations : sur une base mise à jour
-- pas à pas, la colonne manquait et l'application plantait en tentant
-- d'afficher les étiquettes d'une tâche.
--
-- Tant qu'on y est, on ré-affirme l'invariant sur TOUTES les colonnes
-- tableau : jamais NULL, toujours un tableau vide par défaut.
-- ---------------------------------------------------------------------
alter table public.tasks add column if not exists tags text[] not null default '{}';
alter table public.tasks add column if not exists poles member_role[] not null default '{}';
alter table public.tasks add column if not exists assignee_ids uuid[] not null default '{}';
alter table public.tasks add column if not exists checklist jsonb not null default '[]'::jsonb;
alter table public.bugs add column if not exists assignee_ids uuid[] not null default '{}';
alter table public.profiles add column if not exists roles member_role[] not null default '{}';
-- Remet à vide les valeurs NULL héritées, puis verrouille la contrainte.
do $$
declare
r record;
begin
for r in
select * from (values
('tasks', 'tags', '''{}''::text[]'),
('tasks', 'poles', '''{}''::member_role[]'),
('tasks', 'assignee_ids', '''{}''::uuid[]'),
('tasks', 'checklist', '''[]''::jsonb'),
('bugs', 'poles', '''{}''::member_role[]'),
('bugs', 'assignee_ids', '''{}''::uuid[]'),
('profiles', 'roles', '''{}''::member_role[]')
) as t(tbl, col, empty)
loop
if exists (
select 1 from information_schema.columns
where table_schema = 'public' and table_name = r.tbl and column_name = r.col
) then
execute format('update public.%I set %I = %s where %I is null', r.tbl, r.col, r.empty, r.col);
execute format('alter table public.%I alter column %I set default %s', r.tbl, r.col, r.empty);
execute format('alter table public.%I alter column %I set not null', r.tbl, r.col);
end if;
end loop;
end $$;
-- ---------------------------------------------------------------------
-- 1) Parité bugs / tâches : plusieurs pôles par bug
-- ---------------------------------------------------------------------
alter table public.bugs
add column if not exists poles member_role[] not null default '{}';
do $$ begin
if exists (select 1 from information_schema.columns
where table_schema = 'public' and table_name = 'bugs' and column_name = 'pole') then
update public.bugs set poles = array[pole] where pole is not null and poles = '{}';
end if;
end $$;
-- ---------------------------------------------------------------------
-- 2) Commentaires (fil de discussion sur une tâche ou un bug)
-- ---------------------------------------------------------------------
create table if not exists public.comments (
id uuid primary key default gen_random_uuid(),
project_id uuid not null references public.projects(id) on delete cascade,
entity_kind text not null check (entity_kind in ('task', 'bug')),
entity_id uuid not null,
author_id uuid references public.profiles(id) on delete set null,
body text not null check (length(btrim(body)) > 0),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists idx_comments_entity on public.comments(entity_kind, entity_id);
create index if not exists idx_comments_project on public.comments(project_id);
alter table public.comments enable row level security;
drop policy if exists comments_select on public.comments;
drop policy if exists comments_insert on public.comments;
drop policy if exists comments_update on public.comments;
drop policy if exists comments_delete on public.comments;
-- Toute l'équipe lit la discussion ; on n'écrit qu'en son propre nom ;
-- on ne modifie/supprime que ses propres messages (ou n'importe lequel si admin).
create policy comments_select on public.comments
for select to authenticated using (true);
create policy comments_insert on public.comments
for insert to authenticated with check (author_id = auth.uid());
create policy comments_update on public.comments
for update to authenticated
using (author_id = auth.uid()) with check (author_id = auth.uid());
create policy comments_delete on public.comments
for delete to authenticated
using (
author_id = auth.uid()
or exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
);
-- ---------------------------------------------------------------------
-- 3) Pièces jointes (captures d'écran, logs, fichiers)
-- ---------------------------------------------------------------------
create table if not exists public.attachments (
id uuid primary key default gen_random_uuid(),
project_id uuid not null references public.projects(id) on delete cascade,
entity_kind text not null check (entity_kind in ('task', 'bug')),
entity_id uuid not null,
uploader_id uuid references public.profiles(id) on delete set null,
name text not null,
path text not null, -- chemin dans le bucket "attachments"
size bigint not null default 0,
mime text,
created_at timestamptz not null default now()
);
create index if not exists idx_attachments_entity on public.attachments(entity_kind, entity_id);
create index if not exists idx_attachments_project on public.attachments(project_id);
alter table public.attachments enable row level security;
drop policy if exists attachments_select on public.attachments;
drop policy if exists attachments_insert on public.attachments;
drop policy if exists attachments_delete on public.attachments;
create policy attachments_select on public.attachments
for select to authenticated using (true);
create policy attachments_insert on public.attachments
for insert to authenticated with check (uploader_id = auth.uid());
create policy attachments_delete on public.attachments
for delete to authenticated
using (
uploader_id = auth.uid()
or exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
);
-- Bucket de stockage des fichiers joints.
insert into storage.buckets (id, name, public)
values ('attachments', 'attachments', true)
on conflict (id) do nothing;
drop policy if exists attachments_read on storage.objects;
drop policy if exists attachments_write on storage.objects;
drop policy if exists attachments_remove on storage.objects;
create policy attachments_read on storage.objects
for select using (bucket_id = 'attachments');
create policy attachments_write on storage.objects
for insert to authenticated with check (bucket_id = 'attachments');
create policy attachments_remove on storage.objects
for delete to authenticated using (bucket_id = 'attachments');
-- ---------------------------------------------------------------------
-- 4) Notifications : sur QUOI pointe la notification
-- (une notif de commentaire ouvre la tâche ou le bug commenté)
-- ---------------------------------------------------------------------
alter table public.notifications
add column if not exists entity_kind text not null default 'task';
update public.notifications
set entity_kind = kind
where kind in ('task', 'bug') and entity_kind is distinct from kind;
create index if not exists idx_notifications_user_unread
on public.notifications(user_id, read, created_at desc);
-- ---------------------------------------------------------------------
-- 5) Réordonnancement du Kanban en UNE requête
-- Avant : une requête UPDATE par carte de la colonne (jusqu'à 30
-- allers-retours non transactionnels — un échec corrompait l'ordre).
-- ---------------------------------------------------------------------
create or replace function public.reorder_tasks(p_status task_status, p_ids uuid[])
returns void
language plpgsql
security invoker -- la RLS de l'appelant s'applique normalement
set search_path = public
as $$
begin
update public.tasks t
set status = p_status,
position = ordered.idx
from (
select id, (ord - 1)::double precision as idx
from unnest(p_ids) with ordinality as u(id, ord)
) ordered
where t.id = ordered.id;
end $$;
grant execute on function public.reorder_tasks(task_status, uuid[]) to authenticated;
-- ---------------------------------------------------------------------
-- 6) Suppression d'un projet : créateur ou administrateur seulement
-- (avant : n'importe quel membre pouvait effacer un projet entier
-- et tout son contenu en cascade)
-- ---------------------------------------------------------------------
drop policy if exists projects_all on public.projects;
drop policy if exists projects_select on public.projects;
drop policy if exists projects_insert on public.projects;
drop policy if exists projects_update on public.projects;
drop policy if exists projects_delete on public.projects;
create policy projects_select on public.projects
for select to authenticated using (true);
create policy projects_insert on public.projects
for insert to authenticated with check (true);
create policy projects_update on public.projects
for update to authenticated using (true) with check (true);
create policy projects_delete on public.projects
for delete to authenticated
using (
created_by = auth.uid()
or exists (select 1 from public.profiles p where p.id = auth.uid() and p.is_admin)
);
-- ---------------------------------------------------------------------
-- 7) Temps réel : projets, commentaires, pièces jointes
-- ---------------------------------------------------------------------
do $$ begin
alter publication supabase_realtime add table public.projects;
exception when duplicate_object then null; end $$;
do $$ begin
alter publication supabase_realtime add table public.comments;
exception when duplicate_object then null; end $$;
do $$ begin
alter publication supabase_realtime add table public.attachments;
exception when duplicate_object then null; end $$;
-- ---------------------------------------------------------------------
-- 8) Création de profil à l'inscription : couleur d'avatar variée
-- (avant, tout le monde héritait du même violet #7c5cff)
-- ---------------------------------------------------------------------
create or replace function public.handle_new_user()
returns trigger language plpgsql security definer set search_path = public as $$
declare
palette text[] := array['#7c5cff','#00d2b8','#ff5c8a','#ffa53c','#3ca8ff','#9bdb4d','#e15cff','#ff6b6b'];
r member_role;
begin
r := coalesce((new.raw_user_meta_data->>'role')::member_role, 'other');
insert into public.profiles (id, full_name, roles, avatar_color)
values (
new.id,
coalesce(new.raw_user_meta_data->>'full_name', split_part(new.email, '@', 1)),
array[r],
palette[1 + (abs(hashtext(new.id::text)) % array_length(palette, 1))]
)
on conflict (id) do nothing;
return new;
end $$;
-- ---------------------------------------------------------------------
-- 9) Suppression des colonnes « de compatibilité »
-- Elles doublonnaient les tableaux et devaient être re-synchronisées
-- à la main dans six endroits du code. On sauvegarde d'abord, puis
-- on supprime. À exécuter EN DERNIER (la fonction du point 8 ne les
-- utilise plus).
-- ---------------------------------------------------------------------
-- a) profiles.role → profiles.roles[1]
do $$ begin
if exists (select 1 from information_schema.columns
where table_schema = 'public' and table_name = 'profiles' and column_name = 'role') then
update public.profiles set roles = array[role] where roles = '{}' and role is not null;
alter table public.profiles drop column role;
end if;
end $$;
-- b) tasks.pole → tasks.poles[1]
do $$ begin
if exists (select 1 from information_schema.columns
where table_schema = 'public' and table_name = 'tasks' and column_name = 'pole') then
update public.tasks set poles = array[pole] where pole is not null and poles = '{}';
alter table public.tasks drop column pole;
end if;
end $$;
-- c) bugs.assignee_id → bugs.assignee_ids[1] · bugs.pole → bugs.poles[1]
do $$ begin
if exists (select 1 from information_schema.columns
where table_schema = 'public' and table_name = 'bugs' and column_name = 'assignee_id') then
update public.bugs set assignee_ids = array[assignee_id]
where assignee_id is not null and assignee_ids = '{}';
alter table public.bugs drop column assignee_id;
end if;
if exists (select 1 from information_schema.columns
where table_schema = 'public' and table_name = 'bugs' and column_name = 'pole') then
alter table public.bugs drop column pole;
end if;
end $$;
-- d) bugs.priority : plus exposée dans l'interface (la sévérité suffit).
-- La colonne reste en base, avec une valeur par défaut, pour ne rien
-- perdre — mais plus personne ne la remplit.
do $$ begin
if exists (select 1 from information_schema.columns
where table_schema = 'public' and table_name = 'bugs' and column_name = 'priority') then
alter table public.bugs alter column priority set default 'medium';
end if;
end $$;
-- ---------------------------------------------------------------------
-- 10) Index manquants sur les colonnes les plus filtrées
-- ---------------------------------------------------------------------
create index if not exists idx_tasks_project_status on public.tasks(project_id, status, position);
create index if not exists idx_bugs_project_status on public.bugs(project_id, status);
create index if not exists idx_time_logs_date on public.time_logs(project_id, logged_at);
-- ---------------------------------------------------------------------
-- 11) Temps passé sur un BUG (parité avec les tâches)
-- Une entrée de temps porte sur une tâche, OU sur un bug, OU sur
-- rien (activité libre : réunion, veille…). Jamais les deux.
-- ---------------------------------------------------------------------
alter table public.time_logs
add column if not exists bug_id uuid references public.bugs(id) on delete cascade;
create index if not exists idx_time_logs_bug on public.time_logs(bug_id);
do $$ begin
alter table public.time_logs
add constraint time_logs_one_target
check (not (task_id is not null and bug_id is not null));
exception when duplicate_object then null; end $$;
-- ---------------------------------------------------------------------
-- Dernière ligne : forcer l'API à relire le schéma.
-- Sans ça, PostgREST peut continuer à servir l'ancienne structure et
-- renvoyer « Could not find the 'xxx' column ... in the schema cache »
-- pendant plusieurs minutes après la migration.
-- ---------------------------------------------------------------------
notify pgrst, 'reload schema';
-- =====================================================================
-- v11 — Documentation (fiches créées ou importées)
-- =====================================================================
create table if not exists public.documents (
id uuid primary key default gen_random_uuid(),
project_id uuid not null references public.projects(id) on delete cascade,
title text not null,
-- Contenu en Markdown : c'est le format pivot. Un .docx importé est
-- converti en Markdown à l'import, ce qui garde les fiches lisibles,
-- cherchables et modifiables dans l'application.
content text not null default '',
tags text[] not null default '{}',
-- Fichier d'origine, quand la fiche vient d'un import (informatif).
source_name text,
created_by uuid references public.profiles(id) on delete set null,
updated_by uuid references public.profiles(id) on delete set null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists idx_documents_project on public.documents(project_id, updated_at desc);
drop trigger if exists trg_documents_touch on public.documents;
create trigger trg_documents_touch before update on public.documents
for each row execute function public.touch_updated_at();
alter table public.documents enable row level security;
drop policy if exists documents_all on public.documents;
-- Équipe de confiance : tout membre connecté lit et écrit la documentation.
create policy documents_all on public.documents
for all to authenticated using (true) with check (true);
do $$ begin
alter publication supabase_realtime add table public.documents;
exception when duplicate_object then null; end $$;
notify pgrst, 'reload schema';