Skip to content

Commit 8af5293

Browse files
author
Marvin Zhang
committed
feat: implement standardized API response handling and utilities
1 parent cf8856a commit 8af5293

File tree

7 files changed

+407
-54
lines changed

7 files changed

+407
-54
lines changed

packages/mcp/src/api/devlog-api-client.ts

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,19 @@ export class DevlogApiClient {
106106
}
107107
}
108108

109+
/**
110+
* Unwrap standardized API response
111+
*/
112+
private unwrapApiResponse<T>(response: any): T {
113+
// Handle standardized API response format
114+
if (response && response.success === true) {
115+
return response.data;
116+
}
117+
118+
// Handle legacy direct response (during transition)
119+
return response;
120+
}
121+
109122
/**
110123
* GET request helper
111124
*/
@@ -154,33 +167,40 @@ export class DevlogApiClient {
154167

155168
// Project Management
156169
async listProjects(): Promise<any[]> {
157-
return this.get('/api/projects');
170+
const response = await this.get('/api/projects');
171+
return this.unwrapApiResponse<any[]>(response);
158172
}
159173

160174
async getProject(projectId?: number): Promise<any> {
161175
const id = projectId || this.currentProjectId || 0;
162-
return this.get(`/api/projects/${id}`);
176+
const response = await this.get(`/api/projects/${id}`);
177+
return this.unwrapApiResponse<any>(response);
163178
}
164179

165180
async createProject(data: any): Promise<any> {
166-
return this.post('/api/projects', data);
181+
const response = await this.post('/api/projects', data);
182+
return this.unwrapApiResponse<any>(response);
167183
}
168184

169185
// Devlog Operations
170186
async createDevlog(data: CreateDevlogRequest): Promise<DevlogEntry> {
171-
return this.post(`${this.getProjectEndpoint()}/devlogs`, data);
187+
const response = await this.post(`${this.getProjectEndpoint()}/devlogs`, data);
188+
return this.unwrapApiResponse<DevlogEntry>(response);
172189
}
173190

174191
async getDevlog(id: number): Promise<DevlogEntry> {
175-
return this.get(`${this.getProjectEndpoint()}/devlogs/${id}`);
192+
const response = await this.get(`${this.getProjectEndpoint()}/devlogs/${id}`);
193+
return this.unwrapApiResponse<DevlogEntry>(response);
176194
}
177195

178196
async updateDevlog(id: number, data: UpdateDevlogRequest): Promise<DevlogEntry> {
179-
return this.put(`${this.getProjectEndpoint()}/devlogs/${id}`, data);
197+
const response = await this.put(`${this.getProjectEndpoint()}/devlogs/${id}`, data);
198+
return this.unwrapApiResponse<DevlogEntry>(response);
180199
}
181200

182201
async deleteDevlog(id: number): Promise<void> {
183-
return this.delete(`${this.getProjectEndpoint()}/devlogs/${id}`);
202+
const response = await this.delete(`${this.getProjectEndpoint()}/devlogs/${id}`);
203+
return this.unwrapApiResponse<void>(response);
184204
}
185205

186206
async listDevlogs(filter?: DevlogFilter): Promise<PaginatedResult<DevlogEntry>> {
@@ -198,7 +218,8 @@ export class DevlogApiClient {
198218
}
199219

200220
const query = params.toString() ? `?${params.toString()}` : '';
201-
return this.get(`${this.getProjectEndpoint()}/devlogs${query}`);
221+
const response = await this.get(`${this.getProjectEndpoint()}/devlogs${query}`);
222+
return this.unwrapApiResponse<PaginatedResult<DevlogEntry>>(response);
202223
}
203224

204225
async searchDevlogs(query: string, filter?: DevlogFilter): Promise<PaginatedResult<DevlogEntry>> {
@@ -211,7 +232,10 @@ export class DevlogApiClient {
211232
if (filter.archived !== undefined) params.append('archived', String(filter.archived));
212233
}
213234

214-
return this.get(`${this.getProjectEndpoint()}/devlogs/search?${params.toString()}`);
235+
const response = await this.get(
236+
`${this.getProjectEndpoint()}/devlogs/search?${params.toString()}`,
237+
);
238+
return this.unwrapApiResponse<PaginatedResult<DevlogEntry>>(response);
215239
}
216240

217241
async addDevlogNote(
@@ -221,24 +245,28 @@ export class DevlogApiClient {
221245
files?: string[],
222246
codeChanges?: string,
223247
): Promise<DevlogEntry> {
224-
return this.post(`${this.getProjectEndpoint()}/devlogs/${devlogId}/notes`, {
248+
const response = await this.post(`${this.getProjectEndpoint()}/devlogs/${devlogId}/notes`, {
225249
note,
226250
category,
227251
files,
228252
codeChanges,
229253
});
254+
return this.unwrapApiResponse<DevlogEntry>(response);
230255
}
231256

232257
async archiveDevlog(id: number): Promise<DevlogEntry> {
233-
return this.put(`${this.getProjectEndpoint()}/devlogs/${id}/archive`, {});
258+
const response = await this.put(`${this.getProjectEndpoint()}/devlogs/${id}/archive`, {});
259+
return this.unwrapApiResponse<DevlogEntry>(response);
234260
}
235261

236262
async unarchiveDevlog(id: number): Promise<DevlogEntry> {
237-
return this.put(`${this.getProjectEndpoint()}/devlogs/${id}/unarchive`, {});
263+
const response = await this.put(`${this.getProjectEndpoint()}/devlogs/${id}/unarchive`, {});
264+
return this.unwrapApiResponse<DevlogEntry>(response);
238265
}
239266

240267
// Health check
241268
async healthCheck(): Promise<{ status: string; timestamp: string }> {
242-
return this.get('/api/health');
269+
const response = await this.get('/api/health');
270+
return this.unwrapApiResponse<{ status: string; timestamp: string }>(response);
243271
}
244272
}

packages/mcp/src/tools/project-tools.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export async function handleGetCurrentProject(adapter: any) {
7878
try {
7979
const currentProjectId = adapter.getCurrentProjectId();
8080
const projects = await adapter.apiClient.listProjects();
81+
// Both should be numbers now
8182
const currentProject = projects.find((p: any) => p.id === currentProjectId);
8283

8384
if (!currentProject) {
@@ -96,7 +97,7 @@ export async function handleGetCurrentProject(adapter: any) {
9697
ID: ${currentProject.id}
9798
Description: ${currentProject.description || 'No description'}
9899
Created: ${new Date(currentProject.createdAt).toLocaleDateString()}
99-
Updated: ${new Date(currentProject.lastAccessedAt).toLocaleDateString()}
100+
Updated: ${new Date(currentProject.updatedAt).toLocaleDateString()}
100101
101102
Note: This is the MCP server's in-memory current project. Web app project may differ.`;
102103

@@ -123,9 +124,23 @@ Note: This is the MCP server's in-memory current project. Web app project may di
123124

124125
export async function handleSwitchProject(adapter: any, args: { projectId: string }) {
125126
try {
127+
// Convert string argument to number for consistency
128+
const targetProjectId = parseInt(args.projectId, 10);
129+
if (isNaN(targetProjectId)) {
130+
return {
131+
content: [
132+
{
133+
type: 'text',
134+
text: `Invalid project ID '${args.projectId}'. Must be a valid number.`,
135+
},
136+
],
137+
isError: true,
138+
};
139+
}
140+
126141
// Validate that the project exists
127142
const projects = await adapter.apiClient.listProjects();
128-
const targetProject = projects.find((p: any) => p.id === args.projectId);
143+
const targetProject = projects.find((p: any) => p.id === targetProjectId);
129144

130145
if (!targetProject) {
131146
return {
@@ -140,13 +155,13 @@ export async function handleSwitchProject(adapter: any, args: { projectId: strin
140155
}
141156

142157
// Switch current project in memory only
143-
adapter.setCurrentProjectId(args.projectId);
158+
adapter.setCurrentProjectId(targetProjectId);
144159

145160
const switchInfo = `Successfully switched MCP server to project: **${targetProject.name}**
146161
ID: ${targetProject.id}
147162
Description: ${targetProject.description || 'No description'}
148163
Created: ${new Date(targetProject.createdAt).toLocaleDateString()}
149-
Updated: ${new Date(targetProject.lastAccessedAt).toLocaleDateString()}
164+
Updated: ${new Date(targetProject.updatedAt).toLocaleDateString()}
150165
151166
Note: This only affects the MCP server's current project. Web app project is managed separately.`;
152167

packages/web/app/api/projects/route.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import { ProjectService } from '@codervisor/devlog-core';
33
import { ApiValidator, CreateProjectBodySchema, WebToServiceProjectCreateSchema } from '@/schemas';
4+
import {
5+
createSimpleCollectionResponse,
6+
createSuccessResponse,
7+
ResponseTransformer,
8+
} from '@/utils/api-responses';
49

510
// Mark this route as dynamic to prevent static generation
611
export const dynamic = 'force-dynamic';
@@ -12,18 +17,12 @@ export async function GET(request: NextRequest) {
1217
await projectService.initialize();
1318

1419
const coreProjects = await projectService.list();
15-
20+
1621
// Transform core project data to web interface format
17-
const projects = coreProjects.map(project => ({
18-
id: project.id.toString(), // Convert number to string
19-
name: project.name,
20-
description: project.description,
21-
tags: [], // Add empty tags array for compatibility
22-
createdAt: project.createdAt.toISOString(), // Convert Date to string
23-
updatedAt: project.lastAccessedAt.toISOString(), // Map lastAccessedAt to updatedAt
24-
}));
22+
const projects = ResponseTransformer.transformProjects(coreProjects);
2523

26-
return NextResponse.json({ projects });
24+
// Return new standardized format
25+
return createSimpleCollectionResponse(projects);
2726
} catch (error) {
2827
console.error('Error fetching projects:', error);
2928
return ApiValidator.handleServiceError(error);
@@ -41,8 +40,8 @@ export async function POST(request: NextRequest) {
4140

4241
// Transform to service layer type (with additional validation)
4342
const serviceData = ApiValidator.transformForService(
44-
bodyValidation.data,
45-
WebToServiceProjectCreateSchema
43+
bodyValidation.data,
44+
WebToServiceProjectCreateSchema,
4645
);
4746

4847
const projectService = ProjectService.getInstance();
@@ -52,16 +51,9 @@ export async function POST(request: NextRequest) {
5251
const coreProject = await projectService.create(serviceData);
5352

5453
// Transform core project data to web interface format
55-
const createdProject = {
56-
id: coreProject.id.toString(), // Convert number to string
57-
name: coreProject.name,
58-
description: coreProject.description,
59-
tags: [], // Add empty tags array for compatibility
60-
createdAt: coreProject.createdAt.toISOString(), // Convert Date to string
61-
updatedAt: coreProject.lastAccessedAt.toISOString(), // Map lastAccessedAt to updatedAt
62-
};
54+
const createdProject = ResponseTransformer.transformProject(coreProject);
6355

64-
return NextResponse.json(createdProject, { status: 201 });
56+
return createSuccessResponse(createdProject, { status: 201 });
6557
} catch (error) {
6658
console.error('Error creating project:', error);
6759
return ApiValidator.handleServiceError(error);

packages/web/app/components/layout/NavigationSidebar.tsx

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,8 @@ import {
1313
SidebarMenuButton,
1414
SidebarMenuItem,
1515
SidebarTrigger,
16-
useSidebar,
1716
} from '@/components/ui/sidebar';
18-
import { Button } from '@/components/ui/button';
19-
import {
20-
AppWindowIcon,
21-
LayoutDashboardIcon,
22-
FileTextIcon,
23-
PlusIcon,
24-
PanelRightClose,
25-
PanelRightOpen,
26-
} from 'lucide-react';
17+
import { AppWindowIcon, LayoutDashboardIcon, FileTextIcon, PlusIcon } from 'lucide-react';
2718

2819
interface NavigationSidebarProps {
2920
// No props needed - using built-in sidebar state
@@ -34,7 +25,6 @@ export function NavigationSidebar(_props: NavigationSidebarProps) {
3425
const pathname = usePathname();
3526
const [mounted, setMounted] = useState(false);
3627
const { currentProject } = useProject();
37-
const { state } = useSidebar();
3828

3929
// Handle client-side hydration
4030
useEffect(() => {
@@ -226,10 +216,8 @@ export function NavigationSidebar(_props: NavigationSidebarProps) {
226216
</SidebarMenu>
227217
</SidebarContent>
228218

229-
<SidebarFooter className="p-4">
230-
<SidebarTrigger className="h-8 w-8 p-0 justify-center">
231-
{state === 'collapsed' ? <PanelRightOpen size={16} /> : <PanelRightClose size={16} />}
232-
</SidebarTrigger>
219+
<SidebarFooter className="p-4 bg-background border-t-0">
220+
<SidebarTrigger className="h-8 w-8 p-0" />
233221
</SidebarFooter>
234222
</Sidebar>
235223
);

packages/web/app/schemas/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Centralized schema exports for web API validation
3-
*
3+
*
44
* This module provides a single entry point for all validation schemas
55
* used across the web application's API endpoints.
66
*/
@@ -10,6 +10,7 @@ export * from './project';
1010
export * from './devlog';
1111
export * from './validation';
1212
export * from './bridge';
13+
export * from './responses';
1314

1415
// Common schemas that might be used across multiple endpoints
1516
import { z } from 'zod';

0 commit comments

Comments
 (0)