/ server / api / external / batch.post.ts
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  });