supabase/migrations/006_add_application_functions.sql

243 lines
8.1 KiB
PL/PgSQL

-- Migration: Add application-specific functions
-- This migration adds 9 functions that exist on the live database but were
-- not included in earlier schema migrations.
-- =============================================================================
-- 1. setup_initial_admin
-- Sets up an admin user by updating their user_type and username.
-- SECURITY DEFINER: Must be run as service_role or superuser.
-- =============================================================================
CREATE OR REPLACE FUNCTION public.setup_initial_admin(admin_email text)
RETURNS json
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
declare
result json;
begin
-- Only allow this to run as service role or superuser
if not (
current_user = 'service_role'
or exists (
select 1 from pg_roles
where rolname = current_user
and rolsuper
)
) then
raise exception 'Must be run as service_role or superuser';
end if;
-- Update user_type and username for admin
update public.profiles
set user_type = 'admin',
username = coalesce(username, 'superadmin'),
display_name = coalesce(display_name, 'Super Admin')
where email = admin_email
returning json_build_object(
'id', id,
'email', email,
'user_type', user_type,
'username', username,
'display_name', display_name
) into result;
if result is null then
raise exception 'Admin user with email % not found', admin_email;
end if;
return result;
end;
$function$;
-- =============================================================================
-- 2. is_admin
-- Returns true if the current user has admin role.
-- =============================================================================
CREATE OR REPLACE FUNCTION public.is_admin()
RETURNS boolean
LANGUAGE sql
SECURITY DEFINER
AS $function$
select coalesce(
(select true
from public.profiles
where id = auth.uid()
and user_type = 'admin'),
false
);
$function$;
-- =============================================================================
-- 3. is_super_admin
-- Alias for is_admin (same logic).
-- =============================================================================
CREATE OR REPLACE FUNCTION public.is_super_admin()
RETURNS boolean
LANGUAGE sql
SECURITY DEFINER
AS $function$
select coalesce(
(select true
from public.profiles
where id = auth.uid()
and user_type = 'admin'),
false
);
$function$;
-- =============================================================================
-- 4. check_db_ready
-- Health check: verifies essential schemas, tables, and RLS are in place.
-- =============================================================================
CREATE OR REPLACE FUNCTION public.check_db_ready()
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
begin
-- Check if essential schemas exist
if not exists (
select 1
from information_schema.schemata
where schema_name in ('auth', 'storage', 'public')
) then
return false;
end if;
-- Check if essential tables exist
if not exists (
select 1
from information_schema.tables
where table_schema = 'auth'
and table_name = 'users'
) then
return false;
end if;
-- Check if RLS is enabled on public.profiles
if not exists (
select 1
from pg_tables
where schemaname = 'public'
and tablename = 'profiles'
and rowsecurity = true
) then
return false;
end if;
return true;
end;
$function$;
-- =============================================================================
-- 5. match_file_vectors
-- Vector similarity search over file artefacts.
-- NOTE: Requires the `file_vectors` table to exist (vector extension needed).
-- This function was created for a table that may not have been migrated yet.
-- =============================================================================
CREATE OR REPLACE FUNCTION public.match_file_vectors(
filter jsonb,
match_count integer,
query_embedding vector
)
RETURNS TABLE(
id bigint,
file_id uuid,
cabinet_id uuid,
artefact_type text,
artefact_is text,
original_path_prefix text,
original_filename text,
content text,
metadata jsonb,
similarity double precision
)
LANGUAGE sql
STABLE
AS $function$
select
fv.id,
nullif(fv.metadata->>'file_id','')::uuid as file_id,
nullif(fv.metadata->>'cabinet_id','')::uuid as cabinet_id,
nullif(fv.metadata->>'artefact_type','') as artefact_type,
nullif(fv.metadata->>'artefact_is','') as artefact_is,
nullif(fv.metadata->>'original_path_prefix','') as original_path_prefix,
nullif(fv.metadata->>'original_filename','') as original_filename,
fv.content,
fv.metadata,
1 - (fv.embedding <=> query_embedding) as similarity
from public.file_vectors fv
where
(coalesce(filter ? 'file_id', false) = false or (fv.metadata->>'file_id')::uuid = (filter->>'file_id')::uuid)
and (coalesce(filter ? 'cabinet_id', false) = false or (fv.metadata->>'cabinet_id')::uuid = (filter->>'cabinet_id')::uuid)
and (coalesce(filter ? 'artefact_type', false) = false or (fv.metadata->>'artefact_type') = (filter->>'artefact_type'))
and (coalesce(filter ? 'artefact_id', false) = false or (fv.metadata->>'artefact_id') = (filter->>'artefact_id'))
and (coalesce(filter ? 'original_path_prefix', false) = false or (fv.metadata->>'original_path_prefix') like (filter->>'original_path_prefix') || '%')
and (coalesce(filter ? 'original_path_prefix_ilike', false)= false or (fv.metadata->>'original_path_prefix') ilike (filter->>'original_path_prefix_ilike') || '%')
and (coalesce(filter ? 'original_filename', false) = false or (fv.metadata->>'original_filename') = (filter->>'original_filename'))
and (coalesce(filter ? 'original_filename_ilike', false)= false or (fv.metadata->>'original_filename') ilike (filter->>'original_filename_ilike'))
order by fv.embedding <=> query_embedding
limit greatest(coalesce(match_count, 10), 1)
$function$;
-- =============================================================================
-- 6. set_completed_at
-- Trigger function: sets completed_at when status changes to 'completed'.
-- =============================================================================
CREATE OR REPLACE FUNCTION public.set_completed_at()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
begin
if NEW.status = 'completed' and OLD.status != 'completed' then
NEW.completed_at = now();
end if;
return NEW;
end;
$function$;
-- =============================================================================
-- 7. handle_updated_at
-- Trigger function: sets updated_at to current UTC time on UPDATE.
-- =============================================================================
CREATE OR REPLACE FUNCTION public.handle_updated_at()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
begin
new.updated_at = timezone('utc'::text, now());
return new;
end;
$function$;
-- =============================================================================
-- 8. update_updated_at_column
-- Alternative trigger function: sets updated_at to NOW() on UPDATE.
-- (Duplicate of handle_updated_at with slightly different syntax)
-- =============================================================================
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$function$;
-- =============================================================================
-- 9. exec_sql
-- Executes arbitrary SQL. SECURITY DEFINER — use with extreme caution.
-- =============================================================================
CREATE OR REPLACE FUNCTION public.exec_sql(query text)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
begin
execute query;
end;
$function$;