@aetherframework/database 1.0.9 → 1.1.0
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/package.json +1 -2
- package/src/DatabaseManager.js +0 -565
- package/src/core/ConnectionManager.js +0 -351
- package/src/core/DatabaseFactory.js +0 -188
- package/src/core/MongoQueryBuilder.js +0 -576
- package/src/core/PluginManager.js +0 -968
- package/src/core/QueryBuilder.js +0 -4398
- package/src/core/TransactionManager.js +0 -40
- package/src/drivers/clickhouse-driver.js +0 -272
- package/src/drivers/index.js +0 -273
- package/src/drivers/mongodb-driver.js +0 -87
- package/src/drivers/mssql-driver.js +0 -117
- package/src/drivers/mysql-driver.js +0 -169
- package/src/drivers/oracle-driver.js +0 -101
- package/src/drivers/postgres-driver.js +0 -234
- package/src/drivers/redis-driver.js +0 -52
- package/src/drivers/sqlite-driver.js +0 -67
- package/src/middleware/connection-pool.js +0 -455
- package/src/middleware/performance-monitor.js +0 -652
- package/src/middleware/query-cache.js +0 -500
- package/src/middleware/query-logger.js +0 -262
- package/src/plugins/AuditPlugin.js +0 -447
- package/src/plugins/BasePlugin.js +0 -418
- package/src/plugins/BatchOperationPlugin.js +0 -165
- package/src/plugins/CachePlugin.js +0 -407
- package/src/plugins/CtePlugin.js +0 -523
- package/src/plugins/DistributedPlugin.js +0 -543
- package/src/plugins/EncryptionPlugin.js +0 -211
- package/src/plugins/FullTextSearchPlugin.js +0 -164
- package/src/plugins/GeospatialPlugin.js +0 -219
- package/src/plugins/GraphQLPlugin.js +0 -162
- package/src/plugins/HookPlugin.js +0 -211
- package/src/plugins/JsonPlugin.js +0 -366
- package/src/plugins/OptimisticLockPlugin.js +0 -374
- package/src/plugins/PerformancePlugin.js +0 -175
- package/src/plugins/ResiliencePlugin.js +0 -114
- package/src/plugins/ShardingPlugin.js +0 -227
- package/src/plugins/SoftDeletePlugin.js +0 -258
- package/src/plugins/SyncPlugin.js +0 -373
- package/src/plugins/VersioningPlugin.js +0 -314
- package/src/plugins/WindowFunctionPlugin.js +0 -343
- package/src/utils/config-loader.js +0 -632
- package/src/utils/error-handler.js +0 -724
- package/src/utils/migration-runner.js +0 -1066
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/database/middleware/query-logger
|
|
6
|
-
*/
|
|
7
|
-
import { EventEmitter } from 'events';
|
|
8
|
-
|
|
9
|
-
class QueryLogger extends EventEmitter {
|
|
10
|
-
constructor(options = {}) {
|
|
11
|
-
super();
|
|
12
|
-
this.options = {
|
|
13
|
-
enabled: options.enabled !== false,
|
|
14
|
-
logLevel: options.logLevel || 'info',
|
|
15
|
-
slowQueryThreshold: options.slowQueryThreshold || 1000, // ms
|
|
16
|
-
logToConsole: options.logToConsole !== false,
|
|
17
|
-
logToFile: options.logToFile || false,
|
|
18
|
-
logFile: options.logFile || 'query.log',
|
|
19
|
-
...options
|
|
20
|
-
};
|
|
21
|
-
this.queries = [];
|
|
22
|
-
this.slowQueries = [];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Middleware function to log queries
|
|
27
|
-
* @param {Object} query - Query object
|
|
28
|
-
* @param {Function} next - Next middleware function
|
|
29
|
-
* @returns {Promise<Object>} Query result
|
|
30
|
-
*/
|
|
31
|
-
async log(query, next) {
|
|
32
|
-
const startTime = Date.now();
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const result = await next(query);
|
|
36
|
-
const duration = Date.now() - startTime;
|
|
37
|
-
|
|
38
|
-
const logEntry = {
|
|
39
|
-
timestamp: new Date().toISOString(),
|
|
40
|
-
sql: query.sql,
|
|
41
|
-
params: query.params,
|
|
42
|
-
duration,
|
|
43
|
-
success: true,
|
|
44
|
-
connection: query.connectionName,
|
|
45
|
-
type: query.type || 'query'
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
this.queries.push(logEntry);
|
|
49
|
-
|
|
50
|
-
// Check for slow queries
|
|
51
|
-
if (duration > this.options.slowQueryThreshold) {
|
|
52
|
-
this.slowQueries.push(logEntry);
|
|
53
|
-
this.emit('slow-query', logEntry);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Log to console if enabled
|
|
57
|
-
if (this.options.logToConsole) {
|
|
58
|
-
this.logToConsole(logEntry);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Log to file if enabled
|
|
62
|
-
if (this.options.logToFile) {
|
|
63
|
-
this.logToFile(logEntry);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
this.emit('query-logged', logEntry);
|
|
67
|
-
return result;
|
|
68
|
-
} catch (error) {
|
|
69
|
-
const duration = Date.now() - startTime;
|
|
70
|
-
const logEntry = {
|
|
71
|
-
timestamp: new Date().toISOString(),
|
|
72
|
-
sql: query.sql,
|
|
73
|
-
params: query.params,
|
|
74
|
-
duration,
|
|
75
|
-
success: false,
|
|
76
|
-
error: error.message,
|
|
77
|
-
connection: query.connectionName,
|
|
78
|
-
type: query.type || 'query'
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
this.queries.push(logEntry);
|
|
82
|
-
|
|
83
|
-
// Log error to console if enabled
|
|
84
|
-
if (this.options.logToConsole) {
|
|
85
|
-
this.logErrorToConsole(logEntry);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Log error to file if enabled
|
|
89
|
-
if (this.options.logToFile) {
|
|
90
|
-
this.logErrorToFile(logEntry);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
this.emit('query-error', logEntry);
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Log query to console
|
|
100
|
-
* @param {Object} logEntry - Log entry
|
|
101
|
-
*/
|
|
102
|
-
logToConsole(logEntry) {
|
|
103
|
-
const { timestamp, sql, duration, success, connection, type } = logEntry;
|
|
104
|
-
const status = success ? '✅' : '❌';
|
|
105
|
-
const message = `[${timestamp}] ${status} ${type.toUpperCase()} on ${connection} - ${duration}ms`;
|
|
106
|
-
|
|
107
|
-
switch (this.options.logLevel) {
|
|
108
|
-
case 'debug':
|
|
109
|
-
console.debug(message, { sql: sql.substring(0, 200) + (sql.length > 200 ? '...' : '') });
|
|
110
|
-
break;
|
|
111
|
-
case 'warn':
|
|
112
|
-
if (!success || duration > this.options.slowQueryThreshold) {
|
|
113
|
-
console.warn(message);
|
|
114
|
-
}
|
|
115
|
-
break;
|
|
116
|
-
case 'error':
|
|
117
|
-
if (!success) {
|
|
118
|
-
console.error(message, logEntry.error);
|
|
119
|
-
}
|
|
120
|
-
break;
|
|
121
|
-
default: // info
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Log error to console
|
|
127
|
-
* @param {Object} logEntry - Log entry
|
|
128
|
-
*/
|
|
129
|
-
logErrorToConsole(logEntry) {
|
|
130
|
-
const { timestamp, sql, duration, error, connection, type } = logEntry;
|
|
131
|
-
console.error(`[${timestamp}] ❌ ${type.toUpperCase()} ERROR on ${connection} - ${duration}ms`);
|
|
132
|
-
console.error(` SQL: ${sql.substring(0, 200)}${sql.length > 200 ? '...' : ''}`);
|
|
133
|
-
console.error(` Error: ${error}`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Log query to file
|
|
138
|
-
* @param {Object} logEntry - Log entry
|
|
139
|
-
*/
|
|
140
|
-
async logToFile(logEntry) {
|
|
141
|
-
try {
|
|
142
|
-
const fs = await import('fs');
|
|
143
|
-
const path = await import('path');
|
|
144
|
-
|
|
145
|
-
const logDir = path.dirname(this.options.logFile);
|
|
146
|
-
if (!fs.existsSync(logDir)) {
|
|
147
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const logLine = JSON.stringify(logEntry) + '\n';
|
|
151
|
-
fs.appendFileSync(this.options.logFile, logLine, 'utf8');
|
|
152
|
-
} catch (error) {
|
|
153
|
-
console.error('Failed to write query log to file:', error.message);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Log error to file
|
|
159
|
-
* @param {Object} logEntry - Log entry
|
|
160
|
-
*/
|
|
161
|
-
async logErrorToFile(logEntry) {
|
|
162
|
-
await this.logToFile(logEntry);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Get query statistics
|
|
167
|
-
* @returns {Object} Query statistics
|
|
168
|
-
*/
|
|
169
|
-
getStats() {
|
|
170
|
-
const totalQueries = this.queries.length;
|
|
171
|
-
const successfulQueries = this.queries.filter(q => q.success).length;
|
|
172
|
-
const failedQueries = totalQueries - successfulQueries;
|
|
173
|
-
const slowQueries = this.slowQueries.length;
|
|
174
|
-
|
|
175
|
-
const avgDuration = totalQueries > 0
|
|
176
|
-
? this.queries.reduce((sum, q) => sum + q.duration, 0) / totalQueries
|
|
177
|
-
: 0;
|
|
178
|
-
|
|
179
|
-
const maxDuration = totalQueries > 0
|
|
180
|
-
? Math.max(...this.queries.map(q => q.duration))
|
|
181
|
-
: 0;
|
|
182
|
-
|
|
183
|
-
const minDuration = totalQueries > 0
|
|
184
|
-
? Math.min(...this.queries.filter(q => q.success).map(q => q.duration))
|
|
185
|
-
: 0;
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
totalQueries,
|
|
189
|
-
successfulQueries,
|
|
190
|
-
failedQueries,
|
|
191
|
-
slowQueries,
|
|
192
|
-
avgDuration: avgDuration.toFixed(2),
|
|
193
|
-
maxDuration,
|
|
194
|
-
minDuration,
|
|
195
|
-
successRate: totalQueries > 0 ? (successfulQueries / totalQueries * 100).toFixed(2) + '%' : '0%'
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Get recent queries
|
|
201
|
-
* @param {number} limit - Number of queries to return
|
|
202
|
-
* @returns {Array} Recent queries
|
|
203
|
-
*/
|
|
204
|
-
getRecentQueries(limit = 100) {
|
|
205
|
-
return this.queries.slice(-limit);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Get slow queries
|
|
210
|
-
* @param {number} threshold - Slow query threshold in ms
|
|
211
|
-
* @returns {Array} Slow queries
|
|
212
|
-
*/
|
|
213
|
-
getSlowQueries(threshold = null) {
|
|
214
|
-
const actualThreshold = threshold || this.options.slowQueryThreshold;
|
|
215
|
-
return this.queries.filter(q => q.duration > actualThreshold);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Clear query log
|
|
220
|
-
*/
|
|
221
|
-
clear() {
|
|
222
|
-
this.queries = [];
|
|
223
|
-
this.slowQueries = [];
|
|
224
|
-
this.emit('log-cleared');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Export query log
|
|
229
|
-
* @param {string} format - Export format (json, csv)
|
|
230
|
-
* @returns {string} Exported data
|
|
231
|
-
*/
|
|
232
|
-
export(format = 'json') {
|
|
233
|
-
switch (format.toLowerCase()) {
|
|
234
|
-
case 'csv':
|
|
235
|
-
return this.exportToCSV();
|
|
236
|
-
case 'json':
|
|
237
|
-
default:
|
|
238
|
-
return JSON.stringify(this.queries, null, 2);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Export to CSV
|
|
244
|
-
* @returns {string} CSV data
|
|
245
|
-
*/
|
|
246
|
-
exportToCSV() {
|
|
247
|
-
if (this.queries.length === 0) {
|
|
248
|
-
return '';
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const headers = Object.keys(this.queries).join(',');
|
|
252
|
-
const rows = this.queries.map(q =>
|
|
253
|
-
Object.values(q).map(v =>
|
|
254
|
-
typeof v === 'string' ? `"${v.replace(/"/g, '""')}"` : v
|
|
255
|
-
).join(',')
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
return [headers, ...rows].join('\n');
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export default QueryLogger;
|
|
@@ -1,447 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/database/plugin/AuditPlugin
|
|
6
|
-
*/
|
|
7
|
-
import { BasePlugin } from './BasePlugin.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Audit Plugin - Provides comprehensive audit logging functionality
|
|
11
|
-
* Tracks all database operations with user context and metadata
|
|
12
|
-
*/
|
|
13
|
-
export class AuditPlugin extends BasePlugin {
|
|
14
|
-
constructor(queryBuilder) {
|
|
15
|
-
super(queryBuilder);
|
|
16
|
-
this.auditEnabled = false;
|
|
17
|
-
this.auditOptions = {
|
|
18
|
-
userId: null,
|
|
19
|
-
action: 'unknown',
|
|
20
|
-
metadata: {},
|
|
21
|
-
auditTable: 'system_audit_logs'
|
|
22
|
-
};
|
|
23
|
-
this.auditHooks = new Map();
|
|
24
|
-
this.logQueue = []; // 添加日志队列
|
|
25
|
-
this.isProcessingQueue = false;
|
|
26
|
-
this.queueProcessingInterval = null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
_registerMethods() {
|
|
30
|
-
// Register audit methods to QueryBuilder
|
|
31
|
-
this.queryBuilder.enableAuditLog = this.enableAuditLog.bind(this);
|
|
32
|
-
this.queryBuilder.disableAuditLog = this.disableAuditLog.bind(this);
|
|
33
|
-
this.queryBuilder.logAudit = this.logAudit.bind(this);
|
|
34
|
-
this.queryBuilder.setAuditUser = this.setAuditUser.bind(this);
|
|
35
|
-
this.queryBuilder.setAuditAction = this.setAuditAction.bind(this);
|
|
36
|
-
this.queryBuilder.setAuditMetadata = this.setAuditMetadata.bind(this);
|
|
37
|
-
this.queryBuilder.setAuditTable = this.setAuditTable.bind(this);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Enable audit logging
|
|
42
|
-
* @param {Object} options - Audit logging options
|
|
43
|
-
* @returns {QueryBuilder} Query builder instance
|
|
44
|
-
*/
|
|
45
|
-
enableAuditLog(options = {}) {
|
|
46
|
-
this.auditEnabled = true;
|
|
47
|
-
this.auditOptions = {
|
|
48
|
-
...this.auditOptions,
|
|
49
|
-
...options
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// Add audit hooks
|
|
53
|
-
this._addAuditHooks();
|
|
54
|
-
|
|
55
|
-
// Create audit table if not exists
|
|
56
|
-
this._ensureAuditTable();
|
|
57
|
-
|
|
58
|
-
// Start background queue processor
|
|
59
|
-
this._startQueueProcessor();
|
|
60
|
-
|
|
61
|
-
return this.queryBuilder;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Disable audit logging
|
|
66
|
-
* @returns {QueryBuilder} Query builder instance
|
|
67
|
-
*/
|
|
68
|
-
disableAuditLog() {
|
|
69
|
-
this.auditEnabled = false;
|
|
70
|
-
|
|
71
|
-
// Remove audit hooks
|
|
72
|
-
this._removeAuditHooks();
|
|
73
|
-
|
|
74
|
-
// Stop queue processor
|
|
75
|
-
this._stopQueueProcessor();
|
|
76
|
-
|
|
77
|
-
return this.queryBuilder;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Set audit user ID
|
|
82
|
-
* @param {string|number} userId - User identifier
|
|
83
|
-
* @returns {QueryBuilder} Query builder instance
|
|
84
|
-
*/
|
|
85
|
-
setAuditUser(userId) {
|
|
86
|
-
this.auditOptions.userId = userId;
|
|
87
|
-
return this.queryBuilder;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Set audit action
|
|
92
|
-
* @param {string} action - Action name (create, update, delete, etc.)
|
|
93
|
-
* @returns {QueryBuilder} Query builder instance
|
|
94
|
-
*/
|
|
95
|
-
setAuditAction(action) {
|
|
96
|
-
this.auditOptions.action = action;
|
|
97
|
-
return this.queryBuilder;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Set audit metadata
|
|
102
|
-
* @param {Object} metadata - Additional metadata
|
|
103
|
-
* @returns {QueryBuilder} Query builder instance
|
|
104
|
-
*/
|
|
105
|
-
setAuditMetadata(metadata) {
|
|
106
|
-
this.auditOptions.metadata = {
|
|
107
|
-
...this.auditOptions.metadata,
|
|
108
|
-
...metadata
|
|
109
|
-
};
|
|
110
|
-
return this.queryBuilder;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Set audit table name
|
|
115
|
-
* @param {string} tableName - Audit table name
|
|
116
|
-
* @returns {QueryBuilder} Query builder instance
|
|
117
|
-
*/
|
|
118
|
-
setAuditTable(tableName) {
|
|
119
|
-
this.auditOptions.auditTable = tableName;
|
|
120
|
-
return this.queryBuilder;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Start background queue processor
|
|
125
|
-
* @private
|
|
126
|
-
*/
|
|
127
|
-
_startQueueProcessor() {
|
|
128
|
-
if (this.queueProcessingInterval) {
|
|
129
|
-
clearInterval(this.queueProcessingInterval);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Process queue every 100ms instead of immediate write
|
|
133
|
-
this.queueProcessingInterval = setInterval(() => {
|
|
134
|
-
this._processLogQueue();
|
|
135
|
-
}, 100);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Stop background queue processor
|
|
140
|
-
* @private
|
|
141
|
-
*/
|
|
142
|
-
_stopQueueProcessor() {
|
|
143
|
-
if (this.queueProcessingInterval) {
|
|
144
|
-
clearInterval(this.queueProcessingInterval);
|
|
145
|
-
this.queueProcessingInterval = null;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Process remaining logs
|
|
149
|
-
this._processLogQueue();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Process log queue asynchronously
|
|
154
|
-
* @private
|
|
155
|
-
*/
|
|
156
|
-
async _processLogQueue() {
|
|
157
|
-
if (this.isProcessingQueue || this.logQueue.length === 0) {
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
this.isProcessingQueue = true;
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
// Take up to 100 logs at a time
|
|
165
|
-
const logsToProcess = this.logQueue.splice(0, Math.min(100, this.logQueue.length));
|
|
166
|
-
|
|
167
|
-
if (logsToProcess.length > 0) {
|
|
168
|
-
// Batch insert logs
|
|
169
|
-
await this._batchInsertAuditLogs(logsToProcess);
|
|
170
|
-
}
|
|
171
|
-
} catch (error) {
|
|
172
|
-
console.error('Failed to process audit log queue:', error.message);
|
|
173
|
-
// Requeue failed logs
|
|
174
|
-
// In production, you might want to implement retry logic
|
|
175
|
-
} finally {
|
|
176
|
-
this.isProcessingQueue = false;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Batch insert audit logs
|
|
182
|
-
* @param {Array} logs - Array of audit log entries
|
|
183
|
-
* @private
|
|
184
|
-
*/
|
|
185
|
-
async _batchInsertAuditLogs(logs) {
|
|
186
|
-
if (logs.length === 0) return;
|
|
187
|
-
|
|
188
|
-
const values = [];
|
|
189
|
-
const placeholders = [];
|
|
190
|
-
|
|
191
|
-
for (const log of logs) {
|
|
192
|
-
values.push(
|
|
193
|
-
log.userId,
|
|
194
|
-
log.action,
|
|
195
|
-
log.table_name,
|
|
196
|
-
log.sql_query,
|
|
197
|
-
log.bindings,
|
|
198
|
-
log.query_type,
|
|
199
|
-
log.affectedRows || 0,
|
|
200
|
-
log.ip_address,
|
|
201
|
-
log.user_agent,
|
|
202
|
-
log.session_id,
|
|
203
|
-
log.request_id,
|
|
204
|
-
JSON.stringify(log.metadata),
|
|
205
|
-
log.timestamp
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
placeholders.push('(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const sql = `
|
|
212
|
-
INSERT INTO ${this.auditOptions.auditTable}
|
|
213
|
-
(user_id, action, table_name, sql_query, bindings, query_type,
|
|
214
|
-
affected_rows, ip_address, user_agent, session_id, request_id,
|
|
215
|
-
metadata, created_at)
|
|
216
|
-
VALUES ${placeholders.join(', ')}
|
|
217
|
-
`;
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
await this.queryBuilder.connection.query(sql, values);
|
|
221
|
-
} catch (error) {
|
|
222
|
-
console.error('Failed to batch insert audit logs:', error.message);
|
|
223
|
-
throw error;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Log audit entry (async version)
|
|
229
|
-
* @param {Object} data - Additional audit data
|
|
230
|
-
* @returns {Promise<void>}
|
|
231
|
-
*/
|
|
232
|
-
async logAudit(data) {
|
|
233
|
-
if (!this.auditEnabled) {
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const { sql, bindings } = this.queryBuilder.toSQL();
|
|
238
|
-
const auditData = {
|
|
239
|
-
...this.auditOptions,
|
|
240
|
-
...data,
|
|
241
|
-
table_name: this.queryBuilder.tableName,
|
|
242
|
-
sql_query: sql,
|
|
243
|
-
bindings: JSON.stringify(bindings),
|
|
244
|
-
query_type: this.queryBuilder.query.type,
|
|
245
|
-
timestamp: new Date(),
|
|
246
|
-
ip_address: data.ipAddress || null,
|
|
247
|
-
user_agent: data.userAgent || null,
|
|
248
|
-
session_id: data.sessionId || null,
|
|
249
|
-
request_id: data.requestId || null
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
// Queue the log instead of immediate insert
|
|
253
|
-
this.logQueue.push(auditData);
|
|
254
|
-
|
|
255
|
-
// If queue is getting large, trigger immediate processing
|
|
256
|
-
if (this.logQueue.length > 1000) {
|
|
257
|
-
this._processLogQueue();
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Add audit hooks to QueryBuilder
|
|
263
|
-
* @private
|
|
264
|
-
*/
|
|
265
|
-
_addAuditHooks() {
|
|
266
|
-
// Hook for insert operations
|
|
267
|
-
const insertHook = async (data) => {
|
|
268
|
-
await this.logAudit({
|
|
269
|
-
action: 'create',
|
|
270
|
-
affectedRows: data?.insertedCount || 1,
|
|
271
|
-
metadata: { data }
|
|
272
|
-
});
|
|
273
|
-
};
|
|
274
|
-
this.queryBuilder.addHook('afterInsert', insertHook);
|
|
275
|
-
this.auditHooks.set('afterInsert', insertHook);
|
|
276
|
-
|
|
277
|
-
// Hook for update operations
|
|
278
|
-
const updateHook = async (result) => {
|
|
279
|
-
await this.logAudit({
|
|
280
|
-
action: 'update',
|
|
281
|
-
affectedRows: result?.affectedRows || 0,
|
|
282
|
-
metadata: { result }
|
|
283
|
-
});
|
|
284
|
-
};
|
|
285
|
-
this.queryBuilder.addHook('afterUpdate', updateHook);
|
|
286
|
-
this.auditHooks.set('afterUpdate', updateHook);
|
|
287
|
-
|
|
288
|
-
// Hook for delete operations
|
|
289
|
-
const deleteHook = async (result) => {
|
|
290
|
-
await this.logAudit({
|
|
291
|
-
action: 'delete',
|
|
292
|
-
affectedRows: result?.affectedRows || 0,
|
|
293
|
-
metadata: { result }
|
|
294
|
-
});
|
|
295
|
-
};
|
|
296
|
-
this.queryBuilder.addHook('afterDelete', deleteHook);
|
|
297
|
-
this.auditHooks.set('afterDelete', deleteHook);
|
|
298
|
-
|
|
299
|
-
// Hook for select operations (optional, can be heavy)
|
|
300
|
-
const selectHook = async (result) => {
|
|
301
|
-
await this.logAudit({
|
|
302
|
-
action: 'read',
|
|
303
|
-
affectedRows: result?.length || 0,
|
|
304
|
-
metadata: { rowCount: result?.length || 0 }
|
|
305
|
-
});
|
|
306
|
-
};
|
|
307
|
-
this.queryBuilder.addHook('afterSelect', selectHook);
|
|
308
|
-
this.auditHooks.set('afterSelect', selectHook);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Remove audit hooks from QueryBuilder
|
|
313
|
-
* @private
|
|
314
|
-
*/
|
|
315
|
-
_removeAuditHooks() {
|
|
316
|
-
for (const [event, hook] of this.auditHooks) {
|
|
317
|
-
const hooks = this.queryBuilder.hooks[event];
|
|
318
|
-
if (hooks) {
|
|
319
|
-
const index = hooks.indexOf(hook);
|
|
320
|
-
if (index > -1) {
|
|
321
|
-
hooks.splice(index, 1);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
this.auditHooks.clear();
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Ensure audit table exists
|
|
330
|
-
* @private
|
|
331
|
-
*/
|
|
332
|
-
async _ensureAuditTable() {
|
|
333
|
-
try {
|
|
334
|
-
await this.queryBuilder.connection.query(`
|
|
335
|
-
CREATE TABLE IF NOT EXISTS ${this.auditOptions.auditTable} (
|
|
336
|
-
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
337
|
-
user_id VARCHAR(255),
|
|
338
|
-
action VARCHAR(50) NOT NULL,
|
|
339
|
-
table_name VARCHAR(255) NOT NULL,
|
|
340
|
-
sql_query TEXT NOT NULL,
|
|
341
|
-
bindings TEXT,
|
|
342
|
-
query_type VARCHAR(20),
|
|
343
|
-
affected_rows INT DEFAULT 0,
|
|
344
|
-
ip_address VARCHAR(45),
|
|
345
|
-
user_agent TEXT,
|
|
346
|
-
session_id VARCHAR(255),
|
|
347
|
-
request_id VARCHAR(255),
|
|
348
|
-
metadata JSON,
|
|
349
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
350
|
-
INDEX idx_user_id (user_id),
|
|
351
|
-
INDEX idx_action (action),
|
|
352
|
-
INDEX idx_table_name (table_name),
|
|
353
|
-
INDEX idx_created_at (created_at),
|
|
354
|
-
INDEX idx_request_id (request_id)
|
|
355
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
356
|
-
`);
|
|
357
|
-
} catch (error) {
|
|
358
|
-
console.warn('Failed to create audit table:', error.message);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Get audit logs
|
|
364
|
-
* @param {Object} filters - Filter criteria
|
|
365
|
-
* @param {Object} options - Query options
|
|
366
|
-
* @returns {Promise<Array>} Audit logs
|
|
367
|
-
*/
|
|
368
|
-
async getAuditLogs(filters = {}, options = {}) {
|
|
369
|
-
const {
|
|
370
|
-
page = 1,
|
|
371
|
-
perPage = 50,
|
|
372
|
-
orderBy = 'created_at',
|
|
373
|
-
orderDir = 'desc'
|
|
374
|
-
} = options;
|
|
375
|
-
|
|
376
|
-
const auditQuery = new this.queryBuilder.constructor(
|
|
377
|
-
this.auditOptions.auditTable,
|
|
378
|
-
this.queryBuilder.connection,
|
|
379
|
-
this.queryBuilder.dialect
|
|
380
|
-
);
|
|
381
|
-
|
|
382
|
-
// Apply filters
|
|
383
|
-
if (filters.userId) {
|
|
384
|
-
auditQuery.where('user_id', '=', filters.userId);
|
|
385
|
-
}
|
|
386
|
-
if (filters.action) {
|
|
387
|
-
auditQuery.where('action', '=', filters.action);
|
|
388
|
-
}
|
|
389
|
-
if (filters.tableName) {
|
|
390
|
-
auditQuery.where('table_name', '=', filters.tableName);
|
|
391
|
-
}
|
|
392
|
-
if (filters.startDate) {
|
|
393
|
-
auditQuery.where('created_at', '>=', filters.startDate);
|
|
394
|
-
}
|
|
395
|
-
if (filters.endDate) {
|
|
396
|
-
auditQuery.where('created_at', '<=', filters.endDate);
|
|
397
|
-
}
|
|
398
|
-
if (filters.requestId) {
|
|
399
|
-
auditQuery.where('request_id', '=', filters.requestId);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Apply ordering and pagination
|
|
403
|
-
auditQuery.orderBy(orderBy, orderDir);
|
|
404
|
-
|
|
405
|
-
return auditQuery.paginate(page, perPage).get();
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Clean old audit logs
|
|
410
|
-
* @param {number} days - Keep logs for this many days
|
|
411
|
-
* @returns {Promise<Object>} Cleanup result
|
|
412
|
-
*/
|
|
413
|
-
async cleanAuditLogs(days = 90) {
|
|
414
|
-
const cutoffDate = new Date();
|
|
415
|
-
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
416
|
-
|
|
417
|
-
const result = await this.queryBuilder.connection.query(
|
|
418
|
-
`DELETE FROM ${this.auditOptions.auditTable} WHERE created_at < ?`,
|
|
419
|
-
[cutoffDate]
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
return {
|
|
423
|
-
deletedRows: result.affectedRows,
|
|
424
|
-
cutoffDate: cutoffDate.toISOString()
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Get plugin metadata
|
|
430
|
-
* @returns {Object} Plugin metadata
|
|
431
|
-
*/
|
|
432
|
-
getMetadata() {
|
|
433
|
-
return {
|
|
434
|
-
name: 'AuditPlugin',
|
|
435
|
-
version: '1.0.0',
|
|
436
|
-
description: 'Comprehensive audit logging for database operations',
|
|
437
|
-
features: [
|
|
438
|
-
'Automatic operation tracking',
|
|
439
|
-
'User context logging',
|
|
440
|
-
'Metadata support',
|
|
441
|
-
'Audit log querying',
|
|
442
|
-
'Automatic cleanup'
|
|
443
|
-
],
|
|
444
|
-
tables: [this.auditOptions.auditTable]
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
|
-
}
|