@aetherframework/database 1.1.0 → 1.1.2

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.
Files changed (47) hide show
  1. package/examples/mysql-test-pressure.js +1530 -0
  2. package/examples/test-direct.js +116 -0
  3. package/examples/transaction_example.js +127 -0
  4. package/package.json +3 -1
  5. package/src/DatabaseManager.js +565 -0
  6. package/src/core/ConnectionManager.js +351 -0
  7. package/src/core/DatabaseFactory.js +188 -0
  8. package/src/core/MongoQueryBuilder.js +576 -0
  9. package/src/core/PluginManager.js +968 -0
  10. package/src/core/QueryBuilder.js +4394 -0
  11. package/src/core/TransactionManager.js +40 -0
  12. package/src/drivers/clickhouse-driver.js +272 -0
  13. package/src/drivers/index.js +273 -0
  14. package/src/drivers/mongodb-driver.js +87 -0
  15. package/src/drivers/mssql-driver.js +117 -0
  16. package/src/drivers/mysql-driver.js +169 -0
  17. package/src/drivers/oracle-driver.js +101 -0
  18. package/src/drivers/postgres-driver.js +234 -0
  19. package/src/drivers/redis-driver.js +52 -0
  20. package/src/drivers/sqlite-driver.js +67 -0
  21. package/src/middleware/connection-pool.js +455 -0
  22. package/src/middleware/performance-monitor.js +652 -0
  23. package/src/middleware/query-cache.js +500 -0
  24. package/src/middleware/query-logger.js +262 -0
  25. package/src/plugins/AuditPlugin.js +447 -0
  26. package/src/plugins/BasePlugin.js +418 -0
  27. package/src/plugins/BatchOperationPlugin.js +165 -0
  28. package/src/plugins/CachePlugin.js +407 -0
  29. package/src/plugins/CtePlugin.js +523 -0
  30. package/src/plugins/DistributedPlugin.js +543 -0
  31. package/src/plugins/EncryptionPlugin.js +211 -0
  32. package/src/plugins/FullTextSearchPlugin.js +164 -0
  33. package/src/plugins/GeospatialPlugin.js +219 -0
  34. package/src/plugins/GraphQLPlugin.js +162 -0
  35. package/src/plugins/HookPlugin.js +211 -0
  36. package/src/plugins/JsonPlugin.js +366 -0
  37. package/src/plugins/OptimisticLockPlugin.js +374 -0
  38. package/src/plugins/PerformancePlugin.js +175 -0
  39. package/src/plugins/ResiliencePlugin.js +114 -0
  40. package/src/plugins/ShardingPlugin.js +227 -0
  41. package/src/plugins/SoftDeletePlugin.js +258 -0
  42. package/src/plugins/SyncPlugin.js +373 -0
  43. package/src/plugins/VersioningPlugin.js +314 -0
  44. package/src/plugins/WindowFunctionPlugin.js +343 -0
  45. package/src/utils/config-loader.js +632 -0
  46. package/src/utils/error-handler.js +724 -0
  47. package/src/utils/migration-runner.js +1066 -0
@@ -0,0 +1,500 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/database/middleware/query-cache
6
+ */
7
+ import { EventEmitter } from 'events';
8
+
9
+ class QueryCacheMiddleware extends EventEmitter {
10
+ constructor(options = {}) {
11
+ super();
12
+ this.options = {
13
+ enabled: options.enabled !== false,
14
+ ttl: options.ttl || 300000, // 5 minutes default
15
+ maxSize: options.maxSize || 1000,
16
+ strategy: options.strategy || 'lru', // lru, fifo, lfu
17
+ cacheNullResults: options.cacheNullResults !== false,
18
+ cacheErrors: options.cacheErrors || false,
19
+ ...options
20
+ };
21
+
22
+ this.cache = new Map();
23
+ this.stats = {
24
+ hits: 0,
25
+ misses: 0,
26
+ sets: 0,
27
+ deletes: 0,
28
+ evictions: 0,
29
+ size: 0,
30
+ memoryUsage: 0
31
+ };
32
+
33
+ this.accessOrder = new Map(); // For LRU strategy
34
+ this.accessCount = new Map(); // For LFU strategy
35
+
36
+ this.startCleanupTimer();
37
+ }
38
+
39
+ /**
40
+ * Generate cache key from query
41
+ * @param {Object} query - Query object
42
+ * @returns {string} Cache key
43
+ */
44
+ generateCacheKey(query) {
45
+ const { sql, params = [], connectionName, type = 'query' } = query;
46
+
47
+ // Create a deterministic key
48
+ const keyData = {
49
+ sql: sql.trim().toLowerCase(),
50
+ params: JSON.stringify(params),
51
+ connection: connectionName,
52
+ type
53
+ };
54
+
55
+ return JSON.stringify(keyData);
56
+ }
57
+
58
+ /**
59
+ * Check if query should be cached
60
+ * @param {Object} query - Query object
61
+ * @returns {boolean} True if query should be cached
62
+ */
63
+ shouldCache(query) {
64
+ if (!this.options.enabled) {
65
+ return false;
66
+ }
67
+
68
+ const { sql } = query;
69
+
70
+ // Don't cache INSERT, UPDATE, DELETE queries
71
+ const lowerSql = sql.toLowerCase().trim();
72
+ if (lowerSql.startsWith('insert ') ||
73
+ lowerSql.startsWith('update ') ||
74
+ lowerSql.startsWith('delete ') ||
75
+ lowerSql.startsWith('create ') ||
76
+ lowerSql.startsWith('alter ') ||
77
+ lowerSql.startsWith('drop ') ||
78
+ lowerSql.startsWith('truncate ')) {
79
+ return false;
80
+ }
81
+
82
+ // Check for cache hints in SQL comments
83
+ if (sql.includes('/* no-cache */')) {
84
+ return false;
85
+ }
86
+
87
+ if (sql.includes('/* cache */')) {
88
+ return true;
89
+ }
90
+
91
+ // Default: cache SELECT queries
92
+ return lowerSql.startsWith('select ');
93
+ }
94
+
95
+ /**
96
+ * Get cached result
97
+ * @param {Object} query - Query object
98
+ * @returns {Object|null} Cached result or null
99
+ */
100
+ get(query) {
101
+ if (!this.options.enabled) {
102
+ return null;
103
+ }
104
+
105
+ const key = this.generateCacheKey(query);
106
+
107
+ if (!this.cache.has(key)) {
108
+ this.stats.misses++;
109
+ this.emit('cache-miss', { key, query });
110
+ return null;
111
+ }
112
+
113
+ const cached = this.cache.get(key);
114
+
115
+ // Check if cache entry has expired
116
+ if (cached.expiresAt && Date.now() > cached.expiresAt) {
117
+ this.cache.delete(key);
118
+ this.stats.evictions++;
119
+ this.stats.misses++;
120
+ this.stats.size--;
121
+ this.emit('cache-expired', { key, query });
122
+ return null;
123
+ }
124
+
125
+ // Update access order for LRU
126
+ if (this.options.strategy === 'lru') {
127
+ this.accessOrder.set(key, Date.now());
128
+ }
129
+
130
+ // Update access count for LFU
131
+ if (this.options.strategy === 'lfu') {
132
+ const count = this.accessCount.get(key) || 0;
133
+ this.accessCount.set(key, count + 1);
134
+ }
135
+
136
+ this.stats.hits++;
137
+ this.emit('cache-hit', {
138
+ key,
139
+ query,
140
+ cachedAt: cached.cachedAt,
141
+ expiresAt: cached.expiresAt,
142
+ ttl: cached.ttl
143
+ });
144
+
145
+ return cached.result;
146
+ }
147
+
148
+ /**
149
+ * Set cache result
150
+ * @param {Object} query - Query object
151
+ * @param {Object} result - Query result
152
+ * @param {number} ttl - Time to live in milliseconds
153
+ */
154
+ set(query, result, ttl = null) {
155
+ if (!this.options.enabled || !this.shouldCache(query)) {
156
+ return;
157
+ }
158
+
159
+ // Don't cache null results if configured
160
+ if (!this.options.cacheNullResults && (result === null || result === undefined)) {
161
+ return;
162
+ }
163
+
164
+ // Don't cache errors if configured
165
+ if (!this.options.cacheErrors && result.error) {
166
+ return;
167
+ }
168
+
169
+ const key = this.generateCacheKey(query);
170
+ const actualTtl = ttl || this.options.ttl;
171
+ const expiresAt = Date.now() + actualTtl;
172
+
173
+ // Check cache size limit
174
+ if (this.cache.size >= this.options.maxSize) {
175
+ this.evict();
176
+ }
177
+
178
+ const cacheEntry = {
179
+ result,
180
+ cachedAt: Date.now(),
181
+ expiresAt,
182
+ ttl: actualTtl,
183
+ query: {
184
+ sql: query.sql.substring(0, 100) + (query.sql.length > 100 ? '...' : ''),
185
+ params: query.params,
186
+ connection: query.connectionName,
187
+ type: query.type
188
+ }
189
+ };
190
+
191
+ this.cache.set(key, cacheEntry);
192
+
193
+ // Update access order for LRU
194
+ if (this.options.strategy === 'lru') {
195
+ this.accessOrder.set(key, Date.now());
196
+ }
197
+
198
+ // Initialize access count for LFU
199
+ if (this.options.strategy === 'lfu') {
200
+ this.accessCount.set(key, 1);
201
+ }
202
+
203
+ this.stats.sets++;
204
+ this.stats.size = this.cache.size;
205
+ this.stats.memoryUsage = this.estimateMemoryUsage();
206
+
207
+ this.emit('cache-set', {
208
+ key,
209
+ query: cacheEntry.query,
210
+ cachedAt: cacheEntry.cachedAt,
211
+ expiresAt: cacheEntry.expiresAt,
212
+ ttl: cacheEntry.ttl
213
+ });
214
+ }
215
+
216
+ /**
217
+ * Delete cache entry
218
+ * @param {Object} query - Query object
219
+ */
220
+ delete(query) {
221
+ const key = this.generateCacheKey(query);
222
+ if (this.cache.delete(key)) {
223
+ this.accessOrder.delete(key);
224
+ this.accessCount.delete(key);
225
+ this.stats.deletes++;
226
+ this.stats.size = this.cache.size;
227
+ this.stats.memoryUsage = this.estimateMemoryUsage();
228
+ this.emit('cache-delete', { key, query });
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Clear all cache entries
234
+ * @param {string} pattern - Pattern to match (optional)
235
+ */
236
+ clear(pattern = null) {
237
+ if (pattern) {
238
+ const regex = new RegExp(pattern);
239
+ for (const [key] of this.cache.entries()) {
240
+ if (regex.test(key)) {
241
+ this.cache.delete(key);
242
+ this.accessOrder.delete(key);
243
+ this.accessCount.delete(key);
244
+ }
245
+ }
246
+ } else {
247
+ this.cache.clear();
248
+ this.accessOrder.clear();
249
+ this.accessCount.clear();
250
+ }
251
+
252
+ this.stats.size = this.cache.size;
253
+ this.stats.memoryUsage = this.estimateMemoryUsage();
254
+ this.emit('cache-clear', { pattern });
255
+ }
256
+
257
+ /**
258
+ * Evict entries based on strategy
259
+ */
260
+ evict() {
261
+ const entriesToEvict = Math.max(1, Math.floor(this.options.maxSize * 0.1)); // Evict 10%
262
+
263
+ switch (this.options.strategy) {
264
+ case 'lru':
265
+ this.evictLRU(entriesToEvict);
266
+ break;
267
+ case 'lfu':
268
+ this.evictLFU(entriesToEvict);
269
+ break;
270
+ case 'fifo':
271
+ default:
272
+ this.evictFIFO(entriesToEvict);
273
+ break;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Evict using LRU (Least Recently Used) strategy
279
+ * @param {number} count - Number of entries to evict
280
+ */
281
+ evictLRU(count) {
282
+ const entries = Array.from(this.accessOrder.entries())
283
+ .sort((a, b) => a - b) // Sort by access time (oldest first)
284
+ .slice(0, count);
285
+
286
+ for (const [key] of entries) {
287
+ this.cache.delete(key);
288
+ this.accessOrder.delete(key);
289
+ this.accessCount.delete(key);
290
+ this.stats.evictions++;
291
+ }
292
+
293
+ this.stats.size = this.cache.size;
294
+ this.emit('cache-evicted', { strategy: 'lru', count: entries.length });
295
+ }
296
+
297
+ /**
298
+ * Evict using LFU (Least Frequently Used) strategy
299
+ * @param {number} count - Number of entries to evict
300
+ */
301
+ evictLFU(count) {
302
+ const entries = Array.from(this.accessCount.entries())
303
+ .sort((a, b) => a - b) // Sort by access count (least frequent first)
304
+ .slice(0, count);
305
+
306
+ for (const [key] of entries) {
307
+ this.cache.delete(key);
308
+ this.accessOrder.delete(key);
309
+ this.accessCount.delete(key);
310
+ this.stats.evictions++;
311
+ }
312
+
313
+ this.stats.size = this.cache.size;
314
+ this.emit('cache-evicted', { strategy: 'lfu', count: entries.length });
315
+ }
316
+
317
+ /**
318
+ * Evict using FIFO (First In First Out) strategy
319
+ * @param {number} count - Number of entries to evict
320
+ */
321
+ evictFIFO(count) {
322
+ const entries = Array.from(this.cache.entries())
323
+ .sort((a, b) => a.cachedAt - b.cachedAt) // Sort by cache time (oldest first)
324
+ .slice(0, count);
325
+
326
+ for (const [key] of entries) {
327
+ this.cache.delete(key);
328
+ this.accessOrder.delete(key);
329
+ this.accessCount.delete(key);
330
+ this.stats.evictions++;
331
+ }
332
+
333
+ this.stats.size = this.cache.size;
334
+ this.emit('cache-evicted', { strategy: 'fifo', count: entries.length });
335
+ }
336
+
337
+ /**
338
+ * Start cleanup timer
339
+ */
340
+ startCleanupTimer() {
341
+ setInterval(() => {
342
+ this.cleanupExpired();
343
+ }, 60000); // Cleanup every minute
344
+ }
345
+
346
+ /**
347
+ * Cleanup expired cache entries
348
+ */
349
+ cleanupExpired() {
350
+ const now = Date.now();
351
+ let expiredCount = 0;
352
+
353
+ for (const [key, entry] of this.cache.entries()) {
354
+ if (entry.expiresAt && now > entry.expiresAt) {
355
+ this.cache.delete(key);
356
+ this.accessOrder.delete(key);
357
+ this.accessCount.delete(key);
358
+ expiredCount++;
359
+ this.stats.evictions++;
360
+ }
361
+ }
362
+
363
+ if (expiredCount > 0) {
364
+ this.stats.size = this.cache.size;
365
+ this.stats.memoryUsage = this.estimateMemoryUsage();
366
+ this.emit('cache-cleanup', { expiredCount });
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Estimate memory usage
372
+ * @returns {number} Estimated memory usage in bytes
373
+ */
374
+ estimateMemoryUsage() {
375
+ let total = 0;
376
+ for (const [key, value] of this.cache.entries()) {
377
+ total += key.length * 2; // UTF-16 string
378
+ total += JSON.stringify(value).length * 2;
379
+ }
380
+ return total;
381
+ }
382
+
383
+ /**
384
+ * Get cache statistics
385
+ * @returns {Object} Cache statistics
386
+ */
387
+ getStats() {
388
+ const hitRate = this.stats.hits + this.stats.misses > 0
389
+ ? (this.stats.hits / (this.stats.hits + this.stats.misses) * 100).toFixed(2) + '%'
390
+ : '0%';
391
+
392
+ return {
393
+ ...this.stats,
394
+ hitRate,
395
+ strategy: this.options.strategy,
396
+ ttl: this.options.ttl,
397
+ maxSize: this.options.maxSize,
398
+ enabled: this.options.enabled,
399
+ cacheNullResults: this.options.cacheNullResults,
400
+ cacheErrors: this.options.cacheErrors,
401
+ timestamp: new Date().toISOString()
402
+ };
403
+ }
404
+
405
+ /**
406
+ * Get cache entries
407
+ * @param {number} limit - Maximum number of entries to return
408
+ * @returns {Array} Cache entries
409
+ */
410
+ getEntries(limit = 100) {
411
+ const entries = [];
412
+ let count = 0;
413
+
414
+ for (const [key, entry] of this.cache.entries()) {
415
+ if (count >= limit) break;
416
+
417
+ entries.push({
418
+ key: key.substring(0, 100) + (key.length > 100 ? '...' : ''),
419
+ query: entry.query,
420
+ cachedAt: new Date(entry.cachedAt).toISOString(),
421
+ expiresAt: entry.expiresAt ? new Date(entry.expiresAt).toISOString() : null,
422
+ ttl: entry.ttl,
423
+ age: Date.now() - entry.cachedAt,
424
+ expiresIn: entry.expiresAt ? entry.expiresAt - Date.now() : null
425
+ });
426
+
427
+ count++;
428
+ }
429
+
430
+ return entries;
431
+ }
432
+
433
+ /**
434
+ * Invalidate cache by pattern
435
+ * @param {string} pattern - Pattern to match
436
+ * @returns {number} Number of invalidated entries
437
+ */
438
+ invalidate(pattern) {
439
+ const regex = new RegExp(pattern);
440
+ let invalidated = 0;
441
+
442
+ for (const [key] of this.cache.entries()) {
443
+ if (regex.test(key)) {
444
+ this.cache.delete(key);
445
+ this.accessOrder.delete(key);
446
+ this.accessCount.delete(key);
447
+ invalidated++;
448
+ }
449
+ }
450
+
451
+ if (invalidated > 0) {
452
+ this.stats.size = this.cache.size;
453
+ this.stats.memoryUsage = this.estimateMemoryUsage();
454
+ this.emit('cache-invalidated', { pattern, count: invalidated });
455
+ }
456
+
457
+ return invalidated;
458
+ }
459
+
460
+ /**
461
+ * Pre-warm cache with queries
462
+ * @param {Array} queries - Array of queries to pre-warm
463
+ * @param {Function} executeQuery - Function to execute query
464
+ * @returns {Promise<Array>} Pre-warm results
465
+ */
466
+ async prewarm(queries, executeQuery) {
467
+ const results = [];
468
+
469
+ for (const query of queries) {
470
+ try {
471
+ const result = await executeQuery(query);
472
+ this.set(query, result);
473
+ results.push({ query, success: true });
474
+ } catch (error) {
475
+ results.push({ query, success: false, error: error.message });
476
+ }
477
+ }
478
+
479
+ this.emit('cache-prewarmed', { count: queries.length, results });
480
+ return results;
481
+ }
482
+
483
+ /**
484
+ * Reset cache statistics
485
+ */
486
+ resetStats() {
487
+ this.stats = {
488
+ hits: 0,
489
+ misses: 0,
490
+ sets: 0,
491
+ deletes: 0,
492
+ evictions: 0,
493
+ size: this.cache.size,
494
+ memoryUsage: this.estimateMemoryUsage()
495
+ };
496
+ this.emit('stats-reset');
497
+ }
498
+ }
499
+
500
+ export default QueryCacheMiddleware;