@hacimertgokhan/next-audit 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib/mysql.js CHANGED
@@ -3,139 +3,140 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getDbConnection = getDbConnection;
4
4
  exports.saveAuditLog = saveAuditLog;
5
5
  const promise_1 = require("mysql2/promise");
6
- let pool = null;
7
- let currentConfig = null;
6
+ // Use global to persist pool across HMR reloads in Next.js development
7
+ const globalForAudit = global;
8
8
  async function getDbConnection(config) {
9
9
  const configKey = JSON.stringify(config.database);
10
- // If config changed or pool doesn't exist, create a new one
11
- if (!pool || currentConfig !== configKey) {
12
- if (pool) {
13
- await pool.end().catch(() => { });
14
- }
15
- const dbConfig = config.database;
16
- const poolOptions = {
17
- waitForConnections: true,
18
- connectionLimit: 10,
19
- queueLimit: 0,
20
- enableKeepAlive: true,
21
- keepAliveInitialDelay: 10000,
22
- connectTimeout: 10000,
23
- };
24
- if (dbConfig.url) {
25
- // Some environments have issues with URL strings, let's try to parse it if it looks suspicious
26
- // but mysql2 generally handles it. We'll pass it directly first.
27
- pool = (0, promise_1.createPool)(dbConfig.url);
28
- // Re-apply pool options to the URL-based pool if possible,
29
- // but createPool(url) returns a pool already.
30
- // In mysql2, you can't easily merge custom options with a URL string in one call
31
- // unless you parse the URL.
32
- try {
33
- // Basic URL parsing to extract components if it helps with the "IP reversal" issue
34
- const url = new URL(dbConfig.url.replace('mysql://', 'http://')); // URL parser needs a known protocol
35
- const parsedConfig = {
36
- ...poolOptions,
37
- host: url.hostname,
38
- port: parseInt(url.port) || 3306,
39
- user: url.username,
40
- password: url.password,
41
- database: url.pathname.substring(1),
42
- };
43
- // If the user's IP issue is real, maybe passing it as an object fixed it
44
- pool = (0, promise_1.createPool)(parsedConfig);
45
- }
46
- catch (e) {
47
- // If parsing fails (e.g. invalid URL), fallback to direct string
48
- pool = (0, promise_1.createPool)(dbConfig.url);
49
- }
50
- }
51
- else {
10
+ if (globalForAudit._nextAuditPool && globalForAudit._nextAuditConfig === configKey) {
11
+ return globalForAudit._nextAuditPool;
12
+ }
13
+ // Cleanup old pool if config changed
14
+ if (globalForAudit._nextAuditPool) {
15
+ await globalForAudit._nextAuditPool.end().catch(() => { });
16
+ globalForAudit._nextAuditTableInitialized = false;
17
+ }
18
+ const { database: dbConfig } = config;
19
+ const poolOptions = {
20
+ waitForConnections: true,
21
+ connectionLimit: 10,
22
+ maxIdle: 10,
23
+ idleTimeout: 60000,
24
+ queueLimit: 0,
25
+ enableKeepAlive: true,
26
+ keepAliveInitialDelay: 10000,
27
+ connectTimeout: 10000,
28
+ };
29
+ let pool;
30
+ if (dbConfig.url) {
31
+ // Try to parse URL to avoid driver quirks with some IP formats
32
+ try {
33
+ const mysqlUrl = new URL(dbConfig.url.replace('mysql://', 'http://'));
52
34
  pool = (0, promise_1.createPool)({
53
35
  ...poolOptions,
54
- host: dbConfig.host,
55
- user: dbConfig.user,
56
- password: dbConfig.password,
57
- database: dbConfig.database,
58
- port: dbConfig.port || 3306,
36
+ host: mysqlUrl.hostname,
37
+ port: parseInt(mysqlUrl.port) || 3306,
38
+ user: mysqlUrl.username,
39
+ password: mysqlUrl.password,
40
+ database: mysqlUrl.pathname.substring(1),
59
41
  });
60
42
  }
61
- currentConfig = configKey;
62
- // Handle pool errors to prevent stale pools
63
- pool.on('error', (err) => {
64
- console.error('[Next-Audit] Pool Error:', err);
65
- if (err && err.code === 'PROTOCOL_CONNECTION_LOST') {
66
- pool = null; // Force recreation on next call
67
- }
43
+ catch (e) {
44
+ pool = (0, promise_1.createPool)(dbConfig.url);
45
+ }
46
+ }
47
+ else {
48
+ pool = (0, promise_1.createPool)({
49
+ ...poolOptions,
50
+ host: dbConfig.host,
51
+ user: dbConfig.user,
52
+ password: dbConfig.password,
53
+ database: dbConfig.database,
54
+ port: dbConfig.port || 3306,
68
55
  });
69
56
  }
57
+ // Explicit error listener to clear global pool on fatal errors
58
+ pool.on('error', (err) => {
59
+ if (err.code === 'PROTOCOL_CONNECTION_LOST' || err.fatal) {
60
+ globalForAudit._nextAuditPool = undefined;
61
+ globalForAudit._nextAuditTableInitialized = false;
62
+ }
63
+ });
64
+ globalForAudit._nextAuditPool = pool;
65
+ globalForAudit._nextAuditConfig = configKey;
66
+ globalForAudit._nextAuditTableInitialized = false;
70
67
  return pool;
71
68
  }
69
+ async function ensureTableExists(connection, tableName) {
70
+ if (globalForAudit._nextAuditTableInitialized)
71
+ return;
72
+ await connection.query(`
73
+ CREATE TABLE IF NOT EXISTS \`${tableName}\` (
74
+ id INT AUTO_INCREMENT PRIMARY KEY,
75
+ timestamp DATETIME,
76
+ method VARCHAR(10),
77
+ url VARCHAR(255),
78
+ user_id VARCHAR(50),
79
+ entity VARCHAR(50),
80
+ entity_id VARCHAR(50),
81
+ old_value JSON,
82
+ new_value JSON,
83
+ action_type VARCHAR(50),
84
+ status INT,
85
+ duration_ms INT,
86
+ ip VARCHAR(45),
87
+ user_agent VARCHAR(255)
88
+ )
89
+ `);
90
+ globalForAudit._nextAuditTableInitialized = true;
91
+ }
72
92
  async function saveAuditLog(entry, config) {
93
+ if (config.testMode && config.debug) {
94
+ console.log(`[Next-Audit] (Test Mode) Log: ${entry.method} ${entry.url}`);
95
+ return;
96
+ }
97
+ let connection = null;
73
98
  try {
74
- if (config.testMode) {
75
- if (config.debug) {
76
- console.log(`[Next-Audit] (Test Mode) Log would be saved: ${entry.method} ${entry.url}`);
77
- }
78
- return;
79
- }
80
- const db = await getDbConnection(config);
81
- const tableName = config.tableName || 'audit_logs';
82
- // Check connection before proceeding
83
- try {
84
- await db.query('SELECT 1');
85
- }
86
- catch (e) {
87
- if (e.code === 'PROTOCOL_CONNECTION_LOST' || e.code === 'ECONNRESET') {
88
- pool = null; // Reset pool
89
- const retryDb = await getDbConnection(config);
90
- // Continue with retryDb
91
- }
92
- else {
93
- throw e;
94
- }
95
- }
96
- const finalDb = pool || (await getDbConnection(config));
97
- await finalDb.query(`
98
- CREATE TABLE IF NOT EXISTS \`${tableName}\` (
99
- id INT AUTO_INCREMENT PRIMARY KEY,
100
- timestamp DATETIME,
101
- method VARCHAR(10),
102
- url VARCHAR(255),
103
- user_id VARCHAR(50),
104
- entity VARCHAR(50),
105
- entity_id VARCHAR(50),
106
- old_value JSON,
107
- new_value JSON,
108
- action_type VARCHAR(50),
109
- status INT,
110
- duration_ms INT,
111
- ip VARCHAR(45),
112
- user_agent VARCHAR(255)
113
- )
114
- `);
115
- await finalDb.query(`INSERT INTO \`${tableName}\`
116
- (timestamp, method, url, user_id, entity, entity_id, old_value, new_value, action_type, status, duration_ms, ip, user_agent)
117
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
99
+ const pool = await getDbConnection(config);
100
+ // Checkout connection from pool
101
+ connection = await pool.getConnection();
102
+ // One-time table initialization per pool lifecycle
103
+ await ensureTableExists(connection, config.tableName || 'audit_logs');
104
+ // Prepare values - ensure JSON is stringified or null
105
+ const values = [
118
106
  entry.timestamp,
119
107
  entry.method,
120
108
  entry.url,
121
- entry.user_id,
122
- entry.entity,
123
- entry.entity_id,
109
+ entry.user_id || null,
110
+ entry.entity || null,
111
+ entry.entity_id || null,
124
112
  entry.old_value ? JSON.stringify(entry.old_value) : null,
125
113
  entry.new_value ? JSON.stringify(entry.new_value) : null,
126
114
  entry.action_type,
127
115
  entry.status,
128
116
  entry.duration_ms,
129
- entry.ip,
130
- entry.user_agent
131
- ]);
117
+ entry.ip || null,
118
+ entry.user_agent || null
119
+ ];
120
+ const tableName = config.tableName || 'audit_logs';
121
+ await connection.query(`INSERT INTO \`${tableName}\`
122
+ (timestamp, method, url, user_id, entity, entity_id, old_value, new_value, action_type, status, duration_ms, ip, user_agent)
123
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, values);
132
124
  if (config.debug) {
133
125
  console.log(`[Next-Audit] Log saved: ${entry.method} ${entry.url}`);
134
126
  }
135
127
  }
136
128
  catch (error) {
137
- console.error('[Next-Audit] Failed to save audit log:', error);
138
- // If it failed, reset pool just in case it's a connection issue
139
- pool = null;
129
+ const dbHost = config.database.url || config.database.host || 'unknown';
130
+ console.error(`[Next-Audit] Failed to save audit log to ${dbHost}:`, error.message || error);
131
+ // If it's a connection error, clear the pool so it recreates next time
132
+ if (error.code === 'PROTOCOL_CONNECTION_LOST' || error.code === 'ETIMEDOUT' || error.fatal) {
133
+ globalForAudit._nextAuditPool = undefined;
134
+ globalForAudit._nextAuditTableInitialized = false;
135
+ }
136
+ }
137
+ finally {
138
+ if (connection) {
139
+ connection.release();
140
+ }
140
141
  }
141
142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hacimertgokhan/next-audit",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Next.js backend audit system with @Audit decorator",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/lib/mysql.ts CHANGED
@@ -1,155 +1,162 @@
1
- import { createPool, Pool } from 'mysql2/promise';
1
+ import { createPool, Pool, PoolConnection } from 'mysql2/promise';
2
2
  import { AuditConfig, AuditLogEntry } from '../types';
3
3
 
4
- let pool: Pool | null = null;
5
- let currentConfig: string | null = null;
4
+ // Use global to persist pool across HMR reloads in Next.js development
5
+ const globalForAudit = global as unknown as {
6
+ _nextAuditPool?: Pool;
7
+ _nextAuditConfig?: string;
8
+ _nextAuditTableInitialized?: boolean;
9
+ };
6
10
 
7
- export async function getDbConnection(config: AuditConfig) {
11
+ export async function getDbConnection(config: AuditConfig): Promise<Pool> {
8
12
  const configKey = JSON.stringify(config.database);
9
13
 
10
- // If config changed or pool doesn't exist, create a new one
11
- if (!pool || currentConfig !== configKey) {
12
- if (pool) {
13
- await pool.end().catch(() => { });
14
- }
14
+ if (globalForAudit._nextAuditPool && globalForAudit._nextAuditConfig === configKey) {
15
+ return globalForAudit._nextAuditPool;
16
+ }
15
17
 
16
- const dbConfig = config.database;
17
- const poolOptions: any = {
18
- waitForConnections: true,
19
- connectionLimit: 10,
20
- queueLimit: 0,
21
- enableKeepAlive: true,
22
- keepAliveInitialDelay: 10000,
23
- connectTimeout: 10000,
24
- };
25
-
26
- if (dbConfig.url) {
27
- // Some environments have issues with URL strings, let's try to parse it if it looks suspicious
28
- // but mysql2 generally handles it. We'll pass it directly first.
29
- pool = createPool(dbConfig.url);
18
+ // Cleanup old pool if config changed
19
+ if (globalForAudit._nextAuditPool) {
20
+ await globalForAudit._nextAuditPool.end().catch(() => { });
21
+ globalForAudit._nextAuditTableInitialized = false;
22
+ }
30
23
 
31
- // Re-apply pool options to the URL-based pool if possible,
32
- // but createPool(url) returns a pool already.
33
- // In mysql2, you can't easily merge custom options with a URL string in one call
34
- // unless you parse the URL.
35
-
36
- try {
37
- // Basic URL parsing to extract components if it helps with the "IP reversal" issue
38
- const url = new URL(dbConfig.url.replace('mysql://', 'http://')); // URL parser needs a known protocol
39
- const parsedConfig = {
40
- ...poolOptions,
41
- host: url.hostname,
42
- port: parseInt(url.port) || 3306,
43
- user: url.username,
44
- password: url.password,
45
- database: url.pathname.substring(1),
46
- };
47
-
48
- // If the user's IP issue is real, maybe passing it as an object fixed it
49
- pool = createPool(parsedConfig);
50
- } catch (e) {
51
- // If parsing fails (e.g. invalid URL), fallback to direct string
52
- pool = createPool(dbConfig.url);
53
- }
54
- } else {
24
+ const { database: dbConfig } = config;
25
+ const poolOptions: any = {
26
+ waitForConnections: true,
27
+ connectionLimit: 10,
28
+ maxIdle: 10,
29
+ idleTimeout: 60000,
30
+ queueLimit: 0,
31
+ enableKeepAlive: true,
32
+ keepAliveInitialDelay: 10000,
33
+ connectTimeout: 10000,
34
+ };
35
+
36
+ let pool: Pool;
37
+
38
+ if (dbConfig.url) {
39
+ // Try to parse URL to avoid driver quirks with some IP formats
40
+ try {
41
+ const mysqlUrl = new URL(dbConfig.url.replace('mysql://', 'http://'));
55
42
  pool = createPool({
56
43
  ...poolOptions,
57
- host: dbConfig.host,
58
- user: dbConfig.user,
59
- password: dbConfig.password,
60
- database: dbConfig.database,
61
- port: dbConfig.port || 3306,
44
+ host: mysqlUrl.hostname,
45
+ port: parseInt(mysqlUrl.port) || 3306,
46
+ user: mysqlUrl.username,
47
+ password: mysqlUrl.password,
48
+ database: mysqlUrl.pathname.substring(1),
62
49
  });
50
+ } catch (e) {
51
+ pool = createPool(dbConfig.url);
63
52
  }
64
-
65
- currentConfig = configKey;
66
-
67
- // Handle pool errors to prevent stale pools
68
- (pool as any).on('error', (err: any) => {
69
- console.error('[Next-Audit] Pool Error:', err);
70
- if (err && err.code === 'PROTOCOL_CONNECTION_LOST') {
71
- pool = null; // Force recreation on next call
72
- }
53
+ } else {
54
+ pool = createPool({
55
+ ...poolOptions,
56
+ host: dbConfig.host,
57
+ user: dbConfig.user,
58
+ password: dbConfig.password,
59
+ database: dbConfig.database,
60
+ port: dbConfig.port || 3306,
73
61
  });
74
62
  }
75
63
 
64
+ // Explicit error listener to clear global pool on fatal errors
65
+ (pool as any).on('error', (err: any) => {
66
+ if (err.code === 'PROTOCOL_CONNECTION_LOST' || err.fatal) {
67
+ globalForAudit._nextAuditPool = undefined;
68
+ globalForAudit._nextAuditTableInitialized = false;
69
+ }
70
+ });
71
+
72
+ globalForAudit._nextAuditPool = pool;
73
+ globalForAudit._nextAuditConfig = configKey;
74
+ globalForAudit._nextAuditTableInitialized = false;
75
+
76
76
  return pool;
77
77
  }
78
78
 
79
- export async function saveAuditLog(entry: AuditLogEntry, config: AuditConfig) {
80
- try {
81
- if (config.testMode) {
82
- if (config.debug) {
83
- console.log(`[Next-Audit] (Test Mode) Log would be saved: ${entry.method} ${entry.url}`);
84
- }
85
- return;
86
- }
79
+ async function ensureTableExists(connection: PoolConnection, tableName: string) {
80
+ if (globalForAudit._nextAuditTableInitialized) return;
81
+
82
+ await connection.query(`
83
+ CREATE TABLE IF NOT EXISTS \`${tableName}\` (
84
+ id INT AUTO_INCREMENT PRIMARY KEY,
85
+ timestamp DATETIME,
86
+ method VARCHAR(10),
87
+ url VARCHAR(255),
88
+ user_id VARCHAR(50),
89
+ entity VARCHAR(50),
90
+ entity_id VARCHAR(50),
91
+ old_value JSON,
92
+ new_value JSON,
93
+ action_type VARCHAR(50),
94
+ status INT,
95
+ duration_ms INT,
96
+ ip VARCHAR(45),
97
+ user_agent VARCHAR(255)
98
+ )
99
+ `);
87
100
 
88
- const db = await getDbConnection(config);
89
- const tableName = config.tableName || 'audit_logs';
101
+ globalForAudit._nextAuditTableInitialized = true;
102
+ }
90
103
 
91
- // Check connection before proceeding
92
- try {
93
- await db.query('SELECT 1');
94
- } catch (e: any) {
95
- if (e.code === 'PROTOCOL_CONNECTION_LOST' || e.code === 'ECONNRESET') {
96
- pool = null; // Reset pool
97
- const retryDb = await getDbConnection(config);
98
- // Continue with retryDb
99
- } else {
100
- throw e;
101
- }
102
- }
104
+ export async function saveAuditLog(entry: AuditLogEntry, config: AuditConfig) {
105
+ if (config.testMode && config.debug) {
106
+ console.log(`[Next-Audit] (Test Mode) Log: ${entry.method} ${entry.url}`);
107
+ return;
108
+ }
103
109
 
104
- const finalDb = pool || (await getDbConnection(config));
105
-
106
- await finalDb.query(`
107
- CREATE TABLE IF NOT EXISTS \`${tableName}\` (
108
- id INT AUTO_INCREMENT PRIMARY KEY,
109
- timestamp DATETIME,
110
- method VARCHAR(10),
111
- url VARCHAR(255),
112
- user_id VARCHAR(50),
113
- entity VARCHAR(50),
114
- entity_id VARCHAR(50),
115
- old_value JSON,
116
- new_value JSON,
117
- action_type VARCHAR(50),
118
- status INT,
119
- duration_ms INT,
120
- ip VARCHAR(45),
121
- user_agent VARCHAR(255)
122
- )
123
- `);
110
+ let connection: PoolConnection | null = null;
111
+ try {
112
+ const pool = await getDbConnection(config);
113
+
114
+ // Checkout connection from pool
115
+ connection = await pool.getConnection();
116
+
117
+ // One-time table initialization per pool lifecycle
118
+ await ensureTableExists(connection, config.tableName || 'audit_logs');
119
+
120
+ // Prepare values - ensure JSON is stringified or null
121
+ const values = [
122
+ entry.timestamp,
123
+ entry.method,
124
+ entry.url,
125
+ entry.user_id || null,
126
+ entry.entity || null,
127
+ entry.entity_id || null,
128
+ entry.old_value ? JSON.stringify(entry.old_value) : null,
129
+ entry.new_value ? JSON.stringify(entry.new_value) : null,
130
+ entry.action_type,
131
+ entry.status,
132
+ entry.duration_ms,
133
+ entry.ip || null,
134
+ entry.user_agent || null
135
+ ];
124
136
 
125
- await finalDb.query(
137
+ const tableName = config.tableName || 'audit_logs';
138
+ await connection.query(
126
139
  `INSERT INTO \`${tableName}\`
127
- (timestamp, method, url, user_id, entity, entity_id, old_value, new_value, action_type, status, duration_ms, ip, user_agent)
128
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
129
- [
130
- entry.timestamp,
131
- entry.method,
132
- entry.url,
133
- entry.user_id,
134
- entry.entity,
135
- entry.entity_id,
136
- entry.old_value ? JSON.stringify(entry.old_value) : null,
137
- entry.new_value ? JSON.stringify(entry.new_value) : null,
138
- entry.action_type,
139
- entry.status,
140
- entry.duration_ms,
141
- entry.ip,
142
- entry.user_agent
143
- ]
140
+ (timestamp, method, url, user_id, entity, entity_id, old_value, new_value, action_type, status, duration_ms, ip, user_agent)
141
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
142
+ values
144
143
  );
145
144
 
146
145
  if (config.debug) {
147
146
  console.log(`[Next-Audit] Log saved: ${entry.method} ${entry.url}`);
148
147
  }
149
-
150
- } catch (error) {
151
- console.error('[Next-Audit] Failed to save audit log:', error);
152
- // If it failed, reset pool just in case it's a connection issue
153
- pool = null;
148
+ } catch (error: any) {
149
+ const dbHost = config.database.url || config.database.host || 'unknown';
150
+ console.error(`[Next-Audit] Failed to save audit log to ${dbHost}:`, error.message || error);
151
+
152
+ // If it's a connection error, clear the pool so it recreates next time
153
+ if (error.code === 'PROTOCOL_CONNECTION_LOST' || error.code === 'ETIMEDOUT' || error.fatal) {
154
+ globalForAudit._nextAuditPool = undefined;
155
+ globalForAudit._nextAuditTableInitialized = false;
156
+ }
157
+ } finally {
158
+ if (connection) {
159
+ connection.release();
160
+ }
154
161
  }
155
162
  }