masterrecord 0.3.7 → 0.3.8

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.
@@ -0,0 +1,445 @@
1
+ # Query Caching Guide - Like Active Record
2
+
3
+ MasterRecord's query caching works like **Active Record** in Rails:
4
+ - **Opt-in** with `.cache()`
5
+ - **Request-scoped** (cleared after each request)
6
+ - **Automatic invalidation** on data changes
7
+
8
+ ---
9
+
10
+ ## Quick Start
11
+
12
+ ### 1. Express Middleware (Recommended)
13
+
14
+ ```javascript
15
+ const express = require('express');
16
+ const AppContext = require('./models/AppContext');
17
+
18
+ const app = express();
19
+
20
+ // Middleware: Create context per request, auto-clear cache
21
+ app.use((req, res, next) => {
22
+ req.db = new AppContext();
23
+
24
+ // Clear cache when request ends (like Active Record)
25
+ res.on('finish', () => {
26
+ req.db.endRequest();
27
+ });
28
+
29
+ next();
30
+ });
31
+
32
+ // Routes
33
+ app.get('/api/categories', (req, res) => {
34
+ // Opt-in caching with .cache()
35
+ const categories = req.db.Categories.cache().toList();
36
+ res.json(categories);
37
+ // Cache auto-cleared after response
38
+ });
39
+
40
+ app.get('/api/users/:id', (req, res) => {
41
+ // No .cache() = always fresh (default)
42
+ const user = req.db.User.findById(req.params.id);
43
+ res.json(user);
44
+ });
45
+
46
+ app.listen(3000);
47
+ ```
48
+
49
+ ---
50
+
51
+ ## How It Works
52
+
53
+ ### Default Behavior (No Caching)
54
+
55
+ ```javascript
56
+ // Without .cache() - always hits database
57
+ const user1 = db.User.findById(1); // DB query
58
+ const user2 = db.User.findById(1); // DB query again (no cache)
59
+ ```
60
+
61
+ ### Opt-In Caching
62
+
63
+ ```javascript
64
+ // With .cache() - caches result
65
+ const categories1 = db.Categories.cache().toList(); // DB query, cached
66
+ const categories2 = db.Categories.cache().toList(); // Cache hit!
67
+ ```
68
+
69
+ ### Request-Scoped (Auto-Clear)
70
+
71
+ ```javascript
72
+ // Request 1
73
+ app.get('/route1', (req, res) => {
74
+ const cats = req.db.Categories.cache().toList(); // DB query
75
+ res.json(cats);
76
+ // Cache cleared after response
77
+ });
78
+
79
+ // Request 2 - starts with empty cache
80
+ app.get('/route2', (req, res) => {
81
+ const cats = req.db.Categories.cache().toList(); // DB query again (fresh)
82
+ res.json(cats);
83
+ });
84
+ ```
85
+
86
+ ---
87
+
88
+ ## When to Use `.cache()`
89
+
90
+ ### ✅ DO use .cache() for:
91
+
92
+ ```javascript
93
+ // Reference data (rarely changes)
94
+ const categories = db.Categories.cache().toList();
95
+ const countries = db.Countries.cache().toList();
96
+ const settings = db.Settings.cache().toList();
97
+
98
+ // Expensive aggregations (within a request)
99
+ const totalOrders = db.Orders
100
+ .where(o => o.status == $$, 'completed')
101
+ .cache()
102
+ .count();
103
+
104
+ // Lookup tables
105
+ const roles = db.Roles.cache().toList();
106
+ const permissions = db.Permissions.cache().toList();
107
+ ```
108
+
109
+ ### ❌ DON'T use .cache() for:
110
+
111
+ ```javascript
112
+ // User-specific data (default is safe)
113
+ const user = db.User.findById(userId); // No .cache()
114
+
115
+ // Real-time data
116
+ const liveOrders = db.Orders
117
+ .where(o => o.status == $$, 'pending')
118
+ .toList(); // No .cache()
119
+
120
+ // Financial/sensitive data
121
+ const transactions = db.Transactions
122
+ .where(t => t.user_id == $$, userId)
123
+ .toList(); // No .cache()
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Complete Examples
129
+
130
+ ### Example 1: E-Commerce API
131
+
132
+ ```javascript
133
+ const express = require('express');
134
+ const AppContext = require('./models/AppContext');
135
+
136
+ const app = express();
137
+
138
+ // Middleware: Request-scoped caching
139
+ app.use((req, res, next) => {
140
+ req.db = new AppContext();
141
+ res.on('finish', () => req.db.endRequest());
142
+ next();
143
+ });
144
+
145
+ // Categories - cache (rarely changes)
146
+ app.get('/api/categories', (req, res) => {
147
+ const categories = req.db.Categories
148
+ .cache() // Cache for this request
149
+ .toList();
150
+ res.json(categories);
151
+ });
152
+
153
+ // Products - cache (within request)
154
+ app.get('/api/products', (req, res) => {
155
+ const products = req.db.Products
156
+ .where(p => p.active == true)
157
+ .cache() // Cache for this request
158
+ .toList();
159
+ res.json(products);
160
+ });
161
+
162
+ // User profile - NO cache (user-specific)
163
+ app.get('/api/profile', (req, res) => {
164
+ const user = req.db.User.findById(req.user.id); // No .cache()
165
+ res.json(user);
166
+ });
167
+
168
+ // Cart - NO cache (real-time)
169
+ app.get('/api/cart', (req, res) => {
170
+ const cart = req.db.Cart
171
+ .where(c => c.user_id == $$, req.user.id)
172
+ .toList(); // No .cache()
173
+ res.json(cart);
174
+ });
175
+
176
+ app.listen(3000);
177
+ ```
178
+
179
+ ### Example 2: Admin Dashboard
180
+
181
+ ```javascript
182
+ app.get('/admin/dashboard', async (req, res) => {
183
+ const db = new AppContext();
184
+
185
+ // Multiple queries with caching
186
+ const stats = {
187
+ // Cache expensive aggregations
188
+ totalUsers: db.User.cache().count(),
189
+ totalOrders: db.Orders.cache().count(),
190
+
191
+ // Cache reference data
192
+ categories: db.Categories.cache().toList(),
193
+
194
+ // No cache for real-time data
195
+ recentOrders: db.Orders
196
+ .orderByDescending(o => o.created_at)
197
+ .take(10)
198
+ .toList() // No .cache() - always fresh
199
+ };
200
+
201
+ res.render('dashboard', stats);
202
+
203
+ // Clear cache after response
204
+ db.endRequest();
205
+ });
206
+ ```
207
+
208
+ ### Example 3: Background Job
209
+
210
+ ```javascript
211
+ // Cron job - process orders
212
+ cron.schedule('*/5 * * * *', async () => {
213
+ const db = new AppContext();
214
+
215
+ // Get pending orders (no cache - real-time)
216
+ const orders = db.Orders
217
+ .where(o => o.status == $$, 'pending')
218
+ .toList(); // No .cache()
219
+
220
+ for (const order of orders) {
221
+ await processOrder(order);
222
+ order.status = 'processed';
223
+ }
224
+
225
+ await db.saveChanges(); // Invalidates cache
226
+
227
+ // Clear cache after job
228
+ db.endRequest();
229
+ });
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Cache Invalidation
235
+
236
+ Cache is automatically invalidated when you modify data:
237
+
238
+ ```javascript
239
+ app.post('/api/categories', (req, res) => {
240
+ const db = new AppContext();
241
+
242
+ // Cache categories
243
+ const cats = db.Categories.cache().toList(); // DB query, cached
244
+
245
+ // Add new category
246
+ const newCat = db.Categories.new();
247
+ newCat.name = req.body.name;
248
+ db.saveChanges(); // Automatically invalidates Categories cache!
249
+
250
+ // Next query hits database (fresh)
251
+ const freshCats = db.Categories.cache().toList(); // DB query (not cached)
252
+
253
+ res.json(newCat);
254
+ db.endRequest();
255
+ });
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Comparison with Active Record
261
+
262
+ ### Active Record (Rails)
263
+
264
+ ```ruby
265
+ # Rails controller
266
+ class CategoriesController < ApplicationController
267
+ def index
268
+ # Query cache automatically enabled for request
269
+ @categories = Category.where(active: true).to_a
270
+ # Cache automatically cleared after request
271
+ end
272
+ end
273
+ ```
274
+
275
+ ### MasterRecord (Node.js)
276
+
277
+ ```javascript
278
+ // Express route
279
+ app.get('/categories', (req, res) => {
280
+ // Opt-in caching with .cache()
281
+ const categories = req.db.Categories
282
+ .where(c => c.active == true)
283
+ .cache() // Explicitly opt-in
284
+ .toList();
285
+ res.json(categories);
286
+ // Cache cleared by middleware
287
+ });
288
+ ```
289
+
290
+ **Key differences:**
291
+ - Active Record: Cache **enabled by default** for all queries
292
+ - MasterRecord: Cache **opt-in** with `.cache()` (safer)
293
+ - Both: **Request-scoped** (cleared after each request)
294
+
295
+ ---
296
+
297
+ ## Configuration
298
+
299
+ ### Environment Variables
300
+
301
+ ```bash
302
+ # .env
303
+ QUERY_CACHE_TTL=5000 # 5 seconds (request-scoped)
304
+ QUERY_CACHE_SIZE=1000 # Max 1000 cached queries
305
+ QUERY_CACHE_ENABLED=true # Enable caching
306
+ ```
307
+
308
+ ### Custom TTL per Query
309
+
310
+ ```javascript
311
+ // Use default TTL (5 seconds)
312
+ const cats = db.Categories.cache().toList();
313
+
314
+ // For longer-lived cache, increase TTL in config:
315
+ // QUERY_CACHE_TTL=60000 // 1 minute
316
+ ```
317
+
318
+ ---
319
+
320
+ ## Testing
321
+
322
+ ### Unit Tests
323
+
324
+ ```javascript
325
+ const AppContext = require('../models/AppContext');
326
+
327
+ describe('Category API', () => {
328
+ let db;
329
+
330
+ beforeEach(() => {
331
+ db = new AppContext();
332
+ });
333
+
334
+ afterEach(() => {
335
+ db.endRequest(); // Clear cache after each test
336
+ });
337
+
338
+ it('caches category queries', () => {
339
+ const cats1 = db.Categories.cache().toList();
340
+ const cats2 = db.Categories.cache().toList();
341
+
342
+ const stats = db.getCacheStats();
343
+ expect(stats.hits).toBe(1); // Second query hit cache
344
+ });
345
+
346
+ it('does not cache without .cache()', () => {
347
+ const cats1 = db.Categories.toList(); // No .cache()
348
+ const cats2 = db.Categories.toList(); // No .cache()
349
+
350
+ const stats = db.getCacheStats();
351
+ expect(stats.size).toBe(0); // Nothing cached
352
+ });
353
+ });
354
+ ```
355
+
356
+ ---
357
+
358
+ ## Best Practices
359
+
360
+ ### ✅ DO:
361
+ - Use middleware to auto-clear cache per request
362
+ - Cache reference data (categories, settings, countries)
363
+ - Cache within a request for duplicate queries
364
+ - Call `db.endRequest()` at end of request
365
+ - Monitor cache stats in development
366
+
367
+ ### ❌ DON'T:
368
+ - Cache user-specific data
369
+ - Cache real-time data
370
+ - Cache financial/sensitive data
371
+ - Forget to call `endRequest()` in long-running processes
372
+ - Use `.cache()` on frequently updated tables
373
+
374
+ ---
375
+
376
+ ## Troubleshooting
377
+
378
+ ### Cache not clearing between requests
379
+
380
+ ```javascript
381
+ // ❌ BAD: No cache clearing
382
+ app.use((req, res, next) => {
383
+ req.db = new AppContext();
384
+ next();
385
+ });
386
+
387
+ // ✅ GOOD: Auto-clear cache
388
+ app.use((req, res, next) => {
389
+ req.db = new AppContext();
390
+ res.on('finish', () => req.db.endRequest()); // Clear cache
391
+ next();
392
+ });
393
+ ```
394
+
395
+ ### Stale data across requests
396
+
397
+ ```javascript
398
+ // Check TTL - should be short (5 seconds default)
399
+ console.log(process.env.QUERY_CACHE_TTL); // Should be 5000
400
+
401
+ // Make sure endRequest() is called
402
+ db.endRequest(); // Clears cache
403
+ ```
404
+
405
+ ### Low cache hit rate
406
+
407
+ ```javascript
408
+ // Check if queries are actually using .cache()
409
+ const stats = db.getCacheStats();
410
+ console.log(stats);
411
+ // {
412
+ // size: 0, // No cached queries?
413
+ // hits: 0, // No cache hits?
414
+ // misses: 10 // All misses?
415
+ // }
416
+
417
+ // Make sure to use .cache()
418
+ const cats = db.Categories.cache().toList(); // Must have .cache()!
419
+ ```
420
+
421
+ ---
422
+
423
+ ## Summary
424
+
425
+ **MasterRecord caching = Active Record style:**
426
+
427
+ 1. **Opt-in** with `.cache()` (safer than default-on)
428
+ 2. **Request-scoped** with `endRequest()` (auto-clear)
429
+ 3. **Automatic invalidation** on `saveChanges()`
430
+
431
+ **Use it like this:**
432
+ ```javascript
433
+ // Setup (once)
434
+ app.use((req, res, next) => {
435
+ req.db = new AppContext();
436
+ res.on('finish', () => req.db.endRequest());
437
+ next();
438
+ });
439
+
440
+ // Use (in routes)
441
+ const categories = req.db.Categories.cache().toList(); // Cached
442
+ const user = req.db.User.findById(id); // Not cached (default)
443
+ ```
444
+
445
+ **It just works!** ✅
@@ -10,7 +10,7 @@ class queryMethods{
10
10
  this.__entity = entity;
11
11
  this.__context = context;
12
12
  this.__queryObject = new queryScript();
13
- this.__useCache = true; // Enable caching by default
13
+ this.__useCache = false; // Disable caching by default (opt-in with .cache())
14
14
  }
15
15
 
16
16
  // build a single entity
@@ -89,11 +89,19 @@ class queryMethods{
89
89
  }
90
90
 
91
91
  /**
92
- * Disable query caching for this query
93
- * Use for queries that should always hit database
92
+ * Enable query result caching for this query
93
+ * Use for frequently accessed, rarely changed data (categories, settings, etc.)
94
+ * Cache is shared across all context instances and invalidated on saveChanges()
95
+ *
96
+ * @example
97
+ * // Cache this query result
98
+ * const categories = db.Categories.cache().toList();
99
+ *
100
+ * // Without .cache(), always hits database (default)
101
+ * const user = db.User.findById(1);
94
102
  */
95
- noCache() {
96
- this.__useCache = false;
103
+ cache() {
104
+ this.__useCache = true;
97
105
  return this;
98
106
  }
99
107
 
package/context.js CHANGED
@@ -45,7 +45,7 @@ class context {
45
45
  // Initialize shared query cache (only once across all instances)
46
46
  if (!context._sharedQueryCache) {
47
47
  context._sharedQueryCache = new QueryCache({
48
- ttl: process.env.QUERY_CACHE_TTL || 5 * 60 * 1000, // 5 min default
48
+ ttl: process.env.QUERY_CACHE_TTL || 5000, // 5 seconds default (request-scoped)
49
49
  maxSize: process.env.QUERY_CACHE_SIZE || 1000,
50
50
  enabled: process.env.QUERY_CACHE_ENABLED !== 'false'
51
51
  });
@@ -644,6 +644,24 @@ class context {
644
644
  this._queryCache.enabled = enabled;
645
645
  }
646
646
 
647
+ /**
648
+ * End request and clear query cache
649
+ * Call this at the end of each request (like Active Record)
650
+ *
651
+ * @example
652
+ * // In Express middleware
653
+ * app.use((req, res, next) => {
654
+ * req.db = new AppContext();
655
+ * res.on('finish', () => {
656
+ * req.db.endRequest(); // Clears cache
657
+ * });
658
+ * next();
659
+ * });
660
+ */
661
+ endRequest() {
662
+ this.clearQueryCache();
663
+ }
664
+
647
665
  // __track(model){
648
666
  // this.__trackedEntities.push(model);
649
667
  // return model;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "An Object-relational mapping for the Master framework. Master Record connects classes to relational database tables to establish a database with almost zero-configuration ",
5
5
  "main": "MasterRecord.js",
6
6
  "bin": {
package/readme.md CHANGED
@@ -785,29 +785,51 @@ MasterRecord includes a **production-grade two-level caching system** similar to
785
785
  └─────────────────────────────────────────────────────┘
786
786
  ```
787
787
 
788
- #### Basic Usage (Default Behavior)
788
+ #### Basic Usage (Opt-In, Request-Scoped)
789
789
 
790
- Caching is **enabled by default** and requires zero configuration. The cache is **shared across all context instances** to ensure consistency:
790
+ Caching is **opt-in** and **request-scoped** like Active Record. Use `.cache()` to enable caching, and call `endRequest()` to clear:
791
791
 
792
792
  ```javascript
793
793
  const db = new AppContext();
794
794
 
795
- // First query hits database (cache miss)
796
- const user = db.User.where(u => u.id == $$, 1).single();
795
+ // DEFAULT: No caching (always hits database)
796
+ const user = db.User.findById(1); // DB query
797
+ const user2 = db.User.findById(1); // DB query again (no cache)
797
798
 
798
- // Second identical query hits cache (99%+ faster)
799
- const user2 = db.User.where(u => u.id == $$, 1).single();
799
+ // OPT-IN: Enable caching with .cache()
800
+ const categories = db.Categories.cache().toList(); // DB query, cached
801
+ const categories2 = db.Categories.cache().toList(); // Cache hit! (instant)
800
802
 
801
803
  // Update invalidates cache automatically
802
- user2.name = "Updated";
803
- db.saveChanges(); // Cache for User table cleared
804
+ const cat = db.Categories.findById(1);
805
+ cat.name = "Updated";
806
+ db.saveChanges(); // Cache for Categories table cleared
804
807
 
805
- // Next query hits database again (cache miss)
806
- const user3 = db.User.where(u => u.id == $$, 1).single();
808
+ // End request (clears cache - like Active Record)
809
+ db.endRequest(); // Cache cleared for next request
810
+ ```
807
811
 
808
- // Cache is shared across all context instances
809
- const db2 = new AppContext();
810
- const user4 = db2.User.findById(1); // Also uses shared cache
812
+ **Web Application Pattern (Recommended):**
813
+ ```javascript
814
+ // Express middleware - automatic request-scoped caching
815
+ app.use((req, res, next) => {
816
+ req.db = new AppContext();
817
+
818
+ // Clear cache when response finishes (like Active Record)
819
+ res.on('finish', () => {
820
+ req.db.endRequest(); // Clears query cache
821
+ });
822
+
823
+ next();
824
+ });
825
+
826
+ // In your routes
827
+ app.get('/categories', (req, res) => {
828
+ // Cache is fresh for this request
829
+ const categories = req.db.Categories.cache().toList();
830
+ res.json(categories);
831
+ // Cache auto-cleared after response
832
+ });
811
833
  ```
812
834
 
813
835
  #### Configuration
@@ -816,29 +838,43 @@ Configure caching via environment variables:
816
838
 
817
839
  ```bash
818
840
  # Development (.env)
819
- QUERY_CACHE_ENABLED=true # Enable/disable (default: true)
820
- QUERY_CACHE_TTL=300000 # TTL in milliseconds (default: 5 minutes)
841
+ QUERY_CACHE_TTL=5000 # TTL in milliseconds (default: 5 seconds - request-scoped)
821
842
  QUERY_CACHE_SIZE=1000 # Max cache entries (default: 1000)
843
+ QUERY_CACHE_ENABLED=true # Enable/disable globally (default: true)
822
844
 
823
845
  # Production (.env)
824
- QUERY_CACHE_ENABLED=true
825
- QUERY_CACHE_TTL=300 # Redis uses seconds
846
+ QUERY_CACHE_TTL=5 # Redis uses seconds (5 seconds default)
826
847
  REDIS_URL=redis://localhost:6379 # Use Redis for distributed caching
827
848
  ```
828
849
 
829
- #### Disable Caching for Specific Queries
850
+ **Note:**
851
+ - Cache is **opt-in per query** using `.cache()`
852
+ - Default TTL is **5 seconds** (request-scoped like Active Record)
853
+ - Call `db.endRequest()` to clear cache manually (recommended in middleware)
854
+ - Environment variables control the cache system globally
855
+
856
+ #### Enable Caching for Specific Queries
830
857
 
831
- Use `.noCache()` for real-time data that shouldn't be cached:
858
+ Use `.cache()` for frequently accessed, rarely changed data:
832
859
 
833
860
  ```javascript
834
- // Always hit database (never cached)
861
+ // DEFAULT: Always hits database (safe)
835
862
  const liveData = db.Analytics
836
863
  .where(a => a.date == $$, today)
837
- .noCache() // Skip cache
838
- .toList();
864
+ .toList(); // No caching (default)
865
+
866
+ // OPT-IN: Cache reference data
867
+ const categories = db.Categories.cache().toList(); // Cached for 5 minutes
868
+ const settings = db.Settings.cache().toList(); // Cached
869
+ const countries = db.Countries.cache().toList(); // Cached
839
870
 
840
- // Reference data (highly cacheable)
841
- const categories = db.Categories.toList(); // Cached for 5 minutes
871
+ // When to use .cache():
872
+ // Reference data (categories, settings, countries)
873
+ // ✅ Rarely changing data (roles, permissions)
874
+ // ✅ Expensive aggregations with stable results
875
+ // ❌ User-specific data
876
+ // ❌ Real-time data
877
+ // ❌ Financial/critical data
842
878
  ```
843
879
 
844
880
  #### Manual Cache Control
@@ -905,19 +941,22 @@ class AppContext extends context {
905
941
  MasterRecord automatically invalidates cache entries when data changes:
906
942
 
907
943
  ```javascript
908
- // Query is cached
909
- const users = db.User.where(u => u.active == true).toList();
944
+ // Query with caching enabled
945
+ const categories = db.Categories.cache().toList(); // DB query, cached
946
+
947
+ // Any modification to Categories table invalidates ALL cached Category queries
948
+ const cat = db.Categories.findById(1);
949
+ cat.name = "Updated";
950
+ db.saveChanges(); // Invalidates all cached Categories queries
910
951
 
911
- // Any modification to User table invalidates ALL User queries
912
- const user = db.User.findById(1);
913
- user.name = "Updated";
914
- db.saveChanges(); // Invalidates all cached User queries
952
+ // Next cached query hits database (fresh data)
953
+ const categoriesAgain = db.Categories.cache().toList(); // DB query (cache cleared)
915
954
 
916
- // Next query hits database (fresh data)
917
- const usersAgain = db.User.where(u => u.active == true).toList();
955
+ // Non-cached queries are unaffected (always fresh)
956
+ const users = db.User.toList(); // No .cache() = always DB query
918
957
 
919
- // Queries for OTHER tables are unaffected
920
- const posts = db.Post.toList(); // Still cached
958
+ // Queries for OTHER tables' caches are unaffected
959
+ const settings = db.Settings.cache().toList(); // Still cached (different table)
921
960
  ```
922
961
 
923
962
  **Invalidation rules:**
@@ -941,40 +980,39 @@ Expected performance improvements:
941
980
 
942
981
  #### Best Practices
943
982
 
944
- **DO cache:**
983
+ **DO use .cache():**
945
984
  ```javascript
946
985
  // Reference data (rarely changes)
947
- const categories = db.Categories.toList();
948
- const settings = db.Settings.toList();
949
-
950
- // Read-heavy data (user profiles)
951
- const user = db.User.findById(userId);
952
-
953
- // Expensive aggregations
954
- const stats = db.Orders
955
- .where(o => o.status == $$, 'completed')
986
+ const categories = db.Categories.cache().toList();
987
+ const settings = db.Settings.cache().toList();
988
+ const countries = db.Countries.cache().toList();
989
+
990
+ // Expensive aggregations (stable results)
991
+ const totalRevenue = db.Orders
992
+ .where(o => o.year == $$, 2024)
993
+ .cache()
956
994
  .count();
957
995
  ```
958
996
 
959
- **DON'T cache:**
997
+ **DON'T use .cache():**
960
998
  ```javascript
961
- // Real-time data (always needs fresh results)
999
+ // User-specific data (default is safe - no caching)
1000
+ const user = db.User.findById(userId); // Always fresh
1001
+
1002
+ // Real-time data (default is safe)
962
1003
  const liveOrders = db.Orders
963
1004
  .where(o => o.status == $$, 'pending')
964
- .noCache()
965
- .toList();
1005
+ .toList(); // Always fresh
966
1006
 
967
- // Financial transactions (critical accuracy)
1007
+ // Financial transactions (default is safe)
968
1008
  const balance = db.Transactions
969
1009
  .where(t => t.user_id == $$, userId)
970
- .noCache()
971
- .toList();
1010
+ .toList(); // Always fresh
972
1011
 
973
- // User-specific sensitive data (security concern)
1012
+ // User-specific sensitive data (default is safe)
974
1013
  const permissions = db.UserPermissions
975
1014
  .where(p => p.user_id == $$, userId)
976
- .noCache()
977
- .toList();
1015
+ .toList(); // Always fresh
978
1016
  ```
979
1017
 
980
1018
  #### Monitoring Cache Performance
@@ -992,27 +1030,66 @@ if (parseFloat(stats.hitRate) < 50) {
992
1030
  }
993
1031
  ```
994
1032
 
1033
+ #### Request-Scoped Caching (Like Active Record)
1034
+
1035
+ MasterRecord's caching is designed to work like Active Record - **cache within a request, clear after**:
1036
+
1037
+ ```javascript
1038
+ // Express middleware pattern (recommended)
1039
+ app.use((req, res, next) => {
1040
+ req.db = new AppContext();
1041
+
1042
+ // Automatically clear cache when request ends
1043
+ res.on('finish', () => {
1044
+ req.db.endRequest(); // Like Active Record's cache clearing
1045
+ });
1046
+
1047
+ next();
1048
+ });
1049
+
1050
+ // In routes - cache is fresh per request
1051
+ app.get('/api/categories', (req, res) => {
1052
+ // First call in this request - DB query
1053
+ const categories = req.db.Categories.cache().toList();
1054
+
1055
+ // Second call in same request - cache hit
1056
+ const categoriesAgain = req.db.Categories.cache().toList();
1057
+
1058
+ res.json(categories);
1059
+ // After response, cache is automatically cleared
1060
+ });
1061
+
1062
+ // Next request starts with empty cache (fresh)
1063
+ ```
1064
+
1065
+ **Why request-scoped?**
1066
+ - ✅ Like Active Record - familiar pattern
1067
+ - ✅ No stale data across requests
1068
+ - ✅ Cache only lives during request processing
1069
+ - ✅ Automatic cleanup
1070
+
995
1071
  #### Important: Shared Cache Behavior
996
1072
 
997
- **The cache is shared across all context instances of the same class.** This ensures consistency:
1073
+ **The cache is shared across all context instances of the same class.** This ensures consistency within a request:
998
1074
 
999
1075
  ```javascript
1000
1076
  const db1 = new AppContext();
1001
1077
  const db2 = new AppContext();
1002
1078
 
1003
- // Context 1: Cache data
1004
- const user1 = db1.User.findById(1); // DB query, cached
1079
+ // Context 1: Cache data with .cache()
1080
+ const categories1 = db1.Categories.cache().toList(); // DB query, cached
1005
1081
 
1006
1082
  // Context 2: Sees cached data
1007
- const user2 = db2.User.findById(1); // Cache hit!
1083
+ const categories2 = db2.Categories.cache().toList(); // Cache hit!
1008
1084
 
1009
1085
  // Context 2: Updates invalidate cache for BOTH contexts
1010
- user2.name = "Updated";
1086
+ const cat = db2.Categories.findById(1);
1087
+ cat.name = "Updated";
1011
1088
  db2.saveChanges(); // Invalidates shared cache
1012
1089
 
1013
1090
  // Context 1: Sees fresh data
1014
- const user3 = db1.User.findById(1); // Cache miss, fresh data
1015
- console.log(user3.name); // "Updated"
1091
+ const categories3 = db1.Categories.cache().toList(); // Cache miss, fresh data
1092
+ console.log(categories3[0].name); // "Updated"
1016
1093
  ```
1017
1094
 
1018
1095
  **Why shared cache?**
@@ -1098,6 +1175,7 @@ context.remove(entity)
1098
1175
  // Cache management
1099
1176
  context.getCacheStats() // Get cache statistics
1100
1177
  context.clearQueryCache() // Clear all cached queries
1178
+ context.endRequest() // End request and clear cache (like Active Record)
1101
1179
  context.setQueryCacheEnabled(bool) // Enable/disable caching
1102
1180
  ```
1103
1181
 
@@ -1112,7 +1190,7 @@ context.setQueryCacheEnabled(bool) // Enable/disable caching
1112
1190
  .skip(number) // Skip N records
1113
1191
  .take(number) // Limit to N records
1114
1192
  .include(relationship) // Eager load
1115
- .noCache() // Disable caching for this query
1193
+ .cache() // Enable caching for this query (opt-in)
1116
1194
 
1117
1195
  // Terminal methods (execute query)
1118
1196
  .toList() // Return array
@@ -1282,18 +1360,22 @@ console.log(`${author.name} has ${posts.length} posts`);
1282
1360
 
1283
1361
  ## Performance Tips
1284
1362
 
1285
- ### 1. Leverage Query Caching
1363
+ ### 1. Use Query Caching Selectively
1286
1364
 
1287
1365
  ```javascript
1288
- // ✅ GOOD: Cache reference data
1289
- const categories = db.Categories.toList(); // Cached automatically
1290
-
1291
- // ✅ GOOD: Reuse queries (cache hits)
1292
- const user1 = db.User.findById(123); // DB query
1293
- const user2 = db.User.findById(123); // Cache hit (instant)
1294
-
1295
- // ✅ GOOD: Disable cache for real-time data
1296
- const liveOrders = db.Orders.where(o => o.status == 'pending').noCache().toList();
1366
+ // ✅ GOOD: Cache reference data that rarely changes
1367
+ const categories = db.Categories.cache().toList(); // Opt-in caching
1368
+ const settings = db.Settings.cache().toList();
1369
+
1370
+ // GOOD: Queries without .cache() are always fresh (safe default)
1371
+ const user1 = db.User.findById(123); // Always DB query (no cache)
1372
+ const user2 = db.User.findById(123); // Always DB query (no cache)
1373
+
1374
+ // GOOD: Cache expensive queries with stable results
1375
+ const revenue2024 = db.Orders
1376
+ .where(o => o.year == $$, 2024)
1377
+ .cache() // Historical data doesn't change
1378
+ .count();
1297
1379
 
1298
1380
  // Monitor cache performance
1299
1381
  const stats = db.getCacheStats();
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Test: Opt-In Caching Behavior
3
+ *
4
+ * Verifies that caching is disabled by default and only enabled with .cache()
5
+ */
6
+
7
+ const QueryCache = require('../Cache/QueryCache');
8
+
9
+ console.log("╔════════════════════════════════════════════════════════════════╗");
10
+ console.log("║ Opt-In Caching Test ║");
11
+ console.log("╚════════════════════════════════════════════════════════════════╝\n");
12
+
13
+ let passed = 0;
14
+ let failed = 0;
15
+
16
+ // Simulate queryMethods with opt-in caching
17
+ class SimulatedQuery {
18
+ constructor(context) {
19
+ this._context = context;
20
+ this.__useCache = false; // Default: caching disabled
21
+ this._queryString = 'SELECT * FROM test';
22
+ this._tableName = 'Test';
23
+ }
24
+
25
+ // Enable caching for this query
26
+ cache() {
27
+ this.__useCache = true;
28
+ return this;
29
+ }
30
+
31
+ // Execute query
32
+ toList() {
33
+ const cacheKey = this._context.cache.generateKey(this._queryString, [], this._tableName);
34
+
35
+ // Check cache if enabled
36
+ if (this.__useCache) {
37
+ const cached = this._context.cache.get(cacheKey);
38
+ if (cached) {
39
+ return cached;
40
+ }
41
+ }
42
+
43
+ // Simulate DB query
44
+ const result = [{ id: 1, name: 'Test' }];
45
+
46
+ // Store in cache if enabled
47
+ if (this.__useCache) {
48
+ this._context.cache.set(cacheKey, result, this._tableName);
49
+ }
50
+
51
+ return result;
52
+ }
53
+ }
54
+
55
+ class SimulatedContext {
56
+ constructor() {
57
+ this.cache = new QueryCache({ ttl: 60000, maxSize: 100 });
58
+ }
59
+
60
+ createQuery() {
61
+ return new SimulatedQuery(this);
62
+ }
63
+ }
64
+
65
+ // Test 1: Default behavior - no caching
66
+ console.log("📝 Test 1: Queries without .cache() are NOT cached");
67
+ console.log("──────────────────────────────────────────────────");
68
+
69
+ try {
70
+ const ctx = new SimulatedContext();
71
+ ctx.cache.clear();
72
+
73
+ // Execute same query twice WITHOUT .cache()
74
+ const result1 = ctx.createQuery().toList();
75
+ const result2 = ctx.createQuery().toList();
76
+
77
+ const stats = ctx.cache.getStats();
78
+
79
+ if(stats.size === 0 && stats.hits === 0) {
80
+ console.log(" ✓ Queries without .cache() do not store results");
81
+ console.log(" ✓ No cache hits recorded");
82
+ console.log(" ✓ Cache size remains 0");
83
+ passed++;
84
+ } else {
85
+ console.log(` ✗ Queries were cached without .cache() call`);
86
+ console.log(` ✗ Cache size: ${stats.size}, hits: ${stats.hits}`);
87
+ failed++;
88
+ }
89
+ } catch(err) {
90
+ console.log(` ✗ Error: ${err.message}`);
91
+ failed++;
92
+ }
93
+
94
+ // Test 2: Opt-in with .cache() enables caching
95
+ console.log("\n📝 Test 2: Queries with .cache() ARE cached");
96
+ console.log("──────────────────────────────────────────────────");
97
+
98
+ try {
99
+ const ctx = new SimulatedContext();
100
+ ctx.cache.clear();
101
+
102
+ // Execute same query twice WITH .cache()
103
+ const result1 = ctx.createQuery().cache().toList();
104
+ const result2 = ctx.createQuery().cache().toList();
105
+
106
+ const stats = ctx.cache.getStats();
107
+
108
+ if(stats.size === 1 && stats.hits === 1 && stats.misses === 1) {
109
+ console.log(" ✓ First query with .cache() stored result (miss)");
110
+ console.log(" ✓ Second query with .cache() hit cache (hit)");
111
+ console.log(` ✓ Cache stats: ${stats.hits} hit, ${stats.misses} miss`);
112
+ passed++;
113
+ } else {
114
+ console.log(` ✗ Caching with .cache() didn't work properly`);
115
+ console.log(` ✗ Expected: 1 hit, 1 miss. Got: ${stats.hits} hits, ${stats.misses} misses`);
116
+ failed++;
117
+ }
118
+ } catch(err) {
119
+ console.log(` ✗ Error: ${err.message}`);
120
+ failed++;
121
+ }
122
+
123
+ // Test 3: Mixed queries - cached and non-cached
124
+ console.log("\n📝 Test 3: Mixed cached and non-cached queries");
125
+ console.log("──────────────────────────────────────────────────");
126
+
127
+ try {
128
+ const ctx = new SimulatedContext();
129
+ ctx.cache.clear();
130
+
131
+ // Non-cached query (no .cache())
132
+ ctx.createQuery().toList();
133
+ ctx.createQuery().toList();
134
+
135
+ // Cached query (with .cache())
136
+ ctx.createQuery().cache().toList();
137
+ ctx.createQuery().cache().toList();
138
+
139
+ const stats = ctx.cache.getStats();
140
+
141
+ if(stats.size === 1 && stats.hits === 1 && stats.misses === 1) {
142
+ console.log(" ✓ Non-cached queries didn't affect cache");
143
+ console.log(" ✓ Cached queries stored and retrieved correctly");
144
+ console.log(` ✓ Cache contains only .cache() queries: ${stats.size} entry`);
145
+ passed++;
146
+ } else {
147
+ console.log(` ✗ Mixed query handling incorrect`);
148
+ console.log(` ✗ Cache size: ${stats.size}, expected: 1`);
149
+ failed++;
150
+ }
151
+ } catch(err) {
152
+ console.log(` ✗ Error: ${err.message}`);
153
+ failed++;
154
+ }
155
+
156
+ // Test 4: Default __useCache flag is false
157
+ console.log("\n📝 Test 4: Default __useCache flag is false");
158
+ console.log("──────────────────────────────────────────────────");
159
+
160
+ try {
161
+ const ctx = new SimulatedContext();
162
+ const query = ctx.createQuery();
163
+
164
+ if(query.__useCache === false) {
165
+ console.log(" ✓ Default __useCache is false");
166
+ console.log(" ✓ Caching is opt-in by default");
167
+ passed++;
168
+ } else {
169
+ console.log(` ✗ Default __useCache is ${query.__useCache}, expected false`);
170
+ failed++;
171
+ }
172
+ } catch(err) {
173
+ console.log(` ✗ Error: ${err.message}`);
174
+ failed++;
175
+ }
176
+
177
+ // Test 5: .cache() sets __useCache to true
178
+ console.log("\n📝 Test 5: .cache() enables caching flag");
179
+ console.log("──────────────────────────────────────────────────");
180
+
181
+ try {
182
+ const ctx = new SimulatedContext();
183
+ const query = ctx.createQuery();
184
+
185
+ const beforeCache = query.__useCache;
186
+ query.cache();
187
+ const afterCache = query.__useCache;
188
+
189
+ if(beforeCache === false && afterCache === true) {
190
+ console.log(" ✓ __useCache starts as false");
191
+ console.log(" ✓ .cache() sets __useCache to true");
192
+ console.log(" ✓ Caching is explicitly enabled");
193
+ passed++;
194
+ } else {
195
+ console.log(` ✗ Flag transition incorrect`);
196
+ console.log(` ✗ Before: ${beforeCache}, After: ${afterCache}`);
197
+ failed++;
198
+ }
199
+ } catch(err) {
200
+ console.log(` ✗ Error: ${err.message}`);
201
+ failed++;
202
+ }
203
+
204
+ // Summary
205
+ console.log("\n╔════════════════════════════════════════════════════════════════╗");
206
+ console.log("║ Test Summary ║");
207
+ console.log("╚════════════════════════════════════════════════════════════════╝");
208
+ console.log(`\n ✓ Passed: ${passed}`);
209
+ console.log(` ✗ Failed: ${failed}`);
210
+ console.log(` 📊 Total: ${passed + failed}\n`);
211
+
212
+ if(failed === 0) {
213
+ console.log(" 🎉 All tests passed!\n");
214
+ console.log(" ✅ Opt-in caching behavior verified");
215
+ console.log(" ✅ Default is safe (no caching)");
216
+ console.log(" ✅ .cache() explicitly enables caching\n");
217
+ process.exit(0);
218
+ } else {
219
+ console.log(" ❌ Some tests failed\n");
220
+ process.exit(1);
221
+ }