243 lines
8.1 KiB
PL/PgSQL
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$;
|