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
package/readme.md
CHANGED
|
@@ -68,6 +68,30 @@ MasterRecord includes the following database drivers by default:
|
|
|
68
68
|
- `sync-mysql2@^1.0.8` - MySQL
|
|
69
69
|
- `better-sqlite3@^12.6.0` - SQLite
|
|
70
70
|
|
|
71
|
+
## Two Patterns: Entity Framework & Active Record
|
|
72
|
+
|
|
73
|
+
MasterRecord supports **both** ORM patterns - choose what feels natural:
|
|
74
|
+
|
|
75
|
+
### Active Record Style (Recommended for beginners)
|
|
76
|
+
```javascript
|
|
77
|
+
// Entity saves itself
|
|
78
|
+
const user = db.User.findById(1);
|
|
79
|
+
user.name = 'Updated';
|
|
80
|
+
await user.save(); // ✅ Entity knows how to save
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Entity Framework Style (Efficient for batch operations)
|
|
84
|
+
```javascript
|
|
85
|
+
// Context saves all tracked entities
|
|
86
|
+
const user = db.User.findById(1);
|
|
87
|
+
user.name = 'Updated';
|
|
88
|
+
await db.saveChanges(); // ✅ Batch save
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Read more:** [Active Record Pattern Guide](./ACTIVE_RECORD_PATTERN.md) | [Detached Entities Guide](./DETACHED_ENTITIES_GUIDE.md)
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
71
95
|
## Quick Start
|
|
72
96
|
|
|
73
97
|
### 1. Create a Context
|
|
@@ -137,21 +161,21 @@ masterrecord migrate AppContext
|
|
|
137
161
|
const AppContext = require('./app/models/context');
|
|
138
162
|
const db = new AppContext();
|
|
139
163
|
|
|
140
|
-
// Create
|
|
164
|
+
// Create (Active Record style)
|
|
141
165
|
const user = db.User.new();
|
|
142
166
|
user.name = 'Alice';
|
|
143
167
|
user.email = 'alice@example.com';
|
|
144
168
|
user.age = 28;
|
|
145
|
-
await
|
|
169
|
+
await user.save(); // Entity saves itself!
|
|
146
170
|
|
|
147
171
|
// Read with parameterized query
|
|
148
172
|
const alice = db.User
|
|
149
173
|
.where(u => u.email == $$, 'alice@example.com')
|
|
150
174
|
.single();
|
|
151
175
|
|
|
152
|
-
// Update
|
|
176
|
+
// Update (Active Record style)
|
|
153
177
|
alice.age = 29;
|
|
154
|
-
await
|
|
178
|
+
await alice.save(); // Entity saves itself!
|
|
155
179
|
|
|
156
180
|
// Delete
|
|
157
181
|
db.remove(alice);
|
|
@@ -785,29 +809,51 @@ MasterRecord includes a **production-grade two-level caching system** similar to
|
|
|
785
809
|
└─────────────────────────────────────────────────────┘
|
|
786
810
|
```
|
|
787
811
|
|
|
788
|
-
#### Basic Usage (
|
|
812
|
+
#### Basic Usage (Opt-In, Request-Scoped)
|
|
789
813
|
|
|
790
|
-
Caching is **
|
|
814
|
+
Caching is **opt-in** and **request-scoped** like Active Record. Use `.cache()` to enable caching, and call `endRequest()` to clear:
|
|
791
815
|
|
|
792
816
|
```javascript
|
|
793
817
|
const db = new AppContext();
|
|
794
818
|
|
|
795
|
-
//
|
|
796
|
-
const user = db.User.
|
|
819
|
+
// DEFAULT: No caching (always hits database)
|
|
820
|
+
const user = db.User.findById(1); // DB query
|
|
821
|
+
const user2 = db.User.findById(1); // DB query again (no cache)
|
|
797
822
|
|
|
798
|
-
//
|
|
799
|
-
const
|
|
823
|
+
// OPT-IN: Enable caching with .cache()
|
|
824
|
+
const categories = db.Categories.cache().toList(); // DB query, cached
|
|
825
|
+
const categories2 = db.Categories.cache().toList(); // Cache hit! (instant)
|
|
800
826
|
|
|
801
827
|
// Update invalidates cache automatically
|
|
802
|
-
|
|
803
|
-
|
|
828
|
+
const cat = db.Categories.findById(1);
|
|
829
|
+
cat.name = "Updated";
|
|
830
|
+
db.saveChanges(); // Cache for Categories table cleared
|
|
804
831
|
|
|
805
|
-
//
|
|
806
|
-
|
|
832
|
+
// End request (clears cache - like Active Record)
|
|
833
|
+
db.endRequest(); // Cache cleared for next request
|
|
834
|
+
```
|
|
807
835
|
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
836
|
+
**Web Application Pattern (Recommended):**
|
|
837
|
+
```javascript
|
|
838
|
+
// Express middleware - automatic request-scoped caching
|
|
839
|
+
app.use((req, res, next) => {
|
|
840
|
+
req.db = new AppContext();
|
|
841
|
+
|
|
842
|
+
// Clear cache when response finishes (like Active Record)
|
|
843
|
+
res.on('finish', () => {
|
|
844
|
+
req.db.endRequest(); // Clears query cache
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
next();
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
// In your routes
|
|
851
|
+
app.get('/categories', (req, res) => {
|
|
852
|
+
// Cache is fresh for this request
|
|
853
|
+
const categories = req.db.Categories.cache().toList();
|
|
854
|
+
res.json(categories);
|
|
855
|
+
// Cache auto-cleared after response
|
|
856
|
+
});
|
|
811
857
|
```
|
|
812
858
|
|
|
813
859
|
#### Configuration
|
|
@@ -816,29 +862,43 @@ Configure caching via environment variables:
|
|
|
816
862
|
|
|
817
863
|
```bash
|
|
818
864
|
# Development (.env)
|
|
819
|
-
|
|
820
|
-
QUERY_CACHE_TTL=300000 # TTL in milliseconds (default: 5 minutes)
|
|
865
|
+
QUERY_CACHE_TTL=5000 # TTL in milliseconds (default: 5 seconds - request-scoped)
|
|
821
866
|
QUERY_CACHE_SIZE=1000 # Max cache entries (default: 1000)
|
|
867
|
+
QUERY_CACHE_ENABLED=true # Enable/disable globally (default: true)
|
|
822
868
|
|
|
823
869
|
# Production (.env)
|
|
824
|
-
|
|
825
|
-
QUERY_CACHE_TTL=300 # Redis uses seconds
|
|
870
|
+
QUERY_CACHE_TTL=5 # Redis uses seconds (5 seconds default)
|
|
826
871
|
REDIS_URL=redis://localhost:6379 # Use Redis for distributed caching
|
|
827
872
|
```
|
|
828
873
|
|
|
829
|
-
|
|
874
|
+
**Note:**
|
|
875
|
+
- Cache is **opt-in per query** using `.cache()`
|
|
876
|
+
- Default TTL is **5 seconds** (request-scoped like Active Record)
|
|
877
|
+
- Call `db.endRequest()` to clear cache manually (recommended in middleware)
|
|
878
|
+
- Environment variables control the cache system globally
|
|
879
|
+
|
|
880
|
+
#### Enable Caching for Specific Queries
|
|
830
881
|
|
|
831
|
-
Use `.
|
|
882
|
+
Use `.cache()` for frequently accessed, rarely changed data:
|
|
832
883
|
|
|
833
884
|
```javascript
|
|
834
|
-
// Always
|
|
885
|
+
// DEFAULT: Always hits database (safe)
|
|
835
886
|
const liveData = db.Analytics
|
|
836
887
|
.where(a => a.date == $$, today)
|
|
837
|
-
.
|
|
838
|
-
|
|
888
|
+
.toList(); // No caching (default)
|
|
889
|
+
|
|
890
|
+
// OPT-IN: Cache reference data
|
|
891
|
+
const categories = db.Categories.cache().toList(); // Cached for 5 minutes
|
|
892
|
+
const settings = db.Settings.cache().toList(); // Cached
|
|
893
|
+
const countries = db.Countries.cache().toList(); // Cached
|
|
839
894
|
|
|
840
|
-
//
|
|
841
|
-
|
|
895
|
+
// When to use .cache():
|
|
896
|
+
// ✅ Reference data (categories, settings, countries)
|
|
897
|
+
// ✅ Rarely changing data (roles, permissions)
|
|
898
|
+
// ✅ Expensive aggregations with stable results
|
|
899
|
+
// ❌ User-specific data
|
|
900
|
+
// ❌ Real-time data
|
|
901
|
+
// ❌ Financial/critical data
|
|
842
902
|
```
|
|
843
903
|
|
|
844
904
|
#### Manual Cache Control
|
|
@@ -905,19 +965,22 @@ class AppContext extends context {
|
|
|
905
965
|
MasterRecord automatically invalidates cache entries when data changes:
|
|
906
966
|
|
|
907
967
|
```javascript
|
|
908
|
-
// Query
|
|
909
|
-
const
|
|
968
|
+
// Query with caching enabled
|
|
969
|
+
const categories = db.Categories.cache().toList(); // DB query, cached
|
|
910
970
|
|
|
911
|
-
// Any modification to
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
db.saveChanges(); // Invalidates all cached
|
|
971
|
+
// Any modification to Categories table invalidates ALL cached Category queries
|
|
972
|
+
const cat = db.Categories.findById(1);
|
|
973
|
+
cat.name = "Updated";
|
|
974
|
+
db.saveChanges(); // Invalidates all cached Categories queries
|
|
915
975
|
|
|
916
|
-
// Next query hits database (fresh data)
|
|
917
|
-
const
|
|
976
|
+
// Next cached query hits database (fresh data)
|
|
977
|
+
const categoriesAgain = db.Categories.cache().toList(); // DB query (cache cleared)
|
|
918
978
|
|
|
919
|
-
//
|
|
920
|
-
const
|
|
979
|
+
// Non-cached queries are unaffected (always fresh)
|
|
980
|
+
const users = db.User.toList(); // No .cache() = always DB query
|
|
981
|
+
|
|
982
|
+
// Queries for OTHER tables' caches are unaffected
|
|
983
|
+
const settings = db.Settings.cache().toList(); // Still cached (different table)
|
|
921
984
|
```
|
|
922
985
|
|
|
923
986
|
**Invalidation rules:**
|
|
@@ -941,40 +1004,39 @@ Expected performance improvements:
|
|
|
941
1004
|
|
|
942
1005
|
#### Best Practices
|
|
943
1006
|
|
|
944
|
-
**DO cache:**
|
|
1007
|
+
**DO use .cache():**
|
|
945
1008
|
```javascript
|
|
946
1009
|
// 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')
|
|
1010
|
+
const categories = db.Categories.cache().toList();
|
|
1011
|
+
const settings = db.Settings.cache().toList();
|
|
1012
|
+
const countries = db.Countries.cache().toList();
|
|
1013
|
+
|
|
1014
|
+
// Expensive aggregations (stable results)
|
|
1015
|
+
const totalRevenue = db.Orders
|
|
1016
|
+
.where(o => o.year == $$, 2024)
|
|
1017
|
+
.cache()
|
|
956
1018
|
.count();
|
|
957
1019
|
```
|
|
958
1020
|
|
|
959
|
-
**DON'T cache:**
|
|
1021
|
+
**DON'T use .cache():**
|
|
960
1022
|
```javascript
|
|
961
|
-
//
|
|
1023
|
+
// User-specific data (default is safe - no caching)
|
|
1024
|
+
const user = db.User.findById(userId); // Always fresh
|
|
1025
|
+
|
|
1026
|
+
// Real-time data (default is safe)
|
|
962
1027
|
const liveOrders = db.Orders
|
|
963
1028
|
.where(o => o.status == $$, 'pending')
|
|
964
|
-
.
|
|
965
|
-
.toList();
|
|
1029
|
+
.toList(); // Always fresh
|
|
966
1030
|
|
|
967
|
-
// Financial transactions (
|
|
1031
|
+
// Financial transactions (default is safe)
|
|
968
1032
|
const balance = db.Transactions
|
|
969
1033
|
.where(t => t.user_id == $$, userId)
|
|
970
|
-
.
|
|
971
|
-
.toList();
|
|
1034
|
+
.toList(); // Always fresh
|
|
972
1035
|
|
|
973
|
-
// User-specific sensitive data (
|
|
1036
|
+
// User-specific sensitive data (default is safe)
|
|
974
1037
|
const permissions = db.UserPermissions
|
|
975
1038
|
.where(p => p.user_id == $$, userId)
|
|
976
|
-
.
|
|
977
|
-
.toList();
|
|
1039
|
+
.toList(); // Always fresh
|
|
978
1040
|
```
|
|
979
1041
|
|
|
980
1042
|
#### Monitoring Cache Performance
|
|
@@ -992,27 +1054,66 @@ if (parseFloat(stats.hitRate) < 50) {
|
|
|
992
1054
|
}
|
|
993
1055
|
```
|
|
994
1056
|
|
|
1057
|
+
#### Request-Scoped Caching (Like Active Record)
|
|
1058
|
+
|
|
1059
|
+
MasterRecord's caching is designed to work like Active Record - **cache within a request, clear after**:
|
|
1060
|
+
|
|
1061
|
+
```javascript
|
|
1062
|
+
// Express middleware pattern (recommended)
|
|
1063
|
+
app.use((req, res, next) => {
|
|
1064
|
+
req.db = new AppContext();
|
|
1065
|
+
|
|
1066
|
+
// Automatically clear cache when request ends
|
|
1067
|
+
res.on('finish', () => {
|
|
1068
|
+
req.db.endRequest(); // Like Active Record's cache clearing
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
next();
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
// In routes - cache is fresh per request
|
|
1075
|
+
app.get('/api/categories', (req, res) => {
|
|
1076
|
+
// First call in this request - DB query
|
|
1077
|
+
const categories = req.db.Categories.cache().toList();
|
|
1078
|
+
|
|
1079
|
+
// Second call in same request - cache hit
|
|
1080
|
+
const categoriesAgain = req.db.Categories.cache().toList();
|
|
1081
|
+
|
|
1082
|
+
res.json(categories);
|
|
1083
|
+
// After response, cache is automatically cleared
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// Next request starts with empty cache (fresh)
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
**Why request-scoped?**
|
|
1090
|
+
- ✅ Like Active Record - familiar pattern
|
|
1091
|
+
- ✅ No stale data across requests
|
|
1092
|
+
- ✅ Cache only lives during request processing
|
|
1093
|
+
- ✅ Automatic cleanup
|
|
1094
|
+
|
|
995
1095
|
#### Important: Shared Cache Behavior
|
|
996
1096
|
|
|
997
|
-
**The cache is shared across all context instances of the same class.** This ensures consistency:
|
|
1097
|
+
**The cache is shared across all context instances of the same class.** This ensures consistency within a request:
|
|
998
1098
|
|
|
999
1099
|
```javascript
|
|
1000
1100
|
const db1 = new AppContext();
|
|
1001
1101
|
const db2 = new AppContext();
|
|
1002
1102
|
|
|
1003
|
-
// Context 1: Cache data
|
|
1004
|
-
const
|
|
1103
|
+
// Context 1: Cache data with .cache()
|
|
1104
|
+
const categories1 = db1.Categories.cache().toList(); // DB query, cached
|
|
1005
1105
|
|
|
1006
1106
|
// Context 2: Sees cached data
|
|
1007
|
-
const
|
|
1107
|
+
const categories2 = db2.Categories.cache().toList(); // Cache hit!
|
|
1008
1108
|
|
|
1009
1109
|
// Context 2: Updates invalidate cache for BOTH contexts
|
|
1010
|
-
|
|
1110
|
+
const cat = db2.Categories.findById(1);
|
|
1111
|
+
cat.name = "Updated";
|
|
1011
1112
|
db2.saveChanges(); // Invalidates shared cache
|
|
1012
1113
|
|
|
1013
1114
|
// Context 1: Sees fresh data
|
|
1014
|
-
const
|
|
1015
|
-
console.log(
|
|
1115
|
+
const categories3 = db1.Categories.cache().toList(); // Cache miss, fresh data
|
|
1116
|
+
console.log(categories3[0].name); // "Updated"
|
|
1016
1117
|
```
|
|
1017
1118
|
|
|
1018
1119
|
**Why shared cache?**
|
|
@@ -1095,9 +1196,16 @@ context.saveChanges() // MySQL/SQLite (sync)
|
|
|
1095
1196
|
context.EntityName.add(entity)
|
|
1096
1197
|
context.remove(entity)
|
|
1097
1198
|
|
|
1199
|
+
// Attach detached entities (like Entity Framework's Update())
|
|
1200
|
+
context.attach(entity) // Attach and mark as modified
|
|
1201
|
+
context.attach(entity, { field: value }) // Attach with specific changes
|
|
1202
|
+
context.attachAll([entity1, entity2]) // Attach multiple entities
|
|
1203
|
+
await context.update('Entity', id, changes) // Update by primary key
|
|
1204
|
+
|
|
1098
1205
|
// Cache management
|
|
1099
1206
|
context.getCacheStats() // Get cache statistics
|
|
1100
1207
|
context.clearQueryCache() // Clear all cached queries
|
|
1208
|
+
context.endRequest() // End request and clear cache (like Active Record)
|
|
1101
1209
|
context.setQueryCacheEnabled(bool) // Enable/disable caching
|
|
1102
1210
|
```
|
|
1103
1211
|
|
|
@@ -1112,7 +1220,7 @@ context.setQueryCacheEnabled(bool) // Enable/disable caching
|
|
|
1112
1220
|
.skip(number) // Skip N records
|
|
1113
1221
|
.take(number) // Limit to N records
|
|
1114
1222
|
.include(relationship) // Eager load
|
|
1115
|
-
.
|
|
1223
|
+
.cache() // Enable caching for this query (opt-in)
|
|
1116
1224
|
|
|
1117
1225
|
// Terminal methods (execute query)
|
|
1118
1226
|
.toList() // Return array
|
|
@@ -1125,6 +1233,9 @@ context.setQueryCacheEnabled(bool) // Enable/disable caching
|
|
|
1125
1233
|
// Convenience methods
|
|
1126
1234
|
.findById(id) // Find by primary key
|
|
1127
1235
|
.new() // Create new entity instance
|
|
1236
|
+
|
|
1237
|
+
// Entity methods (Active Record style)
|
|
1238
|
+
await entity.save() // Save this entity (and all tracked changes)
|
|
1128
1239
|
```
|
|
1129
1240
|
|
|
1130
1241
|
### Migration Methods
|
|
@@ -1282,18 +1393,22 @@ console.log(`${author.name} has ${posts.length} posts`);
|
|
|
1282
1393
|
|
|
1283
1394
|
## Performance Tips
|
|
1284
1395
|
|
|
1285
|
-
### 1.
|
|
1396
|
+
### 1. Use Query Caching Selectively
|
|
1286
1397
|
|
|
1287
1398
|
```javascript
|
|
1288
|
-
// ✅ GOOD: Cache reference data
|
|
1289
|
-
const categories = db.Categories.toList(); //
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1399
|
+
// ✅ GOOD: Cache reference data that rarely changes
|
|
1400
|
+
const categories = db.Categories.cache().toList(); // Opt-in caching
|
|
1401
|
+
const settings = db.Settings.cache().toList();
|
|
1402
|
+
|
|
1403
|
+
// ✅ GOOD: Queries without .cache() are always fresh (safe default)
|
|
1404
|
+
const user1 = db.User.findById(123); // Always DB query (no cache)
|
|
1405
|
+
const user2 = db.User.findById(123); // Always DB query (no cache)
|
|
1406
|
+
|
|
1407
|
+
// ✅ GOOD: Cache expensive queries with stable results
|
|
1408
|
+
const revenue2024 = db.Orders
|
|
1409
|
+
.where(o => o.year == $$, 2024)
|
|
1410
|
+
.cache() // Historical data doesn't change
|
|
1411
|
+
.count();
|
|
1297
1412
|
|
|
1298
1413
|
// Monitor cache performance
|
|
1299
1414
|
const stats = db.getCacheStats();
|