@aetherframework/database 1.0.9 → 1.1.1
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,374 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/database/plugin/OptimisticLockPlugin
|
|
6
|
-
*/
|
|
7
|
-
import { BasePlugin } from './BasePlugin.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Optimistic Lock Plugin - Provides optimistic locking and data versioning
|
|
11
|
-
* Prevents concurrent updates and maintains version history
|
|
12
|
-
*/
|
|
13
|
-
export class OptimisticLockPlugin extends BasePlugin {
|
|
14
|
-
constructor(queryBuilder) {
|
|
15
|
-
super(queryBuilder);
|
|
16
|
-
this.versionColumn = 'version';
|
|
17
|
-
this.versionHistoryEnabled = false;
|
|
18
|
-
this.versionHistoryTable = null;
|
|
19
|
-
this.keyField = 'id';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Initialize the plugin
|
|
24
|
-
* @returns {Promise<void>}
|
|
25
|
-
*/
|
|
26
|
-
async init() {
|
|
27
|
-
if (this.initialized) return;
|
|
28
|
-
this.initialized = true;
|
|
29
|
-
this._registerMethods();
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Register plugin methods to QueryBuilder
|
|
34
|
-
* @protected
|
|
35
|
-
*/
|
|
36
|
-
_registerMethods() {
|
|
37
|
-
this.queryBuilder.withOptimisticLock = this.withOptimisticLock.bind(this);
|
|
38
|
-
this.queryBuilder.updateWithVersion = this.updateWithVersion.bind(this);
|
|
39
|
-
this.queryBuilder.createVersionHistory = this.createVersionHistory.bind(this);
|
|
40
|
-
this.queryBuilder.getVersionHistory = this.getVersionHistory.bind(this);
|
|
41
|
-
this.queryBuilder.restoreFromVersion = this.restoreFromVersion.bind(this);
|
|
42
|
-
|
|
43
|
-
// Override update method to add version checking
|
|
44
|
-
const originalUpdate = this.queryBuilder.update;
|
|
45
|
-
this.queryBuilder.update = function(data) {
|
|
46
|
-
// If optimistic lock is enabled and version column exists in data
|
|
47
|
-
if (this.versionColumn && data && data[this.versionColumn] !== undefined) {
|
|
48
|
-
const expectedVersion = data[this.versionColumn];
|
|
49
|
-
delete data[this.versionColumn];
|
|
50
|
-
|
|
51
|
-
// Add version check to WHERE clause
|
|
52
|
-
this.where(this.versionColumn, '=', expectedVersion);
|
|
53
|
-
|
|
54
|
-
// Increment version
|
|
55
|
-
data[this.versionColumn] = expectedVersion + 1;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return originalUpdate.call(this, data);
|
|
59
|
-
}.bind(this.queryBuilder);
|
|
60
|
-
|
|
61
|
-
return this;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Cleanup plugin resources
|
|
66
|
-
* @returns {Promise<void>}
|
|
67
|
-
*/
|
|
68
|
-
async cleanup() {
|
|
69
|
-
delete this.queryBuilder.withOptimisticLock;
|
|
70
|
-
delete this.queryBuilder.updateWithVersion;
|
|
71
|
-
delete this.queryBuilder.createVersionHistory;
|
|
72
|
-
delete this.queryBuilder.getVersionHistory;
|
|
73
|
-
delete this.queryBuilder.restoreFromVersion;
|
|
74
|
-
|
|
75
|
-
// Restore original update method
|
|
76
|
-
// Note: This requires storing the original method reference
|
|
77
|
-
|
|
78
|
-
this.initialized = false;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Enable optimistic locking
|
|
83
|
-
* @param {Object} options - Configuration options
|
|
84
|
-
* @param {string} options.column - Version column name (default: 'version')
|
|
85
|
-
* @param {boolean} options.autoIncrement - Auto increment version (default: true)
|
|
86
|
-
* @returns {QueryBuilder} Query builder instance
|
|
87
|
-
*/
|
|
88
|
-
withOptimisticLock(options = {}) {
|
|
89
|
-
this.versionColumn = options.column || 'version';
|
|
90
|
-
this.autoIncrement = options.autoIncrement !== false;
|
|
91
|
-
this.queryBuilder.query.optimisticLock = true;
|
|
92
|
-
|
|
93
|
-
return this.queryBuilder;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Update with version check (optimistic locking)
|
|
98
|
-
* @param {Object} data - Data to update
|
|
99
|
-
* @param {number} expectedVersion - Expected version number
|
|
100
|
-
* @param {Object} options - Update options
|
|
101
|
-
* @param {string} options.changedBy - User who made the change
|
|
102
|
-
* @param {string} options.changeReason - Reason for the change
|
|
103
|
-
* @returns {Promise<Object>} Update result
|
|
104
|
-
*/
|
|
105
|
-
async updateWithVersion(data, expectedVersion, options = {}) {
|
|
106
|
-
if (!this.versionColumn) {
|
|
107
|
-
throw new Error('Optimistic lock is not enabled. Call withOptimisticLock() first.');
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Store original data for version history
|
|
111
|
-
const originalData = await this.queryBuilder.clone().first();
|
|
112
|
-
|
|
113
|
-
if (!originalData) {
|
|
114
|
-
throw new Error('Record not found');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Check version
|
|
118
|
-
if (originalData[this.versionColumn] !== expectedVersion) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
'Optimistic lock failed: record was modified by another transaction. ' +
|
|
121
|
-
`Expected version: ${expectedVersion}, Actual version: ${originalData[this.versionColumn]}`
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Increment version
|
|
126
|
-
const updateData = { ...data };
|
|
127
|
-
updateData[this.versionColumn] = expectedVersion + 1;
|
|
128
|
-
|
|
129
|
-
// Add version check to WHERE clause
|
|
130
|
-
this.queryBuilder.where(this.versionColumn, '=', expectedVersion);
|
|
131
|
-
|
|
132
|
-
// Execute update
|
|
133
|
-
const result = await this.queryBuilder.update(updateData).execute();
|
|
134
|
-
|
|
135
|
-
if (result.affectedRows === 0) {
|
|
136
|
-
throw new Error('Optimistic lock failed: record was modified by another transaction');
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Record version history if enabled
|
|
140
|
-
if (this.versionHistoryEnabled && this.versionHistoryTable) {
|
|
141
|
-
await this._recordVersionHistory(originalData, updateData, options);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
...result,
|
|
146
|
-
newVersion: updateData[this.versionColumn],
|
|
147
|
-
previousVersion: expectedVersion
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Create version history table
|
|
153
|
-
* @param {Object} options - Configuration options
|
|
154
|
-
* @param {string} options.tableName - History table name (default: `${tableName}_history`)
|
|
155
|
-
* @param {string} options.keyField - Primary key field name (default: 'id')
|
|
156
|
-
* @param {boolean} options.autoCreate - Auto create table if not exists (default: true)
|
|
157
|
-
* @returns {Promise<QueryBuilder>} Query builder instance
|
|
158
|
-
*/
|
|
159
|
-
async createVersionHistory(options = {}) {
|
|
160
|
-
const tableName = options.tableName || `${this.queryBuilder.tableName}_history`;
|
|
161
|
-
const keyField = options.keyField || 'id';
|
|
162
|
-
const autoCreate = options.autoCreate !== false;
|
|
163
|
-
|
|
164
|
-
this.versionHistoryEnabled = true;
|
|
165
|
-
this.versionHistoryTable = tableName;
|
|
166
|
-
this.keyField = keyField;
|
|
167
|
-
|
|
168
|
-
if (autoCreate) {
|
|
169
|
-
await this._createHistoryTable(tableName, keyField);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Add hook to record version history
|
|
173
|
-
this.queryBuilder.addHook('beforeUpdate', async (data, query) => {
|
|
174
|
-
await this._recordVersionHistoryOnUpdate(data, query);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
return this.queryBuilder;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Get version history for a record
|
|
182
|
-
* @param {number|string} recordId - Record ID
|
|
183
|
-
* @param {Object} options - Query options
|
|
184
|
-
* @param {number} options.limit - Limit results
|
|
185
|
-
* @param {number} options.offset - Offset for pagination
|
|
186
|
-
* @param {string} options.orderBy - Order by column
|
|
187
|
-
* @param {string} options.orderDirection - Order direction (ASC/DESC)
|
|
188
|
-
* @returns {Promise<Array>} Version history
|
|
189
|
-
*/
|
|
190
|
-
async getVersionHistory(recordId, options = {}) {
|
|
191
|
-
if (!this.versionHistoryEnabled || !this.versionHistoryTable) {
|
|
192
|
-
throw new Error('Version history is not enabled. Call createVersionHistory() first.');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const {
|
|
196
|
-
limit = 100,
|
|
197
|
-
offset = 0,
|
|
198
|
-
orderBy = 'changed_at',
|
|
199
|
-
orderDirection = 'DESC'
|
|
200
|
-
} = options;
|
|
201
|
-
|
|
202
|
-
const historyQuery = new this.queryBuilder.constructor(
|
|
203
|
-
this.versionHistoryTable,
|
|
204
|
-
this.queryBuilder.connection,
|
|
205
|
-
this.queryBuilder.dialect
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
return historyQuery
|
|
209
|
-
.where(this.keyField, '=', recordId)
|
|
210
|
-
.orderBy(orderBy, orderDirection)
|
|
211
|
-
.limit(limit)
|
|
212
|
-
.offset(offset)
|
|
213
|
-
.get();
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Restore record from specific version
|
|
218
|
-
* @param {number} historyId - History record ID
|
|
219
|
-
* @param {Object} options - Restore options
|
|
220
|
-
* @param {string} options.restoredBy - User who performed restoration
|
|
221
|
-
* @param {string} options.restoreReason - Reason for restoration
|
|
222
|
-
* @returns {Promise<Object>} Restore result
|
|
223
|
-
*/
|
|
224
|
-
async restoreFromVersion(historyId, options = {}) {
|
|
225
|
-
if (!this.versionHistoryEnabled || !this.versionHistoryTable) {
|
|
226
|
-
throw new Error('Version history is not enabled. Call createVersionHistory() first.');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Get history record
|
|
230
|
-
const historyQuery = new this.queryBuilder.constructor(
|
|
231
|
-
this.versionHistoryTable,
|
|
232
|
-
this.queryBuilder.connection,
|
|
233
|
-
this.queryBuilder.dialect
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
const historyRecord = await historyQuery
|
|
237
|
-
.where('history_id', '=', historyId)
|
|
238
|
-
.first();
|
|
239
|
-
|
|
240
|
-
if (!historyRecord) {
|
|
241
|
-
throw new Error(`Version history record ${historyId} not found`);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Parse historical data
|
|
245
|
-
const historicalData = typeof historyRecord.data === 'string'
|
|
246
|
-
? JSON.parse(historyRecord.data)
|
|
247
|
-
: historyRecord.data;
|
|
248
|
-
|
|
249
|
-
// Update current record with historical data
|
|
250
|
-
const updateData = {
|
|
251
|
-
...historicalData,
|
|
252
|
-
[this.versionColumn]: historyRecord.version,
|
|
253
|
-
restored_from_version: historyId,
|
|
254
|
-
restored_at: new Date(),
|
|
255
|
-
restored_by: options.restoredBy || null,
|
|
256
|
-
restore_reason: options.restoreReason || null
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
// Remove history-specific fields
|
|
260
|
-
delete updateData.history_id;
|
|
261
|
-
delete updateData.changed_at;
|
|
262
|
-
delete updateData.changed_by;
|
|
263
|
-
|
|
264
|
-
return this.queryBuilder
|
|
265
|
-
.where(this.keyField, '=', historyRecord[this.keyField])
|
|
266
|
-
.update(updateData)
|
|
267
|
-
.execute();
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Create history table
|
|
272
|
-
* @private
|
|
273
|
-
*/
|
|
274
|
-
async _createHistoryTable(tableName, keyField) {
|
|
275
|
-
const sql = `
|
|
276
|
-
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
277
|
-
history_id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
278
|
-
${keyField} BIGINT NOT NULL,
|
|
279
|
-
version INT NOT NULL,
|
|
280
|
-
data JSON NOT NULL,
|
|
281
|
-
changed_by VARCHAR(255),
|
|
282
|
-
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
283
|
-
change_type VARCHAR(50),
|
|
284
|
-
change_reason TEXT,
|
|
285
|
-
INDEX idx_${keyField}_version (${keyField}, version),
|
|
286
|
-
INDEX idx_changed_at (changed_at)
|
|
287
|
-
)
|
|
288
|
-
`;
|
|
289
|
-
|
|
290
|
-
await this.queryBuilder.connection.query(sql);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Record version history on update
|
|
295
|
-
* @private
|
|
296
|
-
*/
|
|
297
|
-
async _recordVersionHistoryOnUpdate(data, query) {
|
|
298
|
-
if (!this.versionHistoryEnabled || !this.versionHistoryTable) {
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const current = await query.clone().first();
|
|
303
|
-
if (current && current[this.versionColumn]) {
|
|
304
|
-
await this._recordVersionHistory(current, data, {
|
|
305
|
-
changed_by: data.changed_by,
|
|
306
|
-
change_type: 'UPDATE',
|
|
307
|
-
change_reason: data.change_reason
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Record version history
|
|
314
|
-
* @private
|
|
315
|
-
*/
|
|
316
|
-
async _recordVersionHistory(originalData, newData, options = {}) {
|
|
317
|
-
const historyData = {
|
|
318
|
-
[this.keyField]: originalData[this.keyField],
|
|
319
|
-
version: originalData[this.versionColumn],
|
|
320
|
-
data: JSON.stringify(originalData),
|
|
321
|
-
changed_by: options.changedBy || null,
|
|
322
|
-
change_type: options.changeType || 'UPDATE',
|
|
323
|
-
change_reason: options.changeReason || null
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const historyQuery = new this.queryBuilder.constructor(
|
|
327
|
-
this.versionHistoryTable,
|
|
328
|
-
this.queryBuilder.connection,
|
|
329
|
-
this.queryBuilder.dialect
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
await historyQuery.insert(historyData);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Get plugin configuration
|
|
337
|
-
* @returns {Object} Configuration object
|
|
338
|
-
*/
|
|
339
|
-
getConfig() {
|
|
340
|
-
return {
|
|
341
|
-
versionColumn: this.versionColumn,
|
|
342
|
-
versionHistoryEnabled: this.versionHistoryEnabled,
|
|
343
|
-
versionHistoryTable: this.versionHistoryTable,
|
|
344
|
-
keyField: this.keyField,
|
|
345
|
-
autoIncrement: this.autoIncrement
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Get current version of record
|
|
351
|
-
* @param {number|string} recordId - Record ID
|
|
352
|
-
* @returns {Promise<number|null>} Current version number or null if not found
|
|
353
|
-
*/
|
|
354
|
-
async getCurrentVersion(recordId) {
|
|
355
|
-
const record = await this.queryBuilder
|
|
356
|
-
.clone()
|
|
357
|
-
.where(this.keyField, '=', recordId)
|
|
358
|
-
.select(this.versionColumn)
|
|
359
|
-
.first();
|
|
360
|
-
|
|
361
|
-
return record ? record[this.versionColumn] : null;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Compare versions
|
|
366
|
-
* @param {number|string} recordId - Record ID
|
|
367
|
-
* @param {number} expectedVersion - Expected version
|
|
368
|
-
* @returns {Promise<boolean>} True if versions match
|
|
369
|
-
*/
|
|
370
|
-
async checkVersion(recordId, expectedVersion) {
|
|
371
|
-
const currentVersion = await this.getCurrentVersion(recordId);
|
|
372
|
-
return currentVersion === expectedVersion;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/database/plugin/PerformancePlugin
|
|
6
|
-
*/
|
|
7
|
-
import { BasePlugin } from "./BasePlugin.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Performance Plugin - Provides query analysis, cost estimation, and index usage suggestions
|
|
11
|
-
*/
|
|
12
|
-
export class PerformancePlugin extends BasePlugin {
|
|
13
|
-
constructor(queryBuilder) {
|
|
14
|
-
super(queryBuilder);
|
|
15
|
-
this.pluginName = "PerformancePlugin";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
_registerMethods() {
|
|
19
|
-
// Register performance analysis methods to QueryBuilder
|
|
20
|
-
this.queryBuilder.explain = this.explain.bind(this);
|
|
21
|
-
this.queryBuilder.analyze = this.analyze.bind(this);
|
|
22
|
-
this.queryBuilder.getPerformanceMetrics = this.getPerformanceMetrics.bind(this);
|
|
23
|
-
this.queryBuilder.estimateQueryCost = this.estimateQueryCost.bind(this);
|
|
24
|
-
this.queryBuilder.getUsedIndexes = this.getUsedIndexes.bind(this);
|
|
25
|
-
this.queryBuilder.getJoinStrategy = this.getJoinStrategy.bind(this);
|
|
26
|
-
this.queryBuilder.getSortMethod = this.getSortMethod.bind(this);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Explain query execution plan
|
|
31
|
-
* @returns {Promise<Object>} Explain plan
|
|
32
|
-
*/
|
|
33
|
-
async explain() {
|
|
34
|
-
const { sql, bindings } = this.queryBuilder.toSQL();
|
|
35
|
-
const explainSql = `EXPLAIN ${sql}`;
|
|
36
|
-
return this.queryBuilder.executeQuery(explainSql, bindings);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Analyze query performance
|
|
41
|
-
* @returns {Promise<Object>} Analysis results
|
|
42
|
-
*/
|
|
43
|
-
async analyze() {
|
|
44
|
-
const { sql, bindings } = this.queryBuilder.toSQL();
|
|
45
|
-
const analyzeSql = `ANALYZE ${sql}`;
|
|
46
|
-
return this.queryBuilder.executeQuery(analyzeSql, bindings);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get query performance metrics
|
|
51
|
-
* @returns {Object} Performance metrics
|
|
52
|
-
*/
|
|
53
|
-
getPerformanceMetrics() {
|
|
54
|
-
const { sql } = this.queryBuilder.toSQL();
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
table: this.queryBuilder.tableName,
|
|
58
|
-
type: this.queryBuilder.query.type,
|
|
59
|
-
estimatedCost: this.estimateQueryCost(),
|
|
60
|
-
indexesUsed: this.getUsedIndexes(),
|
|
61
|
-
joinStrategy: this.getJoinStrategy(),
|
|
62
|
-
sortMethod: this.getSortMethod(),
|
|
63
|
-
filterConditions: this.queryBuilder.query.where.length,
|
|
64
|
-
hasSubqueries: this.queryBuilder.subQueries.size > 0,
|
|
65
|
-
hasAggregations: this.queryBuilder.query.groupBy.length > 0,
|
|
66
|
-
sqlLength: sql.length,
|
|
67
|
-
bindingsCount: this.queryBuilder.bindings.length,
|
|
68
|
-
timestamp: new Date().toISOString(),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Estimate query cost
|
|
74
|
-
* @returns {number} Estimated cost
|
|
75
|
-
*/
|
|
76
|
-
estimateQueryCost() {
|
|
77
|
-
let cost = 1; // Base cost
|
|
78
|
-
|
|
79
|
-
// Add cost for joins
|
|
80
|
-
cost += this.queryBuilder.query.joins.length * 10;
|
|
81
|
-
|
|
82
|
-
// Add cost for WHERE conditions
|
|
83
|
-
cost += this.queryBuilder.query.where.length * 2;
|
|
84
|
-
|
|
85
|
-
// Add cost for GROUP BY
|
|
86
|
-
cost += this.queryBuilder.query.groupBy.length * 5;
|
|
87
|
-
|
|
88
|
-
// Add cost for ORDER BY
|
|
89
|
-
cost += this.queryBuilder.query.orderBy.length * 3;
|
|
90
|
-
|
|
91
|
-
// Add cost for subqueries
|
|
92
|
-
cost += this.queryBuilder.subQueries.size * 20;
|
|
93
|
-
|
|
94
|
-
// Add cost for UNION
|
|
95
|
-
cost += this.queryBuilder.query.union.length * 15;
|
|
96
|
-
|
|
97
|
-
// Add cost for DISTINCT
|
|
98
|
-
if (this.queryBuilder.query.distinct) cost += 5;
|
|
99
|
-
|
|
100
|
-
// Add cost for LIMIT/OFFSET
|
|
101
|
-
if (this.queryBuilder.query.limit !== null) cost += 2;
|
|
102
|
-
if (this.queryBuilder.query.offset !== null) cost += 3;
|
|
103
|
-
|
|
104
|
-
return cost;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Get used indexes
|
|
109
|
-
* @returns {Array} List of used indexes
|
|
110
|
-
*/
|
|
111
|
-
getUsedIndexes() {
|
|
112
|
-
const indexes = new Set();
|
|
113
|
-
|
|
114
|
-
// Analyze WHERE condition fields
|
|
115
|
-
this.queryBuilder.query.where.forEach((condition) => {
|
|
116
|
-
if (condition.column && condition.type === "basic") {
|
|
117
|
-
indexes.add(`${this.queryBuilder.tableName}.${condition.column}`);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Analyze JOIN condition fields
|
|
122
|
-
this.queryBuilder.query.joins.forEach((join) => {
|
|
123
|
-
if (join.first)
|
|
124
|
-
indexes.add(`${join.table}.${join.first.split(".") || join.first}`);
|
|
125
|
-
if (join.second)
|
|
126
|
-
indexes.add(
|
|
127
|
-
`${this.queryBuilder.tableName}.${join.second.split(".") || join.second}`,
|
|
128
|
-
);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Analyze ORDER BY fields
|
|
132
|
-
this.queryBuilder.query.orderBy.forEach((order) => {
|
|
133
|
-
if (order.column && !order.raw) {
|
|
134
|
-
indexes.add(`${this.queryBuilder.tableName}.${order.column}`);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Analyze GROUP BY fields
|
|
139
|
-
this.queryBuilder.query.groupBy.forEach((column) => {
|
|
140
|
-
indexes.add(`${this.queryBuilder.tableName}.${column}`);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
return Array.from(indexes);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get join strategy
|
|
148
|
-
* @returns {Array} Join strategies
|
|
149
|
-
*/
|
|
150
|
-
getJoinStrategy() {
|
|
151
|
-
if (this.queryBuilder.query.joins.length === 0) return ["none"];
|
|
152
|
-
|
|
153
|
-
return this.queryBuilder.query.joins.map((join) => ({
|
|
154
|
-
type: join.type,
|
|
155
|
-
table: join.table,
|
|
156
|
-
condition: `${join.first} ${join.operator} ${join.second}`,
|
|
157
|
-
}));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Get sort method
|
|
162
|
-
* @returns {Array} Sort methods
|
|
163
|
-
*/
|
|
164
|
-
getSortMethod() {
|
|
165
|
-
if (this.queryBuilder.query.orderBy.length === 0) return ["none"];
|
|
166
|
-
|
|
167
|
-
return this.queryBuilder.query.orderBy.map((order) => ({
|
|
168
|
-
column: order.raw ? "raw" : order.column,
|
|
169
|
-
direction: order.raw ? "custom" : order.direction,
|
|
170
|
-
usingIndex: this.getUsedIndexes().some((idx) =>
|
|
171
|
-
idx.includes(order.column),
|
|
172
|
-
),
|
|
173
|
-
}));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/database/plugin/ResiliencePlugin
|
|
6
|
-
*/
|
|
7
|
-
import { BasePlugin } from "./BasePlugin.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Resilience Plugin - Provides query retry, timeout control, and transaction management
|
|
11
|
-
*/
|
|
12
|
-
export class ResiliencePlugin extends BasePlugin {
|
|
13
|
-
constructor(queryBuilder) {
|
|
14
|
-
super(queryBuilder);
|
|
15
|
-
this.pluginName = "ResiliencePlugin";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
_registerMethods() {
|
|
19
|
-
// Register resilience methods to QueryBuilder
|
|
20
|
-
this.queryBuilder.executeWithRetry = this.executeWithRetry.bind(this);
|
|
21
|
-
this.queryBuilder.isRetryableError = this.isRetryableError.bind(this);
|
|
22
|
-
this.queryBuilder.executeWithTimeout = this.executeWithTimeout.bind(this);
|
|
23
|
-
this.queryBuilder.executeInTransaction = this.executeInTransaction.bind(this);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Execute query with retry logic
|
|
28
|
-
* @param {number} maxRetries - Maximum number of retries
|
|
29
|
-
* @param {number} retryDelay - Delay between retries in milliseconds
|
|
30
|
-
* @returns {Promise<Object>} Query result
|
|
31
|
-
*/
|
|
32
|
-
async executeWithRetry(maxRetries = 3, retryDelay = 1000) {
|
|
33
|
-
let lastError;
|
|
34
|
-
|
|
35
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
36
|
-
try {
|
|
37
|
-
return await this.queryBuilder.execute();
|
|
38
|
-
} catch (error) {
|
|
39
|
-
lastError = error;
|
|
40
|
-
|
|
41
|
-
// Check if error is retryable
|
|
42
|
-
if (!this.isRetryableError(error) || attempt === maxRetries) {
|
|
43
|
-
throw error;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Wait before retrying
|
|
49
|
-
await new Promise((resolve) =>
|
|
50
|
-
setTimeout(resolve, retryDelay * attempt),
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
throw lastError;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Check if error is retryable
|
|
60
|
-
* @param {Error} error - Error object
|
|
61
|
-
* @returns {boolean} True if error is retryable
|
|
62
|
-
*/
|
|
63
|
-
isRetryableError(error) {
|
|
64
|
-
const retryableMessages = [
|
|
65
|
-
"deadlock",
|
|
66
|
-
"timeout",
|
|
67
|
-
"connection",
|
|
68
|
-
"lock",
|
|
69
|
-
"busy",
|
|
70
|
-
"try again",
|
|
71
|
-
"retry",
|
|
72
|
-
"temporary",
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
const errorMessage = error.message.toLowerCase();
|
|
76
|
-
return retryableMessages.some((msg) => errorMessage.includes(msg));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Execute query with timeout
|
|
81
|
-
* @param {number} timeout - Timeout in milliseconds
|
|
82
|
-
* @returns {Promise<Object>} Query result
|
|
83
|
-
*/
|
|
84
|
-
async executeWithTimeout(timeout = 30000) {
|
|
85
|
-
return Promise.race([
|
|
86
|
-
this.queryBuilder.execute(),
|
|
87
|
-
new Promise((_, reject) => {
|
|
88
|
-
setTimeout(
|
|
89
|
-
() => reject(new Error(`Query timeout after ${timeout}ms`)),
|
|
90
|
-
timeout,
|
|
91
|
-
);
|
|
92
|
-
}),
|
|
93
|
-
]);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Execute query with transaction
|
|
98
|
-
* @param {Function} callback - Transaction callback
|
|
99
|
-
* @returns {Promise<Object>} Transaction result
|
|
100
|
-
*/
|
|
101
|
-
async executeInTransaction(callback) {
|
|
102
|
-
try {
|
|
103
|
-
await this.queryBuilder.connection.beginTransaction();
|
|
104
|
-
|
|
105
|
-
const result = await callback(this.queryBuilder);
|
|
106
|
-
|
|
107
|
-
await this.queryBuilder.connection.commit();
|
|
108
|
-
return result;
|
|
109
|
-
} catch (error) {
|
|
110
|
-
await this.queryBuilder.connection.rollback();
|
|
111
|
-
throw error;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|