Files
thrilltrack-explorer/supabase/functions/create-novu-subscriber/index.ts
gpt-engineer-app[bot] 2d65f13b85 Connect to Lovable Cloud
Add centralized errorFormatter to convert various error types into readable messages, and apply it across edge functions. Replace String(error) usage with formatEdgeError, update relevant imports, fix a throw to use toError, and enhance logger to log formatted errors. Includes new errorFormatter.ts and widespread updates to 18+ edge functions plus logger integration.
2025-11-10 18:09:15 +00:00

260 lines
7.6 KiB
TypeScript

import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
import { Novu } from "npm:@novu/api@1.6.0";
import { edgeLogger } from '../_shared/logger.ts';
import { formatEdgeError } from '../_shared/errorFormatter.ts';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
};
// Simple request tracking
const startRequest = () => ({ requestId: crypto.randomUUID(), start: Date.now() });
const endRequest = (tracking: { start: number }) => Date.now() - tracking.start;
serve(async (req) => {
const tracking = startRequest();
if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
const novuApiKey = Deno.env.get('NOVU_API_KEY');
if (!novuApiKey) {
throw new Error('NOVU_API_KEY is not configured');
}
const novu = new Novu({
secretKey: novuApiKey
});
// Parse and validate request body
let requestBody;
try {
requestBody = await req.json();
} catch (parseError) {
return new Response(
JSON.stringify({
success: false,
error: 'Invalid JSON in request body',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
}
);
}
const { subscriberId, email, firstName, lastName, phone, avatar, data } = requestBody;
// Validate required fields
if (!subscriberId || typeof subscriberId !== 'string' || subscriberId.trim() === '') {
return new Response(
JSON.stringify({
success: false,
error: 'subscriberId is required and must be a non-empty string',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
}
);
}
if (!email || typeof email !== 'string' || email.trim() === '') {
return new Response(
JSON.stringify({
success: false,
error: 'email is required and must be a non-empty string',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
}
);
}
// Validate email format using regex
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return new Response(
JSON.stringify({
success: false,
error: 'Invalid email format. Please provide a valid email address',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
}
);
}
// Validate optional fields if provided
if (firstName !== undefined && firstName !== null && (typeof firstName !== 'string' || firstName.length > 100)) {
return new Response(
JSON.stringify({
success: false,
error: 'firstName must be a string with maximum 100 characters',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
}
);
}
if (lastName !== undefined && lastName !== null && (typeof lastName !== 'string' || lastName.length > 100)) {
return new Response(
JSON.stringify({
success: false,
error: 'lastName must be a string with maximum 100 characters',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
}
);
}
if (phone !== undefined && phone !== null) {
if (typeof phone !== 'string') {
return new Response(
JSON.stringify({
success: false,
error: 'phone must be a string',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
}
);
}
// Validate phone format (basic validation for international numbers)
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(phone.replace(/[\s\-\(\)]/g, ''))) {
return new Response(
JSON.stringify({
success: false,
error: 'Invalid phone format. Please provide a valid international phone number',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
}
);
}
}
if (avatar !== undefined && avatar !== null && (typeof avatar !== 'string' || !avatar.startsWith('http'))) {
return new Response(
JSON.stringify({
success: false,
error: 'avatar must be a valid URL',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
}
);
}
// Validate data field if provided
if (data !== undefined && data !== null) {
if (typeof data !== 'object' || Array.isArray(data)) {
return new Response(
JSON.stringify({
success: false,
error: 'data must be a valid object',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
}
);
}
// Check data size (limit to 10KB serialized)
const dataSize = JSON.stringify(data).length;
if (dataSize > 10240) {
return new Response(
JSON.stringify({
success: false,
error: 'data field is too large (maximum 10KB)',
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 400,
}
);
}
}
edgeLogger.info('Creating Novu subscriber', { subscriberId, email: '***', firstName, requestId: tracking.requestId });
const subscriber = await novu.subscribers.identify(subscriberId, {
email,
firstName,
lastName,
phone,
avatar,
data,
});
const duration = endRequest(tracking);
edgeLogger.info('Subscriber created successfully', {
subscriberId: subscriber.data._id,
requestId: tracking.requestId,
duration
});
// Add subscriber to "users" topic for global announcements
try {
edgeLogger.info('Adding subscriber to users topic', { subscriberId, requestId: tracking.requestId });
await novu.topics.addSubscribers('users', {
subscribers: [subscriberId],
});
edgeLogger.info('Successfully added subscriber to users topic', { subscriberId, requestId: tracking.requestId });
} catch (topicError: unknown) {
// Non-blocking - log error but don't fail the request
edgeLogger.error('Failed to add subscriber to users topic', {
error: formatEdgeError(topicError),
subscriberId,
requestId: tracking.requestId
});
}
return new Response(
JSON.stringify({
success: true,
subscriberId: subscriber.data._id,
requestId: tracking.requestId
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
status: 200,
}
);
} catch (error: unknown) {
const duration = endRequest(tracking);
edgeLogger.error('Error creating Novu subscriber', {
error: formatEdgeError(error),
requestId: tracking.requestId,
duration
});
return new Response(
JSON.stringify({
success: false,
error: error.message,
requestId: tracking.requestId
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId },
status: 500,
}
);
}
});