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.
- package/QUERY_CACHING_GUIDE.md +445 -0
- package/QueryLanguage/queryMethods.js +13 -5
- package/context.js +19 -1
- package/package.json +1 -1
- package/readme.md +154 -72
- package/test/optInCache.test.js +221 -0
|
@@ -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 =
|
|
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
|
-
*
|
|
93
|
-
* Use for
|
|
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
|
-
|
|
96
|
-
this.__useCache =
|
|
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 ||
|
|
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.
|
|
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 (
|
|
788
|
+
#### Basic Usage (Opt-In, Request-Scoped)
|
|
789
789
|
|
|
790
|
-
Caching is **
|
|
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
|
-
//
|
|
796
|
-
const user = db.User.
|
|
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
|
-
//
|
|
799
|
-
const
|
|
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
|
-
|
|
803
|
-
|
|
804
|
+
const cat = db.Categories.findById(1);
|
|
805
|
+
cat.name = "Updated";
|
|
806
|
+
db.saveChanges(); // Cache for Categories table cleared
|
|
804
807
|
|
|
805
|
-
//
|
|
806
|
-
|
|
808
|
+
// End request (clears cache - like Active Record)
|
|
809
|
+
db.endRequest(); // Cache cleared for next request
|
|
810
|
+
```
|
|
807
811
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 `.
|
|
858
|
+
Use `.cache()` for frequently accessed, rarely changed data:
|
|
832
859
|
|
|
833
860
|
```javascript
|
|
834
|
-
// Always
|
|
861
|
+
// DEFAULT: Always hits database (safe)
|
|
835
862
|
const liveData = db.Analytics
|
|
836
863
|
.where(a => a.date == $$, today)
|
|
837
|
-
.
|
|
838
|
-
|
|
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
|
-
//
|
|
841
|
-
|
|
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
|
|
909
|
-
const
|
|
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
|
-
//
|
|
912
|
-
const
|
|
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
|
-
//
|
|
917
|
-
const
|
|
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
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
//
|
|
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
|
-
.
|
|
965
|
-
.toList();
|
|
1005
|
+
.toList(); // Always fresh
|
|
966
1006
|
|
|
967
|
-
// Financial transactions (
|
|
1007
|
+
// Financial transactions (default is safe)
|
|
968
1008
|
const balance = db.Transactions
|
|
969
1009
|
.where(t => t.user_id == $$, userId)
|
|
970
|
-
.
|
|
971
|
-
.toList();
|
|
1010
|
+
.toList(); // Always fresh
|
|
972
1011
|
|
|
973
|
-
// User-specific sensitive data (
|
|
1012
|
+
// User-specific sensitive data (default is safe)
|
|
974
1013
|
const permissions = db.UserPermissions
|
|
975
1014
|
.where(p => p.user_id == $$, userId)
|
|
976
|
-
.
|
|
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
|
|
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
|
|
1083
|
+
const categories2 = db2.Categories.cache().toList(); // Cache hit!
|
|
1008
1084
|
|
|
1009
1085
|
// Context 2: Updates invalidate cache for BOTH contexts
|
|
1010
|
-
|
|
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
|
|
1015
|
-
console.log(
|
|
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
|
-
.
|
|
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.
|
|
1363
|
+
### 1. Use Query Caching Selectively
|
|
1286
1364
|
|
|
1287
1365
|
```javascript
|
|
1288
|
-
// ✅ GOOD: Cache reference data
|
|
1289
|
-
const categories = db.Categories.toList(); //
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
+
}
|