Add version files and update imports for trip model; enhance error handling
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (select 1 from pg_type where typname = 'organization_role') then
|
||||
create type public.organization_role as enum ('owner', 'admin', 'member');
|
||||
end if;
|
||||
if not exists (select 1 from pg_type where typname = 'channel_type') then
|
||||
create type public.channel_type as enum ('text', 'voice', 'announcement');
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
create table if not exists public.organizations (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null,
|
||||
slug text not null unique,
|
||||
owner_user_id uuid not null references auth.users(id) on delete restrict,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists public.organization_members (
|
||||
organization_id uuid not null references public.organizations(id) on delete cascade,
|
||||
user_id uuid not null references auth.users(id) on delete cascade,
|
||||
role public.organization_role not null default 'member',
|
||||
joined_at timestamptz not null default now(),
|
||||
primary key (organization_id, user_id)
|
||||
);
|
||||
|
||||
create table if not exists public.channels (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
organization_id uuid not null references public.organizations(id) on delete cascade,
|
||||
name text not null,
|
||||
slug text not null,
|
||||
type public.channel_type not null default 'text',
|
||||
position integer not null default 0,
|
||||
topic text,
|
||||
is_private boolean not null default false,
|
||||
created_by uuid not null references auth.users(id) on delete restrict,
|
||||
created_at timestamptz not null default now(),
|
||||
unique (organization_id, slug)
|
||||
);
|
||||
|
||||
create table if not exists public.channel_members (
|
||||
channel_id uuid not null references public.channels(id) on delete cascade,
|
||||
user_id uuid not null references auth.users(id) on delete cascade,
|
||||
joined_at timestamptz not null default now(),
|
||||
primary key (channel_id, user_id)
|
||||
);
|
||||
|
||||
create table if not exists public.messages (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
channel_id uuid not null references public.channels(id) on delete cascade,
|
||||
author_user_id uuid not null references auth.users(id) on delete restrict,
|
||||
content text not null check (char_length(content) <= 4000),
|
||||
created_at timestamptz not null default now(),
|
||||
edited_at timestamptz,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
|
||||
create index if not exists idx_org_members_user
|
||||
on public.organization_members(user_id);
|
||||
|
||||
create index if not exists idx_channels_org_position
|
||||
on public.channels(organization_id, position);
|
||||
|
||||
create index if not exists idx_messages_channel_created_at_desc
|
||||
on public.messages(channel_id, created_at desc);
|
||||
|
||||
create or replace function public.is_org_member(org_id uuid, uid uuid default auth.uid())
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select exists (
|
||||
select 1
|
||||
from public.organization_members om
|
||||
where om.organization_id = org_id
|
||||
and om.user_id = uid
|
||||
);
|
||||
$$;
|
||||
|
||||
create or replace function public.org_role(org_id uuid, uid uuid default auth.uid())
|
||||
returns public.organization_role
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select om.role
|
||||
from public.organization_members om
|
||||
where om.organization_id = org_id
|
||||
and om.user_id = uid
|
||||
limit 1;
|
||||
$$;
|
||||
|
||||
create or replace function public.can_access_channel(ch_id uuid, uid uuid default auth.uid())
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = ch_id
|
||||
and public.is_org_member(c.organization_id, uid)
|
||||
and (
|
||||
c.is_private = false
|
||||
or exists (
|
||||
select 1
|
||||
from public.channel_members cm
|
||||
where cm.channel_id = c.id
|
||||
and cm.user_id = uid
|
||||
)
|
||||
)
|
||||
);
|
||||
$$;
|
||||
|
||||
alter table public.organizations enable row level security;
|
||||
alter table public.organization_members enable row level security;
|
||||
alter table public.channels enable row level security;
|
||||
alter table public.channel_members enable row level security;
|
||||
alter table public.messages enable row level security;
|
||||
|
||||
drop policy if exists "organizations_select_members" on public.organizations;
|
||||
create policy "organizations_select_members"
|
||||
on public.organizations
|
||||
for select
|
||||
to authenticated
|
||||
using (public.is_org_member(id));
|
||||
|
||||
drop policy if exists "organizations_insert_owner" on public.organizations;
|
||||
create policy "organizations_insert_owner"
|
||||
on public.organizations
|
||||
for insert
|
||||
to authenticated
|
||||
with check (owner_user_id = auth.uid());
|
||||
|
||||
drop policy if exists "organizations_update_admins" on public.organizations;
|
||||
create policy "organizations_update_admins"
|
||||
on public.organizations
|
||||
for update
|
||||
to authenticated
|
||||
using (public.org_role(id) in ('owner', 'admin'))
|
||||
with check (public.org_role(id) in ('owner', 'admin'));
|
||||
|
||||
drop policy if exists "organization_members_select_members" on public.organization_members;
|
||||
create policy "organization_members_select_members"
|
||||
on public.organization_members
|
||||
for select
|
||||
to authenticated
|
||||
using (public.is_org_member(organization_id));
|
||||
|
||||
drop policy if exists "organization_members_insert_admins" on public.organization_members;
|
||||
create policy "organization_members_insert_admins"
|
||||
on public.organization_members
|
||||
for insert
|
||||
to authenticated
|
||||
with check (public.org_role(organization_id) in ('owner', 'admin'));
|
||||
|
||||
drop policy if exists "organization_members_update_admins" on public.organization_members;
|
||||
create policy "organization_members_update_admins"
|
||||
on public.organization_members
|
||||
for update
|
||||
to authenticated
|
||||
using (public.org_role(organization_id) in ('owner', 'admin'))
|
||||
with check (public.org_role(organization_id) in ('owner', 'admin'));
|
||||
|
||||
drop policy if exists "organization_members_delete_admins" on public.organization_members;
|
||||
create policy "organization_members_delete_admins"
|
||||
on public.organization_members
|
||||
for delete
|
||||
to authenticated
|
||||
using (public.org_role(organization_id) in ('owner', 'admin'));
|
||||
|
||||
drop policy if exists "channels_select_visible" on public.channels;
|
||||
create policy "channels_select_visible"
|
||||
on public.channels
|
||||
for select
|
||||
to authenticated
|
||||
using (public.can_access_channel(id));
|
||||
|
||||
drop policy if exists "channels_insert_admins" on public.channels;
|
||||
create policy "channels_insert_admins"
|
||||
on public.channels
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
public.org_role(organization_id) in ('owner', 'admin')
|
||||
and created_by = auth.uid()
|
||||
);
|
||||
|
||||
drop policy if exists "channels_update_admins" on public.channels;
|
||||
create policy "channels_update_admins"
|
||||
on public.channels
|
||||
for update
|
||||
to authenticated
|
||||
using (public.org_role(organization_id) in ('owner', 'admin'))
|
||||
with check (public.org_role(organization_id) in ('owner', 'admin'));
|
||||
|
||||
drop policy if exists "channels_delete_admins" on public.channels;
|
||||
create policy "channels_delete_admins"
|
||||
on public.channels
|
||||
for delete
|
||||
to authenticated
|
||||
using (public.org_role(organization_id) in ('owner', 'admin'));
|
||||
|
||||
drop policy if exists "channel_members_select_visible" on public.channel_members;
|
||||
create policy "channel_members_select_visible"
|
||||
on public.channel_members
|
||||
for select
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and public.is_org_member(c.organization_id)
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "channel_members_insert_admins" on public.channel_members;
|
||||
create policy "channel_members_insert_admins"
|
||||
on public.channel_members
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "channel_members_delete_admins" on public.channel_members;
|
||||
create policy "channel_members_delete_admins"
|
||||
on public.channel_members
|
||||
for delete
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "messages_select_visible_channel" on public.messages;
|
||||
create policy "messages_select_visible_channel"
|
||||
on public.messages
|
||||
for select
|
||||
to authenticated
|
||||
using (public.can_access_channel(channel_id));
|
||||
|
||||
drop policy if exists "messages_insert_visible_channel" on public.messages;
|
||||
create policy "messages_insert_visible_channel"
|
||||
on public.messages
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
public.can_access_channel(channel_id)
|
||||
and author_user_id = auth.uid()
|
||||
and deleted_at is null
|
||||
);
|
||||
|
||||
drop policy if exists "messages_update_author_or_admin" on public.messages;
|
||||
create policy "messages_update_author_or_admin"
|
||||
on public.messages
|
||||
for update
|
||||
to authenticated
|
||||
using (
|
||||
author_user_id = auth.uid()
|
||||
or exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
)
|
||||
with check (
|
||||
author_user_id = auth.uid()
|
||||
or exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "messages_delete_author_or_admin" on public.messages;
|
||||
create policy "messages_delete_author_or_admin"
|
||||
on public.messages
|
||||
for delete
|
||||
to authenticated
|
||||
using (
|
||||
author_user_id = auth.uid()
|
||||
or exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
-- Fix collaboration bootstrap RLS flow:
|
||||
-- 1) Allow authenticated users to create organizations they own.
|
||||
-- 2) Allow org owner to insert their initial owner membership row.
|
||||
|
||||
drop policy if exists "organizations_insert_owner" on public.organizations;
|
||||
create policy "organizations_insert_owner"
|
||||
on public.organizations
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
owner_user_id = auth.uid()
|
||||
and owner_user_id is not null
|
||||
);
|
||||
|
||||
drop policy if exists "organization_members_insert_admins" on public.organization_members;
|
||||
create policy "organization_members_insert_admins"
|
||||
on public.organization_members
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
user_id = auth.uid()
|
||||
and (
|
||||
public.org_role(organization_id) in ('owner', 'admin')
|
||||
or exists (
|
||||
select 1
|
||||
from public.organizations o
|
||||
where o.id = organization_id
|
||||
and o.owner_user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
-- Switch channel types to text|voice|operations.
|
||||
-- Existing "announcement" rows are mapped to "operations".
|
||||
|
||||
do $$
|
||||
begin
|
||||
if exists (select 1 from pg_type where typname = 'channel_type') then
|
||||
alter table public.channels
|
||||
alter column type drop default;
|
||||
|
||||
alter table public.channels
|
||||
alter column type type text
|
||||
using type::text;
|
||||
|
||||
drop type public.channel_type;
|
||||
create type public.channel_type as enum ('text', 'voice', 'operations');
|
||||
|
||||
alter table public.channels
|
||||
alter column type type public.channel_type
|
||||
using (
|
||||
case
|
||||
when type = 'announcement' then 'operations'
|
||||
when type in ('text', 'voice', 'operations') then type
|
||||
else 'text'
|
||||
end
|
||||
)::public.channel_type;
|
||||
|
||||
alter table public.channels
|
||||
alter column type set default 'text'::public.channel_type;
|
||||
end if;
|
||||
end $$;
|
||||
@@ -0,0 +1,322 @@
|
||||
-- Fresh reset migration: replace collaboration IDs with lowercase hash-like text IDs.
|
||||
-- No compatibility shims by design.
|
||||
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
-- Drop existing collaboration objects.
|
||||
drop table if exists public.messages cascade;
|
||||
drop table if exists public.channel_members cascade;
|
||||
drop table if exists public.channels cascade;
|
||||
drop table if exists public.organization_members cascade;
|
||||
drop table if exists public.organizations cascade;
|
||||
|
||||
drop function if exists public.can_access_channel(uuid, uuid);
|
||||
drop function if exists public.org_role(uuid, uuid);
|
||||
drop function if exists public.is_org_member(uuid, uuid);
|
||||
drop function if exists public.can_access_channel(text, uuid);
|
||||
drop function if exists public.org_role(text, uuid);
|
||||
drop function if exists public.is_org_member(text, uuid);
|
||||
|
||||
-- Keep enum types if already present.
|
||||
do $$
|
||||
begin
|
||||
if not exists (select 1 from pg_type where typname = 'organization_role') then
|
||||
create type public.organization_role as enum ('owner', 'admin', 'member');
|
||||
end if;
|
||||
if not exists (select 1 from pg_type where typname = 'channel_type') then
|
||||
create type public.channel_type as enum ('text', 'voice', 'announcement');
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
create or replace function public.gen_hash_id()
|
||||
returns text
|
||||
language sql
|
||||
volatile
|
||||
as $$
|
||||
select substring(md5(random()::text || clock_timestamp()::text) from 1 for 16);
|
||||
$$;
|
||||
|
||||
create table public.organizations (
|
||||
id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'),
|
||||
name text not null,
|
||||
slug text not null unique,
|
||||
owner_user_id uuid not null references auth.users(id) on delete restrict,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table public.organization_members (
|
||||
organization_id text not null references public.organizations(id) on delete cascade,
|
||||
user_id uuid not null references auth.users(id) on delete cascade,
|
||||
role public.organization_role not null default 'member',
|
||||
joined_at timestamptz not null default now(),
|
||||
primary key (organization_id, user_id)
|
||||
);
|
||||
|
||||
create table public.channels (
|
||||
id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'),
|
||||
organization_id text not null references public.organizations(id) on delete cascade,
|
||||
name text not null,
|
||||
slug text not null,
|
||||
type public.channel_type not null default 'text',
|
||||
position integer not null default 0,
|
||||
topic text,
|
||||
is_private boolean not null default false,
|
||||
created_by uuid not null references auth.users(id) on delete restrict,
|
||||
created_at timestamptz not null default now(),
|
||||
unique (organization_id, slug)
|
||||
);
|
||||
|
||||
create table public.channel_members (
|
||||
channel_id text not null references public.channels(id) on delete cascade,
|
||||
user_id uuid not null references auth.users(id) on delete cascade,
|
||||
joined_at timestamptz not null default now(),
|
||||
primary key (channel_id, user_id)
|
||||
);
|
||||
|
||||
create table public.messages (
|
||||
id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'),
|
||||
channel_id text not null references public.channels(id) on delete cascade,
|
||||
author_user_id uuid not null references auth.users(id) on delete restrict,
|
||||
content text not null check (char_length(content) <= 4000),
|
||||
created_at timestamptz not null default now(),
|
||||
edited_at timestamptz,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
|
||||
create index idx_org_members_user on public.organization_members(user_id);
|
||||
create index idx_channels_org_position on public.channels(organization_id, position);
|
||||
create index idx_messages_channel_created_at_desc on public.messages(channel_id, created_at desc);
|
||||
|
||||
create or replace function public.is_org_member(org_id text, uid uuid default auth.uid())
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select exists (
|
||||
select 1
|
||||
from public.organization_members om
|
||||
where om.organization_id = org_id
|
||||
and om.user_id = uid
|
||||
);
|
||||
$$;
|
||||
|
||||
create or replace function public.org_role(org_id text, uid uuid default auth.uid())
|
||||
returns public.organization_role
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select om.role
|
||||
from public.organization_members om
|
||||
where om.organization_id = org_id
|
||||
and om.user_id = uid
|
||||
limit 1;
|
||||
$$;
|
||||
|
||||
create or replace function public.can_access_channel(ch_id text, uid uuid default auth.uid())
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = ch_id
|
||||
and public.is_org_member(c.organization_id, uid)
|
||||
and (
|
||||
c.is_private = false
|
||||
or exists (
|
||||
select 1
|
||||
from public.channel_members cm
|
||||
where cm.channel_id = c.id
|
||||
and cm.user_id = uid
|
||||
)
|
||||
)
|
||||
);
|
||||
$$;
|
||||
|
||||
alter table public.organizations enable row level security;
|
||||
alter table public.organization_members enable row level security;
|
||||
alter table public.channels enable row level security;
|
||||
alter table public.channel_members enable row level security;
|
||||
alter table public.messages enable row level security;
|
||||
|
||||
create policy "organizations_select_members"
|
||||
on public.organizations
|
||||
for select
|
||||
to authenticated
|
||||
using (public.is_org_member(id));
|
||||
|
||||
create policy "organizations_insert_owner"
|
||||
on public.organizations
|
||||
for insert
|
||||
to authenticated
|
||||
with check (owner_user_id = auth.uid());
|
||||
|
||||
create policy "organizations_update_admins"
|
||||
on public.organizations
|
||||
for update
|
||||
to authenticated
|
||||
using (public.org_role(id) in ('owner', 'admin'))
|
||||
with check (public.org_role(id) in ('owner', 'admin'));
|
||||
|
||||
create policy "organization_members_select_members"
|
||||
on public.organization_members
|
||||
for select
|
||||
to authenticated
|
||||
using (public.is_org_member(organization_id));
|
||||
|
||||
create policy "organization_members_insert_admins"
|
||||
on public.organization_members
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
user_id = auth.uid()
|
||||
and (
|
||||
public.org_role(organization_id) in ('owner', 'admin')
|
||||
or exists (
|
||||
select 1
|
||||
from public.organizations o
|
||||
where o.id = organization_id
|
||||
and o.owner_user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
create policy "organization_members_update_admins"
|
||||
on public.organization_members
|
||||
for update
|
||||
to authenticated
|
||||
using (public.org_role(organization_id) in ('owner', 'admin'))
|
||||
with check (public.org_role(organization_id) in ('owner', 'admin'));
|
||||
|
||||
create policy "organization_members_delete_admins"
|
||||
on public.organization_members
|
||||
for delete
|
||||
to authenticated
|
||||
using (public.org_role(organization_id) in ('owner', 'admin'));
|
||||
|
||||
create policy "channels_select_visible"
|
||||
on public.channels
|
||||
for select
|
||||
to authenticated
|
||||
using (public.can_access_channel(id));
|
||||
|
||||
create policy "channels_insert_admins"
|
||||
on public.channels
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
public.org_role(organization_id) in ('owner', 'admin')
|
||||
and created_by = auth.uid()
|
||||
);
|
||||
|
||||
create policy "channels_update_admins"
|
||||
on public.channels
|
||||
for update
|
||||
to authenticated
|
||||
using (public.org_role(organization_id) in ('owner', 'admin'))
|
||||
with check (public.org_role(organization_id) in ('owner', 'admin'));
|
||||
|
||||
create policy "channels_delete_admins"
|
||||
on public.channels
|
||||
for delete
|
||||
to authenticated
|
||||
using (public.org_role(organization_id) in ('owner', 'admin'));
|
||||
|
||||
create policy "channel_members_select_visible"
|
||||
on public.channel_members
|
||||
for select
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and public.is_org_member(c.organization_id)
|
||||
)
|
||||
);
|
||||
|
||||
create policy "channel_members_insert_admins"
|
||||
on public.channel_members
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
create policy "channel_members_delete_admins"
|
||||
on public.channel_members
|
||||
for delete
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
create policy "messages_select_visible_channel"
|
||||
on public.messages
|
||||
for select
|
||||
to authenticated
|
||||
using (public.can_access_channel(channel_id));
|
||||
|
||||
create policy "messages_insert_visible_channel"
|
||||
on public.messages
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
public.can_access_channel(channel_id)
|
||||
and author_user_id = auth.uid()
|
||||
and deleted_at is null
|
||||
);
|
||||
|
||||
create policy "messages_update_author_or_admin"
|
||||
on public.messages
|
||||
for update
|
||||
to authenticated
|
||||
using (
|
||||
author_user_id = auth.uid()
|
||||
or exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
)
|
||||
with check (
|
||||
author_user_id = auth.uid()
|
||||
or exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
create policy "messages_delete_author_or_admin"
|
||||
on public.messages
|
||||
for delete
|
||||
to authenticated
|
||||
using (
|
||||
author_user_id = auth.uid()
|
||||
or exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,39 @@
|
||||
create table if not exists public.organization_invites (
|
||||
id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'),
|
||||
token text not null unique check (token ~ '^[0-9a-f]{24}$'),
|
||||
organization_id text not null references public.organizations(id) on delete cascade,
|
||||
created_by uuid not null references auth.users(id) on delete restrict,
|
||||
role public.organization_role not null default 'member',
|
||||
max_uses integer not null default 1 check (max_uses > 0),
|
||||
uses_count integer not null default 0 check (uses_count >= 0),
|
||||
expires_at timestamptz,
|
||||
revoked boolean not null default false,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_org_invites_org on public.organization_invites(organization_id);
|
||||
create index if not exists idx_org_invites_token on public.organization_invites(token);
|
||||
|
||||
alter table public.organization_invites enable row level security;
|
||||
|
||||
drop policy if exists "organization_invites_select_admins" on public.organization_invites;
|
||||
create policy "organization_invites_select_admins"
|
||||
on public.organization_invites
|
||||
for select
|
||||
to authenticated
|
||||
using (public.org_role(organization_id) in ('owner', 'admin'));
|
||||
|
||||
drop policy if exists "organization_invites_insert_admins" on public.organization_invites;
|
||||
create policy "organization_invites_insert_admins"
|
||||
on public.organization_invites
|
||||
for insert
|
||||
to authenticated
|
||||
with check (public.org_role(organization_id) in ('owner', 'admin'));
|
||||
|
||||
drop policy if exists "organization_invites_update_admins" on public.organization_invites;
|
||||
create policy "organization_invites_update_admins"
|
||||
on public.organization_invites
|
||||
for update
|
||||
to authenticated
|
||||
using (public.org_role(organization_id) in ('owner', 'admin'))
|
||||
with check (public.org_role(organization_id) in ('owner', 'admin'));
|
||||
@@ -0,0 +1,42 @@
|
||||
-- Ensure collaboration tables are included in Supabase Realtime publication.
|
||||
|
||||
do $$
|
||||
begin
|
||||
begin
|
||||
alter publication supabase_realtime add table public.organizations;
|
||||
exception
|
||||
when duplicate_object then null;
|
||||
when undefined_object then null;
|
||||
end;
|
||||
|
||||
begin
|
||||
alter publication supabase_realtime add table public.organization_members;
|
||||
exception
|
||||
when duplicate_object then null;
|
||||
when undefined_object then null;
|
||||
end;
|
||||
|
||||
begin
|
||||
alter publication supabase_realtime add table public.channels;
|
||||
exception
|
||||
when duplicate_object then null;
|
||||
when undefined_object then null;
|
||||
end;
|
||||
|
||||
begin
|
||||
alter publication supabase_realtime add table public.channel_members;
|
||||
exception
|
||||
when duplicate_object then null;
|
||||
when undefined_object then null;
|
||||
end;
|
||||
|
||||
begin
|
||||
alter publication supabase_realtime add table public.messages;
|
||||
exception
|
||||
when duplicate_object then null;
|
||||
when undefined_object then null;
|
||||
end;
|
||||
end $$;
|
||||
|
||||
-- Helpful for update/delete realtime payload completeness.
|
||||
alter table public.messages replica identity full;
|
||||
@@ -0,0 +1,341 @@
|
||||
-- Operations channel data model (no service-days table):
|
||||
-- schedules -> trips -> trip_stops -> stop_updates
|
||||
|
||||
create table if not exists public.operations_schedules (
|
||||
id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'),
|
||||
channel_id text not null references public.channels(id) on delete cascade,
|
||||
version integer not null check (version > 0),
|
||||
source_file_name text not null,
|
||||
source_mime text,
|
||||
storage_path text,
|
||||
file_sha256 text,
|
||||
parser text not null default 'unknown',
|
||||
parse_status text not null default 'pending',
|
||||
parse_error text,
|
||||
uploaded_by uuid not null references auth.users(id) on delete restrict,
|
||||
uploaded_at timestamptz not null default now(),
|
||||
parsed_at timestamptz,
|
||||
is_active boolean not null default false,
|
||||
unique (channel_id, version)
|
||||
);
|
||||
|
||||
create unique index if not exists idx_operations_schedules_active_per_channel
|
||||
on public.operations_schedules(channel_id)
|
||||
where is_active = true;
|
||||
|
||||
create index if not exists idx_operations_schedules_channel_uploaded
|
||||
on public.operations_schedules(channel_id, uploaded_at desc);
|
||||
|
||||
create table if not exists public.operations_trips (
|
||||
id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'),
|
||||
schedule_id text not null references public.operations_schedules(id) on delete cascade,
|
||||
trip_number text not null,
|
||||
duty_number text,
|
||||
running_number text,
|
||||
direction text,
|
||||
service_code text,
|
||||
sort_order integer not null default 0,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists idx_operations_trips_schedule_sort
|
||||
on public.operations_trips(schedule_id, sort_order, trip_number);
|
||||
|
||||
create table if not exists public.operations_trip_stops (
|
||||
id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'),
|
||||
trip_id text not null references public.operations_trips(id) on delete cascade,
|
||||
stop_sequence integer not null check (stop_sequence > 0),
|
||||
stop_name text not null,
|
||||
stop_code text,
|
||||
scheduled_time text,
|
||||
is_timing_point boolean not null default false,
|
||||
raw_label text,
|
||||
created_at timestamptz not null default now(),
|
||||
unique (trip_id, stop_sequence)
|
||||
);
|
||||
|
||||
create index if not exists idx_operations_trip_stops_trip_sequence
|
||||
on public.operations_trip_stops(trip_id, stop_sequence);
|
||||
|
||||
create index if not exists idx_operations_trip_stops_trip_time
|
||||
on public.operations_trip_stops(trip_id, scheduled_time);
|
||||
|
||||
create table if not exists public.operations_stop_updates (
|
||||
id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'),
|
||||
trip_stop_id text not null references public.operations_trip_stops(id) on delete cascade,
|
||||
status text,
|
||||
actual_time text,
|
||||
vehicle_id text,
|
||||
notes text,
|
||||
updated_by uuid not null references auth.users(id) on delete restrict,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (trip_stop_id)
|
||||
);
|
||||
|
||||
create index if not exists idx_operations_stop_updates_trip_stop
|
||||
on public.operations_stop_updates(trip_stop_id);
|
||||
|
||||
alter table public.operations_schedules enable row level security;
|
||||
alter table public.operations_trips enable row level security;
|
||||
alter table public.operations_trip_stops enable row level security;
|
||||
alter table public.operations_stop_updates enable row level security;
|
||||
|
||||
drop policy if exists "operations_schedules_select_visible" on public.operations_schedules;
|
||||
create policy "operations_schedules_select_visible"
|
||||
on public.operations_schedules
|
||||
for select
|
||||
to authenticated
|
||||
using (public.can_access_channel(channel_id));
|
||||
|
||||
drop policy if exists "operations_schedules_insert_admins" on public.operations_schedules;
|
||||
create policy "operations_schedules_insert_admins"
|
||||
on public.operations_schedules
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
uploaded_by = auth.uid()
|
||||
and exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and c.type = 'operations'
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "operations_schedules_update_admins" on public.operations_schedules;
|
||||
create policy "operations_schedules_update_admins"
|
||||
on public.operations_schedules
|
||||
for update
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and c.type = 'operations'
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
)
|
||||
with check (
|
||||
exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and c.type = 'operations'
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "operations_schedules_delete_admins" on public.operations_schedules;
|
||||
create policy "operations_schedules_delete_admins"
|
||||
on public.operations_schedules
|
||||
for delete
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and c.type = 'operations'
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "operations_trips_select_visible" on public.operations_trips;
|
||||
create policy "operations_trips_select_visible"
|
||||
on public.operations_trips
|
||||
for select
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.operations_schedules s
|
||||
where s.id = schedule_id
|
||||
and public.can_access_channel(s.channel_id)
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "operations_trips_write_admins" on public.operations_trips;
|
||||
create policy "operations_trips_write_admins"
|
||||
on public.operations_trips
|
||||
for all
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.operations_schedules s
|
||||
join public.channels c on c.id = s.channel_id
|
||||
where s.id = schedule_id
|
||||
and c.type = 'operations'
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
)
|
||||
with check (
|
||||
exists (
|
||||
select 1
|
||||
from public.operations_schedules s
|
||||
join public.channels c on c.id = s.channel_id
|
||||
where s.id = schedule_id
|
||||
and c.type = 'operations'
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "operations_trip_stops_select_visible" on public.operations_trip_stops;
|
||||
create policy "operations_trip_stops_select_visible"
|
||||
on public.operations_trip_stops
|
||||
for select
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.operations_trips t
|
||||
join public.operations_schedules s on s.id = t.schedule_id
|
||||
where t.id = trip_id
|
||||
and public.can_access_channel(s.channel_id)
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "operations_trip_stops_write_admins" on public.operations_trip_stops;
|
||||
create policy "operations_trip_stops_write_admins"
|
||||
on public.operations_trip_stops
|
||||
for all
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.operations_trips t
|
||||
join public.operations_schedules s on s.id = t.schedule_id
|
||||
join public.channels c on c.id = s.channel_id
|
||||
where t.id = trip_id
|
||||
and c.type = 'operations'
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
)
|
||||
with check (
|
||||
exists (
|
||||
select 1
|
||||
from public.operations_trips t
|
||||
join public.operations_schedules s on s.id = t.schedule_id
|
||||
join public.channels c on c.id = s.channel_id
|
||||
where t.id = trip_id
|
||||
and c.type = 'operations'
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "operations_stop_updates_select_visible" on public.operations_stop_updates;
|
||||
create policy "operations_stop_updates_select_visible"
|
||||
on public.operations_stop_updates
|
||||
for select
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.operations_trip_stops ts
|
||||
join public.operations_trips t on t.id = ts.trip_id
|
||||
join public.operations_schedules s on s.id = t.schedule_id
|
||||
where ts.id = trip_stop_id
|
||||
and public.can_access_channel(s.channel_id)
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "operations_stop_updates_insert_members" on public.operations_stop_updates;
|
||||
create policy "operations_stop_updates_insert_members"
|
||||
on public.operations_stop_updates
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
updated_by = auth.uid()
|
||||
and exists (
|
||||
select 1
|
||||
from public.operations_trip_stops ts
|
||||
join public.operations_trips t on t.id = ts.trip_id
|
||||
join public.operations_schedules s on s.id = t.schedule_id
|
||||
where ts.id = trip_stop_id
|
||||
and public.can_access_channel(s.channel_id)
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "operations_stop_updates_update_members" on public.operations_stop_updates;
|
||||
create policy "operations_stop_updates_update_members"
|
||||
on public.operations_stop_updates
|
||||
for update
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.operations_trip_stops ts
|
||||
join public.operations_trips t on t.id = ts.trip_id
|
||||
join public.operations_schedules s on s.id = t.schedule_id
|
||||
where ts.id = trip_stop_id
|
||||
and public.can_access_channel(s.channel_id)
|
||||
)
|
||||
)
|
||||
with check (
|
||||
updated_by = auth.uid()
|
||||
and exists (
|
||||
select 1
|
||||
from public.operations_trip_stops ts
|
||||
join public.operations_trips t on t.id = ts.trip_id
|
||||
join public.operations_schedules s on s.id = t.schedule_id
|
||||
where ts.id = trip_stop_id
|
||||
and public.can_access_channel(s.channel_id)
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "operations_stop_updates_delete_members" on public.operations_stop_updates;
|
||||
create policy "operations_stop_updates_delete_members"
|
||||
on public.operations_stop_updates
|
||||
for delete
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.operations_trip_stops ts
|
||||
join public.operations_trips t on t.id = ts.trip_id
|
||||
join public.operations_schedules s on s.id = t.schedule_id
|
||||
where ts.id = trip_stop_id
|
||||
and public.can_access_channel(s.channel_id)
|
||||
)
|
||||
);
|
||||
|
||||
-- Keep updated_at fresh on edits.
|
||||
create or replace function public.tg_set_updated_at()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
new.updated_at := now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_operations_stop_updates_updated_at on public.operations_stop_updates;
|
||||
create trigger trg_operations_stop_updates_updated_at
|
||||
before update on public.operations_stop_updates
|
||||
for each row execute procedure public.tg_set_updated_at();
|
||||
|
||||
-- Realtime availability for operations collaboration features.
|
||||
do $$
|
||||
begin
|
||||
begin
|
||||
alter publication supabase_realtime add table public.operations_schedules;
|
||||
exception when duplicate_object then null;
|
||||
end;
|
||||
begin
|
||||
alter publication supabase_realtime add table public.operations_trips;
|
||||
exception when duplicate_object then null;
|
||||
end;
|
||||
begin
|
||||
alter publication supabase_realtime add table public.operations_trip_stops;
|
||||
exception when duplicate_object then null;
|
||||
end;
|
||||
begin
|
||||
alter publication supabase_realtime add table public.operations_stop_updates;
|
||||
exception when duplicate_object then null;
|
||||
end;
|
||||
end $$;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
alter table public.organizations
|
||||
add column if not exists icon_url text;
|
||||
|
||||
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
values (
|
||||
'organization-icons',
|
||||
'organization-icons',
|
||||
true,
|
||||
5242880,
|
||||
array['image/png', 'image/jpeg', 'image/webp', 'image/gif']
|
||||
)
|
||||
on conflict (id) do update
|
||||
set
|
||||
public = excluded.public,
|
||||
file_size_limit = excluded.file_size_limit,
|
||||
allowed_mime_types = excluded.allowed_mime_types;
|
||||
|
||||
drop policy if exists "org_icons_public_read" on storage.objects;
|
||||
create policy "org_icons_public_read"
|
||||
on storage.objects
|
||||
for select
|
||||
to public
|
||||
using (bucket_id = 'organization-icons');
|
||||
|
||||
drop policy if exists "org_icons_insert_admins" on storage.objects;
|
||||
create policy "org_icons_insert_admins"
|
||||
on storage.objects
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
bucket_id = 'organization-icons'
|
||||
and owner_id = auth.uid()::text
|
||||
and public.org_role(split_part(name, '/', 1)) in ('owner', 'admin')
|
||||
);
|
||||
|
||||
drop policy if exists "org_icons_update_admins" on storage.objects;
|
||||
create policy "org_icons_update_admins"
|
||||
on storage.objects
|
||||
for update
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'organization-icons'
|
||||
and public.org_role(split_part(name, '/', 1)) in ('owner', 'admin')
|
||||
)
|
||||
with check (
|
||||
bucket_id = 'organization-icons'
|
||||
and owner_id = auth.uid()::text
|
||||
and public.org_role(split_part(name, '/', 1)) in ('owner', 'admin')
|
||||
);
|
||||
|
||||
drop policy if exists "org_icons_delete_admins" on storage.objects;
|
||||
create policy "org_icons_delete_admins"
|
||||
on storage.objects
|
||||
for delete
|
||||
to authenticated
|
||||
using (
|
||||
bucket_id = 'organization-icons'
|
||||
and public.org_role(split_part(name, '/', 1)) in ('owner', 'admin')
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table public.operations_trips
|
||||
rename column running_number to bus_work_number;
|
||||
@@ -0,0 +1,7 @@
|
||||
alter table public.channels
|
||||
add column if not exists description text not null default '';
|
||||
|
||||
update public.channels
|
||||
set description = topic
|
||||
where coalesce(description, '') = ''
|
||||
and coalesce(topic, '') <> '';
|
||||
@@ -0,0 +1,84 @@
|
||||
create table if not exists public.operations_stop_aliases (
|
||||
id text primary key default public.gen_hash_id() check (id ~ '^[0-9a-f]{16}$'),
|
||||
channel_id text not null references public.channels(id) on delete cascade,
|
||||
raw_stop_name text not null,
|
||||
raw_stop_name_normalized text generated always as (lower(btrim(raw_stop_name))) stored,
|
||||
alias_stop_name text not null,
|
||||
created_by uuid not null references auth.users(id) on delete restrict,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint operations_stop_aliases_alias_not_blank check (char_length(btrim(alias_stop_name)) > 0),
|
||||
unique (channel_id, raw_stop_name_normalized)
|
||||
);
|
||||
|
||||
create index if not exists idx_operations_stop_aliases_channel
|
||||
on public.operations_stop_aliases(channel_id);
|
||||
|
||||
alter table public.operations_stop_aliases enable row level security;
|
||||
|
||||
drop policy if exists "operations_stop_aliases_select_visible" on public.operations_stop_aliases;
|
||||
create policy "operations_stop_aliases_select_visible"
|
||||
on public.operations_stop_aliases
|
||||
for select
|
||||
to authenticated
|
||||
using (public.can_access_channel(channel_id));
|
||||
|
||||
drop policy if exists "operations_stop_aliases_insert_admins" on public.operations_stop_aliases;
|
||||
create policy "operations_stop_aliases_insert_admins"
|
||||
on public.operations_stop_aliases
|
||||
for insert
|
||||
to authenticated
|
||||
with check (
|
||||
created_by = auth.uid()
|
||||
and exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and c.type = 'operations'
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "operations_stop_aliases_update_admins" on public.operations_stop_aliases;
|
||||
create policy "operations_stop_aliases_update_admins"
|
||||
on public.operations_stop_aliases
|
||||
for update
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and c.type = 'operations'
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
)
|
||||
with check (
|
||||
exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and c.type = 'operations'
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
drop policy if exists "operations_stop_aliases_delete_admins" on public.operations_stop_aliases;
|
||||
create policy "operations_stop_aliases_delete_admins"
|
||||
on public.operations_stop_aliases
|
||||
for delete
|
||||
to authenticated
|
||||
using (
|
||||
exists (
|
||||
select 1
|
||||
from public.channels c
|
||||
where c.id = channel_id
|
||||
and c.type = 'operations'
|
||||
and public.org_role(c.organization_id) in ('owner', 'admin')
|
||||
)
|
||||
);
|
||||
|
||||
drop trigger if exists trg_operations_stop_aliases_updated_at on public.operations_stop_aliases;
|
||||
create trigger trg_operations_stop_aliases_updated_at
|
||||
before update on public.operations_stop_aliases
|
||||
for each row execute procedure public.tg_set_updated_at();
|
||||
@@ -0,0 +1,13 @@
|
||||
alter table public.operations_stop_aliases
|
||||
add column if not exists source text not null default 'user';
|
||||
|
||||
update public.operations_stop_aliases
|
||||
set source = 'user'
|
||||
where source is null or btrim(source) = '';
|
||||
|
||||
alter table public.operations_stop_aliases
|
||||
drop constraint if exists operations_stop_aliases_source_check;
|
||||
|
||||
alter table public.operations_stop_aliases
|
||||
add constraint operations_stop_aliases_source_check
|
||||
check (source in ('user', 'ai'));
|
||||
Reference in New Issue
Block a user