Initial commit: Due Diligence Tracker project

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Alexander Domene
2025-09-28 10:14:13 +02:00
commit b056725f02
58 changed files with 12011 additions and 0 deletions

View File

@@ -0,0 +1,313 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { DataService } from '../dataService';
import { Database } from '../../database/database';
// Mock database
const mockDb = {
exec: vi.fn(),
prepare: vi.fn(),
run: vi.fn(),
} as unknown as Database;
describe('DataService', () => {
let dataService: DataService;
beforeEach(() => {
vi.clearAllMocks();
dataService = new DataService(mockDb);
});
describe('getDimensions', () => {
it('should return dimensions ordered by order_index', () => {
const mockResult = [{
columns: ['id', 'name', 'icon', 'color', 'order_index', 'created_at'],
values: [
[1, 'Strategy', 'Briefcase', '#4F46E5', 1, '2023-01-01'],
[2, 'Engineering', 'Code', '#F59E0B', 2, '2023-01-01']
]
}];
(mockDb.exec as any).mockReturnValue(mockResult);
const result = dataService.getDimensions();
expect(mockDb.exec).toHaveBeenCalledWith('SELECT * FROM dimensions ORDER BY order_index');
expect(result).toEqual([
{
id: 1,
name: 'Strategy',
icon: 'Briefcase',
color: '#4F46E5',
order_index: 1,
created_at: '2023-01-01'
},
{
id: 2,
name: 'Engineering',
icon: 'Code',
color: '#F59E0B',
order_index: 2,
created_at: '2023-01-01'
}
]);
});
it('should return empty array when no dimensions exist', () => {
(mockDb.exec as any).mockReturnValue([]);
const result = dataService.getDimensions();
expect(result).toEqual([]);
});
it('should handle database errors', () => {
(mockDb.exec as any).mockImplementation(() => {
throw new Error('Database error');
});
expect(() => dataService.getDimensions()).toThrow('Database error');
});
});
describe('getDimensionsWithStats', () => {
beforeEach(() => {
// Mock getDimensions
vi.spyOn(dataService, 'getDimensions').mockReturnValue([
{ id: 1, name: 'Strategy', icon: 'Briefcase', color: '#4F46E5', order_index: 1 }
]);
});
it('should calculate stats correctly', () => {
// Mock KPI count query
(mockDb.exec as any)
.mockReturnValueOnce([{ values: [[3]] }]) // KPI count
.mockReturnValueOnce([{ values: [[10, 7]] }]); // total and completed checks
const result = dataService.getDimensionsWithStats();
expect(result).toEqual([{
dimension: { id: 1, name: 'Strategy', icon: 'Briefcase', color: '#4F46E5', order_index: 1 },
kpi_count: 3,
total_checks: 10,
completed_checks: 7,
progress: 70
}]);
});
it('should handle zero checks correctly', () => {
(mockDb.exec as any)
.mockReturnValueOnce([{ values: [[2]] }]) // KPI count
.mockReturnValueOnce([{ values: [[0, 0]] }]); // total and completed checks
const result = dataService.getDimensionsWithStats();
expect(result[0].progress).toBe(0);
});
it('should handle null completed checks', () => {
(mockDb.exec as any)
.mockReturnValueOnce([{ values: [[2]] }]) // KPI count
.mockReturnValueOnce([{ values: [[5, null]] }]); // total and completed checks
const result = dataService.getDimensionsWithStats();
expect(result[0].completed_checks).toBe(0);
expect(result[0].progress).toBe(0);
});
});
describe('getKPIsByDimension', () => {
it('should return KPIs for a dimension ordered by order_index', () => {
const mockResult = [{
columns: ['id', 'dimension_id', 'name', 'description', 'order_index', 'created_at'],
values: [
[1, 1, 'Strategic Alignment', 'Vision clarity', 1, '2023-01-01'],
[2, 1, 'Market Position', 'Competitive analysis', 2, '2023-01-01']
]
}];
(mockDb.exec as any).mockReturnValue(mockResult);
const result = dataService.getKPIsByDimension(1);
expect(mockDb.exec).toHaveBeenCalledWith('SELECT * FROM kpis WHERE dimension_id = 1 ORDER BY order_index');
expect(result).toHaveLength(2);
expect(result[0].name).toBe('Strategic Alignment');
});
it('should return empty array when no KPIs exist', () => {
(mockDb.exec as any).mockReturnValue([]);
const result = dataService.getKPIsByDimension(999);
expect(result).toEqual([]);
});
});
describe('getChecksByKPI', () => {
it('should return checks for a KPI ordered by created_at', () => {
const mockResult = [{
columns: ['id', 'kpi_id', 'question', 'check_type', 'current_value', 'expected_value',
'reference_url', 'reference_file_id', 'comment', 'is_completed', 'created_at', 'updated_at'],
values: [
[1, 1, 'Is vision clear?', 'dropdown', 'Yes', 'Yes', null, null, 'Good', 1, '2023-01-01', '2023-01-01'],
[2, 1, 'Goals documented?', 'dropdown', 'No', 'Yes', null, null, 'Needs work', 0, '2023-01-02', '2023-01-02']
]
}];
(mockDb.exec as any).mockReturnValue(mockResult);
const result = dataService.getChecksByKPI(1);
expect(mockDb.exec).toHaveBeenCalledWith('SELECT * FROM checks WHERE kpi_id = 1 ORDER BY created_at');
expect(result).toHaveLength(2);
expect(result[0].is_completed).toBe(true);
expect(result[1].is_completed).toBe(false);
});
});
describe('updateCheck', () => {
it('should update check with escaped values', () => {
const updates = {
current_value: "Yes",
is_completed: true,
comment: "Test's comment"
};
dataService.updateCheck(1, updates);
expect(mockDb.exec).toHaveBeenCalledWith(
expect.stringContaining("UPDATE checks SET")
);
expect(mockDb.exec).toHaveBeenCalledWith(
expect.stringContaining("WHERE id = 1")
);
});
it('should handle boolean values correctly', () => {
const updates = { is_completed: true };
dataService.updateCheck(1, updates);
const sqlCall = (mockDb.exec as any).mock.calls[0][0];
expect(sqlCall).toContain('is_completed = 1');
});
it('should escape string values with quotes', () => {
const updates = { comment: "It's working" };
dataService.updateCheck(1, updates);
const sqlCall = (mockDb.exec as any).mock.calls[0][0];
expect(sqlCall).toContain("comment = 'It''s working'");
});
});
describe('createCheck', () => {
it('should create new check with proper escaping', () => {
const checkData = {
kpi_id: 1,
question: 'Test question?',
check_type: 'text',
current_value: 'Test value',
expected_value: 'Expected value',
reference_url: 'https://example.com',
reference_file_id: undefined,
comment: 'Test comment',
is_completed: false
};
(mockDb.exec as any)
.mockReturnValueOnce(undefined) // INSERT
.mockReturnValueOnce([{ values: [[123]] }]); // SELECT last_insert_rowid()
const result = dataService.createCheck(checkData);
expect(mockDb.exec).toHaveBeenCalledWith(
expect.stringContaining('INSERT INTO checks')
);
expect(result).toBe(123);
});
it('should handle null values correctly', () => {
const checkData = {
kpi_id: 1,
question: 'Test question?',
check_type: 'text',
current_value: undefined,
expected_value: null,
reference_url: undefined,
reference_file_id: undefined,
comment: undefined,
is_completed: false
};
(mockDb.exec as any)
.mockReturnValueOnce(undefined)
.mockReturnValueOnce([{ values: [[456]] }]);
const result = dataService.createCheck(checkData as any);
const sqlCall = (mockDb.exec as any).mock.calls[0][0];
expect(sqlCall).toContain('NULL');
expect(result).toBe(456);
});
it('should handle boolean completion status', () => {
const checkData = {
kpi_id: 1,
question: 'Test?',
check_type: 'text',
is_completed: true
};
(mockDb.exec as any)
.mockReturnValueOnce(undefined)
.mockReturnValueOnce([{ values: [[789]] }]);
dataService.createCheck(checkData as any);
const sqlCall = (mockDb.exec as any).mock.calls[0][0];
expect(sqlCall).toContain(', 1)'); // boolean true as 1
});
});
describe('deleteCheck', () => {
it('should delete check by ID', () => {
dataService.deleteCheck(123);
expect(mockDb.exec).toHaveBeenCalledWith('DELETE FROM checks WHERE id = 123');
});
it('should handle database errors', () => {
(mockDb.exec as any).mockImplementation(() => {
throw new Error('Delete failed');
});
expect(() => dataService.deleteCheck(123)).toThrow('Delete failed');
});
});
describe('edge cases and error handling', () => {
it('should handle empty result sets gracefully', () => {
(mockDb.exec as any).mockReturnValue([]);
expect(() => dataService.getDimensions()).not.toThrow();
expect(() => dataService.getKPIsByDimension(1)).not.toThrow();
expect(() => dataService.getChecksByKPI(1)).not.toThrow();
});
it('should handle malformed result sets', () => {
(mockDb.exec as any).mockReturnValue([{ columns: [], values: [] }]);
const result = dataService.getDimensions();
expect(result).toEqual([]);
});
it('should handle undefined database responses', () => {
(mockDb.exec as any).mockReturnValue(undefined);
expect(() => dataService.getDimensions()).not.toThrow();
});
});
});

View File

@@ -0,0 +1,175 @@
import { Database } from '../database/database';
import { Dimension, KPI, Check, DimensionWithStats } from '../types';
export class DataService {
constructor(private db: Database) {}
getDimensions(): Dimension[] {
const result = this.db.exec('SELECT * FROM dimensions ORDER BY order_index');
const dimensions: Dimension[] = [];
if (result.length > 0) {
const columns = result[0].columns;
const values = result[0].values;
values.forEach((row: any[]) => {
const dimension: any = {};
columns.forEach((column: string, index: number) => {
dimension[column] = row[index];
});
dimensions.push({
id: dimension.id,
name: dimension.name,
icon: dimension.icon,
color: dimension.color,
order_index: dimension.order_index,
created_at: dimension.created_at
});
});
}
return dimensions;
}
getDimensionsWithStats(): DimensionWithStats[] {
const dimensions = this.getDimensions();
return dimensions.map(dimension => {
// Get KPI count
const kpiResult = this.db.exec('SELECT COUNT(*) as count FROM kpis WHERE dimension_id = ' + dimension.id);
const kpi_count = kpiResult.length > 0 ? kpiResult[0].values[0][0] as number : 0;
// Get total checks and completed checks
const checksResult = this.db.exec(`
SELECT
COUNT(*) as total_checks,
SUM(CASE WHEN is_completed = 1 THEN 1 ELSE 0 END) as completed_checks
FROM checks c
JOIN kpis k ON c.kpi_id = k.id
WHERE k.dimension_id = ${dimension.id}
`);
const total_checks = checksResult.length > 0 ? (checksResult[0].values[0][0] as number || 0) : 0;
const completed_checks = checksResult.length > 0 ? (checksResult[0].values[0][1] as number || 0) : 0;
const progress = total_checks > 0 ? (completed_checks / total_checks) * 100 : 0;
return {
dimension,
kpi_count,
total_checks,
completed_checks,
progress
};
});
}
getKPIsByDimension(dimensionId: number): KPI[] {
const result = this.db.exec(`SELECT * FROM kpis WHERE dimension_id = ${dimensionId} ORDER BY order_index`);
const kpis: KPI[] = [];
if (result.length > 0) {
const columns = result[0].columns;
const values = result[0].values;
values.forEach((row: any[]) => {
const kpi: any = {};
columns.forEach((column: string, index: number) => {
kpi[column] = row[index];
});
kpis.push({
id: kpi.id,
dimension_id: kpi.dimension_id,
name: kpi.name,
description: kpi.description,
order_index: kpi.order_index,
created_at: kpi.created_at
});
});
}
return kpis;
}
getChecksByKPI(kpiId: number): Check[] {
const result = this.db.exec(`SELECT * FROM checks WHERE kpi_id = ${kpiId} ORDER BY created_at`);
const checks: Check[] = [];
if (result.length > 0) {
const columns = result[0].columns;
const values = result[0].values;
values.forEach((row: any[]) => {
const check: any = {};
columns.forEach((column: string, index: number) => {
check[column] = row[index];
});
checks.push({
id: check.id,
kpi_id: check.kpi_id,
question: check.question,
check_type: check.check_type,
current_value: check.current_value,
expected_value: check.expected_value,
reference_url: check.reference_url,
reference_file_id: check.reference_file_id,
comment: check.comment,
is_completed: Boolean(check.is_completed),
created_at: check.created_at,
updated_at: check.updated_at
});
});
}
return checks;
}
updateCheck(checkId: number, updates: Partial<Check>): void {
const fields = Object.keys(updates).filter(key => key !== 'id');
const values = fields.map(key => {
const value = updates[key as keyof Check];
if (typeof value === 'boolean') return value ? 1 : 0;
return value;
});
const setClause = fields.map(field => `${field} = ?`).join(', ');
const sql = `UPDATE checks SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ${checkId}`;
// For sql.js, we need to use exec with direct values
const finalSql = sql.replace(/\?/g, () => {
const value = values.shift();
return typeof value === 'string' ? `'${value.replace(/'/g, "''")}'` : String(value);
});
this.db.exec(finalSql);
}
createCheck(check: Omit<Check, 'id' | 'created_at' | 'updated_at'>): number {
const escapeString = (str: string | undefined | null) => {
if (!str) return 'NULL';
return `'${str.replace(/'/g, "''")}'`;
};
const sql = `
INSERT INTO checks (kpi_id, question, check_type, current_value, expected_value,
reference_url, reference_file_id, comment, is_completed)
VALUES (${check.kpi_id}, ${escapeString(check.question)}, ${escapeString(check.check_type)},
${escapeString(check.current_value)}, ${escapeString(check.expected_value)},
${escapeString(check.reference_url)}, ${check.reference_file_id || 'NULL'},
${escapeString(check.comment)}, ${check.is_completed ? 1 : 0})
`;
this.db.exec(sql);
// Return the last inserted row ID
const result = this.db.exec('SELECT last_insert_rowid() as id');
return result[0].values[0][0] as number;
}
deleteCheck(checkId: number): void {
this.db.exec(`DELETE FROM checks WHERE id = ${checkId}`);
}
}