mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:51:14 -05:00
Migrate Phase 1 user-facing functions
Refactor export-user-data, notify-user-submission-status, and resend-deletion-code to use createEdgeFunction wrapper. Remove manual CORS, auth, rate limiting boilerplate; adopt standardized EdgeFunctionContext (supabase, user, span, requestId), and integrate built-in tracing, rate limiting, and logging through the wrapper. Update handlers to rely on wrapper context and ensure consistent error handling and observability.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
||||
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4";
|
||||
import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts';
|
||||
import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts";
|
||||
import { createEdgeFunction, type EdgeFunctionContext } from '../_shared/edgeFunctionWrapper.ts';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { addSpanEvent } from '../_shared/logger.ts';
|
||||
|
||||
interface RequestBody {
|
||||
submission_id: string;
|
||||
@@ -81,203 +81,151 @@ async function constructEntityURL(
|
||||
return `${baseURL}`;
|
||||
}
|
||||
|
||||
serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { headers: corsHeaders });
|
||||
const handler = async (req: Request, { supabase, span, requestId }: EdgeFunctionContext) => {
|
||||
const { submission_id, user_id, submission_type, status, reviewer_notes } = await req.json() as RequestBody;
|
||||
|
||||
addSpanEvent(span, 'notification_request', {
|
||||
submissionId: submission_id,
|
||||
userId: user_id,
|
||||
status
|
||||
});
|
||||
|
||||
// Fetch submission items to get entity data
|
||||
const { data: items, error: itemsError } = await supabase
|
||||
.from('submission_items')
|
||||
.select('item_data')
|
||||
.eq('submission_id', submission_id)
|
||||
.order('order_index', { ascending: true })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
if (itemsError) {
|
||||
throw new Error(`Failed to fetch submission items: ${itemsError.message}`);
|
||||
}
|
||||
|
||||
const tracking = startRequest('notify-user-submission-status');
|
||||
if (!items || !items.item_data) {
|
||||
throw new Error('No submission items found');
|
||||
}
|
||||
|
||||
try {
|
||||
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
||||
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
||||
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||
// Extract entity data
|
||||
const entityName = items.item_data.name || 'your submission';
|
||||
const entityType = submission_type.replace('_', ' ');
|
||||
|
||||
// Construct entity URL
|
||||
const entityURL = await constructEntityURL(supabase, submission_type, items.item_data);
|
||||
|
||||
const { submission_id, user_id, submission_type, status, reviewer_notes } = await req.json() as RequestBody;
|
||||
|
||||
// Fetch submission items to get entity data
|
||||
const { data: items, error: itemsError } = await supabase
|
||||
.from('submission_items')
|
||||
.select('item_data')
|
||||
.eq('submission_id', submission_id)
|
||||
.order('order_index', { ascending: true })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
if (itemsError) {
|
||||
throw new Error(`Failed to fetch submission items: ${itemsError.message}`);
|
||||
}
|
||||
|
||||
if (!items || !items.item_data) {
|
||||
throw new Error('No submission items found');
|
||||
}
|
||||
|
||||
// Extract entity data
|
||||
const entityName = items.item_data.name || 'your submission';
|
||||
const entityType = submission_type.replace('_', ' ');
|
||||
|
||||
// Construct entity URL
|
||||
const entityURL = await constructEntityURL(supabase, submission_type, items.item_data);
|
||||
|
||||
// Determine workflow and build payload based on status
|
||||
const workflowId = status === 'approved' ? 'submission-approved' : 'submission-rejected';
|
||||
|
||||
let payload: Record<string, string>;
|
||||
|
||||
if (status === 'approved') {
|
||||
// Approval payload
|
||||
payload = {
|
||||
baseUrl: 'https://www.thrillwiki.com',
|
||||
entityType,
|
||||
entityName,
|
||||
submissionId: submission_id,
|
||||
entityURL,
|
||||
moderationNotes: reviewer_notes || '',
|
||||
};
|
||||
} else {
|
||||
// Rejection payload
|
||||
payload = {
|
||||
baseUrl: 'https://www.thrillwiki.com',
|
||||
rejectionReason: reviewer_notes || 'No reason provided',
|
||||
entityType,
|
||||
entityName,
|
||||
entityURL,
|
||||
actualStatus: 'rejected',
|
||||
};
|
||||
}
|
||||
|
||||
// Generate idempotency key for duplicate prevention
|
||||
const { data: keyData, error: keyError } = await supabase
|
||||
.rpc('generate_notification_idempotency_key', {
|
||||
p_notification_type: `submission_${status}`,
|
||||
p_entity_id: submission_id,
|
||||
p_recipient_id: user_id,
|
||||
});
|
||||
|
||||
const idempotencyKey = keyData || `user_sub_${submission_id}_${user_id}_${status}_${Date.now()}`;
|
||||
|
||||
// Check for duplicate within 24h window
|
||||
const { data: existingLog, error: logCheckError } = await supabase
|
||||
.from('notification_logs')
|
||||
.select('id')
|
||||
.eq('user_id', user_id)
|
||||
.eq('idempotency_key', idempotencyKey)
|
||||
.gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString())
|
||||
.maybeSingle();
|
||||
|
||||
if (existingLog) {
|
||||
// Duplicate detected - log and skip
|
||||
await supabase.from('notification_logs').update({
|
||||
is_duplicate: true
|
||||
}).eq('id', existingLog.id);
|
||||
|
||||
edgeLogger.info('Duplicate notification prevented', {
|
||||
action: 'notify_user_submission_status',
|
||||
userId: user_id,
|
||||
idempotencyKey,
|
||||
submissionId: submission_id,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
|
||||
endRequest(tracking, 200);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: 'Duplicate notification prevented',
|
||||
idempotencyKey,
|
||||
requestId: tracking.requestId
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
},
|
||||
status: 200,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
edgeLogger.info('Sending notification to user', {
|
||||
action: 'notify_user_submission_status',
|
||||
userId: user_id,
|
||||
workflowId,
|
||||
// Determine workflow and build payload based on status
|
||||
const workflowId = status === 'approved' ? 'submission-approved' : 'submission-rejected';
|
||||
|
||||
let payload: Record<string, string>;
|
||||
|
||||
if (status === 'approved') {
|
||||
payload = {
|
||||
baseUrl: 'https://www.thrillwiki.com',
|
||||
entityType,
|
||||
entityName,
|
||||
status,
|
||||
idempotencyKey,
|
||||
requestId: tracking.requestId
|
||||
});
|
||||
|
||||
// Call trigger-notification function
|
||||
const { data: notificationResult, error: notificationError } = await supabase.functions.invoke(
|
||||
'trigger-notification',
|
||||
{
|
||||
body: {
|
||||
workflowId,
|
||||
subscriberId: user_id,
|
||||
payload,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (notificationError) {
|
||||
throw new Error(`Failed to trigger notification: ${notificationError.message}`);
|
||||
}
|
||||
|
||||
// Log notification in notification_logs with idempotency key
|
||||
await supabase.from('notification_logs').insert({
|
||||
user_id,
|
||||
notification_type: `submission_${status}`,
|
||||
idempotency_key: idempotencyKey,
|
||||
is_duplicate: false,
|
||||
metadata: {
|
||||
submission_id,
|
||||
submission_type,
|
||||
transaction_id: notificationResult?.transactionId
|
||||
}
|
||||
});
|
||||
|
||||
edgeLogger.info('User notification sent successfully', { action: 'notify_user_submission_status', requestId: tracking.requestId, result: notificationResult });
|
||||
|
||||
endRequest(tracking, 200);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
transactionId: notificationResult?.transactionId,
|
||||
requestId: tracking.requestId
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
},
|
||||
status: 200,
|
||||
}
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
edgeLogger.error('Error notifying user about submission status', { action: 'notify_user_submission_status', requestId: tracking.requestId, error: errorMessage });
|
||||
|
||||
endRequest(tracking, 500, errorMessage);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
requestId: tracking.requestId
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
...corsHeaders,
|
||||
'Content-Type': 'application/json',
|
||||
'X-Request-ID': tracking.requestId
|
||||
},
|
||||
status: 500,
|
||||
}
|
||||
);
|
||||
submissionId: submission_id,
|
||||
entityURL,
|
||||
moderationNotes: reviewer_notes || '',
|
||||
};
|
||||
} else {
|
||||
payload = {
|
||||
baseUrl: 'https://www.thrillwiki.com',
|
||||
rejectionReason: reviewer_notes || 'No reason provided',
|
||||
entityType,
|
||||
entityName,
|
||||
entityURL,
|
||||
actualStatus: 'rejected',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Generate idempotency key for duplicate prevention
|
||||
const { data: keyData, error: keyError } = await supabase
|
||||
.rpc('generate_notification_idempotency_key', {
|
||||
p_notification_type: `submission_${status}`,
|
||||
p_entity_id: submission_id,
|
||||
p_recipient_id: user_id,
|
||||
});
|
||||
|
||||
const idempotencyKey = keyData || `user_sub_${submission_id}_${user_id}_${status}_${Date.now()}`;
|
||||
|
||||
// Check for duplicate within 24h window
|
||||
const { data: existingLog, error: logCheckError } = await supabase
|
||||
.from('notification_logs')
|
||||
.select('id')
|
||||
.eq('user_id', user_id)
|
||||
.eq('idempotency_key', idempotencyKey)
|
||||
.gte('created_at', new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString())
|
||||
.maybeSingle();
|
||||
|
||||
if (existingLog) {
|
||||
// Duplicate detected - log and skip
|
||||
await supabase.from('notification_logs').update({
|
||||
is_duplicate: true
|
||||
}).eq('id', existingLog.id);
|
||||
|
||||
addSpanEvent(span, 'duplicate_notification_prevented', {
|
||||
idempotencyKey,
|
||||
submissionId: submission_id
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Duplicate notification prevented',
|
||||
idempotencyKey,
|
||||
};
|
||||
}
|
||||
|
||||
addSpanEvent(span, 'sending_notification', {
|
||||
workflowId,
|
||||
entityName,
|
||||
idempotencyKey
|
||||
});
|
||||
|
||||
// Call trigger-notification function
|
||||
const { data: notificationResult, error: notificationError } = await supabase.functions.invoke(
|
||||
'trigger-notification',
|
||||
{
|
||||
body: {
|
||||
workflowId,
|
||||
subscriberId: user_id,
|
||||
payload,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (notificationError) {
|
||||
throw new Error(`Failed to trigger notification: ${notificationError.message}`);
|
||||
}
|
||||
|
||||
// Log notification in notification_logs with idempotency key
|
||||
await supabase.from('notification_logs').insert({
|
||||
user_id,
|
||||
notification_type: `submission_${status}`,
|
||||
idempotency_key: idempotencyKey,
|
||||
is_duplicate: false,
|
||||
metadata: {
|
||||
submission_id,
|
||||
submission_type,
|
||||
transaction_id: notificationResult?.transactionId
|
||||
}
|
||||
});
|
||||
|
||||
addSpanEvent(span, 'notification_sent', {
|
||||
transactionId: notificationResult?.transactionId
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transactionId: notificationResult?.transactionId,
|
||||
};
|
||||
};
|
||||
|
||||
serve(createEdgeFunction({
|
||||
name: 'notify-user-submission-status',
|
||||
requireAuth: false,
|
||||
useServiceRole: true,
|
||||
corsHeaders,
|
||||
enableTracing: true,
|
||||
logRequests: true,
|
||||
}, handler));
|
||||
|
||||
Reference in New Issue
Block a user