Skip to content

Commit 82789d3

Browse files
author
Umutcan ÖNER
committed
feat: add comprehensive logging and error handling system to core package
- Add structured logger with levels, child loggers, and contextual metadata - Add typed error system with custom error classes and factory functions - Update database package to use new logger and error handling - Update API package to integrate logging and error handling - Add comprehensive test coverage (40 tests for core, updated database tests) - All 59 tests passing across the monorepo
1 parent b4dcb2f commit 82789d3

File tree

8 files changed

+1041
-18
lines changed

8 files changed

+1041
-18
lines changed

apps/api/src/index.ts

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,46 @@
11
// API Server Entry Point
2-
import { log } from '@aicd/core';
2+
import { logger } from '@aicd/core';
33
import { Database } from '@aicd/database';
44

5-
log('Starting AICD API Server...');
5+
const apiLogger = logger.createChild('[API]');
66

77
// Placeholder API server
88
export async function startServer() {
9-
const db = new Database({
10-
connectionString: 'postgresql://localhost/aicd',
11-
});
9+
try {
10+
apiLogger.info('Starting AICD API Server...');
1211

13-
await db.connect();
14-
log('Database connected');
12+
const db = new Database({
13+
connectionString: process.env.DATABASE_URL || 'postgresql://localhost/aicd',
14+
poolSize: Number(process.env.DB_POOL_SIZE) || 10,
15+
});
1516

16-
// Placeholder server logic
17-
const port = process.env.PORT || 3000;
18-
log(`API Server listening on port ${port}`);
17+
await db.connect();
18+
19+
// Placeholder server logic
20+
const port = process.env.PORT || 3000;
21+
22+
// TODO: Implement actual HTTP server
23+
apiLogger.info(`API Server listening on port ${port}`);
24+
25+
// Graceful shutdown handling
26+
process.on('SIGTERM', async () => {
27+
apiLogger.info('SIGTERM received, shutting down gracefully...');
28+
await db.disconnect();
29+
process.exit(0);
30+
});
31+
32+
process.on('SIGINT', async () => {
33+
apiLogger.info('SIGINT received, shutting down gracefully...');
34+
await db.disconnect();
35+
process.exit(0);
36+
});
37+
} catch (error) {
38+
apiLogger.fatal('Failed to start server', error);
39+
process.exit(1);
40+
}
1941
}
2042

21-
startServer();
43+
// Only start the server if this file is run directly
44+
if (require.main === module) {
45+
startServer();
46+
}

packages/core/src/errors.test.ts

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { AppError, ErrorCode, errors, handleError, isAppError } from './errors';
3+
4+
describe('AppError', () => {
5+
it('should create an error with all properties', () => {
6+
const error = new AppError({
7+
code: ErrorCode.VALIDATION_ERROR,
8+
message: 'Test error',
9+
statusCode: 400,
10+
context: { field: 'email' },
11+
cause: new Error('Original error'),
12+
});
13+
14+
expect(error).toBeInstanceOf(Error);
15+
expect(error).toBeInstanceOf(AppError);
16+
expect(error.name).toBe('AppError');
17+
expect(error.code).toBe(ErrorCode.VALIDATION_ERROR);
18+
expect(error.message).toBe('Test error');
19+
expect(error.statusCode).toBe(400);
20+
expect(error.context).toEqual({ field: 'email' });
21+
expect(error.cause).toBeInstanceOf(Error);
22+
expect(error.timestamp).toBeInstanceOf(Date);
23+
});
24+
25+
it('should default to 500 status code', () => {
26+
const error = new AppError({
27+
code: ErrorCode.INTERNAL_ERROR,
28+
message: 'Test error',
29+
});
30+
31+
expect(error.statusCode).toBe(500);
32+
});
33+
34+
it('should serialize to JSON correctly', () => {
35+
const error = new AppError({
36+
code: ErrorCode.VALIDATION_ERROR,
37+
message: 'Test error',
38+
statusCode: 400,
39+
context: { field: 'email' },
40+
});
41+
42+
const json = error.toJSON();
43+
expect(json).toMatchObject({
44+
name: 'AppError',
45+
code: ErrorCode.VALIDATION_ERROR,
46+
message: 'Test error',
47+
statusCode: 400,
48+
context: { field: 'email' },
49+
});
50+
expect(json.timestamp).toBeDefined();
51+
expect(json.stack).toBeDefined();
52+
});
53+
});
54+
55+
describe('Error factory functions', () => {
56+
describe('internal', () => {
57+
it('should create internal error', () => {
58+
const error = errors.internal('Internal server error');
59+
60+
expect(error.code).toBe(ErrorCode.INTERNAL_ERROR);
61+
expect(error.message).toBe('Internal server error');
62+
expect(error.statusCode).toBe(500);
63+
});
64+
65+
it('should include cause if provided', () => {
66+
const cause = new Error('Original error');
67+
const error = errors.internal('Internal server error', cause);
68+
69+
expect(error.cause).toBe(cause);
70+
});
71+
});
72+
73+
describe('validation', () => {
74+
it('should create validation error', () => {
75+
const error = errors.validation('Invalid email format');
76+
77+
expect(error.code).toBe(ErrorCode.VALIDATION_ERROR);
78+
expect(error.message).toBe('Invalid email format');
79+
expect(error.statusCode).toBe(400);
80+
});
81+
82+
it('should include context if provided', () => {
83+
const error = errors.validation('Invalid email format', { field: 'email' });
84+
85+
expect(error.context).toEqual({ field: 'email' });
86+
});
87+
});
88+
89+
describe('notFound', () => {
90+
it('should create not found error without id', () => {
91+
const error = errors.notFound('User');
92+
93+
expect(error.code).toBe(ErrorCode.NOT_FOUND);
94+
expect(error.message).toBe('User not found');
95+
expect(error.statusCode).toBe(404);
96+
expect(error.context).toEqual({ resource: 'User', id: undefined });
97+
});
98+
99+
it('should create not found error with id', () => {
100+
const error = errors.notFound('User', '123');
101+
102+
expect(error.message).toBe('User not found: 123');
103+
expect(error.context).toEqual({ resource: 'User', id: '123' });
104+
});
105+
});
106+
107+
describe('unauthorized', () => {
108+
it('should create unauthorized error with default message', () => {
109+
const error = errors.unauthorized();
110+
111+
expect(error.code).toBe(ErrorCode.UNAUTHORIZED);
112+
expect(error.message).toBe('Unauthorized');
113+
expect(error.statusCode).toBe(401);
114+
});
115+
116+
it('should create unauthorized error with custom message', () => {
117+
const error = errors.unauthorized('Invalid token');
118+
119+
expect(error.message).toBe('Invalid token');
120+
});
121+
});
122+
123+
describe('forbidden', () => {
124+
it('should create forbidden error with default message', () => {
125+
const error = errors.forbidden();
126+
127+
expect(error.code).toBe(ErrorCode.FORBIDDEN);
128+
expect(error.message).toBe('Forbidden');
129+
expect(error.statusCode).toBe(403);
130+
});
131+
132+
it('should create forbidden error with custom message', () => {
133+
const error = errors.forbidden('Access denied');
134+
135+
expect(error.message).toBe('Access denied');
136+
});
137+
});
138+
139+
describe('database errors', () => {
140+
it('should create database connection error', () => {
141+
const error = errors.database.connection('Failed to connect');
142+
143+
expect(error.code).toBe(ErrorCode.DATABASE_CONNECTION_ERROR);
144+
expect(error.message).toBe('Failed to connect');
145+
expect(error.statusCode).toBe(503);
146+
});
147+
148+
it('should create database query error', () => {
149+
const error = errors.database.query('Query failed', 'SELECT * FROM users');
150+
151+
expect(error.code).toBe(ErrorCode.DATABASE_QUERY_ERROR);
152+
expect(error.message).toBe('Query failed');
153+
expect(error.statusCode).toBe(500);
154+
expect(error.context).toEqual({ query: 'SELECT * FROM users' });
155+
});
156+
157+
it('should create database transaction error', () => {
158+
const error = errors.database.transaction('Transaction failed');
159+
160+
expect(error.code).toBe(ErrorCode.DATABASE_TRANSACTION_ERROR);
161+
expect(error.message).toBe('Transaction failed');
162+
expect(error.statusCode).toBe(500);
163+
});
164+
});
165+
166+
describe('API errors', () => {
167+
it('should create rate limit error', () => {
168+
const error = errors.api.rateLimit();
169+
170+
expect(error.code).toBe(ErrorCode.API_RATE_LIMIT);
171+
expect(error.message).toBe('Rate limit exceeded');
172+
expect(error.statusCode).toBe(429);
173+
});
174+
175+
it('should create timeout error', () => {
176+
const error = errors.api.timeout();
177+
178+
expect(error.code).toBe(ErrorCode.API_TIMEOUT);
179+
expect(error.message).toBe('Request timeout');
180+
expect(error.statusCode).toBe(408);
181+
});
182+
183+
it('should create bad request error', () => {
184+
const error = errors.api.badRequest('Invalid request body', { body: {} });
185+
186+
expect(error.code).toBe(ErrorCode.API_BAD_REQUEST);
187+
expect(error.message).toBe('Invalid request body');
188+
expect(error.statusCode).toBe(400);
189+
expect(error.context).toEqual({ body: {} });
190+
});
191+
});
192+
});
193+
194+
describe('isAppError', () => {
195+
it('should return true for AppError instances', () => {
196+
const error = new AppError({
197+
code: ErrorCode.INTERNAL_ERROR,
198+
message: 'Test',
199+
});
200+
201+
expect(isAppError(error)).toBe(true);
202+
});
203+
204+
it('should return false for regular Error instances', () => {
205+
const error = new Error('Test');
206+
expect(isAppError(error)).toBe(false);
207+
});
208+
209+
it('should return false for non-error values', () => {
210+
expect(isAppError('error')).toBe(false);
211+
expect(isAppError(123)).toBe(false);
212+
expect(isAppError(null)).toBe(false);
213+
expect(isAppError(undefined)).toBe(false);
214+
});
215+
});
216+
217+
describe('handleError', () => {
218+
it('should return AppError as-is', () => {
219+
const appError = new AppError({
220+
code: ErrorCode.VALIDATION_ERROR,
221+
message: 'Test',
222+
});
223+
224+
const result = handleError(appError);
225+
expect(result).toBe(appError);
226+
});
227+
228+
it('should convert Error to AppError', () => {
229+
const error = new Error('Test error');
230+
const result = handleError(error);
231+
232+
expect(result).toBeInstanceOf(AppError);
233+
expect(result.code).toBe(ErrorCode.INTERNAL_ERROR);
234+
expect(result.message).toBe('Test error');
235+
expect(result.cause).toBe(error);
236+
});
237+
238+
it('should handle unknown error types', () => {
239+
const result = handleError('string error');
240+
241+
expect(result).toBeInstanceOf(AppError);
242+
expect(result.code).toBe(ErrorCode.INTERNAL_ERROR);
243+
expect(result.message).toBe('An unknown error occurred');
244+
expect(result.cause).toBe('string error');
245+
});
246+
});

0 commit comments

Comments
 (0)