masterrecord 0.3.5 → 0.3.6

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/readme.md CHANGED
@@ -10,6 +10,7 @@
10
10
  šŸ”¹ **Multi-Database Support** - MySQL, PostgreSQL, SQLite with consistent API
11
11
  šŸ”¹ **Code-First Design** - Define entities in JavaScript, generate schema automatically
12
12
  šŸ”¹ **Fluent Query API** - Lambda-based queries with parameterized placeholders
13
+ šŸ”¹ **Query Result Caching** - Production-grade in-memory and Redis caching with automatic invalidation
13
14
  šŸ”¹ **Migration System** - CLI-driven migrations with rollback support
14
15
  šŸ”¹ **SQL Injection Protection** - Automatic parameterized queries throughout
15
16
  šŸ”¹ **Field Transformers** - Custom serialization/deserialization for complex types
@@ -34,8 +35,16 @@
34
35
  - [Querying](#querying)
35
36
  - [Migrations](#migrations)
36
37
  - [Advanced Features](#advanced-features)
38
+ - [Query Result Caching](#query-result-caching)
39
+ - [Field Transformers](#field-transformers-advanced)
40
+ - [Table Prefixes](#table-prefixes)
41
+ - [Transactions](#transactions-postgresql)
42
+ - [Multi-Context Applications](#multi-context-applications)
43
+ - [Raw SQL Queries](#raw-sql-queries)
37
44
  - [API Reference](#api-reference)
38
45
  - [Examples](#examples)
46
+ - [Performance Tips](#performance-tips)
47
+ - [Security](#security)
39
48
 
40
49
  ## Installation
41
50
 
@@ -754,6 +763,231 @@ const result = await connection.transaction(async (client) => {
754
763
  // Automatically commits on success, rolls back on error
755
764
  ```
756
765
 
766
+ ### Query Result Caching
767
+
768
+ MasterRecord includes a **production-grade two-level caching system** similar to Entity Framework and Hibernate. The cache dramatically improves performance by storing query results and automatically invalidating them when data changes.
769
+
770
+ #### How It Works
771
+
772
+ ```
773
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
774
+ │ First-Level Cache (Identity Map) │
775
+ │ - Request-scoped entity tracking │
776
+ │ - O(1) entity lookup │
777
+ │ - Already in MasterRecord │
778
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
779
+ ā–¼
780
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
781
+ │ Second-Level Cache (Query Result Cache) │
782
+ │ - Application-wide query result storage │
783
+ │ - Automatic invalidation on data changes │
784
+ │ - In-memory (development) or Redis (production) │
785
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
786
+ ```
787
+
788
+ #### Basic Usage (Default Behavior)
789
+
790
+ Caching is **enabled by default** and requires zero configuration:
791
+
792
+ ```javascript
793
+ const db = new AppContext();
794
+
795
+ // First query hits database (cache miss)
796
+ const user = db.User.where(u => u.id == $$, 1).single();
797
+
798
+ // Second identical query hits cache (99%+ faster)
799
+ const user2 = db.User.where(u => u.id == $$, 1).single();
800
+
801
+ // Update invalidates cache automatically
802
+ user2.name = "Updated";
803
+ db.saveChanges(); // Cache for User table cleared
804
+
805
+ // Next query hits database again (cache miss)
806
+ const user3 = db.User.where(u => u.id == $$, 1).single();
807
+ ```
808
+
809
+ #### Configuration
810
+
811
+ Configure caching via environment variables:
812
+
813
+ ```bash
814
+ # Development (.env)
815
+ QUERY_CACHE_ENABLED=true # Enable/disable (default: true)
816
+ QUERY_CACHE_TTL=300000 # TTL in milliseconds (default: 5 minutes)
817
+ QUERY_CACHE_SIZE=1000 # Max cache entries (default: 1000)
818
+
819
+ # Production (.env)
820
+ QUERY_CACHE_ENABLED=true
821
+ QUERY_CACHE_TTL=300 # Redis uses seconds
822
+ REDIS_URL=redis://localhost:6379 # Use Redis for distributed caching
823
+ ```
824
+
825
+ #### Disable Caching for Specific Queries
826
+
827
+ Use `.noCache()` for real-time data that shouldn't be cached:
828
+
829
+ ```javascript
830
+ // Always hit database (never cached)
831
+ const liveData = db.Analytics
832
+ .where(a => a.date == $$, today)
833
+ .noCache() // Skip cache
834
+ .toList();
835
+
836
+ // Reference data (highly cacheable)
837
+ const categories = db.Categories.toList(); // Cached for 5 minutes
838
+ ```
839
+
840
+ #### Manual Cache Control
841
+
842
+ ```javascript
843
+ const db = new AppContext();
844
+
845
+ // Check cache performance
846
+ const stats = db.getCacheStats();
847
+ console.log(stats);
848
+ // {
849
+ // size: 45,
850
+ // maxSize: 1000,
851
+ // hits: 234,
852
+ // misses: 67,
853
+ // hitRate: '77.74%',
854
+ // enabled: true
855
+ // }
856
+
857
+ // Clear cache manually
858
+ db.clearQueryCache();
859
+
860
+ // Disable caching temporarily
861
+ db.setQueryCacheEnabled(false);
862
+ const freshData = db.User.toList();
863
+ db.setQueryCacheEnabled(true);
864
+ ```
865
+
866
+ #### Redis-Based Distributed Caching (Production)
867
+
868
+ For multi-process or clustered deployments, use Redis:
869
+
870
+ ```javascript
871
+ const redis = require('redis');
872
+ const RedisQueryCache = require('masterrecord/Cache/RedisQueryCache');
873
+
874
+ class AppContext extends context {
875
+ constructor() {
876
+ super();
877
+
878
+ // Use Redis cache in production
879
+ if (process.env.NODE_ENV === 'production' && process.env.REDIS_URL) {
880
+ const redisClient = redis.createClient(process.env.REDIS_URL);
881
+ this._queryCache = new RedisQueryCache(redisClient, {
882
+ ttl: 300, // 5 minutes (seconds for Redis)
883
+ prefix: 'myapp:'
884
+ });
885
+ }
886
+ // In-memory cache used automatically in development
887
+
888
+ this.dbset(User);
889
+ }
890
+ }
891
+ ```
892
+
893
+ **Benefits of Redis cache:**
894
+ - Shared across processes (horizontally scalable)
895
+ - Pub/sub invalidation (cache stays consistent)
896
+ - Two-level cache (L1 in-memory + L2 Redis)
897
+ - Automatic failover to database on Redis errors
898
+
899
+ #### Cache Invalidation Strategy
900
+
901
+ MasterRecord automatically invalidates cache entries when data changes:
902
+
903
+ ```javascript
904
+ // Query is cached
905
+ const users = db.User.where(u => u.active == true).toList();
906
+
907
+ // Any modification to User table invalidates ALL User queries
908
+ const user = db.User.findById(1);
909
+ user.name = "Updated";
910
+ db.saveChanges(); // Invalidates all cached User queries
911
+
912
+ // Next query hits database (fresh data)
913
+ const usersAgain = db.User.where(u => u.active == true).toList();
914
+
915
+ // Queries for OTHER tables are unaffected
916
+ const posts = db.Post.toList(); // Still cached
917
+ ```
918
+
919
+ **Invalidation rules:**
920
+ - `INSERT` invalidates all queries for that table
921
+ - `UPDATE` invalidates all queries for that table
922
+ - `DELETE` invalidates all queries for that table
923
+ - Queries for other tables are not affected
924
+
925
+ #### Performance Impact
926
+
927
+ Expected performance improvements:
928
+
929
+ | Scenario | Without Cache | With Cache | Improvement |
930
+ |----------|---------------|------------|-------------|
931
+ | Single query (100 calls) | 100 DB queries | 1 DB + 99 cache | **99% faster** |
932
+ | List query (50 calls) | 50 DB queries | 1 DB + 49 cache | **98% faster** |
933
+ | Reference data (1000 calls) | 1000 DB queries | 1 DB + 999 cache | **99.9% faster** |
934
+ | Mixed operations | Baseline | 70-90% hit rate | **3-10x faster** |
935
+
936
+ **Memory usage:** ~1KB per cached query (1000 entries ā‰ˆ 1MB)
937
+
938
+ #### Best Practices
939
+
940
+ **DO cache:**
941
+ ```javascript
942
+ // Reference data (rarely changes)
943
+ const categories = db.Categories.toList();
944
+ const settings = db.Settings.toList();
945
+
946
+ // Read-heavy data (user profiles)
947
+ const user = db.User.findById(userId);
948
+
949
+ // Expensive aggregations
950
+ const stats = db.Orders
951
+ .where(o => o.status == $$, 'completed')
952
+ .count();
953
+ ```
954
+
955
+ **DON'T cache:**
956
+ ```javascript
957
+ // Real-time data (always needs fresh results)
958
+ const liveOrders = db.Orders
959
+ .where(o => o.status == $$, 'pending')
960
+ .noCache()
961
+ .toList();
962
+
963
+ // Financial transactions (critical accuracy)
964
+ const balance = db.Transactions
965
+ .where(t => t.user_id == $$, userId)
966
+ .noCache()
967
+ .toList();
968
+
969
+ // User-specific sensitive data (security concern)
970
+ const permissions = db.UserPermissions
971
+ .where(p => p.user_id == $$, userId)
972
+ .noCache()
973
+ .toList();
974
+ ```
975
+
976
+ #### Monitoring Cache Performance
977
+
978
+ ```javascript
979
+ // Log cache stats periodically
980
+ setInterval(() => {
981
+ const stats = db.getCacheStats();
982
+ console.log(`Cache: ${stats.hitRate} hit rate, ${stats.size}/${stats.maxSize} entries`);
983
+ }, 60000);
984
+
985
+ // Watch for low hit rates (< 50% might indicate poor cache strategy)
986
+ if (parseFloat(stats.hitRate) < 50) {
987
+ console.warn('Cache hit rate is low, consider tuning cache TTL or size');
988
+ }
989
+ ```
990
+
757
991
  ### Multi-Context Applications
758
992
 
759
993
  Manage multiple databases in one application:
@@ -827,6 +1061,11 @@ context.saveChanges() // MySQL/SQLite (sync)
827
1061
  // Add/Remove entities
828
1062
  context.EntityName.add(entity)
829
1063
  context.remove(entity)
1064
+
1065
+ // Cache management
1066
+ context.getCacheStats() // Get cache statistics
1067
+ context.clearQueryCache() // Clear all cached queries
1068
+ context.setQueryCacheEnabled(bool) // Enable/disable caching
830
1069
  ```
831
1070
 
832
1071
  ### Query Methods
@@ -840,6 +1079,7 @@ context.remove(entity)
840
1079
  .skip(number) // Skip N records
841
1080
  .take(number) // Limit to N records
842
1081
  .include(relationship) // Eager load
1082
+ .noCache() // Disable caching for this query
843
1083
 
844
1084
  // Terminal methods (execute query)
845
1085
  .toList() // Return array
@@ -1009,7 +1249,25 @@ console.log(`${author.name} has ${posts.length} posts`);
1009
1249
 
1010
1250
  ## Performance Tips
1011
1251
 
1012
- ### 1. Use Bulk Operations
1252
+ ### 1. Leverage Query Caching
1253
+
1254
+ ```javascript
1255
+ // āœ… GOOD: Cache reference data
1256
+ const categories = db.Categories.toList(); // Cached automatically
1257
+
1258
+ // āœ… GOOD: Reuse queries (cache hits)
1259
+ const user1 = db.User.findById(123); // DB query
1260
+ const user2 = db.User.findById(123); // Cache hit (instant)
1261
+
1262
+ // āœ… GOOD: Disable cache for real-time data
1263
+ const liveOrders = db.Orders.where(o => o.status == 'pending').noCache().toList();
1264
+
1265
+ // Monitor cache performance
1266
+ const stats = db.getCacheStats();
1267
+ console.log(`Cache hit rate: ${stats.hitRate}`); // Target: > 70%
1268
+ ```
1269
+
1270
+ ### 2. Use Bulk Operations
1013
1271
 
1014
1272
  ```javascript
1015
1273
  // āŒ BAD: Multiple inserts
@@ -1027,7 +1285,7 @@ for (const item of items) {
1027
1285
  await db.saveChanges(); // Batch insert
1028
1286
  ```
1029
1287
 
1030
- ### 2. Use Indexes
1288
+ ### 3. Use Indexes
1031
1289
 
1032
1290
  ```javascript
1033
1291
  class User {
@@ -1043,7 +1301,7 @@ class User {
1043
1301
  // CREATE INDEX idx_user_status ON User(status);
1044
1302
  ```
1045
1303
 
1046
- ### 3. Limit Result Sets
1304
+ ### 4. Limit Result Sets
1047
1305
 
1048
1306
  ```javascript
1049
1307
  // āœ… GOOD: Limit results
@@ -1056,7 +1314,7 @@ const recentUsers = db.User
1056
1314
  const allUsers = db.User.all();
1057
1315
  ```
1058
1316
 
1059
- ### 4. Use Connection Pooling (PostgreSQL)
1317
+ ### 5. Use Connection Pooling (PostgreSQL)
1060
1318
 
1061
1319
  ```javascript
1062
1320
  this.env({
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Test: Query Cache Integration
3
+ *
4
+ * Tests the query result caching system integration points
5
+ */
6
+
7
+ const QueryCache = require('../Cache/QueryCache');
8
+
9
+ console.log("╔════════════════════════════════════════════════════════════════╗");
10
+ console.log("ā•‘ Query Cache Integration Test ā•‘");
11
+ console.log("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n");
12
+
13
+ let passed = 0;
14
+ let failed = 0;
15
+
16
+ // Test 1: Query cache can be instantiated
17
+ console.log("šŸ“ Test 1: Query cache instantiation");
18
+ console.log("──────────────────────────────────────────────────");
19
+
20
+ try {
21
+ const cache = new QueryCache({ ttl: 5000, maxSize: 100 });
22
+
23
+ if(cache.enabled && cache.ttl === 5000 && cache.maxSize === 100) {
24
+ console.log(" āœ“ Query cache instantiated successfully");
25
+ console.log(" āœ“ Configuration options applied correctly");
26
+ passed++;
27
+ } else {
28
+ console.log(` āœ— Cache configuration incorrect`);
29
+ failed++;
30
+ }
31
+ } catch(err) {
32
+ console.log(` āœ— Error: ${err.message}`);
33
+ failed++;
34
+ }
35
+
36
+ // Test 2: Cache key generation is deterministic
37
+ console.log("\nšŸ“ Test 2: Cache key generation");
38
+ console.log("──────────────────────────────────────────────────");
39
+
40
+ try {
41
+ const cache = new QueryCache();
42
+
43
+ const key1 = cache.generateKey('SELECT * FROM users WHERE id = ?', [1], 'users');
44
+ const key2 = cache.generateKey('SELECT * FROM users WHERE id = ?', [1], 'users');
45
+ const key3 = cache.generateKey('SELECT * FROM users WHERE id = ?', [2], 'users');
46
+
47
+ if(key1 === key2 && key1 !== key3) {
48
+ console.log(" āœ“ Cache keys are deterministic (same input = same key)");
49
+ console.log(" āœ“ Different parameters produce different keys");
50
+ passed++;
51
+ } else {
52
+ console.log(` āœ— Cache key generation not working correctly`);
53
+ failed++;
54
+ }
55
+ } catch(err) {
56
+ console.log(` āœ— Error: ${err.message}`);
57
+ failed++;
58
+ }
59
+
60
+ // Test 3: Cache set and get operations
61
+ console.log("\nšŸ“ Test 3: Cache set and get operations");
62
+ console.log("──────────────────────────────────────────────────");
63
+
64
+ try {
65
+ const cache = new QueryCache();
66
+ const key = cache.generateKey('SELECT * FROM users', [], 'users');
67
+ const data = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
68
+
69
+ cache.set(key, data, 'users');
70
+ const retrieved = cache.get(key);
71
+
72
+ if(JSON.stringify(retrieved) === JSON.stringify(data)) {
73
+ console.log(" āœ“ Data stored in cache successfully");
74
+ console.log(" āœ“ Data retrieved from cache matches stored data");
75
+ passed++;
76
+ } else {
77
+ console.log(` āœ— Retrieved data doesn't match stored data`);
78
+ failed++;
79
+ }
80
+ } catch(err) {
81
+ console.log(` āœ— Error: ${err.message}`);
82
+ failed++;
83
+ }
84
+
85
+ // Test 4: Cache miss returns null
86
+ console.log("\nšŸ“ Test 4: Cache miss behavior");
87
+ console.log("──────────────────────────────────────────────────");
88
+
89
+ try {
90
+ const cache = new QueryCache();
91
+ const key = cache.generateKey('SELECT * FROM users WHERE id = 999', [], 'users');
92
+
93
+ const result = cache.get(key);
94
+
95
+ if(result === null) {
96
+ console.log(" āœ“ Cache miss returns null");
97
+ console.log(" āœ“ Miss count incremented");
98
+ passed++;
99
+ } else {
100
+ console.log(` āœ— Cache miss didn't return null`);
101
+ failed++;
102
+ }
103
+ } catch(err) {
104
+ console.log(` āœ— Error: ${err.message}`);
105
+ failed++;
106
+ }
107
+
108
+ // Test 5: Cache invalidation by table
109
+ console.log("\nšŸ“ Test 5: Table-based cache invalidation");
110
+ console.log("──────────────────────────────────────────────────");
111
+
112
+ try {
113
+ const cache = new QueryCache();
114
+
115
+ const key1 = cache.generateKey('SELECT * FROM users', [], 'users');
116
+ const key2 = cache.generateKey('SELECT * FROM users WHERE id = 1', [], 'users');
117
+ const key3 = cache.generateKey('SELECT * FROM posts', [], 'posts');
118
+
119
+ cache.set(key1, [{ id: 1 }], 'users');
120
+ cache.set(key2, { id: 1 }, 'users');
121
+ cache.set(key3, [{ id: 1 }], 'posts');
122
+
123
+ cache.invalidateTable('users');
124
+
125
+ const result1 = cache.get(key1);
126
+ const result2 = cache.get(key2);
127
+ const result3 = cache.get(key3);
128
+
129
+ if(result1 === null && result2 === null && result3 !== null) {
130
+ console.log(" āœ“ Invalidated all entries for 'users' table");
131
+ console.log(" āœ“ Did not invalidate 'posts' table entries");
132
+ passed++;
133
+ } else {
134
+ console.log(` āœ— Invalidation didn't work as expected`);
135
+ failed++;
136
+ }
137
+ } catch(err) {
138
+ console.log(` āœ— Error: ${err.message}`);
139
+ failed++;
140
+ }
141
+
142
+ // Test 6: Cache statistics tracking
143
+ console.log("\nšŸ“ Test 6: Cache statistics");
144
+ console.log("──────────────────────────────────────────────────");
145
+
146
+ try {
147
+ const cache = new QueryCache();
148
+ const key = cache.generateKey('query', [], 'users');
149
+
150
+ cache.set(key, 'data', 'users');
151
+ cache.get(key); // Hit
152
+ cache.get('nonexistent'); // Miss
153
+
154
+ const stats = cache.getStats();
155
+
156
+ if(stats.hits === 1 && stats.misses === 1 && stats.hitRate === '50.00%') {
157
+ console.log(" āœ“ Hit count tracked correctly");
158
+ console.log(" āœ“ Miss count tracked correctly");
159
+ console.log(` āœ“ Hit rate calculated correctly: ${stats.hitRate}`);
160
+ passed++;
161
+ } else {
162
+ console.log(` āœ— Stats incorrect: ${JSON.stringify(stats)}`);
163
+ failed++;
164
+ }
165
+ } catch(err) {
166
+ console.log(` āœ— Error: ${err.message}`);
167
+ failed++;
168
+ }
169
+
170
+ // Test 7: Cache can be cleared
171
+ console.log("\nšŸ“ Test 7: Cache clearing");
172
+ console.log("──────────────────────────────────────────────────");
173
+
174
+ try {
175
+ const cache = new QueryCache();
176
+ const key = cache.generateKey('query', [], 'users');
177
+
178
+ cache.set(key, 'data', 'users');
179
+ cache.get(key);
180
+
181
+ const statsBefore = cache.getStats();
182
+ cache.clear();
183
+ const statsAfter = cache.getStats();
184
+
185
+ if(statsAfter.size === 0 && statsAfter.hits === 0 && statsAfter.misses === 0) {
186
+ console.log(" āœ“ All cache entries removed");
187
+ console.log(" āœ“ Statistics reset to zero");
188
+ passed++;
189
+ } else {
190
+ console.log(` āœ— Cache not properly cleared`);
191
+ failed++;
192
+ }
193
+ } catch(err) {
194
+ console.log(` āœ— Error: ${err.message}`);
195
+ failed++;
196
+ }
197
+
198
+ // Test 8: Cache can be disabled
199
+ console.log("\nšŸ“ Test 8: Cache enable/disable");
200
+ console.log("──────────────────────────────────────────────────");
201
+
202
+ try {
203
+ const cache = new QueryCache({ enabled: false });
204
+ const key = cache.generateKey('query', [], 'users');
205
+
206
+ cache.set(key, 'data', 'users');
207
+ const result = cache.get(key);
208
+
209
+ if(result === null && cache.cache.size === 0) {
210
+ console.log(" āœ“ Disabled cache doesn't store data");
211
+ console.log(" āœ“ Disabled cache always returns null");
212
+ passed++;
213
+ } else {
214
+ console.log(` āœ— Disabled cache still storing data`);
215
+ failed++;
216
+ }
217
+ } catch(err) {
218
+ console.log(` āœ— Error: ${err.message}`);
219
+ failed++;
220
+ }
221
+
222
+ // Test 9: LRU eviction works
223
+ console.log("\nšŸ“ Test 9: LRU eviction");
224
+ console.log("──────────────────────────────────────────────────");
225
+
226
+ try {
227
+ const cache = new QueryCache({ maxSize: 3 });
228
+
229
+ cache.set('key1', 'data1', 'table1');
230
+
231
+ // Small delay to ensure different timestamps
232
+ const delay = (ms) => {
233
+ const start = Date.now();
234
+ while (Date.now() - start < ms) {}
235
+ };
236
+
237
+ delay(2);
238
+ cache.set('key2', 'data2', 'table2');
239
+ delay(2);
240
+ cache.set('key3', 'data3', 'table3');
241
+
242
+ // Access key1 to make it recently used
243
+ cache.get('key1');
244
+
245
+ // Add key4 - should evict key2 (least recently used)
246
+ cache.set('key4', 'data4', 'table4');
247
+
248
+ const key1Result = cache.get('key1');
249
+ const key2Result = cache.get('key2');
250
+ const key3Result = cache.get('key3');
251
+ const key4Result = cache.get('key4');
252
+
253
+ if(key1Result !== null && key2Result === null && key3Result !== null && key4Result !== null) {
254
+ console.log(" āœ“ LRU eviction removed least recently used entry");
255
+ console.log(" āœ“ Recently accessed entries preserved");
256
+ passed++;
257
+ } else {
258
+ console.log(` āœ— LRU eviction didn't work correctly`);
259
+ console.log(` āœ— Debug: key1=${key1Result !== null}, key2=${key2Result !== null}, key3=${key3Result !== null}, key4=${key4Result !== null}`);
260
+ failed++;
261
+ }
262
+ } catch(err) {
263
+ console.log(` āœ— Error: ${err.message}`);
264
+ failed++;
265
+ }
266
+
267
+ // Test 10: TTL expiration (short test)
268
+ console.log("\nšŸ“ Test 10: TTL expiration");
269
+ console.log("──────────────────────────────────────────────────");
270
+
271
+ try {
272
+ const cache = new QueryCache({ ttl: 100 }); // 100ms TTL
273
+ const key = cache.generateKey('query', [], 'users');
274
+
275
+ cache.set(key, 'data', 'users');
276
+
277
+ // Should be cached immediately
278
+ const result1 = cache.get(key);
279
+
280
+ // Wait for TTL expiration
281
+ setTimeout(() => {
282
+ const result2 = cache.get(key);
283
+
284
+ if(result1 !== null && result2 === null) {
285
+ console.log(" āœ“ Data cached initially");
286
+ console.log(" āœ“ Data expired after TTL");
287
+ passed++;
288
+ } else {
289
+ console.log(` āœ— TTL expiration didn't work correctly`);
290
+ failed++;
291
+ }
292
+
293
+ // Continue with summary after async test
294
+ printSummary();
295
+ }, 150);
296
+ } catch(err) {
297
+ console.log(` āœ— Error: ${err.message}`);
298
+ failed++;
299
+ printSummary();
300
+ }
301
+
302
+ function printSummary() {
303
+
304
+ // Summary
305
+ console.log("\n╔════════════════════════════════════════════════════════════════╗");
306
+ console.log("ā•‘ Test Summary ā•‘");
307
+ console.log("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•");
308
+ console.log(`\n āœ“ Passed: ${passed}`);
309
+ console.log(` āœ— Failed: ${failed}`);
310
+ console.log(` šŸ“Š Total: ${passed + failed}\n`);
311
+
312
+ if(failed === 0) {
313
+ console.log(" šŸŽ‰ All tests passed!\n");
314
+ process.exit(0);
315
+ } else {
316
+ console.log(" āŒ Some tests failed\n");
317
+ process.exit(1);
318
+ }
319
+ }