Skip to content

Commit 17eba09

Browse files
committed
feat: enhance DevlogService to manage notes, update API routes for notes retrieval and inclusion
1 parent 2a32063 commit 17eba09

File tree

4 files changed

+170
-7
lines changed

4 files changed

+170
-7
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,6 @@
6464
"better-sqlite3": "^11.10.0",
6565
"dotenv": "16.5.0",
6666
"tsx": "^4.0.0"
67-
}
67+
},
68+
"packageManager": "[email protected]+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad"
6869
}

packages/core/src/services/devlog-service.ts

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ export class DevlogService {
3131
private static readonly TTL_MS = 5 * 60 * 1000; // 5 minutes TTL
3232
private database: DataSource;
3333
private devlogRepository: Repository<DevlogEntryEntity>;
34+
private noteRepository: Repository<DevlogNoteEntity>;
3435

3536
private constructor(private projectId?: number) {
3637
// Database initialization will happen in ensureInitialized()
3738
this.database = null as any; // Temporary placeholder
3839
this.devlogRepository = null as any; // Temporary placeholder
40+
this.noteRepository = null as any; // Temporary placeholder
3941
}
4042

4143
/**
@@ -47,6 +49,7 @@ export class DevlogService {
4749
console.log('[DevlogService] Getting initialized DataSource...');
4850
this.database = await getDataSource();
4951
this.devlogRepository = this.database.getRepository(DevlogEntryEntity);
52+
this.noteRepository = this.database.getRepository(DevlogNoteEntity);
5053
console.log(
5154
'[DevlogService] DataSource ready with entities:',
5255
this.database.entityMetadatas.length,
@@ -79,7 +82,7 @@ export class DevlogService {
7982
return existingInstance.service;
8083
}
8184

82-
async get(id: DevlogId): Promise<DevlogEntry | null> {
85+
async get(id: DevlogId, includeNotes = true): Promise<DevlogEntry | null> {
8386
await this.ensureInitialized();
8487

8588
// Validate devlog ID
@@ -94,7 +97,51 @@ export class DevlogService {
9497
return null;
9598
}
9699

97-
return entity.toDevlogEntry();
100+
const devlogEntry = entity.toDevlogEntry();
101+
102+
// Load notes if requested
103+
if (includeNotes) {
104+
devlogEntry.notes = await this.getNotes(id);
105+
}
106+
107+
return devlogEntry;
108+
}
109+
110+
/**
111+
* Get notes for a specific devlog entry
112+
*/
113+
async getNotes(
114+
devlogId: DevlogId,
115+
limit?: number,
116+
): Promise<import('../types/index.js').DevlogNote[]> {
117+
await this.ensureInitialized();
118+
119+
// Validate devlog ID
120+
const idValidation = DevlogValidator.validateDevlogId(devlogId);
121+
if (!idValidation.success) {
122+
throw new Error(`Invalid devlog ID: ${idValidation.errors.join(', ')}`);
123+
}
124+
125+
const queryBuilder = this.noteRepository
126+
.createQueryBuilder('note')
127+
.where('note.devlogId = :devlogId', { devlogId: idValidation.data })
128+
.orderBy('note.timestamp', 'DESC');
129+
130+
if (limit && limit > 0) {
131+
queryBuilder.limit(limit);
132+
}
133+
134+
const noteEntities = await queryBuilder.getMany();
135+
136+
return noteEntities.map((entity) => ({
137+
id: entity.id,
138+
timestamp: entity.timestamp.toISOString(),
139+
category: entity.category,
140+
content: entity.content,
141+
files: entity.files || [],
142+
codeChanges: entity.codeChanges,
143+
metadata: entity.metadata,
144+
}));
98145
}
99146

100147
async save(entry: DevlogEntry): Promise<void> {
@@ -139,9 +186,55 @@ export class DevlogService {
139186
}
140187
}
141188

142-
// Convert to entity and save
143-
const entity = DevlogEntryEntity.fromDevlogEntry(validatedEntry);
144-
await this.devlogRepository.save(entity);
189+
// Handle notes separately - save to DevlogNoteEntity table
190+
const notesToSave = validatedEntry.notes || [];
191+
192+
// Convert to entity and save (without notes in JSON)
193+
const entryWithoutNotes = { ...validatedEntry };
194+
delete entryWithoutNotes.notes; // Remove notes from the main entity
195+
196+
const entity = DevlogEntryEntity.fromDevlogEntry(entryWithoutNotes);
197+
const savedEntity = await this.devlogRepository.save(entity);
198+
199+
// Save notes to separate table if entry has an ID
200+
if (savedEntity.id && notesToSave.length > 0) {
201+
await this.saveNotes(savedEntity.id, notesToSave);
202+
}
203+
}
204+
205+
/**
206+
* Save notes for a devlog entry to the notes table
207+
*/
208+
private async saveNotes(
209+
devlogId: number,
210+
notes: import('../types/index.js').DevlogNote[],
211+
): Promise<void> {
212+
// Get existing notes to determine which are new
213+
const existingNotes = await this.noteRepository.find({
214+
where: { devlogId },
215+
select: ['id'],
216+
});
217+
const existingNoteIds = new Set(existingNotes.map((n) => n.id));
218+
219+
// Only save new notes (ones that don't exist in DB)
220+
const newNotes = notes.filter((note) => !existingNoteIds.has(note.id));
221+
222+
if (newNotes.length > 0) {
223+
const noteEntities = newNotes.map((note) => {
224+
const entity = new DevlogNoteEntity();
225+
entity.id = note.id;
226+
entity.devlogId = devlogId;
227+
entity.timestamp = new Date(note.timestamp);
228+
entity.category = note.category;
229+
entity.content = note.content;
230+
entity.files = note.files || [];
231+
entity.codeChanges = note.codeChanges;
232+
entity.metadata = note.metadata;
233+
return entity;
234+
});
235+
236+
await this.noteRepository.save(noteEntities);
237+
}
145238
}
146239

147240
async delete(id: DevlogId): Promise<void> {

packages/web/app/api/projects/[id]/devlogs/[devlogId]/notes/route.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,63 @@ import type { NoteCategory } from '@codervisor/devlog-core';
77
// Mark this route as dynamic to prevent static generation
88
export const dynamic = 'force-dynamic';
99

10+
// GET /api/projects/[id]/devlogs/[devlogId]/notes - List notes for a devlog entry
11+
export async function GET(
12+
request: NextRequest,
13+
{ params }: { params: { id: string; devlogId: string } },
14+
) {
15+
try {
16+
// Parse and validate parameters
17+
const paramResult = RouteParams.parseProjectAndDevlogId(params);
18+
if (!paramResult.success) {
19+
return paramResult.response;
20+
}
21+
22+
const { projectId, devlogId } = paramResult.data;
23+
24+
// Parse query parameters
25+
const { searchParams } = new URL(request.url);
26+
const limit = searchParams.get('limit') ? parseInt(searchParams.get('limit')!) : undefined;
27+
const category = searchParams.get('category');
28+
29+
// Validate limit if provided
30+
if (limit !== undefined && (isNaN(limit) || limit < 1 || limit > 1000)) {
31+
return ApiErrors.invalidRequest('Limit must be a number between 1 and 1000');
32+
}
33+
34+
// Ensure project exists
35+
const projectService = ProjectService.getInstance();
36+
const project = await projectService.get(projectId);
37+
if (!project) {
38+
return ApiErrors.projectNotFound();
39+
}
40+
41+
// Create project-aware devlog service
42+
const devlogService = DevlogService.getInstance(projectId);
43+
44+
// Verify devlog exists
45+
const devlogEntry = await devlogService.get(devlogId, false); // Don't load notes yet
46+
if (!devlogEntry) {
47+
return ApiErrors.devlogNotFound();
48+
}
49+
50+
// Get notes for this devlog
51+
const notes = await devlogService.getNotes(devlogId, limit);
52+
53+
// Filter by category if specified
54+
const filteredNotes = category ? notes.filter((note) => note.category === category) : notes;
55+
56+
return NextResponse.json({
57+
devlogId,
58+
total: filteredNotes.length,
59+
notes: filteredNotes,
60+
});
61+
} catch (error) {
62+
console.error('Error listing devlog notes:', error);
63+
return ApiErrors.internalError('Failed to list notes for devlog entry');
64+
}
65+
}
66+
1067
// Schema for adding notes
1168
const AddNoteBodySchema = z.object({
1269
note: z.string().min(1, 'Note is required'),

packages/web/app/api/projects/[id]/devlogs/[devlogId]/route.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,31 @@ export async function GET(
1919

2020
const { projectId, devlogId } = paramResult.data;
2121

22+
// Parse query parameters for notes
23+
const { searchParams } = new URL(request.url);
24+
const includeNotes = searchParams.get('includeNotes') !== 'false'; // Include by default
25+
const notesLimit = searchParams.get('notesLimit')
26+
? parseInt(searchParams.get('notesLimit')!)
27+
: undefined;
28+
2229
const projectService = ProjectService.getInstance();
2330
const project = await projectService.get(projectId);
2431
if (!project) {
2532
return ApiErrors.projectNotFound();
2633
}
2734

2835
const devlogService = DevlogService.getInstance(projectId);
29-
const entry = await devlogService.get(devlogId);
36+
const entry = await devlogService.get(devlogId, includeNotes);
3037

3138
if (!entry) {
3239
return ApiErrors.devlogNotFound();
3340
}
3441

42+
// If notesLimit is specified and we have notes, limit them to the most recent
43+
if (entry.notes && notesLimit && entry.notes.length > notesLimit) {
44+
entry.notes = entry.notes.slice(0, notesLimit);
45+
}
46+
3547
return NextResponse.json(entry);
3648
} catch (error) {
3749
console.error('Error fetching devlog:', error);

0 commit comments

Comments
 (0)