-
Notifications
You must be signed in to change notification settings - Fork 5
feat: migrate memories get to /api/chats/{id}/messages #383
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
ffccf81
feat: migrate chat messages get endpoint
arpitgupta1214 7936b16
refactor: move chat messages handler from memories to chats
arpitgupta1214 68cf5c2
fix: harden chat access and memories error handling
arpitgupta1214 5b193bf
fix: move chat error handling to domain handler
arpitgupta1214 46a5c72
Merge remote-tracking branch 'origin/test' into codex/arpit-migrate-m…
sweetmantech d61455a
refactor: extract validateGetChatMessagesQuery per project conventions
sweetmantech 21923d7
fix: format validateGetChatMessagesQuery and handler
sweetmantech File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import type { NextRequest } from "next/server"; | ||
| import { NextResponse } from "next/server"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { getChatMessagesHandler } from "@/lib/chats/getChatMessagesHandler"; | ||
|
|
||
| /** | ||
| * OPTIONS handler for CORS preflight requests. | ||
| * | ||
| * @returns Empty response with CORS headers. | ||
| */ | ||
| export async function OPTIONS(): Promise<NextResponse> { | ||
| return new NextResponse(null, { | ||
| status: 200, | ||
| headers: getCorsHeaders(), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * GET /api/chats/[id]/messages | ||
| * | ||
| * Returns memories (messages) for a chat in chronological order. | ||
| * | ||
| * @param request - Incoming request. | ||
| * @param context - Next.js route context. | ||
| * @param context.params - Async route params containing chat id. | ||
| * @returns JSON response with chat messages. | ||
| */ | ||
| export async function GET( | ||
| request: NextRequest, | ||
| context: { params: Promise<{ id: string }> }, | ||
| ): Promise<NextResponse> { | ||
| const { params } = context; | ||
| const { id } = await params; | ||
| return getChatMessagesHandler(request, id); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { getChatMessagesHandler } from "@/lib/chats/getChatMessagesHandler"; | ||
| import { validateGetChatMessagesQuery } from "@/lib/chats/validateGetChatMessagesQuery"; | ||
| import selectMemories from "@/lib/supabase/memories/selectMemories"; | ||
|
|
||
| vi.mock("@/lib/chats/validateGetChatMessagesQuery", () => ({ | ||
| validateGetChatMessagesQuery: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock("@/lib/supabase/memories/selectMemories", () => ({ | ||
| default: vi.fn(), | ||
| })); | ||
|
|
||
| const createRequest = (roomId?: string) => | ||
| new NextRequest(`http://localhost/api/chats/${roomId ?? "missing"}/messages`); | ||
|
|
||
| describe("getChatMessagesHandler", () => { | ||
| const roomId = "123e4567-e89b-42d3-a456-426614174000"; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it("returns 400 when validation fails", async () => { | ||
| vi.mocked(validateGetChatMessagesQuery).mockResolvedValue( | ||
| NextResponse.json({ status: "error", error: "id must be a valid UUID" }, { status: 400 }), | ||
| ); | ||
|
|
||
| const response = await getChatMessagesHandler(createRequest(), "invalid-id"); | ||
| const body = await response.json(); | ||
|
|
||
| expect(response.status).toBe(400); | ||
| expect(body).toEqual({ | ||
| status: "error", | ||
| error: "id must be a valid UUID", | ||
| }); | ||
| }); | ||
|
|
||
| it("returns room access error when validation fails", async () => { | ||
| vi.mocked(validateGetChatMessagesQuery).mockResolvedValue( | ||
| NextResponse.json({ status: "error", error: "Unauthorized" }, { status: 401 }), | ||
| ); | ||
|
|
||
| const response = await getChatMessagesHandler(createRequest(roomId), roomId); | ||
| expect(response.status).toBe(401); | ||
| }); | ||
|
|
||
| it("returns 500 when memories query fails", async () => { | ||
| vi.mocked(validateGetChatMessagesQuery).mockResolvedValue({ | ||
| room: { | ||
| id: roomId, | ||
| account_id: null, | ||
| artist_id: null, | ||
| topic: null, | ||
| updated_at: null, | ||
| }, | ||
| accountId: "11111111-1111-1111-1111-111111111111", | ||
| }); | ||
| vi.mocked(selectMemories).mockResolvedValue(null); | ||
|
|
||
| const response = await getChatMessagesHandler(createRequest(roomId), roomId); | ||
| const body = await response.json(); | ||
|
|
||
| expect(response.status).toBe(500); | ||
| expect(body).toEqual({ | ||
| status: "error", | ||
| error: "Failed to retrieve memories", | ||
| }); | ||
| }); | ||
|
|
||
| it("returns memories for an accessible room", async () => { | ||
| vi.mocked(validateGetChatMessagesQuery).mockResolvedValue({ | ||
| room: { | ||
| id: roomId, | ||
| account_id: null, | ||
| artist_id: null, | ||
| topic: null, | ||
| updated_at: null, | ||
| }, | ||
| accountId: "11111111-1111-1111-1111-111111111111", | ||
| }); | ||
| vi.mocked(selectMemories).mockResolvedValue([ | ||
| { | ||
| id: "123e4567-e89b-42d3-a456-426614174111", | ||
| room_id: roomId, | ||
| content: { role: "user", content: "hello" }, | ||
| updated_at: "2026-03-31T00:00:00.000Z", | ||
| }, | ||
| ]); | ||
|
|
||
| const response = await getChatMessagesHandler(createRequest(roomId), roomId); | ||
| const body = await response.json(); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(body).toEqual({ | ||
| data: [ | ||
| { | ||
| id: "123e4567-e89b-42d3-a456-426614174111", | ||
| room_id: roomId, | ||
| content: { role: "user", content: "hello" }, | ||
| updated_at: "2026-03-31T00:00:00.000Z", | ||
| }, | ||
| ], | ||
| }); | ||
| }); | ||
|
|
||
| it("returns 500 when validateGetChatMessagesQuery throws unexpectedly", async () => { | ||
| vi.mocked(validateGetChatMessagesQuery).mockRejectedValue(new Error("boom")); | ||
|
|
||
| const response = await getChatMessagesHandler(createRequest(roomId), roomId); | ||
| const body = await response.json(); | ||
|
|
||
| expect(response.status).toBe(500); | ||
| expect(body).toEqual({ | ||
| status: "error", | ||
| error: "Failed to retrieve memories", | ||
| }); | ||
| }); | ||
|
|
||
| it("returns 500 when selectMemories throws unexpectedly", async () => { | ||
| vi.mocked(validateGetChatMessagesQuery).mockResolvedValue({ | ||
| room: { | ||
| id: roomId, | ||
| account_id: null, | ||
| artist_id: null, | ||
| topic: null, | ||
| updated_at: null, | ||
| }, | ||
| accountId: "11111111-1111-1111-1111-111111111111", | ||
| }); | ||
| vi.mocked(selectMemories).mockRejectedValue(new Error("db blew up")); | ||
|
|
||
| const response = await getChatMessagesHandler(createRequest(roomId), roomId); | ||
| const body = await response.json(); | ||
|
|
||
| expect(response.status).toBe(500); | ||
| expect(body).toEqual({ | ||
| status: "error", | ||
| error: "Failed to retrieve memories", | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import { validateGetChatMessagesQuery } from "@/lib/chats/validateGetChatMessagesQuery"; | ||
|
|
||
| vi.mock("@/lib/chats/validateChatAccess", () => ({ | ||
| validateChatAccess: vi.fn(), | ||
| })); | ||
|
|
||
| const { validateChatAccess } = await import("@/lib/chats/validateChatAccess"); | ||
|
|
||
| const createRequest = () => new NextRequest("http://localhost/api/chats/test/messages"); | ||
|
|
||
| describe("validateGetChatMessagesQuery", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| it("returns 400 for invalid UUID", async () => { | ||
| const result = await validateGetChatMessagesQuery(createRequest(), "not-a-uuid"); | ||
|
|
||
| expect(result).toBeInstanceOf(NextResponse); | ||
| expect((result as NextResponse).status).toBe(400); | ||
| }); | ||
|
|
||
| it("returns NextResponse when chat access fails", async () => { | ||
| vi.mocked(validateChatAccess).mockResolvedValue( | ||
| NextResponse.json({ status: "error" }, { status: 403 }), | ||
| ); | ||
|
|
||
| const result = await validateGetChatMessagesQuery( | ||
| createRequest(), | ||
| "123e4567-e89b-42d3-a456-426614174000", | ||
| ); | ||
|
|
||
| expect(result).toBeInstanceOf(NextResponse); | ||
| expect((result as NextResponse).status).toBe(403); | ||
| }); | ||
|
|
||
| it("returns validated room data on success", async () => { | ||
| const roomId = "123e4567-e89b-42d3-a456-426614174000"; | ||
| vi.mocked(validateChatAccess).mockResolvedValue({ | ||
| roomId, | ||
| room: { id: roomId, account_id: null, artist_id: null, topic: null, updated_at: null }, | ||
| accountId: "acc-123", | ||
| }); | ||
|
|
||
| const result = await validateGetChatMessagesQuery(createRequest(), roomId); | ||
|
|
||
| expect(result).not.toBeInstanceOf(NextResponse); | ||
| expect((result as { roomId: string }).roomId).toBe(roomId); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import type { NextRequest } from "next/server"; | ||
| import { NextResponse } from "next/server"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { validateGetChatMessagesQuery } from "@/lib/chats/validateGetChatMessagesQuery"; | ||
| import selectMemories from "@/lib/supabase/memories/selectMemories"; | ||
|
|
||
| /** | ||
| * Handles GET /api/chats/[id]/messages. | ||
| * | ||
| * Returns all messages for a chat in ascending order by `updated_at`. | ||
| * | ||
| * @param request - Incoming request used to validate chat access. | ||
| * @param id - Chat identifier from route params. | ||
| * @returns JSON response containing ordered chat messages. | ||
| */ | ||
| export async function getChatMessagesHandler( | ||
| request: NextRequest, | ||
| id: string, | ||
| ): Promise<NextResponse> { | ||
| try { | ||
| const validated = await validateGetChatMessagesQuery(request, id); | ||
| if (validated instanceof NextResponse) { | ||
| return validated; | ||
| } | ||
|
|
||
| const memories = await selectMemories(validated.room.id, { ascending: true }); | ||
| if (memories === null) { | ||
| return NextResponse.json( | ||
| { status: "error", error: "Failed to retrieve memories" }, | ||
| { status: 500, headers: getCorsHeaders() }, | ||
| ); | ||
| } | ||
|
|
||
| return NextResponse.json({ data: memories }, { status: 200, headers: getCorsHeaders() }); | ||
| } catch (error) { | ||
| console.error("Unexpected error in getChatMessagesHandler:", error); | ||
| return NextResponse.json( | ||
| { status: "error", error: "Failed to retrieve memories" }, | ||
| { status: 500, headers: getCorsHeaders() }, | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import type { NextRequest } from "next/server"; | ||
| import { NextResponse } from "next/server"; | ||
| import { z } from "zod"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { validateChatAccess, type ValidatedChatAccess } from "@/lib/chats/validateChatAccess"; | ||
|
|
||
| const chatIdSchema = z.string().uuid("id must be a valid UUID"); | ||
|
|
||
| /** | ||
| * Validates auth and params for GET /api/chats/[id]/messages. | ||
| * | ||
| * @param request - Incoming request used to validate chat access. | ||
| * @param id - Chat identifier from route params. | ||
| * @returns NextResponse on failure, or validated chat access data. | ||
| */ | ||
| export async function validateGetChatMessagesQuery( | ||
| request: NextRequest, | ||
| id: string, | ||
| ): Promise<NextResponse | ValidatedChatAccess> { | ||
| const parsedId = chatIdSchema.safeParse(id); | ||
| if (!parsedId.success) { | ||
| return NextResponse.json( | ||
| { | ||
| status: "error", | ||
| error: parsedId.error.issues[0]?.message || "Invalid chat ID", | ||
| }, | ||
| { status: 400, headers: getCorsHeaders() }, | ||
| ); | ||
| } | ||
|
|
||
| return validateChatAccess(request, parsedId.data); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.