From 38a1b12985fd445bb050ca3e6631046bbdde77c5 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Tue, 2 Jun 2026 22:47:32 +0530 Subject: [PATCH 1/3] fix(chats): exclude chats with zero messages from listing Inner-join chat_messages in selectChatsWithSessions so GET /api/chats and the get_chats MCP tool only return chats that have at least one message. The embedded count keeps the payload to a single aggregate row. Co-Authored-By: Claude Opus 4.8 --- .../chats/__tests__/selectChatsWithSessions.test.ts | 9 +++++++++ lib/supabase/chats/selectChatsWithSessions.ts | 10 +++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/supabase/chats/__tests__/selectChatsWithSessions.test.ts b/lib/supabase/chats/__tests__/selectChatsWithSessions.test.ts index 0c88d411..408ace0a 100644 --- a/lib/supabase/chats/__tests__/selectChatsWithSessions.test.ts +++ b/lib/supabase/chats/__tests__/selectChatsWithSessions.test.ts @@ -55,6 +55,8 @@ describe("selectChatsWithSessions", () => { expect(String(selectArg)).toContain("account_id"); expect(String(selectArg)).toContain("artist_id"); expect(String(selectArg)).toContain("status"); + // Inner-join on chat_messages excludes chats with zero messages. + expect(String(selectArg)).toContain("chat_messages!inner"); expect(neqMock).toHaveBeenCalledWith("session.status", "archived"); expect(inMock).toHaveBeenCalledWith("session.account_id", ["acc-1", "acc-2"]); @@ -69,6 +71,13 @@ describe("selectChatsWithSessions", () => { expect(neqMock).toHaveBeenCalledWith("session.status", "archived"); }); + it("inner-joins chat_messages so chats with zero messages are excluded", async () => { + await selectChatsWithSessions({}); + + const selectArg = String(selectMock.mock.calls[0]?.[0]); + expect(selectArg).toContain("chat_messages!inner"); + }); + it("composes accountIds + artistAccountId — both filters applied", async () => { orderMock.mockResolvedValueOnce({ data: [], error: null }); diff --git a/lib/supabase/chats/selectChatsWithSessions.ts b/lib/supabase/chats/selectChatsWithSessions.ts index ee3d204d..748f68db 100644 --- a/lib/supabase/chats/selectChatsWithSessions.ts +++ b/lib/supabase/chats/selectChatsWithSessions.ts @@ -18,7 +18,8 @@ export interface SelectChatsWithSessionsParams { const SELECT = ` *, - session:sessions!inner ( id, account_id, artist_id, status ) + session:sessions!inner ( id, account_id, artist_id, status ), + messages:chat_messages!inner ( count ) ` as const; /** @@ -27,8 +28,11 @@ const SELECT = ` * `sessions.artist_id`. Chats whose session is archived * (`sessions.status === "archived"`) are excluded — archive is the * soft-delete path, and archived sessions should not surface in chat - * listings. Results are ordered by `chats.updated_at` descending so newest - * activity surfaces first. + * listings. Chats with no messages are also excluded: the + * `chat_messages!inner` embed inner-joins messages, so a chat only surfaces + * once it has at least one message (the embedded `count` keeps the payload to + * a single aggregate row rather than every message). Results are ordered by + * `chats.updated_at` descending so newest activity surfaces first. * * Returns `null` when Supabase reports an error so callers can distinguish a * transient failure from an empty result. Row shape is inferred from the From cf0fcc0a0312e3ecd9143a557270f5dd156d4cd9 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Tue, 2 Jun 2026 22:58:22 +0530 Subject: [PATCH 2/3] fix(chats): use plain inner-join embed to exclude empty chats The embedded `count` aggregate caused PostgREST to compute via a left join + group-by, dropping the `!inner` filter so zero-message chats still returned. Switch to a plain `chat_messages!inner ( id )` embed, which produces a true inner join and excludes chats with no messages. Co-Authored-By: Claude Opus 4.8 --- lib/supabase/chats/selectChatsWithSessions.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/supabase/chats/selectChatsWithSessions.ts b/lib/supabase/chats/selectChatsWithSessions.ts index 748f68db..9c4817f9 100644 --- a/lib/supabase/chats/selectChatsWithSessions.ts +++ b/lib/supabase/chats/selectChatsWithSessions.ts @@ -19,7 +19,7 @@ export interface SelectChatsWithSessionsParams { const SELECT = ` *, session:sessions!inner ( id, account_id, artist_id, status ), - messages:chat_messages!inner ( count ) + messages:chat_messages!inner ( id ) ` as const; /** @@ -30,8 +30,7 @@ const SELECT = ` * soft-delete path, and archived sessions should not surface in chat * listings. Chats with no messages are also excluded: the * `chat_messages!inner` embed inner-joins messages, so a chat only surfaces - * once it has at least one message (the embedded `count` keeps the payload to - * a single aggregate row rather than every message). Results are ordered by + * once it has at least one message. Results are ordered by * `chats.updated_at` descending so newest activity surfaces first. * * Returns `null` when Supabase reports an error so callers can distinguish a From 93df81688ec52a2ae23db16ea5a6367419243d6f Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Wed, 3 Jun 2026 00:57:33 +0530 Subject: [PATCH 3/3] docs(chats): trim oversized comments in selectChatsWithSessions Co-Authored-By: Claude Opus 4.8 --- lib/supabase/chats/selectChatsWithSessions.ts | 30 ++++--------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/lib/supabase/chats/selectChatsWithSessions.ts b/lib/supabase/chats/selectChatsWithSessions.ts index 9c4817f9..3e5dbf35 100644 --- a/lib/supabase/chats/selectChatsWithSessions.ts +++ b/lib/supabase/chats/selectChatsWithSessions.ts @@ -1,18 +1,9 @@ import supabase from "@/lib/supabase/serverClient"; export interface SelectChatsWithSessionsParams { - /** - * Owning account IDs to filter by (via `sessions.account_id`). - * - `undefined` returns chats across all accounts (admin scope). - * - `[]` short-circuits to an empty result. - */ + /** Filter by `sessions.account_id`. `undefined` = all accounts; `[]` = empty result. */ accountIds?: string[]; - /** - * Optional artist account id to filter by (via `sessions.artist_id`). - * Composes with `accountIds`: scopes the result to chats whose owning - * session both belongs to one of the accountIds AND is in the given - * artist context. - */ + /** Filter by `sessions.artist_id`. Composes with `accountIds`. */ artistAccountId?: string; } @@ -23,20 +14,9 @@ const SELECT = ` ` as const; /** - * Reads chats joined to their owning session, optionally scoped to a set of - * account IDs through `sessions.account_id` and/or an artist context through - * `sessions.artist_id`. Chats whose session is archived - * (`sessions.status === "archived"`) are excluded — archive is the - * soft-delete path, and archived sessions should not surface in chat - * listings. Chats with no messages are also excluded: the - * `chat_messages!inner` embed inner-joins messages, so a chat only surfaces - * once it has at least one message. Results are ordered by - * `chats.updated_at` descending so newest activity surfaces first. - * - * Returns `null` when Supabase reports an error so callers can distinguish a - * transient failure from an empty result. Row shape is inferred from the - * typed supabase-js client — both `chats.*` and the embedded `session` - * projection surface to callers without an explicit type alias. + * Lists chats joined to their owning session, newest first. Excludes archived + * sessions and (via `chat_messages!inner`) chats with no messages. Returns + * `null` on error so callers can distinguish failure from an empty result. */ export async function selectChatsWithSessions(params: SelectChatsWithSessionsParams = {}) { const { accountIds, artistAccountId } = params;