Skip to content

Commit 699ee3e

Browse files
committed
refactor(database): database service and related classes to improve error handling and connection management
1 parent 2a1c541 commit 699ee3e

File tree

7 files changed

+70
-73
lines changed

7 files changed

+70
-73
lines changed

src/bot/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ export class CopBot {
137137
}
138138
@MessageValidator()
139139
@SaveUserData()
140-
async handleMessage(ctx: Context) {}
140+
async handleMessage(ctx: Context) {
141+
return;
142+
}
141143
@SaveUserData()
142144
async handleJoinNewChat(ctx: Context) {
143145
if (!ctx.message?.text) {

src/bot/service/admin/Approved.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Context } from 'grammy';
21
import { ServiceProvider } from '../../../service/database/ServiceProvider';
32
import logger from '../../../utils/logger';
43
export class ApprovedService {

src/database/models/Group.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Group } from '../../types/database/TablesTypes';
33
import { Context } from 'grammy';
44
import { MembersService } from '../service/Members';
55
import { DatabaseService } from '../service/Database';
6-
import { Catch } from '../../decorators/Catch';
76
export class GroupService {
87
private _db: DatabaseService;
98
constructor(private _client: PoolClient) {

src/database/service/Database.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
import { PoolClient, QueryResult, QueryResultRow } from 'pg';
22
export class DatabaseService {
3-
constructor(private _client: PoolClient) {}
3+
private _client: PoolClient;
4+
5+
constructor(client: PoolClient) {
6+
if (!client) {
7+
throw new Error('Database client must be provided.');
8+
}
9+
this._client = client;
10+
}
411

512
/**
613
* Runs a query with parameters and returns the result.
714
*/
815
async query<T extends QueryResultRow>(sql: string, params: any[] = []): Promise<QueryResult<T>> {
9-
return await this._client.query<T>(sql, params);
16+
try {
17+
return await this._client.query<T>(sql, params);
18+
} catch (error: any) {
19+
console.error(`Error executing query: ${sql}`, error);
20+
throw new Error(`Database query failed: ${error.message}`);
21+
}
1022
}
11-
1223
/**
1324
* Inserts a new record and returns the inserted row.
1425
*/

src/database/service/Tables.ts

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,38 @@ import Config from '../../config/index';
66
import logger from '../../utils/logger';
77
export class TablesService {
88
constructor(private _connectionPool: ConnectionPool) {}
9-
@Catch({
10-
category: 'Database',
11-
message: 'Failed to set up initial tables.',
12-
statusCode: 500,
13-
})
149
async initialTables() {
1510
const client = await this._connectionPool.getClient();
16-
const productionPath = path.join(process.cwd(), 'src', 'database', 'sql', 'Tables.sql');
17-
const sqlFilePath = Config.environment === 'production' ? productionPath : path.join(__dirname, '..', './sql/Tables.sql');
18-
const sql = await fs.readFile(sqlFilePath, 'utf-8');
19-
await client.query(sql);
20-
logger.info('Initial tables have been set up successfully.');
11+
try {
12+
const productionPath = path.join(process.cwd(), 'src', 'database', 'sql', 'Tables.sql');
13+
const sqlFilePath = Config.environment === 'production' ? productionPath : path.join(__dirname, '..', './sql/Tables.sql');
14+
const sql = await fs.readFile(sqlFilePath, 'utf-8');
15+
await client.query(sql);
16+
logger.info('Initial tables have been set up successfully.');
17+
} catch (error) {
18+
throw error;
19+
} finally {
20+
client.release(); // Release connection
21+
}
2122
}
22-
@Catch({
23-
category: 'Database',
24-
message: 'Failed to seed tables.',
25-
statusCode: 500,
26-
})
2723
async seedTables() {
2824
const client = await this._connectionPool.getClient();
29-
const result = await client.query(`SELECT COUNT(*) FROM "User";`);
30-
const userCount = parseInt(result.rows[0].count, 10);
31-
if (userCount > 0) {
32-
logger.info('The tables have already been seeded. Skipping seeding process.');
33-
return; // Skip seeding if there's already data in the User table
25+
try {
26+
const result = await client.query(`SELECT COUNT(*) FROM "User";`);
27+
const userCount = parseInt(result.rows[0].count, 10);
28+
if (userCount > 0) {
29+
logger.info('The tables have already been seeded. Skipping seeding process.');
30+
return; // Skip seeding if there's already data in the User table
31+
}
32+
const productionPath = path.join(process.cwd(), 'src', 'database', 'sql', 'seed', 'SeedDataTables.sql');
33+
const sqlFilePath = Config.environment === 'production' ? productionPath : path.join(__dirname, '..', './sql/seed/SeedDataTables.sql');
34+
const sql = await fs.readFile(sqlFilePath, 'utf-8');
35+
await client.query(sql);
36+
logger.info('All tables have been seeded successfully.');
37+
} catch (error) {
38+
throw error;
39+
} finally {
40+
client.release();
3441
}
35-
const productionPath = path.join(process.cwd(), 'src', 'database', 'sql', 'seed', 'SeedDataTables.sql');
36-
const sqlFilePath = Config.environment === 'production' ? productionPath : path.join(__dirname, '..', './sql/seed/SeedDataTables.sql');
37-
const sql = await fs.readFile(sqlFilePath, 'utf-8');
38-
await client.query(sql);
39-
logger.info('All tables have been seeded successfully.');
4042
}
4143
}

src/decorators/index.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,34 @@
11
import { Context } from 'grammy';
22
import { handleDecoratorError } from '../errors/decoratorErrorHandler';
3+
import logger from '../utils/logger';
4+
35
export function createDecorator(middleware: (ctx: Context, next: () => Promise<void>, close: () => void) => Promise<void>) {
46
return function (_target: any, _propertyKey: string | symbol, descriptor: PropertyDescriptor) {
57
const originalMethod = descriptor.value;
8+
69
descriptor.value = async function (...args: any[]) {
710
try {
811
const ctx: Context = (this as any)?.ctx || args[0];
12+
913
let shouldContinue = true;
1014
const close = () => {
1115
shouldContinue = false;
16+
logger.debug(`Execution stopped by close() in ${originalMethod.name}`);
1217
};
1318
return await middleware(
1419
ctx,
1520
async () => {
1621
if (shouldContinue) {
1722
try {
18-
return await originalMethod.apply(this, [...args]);
19-
} catch (error: any) {
23+
return await originalMethod.apply(this, args);
24+
} catch (error) {
2025
handleDecoratorError(error);
2126
}
2227
}
2328
},
2429
close
2530
);
26-
} catch (error: any) {
31+
} catch (error) {
2732
handleDecoratorError(error);
2833
}
2934
};

src/service/database/ServiceProvider.ts

Lines changed: 18 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,13 @@ export class ServiceProvider {
1414
private constructor() {
1515
this._clientInstance = new Client();
1616
}
17-
1817
static async initialize(): Promise<ServiceProvider | null> {
1918
if (!ServiceProvider.instance) {
2019
const instance = new ServiceProvider();
2120
const isInitialized = await instance._clientInstance.initialize();
2221
if (!isInitialized) {
2322
logger.error('Failed to initialize client instance. Returning null.');
24-
return null; // Return null if initialization fails
23+
return null;
2524
}
2625
instance._connectionPool = instance._clientInstance.getConnectionPool();
2726
ServiceProvider.instance = instance;
@@ -42,61 +41,41 @@ export class ServiceProvider {
4241
await this._connectionPool.close();
4342
}
4443
async getPoolClint(): Promise<PoolClient> {
45-
return await this._connectionPool.getClient();
44+
const client = await this._connectionPool.getClient();
45+
client.on('error', (err: any) => {
46+
logger.error('Unexpected client error:', err);
47+
client.release();
48+
});
49+
return client;
4650
}
4751
async getGroupService() {
48-
return await this.retryConnect(async () => {
49-
const client = await this.getPoolClint();
50-
return new GroupService(client);
51-
});
52+
const client = await this.getPoolClint();
53+
return new GroupService(client);
5254
}
5355
async getUserService() {
54-
return await this.retryConnect(async () => {
55-
const client = await this.getPoolClint();
56-
return new UserService(client);
57-
});
56+
const client = await this.getPoolClint();
57+
return new UserService(client);
5858
}
5959
async getRulesService() {
60-
return await this.retryConnect(async () => {
61-
const client = await this.getPoolClint();
62-
return new GroupRuleService(client);
63-
});
60+
const client = await this.getPoolClint();
61+
return new GroupRuleService(client);
6462
}
6563
async getWarnsService() {
66-
return await this.retryConnect(async () => {
67-
const clint = await this.getPoolClint();
68-
return new WarningDatabaseService(clint);
69-
});
64+
const clint = await this.getPoolClint();
65+
return new WarningDatabaseService(clint);
7066
}
7167
async healthCheck(): Promise<boolean> {
68+
const client = await this.getPoolClint();
7269
try {
73-
const client = await this.getPoolClint();
7470
await client.query('SELECT NOW()');
7571
client.release();
7672
logger.info('Database is healthy.');
7773
return true;
7874
} catch (err: any) {
7975
logger.error('Database health check failed:', err.message);
8076
return false;
77+
} finally {
78+
client.release();
8179
}
8280
}
83-
private async retryConnect<T>(fn: () => Promise<T>, retries = 3, delay = 5000): Promise<T | null> {
84-
let lastError: any;
85-
86-
for (let attempt = 0; attempt < retries; attempt++) {
87-
try {
88-
return await fn();
89-
} catch (error: any) {
90-
lastError = error;
91-
logger.warn(`Retry Attempt ${attempt + 1} failed with error: ${error.message || error}.`, 'Database');
92-
if (attempt < retries - 1) {
93-
const backoffTime = delay * Math.pow(2, attempt);
94-
logger.warn(`Retrying in ${backoffTime}ms...`, 'Database');
95-
await new Promise((res) => setTimeout(res, backoffTime));
96-
}
97-
}
98-
}
99-
logger.error(`All ${retries} retry attempts failed. Last error: ${lastError?.message || lastError}`, 'Database');
100-
return null;
101-
}
10281
}

0 commit comments

Comments
 (0)