mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 17:31:13 -05:00
Refactor Phase 3 Batch 2–4 Novu-related functions to use the createEdgeFunction wrapper, replacing explicit HTTP servers with edge wrapper, adding standardized logging, tracing, and error handling across subscriber management, topic/notification, and migration/sync functions.
153 lines
4.1 KiB
TypeScript
153 lines
4.1 KiB
TypeScript
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
|
import { Novu } from "npm:@novu/api@1.6.0";
|
|
import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts';
|
|
import { edgeLogger } from "../_shared/logger.ts";
|
|
import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts';
|
|
|
|
export default createEdgeFunction(
|
|
{
|
|
name: 'migrate-novu-users',
|
|
requireAuth: false,
|
|
corsHeaders: corsHeaders
|
|
},
|
|
async (req, context) => {
|
|
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
|
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
|
const novuApiKey = Deno.env.get('NOVU_API_KEY');
|
|
|
|
if (!novuApiKey) {
|
|
throw new Error('NOVU_API_KEY is not configured');
|
|
}
|
|
|
|
context.span.setAttribute('action', 'migrate_novu_users');
|
|
|
|
// Create Supabase client with service role for admin access
|
|
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
|
|
const novu = new Novu({
|
|
secretKey: novuApiKey
|
|
});
|
|
|
|
// Fetch users who don't have Novu subscriber IDs
|
|
const { data: existingPrefs, error: prefsError } = await supabase
|
|
.from('user_notification_preferences')
|
|
.select('user_id')
|
|
.not('novu_subscriber_id', 'is', null);
|
|
|
|
if (prefsError) throw prefsError;
|
|
|
|
const existingUserIds = existingPrefs?.map(p => p.user_id) || [];
|
|
|
|
// Fetch all profiles with usernames
|
|
let query = supabase
|
|
.from('profiles')
|
|
.select('user_id, username');
|
|
|
|
// Only add the not filter if there are existing user IDs
|
|
if (existingUserIds.length > 0) {
|
|
query = query.not('user_id', 'in', `(${existingUserIds.join(',')})`);
|
|
}
|
|
|
|
const { data: profiles, error: profilesError } = await query;
|
|
|
|
if (profilesError) throw profilesError;
|
|
|
|
if (!profiles || profiles.length === 0) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: true,
|
|
message: 'No users to migrate',
|
|
results: [],
|
|
}),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
status: 200,
|
|
}
|
|
);
|
|
}
|
|
|
|
// Fetch user emails from auth.users using service role
|
|
const userIds = profiles.map(p => p.user_id);
|
|
const { data: authUsers, error: authError } = await supabase.auth.admin.listUsers();
|
|
|
|
if (authError) throw authError;
|
|
|
|
const userEmails = new Map(
|
|
authUsers.users
|
|
.filter(u => userIds.includes(u.id))
|
|
.map(u => [u.id, u.email])
|
|
);
|
|
|
|
// Migrate users
|
|
const results = [];
|
|
for (const profile of profiles) {
|
|
const email = userEmails.get(profile.user_id);
|
|
|
|
if (!email) {
|
|
results.push({
|
|
userId: profile.user_id,
|
|
email: 'No email found',
|
|
success: false,
|
|
error: 'User email not found',
|
|
});
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const subscriber = await novu.subscribers.identify(profile.user_id, {
|
|
email,
|
|
firstName: profile.username,
|
|
data: { userId: profile.user_id },
|
|
});
|
|
|
|
// Update the user's notification preferences with the Novu subscriber ID
|
|
await supabase
|
|
.from('user_notification_preferences')
|
|
.upsert({
|
|
user_id: profile.user_id,
|
|
novu_subscriber_id: subscriber.data._id,
|
|
});
|
|
|
|
results.push({
|
|
userId: profile.user_id,
|
|
email,
|
|
username: profile.username,
|
|
success: true,
|
|
});
|
|
} catch (error: any) {
|
|
results.push({
|
|
userId: profile.user_id,
|
|
email,
|
|
success: false,
|
|
error: error.message,
|
|
});
|
|
}
|
|
|
|
// Small delay to avoid overwhelming the API
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
}
|
|
|
|
edgeLogger.info('Migration complete', {
|
|
requestId: context.requestId,
|
|
total: profiles.length,
|
|
successful: results.filter(r => r.success).length
|
|
});
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: true,
|
|
total: profiles.length,
|
|
results,
|
|
}),
|
|
{
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
status: 200,
|
|
}
|
|
);
|
|
}
|
|
);
|