From 5a56267cf382cbbc2087fbe32ec86ddc48ed5214 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Sat, 30 May 2026 03:16:21 +0530 Subject: [PATCH 1/2] feat(sessions): accept artistId on POST /api/sessions Threads optional artistId through the session-create flow and exposes it on session reads. - validateCreateSessionBody accepts artistId (UUID, optional). - buildSessionInsertRow writes artist_id from artistId (defaults to null). - createSessionHandler forwards the validated artistId into the insert. - toSessionResponse exposes artistId on the camelCase response. - types/database.types.ts: add artist_id to sessions Row/Insert/Update + sessions_artist_id_fkey relationship (mirrors what supabase gen would emit after database#27 applies). Anyone who regenerates types post-migration should see no diff. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../[sessionId]/__tests__/route.test.ts | 1 + lib/sessions/__tests__/baseSessionRow.ts | 1 + .../__tests__/buildSessionInsertRow.test.ts | 19 ++++++++++++++ .../createSessionHandler.persistence.test.ts | 25 +++++++++++++++++++ .../validateCreateSessionBody.test.ts | 23 +++++++++++++++++ lib/sessions/buildSessionInsertRow.ts | 11 ++++++-- lib/sessions/createSessionHandler.ts | 1 + lib/sessions/toSessionResponse.ts | 1 + lib/sessions/validateCreateSessionBody.ts | 1 + types/database.types.ts | 10 ++++++++ 10 files changed, 91 insertions(+), 2 deletions(-) diff --git a/app/api/sessions/[sessionId]/__tests__/route.test.ts b/app/api/sessions/[sessionId]/__tests__/route.test.ts index 17afe983e..252ed4cb5 100644 --- a/app/api/sessions/[sessionId]/__tests__/route.test.ts +++ b/app/api/sessions/[sessionId]/__tests__/route.test.ts @@ -63,6 +63,7 @@ function makePatchReqRaw( const mockRow: SessionRow = { id: "sess_1", account_id: "acc-uuid-1", + artist_id: null, title: "Test session", status: "running", repo_owner: "acme", diff --git a/lib/sessions/__tests__/baseSessionRow.ts b/lib/sessions/__tests__/baseSessionRow.ts index 8c5fe8095..0f40d0152 100644 --- a/lib/sessions/__tests__/baseSessionRow.ts +++ b/lib/sessions/__tests__/baseSessionRow.ts @@ -9,6 +9,7 @@ export function baseSessionRow(overrides: Partial> = {}): Tab return { id: "sess_1", account_id: "acc-uuid-1", + artist_id: null, title: "Test session", status: "running", repo_owner: null, diff --git a/lib/sessions/__tests__/buildSessionInsertRow.test.ts b/lib/sessions/__tests__/buildSessionInsertRow.test.ts index 2b75c679a..0e2ac881b 100644 --- a/lib/sessions/__tests__/buildSessionInsertRow.test.ts +++ b/lib/sessions/__tests__/buildSessionInsertRow.test.ts @@ -47,4 +47,23 @@ describe("buildSessionInsertRow", () => { }); expect(row.sandbox_state).toEqual({ type: "vercel" }); }); + + it("writes artist_id when artistId is provided", () => { + const row = buildSessionInsertRow({ + accountId: "acc-1", + title: "Berlin", + cloneUrl: DEFAULT_CLONE, + artistId: "artist-uuid-1", + }); + expect(row.artist_id).toBe("artist-uuid-1"); + }); + + it("sets artist_id to null when artistId is omitted", () => { + const row = buildSessionInsertRow({ + accountId: "acc-1", + title: "Berlin", + cloneUrl: DEFAULT_CLONE, + }); + expect(row.artist_id).toBeNull(); + }); }); diff --git a/lib/sessions/__tests__/createSessionHandler.persistence.test.ts b/lib/sessions/__tests__/createSessionHandler.persistence.test.ts index cf54c9eff..043b65e6e 100644 --- a/lib/sessions/__tests__/createSessionHandler.persistence.test.ts +++ b/lib/sessions/__tests__/createSessionHandler.persistence.test.ts @@ -107,6 +107,31 @@ describe("createSessionHandler — persistence", () => { expect(vi.mocked(insertSession).mock.calls[0][0].title).toBe("Hello world"); }); + it("writes artist_id when body carries artistId, and exposes it on the response", async () => { + const artistId = "a25c5dc5-3eb2-4fff-9a5e-39e90c9d4f02"; + vi.mocked(validateCreateSessionBody).mockResolvedValue(okValidated({ body: { artistId } })); + vi.mocked(insertSession).mockResolvedValue(baseSessionRow({ artist_id: artistId })); + vi.mocked(insertChat).mockResolvedValue(baseChatRow()); + + const res = await createSessionHandler(makeCreateSessionReq({ artistId })); + expect(res.status).toBe(200); + + expect(vi.mocked(insertSession).mock.calls[0][0].artist_id).toBe(artistId); + + const body = (await res.json()) as { session: { artistId: string | null } }; + expect(body.session.artistId).toBe(artistId); + }); + + it("writes artist_id as null when artistId is omitted", async () => { + vi.mocked(validateCreateSessionBody).mockResolvedValue(okValidated()); + vi.mocked(insertSession).mockResolvedValue(baseSessionRow()); + vi.mocked(insertChat).mockResolvedValue(baseChatRow()); + + await createSessionHandler(makeCreateSessionReq({})); + + expect(vi.mocked(insertSession).mock.calls[0][0].artist_id).toBeNull(); + }); + it("returns 500 when insertSession fails", async () => { vi.mocked(validateCreateSessionBody).mockResolvedValue(okValidated()); vi.mocked(insertSession).mockResolvedValue(null); diff --git a/lib/sessions/__tests__/validateCreateSessionBody.test.ts b/lib/sessions/__tests__/validateCreateSessionBody.test.ts index 85491c44d..39fc1c42b 100644 --- a/lib/sessions/__tests__/validateCreateSessionBody.test.ts +++ b/lib/sessions/__tests__/validateCreateSessionBody.test.ts @@ -74,4 +74,27 @@ describe("validateCreateSessionBody", () => { expect.objectContaining({ organizationId: orgId }), ); }); + + it("accepts a valid artistId and surfaces it on the validated body", async () => { + vi.mocked(validateAuthContext).mockResolvedValue(okAuth); + + const artistId = "a25c5dc5-3eb2-4fff-9a5e-39e90c9d4f02"; + const result = await validateCreateSessionBody(req({ artistId })); + expect(result).not.toBeInstanceOf(NextResponse); + if (!(result instanceof NextResponse)) { + expect(result.body.artistId).toBe(artistId); + } + }); + + it("rejects a non-UUID artistId with 400", async () => { + vi.mocked(validateAuthContext).mockResolvedValue(okAuth); + + const result = await validateCreateSessionBody(req({ artistId: "not-a-uuid" })); + expect(result).toBeInstanceOf(NextResponse); + if (result instanceof NextResponse) { + expect(result.status).toBe(400); + const body = (await result.json()) as { status: string; error: string }; + expect(body.error).toMatch(/UUID/i); + } + }); }); diff --git a/lib/sessions/buildSessionInsertRow.ts b/lib/sessions/buildSessionInsertRow.ts index 57402034a..c37f84722 100644 --- a/lib/sessions/buildSessionInsertRow.ts +++ b/lib/sessions/buildSessionInsertRow.ts @@ -10,6 +10,12 @@ interface BuildSessionInsertRowInput { * `cloneUrl`. */ cloneUrl: string; + /** + * Optional artist account this session belongs to. Backs the chat + * sidebar's artist filter; omitted for sessions not tied to an + * artist context. + */ + artistId?: string; } /** @@ -18,14 +24,15 @@ interface BuildSessionInsertRowInput { * the default / null-coalescing rules so the handler can stay focused * on HTTP and persistence flow. * - * @param input - The validated body, owning account id, resolved title, and resolved clone URL. + * @param input - The validated body, owning account id, resolved title, resolved clone URL, and optional artist id. * @returns A row ready to pass to `insertSession`. */ export function buildSessionInsertRow(input: BuildSessionInsertRowInput): TablesInsert<"sessions"> { - const { accountId, title, cloneUrl } = input; + const { accountId, title, cloneUrl, artistId } = input; return { id: generateUUID(), account_id: accountId, + artist_id: artistId ?? null, title, status: "running", branch: null, diff --git a/lib/sessions/createSessionHandler.ts b/lib/sessions/createSessionHandler.ts index 87e81066b..e48020537 100644 --- a/lib/sessions/createSessionHandler.ts +++ b/lib/sessions/createSessionHandler.ts @@ -58,6 +58,7 @@ export async function createSessionHandler(request: NextRequest): Promise) { return { id: row.id, userId: row.account_id, + artistId: row.artist_id, title: row.title, status: row.status, repoOwner: row.repo_owner, diff --git a/lib/sessions/validateCreateSessionBody.ts b/lib/sessions/validateCreateSessionBody.ts index d9aa07175..0b6059b68 100644 --- a/lib/sessions/validateCreateSessionBody.ts +++ b/lib/sessions/validateCreateSessionBody.ts @@ -21,6 +21,7 @@ import type { AuthContext } from "@/lib/auth/validateAuthContext"; export const createSessionBodySchema = z.object({ title: z.string().optional(), organizationId: z.string().uuid("organizationId must be a valid UUID").optional(), + artistId: z.string().uuid("artistId must be a valid UUID").optional(), }); export type CreateSessionBody = z.infer; diff --git a/types/database.types.ts b/types/database.types.ts index cc7d03e1e..191472132 100644 --- a/types/database.types.ts +++ b/types/database.types.ts @@ -2878,6 +2878,7 @@ export type Database = { sessions: { Row: { account_id: string; + artist_id: string | null; branch: string | null; cached_diff: Json | null; cached_diff_updated_at: string | null; @@ -2907,6 +2908,7 @@ export type Database = { }; Insert: { account_id: string; + artist_id?: string | null; branch?: string | null; cached_diff?: Json | null; cached_diff_updated_at?: string | null; @@ -2936,6 +2938,7 @@ export type Database = { }; Update: { account_id?: string; + artist_id?: string | null; branch?: string | null; cached_diff?: Json | null; cached_diff_updated_at?: string | null; @@ -2971,6 +2974,13 @@ export type Database = { referencedRelation: "accounts"; referencedColumns: ["id"]; }, + { + foreignKeyName: "sessions_artist_id_fkey"; + columns: ["artist_id"]; + isOneToOne: false; + referencedRelation: "accounts"; + referencedColumns: ["id"]; + }, ]; }; social_fans: { From d8f83c3b3224a2d6a26696b768b67a1abc2229d0 Mon Sep 17 00:00:00 2001 From: Arpit Gupta Date: Sat, 30 May 2026 03:21:42 +0530 Subject: [PATCH 2/2] fix(test): include artistId in GET /api/sessions/{sid} happy-path shape The existing test used toEqual against the full camelCase session shape; adding artistId to toSessionResponse required the expected object to carry it too. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/api/sessions/[sessionId]/__tests__/route.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/sessions/[sessionId]/__tests__/route.test.ts b/app/api/sessions/[sessionId]/__tests__/route.test.ts index 252ed4cb5..e75ba6961 100644 --- a/app/api/sessions/[sessionId]/__tests__/route.test.ts +++ b/app/api/sessions/[sessionId]/__tests__/route.test.ts @@ -187,6 +187,7 @@ describe("GET /api/sessions/[sessionId]", () => { session: { id: "sess_1", userId: "acc-uuid-1", + artistId: null, title: "Test session", status: "running", repoOwner: "acme",