diff --git a/app/api/chats/[id]/segment/route.ts b/app/api/chats/[id]/segment/route.ts new file mode 100644 index 00000000..f0bbdb72 --- /dev/null +++ b/app/api/chats/[id]/segment/route.ts @@ -0,0 +1,35 @@ +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { getChatSegmentHandler } from "@/lib/chats/getChatSegmentHandler"; + +/** + * OPTIONS handler for CORS preflight requests. + * + * @returns A NextResponse with CORS headers. + */ +export async function OPTIONS() { + return new NextResponse(null, { + status: 200, + headers: getCorsHeaders(), + }); +} + +/** + * GET /api/chats/[id]/segment + * + * Retrieves the segment associated with a chat room. + * Returns 404 when the room does not exist or is not accessible by the caller. + * + * @param request - The incoming request object. + * @param root0 - The route context object. + * @param root0.params - The dynamic route params containing chat id. + * @returns A NextResponse with segment linkage data or an error. + */ +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +): Promise { + const { id } = await params; + return getChatSegmentHandler(request, id); +} diff --git a/lib/chats/__tests__/getChatSegmentHandler.test.ts b/lib/chats/__tests__/getChatSegmentHandler.test.ts new file mode 100644 index 00000000..18cfb92c --- /dev/null +++ b/lib/chats/__tests__/getChatSegmentHandler.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { NextRequest, NextResponse } from "next/server"; +import { getChatSegmentHandler } from "@/lib/chats/getChatSegmentHandler"; +import { validateChatAccess } from "@/lib/chats/validateChatAccess"; +import { selectSegmentRoomByRoomId } from "@/lib/supabase/segment_rooms/selectSegmentRoomByRoomId"; + +vi.mock("@/lib/chats/validateChatAccess", () => ({ + validateChatAccess: vi.fn(), +})); + +vi.mock("@/lib/supabase/segment_rooms/selectSegmentRoomByRoomId", () => ({ + selectSegmentRoomByRoomId: vi.fn(), +})); + +const createRequest = () => new NextRequest("http://localhost/api/chats/chat-id/segment"); + +describe("getChatSegmentHandler", () => { + const roomId = "123e4567-e89b-42d3-a456-426614174000"; + const segmentId = "123e4567-e89b-42d3-a456-426614174003"; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns validation/auth response from resolver", async () => { + vi.mocked(validateChatAccess).mockResolvedValue( + NextResponse.json({ status: "error", error: "Unauthorized" }, { status: 401 }), + ); + + const response = await getChatSegmentHandler(createRequest(), roomId); + const body = await response.json(); + + expect(response.status).toBe(401); + expect(body).toEqual({ + status: "error", + error: "Unauthorized", + }); + expect(selectSegmentRoomByRoomId).not.toHaveBeenCalled(); + }); + + it("returns linked segment when segment_room exists", async () => { + vi.mocked(validateChatAccess).mockResolvedValue({ + room: { + id: roomId, + account_id: "11111111-1111-1111-1111-111111111111", + artist_id: null, + topic: "Test", + updated_at: null, + }, + accountId: "11111111-1111-1111-1111-111111111111", + }); + vi.mocked(selectSegmentRoomByRoomId).mockResolvedValue({ + room_id: roomId, + segment_id: segmentId, + id: "123e4567-e89b-42d3-a456-426614174004", + created_at: null, + updated_at: null, + }); + + const response = await getChatSegmentHandler(createRequest(), roomId); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ + status: "success", + room_id: roomId, + segment_id: segmentId, + segment_exists: true, + }); + }); + + it("returns null segment when no segment_room exists", async () => { + vi.mocked(validateChatAccess).mockResolvedValue({ + room: { + id: roomId, + account_id: "11111111-1111-1111-1111-111111111111", + artist_id: null, + topic: "Test", + updated_at: null, + }, + accountId: "11111111-1111-1111-1111-111111111111", + }); + vi.mocked(selectSegmentRoomByRoomId).mockResolvedValue(null); + + const response = await getChatSegmentHandler(createRequest(), roomId); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ + status: "success", + room_id: roomId, + segment_id: null, + segment_exists: false, + }); + }); +}); diff --git a/lib/chats/getChatSegmentHandler.ts b/lib/chats/getChatSegmentHandler.ts new file mode 100644 index 00000000..ab3d8518 --- /dev/null +++ b/lib/chats/getChatSegmentHandler.ts @@ -0,0 +1,36 @@ +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; +import { validateChatAccess } from "@/lib/chats/validateChatAccess"; +import { selectSegmentRoomByRoomId } from "@/lib/supabase/segment_rooms/selectSegmentRoomByRoomId"; + +/** + * Handles GET /api/chats/[id]/segment. + * + * Returns the segment associated with a chat room if one exists. + * + * @param request - The incoming request object for auth context. + * @param id - The chat room ID from route params. + * @returns A NextResponse with segment linkage data or an error. + */ +export async function getChatSegmentHandler( + request: NextRequest, + id: string, +): Promise { + const roomResult = await validateChatAccess(request, id); + if (roomResult instanceof NextResponse) { + return roomResult; + } + + const segmentRoom = await selectSegmentRoomByRoomId(roomResult.room.id); + + return NextResponse.json( + { + status: "success", + room_id: roomResult.room.id, + segment_id: segmentRoom?.segment_id || null, + segment_exists: Boolean(segmentRoom?.segment_id), + }, + { status: 200, headers: getCorsHeaders() }, + ); +} diff --git a/lib/supabase/segment_rooms/selectSegmentRoomByRoomId.ts b/lib/supabase/segment_rooms/selectSegmentRoomByRoomId.ts new file mode 100644 index 00000000..b0d7bee2 --- /dev/null +++ b/lib/supabase/segment_rooms/selectSegmentRoomByRoomId.ts @@ -0,0 +1,25 @@ +import supabase from "@/lib/supabase/serverClient"; +import type { Tables } from "@/types/database.types"; + +/** + * Retrieves a segment_rooms row by room_id. + * + * @param roomId - The chat room ID. + * @returns The first matching segment_rooms row or null if none exists. + */ +export async function selectSegmentRoomByRoomId( + roomId: string, +): Promise | null> { + const { data, error } = await supabase + .from("segment_rooms") + .select("*") + .eq("room_id", roomId) + .maybeSingle(); + + if (error) { + console.error("[ERROR] selectSegmentRoomByRoomId:", error); + throw error; + } + + return data; +}