Initial commit v1.1
This commit is contained in:
@@ -1,159 +0,0 @@
|
|||||||
-- ============================================================
|
|
||||||
-- MulliganMates — Initial Schema
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
-- Profiles (extends auth.users)
|
|
||||||
create table public.profiles (
|
|
||||||
id uuid primary key references auth.users on delete cascade,
|
|
||||||
display_name text not null,
|
|
||||||
avatar_url text,
|
|
||||||
handicap_index numeric(4,1) check (handicap_index between -10 and 54),
|
|
||||||
created_at timestamptz not null default now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Automatically create a profile when a new user is confirmed
|
|
||||||
create or replace function public.handle_new_user()
|
|
||||||
returns trigger language plpgsql security definer set search_path = public
|
|
||||||
as $$
|
|
||||||
begin
|
|
||||||
insert into public.profiles (id, display_name)
|
|
||||||
values (new.id, coalesce(new.raw_user_meta_data->>'display_name', split_part(new.email, '@', 1)));
|
|
||||||
return new;
|
|
||||||
end;
|
|
||||||
$$;
|
|
||||||
|
|
||||||
create trigger on_auth_user_created
|
|
||||||
after insert on auth.users
|
|
||||||
for each row execute procedure public.handle_new_user();
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Courses
|
|
||||||
-- ============================================================
|
|
||||||
create table public.courses (
|
|
||||||
id uuid primary key default gen_random_uuid(),
|
|
||||||
name text not null,
|
|
||||||
par int not null check (par between 27 and 90),
|
|
||||||
created_by uuid not null references public.profiles on delete set null,
|
|
||||||
created_at timestamptz not null default now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tees per course
|
|
||||||
create table public.tees (
|
|
||||||
id uuid primary key default gen_random_uuid(),
|
|
||||||
course_id uuid not null references public.courses on delete cascade,
|
|
||||||
name text not null, -- e.g. "Yellow", "White", "Blue"
|
|
||||||
color text not null default '#888888',
|
|
||||||
course_rating numeric(4,1) not null,
|
|
||||||
slope_rating int not null check (slope_rating between 55 and 155),
|
|
||||||
created_at timestamptz not null default now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Holes (shared across tees on the same course)
|
|
||||||
create table public.holes (
|
|
||||||
id uuid primary key default gen_random_uuid(),
|
|
||||||
course_id uuid not null references public.courses on delete cascade,
|
|
||||||
hole_number int not null check (hole_number between 1 and 18),
|
|
||||||
par int not null check (par between 3 and 5),
|
|
||||||
stroke_index int not null check (stroke_index between 1 and 18),
|
|
||||||
unique (course_id, hole_number)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Validate that sum of hole pars equals the course par
|
|
||||||
create or replace function public.check_course_par()
|
|
||||||
returns trigger language plpgsql as $$
|
|
||||||
declare
|
|
||||||
total_par int;
|
|
||||||
course_par int;
|
|
||||||
hole_count int;
|
|
||||||
begin
|
|
||||||
select count(*), sum(h.par)
|
|
||||||
into hole_count, total_par
|
|
||||||
from public.holes h
|
|
||||||
where h.course_id = new.course_id;
|
|
||||||
|
|
||||||
select c.par into course_par
|
|
||||||
from public.courses c
|
|
||||||
where c.id = new.course_id;
|
|
||||||
|
|
||||||
-- Only enforce once all holes are entered (9 or 18)
|
|
||||||
if hole_count in (9, 18) then
|
|
||||||
if total_par <> course_par then
|
|
||||||
raise exception 'Sum of hole pars (%) does not equal course par (%)',
|
|
||||||
total_par, course_par;
|
|
||||||
end if;
|
|
||||||
end if;
|
|
||||||
|
|
||||||
return new;
|
|
||||||
end;
|
|
||||||
$$;
|
|
||||||
|
|
||||||
create trigger enforce_course_par
|
|
||||||
after insert or update on public.holes
|
|
||||||
for each row execute procedure public.check_course_par();
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Rounds
|
|
||||||
-- ============================================================
|
|
||||||
create type public.round_status as enum ('lobby', 'active', 'completed');
|
|
||||||
|
|
||||||
create table public.rounds (
|
|
||||||
id uuid primary key default gen_random_uuid(),
|
|
||||||
course_id uuid not null references public.courses,
|
|
||||||
tee_id uuid not null references public.tees,
|
|
||||||
name text,
|
|
||||||
date date not null default current_date,
|
|
||||||
holes_count int not null check (holes_count in (9, 18)),
|
|
||||||
status public.round_status not null default 'lobby',
|
|
||||||
created_by uuid not null references public.profiles,
|
|
||||||
created_at timestamptz not null default now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Players in a round
|
|
||||||
create table public.round_players (
|
|
||||||
id uuid primary key default gen_random_uuid(),
|
|
||||||
round_id uuid not null references public.rounds on delete cascade,
|
|
||||||
user_id uuid not null references public.profiles on delete cascade,
|
|
||||||
role text not null default 'player' check (role in ('admin', 'player')),
|
|
||||||
handicap_index numeric(4,1),
|
|
||||||
course_handicap int,
|
|
||||||
joined_at timestamptz not null default now(),
|
|
||||||
unique (round_id, user_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Round invitations
|
|
||||||
create table public.round_invites (
|
|
||||||
id uuid primary key default gen_random_uuid(),
|
|
||||||
round_id uuid not null references public.rounds on delete cascade,
|
|
||||||
email text not null,
|
|
||||||
invited_by uuid not null references public.profiles,
|
|
||||||
accepted_at timestamptz,
|
|
||||||
expires_at timestamptz not null default (now() + interval '7 days'),
|
|
||||||
created_at timestamptz not null default now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Scores
|
|
||||||
-- ============================================================
|
|
||||||
create table public.scores (
|
|
||||||
id uuid primary key default gen_random_uuid(),
|
|
||||||
round_id uuid not null references public.rounds on delete cascade,
|
|
||||||
hole_id uuid not null references public.holes on delete cascade,
|
|
||||||
player_id uuid not null references public.profiles on delete cascade,
|
|
||||||
strokes int not null check (strokes > 0),
|
|
||||||
updated_by uuid not null references public.profiles,
|
|
||||||
updated_at timestamptz not null default now(),
|
|
||||||
unique (round_id, hole_id, player_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Update updated_at on score change
|
|
||||||
create or replace function public.set_score_updated_at()
|
|
||||||
returns trigger language plpgsql as $$
|
|
||||||
begin
|
|
||||||
new.updated_at = now();
|
|
||||||
return new;
|
|
||||||
end;
|
|
||||||
$$;
|
|
||||||
|
|
||||||
create trigger scores_updated_at
|
|
||||||
before update on public.scores
|
|
||||||
for each row execute procedure public.set_score_updated_at();
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
-- ============================================================
|
|
||||||
-- MulliganMates — Row Level Security Policies
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
alter table public.profiles enable row level security;
|
|
||||||
alter table public.courses enable row level security;
|
|
||||||
alter table public.tees enable row level security;
|
|
||||||
alter table public.holes enable row level security;
|
|
||||||
alter table public.rounds enable row level security;
|
|
||||||
alter table public.round_players enable row level security;
|
|
||||||
alter table public.round_invites enable row level security;
|
|
||||||
alter table public.scores enable row level security;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Helper: is the current user a member of the given round?
|
|
||||||
-- ============================================================
|
|
||||||
create or replace function public.is_round_member(p_round_id uuid)
|
|
||||||
returns boolean language sql security definer stable as $$
|
|
||||||
select exists (
|
|
||||||
select 1 from public.round_players
|
|
||||||
where round_id = p_round_id
|
|
||||||
and user_id = auth.uid()
|
|
||||||
)
|
|
||||||
$$;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Profiles
|
|
||||||
-- ============================================================
|
|
||||||
create policy "Anyone can read profiles"
|
|
||||||
on public.profiles for select using (true);
|
|
||||||
|
|
||||||
create policy "Users can update their own profile"
|
|
||||||
on public.profiles for update using (id = auth.uid());
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Courses
|
|
||||||
-- ============================================================
|
|
||||||
create policy "Authenticated users can read courses"
|
|
||||||
on public.courses for select using (auth.role() = 'authenticated');
|
|
||||||
|
|
||||||
create policy "Authenticated users can create courses"
|
|
||||||
on public.courses for insert with check (auth.role() = 'authenticated');
|
|
||||||
|
|
||||||
create policy "Course creator can update"
|
|
||||||
on public.courses for update using (created_by = auth.uid());
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Tees
|
|
||||||
-- ============================================================
|
|
||||||
create policy "Authenticated users can read tees"
|
|
||||||
on public.tees for select using (auth.role() = 'authenticated');
|
|
||||||
|
|
||||||
create policy "Authenticated users can manage tees on their courses"
|
|
||||||
on public.tees for all using (
|
|
||||||
exists (
|
|
||||||
select 1 from public.courses
|
|
||||||
where id = tees.course_id
|
|
||||||
and created_by = auth.uid()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Holes
|
|
||||||
-- ============================================================
|
|
||||||
create policy "Authenticated users can read holes"
|
|
||||||
on public.holes for select using (auth.role() = 'authenticated');
|
|
||||||
|
|
||||||
create policy "Course creator can manage holes"
|
|
||||||
on public.holes for all using (
|
|
||||||
exists (
|
|
||||||
select 1 from public.courses
|
|
||||||
where id = holes.course_id
|
|
||||||
and created_by = auth.uid()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Rounds
|
|
||||||
-- ============================================================
|
|
||||||
create policy "Round members can read round"
|
|
||||||
on public.rounds for select using (public.is_round_member(id));
|
|
||||||
|
|
||||||
create policy "Authenticated users can create rounds"
|
|
||||||
on public.rounds for insert with check (
|
|
||||||
auth.role() = 'authenticated' and created_by = auth.uid()
|
|
||||||
);
|
|
||||||
|
|
||||||
create policy "Round creator can update round"
|
|
||||||
on public.rounds for update using (created_by = auth.uid());
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Round Players
|
|
||||||
-- ============================================================
|
|
||||||
create policy "Round members can read player list"
|
|
||||||
on public.round_players for select using (public.is_round_member(round_id));
|
|
||||||
|
|
||||||
create policy "Round creator can add players"
|
|
||||||
on public.round_players for insert with check (
|
|
||||||
exists (
|
|
||||||
select 1 from public.rounds
|
|
||||||
where id = round_players.round_id
|
|
||||||
and created_by = auth.uid()
|
|
||||||
and status = 'lobby'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
create policy "Users can join a round via insert of own record"
|
|
||||||
on public.round_players for insert with check (user_id = auth.uid());
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Round Invites
|
|
||||||
-- ============================================================
|
|
||||||
create policy "Round members can read invites"
|
|
||||||
on public.round_invites for select using (public.is_round_member(round_id));
|
|
||||||
|
|
||||||
create policy "Round creator can send invites"
|
|
||||||
on public.round_invites for insert with check (
|
|
||||||
exists (
|
|
||||||
select 1 from public.rounds
|
|
||||||
where id = round_invites.round_id
|
|
||||||
and created_by = auth.uid()
|
|
||||||
and status = 'lobby'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
create policy "Invited user can mark invite accepted"
|
|
||||||
on public.round_invites for update using (
|
|
||||||
email = (select email from auth.users where id = auth.uid())
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Scores
|
|
||||||
-- ============================================================
|
|
||||||
create policy "Round members can read scores"
|
|
||||||
on public.scores for select using (public.is_round_member(round_id));
|
|
||||||
|
|
||||||
create policy "Round members can enter and edit scores"
|
|
||||||
on public.scores for insert with check (
|
|
||||||
public.is_round_member(round_id) and
|
|
||||||
exists (
|
|
||||||
select 1 from public.rounds
|
|
||||||
where id = scores.round_id
|
|
||||||
and status = 'active'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
create policy "Round members can update scores"
|
|
||||||
on public.scores for update using (
|
|
||||||
public.is_round_member(round_id) and
|
|
||||||
exists (
|
|
||||||
select 1 from public.rounds
|
|
||||||
where id = scores.round_id
|
|
||||||
and status = 'active'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
Reference in New Issue
Block a user