Skip to content

Commit 79f05b4

Browse files
Copilottikazyq
andcommitted
Update DevlogService for document support and add tests
Co-authored-by: tikazyq <[email protected]>
1 parent ce9de87 commit 79f05b4

File tree

4 files changed

+159
-19
lines changed

4 files changed

+159
-19
lines changed

.devlog/devlog.sqlite

Whitespace-only changes.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Document service tests
3+
*/
4+
5+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6+
import { DocumentService } from '../document-service.js';
7+
import type { DevlogDocument } from '../../types/index.js';
8+
9+
// Mock data for testing
10+
const mockFile = {
11+
originalName: 'test-document.txt',
12+
mimeType: 'text/plain',
13+
size: 1024,
14+
content: Buffer.from('This is a test document content', 'utf-8'),
15+
};
16+
17+
const mockDevlogId = 1;
18+
19+
describe('DocumentService', () => {
20+
// Note: Database tests are skipped due to enum column compatibility issues with SQLite
21+
// These tests focus on the business logic and type detection functionality
22+
23+
describe('Document Type Detection', () => {
24+
it('should detect text documents correctly', () => {
25+
const service = DocumentService.getInstance();
26+
27+
// Access private method through any to test it
28+
const detectType = (service as any).determineDocumentType.bind(service);
29+
30+
expect(detectType('text/plain', '.txt')).toBe('text');
31+
expect(detectType('text/markdown', '.md')).toBe('markdown');
32+
expect(detectType('application/json', '.json')).toBe('json');
33+
expect(detectType('text/csv', '.csv')).toBe('csv');
34+
});
35+
36+
it('should detect code documents correctly', () => {
37+
const service = DocumentService.getInstance();
38+
const detectType = (service as any).determineDocumentType.bind(service);
39+
40+
expect(detectType('text/plain', '.js')).toBe('code');
41+
expect(detectType('text/plain', '.ts')).toBe('code');
42+
expect(detectType('text/plain', '.py')).toBe('code');
43+
expect(detectType('text/plain', '.java')).toBe('code');
44+
});
45+
46+
it('should detect images correctly', () => {
47+
const service = DocumentService.getInstance();
48+
const detectType = (service as any).determineDocumentType.bind(service);
49+
50+
expect(detectType('image/png', '.png')).toBe('image');
51+
expect(detectType('image/jpeg', '.jpg')).toBe('image');
52+
expect(detectType('image/gif', '.gif')).toBe('image');
53+
});
54+
55+
it('should detect PDFs correctly', () => {
56+
const service = DocumentService.getInstance();
57+
const detectType = (service as any).determineDocumentType.bind(service);
58+
59+
expect(detectType('application/pdf', '.pdf')).toBe('pdf');
60+
});
61+
62+
it('should default to other for unknown types', () => {
63+
const service = DocumentService.getInstance();
64+
const detectType = (service as any).determineDocumentType.bind(service);
65+
66+
expect(detectType('application/unknown', '.xyz')).toBe('other');
67+
});
68+
});
69+
70+
describe('Text Content Extraction', () => {
71+
it('should identify text-based types correctly', () => {
72+
const service = DocumentService.getInstance();
73+
const isTextBased = (service as any).isTextBasedType.bind(service);
74+
75+
expect(isTextBased('text')).toBe(true);
76+
expect(isTextBased('markdown')).toBe(true);
77+
expect(isTextBased('code')).toBe(true);
78+
expect(isTextBased('json')).toBe(true);
79+
expect(isTextBased('csv')).toBe(true);
80+
expect(isTextBased('log')).toBe(true);
81+
expect(isTextBased('config')).toBe(true);
82+
83+
expect(isTextBased('image')).toBe(false);
84+
expect(isTextBased('pdf')).toBe(false);
85+
expect(isTextBased('other')).toBe(false);
86+
});
87+
88+
it('should extract text content from strings and buffers', () => {
89+
const service = DocumentService.getInstance();
90+
const extractText = (service as any).extractTextContent.bind(service);
91+
92+
const textContent = 'Hello, World!';
93+
const bufferContent = Buffer.from(textContent, 'utf-8');
94+
95+
expect(extractText(textContent, 'text')).toBe(textContent);
96+
expect(extractText(bufferContent, 'text')).toBe(textContent);
97+
expect(extractText(bufferContent, 'image')).toBe('');
98+
});
99+
});
100+
101+
// Note: More comprehensive integration tests would require a test database
102+
// These tests focus on the business logic and type detection functionality
103+
});

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

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import type {
2323
TimeSeriesRequest,
2424
TimeSeriesStats,
2525
} from '../types/index.js';
26-
import { DevlogEntryEntity, DevlogNoteEntity } from '../entities/index.js';
26+
import { DevlogEntryEntity, DevlogNoteEntity, DevlogDocumentEntity } from '../entities/index.js';
2727
import { getDataSource } from '../utils/typeorm-config.js';
2828
import { getStorageType } from '../entities/decorators.js';
2929
import { DevlogValidator } from '../validation/devlog-schemas.js';
@@ -40,6 +40,7 @@ export class DevlogService {
4040
private database: DataSource;
4141
private devlogRepository: Repository<DevlogEntryEntity>;
4242
private noteRepository: Repository<DevlogNoteEntity>;
43+
private documentRepository: Repository<DevlogDocumentEntity>;
4344
private pgTrgmAvailable: boolean = false;
4445
private initPromise: Promise<void> | null = null;
4546

@@ -48,6 +49,7 @@ export class DevlogService {
4849
this.database = null as any; // Temporary placeholder
4950
this.devlogRepository = null as any; // Temporary placeholder
5051
this.noteRepository = null as any; // Temporary placeholder
52+
this.documentRepository = null as any; // Temporary placeholder
5153
}
5254

5355
/**
@@ -72,6 +74,7 @@ export class DevlogService {
7274
this.database = await getDataSource();
7375
this.devlogRepository = this.database.getRepository(DevlogEntryEntity);
7476
this.noteRepository = this.database.getRepository(DevlogNoteEntity);
77+
this.documentRepository = this.database.getRepository(DevlogDocumentEntity);
7578
console.log(
7679
'[DevlogService] DataSource ready with entities:',
7780
this.database.entityMetadatas.length,
@@ -146,7 +149,7 @@ export class DevlogService {
146149
return existingInstance.service;
147150
}
148151

149-
async get(id: DevlogId, includeNotes = true): Promise<DevlogEntry | null> {
152+
async get(id: DevlogId, includeNotes = true, includeDocuments = false): Promise<DevlogEntry | null> {
150153
await this.ensureInitialized();
151154

152155
// Validate devlog ID
@@ -168,6 +171,11 @@ export class DevlogService {
168171
devlogEntry.notes = await this.getNotes(id);
169172
}
170173

174+
// Load documents if requested
175+
if (includeDocuments) {
176+
devlogEntry.documents = await this.getDocuments(id);
177+
}
178+
171179
return devlogEntry;
172180
}
173181

@@ -205,6 +213,35 @@ export class DevlogService {
205213
}));
206214
}
207215

216+
/**
217+
* Get documents for a specific devlog entry
218+
*/
219+
async getDocuments(
220+
devlogId: DevlogId,
221+
limit?: number,
222+
): Promise<import('../types/index.js').DevlogDocument[]> {
223+
await this.ensureInitialized();
224+
225+
// Validate devlog ID
226+
const idValidation = DevlogValidator.validateDevlogId(devlogId);
227+
if (!idValidation.success) {
228+
throw new Error(`Invalid devlog ID: ${idValidation.errors.join(', ')}`);
229+
}
230+
231+
const queryBuilder = this.documentRepository
232+
.createQueryBuilder('document')
233+
.where('document.devlogId = :devlogId', { devlogId: idValidation.data })
234+
.orderBy('document.uploadedAt', 'DESC');
235+
236+
if (limit && limit > 0) {
237+
queryBuilder.limit(limit);
238+
}
239+
240+
const documentEntities = await queryBuilder.getMany();
241+
242+
return documentEntities.map((entity) => entity.toDevlogDocument());
243+
}
244+
208245
/**
209246
* Add a note to a devlog entry
210247
*/

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

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -293,37 +293,37 @@ export class DocumentService {
293293
return 'pdf';
294294
}
295295

296-
// Text-based types
297-
if (mimeType.startsWith('text/')) {
298-
if (mimeType === 'text/markdown' || extension === '.md') {
299-
return 'markdown';
300-
}
301-
if (extension === '.csv') {
302-
return 'csv';
303-
}
304-
if (extension === '.log') {
305-
return 'log';
306-
}
307-
return 'text';
308-
}
309-
310-
// JSON
296+
// JSON (check before text types)
311297
if (mimeType === 'application/json' || extension === '.json') {
312298
return 'json';
313299
}
314300

315-
// Code files
301+
// Code files (check before general text types)
316302
const codeExtensions = ['.js', '.ts', '.py', '.java', '.cpp', '.c', '.go', '.rs', '.php', '.rb', '.swift', '.kt'];
317303
if (codeExtensions.includes(extension.toLowerCase())) {
318304
return 'code';
319305
}
320306

321-
// Config files
307+
// Config files (check before general text types)
322308
const configExtensions = ['.env', '.conf', '.ini', '.yaml', '.yml', '.toml', '.properties'];
323309
if (configExtensions.includes(extension.toLowerCase())) {
324310
return 'config';
325311
}
326312

313+
// Text-based types (more specific checks first)
314+
if (mimeType.startsWith('text/')) {
315+
if (mimeType === 'text/markdown' || extension === '.md') {
316+
return 'markdown';
317+
}
318+
if (extension === '.csv') {
319+
return 'csv';
320+
}
321+
if (extension === '.log') {
322+
return 'log';
323+
}
324+
return 'text';
325+
}
326+
327327
return 'other';
328328
}
329329

0 commit comments

Comments
 (0)