Skip to content

Commit 7930982

Browse files
author
Marvin Zhang
committed
feat(api): enhance route parameter handling with type-safe parsing and validation
1 parent 0eda40e commit 7930982

File tree

13 files changed

+233
-103
lines changed

13 files changed

+233
-103
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,6 @@
6262
"better-sqlite3": "^11.10.0",
6363
"dotenv": "16.5.0",
6464
"tsx": "^4.0.0"
65-
}
65+
},
66+
"packageManager": "[email protected]+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad"
6667
}

packages/mcp/src/schemas/mcp-tool-schemas.ts

Lines changed: 58 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* MCP Tool validation schemas
3-
*
3+
*
44
* This module defines Zod schemas for validating MCP tool arguments.
55
* It reuses business logic schemas from @codervisor/devlog-core and adds
66
* MCP-specific validation layers.
@@ -12,7 +12,6 @@ import {
1212
UpdateDevlogEntrySchema,
1313
DevlogIdSchema,
1414
DevlogFilterSchema,
15-
1615
CreateProjectRequestSchema,
1716
UpdateProjectRequestSchema,
1817
ProjectIdSchema,
@@ -21,25 +20,29 @@ import {
2120
/**
2221
* Devlog tool argument schemas
2322
*/
24-
export const CreateDevlogArgsSchema = z.object({
25-
title: z.string().min(1, 'Title is required').max(200, 'Title too long'),
26-
type: z.enum(['feature', 'bugfix', 'task', 'refactor', 'docs']),
27-
description: z.string().min(1, 'Description is required').max(2000, 'Description too long'),
28-
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
29-
businessContext: z.string().max(1000).optional(),
30-
technicalContext: z.string().max(1000).optional(),
31-
acceptanceCriteria: z.array(z.string()).optional(),
32-
}).transform(data => ({
33-
...data,
34-
priority: data.priority ?? 'medium' as const,
35-
}));
23+
export const CreateDevlogArgsSchema = z
24+
.object({
25+
title: z.string().min(1, 'Title is required').max(200, 'Title too long'),
26+
type: z.enum(['feature', 'bugfix', 'task', 'refactor', 'docs']),
27+
description: z.string().min(1, 'Description is required'),
28+
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
29+
businessContext: z.string().optional(),
30+
technicalContext: z.string().optional(),
31+
acceptanceCriteria: z.array(z.string()).optional(),
32+
})
33+
.transform((data) => ({
34+
...data,
35+
priority: data.priority ?? ('medium' as const),
36+
}));
3637

3738
export const UpdateDevlogArgsSchema = z.object({
3839
id: DevlogIdSchema,
39-
status: z.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled']).optional(),
40+
status: z
41+
.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled'])
42+
.optional(),
4043
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
41-
businessContext: z.string().max(1000).optional(),
42-
technicalContext: z.string().max(1000).optional(),
44+
businessContext: z.string().optional(),
45+
technicalContext: z.string().optional(),
4346
acceptanceCriteria: z.array(z.string()).optional(),
4447
});
4548

@@ -48,7 +51,9 @@ export const GetDevlogArgsSchema = z.object({
4851
});
4952

5053
export const ListDevlogsArgsSchema = z.object({
51-
status: z.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled']).optional(),
54+
status: z
55+
.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled'])
56+
.optional(),
5257
type: z.enum(['feature', 'bugfix', 'task', 'refactor', 'docs']).optional(),
5358
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
5459
archived: z.boolean().optional(),
@@ -60,35 +65,43 @@ export const ListDevlogsArgsSchema = z.object({
6065

6166
export const SearchDevlogsArgsSchema = z.object({
6267
query: z.string().min(1, 'Search query is required'),
63-
status: z.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled']).optional(),
68+
status: z
69+
.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled'])
70+
.optional(),
6471
type: z.enum(['feature', 'bugfix', 'task', 'refactor', 'docs']).optional(),
6572
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
6673
archived: z.boolean().optional(),
6774
});
6875

69-
export const AddDevlogNoteArgsSchema = z.object({
70-
id: DevlogIdSchema,
71-
note: z.string().min(1, 'Note content is required'),
72-
category: z.enum(['progress', 'issue', 'solution', 'idea', 'reminder', 'feedback']).optional(),
73-
files: z.array(z.string()).optional(),
74-
codeChanges: z.string().optional(),
75-
}).transform(data => ({
76-
...data,
77-
category: data.category ?? 'progress' as const,
78-
}));
79-
80-
export const UpdateDevlogWithNoteArgsSchema = z.object({
81-
id: DevlogIdSchema,
82-
status: z.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled']).optional(),
83-
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
84-
note: z.string().min(1, 'Note content is required'),
85-
category: z.enum(['progress', 'issue', 'solution', 'idea', 'reminder', 'feedback']).optional(),
86-
files: z.array(z.string()).optional(),
87-
codeChanges: z.string().optional(),
88-
}).transform(data => ({
89-
...data,
90-
category: data.category ?? 'progress' as const,
91-
}));
76+
export const AddDevlogNoteArgsSchema = z
77+
.object({
78+
id: DevlogIdSchema,
79+
note: z.string().min(1, 'Note content is required'),
80+
category: z.enum(['progress', 'issue', 'solution', 'idea', 'reminder', 'feedback']).optional(),
81+
files: z.array(z.string()).optional(),
82+
codeChanges: z.string().optional(),
83+
})
84+
.transform((data) => ({
85+
...data,
86+
category: data.category ?? ('progress' as const),
87+
}));
88+
89+
export const UpdateDevlogWithNoteArgsSchema = z
90+
.object({
91+
id: DevlogIdSchema,
92+
status: z
93+
.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled'])
94+
.optional(),
95+
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
96+
note: z.string().min(1, 'Note content is required'),
97+
category: z.enum(['progress', 'issue', 'solution', 'idea', 'reminder', 'feedback']).optional(),
98+
files: z.array(z.string()).optional(),
99+
codeChanges: z.string().optional(),
100+
})
101+
.transform((data) => ({
102+
...data,
103+
category: data.category ?? ('progress' as const),
104+
}));
92105

93106
export const CompleteDevlogArgsSchema = z.object({
94107
id: DevlogIdSchema,
@@ -153,17 +166,17 @@ export class McpToolValidator {
153166
*/
154167
static validate<T>(
155168
schema: z.ZodSchema<T>,
156-
data: unknown
169+
data: unknown,
157170
): { success: true; data: T } | { success: false; errors: string[] } {
158171
const result = schema.safeParse(data);
159-
172+
160173
if (result.success) {
161174
return { success: true, data: result.data };
162175
}
163176

164177
return {
165178
success: false,
166-
errors: result.error.errors.map(err => `${err.path.join('.')}: ${err.message}`),
179+
errors: result.error.errors.map((err) => `${err.path.join('.')}: ${err.message}`),
167180
};
168181
}
169182

packages/web/app/devlogs/[id]/DevlogDetailsPage.tsx

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,19 @@ import { useDevlogs } from '@/hooks/useDevlogs';
77
import { useRouter } from 'next/navigation';
88
import { Button } from '@/components/ui/button';
99
import { Alert, AlertDescription } from '@/components/ui/alert';
10+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
1011
import {
11-
Popover,
12-
PopoverContent,
13-
PopoverTrigger,
14-
} from '@/components/ui/popover';
15-
import {
16-
ArrowLeftIcon,
17-
TrashIcon,
18-
SaveIcon,
19-
UndoIcon,
12+
ArrowLeftIcon,
13+
TrashIcon,
14+
SaveIcon,
15+
UndoIcon,
2016
AlertTriangleIcon,
21-
InfoIcon
17+
InfoIcon,
2218
} from 'lucide-react';
2319
import { toast } from 'sonner';
2420

2521
interface DevlogDetailsPageProps {
26-
id: string;
22+
id: number;
2723
}
2824

2925
export function DevlogDetailsPage({ id }: DevlogDetailsPageProps) {
@@ -68,16 +64,14 @@ export function DevlogDetailsPage({ id }: DevlogDetailsPageProps) {
6864

6965
const handleDelete = async () => {
7066
try {
71-
const numericId = parseInt(id);
72-
7367
// Call both delete functions to ensure proper state synchronization:
7468
// 1. Delete from details hook (updates local state immediately)
75-
await deleteDevlogFromDetails(numericId);
69+
await deleteDevlogFromDetails(id);
7670

7771
// 2. Delete from list context (ensures list state is updated even if SSE is delayed)
7872
// Note: This is a safety measure in case there are timing issues with real-time events
7973
try {
80-
await deleteDevlogFromList(numericId);
74+
await deleteDevlogFromList(id);
8175
} catch (error) {
8276
// This might fail if the item is already deleted, which is fine
8377
console.debug('List deletion failed (likely already removed by SSE):', error);

packages/web/app/devlogs/[id]/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DevlogDetailsPage } from './DevlogDetailsPage';
2+
import { RouteParamParsers } from '@/lib/route-params';
23

34
// Disable static generation for this page since it uses client-side features
45
export const dynamic = 'force-dynamic';
@@ -10,5 +11,6 @@ interface DevlogPageProps {
1011
}
1112

1213
export default function DevlogPage({ params }: DevlogPageProps) {
13-
return <DevlogDetailsPage id={params.id} />;
14+
const { devlogId } = RouteParamParsers.parseDevlogId(params);
15+
return <DevlogDetailsPage id={devlogId} />;
1416
}

packages/web/app/lib/api-utils.ts

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,38 +35,77 @@ export function parseParams<T extends Record<string, string>>(
3535
}
3636

3737
/**
38-
* Type-safe parameter parser for specific route patterns
38+
* Type-safe parameter parser for API routes
3939
*/
4040
export const RouteParams = {
4141
/**
4242
* Parse project ID parameter
4343
* Usage: /api/projects/[id]
4444
*/
4545
parseProjectId(params: { id: string }) {
46-
const result = parseParams(params);
47-
if (!result.success) return result;
46+
try {
47+
const projectId = parseInt(params.id, 10);
48+
if (isNaN(projectId) || projectId <= 0) {
49+
return {
50+
success: false as const,
51+
response: NextResponse.json(
52+
{ error: 'Invalid project ID: must be a positive integer' },
53+
{ status: 400 },
54+
),
55+
};
56+
}
4857

49-
return {
50-
success: true as const,
51-
data: { projectId: result.data.id },
52-
};
58+
return {
59+
success: true as const,
60+
data: { projectId },
61+
};
62+
} catch (error) {
63+
return {
64+
success: false as const,
65+
response: NextResponse.json({ error: 'Invalid project ID format' }, { status: 400 }),
66+
};
67+
}
5368
},
5469

5570
/**
5671
* Parse project ID and devlog ID parameters
5772
* Usage: /api/projects/[id]/devlogs/[devlogId]
5873
*/
5974
parseProjectAndDevlogId(params: { id: string; devlogId: string }) {
60-
const result = parseParams(params);
61-
if (!result.success) return result;
62-
63-
return {
64-
success: true as const,
65-
data: {
66-
projectId: result.data.id,
67-
devlogId: result.data.devlogId,
68-
},
69-
};
75+
try {
76+
const projectId = parseInt(params.id, 10);
77+
const devlogId = parseInt(params.devlogId, 10);
78+
79+
if (isNaN(projectId) || projectId <= 0) {
80+
return {
81+
success: false as const,
82+
response: NextResponse.json(
83+
{ error: 'Invalid project ID: must be a positive integer' },
84+
{ status: 400 },
85+
),
86+
};
87+
}
88+
89+
if (isNaN(devlogId) || devlogId <= 0) {
90+
return {
91+
success: false as const,
92+
response: NextResponse.json(
93+
{ error: 'Invalid devlog ID: must be a positive integer' },
94+
{ status: 400 },
95+
),
96+
};
97+
}
98+
99+
return {
100+
success: true as const,
101+
data: { projectId, devlogId },
102+
};
103+
} catch (error) {
104+
return {
105+
success: false as const,
106+
response: NextResponse.json({ error: 'Invalid parameter format' }, { status: 400 }),
107+
};
108+
}
70109
},
71110
};
72111

0 commit comments

Comments
 (0)