Skip to content

Commit dec68d0

Browse files
sweetmantechclaude
andauthored
Test (#105)
* feat: make room_id required in send_email tool The room_id parameter is now required to ensure all outbound emails include the chat link footer, enabling email thread continuity. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add POST /api/chats endpoint for creating chat rooms - Add createChatHandler in lib/chats/ - Add POST route at app/api/chats/ - Account ID inferred from API key - Optional artistId and chatId params - chatId auto-generated if not provided Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add input validation pattern to CLAUDE.md and chats endpoint - Document validate function pattern using Zod in CLAUDE.md - Add validateCreateChatBody.ts for POST /api/chats - Update createChatHandler to use the validate function Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Remove try-catch around request.json(), follow existing pattern Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add safeParseJson utility for optional request bodies - Create safeParseJson helper that returns {} if body is empty/invalid - Use in createChatHandler so body is not required - All params are optional, so empty body should work Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b90ec3a commit dec68d0

File tree

6 files changed

+222
-2
lines changed

6 files changed

+222
-2
lines changed

CLAUDE.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,71 @@ pnpm format:check # Check formatting
5959
- All API routes should have JSDoc comments
6060
- Run `pnpm lint` before committing
6161

62+
## Input Validation
63+
64+
All API endpoints should use a **validate function** for input parsing. Use Zod for schema validation.
65+
66+
### Pattern
67+
68+
Create a `validate<EndpointName>Body.ts` or `validate<EndpointName>Query.ts` file:
69+
70+
```typescript
71+
import { NextResponse } from "next/server";
72+
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
73+
import { z } from "zod";
74+
75+
// Define the schema
76+
export const createExampleBodySchema = z.object({
77+
name: z.string({ message: "name is required" }).min(1, "name cannot be empty"),
78+
id: z.string().uuid("id must be a valid UUID").optional(),
79+
});
80+
81+
// Export the inferred type
82+
export type CreateExampleBody = z.infer<typeof createExampleBodySchema>;
83+
84+
/**
85+
* Validates request body for POST /api/example.
86+
*
87+
* @param body - The request body
88+
* @returns A NextResponse with an error if validation fails, or the validated body if validation passes.
89+
*/
90+
export function validateCreateExampleBody(body: unknown): NextResponse | CreateExampleBody {
91+
const result = createExampleBodySchema.safeParse(body);
92+
93+
if (!result.success) {
94+
const firstError = result.error.issues[0];
95+
return NextResponse.json(
96+
{
97+
status: "error",
98+
missing_fields: firstError.path,
99+
error: firstError.message,
100+
},
101+
{
102+
status: 400,
103+
headers: getCorsHeaders(),
104+
},
105+
);
106+
}
107+
108+
return result.data;
109+
}
110+
```
111+
112+
### Usage in Handler
113+
114+
```typescript
115+
const validated = validateCreateExampleBody(body);
116+
if (validated instanceof NextResponse) {
117+
return validated;
118+
}
119+
// validated is now typed as CreateExampleBody
120+
```
121+
122+
### Naming Convention
123+
124+
- `validate<Name>Body.ts` - For POST/PUT request bodies
125+
- `validate<Name>Query.ts` - For GET query parameters
126+
62127
## Constants (`lib/const.ts`)
63128

64129
All shared constants live in `lib/const.ts`:

app/api/chats/route.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { NextRequest } from "next/server";
2+
import { NextResponse } from "next/server";
3+
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
4+
import { createChatHandler } from "@/lib/chats/createChatHandler";
5+
6+
/**
7+
* OPTIONS handler for CORS preflight requests.
8+
*
9+
* @returns A NextResponse with CORS headers.
10+
*/
11+
export async function OPTIONS() {
12+
return new NextResponse(null, {
13+
status: 200,
14+
headers: getCorsHeaders(),
15+
});
16+
}
17+
18+
/**
19+
* POST /api/chats
20+
*
21+
* Create a new chat room.
22+
*
23+
* Authentication: x-api-key header required.
24+
* The account ID is inferred from the API key.
25+
*
26+
* Optional body parameters:
27+
* - artistId: UUID of the artist account the chat is associated with
28+
* - chatId: UUID for the new chat (auto-generated if not provided)
29+
*
30+
* @param request - The request object
31+
* @returns A NextResponse with the created chat or an error
32+
*/
33+
export async function POST(request: NextRequest): Promise<NextResponse> {
34+
return createChatHandler(request);
35+
}

lib/chats/createChatHandler.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
3+
import { getApiKeyAccountId } from "@/lib/auth/getApiKeyAccountId";
4+
import { insertRoom } from "@/lib/supabase/rooms/insertRoom";
5+
import { generateUUID } from "@/lib/uuid/generateUUID";
6+
import { validateCreateChatBody } from "@/lib/chats/validateCreateChatBody";
7+
import { safeParseJson } from "@/lib/networking/safeParseJson";
8+
9+
/**
10+
* Handler for creating a new chat room.
11+
*
12+
* Requires authentication via x-api-key header.
13+
* The account ID is inferred from the API key.
14+
*
15+
* @param request - The NextRequest object
16+
* @returns A NextResponse with the created chat or an error
17+
*/
18+
export async function createChatHandler(request: NextRequest): Promise<NextResponse> {
19+
try {
20+
const accountIdOrError = await getApiKeyAccountId(request);
21+
if (accountIdOrError instanceof NextResponse) {
22+
return accountIdOrError;
23+
}
24+
25+
const accountId = accountIdOrError;
26+
27+
const body = await safeParseJson(request);
28+
29+
const validated = validateCreateChatBody(body);
30+
if (validated instanceof NextResponse) {
31+
return validated;
32+
}
33+
34+
const { artistId, chatId } = validated;
35+
36+
const roomId = chatId || generateUUID();
37+
38+
const chat = await insertRoom({
39+
id: roomId,
40+
account_id: accountId,
41+
artist_id: artistId || null,
42+
topic: null,
43+
});
44+
45+
return NextResponse.json(
46+
{
47+
status: "success",
48+
chat,
49+
},
50+
{
51+
status: 200,
52+
headers: getCorsHeaders(),
53+
},
54+
);
55+
} catch (error) {
56+
console.error("[ERROR] createChatHandler:", error);
57+
return NextResponse.json(
58+
{
59+
status: "error",
60+
message: "Failed to create chat",
61+
},
62+
{
63+
status: 500,
64+
headers: getCorsHeaders(),
65+
},
66+
);
67+
}
68+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { NextResponse } from "next/server";
2+
import { getCorsHeaders } from "@/lib/networking/getCorsHeaders";
3+
import { z } from "zod";
4+
5+
export const createChatBodySchema = z.object({
6+
artistId: z.string().uuid("artistId must be a valid UUID").optional(),
7+
chatId: z.string().uuid("chatId must be a valid UUID").optional(),
8+
});
9+
10+
export type CreateChatBody = z.infer<typeof createChatBodySchema>;
11+
12+
/**
13+
* Validates request body for POST /api/chats.
14+
*
15+
* @param body - The request body
16+
* @returns A NextResponse with an error if validation fails, or the validated body if validation passes.
17+
*/
18+
export function validateCreateChatBody(body: unknown): NextResponse | CreateChatBody {
19+
const result = createChatBodySchema.safeParse(body);
20+
21+
if (!result.success) {
22+
const firstError = result.error.issues[0];
23+
return NextResponse.json(
24+
{
25+
status: "error",
26+
missing_fields: firstError.path,
27+
error: firstError.message,
28+
},
29+
{
30+
status: 400,
31+
headers: getCorsHeaders(),
32+
},
33+
);
34+
}
35+
36+
return result.data;
37+
}

lib/emails/sendEmailSchema.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ export const sendEmailSchema = z.object({
2828
.string()
2929
.describe(
3030
"Room ID to include in the email footer link. Use the active_conversation_id from context.",
31-
)
32-
.optional(),
31+
),
3332
});
3433

3534
export type SendEmailInput = z.infer<typeof sendEmailSchema>;

lib/networking/safeParseJson.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { NextRequest } from "next/server";
2+
3+
/**
4+
* Safely parses JSON from a request body.
5+
* Returns an empty object if the body is empty or invalid JSON.
6+
*
7+
* @param request - The NextRequest object
8+
* @returns The parsed JSON body or an empty object
9+
*/
10+
export async function safeParseJson(request: NextRequest): Promise<unknown> {
11+
try {
12+
return await request.json();
13+
} catch {
14+
return {};
15+
}
16+
}

0 commit comments

Comments
 (0)