Skip to content

Commit aa2562e

Browse files
author
Marvin Zhang
committed
feat(api): implement archive and unarchive routes for devlog entries
refactor(types): update nullable fields in DevlogEntry type and schemas fix(api): enhance error handling in DevlogApiClient
1 parent d0140a8 commit aa2562e

File tree

8 files changed

+200
-42
lines changed

8 files changed

+200
-42
lines changed

packages/core/src/entities/devlog-entry.entity.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,23 +69,23 @@ export class DevlogEntryEntity {
6969
updatedAt!: Date;
7070

7171
@TimestampColumn({ nullable: true, name: 'closed_at' })
72-
closedAt?: Date;
72+
closedAt?: Date | null;
7373

7474
@Column({ type: 'boolean', default: false })
7575
archived!: boolean;
7676

7777
@Column({ type: 'varchar', length: 255, nullable: true })
78-
assignee?: string;
78+
assignee?: string | null;
7979

8080
@Column({ type: 'int', name: 'project_id' })
8181
projectId!: number;
8282

8383
// Flattened DevlogContext fields (simple strings and arrays)
8484
@Column({ type: 'text', nullable: true, name: 'business_context' })
85-
businessContext?: string;
85+
businessContext?: string | null;
8686

8787
@Column({ type: 'text', nullable: true, name: 'technical_context' })
88-
technicalContext?: string;
88+
technicalContext?: string | null;
8989

9090
@JsonColumn({ default: getStorageType() === 'sqlite' ? '[]' : [], name: 'acceptance_criteria' })
9191
acceptanceCriteria!: string[];

packages/core/src/types/core.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,15 +177,15 @@ export interface DevlogEntry {
177177
priority: DevlogPriority;
178178
createdAt: string;
179179
updatedAt: string;
180-
closedAt?: string; // ISO timestamp when status changed to 'done' or 'cancelled'
181-
assignee?: string;
180+
closedAt?: string | null; // ISO timestamp when status changed to 'done' or 'cancelled'
181+
assignee?: string | null;
182182
archived?: boolean; // For long-term management and performance
183183
projectId: number; // Project context for multi-project isolation - REQUIRED
184184

185185
// Flattened context fields
186186
acceptanceCriteria?: string[];
187-
businessContext?: string;
188-
technicalContext?: string;
187+
businessContext?: string | null;
188+
technicalContext?: string | null;
189189

190190
// Related entities (loaded separately, not stored as JSON)
191191
notes?: DevlogNote[];
@@ -205,7 +205,7 @@ export interface DevlogFilter {
205205
status?: DevlogStatus[];
206206
type?: DevlogType[];
207207
priority?: DevlogPriority[];
208-
assignee?: string;
208+
assignee?: string | null;
209209
fromDate?: string;
210210
toDate?: string;
211211
search?: string;

packages/core/src/validation/devlog-schemas.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ export const DevlogEntrySchema = z.object({
2424
priority: z.enum(['low', 'medium', 'high', 'critical']),
2525
createdAt: z.string().datetime('Invalid createdAt timestamp'),
2626
updatedAt: z.string().datetime('Invalid updatedAt timestamp'),
27-
closedAt: z.string().datetime('Invalid closedAt timestamp').optional(),
28-
assignee: z.string().optional(),
27+
closedAt: z.string().datetime('Invalid closedAt timestamp').nullable().optional(),
28+
assignee: z.string().nullable().optional(),
2929
archived: z.boolean().optional(),
3030
projectId: z.number().int().positive(),
3131
acceptanceCriteria: z.array(z.string()).optional(),
32-
businessContext: z.string().max(1000, 'Business context too long').optional(),
33-
technicalContext: z.string().max(1000, 'Technical context too long').optional(),
32+
businessContext: z.string().max(10000, 'Business context too long').nullable().optional(),
33+
technicalContext: z.string().max(10000, 'Technical context too long').nullable().optional(),
3434
notes: z.array(z.any()).optional(), // Notes have their own validation
3535
dependencies: z.array(z.any()).optional(), // Dependencies have their own validation
3636
}) satisfies z.ZodType<DevlogEntry>;
@@ -50,12 +50,12 @@ export const CreateDevlogEntrySchema = z.object({
5050
.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled'])
5151
.default('new'),
5252
priority: z.enum(['low', 'medium', 'high', 'critical']).default('medium'),
53-
assignee: z.string().optional(),
53+
assignee: z.string().nullable().optional(),
5454
archived: z.boolean().default(false).optional(),
5555
projectId: z.number().int().positive().optional(),
5656
acceptanceCriteria: z.array(z.string()).optional(),
57-
businessContext: z.string().max(1000, 'Business context too long').optional(),
58-
technicalContext: z.string().max(1000, 'Technical context too long').optional(),
57+
businessContext: z.string().max(10000, 'Business context too long').nullable().optional(),
58+
technicalContext: z.string().max(10000, 'Technical context too long').nullable().optional(),
5959
});
6060

6161
/**
@@ -78,11 +78,11 @@ export const UpdateDevlogEntrySchema = z.object({
7878
.enum(['new', 'in-progress', 'blocked', 'in-review', 'testing', 'done', 'cancelled'])
7979
.optional(),
8080
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
81-
assignee: z.string().optional(),
81+
assignee: z.string().nullable().optional(),
8282
archived: z.boolean().optional(),
8383
acceptanceCriteria: z.array(z.string()).optional(),
84-
businessContext: z.string().max(1000, 'Business context too long').optional(),
85-
technicalContext: z.string().max(1000, 'Technical context too long').optional(),
84+
businessContext: z.string().max(10000, 'Business context too long').nullable().optional(),
85+
technicalContext: z.string().max(10000, 'Technical context too long').nullable().optional(),
8686
});
8787

8888
/**
@@ -113,7 +113,7 @@ export const DevlogFilterSchema = z.object({
113113
.optional(),
114114
type: z.array(z.enum(['feature', 'bugfix', 'task', 'refactor', 'docs'])).optional(),
115115
priority: z.array(z.enum(['low', 'medium', 'high', 'critical'])).optional(),
116-
assignee: z.string().optional(),
116+
assignee: z.string().nullable().optional(),
117117
fromDate: z.string().datetime().optional(),
118118
toDate: z.string().datetime().optional(),
119119
search: z.string().optional(),

packages/mcp/src/adapters/mcp-api-adapter.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -358,10 +358,11 @@ export class MCPApiAdapter {
358358
};
359359
const entry = await this.apiClient.updateDevlog(args.id, updateRequest);
360360

361-
// Add completion note if provided
362-
if (args.summary) {
363-
await this.apiClient.addDevlogNote(args.id, `Completed: ${args.summary}`, 'progress');
364-
}
361+
// Add completion note if provided (skip for now since notes API doesn't exist)
362+
// TODO: Re-enable when notes API is implemented
363+
// if (args.summary) {
364+
// await this.apiClient.addDevlogNote(args.id, `Completed: ${args.summary}`, 'progress');
365+
// }
365366

366367
return {
367368
content: [
@@ -390,10 +391,11 @@ export class MCPApiAdapter {
390391
};
391392
const entry = await this.apiClient.updateDevlog(args.id, updateRequest);
392393

393-
// Add closure note if provided
394-
if (args.reason) {
395-
await this.apiClient.addDevlogNote(args.id, `Closed: ${args.reason}`, 'feedback');
396-
}
394+
// Add closure note if provided (skip for now since notes API doesn't exist)
395+
// TODO: Re-enable when notes API is implemented
396+
// if (args.reason) {
397+
// await this.apiClient.addDevlogNote(args.id, `Closed: ${args.reason}`, 'feedback');
398+
// }
397399

398400
return {
399401
content: [

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

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,24 @@ export class DevlogApiClient {
8787
const response = await fetch(url, requestOptions);
8888

8989
if (!response.ok) {
90-
const errorText = await response.text();
90+
let errorText = '';
91+
try {
92+
errorText = await response.text();
93+
} catch {
94+
errorText = response.statusText;
95+
}
96+
97+
// Try to parse JSON error response
98+
let errorData = errorText;
99+
try {
100+
const parsed = JSON.parse(errorText);
101+
errorData = parsed.error?.message || parsed.message || errorText;
102+
} catch {
103+
// Keep original text if not JSON
104+
}
105+
91106
throw new DevlogApiClientError(
92-
`HTTP ${response.status}: ${errorText || response.statusText}`,
107+
`HTTP ${response.status}: ${errorData}`,
93108
response.status,
94109
errorText,
95110
);
@@ -279,7 +294,32 @@ export class DevlogApiClient {
279294

280295
// Health check
281296
async healthCheck(): Promise<{ status: string; timestamp: string }> {
282-
const response = await this.get('/api/health');
283-
return this.unwrapApiResponse<{ status: string; timestamp: string }>(response);
297+
try {
298+
const response = await this.get('/api/health');
299+
const result = this.unwrapApiResponse<{ status: string; timestamp: string }>(response);
300+
301+
// Validate the health check response
302+
if (!result || typeof result !== 'object' || !result.status) {
303+
throw new Error('Invalid health check response format');
304+
}
305+
306+
return result;
307+
} catch (error) {
308+
// If health endpoint doesn't exist, try a basic endpoint
309+
console.warn('Health endpoint failed, trying projects endpoint as backup...');
310+
try {
311+
await this.get('/api/projects');
312+
return {
313+
status: 'ok',
314+
timestamp: new Date().toISOString(),
315+
};
316+
} catch (backupError) {
317+
throw new DevlogApiClientError(
318+
`Health check failed: ${error instanceof Error ? error.message : String(error)}`,
319+
0,
320+
error,
321+
);
322+
}
323+
}
284324
}
285325
}

packages/mcp/src/utils/schema-converter.ts

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Zod to JSON Schema converter for MCP tools
3-
*
3+
*
44
* This module converts Zod schemas to JSON Schema format
55
* required by MCP tool definitions.
66
*/
@@ -18,7 +18,7 @@ function zodToJsonSchemaRecursive(def: any): any {
1818
switch (def.typeName) {
1919
case 'ZodString':
2020
const stringSchema: any = { type: 'string' };
21-
21+
2222
// Handle string validations
2323
if (def.checks) {
2424
for (const check of def.checks) {
@@ -35,12 +35,12 @@ function zodToJsonSchemaRecursive(def: any): any {
3535
}
3636
}
3737
}
38-
38+
3939
return stringSchema;
4040

4141
case 'ZodNumber':
4242
const numberSchema: any = { type: 'number' };
43-
43+
4444
// Handle number validations
4545
if (def.checks) {
4646
for (const check of def.checks) {
@@ -57,7 +57,7 @@ function zodToJsonSchemaRecursive(def: any): any {
5757
}
5858
}
5959
}
60-
60+
6161
return numberSchema;
6262

6363
case 'ZodBoolean':
@@ -78,26 +78,26 @@ function zodToJsonSchemaRecursive(def: any): any {
7878
case 'ZodObject':
7979
const properties: any = {};
8080
const required: string[] = [];
81-
81+
8282
for (const [key, value] of Object.entries(def.shape())) {
8383
const fieldDef = (value as any)._def;
8484
properties[key] = zodToJsonSchemaRecursive(fieldDef);
85-
85+
8686
// Check if field is optional
8787
if (fieldDef.typeName !== 'ZodOptional' && fieldDef.typeName !== 'ZodDefault') {
8888
required.push(key);
8989
}
9090
}
91-
91+
9292
const objectSchema: any = {
9393
type: 'object',
9494
properties,
9595
};
96-
96+
9797
if (required.length > 0) {
9898
objectSchema.required = required;
9999
}
100-
100+
101101
return objectSchema;
102102

103103
case 'ZodOptional':
@@ -112,6 +112,20 @@ function zodToJsonSchemaRecursive(def: any): any {
112112
// For transforms, just use the input schema
113113
return zodToJsonSchemaRecursive(def.schema._def);
114114

115+
case 'ZodEffects':
116+
// For effects (including transforms), use the underlying schema
117+
return zodToJsonSchemaRecursive(def.schema._def);
118+
119+
case 'ZodNullable':
120+
// Handle nullable types
121+
const nullableSchema = zodToJsonSchemaRecursive(def.innerType._def);
122+
if (Array.isArray(nullableSchema.type)) {
123+
nullableSchema.type.push('null');
124+
} else {
125+
nullableSchema.type = [nullableSchema.type, 'null'];
126+
}
127+
return nullableSchema;
128+
115129
default:
116130
// Fallback for unsupported types
117131
console.warn(`Unsupported Zod type: ${def.typeName}, falling back to any`);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { DevlogService, ProjectService } from '@codervisor/devlog-core';
3+
import { RouteParams, ApiErrors } from '@/lib/api-utils';
4+
5+
// Mark this route as dynamic to prevent static generation
6+
export const dynamic = 'force-dynamic';
7+
8+
// PUT /api/projects/[id]/devlogs/[devlogId]/archive - Archive devlog entry
9+
export async function PUT(
10+
request: NextRequest,
11+
{ params }: { params: { id: string; devlogId: string } },
12+
) {
13+
try {
14+
// Parse and validate parameters
15+
const paramResult = RouteParams.parseProjectAndDevlogId(params);
16+
if (!paramResult.success) {
17+
return paramResult.response;
18+
}
19+
20+
const { projectId, devlogId } = paramResult.data;
21+
22+
const projectService = ProjectService.getInstance();
23+
const project = await projectService.get(projectId);
24+
if (!project) {
25+
return ApiErrors.projectNotFound();
26+
}
27+
28+
const devlogService = DevlogService.getInstance(projectId);
29+
30+
// Verify entry exists and belongs to project
31+
const existingEntry = await devlogService.get(devlogId);
32+
if (!existingEntry) {
33+
return ApiErrors.devlogNotFound();
34+
}
35+
36+
// Archive the entry
37+
const archivedEntry = {
38+
...existingEntry,
39+
archived: true,
40+
updatedAt: new Date().toISOString(),
41+
};
42+
43+
await devlogService.save(archivedEntry);
44+
45+
return NextResponse.json(archivedEntry);
46+
} catch (error) {
47+
console.error('Error archiving devlog:', error);
48+
const message = error instanceof Error ? error.message : 'Failed to archive devlog';
49+
return ApiErrors.internalError(message);
50+
}
51+
}

0 commit comments

Comments
 (0)