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
195 lines
5.8 KiB
TypeScript
195 lines
5.8 KiB
TypeScript
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
import { sql, type Selectable } from "https://esm.sh/kysely@0.27.2";
|
|
import { db, type ContactsTable, CompiledQuery } from "../_shared/db.ts";
|
|
import { corsHeaders, OptionsMiddleware } from "../_shared/cors.ts";
|
|
import { createErrorResponse } from "../_shared/utils.ts";
|
|
import { AuthMiddleware, UserMiddleware } from "../_shared/authentication.ts";
|
|
|
|
type Contact = Selectable<ContactsTable>;
|
|
|
|
// Helper functions to merge arrays
|
|
function mergeArraysUnique<T>(arr1: T[], arr2: T[]): T[] {
|
|
return [...new Set([...arr1, ...arr2])];
|
|
}
|
|
|
|
function mergeObjectArraysUnique<T>(
|
|
arr1: T[],
|
|
arr2: T[],
|
|
getKey: (item: T) => string,
|
|
): T[] {
|
|
const map = new Map<string, T>();
|
|
|
|
arr1.forEach((item) => {
|
|
const key = getKey(item);
|
|
if (key) map.set(key, item);
|
|
});
|
|
|
|
arr2.forEach((item) => {
|
|
const key = getKey(item);
|
|
if (key && !map.has(key)) {
|
|
map.set(key, item);
|
|
}
|
|
});
|
|
|
|
return Array.from(map.values());
|
|
}
|
|
|
|
function mergeContactData(winner: Contact, loser: Contact) {
|
|
// Merge emails
|
|
const mergedEmails = mergeObjectArraysUnique(
|
|
winner.email_jsonb || [],
|
|
loser.email_jsonb || [],
|
|
(email: any) => email.email,
|
|
);
|
|
|
|
// Merge phones
|
|
const mergedPhones = mergeObjectArraysUnique(
|
|
winner.phone_jsonb || [],
|
|
loser.phone_jsonb || [],
|
|
(phone: any) => phone.number,
|
|
);
|
|
|
|
const selectedAvatar =
|
|
winner.avatar && winner.avatar.src ? winner.avatar : loser.avatar;
|
|
|
|
return {
|
|
avatar: selectedAvatar ? (JSON.stringify(selectedAvatar) as any) : null,
|
|
gender: winner.gender ?? loser.gender,
|
|
first_name: winner.first_name ?? loser.first_name,
|
|
last_name: winner.last_name ?? loser.last_name,
|
|
title: winner.title ?? loser.title,
|
|
company_id: winner.company_id ?? loser.company_id,
|
|
email_jsonb: JSON.stringify(mergedEmails) as any,
|
|
phone_jsonb: JSON.stringify(mergedPhones) as any,
|
|
linkedin_url: winner.linkedin_url || loser.linkedin_url,
|
|
background: winner.background ?? loser.background,
|
|
has_newsletter: winner.has_newsletter ?? loser.has_newsletter,
|
|
first_seen: winner.first_seen ?? loser.first_seen,
|
|
last_seen:
|
|
winner.last_seen && loser.last_seen
|
|
? winner.last_seen > loser.last_seen
|
|
? winner.last_seen
|
|
: loser.last_seen
|
|
: (winner.last_seen ?? loser.last_seen),
|
|
sales_id: winner.sales_id ?? loser.sales_id,
|
|
tags: mergeArraysUnique(winner.tags || [], loser.tags || []),
|
|
};
|
|
}
|
|
|
|
async function mergeContacts(
|
|
loserId: number,
|
|
winnerId: number,
|
|
userId: string,
|
|
) {
|
|
try {
|
|
return await db.transaction().execute(async (trx) => {
|
|
// Enable RLS by switching to authenticated role and setting user context
|
|
await trx.executeQuery(CompiledQuery.raw("SET LOCAL ROLE authenticated"));
|
|
await trx.executeQuery(
|
|
CompiledQuery.raw(
|
|
`SELECT set_config('request.jwt.claim.sub', '${userId}', true)`,
|
|
),
|
|
);
|
|
|
|
// 1. Fetch both contacts
|
|
const [winner, loser] = await Promise.all([
|
|
trx
|
|
.selectFrom("contacts")
|
|
.selectAll()
|
|
.where("id", "=", winnerId)
|
|
.executeTakeFirstOrThrow(),
|
|
trx
|
|
.selectFrom("contacts")
|
|
.selectAll()
|
|
.where("id", "=", loserId)
|
|
.executeTakeFirstOrThrow(),
|
|
]);
|
|
|
|
// 2. Reassign tasks from loser to winner
|
|
await trx
|
|
.updateTable("tasks")
|
|
.set({ contact_id: winnerId })
|
|
.where("contact_id", "=", loserId)
|
|
.execute();
|
|
|
|
// 3. Reassign notes from loser to winner
|
|
await trx
|
|
.updateTable("contact_notes")
|
|
.set({ contact_id: winnerId })
|
|
.where("contact_id", "=", loserId)
|
|
.execute();
|
|
|
|
// 4. Update deals - replace loserId with winnerId in contact_ids array
|
|
const deals = await trx
|
|
.selectFrom("deals")
|
|
.selectAll()
|
|
.where(sql`contact_ids @> ARRAY[${loserId}]::bigint[]`)
|
|
.execute();
|
|
|
|
for (const deal of deals) {
|
|
const newContactIds = [
|
|
...new Set(
|
|
deal.contact_ids.filter((id) => id !== loserId).concat(winnerId),
|
|
),
|
|
];
|
|
await trx
|
|
.updateTable("deals")
|
|
.set({ contact_ids: newContactIds })
|
|
.where("id", "=", deal.id)
|
|
.execute();
|
|
}
|
|
|
|
// 5. Merge and update winner contact
|
|
const mergedData = mergeContactData(winner as Contact, loser as Contact);
|
|
await trx
|
|
.updateTable("contacts")
|
|
.set(mergedData)
|
|
.where("id", "=", winnerId)
|
|
.execute();
|
|
|
|
// 6. Delete loser contact
|
|
await trx.deleteFrom("contacts").where("id", "=", loserId).execute();
|
|
|
|
return { success: true, winnerId };
|
|
});
|
|
} catch (error) {
|
|
console.error("Transaction failed:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
Deno.serve(async (req: Request) =>
|
|
OptionsMiddleware(req, async (req) =>
|
|
AuthMiddleware(req, async (req) =>
|
|
UserMiddleware(req, async (req, user) => {
|
|
// Handle POST request
|
|
if (req.method === "POST") {
|
|
try {
|
|
const { loserId, winnerId } = await req.json();
|
|
|
|
if (!loserId || !winnerId) {
|
|
return createErrorResponse(400, "Missing loserId or winnerId");
|
|
}
|
|
|
|
const result = await mergeContacts(loserId, winnerId, user.id);
|
|
|
|
return new Response(JSON.stringify(result), {
|
|
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
});
|
|
} catch (error) {
|
|
console.error("Merge failed:", error);
|
|
return createErrorResponse(
|
|
500,
|
|
`Failed to merge contacts: ${
|
|
error instanceof Error ? error.message : "Unknown error"
|
|
}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return createErrorResponse(405, "Method Not Allowed");
|
|
}),
|
|
),
|
|
),
|
|
);
|