batch.post.ts
1 import { PrismaClient } from "@prisma/client"; 2 import { H3Event } from "h3"; 3 import { z } from "zod"; 4 import { handleApiError } from "~~/server/utils/logging"; 5 6 const prisma = new PrismaClient({ 7 log: ["warn", "error"], 8 }); 9 10 const apiKeySchema = z.string().uuid(); 11 12 const heartbeatSchema = z.object({ 13 timestamp: z.string().datetime().or(z.number()), 14 project: z.string().min(1).max(255), 15 language: z.string().min(1).max(50), 16 editor: z.string().min(1).max(50), 17 os: z.string().min(1).max(50), 18 branch: z.string().max(255).optional(), 19 file: z.string().max(255), 20 }); 21 22 const batchSchema = z.array(heartbeatSchema).min(1).max(2000); 23 24 export default defineEventHandler(async (event: H3Event) => { 25 try { 26 const authHeader = getHeader(event, "authorization"); 27 if (!authHeader || !authHeader.startsWith("Bearer ")) { 28 throw handleApiError( 29 401, 30 "Batch API error: Missing or invalid API key format in header." 31 ); 32 } 33 34 const apiKey = authHeader.substring(7); 35 const validationResult = apiKeySchema.safeParse(apiKey); 36 37 if (!validationResult.success) { 38 throw handleApiError( 39 401, 40 `Batch API error: Invalid API key format. Key: ${apiKey.substring(0, 4)}...` 41 ); 42 } 43 44 const user = await prisma.user.findUnique({ 45 where: { apiKey }, 46 select: { id: true, apiKey: true }, 47 }); 48 49 if (!user || user.apiKey !== apiKey) { 50 throw handleApiError( 51 401, 52 `Batch API error: Invalid API key. Key: ${apiKey.substring(0, 4)}...` 53 ); 54 } 55 56 const body = await readBody(event); 57 const heartbeats = batchSchema.parse(body); 58 59 const heartbeatsData = heartbeats.map((heartbeat) => { 60 const timestamp = 61 typeof heartbeat.timestamp === "number" 62 ? BigInt(heartbeat.timestamp) 63 : BigInt(new Date(heartbeat.timestamp).getTime()); 64 65 return { 66 userId: user.id, 67 timestamp, 68 project: heartbeat.project, 69 language: heartbeat.language, 70 editor: heartbeat.editor, 71 os: heartbeat.os, 72 branch: heartbeat.branch, 73 file: heartbeat.file, 74 }; 75 }); 76 77 const result = await prisma.heartbeats.createMany({ 78 data: heartbeatsData, 79 skipDuplicates: true, 80 }); 81 82 return { 83 success: true, 84 count: result.count, 85 }; 86 } catch (error: any) { 87 if (error && typeof error === "object" && error.statusCode) throw error; 88 if (error instanceof z.ZodError) { 89 throw handleApiError( 90 400, 91 `Batch API error: Validation error. Details: ${error.errors[0].message}` 92 ); 93 } 94 const detailedMessage = 95 error instanceof Error 96 ? error.message 97 : "An unknown error occurred processing batch heartbeats."; 98 const apiKeyPrefix = 99 getHeader(event, "authorization")?.substring(7, 11) || "UNKNOWN"; 100 throw handleApiError( 101 500, 102 `Batch API error: Failed to process heartbeats. API Key prefix: ${apiKeyPrefix}... Error: ${detailedMessage}`, 103 "Failed to process your request." 104 ); 105 } 106 });