masterrecord 0.3.6 → 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
@@ -33,18 +33,26 @@ class context {
33
33
  isMySQL = false;
34
34
  isPostgres = false;
35
35
 
36
+ // Static shared cache - all context instances share the same cache
37
+ static _sharedQueryCache = null;
38
+
36
39
  constructor(){
37
40
  this. __environment = process.env.master;
38
41
  this.__name = this.constructor.name;
39
42
  this._SQLEngine = "";
40
43
  this.__trackedEntitiesMap = new Map(); // Initialize Map for O(1) lookups
41
44
 
42
- // Initialize query cache
43
- this._queryCache = new QueryCache({
44
- ttl: process.env.QUERY_CACHE_TTL || 5 * 60 * 1000, // 5 min default
45
- maxSize: process.env.QUERY_CACHE_SIZE || 1000,
46
- enabled: process.env.QUERY_CACHE_ENABLED !== 'false'
47
- });
45
+ // Initialize shared query cache (only once across all instances)
46
+ if (!context._sharedQueryCache) {
47
+ context._sharedQueryCache = new QueryCache({
48
+ ttl: process.env.QUERY_CACHE_TTL || 5000, // 5 seconds default (request-scoped)
49
+ maxSize: process.env.QUERY_CACHE_SIZE || 1000,
50
+ enabled: process.env.QUERY_CACHE_ENABLED !== 'false'
51
+ });
52
+ }
53
+
54
+ // Reference the shared cache
55
+ this._queryCache = context._sharedQueryCache;
48
56
  }
49
57
 
50
58
  /*
@@ -636,6 +644,24 @@ class context {
636
644
  this._queryCache.enabled = enabled;
637
645
  }
638
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
+
639
665
  // __track(model){
640
666
  // this.__trackedEntities.push(model);
641
667
  // return model;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.6",
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": {