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/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 db.saveChanges();
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 db.saveChanges();
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 (Default Behavior)
812
+ #### Basic Usage (Opt-In, Request-Scoped)
789
813
 
790
- Caching is **enabled by default** and requires zero configuration. The cache is **shared across all context instances** to ensure consistency:
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
- // First query hits database (cache miss)
796
- const user = db.User.where(u => u.id == $$, 1).single();
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
- // Second identical query hits cache (99%+ faster)
799
- const user2 = db.User.where(u => u.id == $$, 1).single();
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
- user2.name = "Updated";
803
- db.saveChanges(); // Cache for User table cleared
828
+ const cat = db.Categories.findById(1);
829
+ cat.name = "Updated";
830
+ db.saveChanges(); // Cache for Categories table cleared
804
831
 
805
- // Next query hits database again (cache miss)
806
- const user3 = db.User.where(u => u.id == $$, 1).single();
832
+ // End request (clears cache - like Active Record)
833
+ db.endRequest(); // Cache cleared for next request
834
+ ```
807
835
 
808
- // Cache is shared across all context instances
809
- const db2 = new AppContext();
810
- const user4 = db2.User.findById(1); // Also uses shared cache
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
- QUERY_CACHE_ENABLED=true # Enable/disable (default: true)
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
- QUERY_CACHE_ENABLED=true
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
- #### Disable Caching for Specific Queries
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 `.noCache()` for real-time data that shouldn't be cached:
882
+ Use `.cache()` for frequently accessed, rarely changed data:
832
883
 
833
884
  ```javascript
834
- // Always hit database (never cached)
885
+ // DEFAULT: Always hits database (safe)
835
886
  const liveData = db.Analytics
836
887
  .where(a => a.date == $$, today)
837
- .noCache() // Skip cache
838
- .toList();
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
- // Reference data (highly cacheable)
841
- const categories = db.Categories.toList(); // Cached for 5 minutes
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 is cached
909
- const users = db.User.where(u => u.active == true).toList();
968
+ // Query with caching enabled
969
+ const categories = db.Categories.cache().toList(); // DB query, cached
910
970
 
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
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 usersAgain = db.User.where(u => u.active == true).toList();
976
+ // Next cached query hits database (fresh data)
977
+ const categoriesAgain = db.Categories.cache().toList(); // DB query (cache cleared)
918
978
 
919
- // Queries for OTHER tables are unaffected
920
- const posts = db.Post.toList(); // Still cached
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
- // 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')
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
- // Real-time data (always needs fresh results)
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
- .noCache()
965
- .toList();
1029
+ .toList(); // Always fresh
966
1030
 
967
- // Financial transactions (critical accuracy)
1031
+ // Financial transactions (default is safe)
968
1032
  const balance = db.Transactions
969
1033
  .where(t => t.user_id == $$, userId)
970
- .noCache()
971
- .toList();
1034
+ .toList(); // Always fresh
972
1035
 
973
- // User-specific sensitive data (security concern)
1036
+ // User-specific sensitive data (default is safe)
974
1037
  const permissions = db.UserPermissions
975
1038
  .where(p => p.user_id == $$, userId)
976
- .noCache()
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 user1 = db1.User.findById(1); // DB query, cached
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 user2 = db2.User.findById(1); // Cache hit!
1107
+ const categories2 = db2.Categories.cache().toList(); // Cache hit!
1008
1108
 
1009
1109
  // Context 2: Updates invalidate cache for BOTH contexts
1010
- user2.name = "Updated";
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 user3 = db1.User.findById(1); // Cache miss, fresh data
1015
- console.log(user3.name); // "Updated"
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
- .noCache() // Disable caching for this query
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. Leverage Query Caching
1396
+ ### 1. Use Query Caching Selectively
1286
1397
 
1287
1398
  ```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();
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();