Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
456 lines
13 KiB
PL/PgSQL
456 lines
13 KiB
PL/PgSQL
--
|
|
-- Functions
|
|
-- This file declares all PL/pgSQL functions in the public schema.
|
|
--
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."cleanup_note_attachments"() RETURNS "trigger"
|
|
LANGUAGE "plpgsql" SECURITY DEFINER
|
|
SET "search_path" TO ''
|
|
AS $$
|
|
DECLARE
|
|
payload jsonb;
|
|
request_headers jsonb;
|
|
auth_header text;
|
|
BEGIN
|
|
request_headers := coalesce(
|
|
nullif(current_setting('request.headers', true), '')::jsonb,
|
|
'{}'::jsonb
|
|
);
|
|
auth_header := request_headers ->> 'authorization';
|
|
|
|
IF auth_header IS NULL OR auth_header = '' THEN
|
|
IF TG_OP = 'DELETE' THEN
|
|
RETURN OLD;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END IF;
|
|
|
|
payload := jsonb_build_object(
|
|
'old_record', OLD,
|
|
'record', NEW,
|
|
'type', TG_OP
|
|
);
|
|
|
|
PERFORM net.http_post(
|
|
url := public.get_note_attachments_function_url(),
|
|
body := payload,
|
|
params := '{}'::jsonb,
|
|
headers := jsonb_build_object(
|
|
'Content-Type',
|
|
'application/json',
|
|
'Authorization',
|
|
auth_header
|
|
),
|
|
timeout_milliseconds := 10000
|
|
);
|
|
|
|
IF TG_OP = 'DELETE' THEN
|
|
RETURN OLD;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."get_avatar_for_email"("email" "text") RETURNS "text"
|
|
LANGUAGE "plpgsql"
|
|
SET "search_path" TO 'public'
|
|
AS $$
|
|
declare email_hash text;
|
|
declare gravatar_url text;
|
|
declare gravatar_status int8;
|
|
declare email_domain text;
|
|
declare favicon_url text;
|
|
declare domain_status int8;
|
|
|
|
begin
|
|
-- Try to fetch a gravatar image
|
|
email_hash = encode(extensions.digest(email, 'sha256'), 'hex');
|
|
gravatar_url = concat('https://www.gravatar.com/avatar/', email_hash, '?d=404');
|
|
|
|
select status from extensions.http_get(gravatar_url) into gravatar_status;
|
|
|
|
if gravatar_status = 200 then
|
|
return gravatar_url;
|
|
end if;
|
|
|
|
-- Fallback to email's domain favicon if not excluded
|
|
email_domain = split_part(email, '@', 2);
|
|
return get_domain_favicon(email_domain);
|
|
exception
|
|
when others then
|
|
return 'ERROR';
|
|
end;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."get_domain_favicon"("domain_name" "text") RETURNS "text"
|
|
LANGUAGE "plpgsql"
|
|
SET "search_path" TO 'public'
|
|
AS $$
|
|
declare domain_status int8;
|
|
|
|
begin
|
|
if exists (select from favicons_excluded_domains as fav where fav.domain = domain_name) then
|
|
return null;
|
|
end if;
|
|
|
|
return concat(
|
|
'https://favicon.show/',
|
|
(regexp_matches(domain_name, '^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/?\n]+)', 'i'))[1]
|
|
);
|
|
end;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."get_note_attachments_function_url"() RETURNS "text"
|
|
LANGUAGE "plpgsql"
|
|
SET "search_path" TO 'public'
|
|
AS $$
|
|
DECLARE
|
|
issuer text;
|
|
function_url text;
|
|
BEGIN
|
|
issuer := coalesce(
|
|
nullif(current_setting('request.jwt.claim.iss', true), ''),
|
|
(
|
|
coalesce(
|
|
nullif(current_setting('request.jwt.claims', true), ''),
|
|
'{}'
|
|
)::jsonb ->> 'iss'
|
|
)
|
|
);
|
|
issuer := nullif(issuer, '');
|
|
IF issuer IS NOT NULL THEN
|
|
issuer := rtrim(issuer, '/');
|
|
IF right(issuer, 8) = '/auth/v1' THEN
|
|
function_url :=
|
|
left(issuer, length(issuer) - 8) || '/functions/v1/delete_note_attachments';
|
|
|
|
IF function_url LIKE 'http://127.0.0.1:%' THEN
|
|
RETURN replace(
|
|
function_url,
|
|
'http://127.0.0.1:',
|
|
'http://host.docker.internal:'
|
|
);
|
|
END IF;
|
|
|
|
IF function_url LIKE 'http://localhost:%' THEN
|
|
RETURN replace(
|
|
function_url,
|
|
'http://localhost:',
|
|
'http://host.docker.internal:'
|
|
);
|
|
END IF;
|
|
|
|
RETURN function_url;
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN 'http://host.docker.internal:54321/functions/v1/delete_note_attachments';
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."get_user_id_by_email"("email" "text") RETURNS TABLE("id" "uuid")
|
|
LANGUAGE "plpgsql" SECURITY DEFINER
|
|
SET "search_path" TO 'public'
|
|
AS $_$
|
|
BEGIN
|
|
RETURN QUERY SELECT au.id FROM auth.users au WHERE au.email = $1;
|
|
END;
|
|
$_$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."handle_company_saved"() RETURNS "trigger"
|
|
LANGUAGE "plpgsql"
|
|
SET "search_path" TO 'public'
|
|
AS $$
|
|
declare company_logo text;
|
|
|
|
begin
|
|
if new.logo is not null then
|
|
return new;
|
|
end if;
|
|
|
|
company_logo = get_domain_favicon(new.website);
|
|
if company_logo is null then
|
|
return new;
|
|
end if;
|
|
|
|
new.logo = concat('{"src":"', company_logo, '","title":"Company favicon"}');
|
|
return new;
|
|
end;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."handle_contact_note_created_or_updated"() RETURNS "trigger"
|
|
LANGUAGE "plpgsql" SECURITY DEFINER
|
|
SET "search_path" TO ''
|
|
AS $$
|
|
begin
|
|
update public.contacts set last_seen = new.date where contacts.id = new.contact_id and contacts.last_seen < new.date;
|
|
return new;
|
|
end;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."handle_contact_saved"() RETURNS "trigger"
|
|
LANGUAGE "plpgsql"
|
|
SET "search_path" TO 'public'
|
|
AS $$declare contact_avatar text;
|
|
declare emails_length int8;
|
|
declare item jsonb;
|
|
|
|
begin
|
|
if new.avatar is not null then
|
|
return new;
|
|
end if;
|
|
|
|
select coalesce(jsonb_array_length(new.email_jsonb), 0) into emails_length;
|
|
|
|
if emails_length = 0 then
|
|
return new;
|
|
end if;
|
|
|
|
for item in select jsonb_array_elements(new.email_jsonb)
|
|
loop
|
|
select public.get_avatar_for_email(item->>'email') into contact_avatar;
|
|
if (contact_avatar is not null) then
|
|
exit;
|
|
end if;
|
|
end loop;
|
|
|
|
if contact_avatar is null then
|
|
return new;
|
|
end if;
|
|
|
|
new.avatar = concat('{"src":"', contact_avatar, '"}');
|
|
return new;
|
|
end;$$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."handle_new_user"() RETURNS "trigger"
|
|
LANGUAGE "plpgsql" SECURITY DEFINER
|
|
SET "search_path" TO ''
|
|
AS $$
|
|
declare
|
|
sales_count int;
|
|
begin
|
|
select count(id) into sales_count
|
|
from public.sales;
|
|
|
|
insert into public.sales (first_name, last_name, email, user_id, administrator)
|
|
values (
|
|
coalesce(new.raw_user_meta_data ->> 'first_name', new.raw_user_meta_data -> 'custom_claims' ->> 'first_name', 'Pending'),
|
|
coalesce(new.raw_user_meta_data ->> 'last_name', new.raw_user_meta_data -> 'custom_claims' ->> 'last_name', 'Pending'),
|
|
new.email,
|
|
new.id,
|
|
case when sales_count > 0 then FALSE else TRUE end
|
|
);
|
|
return new;
|
|
end;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."handle_update_user"() RETURNS "trigger"
|
|
LANGUAGE "plpgsql" SECURITY DEFINER
|
|
SET "search_path" TO ''
|
|
AS $$
|
|
begin
|
|
update public.sales
|
|
set
|
|
first_name = coalesce(new.raw_user_meta_data ->> 'first_name', new.raw_user_meta_data -> 'custom_claims' ->> 'first_name', 'Pending'),
|
|
last_name = coalesce(new.raw_user_meta_data ->> 'last_name', new.raw_user_meta_data -> 'custom_claims' ->> 'last_name', 'Pending'),
|
|
email = new.email
|
|
where user_id = new.id;
|
|
|
|
return new;
|
|
end;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."is_admin"() RETURNS boolean
|
|
LANGUAGE "plpgsql" SECURITY DEFINER
|
|
SET "search_path" TO ''
|
|
AS $$
|
|
begin
|
|
return exists (
|
|
select 1 from public.sales where user_id = auth.uid() and administrator = true
|
|
);
|
|
end;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."merge_contacts"("loser_id" bigint, "winner_id" bigint) RETURNS bigint
|
|
LANGUAGE "plpgsql"
|
|
SET "search_path" TO 'public'
|
|
AS $$
|
|
DECLARE
|
|
winner_contact contacts%ROWTYPE;
|
|
loser_contact contacts%ROWTYPE;
|
|
deal_record RECORD;
|
|
merged_emails jsonb;
|
|
merged_phones jsonb;
|
|
merged_tags bigint[];
|
|
winner_emails jsonb;
|
|
loser_emails jsonb;
|
|
winner_phones jsonb;
|
|
loser_phones jsonb;
|
|
email_map jsonb;
|
|
phone_map jsonb;
|
|
BEGIN
|
|
-- Fetch both contacts
|
|
SELECT * INTO winner_contact FROM contacts WHERE id = winner_id;
|
|
SELECT * INTO loser_contact FROM contacts WHERE id = loser_id;
|
|
|
|
IF winner_contact IS NULL OR loser_contact IS NULL THEN
|
|
RAISE EXCEPTION 'Contact not found';
|
|
END IF;
|
|
|
|
-- 1. Reassign tasks from loser to winner
|
|
UPDATE tasks SET contact_id = winner_id WHERE contact_id = loser_id;
|
|
|
|
-- 2. Reassign contact notes from loser to winner
|
|
UPDATE contact_notes SET contact_id = winner_id WHERE contact_id = loser_id;
|
|
|
|
-- 3. Update deals - replace loser with winner in contact_ids array
|
|
FOR deal_record IN
|
|
SELECT id, contact_ids
|
|
FROM deals
|
|
WHERE contact_ids @> ARRAY[loser_id]
|
|
LOOP
|
|
UPDATE deals
|
|
SET contact_ids = (
|
|
SELECT ARRAY(
|
|
SELECT DISTINCT unnest(
|
|
array_remove(deal_record.contact_ids, loser_id) || ARRAY[winner_id]
|
|
)
|
|
)
|
|
)
|
|
WHERE id = deal_record.id;
|
|
END LOOP;
|
|
|
|
-- 4. Merge contact data
|
|
|
|
-- Get email arrays
|
|
winner_emails := COALESCE(winner_contact.email_jsonb, '[]'::jsonb);
|
|
loser_emails := COALESCE(loser_contact.email_jsonb, '[]'::jsonb);
|
|
|
|
-- Merge emails with deduplication by email address
|
|
-- Build a map of email -> email object, then convert back to array
|
|
email_map := '{}'::jsonb;
|
|
|
|
-- Add winner emails to map
|
|
IF jsonb_array_length(winner_emails) > 0 THEN
|
|
FOR i IN 0..jsonb_array_length(winner_emails)-1 LOOP
|
|
email_map := email_map || jsonb_build_object(
|
|
winner_emails->i->>'email',
|
|
winner_emails->i
|
|
);
|
|
END LOOP;
|
|
END IF;
|
|
|
|
-- Add loser emails to map (won't overwrite existing keys)
|
|
IF jsonb_array_length(loser_emails) > 0 THEN
|
|
FOR i IN 0..jsonb_array_length(loser_emails)-1 LOOP
|
|
IF NOT email_map ? (loser_emails->i->>'email') THEN
|
|
email_map := email_map || jsonb_build_object(
|
|
loser_emails->i->>'email',
|
|
loser_emails->i
|
|
);
|
|
END IF;
|
|
END LOOP;
|
|
END IF;
|
|
|
|
-- Convert map back to array
|
|
merged_emails := (SELECT jsonb_agg(value) FROM jsonb_each(email_map));
|
|
merged_emails := COALESCE(merged_emails, '[]'::jsonb);
|
|
|
|
-- Get phone arrays
|
|
winner_phones := COALESCE(winner_contact.phone_jsonb, '[]'::jsonb);
|
|
loser_phones := COALESCE(loser_contact.phone_jsonb, '[]'::jsonb);
|
|
|
|
-- Merge phones with deduplication by number
|
|
phone_map := '{}'::jsonb;
|
|
|
|
-- Add winner phones to map
|
|
IF jsonb_array_length(winner_phones) > 0 THEN
|
|
FOR i IN 0..jsonb_array_length(winner_phones)-1 LOOP
|
|
phone_map := phone_map || jsonb_build_object(
|
|
winner_phones->i->>'number',
|
|
winner_phones->i
|
|
);
|
|
END LOOP;
|
|
END IF;
|
|
|
|
-- Add loser phones to map (won't overwrite existing keys)
|
|
IF jsonb_array_length(loser_phones) > 0 THEN
|
|
FOR i IN 0..jsonb_array_length(loser_phones)-1 LOOP
|
|
IF NOT phone_map ? (loser_phones->i->>'number') THEN
|
|
phone_map := phone_map || jsonb_build_object(
|
|
loser_phones->i->>'number',
|
|
loser_phones->i
|
|
);
|
|
END IF;
|
|
END LOOP;
|
|
END IF;
|
|
|
|
-- Convert map back to array
|
|
merged_phones := (SELECT jsonb_agg(value) FROM jsonb_each(phone_map));
|
|
merged_phones := COALESCE(merged_phones, '[]'::jsonb);
|
|
|
|
-- Merge tags (remove duplicates)
|
|
merged_tags := ARRAY(
|
|
SELECT DISTINCT unnest(
|
|
COALESCE(winner_contact.tags, ARRAY[]::bigint[]) ||
|
|
COALESCE(loser_contact.tags, ARRAY[]::bigint[])
|
|
)
|
|
);
|
|
|
|
-- 5. Update winner with merged data
|
|
UPDATE contacts SET
|
|
avatar = COALESCE(winner_contact.avatar, loser_contact.avatar),
|
|
gender = COALESCE(winner_contact.gender, loser_contact.gender),
|
|
first_name = COALESCE(winner_contact.first_name, loser_contact.first_name),
|
|
last_name = COALESCE(winner_contact.last_name, loser_contact.last_name),
|
|
title = COALESCE(winner_contact.title, loser_contact.title),
|
|
company_id = COALESCE(winner_contact.company_id, loser_contact.company_id),
|
|
email_jsonb = merged_emails,
|
|
phone_jsonb = merged_phones,
|
|
linkedin_url = COALESCE(winner_contact.linkedin_url, loser_contact.linkedin_url),
|
|
background = COALESCE(winner_contact.background, loser_contact.background),
|
|
has_newsletter = COALESCE(winner_contact.has_newsletter, loser_contact.has_newsletter),
|
|
first_seen = LEAST(COALESCE(winner_contact.first_seen, loser_contact.first_seen), COALESCE(loser_contact.first_seen, winner_contact.first_seen)),
|
|
last_seen = GREATEST(COALESCE(winner_contact.last_seen, loser_contact.last_seen), COALESCE(loser_contact.last_seen, winner_contact.last_seen)),
|
|
sales_id = COALESCE(winner_contact.sales_id, loser_contact.sales_id),
|
|
tags = merged_tags
|
|
WHERE id = winner_id;
|
|
|
|
-- 6. Delete loser contact
|
|
DELETE FROM contacts WHERE id = loser_id;
|
|
|
|
RETURN winner_id;
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."lowercase_email_jsonb"() RETURNS "trigger"
|
|
LANGUAGE "plpgsql"
|
|
SET "search_path" TO 'public'
|
|
AS $$
|
|
BEGIN
|
|
IF NEW.email_jsonb IS NOT NULL THEN
|
|
NEW.email_jsonb = COALESCE((
|
|
SELECT jsonb_agg(
|
|
jsonb_set(elem, '{email}', to_jsonb(LOWER(elem->>'email')))
|
|
)
|
|
FROM jsonb_array_elements(NEW.email_jsonb) AS elem
|
|
), '[]'::jsonb);
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION "public"."set_sales_id_default"() RETURNS "trigger"
|
|
LANGUAGE "plpgsql"
|
|
SET "search_path" TO 'public'
|
|
AS $$
|
|
BEGIN
|
|
IF NEW.sales_id IS NULL THEN
|
|
SELECT id INTO NEW.sales_id FROM sales WHERE user_id = auth.uid();
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|