masterrecord 0.3.7 → 0.3.9
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/Entity/entityTrackerModel.js +17 -3
- package/QueryLanguage/queryMethods.js +28 -5
- package/context.js +130 -1
- package/docs/ACTIVE_RECORD_PATTERN.md +477 -0
- package/docs/DETACHED_ENTITIES_GUIDE.md +445 -0
- package/docs/QUERY_CACHING_GUIDE.md +445 -0
- package/package.json +1 -1
- package/readme.md +191 -76
- package/test/attachDetached.test.js +303 -0
- 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!** ✅
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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": {
|