masterrecord 0.3.26 → 0.3.29

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
@@ -10,6 +10,11 @@
10
10
  🔹 **Multi-Database Support** - MySQL, PostgreSQL, SQLite with consistent API
11
11
  🔹 **Code-First Design** - Define entities in JavaScript, generate schema automatically
12
12
  🔹 **Fluent Query API** - Lambda-based queries with parameterized placeholders
13
+ 🔹 **Active Record Pattern** - Entities with `.save()`, `.delete()`, `.reload()` methods
14
+ 🔹 **Entity Serialization** - `.toObject()` and `.toJSON()` with circular reference protection
15
+ 🔹 **Lifecycle Hooks** - `beforeSave`, `afterSave`, `beforeDelete`, `afterDelete` hooks
16
+ 🔹 **Business Validation** - Built-in validators (required, email, length, pattern, custom)
17
+ 🔹 **Bulk Operations** - Efficient `bulkCreate`, `bulkUpdate`, `bulkDelete` APIs
13
18
  🔹 **Query Result Caching** - Production-grade in-memory and Redis caching with automatic invalidation
14
19
  🔹 **Migration System** - CLI-driven migrations with rollback support
15
20
  🔹 **SQL Injection Protection** - Automatic parameterized queries throughout
@@ -33,6 +38,24 @@
33
38
  - [Database Configuration](#database-configuration)
34
39
  - [Entity Definitions](#entity-definitions)
35
40
  - [Querying](#querying)
41
+ - [Entity Serialization](#entity-serialization)
42
+ - [.toObject()](#toobjectoptions)
43
+ - [.toJSON()](#tojson)
44
+ - [Entity Instance Methods](#entity-instance-methods)
45
+ - [.delete()](#delete)
46
+ - [.reload()](#reload)
47
+ - [.clone()](#clone)
48
+ - [Query Helper Methods](#query-helper-methods)
49
+ - [.first()](#first)
50
+ - [.last()](#last)
51
+ - [.exists()](#exists)
52
+ - [.pluck()](#pluckfieldname)
53
+ - [Lifecycle Hooks](#lifecycle-hooks)
54
+ - [Business Logic Validation](#business-logic-validation)
55
+ - [Bulk Operations API](#bulk-operations-api)
56
+ - [bulkCreate()](#bulkcreateentityname-data)
57
+ - [bulkUpdate()](#bulkupdateentityname-updates)
58
+ - [bulkDelete()](#bulkdeleteentityname-ids)
36
59
  - [Migrations](#migrations)
37
60
  - [Advanced Features](#advanced-features)
38
61
  - [Query Result Caching](#query-result-caching)
@@ -45,6 +68,113 @@
45
68
  - [Examples](#examples)
46
69
  - [Performance Tips](#performance-tips)
47
70
  - [Security](#security)
71
+ - [Best Practices](#best-practices-critical)
72
+
73
+ ---
74
+
75
+ ## ⚠️ Best Practices (CRITICAL)
76
+
77
+ ### 1. Creating Entity Instances
78
+
79
+ **ALWAYS** use `context.Entity.new()` to create new entity instances:
80
+
81
+ ```javascript
82
+ // ✅ CORRECT - Creates proper data instance with getters/setters
83
+ const task = this._qaContext.QaTask.new();
84
+ const annotation = this._qaContext.QaAnnotation.new();
85
+ const project = this._qaContext.QaProject.new();
86
+
87
+ task.name = "My Task";
88
+ task.status = "active";
89
+ await db.saveChanges(); // ✅ Saves correctly
90
+
91
+ // ❌ WRONG - Creates schema definition object with function properties
92
+ const task = new QaTask(); // task.name is a FUNCTION, not a property!
93
+
94
+ task.name = "My Task"; // ❌ Doesn't work - name is a function
95
+ await db.saveChanges(); // ❌ Error: "Type mismatch: Expected string, got function"
96
+ ```
97
+
98
+ **Why?**
99
+ - `new Entity()` creates a **schema definition object** where properties are methods that define the schema
100
+ - `context.Entity.new()` creates a **data instance** with proper getters/setters for storing values
101
+ - Using `new Entity()` causes runtime errors: `"Type mismatch for Entity.field: Expected integer, got function with value undefined"`
102
+
103
+ **Error Example:**
104
+ ```
105
+ Error: INSERT failed: Type mismatch for QaTask.name: Expected string, got function with value undefined
106
+ at SQLLiteEngine._buildSQLInsertObjectParameterized
107
+ ```
108
+
109
+ **This error means:** You used `new Entity()` instead of `context.Entity.new()`
110
+
111
+ ### 2. Saving Changes - ALWAYS use `await`
112
+
113
+ **ALWAYS** use `await` when calling `saveChanges()`:
114
+
115
+ ```javascript
116
+ // ✅ CORRECT - Waits for database write to complete
117
+ await this._qaContext.saveChanges();
118
+
119
+ // ❌ WRONG - Returns immediately without waiting for database write
120
+ this._qaContext.saveChanges(); // Promise never completes!
121
+ ```
122
+
123
+ **Why?**
124
+ - `saveChanges()` is **async** and returns a Promise
125
+ - Without `await`, code continues before database write completes
126
+ - Causes **data loss** - appears successful but nothing saves to database
127
+ - Results in "phantom saves" - data in memory but not persisted
128
+
129
+ **Symptoms of missing `await`:**
130
+ - API returns success but data not in database
131
+ - Queries after save return old/missing data
132
+ - Intermittent save failures
133
+ - Race conditions
134
+
135
+ **Repository Pattern - Make Methods Async:**
136
+ ```javascript
137
+ // ✅ CORRECT - Async method with await
138
+ async create(entity) {
139
+ this._qaContext.Entity.add(entity);
140
+ await this._qaContext.saveChanges();
141
+ return entity;
142
+ }
143
+
144
+ // ❌ WRONG - Synchronous method calling async saveChanges
145
+ create(entity) {
146
+ this._qaContext.Entity.add(entity);
147
+ this._qaContext.saveChanges(); // No await - returns before save completes!
148
+ return entity; // Returns entity with undefined ID
149
+ }
150
+ ```
151
+
152
+ ### 3. Quick Reference Card
153
+
154
+ ```javascript
155
+ // Entity Creation
156
+ ✅ const user = db.User.new(); // CORRECT
157
+ ❌ const user = new User(); // WRONG - creates schema object
158
+
159
+ // Saving Data
160
+ ✅ await db.saveChanges(); // CORRECT - waits for completion
161
+ ❌ db.saveChanges(); // WRONG - fire and forget
162
+
163
+ // Repository Methods
164
+ ✅ async create(entity) { // CORRECT - async method
165
+ await db.saveChanges();
166
+ }
167
+ ❌ create(entity) { // WRONG - sync method
168
+ db.saveChanges(); // No await!
169
+ }
170
+
171
+ // Querying (all require await)
172
+ ✅ const users = await db.User.toList(); // CORRECT
173
+ ✅ const user = await db.User.findById(1); // CORRECT
174
+ ❌ const users = db.User.toList(); // WRONG - returns Promise
175
+ ```
176
+
177
+ ---
48
178
 
49
179
  ## Installation
50
180
 
@@ -416,7 +546,7 @@ const user = db.User.new();
416
546
  user.tags = ['admin', 'moderator']; // Assign array
417
547
  await db.saveChanges(); // Stored as '["admin","moderator"]'
418
548
 
419
- const loaded = db.User.findById(user.id);
549
+ const loaded = await db.User.findById(user.id);
420
550
  console.log(loaded.tags); // ['admin', 'moderator'] - JavaScript array!
421
551
  ```
422
552
 
@@ -425,19 +555,19 @@ console.log(loaded.tags); // ['admin', 'moderator'] - JavaScript array!
425
555
  ### Basic Queries
426
556
 
427
557
  ```javascript
428
- // Find all
429
- const users = db.User.toList();
558
+ // Find all (requires await)
559
+ const users = await db.User.toList();
430
560
 
431
- // Find by primary key
432
- const user = db.User.findById(123);
561
+ // Find by primary key (requires await)
562
+ const user = await db.User.findById(123);
433
563
 
434
- // Find single with where clause
435
- const alice = db.User
564
+ // Find single with where clause (requires await)
565
+ const alice = await db.User
436
566
  .where(u => u.email == $$, 'alice@example.com')
437
567
  .single();
438
568
 
439
- // Find multiple with conditions
440
- const adults = db.User
569
+ // Find multiple with conditions (requires await)
570
+ const adults = await db.User
441
571
  .where(u => u.age >= $$, 18)
442
572
  .toList();
443
573
  ```
@@ -447,16 +577,16 @@ const adults = db.User
447
577
  **Always use `$$` placeholders** for SQL injection protection:
448
578
 
449
579
  ```javascript
450
- // Single parameter
451
- const user = db.User.where(u => u.id == $$, 123).single();
580
+ // Single parameter (requires await)
581
+ const user = await db.User.where(u => u.id == $$, 123).single();
452
582
 
453
- // Multiple parameters
454
- const results = db.User
583
+ // Multiple parameters (requires await)
584
+ const results = await db.User
455
585
  .where(u => u.age > $$ && u.status == $$, 25, 'active')
456
586
  .toList();
457
587
 
458
- // Single $ for OR conditions
459
- const results = db.User
588
+ // Single $ for OR conditions (requires await)
589
+ const results = await db.User
460
590
  .where(u => u.status == $ || u.status == null, 'active')
461
591
  .toList();
462
592
  ```
@@ -464,22 +594,22 @@ const results = db.User
464
594
  ### IN Clauses
465
595
 
466
596
  ```javascript
467
- // Array parameter with .includes()
597
+ // Array parameter with .includes() (requires await)
468
598
  const ids = [1, 2, 3, 4, 5];
469
- const users = db.User
599
+ const users = await db.User
470
600
  .where(u => $$.includes(u.id), ids)
471
601
  .toList();
472
602
 
473
603
  // Generated SQL: WHERE id IN ($1, $2, $3, $4, $5)
474
604
  // PostgreSQL parameters: [1, 2, 3, 4, 5]
475
605
 
476
- // Alternative .any() syntax
477
- const users = db.User
606
+ // Alternative .any() syntax (requires await)
607
+ const users = await db.User
478
608
  .where(u => u.id.any($$), [1, 2, 3])
479
609
  .toList();
480
610
 
481
- // Comma-separated strings (auto-splits)
482
- const users = db.User
611
+ // Comma-separated strings (auto-splits) (requires await)
612
+ const users = await db.User
483
613
  .where(u => u.id.any($$), "1,2,3,4,5")
484
614
  .toList();
485
615
  ```
@@ -498,8 +628,8 @@ if (minAge) {
498
628
  query = query.where(u => u.age >= $$, minAge);
499
629
  }
500
630
 
501
- // Add sorting and pagination
502
- const users = query
631
+ // Add sorting and pagination (requires await)
632
+ const users = await query
503
633
  .orderBy(u => u.created_at)
504
634
  .skip(offset)
505
635
  .take(limit)
@@ -509,13 +639,13 @@ const users = query
509
639
  ### Ordering
510
640
 
511
641
  ```javascript
512
- // Ascending
513
- const users = db.User
642
+ // Ascending (requires await)
643
+ const users = await db.User
514
644
  .orderBy(u => u.name)
515
645
  .toList();
516
646
 
517
- // Descending
518
- const users = db.User
647
+ // Descending (requires await)
648
+ const users = await db.User
519
649
  .orderByDescending(u => u.created_at)
520
650
  .toList();
521
651
  ```
@@ -523,17 +653,17 @@ const users = db.User
523
653
  ### Pagination
524
654
 
525
655
  ```javascript
526
- // Skip 20, take 10
527
- const users = db.User
656
+ // Skip 20, take 10 (requires await)
657
+ const users = await db.User
528
658
  .orderBy(u => u.id)
529
659
  .skip(20)
530
660
  .take(10)
531
661
  .toList();
532
662
 
533
- // Page-based pagination
663
+ // Page-based pagination (requires await)
534
664
  const page = 2;
535
665
  const pageSize = 10;
536
- const users = db.User
666
+ const users = await db.User
537
667
  .skip(page * pageSize)
538
668
  .take(pageSize)
539
669
  .toList();
@@ -542,11 +672,11 @@ const users = db.User
542
672
  ### Counting
543
673
 
544
674
  ```javascript
545
- // Count all
546
- const total = db.User.count();
675
+ // Count all (requires await)
676
+ const total = await db.User.count();
547
677
 
548
- // Count with conditions
549
- const activeCount = db.User
678
+ // Count with conditions (requires await)
679
+ const activeCount = await db.User
550
680
  .where(u => u.status == $$, 'active')
551
681
  .count();
552
682
  ```
@@ -554,19 +684,19 @@ const activeCount = db.User
554
684
  ### Complex Queries
555
685
 
556
686
  ```javascript
557
- // Multiple conditions with OR
558
- const results = db.User
687
+ // Multiple conditions with OR (requires await)
688
+ const results = await db.User
559
689
  .where(u => (u.status == 'active' || u.status == 'pending') && u.age >= $$, 18)
560
690
  .orderBy(u => u.name)
561
691
  .toList();
562
692
 
563
- // Nullable checks
564
- const usersWithoutEmail = db.User
693
+ // Nullable checks (requires await)
694
+ const usersWithoutEmail = await db.User
565
695
  .where(u => u.email == null)
566
696
  .toList();
567
697
 
568
- // LIKE queries
569
- const matching = db.User
698
+ // LIKE queries (requires await)
699
+ const matching = await db.User
570
700
  .where(u => u.name.like($$), '%john%')
571
701
  .toList();
572
702
  ```
@@ -838,13 +968,13 @@ const user = db.User.findById(1); // DB query
838
968
  const user2 = db.User.findById(1); // DB query again (no cache)
839
969
 
840
970
  // OPT-IN: Enable caching with .cache()
841
- const categories = db.Categories.cache().toList(); // DB query, cached
842
- const categories2 = db.Categories.cache().toList(); // Cache hit! (instant)
971
+ const categories = await db.Categories.cache().toList(); // DB query, cached
972
+ const categories2 = await db.Categories.cache().toList(); // Cache hit! (instant)
843
973
 
844
974
  // Update invalidates cache automatically
845
- const cat = db.Categories.findById(1);
975
+ const cat = await db.Categories.findById(1);
846
976
  cat.name = "Updated";
847
- db.saveChanges(); // Cache for Categories table cleared
977
+ await db.saveChanges(); // Cache for Categories table cleared
848
978
 
849
979
  // End request (clears cache - like Active Record)
850
980
  db.endRequest(); // Cache cleared for next request
@@ -865,9 +995,9 @@ app.use((req, res, next) => {
865
995
  });
866
996
 
867
997
  // In your routes
868
- app.get('/categories', (req, res) => {
998
+ app.get('/categories', async (req, res) => {
869
999
  // Cache is fresh for this request
870
- const categories = req.db.Categories.cache().toList();
1000
+ const categories = await req.db.Categories.cache().toList();
871
1001
  res.json(categories);
872
1002
  // Cache auto-cleared after response
873
1003
  });
@@ -900,14 +1030,14 @@ Use `.cache()` for frequently accessed, rarely changed data:
900
1030
 
901
1031
  ```javascript
902
1032
  // DEFAULT: Always hits database (safe)
903
- const liveData = db.Analytics
1033
+ const liveData = await db.Analytics
904
1034
  .where(a => a.date == $$, today)
905
1035
  .toList(); // No caching (default)
906
1036
 
907
1037
  // OPT-IN: Cache reference data
908
- const categories = db.Categories.cache().toList(); // Cached for 5 seconds (default TTL)
909
- const settings = db.Settings.cache().toList(); // Cached
910
- const countries = db.Countries.cache().toList(); // Cached
1038
+ const categories = await db.Categories.cache().toList(); // Cached for 5 seconds (default TTL)
1039
+ const settings = await db.Settings.cache().toList(); // Cached
1040
+ const countries = await db.Countries.cache().toList(); // Cached
911
1041
 
912
1042
  // When to use .cache():
913
1043
  // ✅ Reference data (categories, settings, countries)
@@ -940,7 +1070,7 @@ db.clearQueryCache();
940
1070
 
941
1071
  // Disable caching temporarily
942
1072
  db.setQueryCacheEnabled(false);
943
- const freshData = db.User.toList();
1073
+ const freshData = await db.User.toList();
944
1074
  db.setQueryCacheEnabled(true);
945
1075
  ```
946
1076
 
@@ -983,21 +1113,21 @@ MasterRecord automatically invalidates cache entries when data changes:
983
1113
 
984
1114
  ```javascript
985
1115
  // Query with caching enabled
986
- const categories = db.Categories.cache().toList(); // DB query, cached
1116
+ const categories = await db.Categories.cache().toList(); // DB query, cached
987
1117
 
988
1118
  // Any modification to Categories table invalidates ALL cached Category queries
989
- const cat = db.Categories.findById(1);
1119
+ const cat = await db.Categories.findById(1);
990
1120
  cat.name = "Updated";
991
- db.saveChanges(); // Invalidates all cached Categories queries
1121
+ await db.saveChanges(); // Invalidates all cached Categories queries
992
1122
 
993
1123
  // Next cached query hits database (fresh data)
994
- const categoriesAgain = db.Categories.cache().toList(); // DB query (cache cleared)
1124
+ const categoriesAgain = await db.Categories.cache().toList(); // DB query (cache cleared)
995
1125
 
996
1126
  // Non-cached queries are unaffected (always fresh)
997
- const users = db.User.toList(); // No .cache() = always DB query
1127
+ const users = await db.User.toList(); // No .cache() = always DB query
998
1128
 
999
1129
  // Queries for OTHER tables' caches are unaffected
1000
- const settings = db.Settings.cache().toList(); // Still cached (different table)
1130
+ const settings = await db.Settings.cache().toList(); // Still cached (different table)
1001
1131
  ```
1002
1132
 
1003
1133
  **Invalidation rules:**
@@ -1024,12 +1154,12 @@ Expected performance improvements:
1024
1154
  **DO use .cache():**
1025
1155
  ```javascript
1026
1156
  // Reference data (rarely changes)
1027
- const categories = db.Categories.cache().toList();
1028
- const settings = db.Settings.cache().toList();
1029
- const countries = db.Countries.cache().toList();
1157
+ const categories = await db.Categories.cache().toList();
1158
+ const settings = await db.Settings.cache().toList();
1159
+ const countries = await db.Countries.cache().toList();
1030
1160
 
1031
1161
  // Expensive aggregations (stable results)
1032
- const totalRevenue = db.Orders
1162
+ const totalRevenue = await db.Orders
1033
1163
  .where(o => o.year == $$, 2024)
1034
1164
  .cache()
1035
1165
  .count();
@@ -1038,20 +1168,20 @@ const totalRevenue = db.Orders
1038
1168
  **DON'T use .cache():**
1039
1169
  ```javascript
1040
1170
  // User-specific data (default is safe - no caching)
1041
- const user = db.User.findById(userId); // Always fresh
1171
+ const user = await db.User.findById(userId); // Always fresh
1042
1172
 
1043
1173
  // Real-time data (default is safe)
1044
- const liveOrders = db.Orders
1174
+ const liveOrders = await db.Orders
1045
1175
  .where(o => o.status == $$, 'pending')
1046
1176
  .toList(); // Always fresh
1047
1177
 
1048
1178
  // Financial transactions (default is safe)
1049
- const balance = db.Transactions
1179
+ const balance = await db.Transactions
1050
1180
  .where(t => t.user_id == $$, userId)
1051
1181
  .toList(); // Always fresh
1052
1182
 
1053
1183
  // User-specific sensitive data (default is safe)
1054
- const permissions = db.UserPermissions
1184
+ const permissions = await db.UserPermissions
1055
1185
  .where(p => p.user_id == $$, userId)
1056
1186
  .toList(); // Always fresh
1057
1187
  ```
@@ -1089,12 +1219,12 @@ app.use((req, res, next) => {
1089
1219
  });
1090
1220
 
1091
1221
  // In routes - cache is fresh per request
1092
- app.get('/api/categories', (req, res) => {
1222
+ app.get('/api/categories', async (req, res) => {
1093
1223
  // First call in this request - DB query
1094
- const categories = req.db.Categories.cache().toList();
1224
+ const categories = await req.db.Categories.cache().toList();
1095
1225
 
1096
1226
  // Second call in same request - cache hit
1097
- const categoriesAgain = req.db.Categories.cache().toList();
1227
+ const categoriesAgain = await req.db.Categories.cache().toList();
1098
1228
 
1099
1229
  res.json(categories);
1100
1230
  // After response, cache is automatically cleared
@@ -1118,18 +1248,18 @@ const db1 = new AppContext();
1118
1248
  const db2 = new AppContext();
1119
1249
 
1120
1250
  // Context 1: Cache data with .cache()
1121
- const categories1 = db1.Categories.cache().toList(); // DB query, cached
1251
+ const categories1 = await db1.Categories.cache().toList(); // DB query, cached
1122
1252
 
1123
1253
  // Context 2: Sees cached data
1124
- const categories2 = db2.Categories.cache().toList(); // Cache hit!
1254
+ const categories2 = await db2.Categories.cache().toList(); // Cache hit!
1125
1255
 
1126
1256
  // Context 2: Updates invalidate cache for BOTH contexts
1127
- const cat = db2.Categories.findById(1);
1257
+ const cat = await db2.Categories.findById(1);
1128
1258
  cat.name = "Updated";
1129
- db2.saveChanges(); // Invalidates shared cache
1259
+ await db2.saveChanges(); // Invalidates shared cache
1130
1260
 
1131
1261
  // Context 1: Sees fresh data
1132
- const categories3 = db1.Categories.cache().toList(); // Cache miss, fresh data
1262
+ const categories3 = await db1.Categories.cache().toList(); // Cache miss, fresh data
1133
1263
  console.log(categories3[0].name); // "Updated"
1134
1264
  ```
1135
1265
 
@@ -1168,8 +1298,9 @@ class AnalyticsContext extends context {
1168
1298
  const userDb = new UserContext();
1169
1299
  const analyticsDb = new AnalyticsContext();
1170
1300
 
1171
- const user = userDb.User.findById(123);
1172
- analyticsDb.Event.new().log('user_login', user.id);
1301
+ const user = await userDb.User.findById(123);
1302
+ const event = analyticsDb.Event.new();
1303
+ event.log('user_login', user.id);
1173
1304
  await analyticsDb.saveChanges();
1174
1305
  ```
1175
1306
 
@@ -1232,7 +1363,7 @@ context.setQueryCacheEnabled(bool) // Enable/disable caching
1232
1363
  ### Query Methods
1233
1364
 
1234
1365
  ```javascript
1235
- // Chainable query builders
1366
+ // Chainable query builders (do not execute query)
1236
1367
  .where(query, ...params) // Add WHERE condition
1237
1368
  .and(query, ...params) // Add AND condition
1238
1369
  .orderBy(field) // Sort ascending
@@ -1242,21 +1373,737 @@ context.setQueryCacheEnabled(bool) // Enable/disable caching
1242
1373
  .include(relationship) // Eager load
1243
1374
  .cache() // Enable caching for this query (opt-in)
1244
1375
 
1245
- // Terminal methods (execute query)
1246
- .toList() // Return array of all records
1247
- .single() // Return one or null
1248
- .first() // Return first or null
1249
- .count() // Return count
1250
- .any() // Return boolean
1376
+ // Terminal methods (execute query - ALL REQUIRE AWAIT)
1377
+ await .toList() // Return array of all records
1378
+ await .single() // Return one or null
1379
+ await .first() // Return first or null
1380
+ await .count() // Return count
1381
+ await .any() // Return boolean
1251
1382
 
1252
- // Convenience methods
1253
- .findById(id) // Find by primary key
1254
- .new() // Create new entity instance
1383
+ // Convenience methods (REQUIRE AWAIT)
1384
+ await .findById(id) // Find by primary key
1385
+ .new() // Create new entity instance (synchronous)
1255
1386
 
1256
- // Entity methods (Active Record style)
1387
+ // Entity methods (Active Record style - REQUIRE AWAIT)
1257
1388
  await entity.save() // Save this entity (and all tracked changes)
1389
+ await entity.delete() // Delete this entity
1390
+ await entity.reload() // Reload from database, discarding changes
1391
+ entity.clone() // Create a copy for duplication (synchronous)
1392
+ entity.toObject(options) // Convert to plain JavaScript object (synchronous)
1393
+ entity.toJSON() // JSON.stringify compatibility (synchronous)
1394
+ ```
1395
+
1396
+ ---
1397
+
1398
+ ## Entity Serialization
1399
+
1400
+ ### .toObject(options)
1401
+
1402
+ Convert a MasterRecord entity to a plain JavaScript object, removing all internal properties and handling circular references automatically.
1403
+
1404
+ **Parameters:**
1405
+ - `options.includeRelationships` (boolean, default: `true`) - Include related entities
1406
+ - `options.depth` (number, default: `1`) - Maximum depth for relationship traversal
1407
+
1408
+ **Examples:**
1409
+
1410
+ ```javascript
1411
+ // Basic usage - get plain object
1412
+ const user = await db.User.findById(1);
1413
+ const plain = user.toObject();
1414
+ console.log(plain);
1415
+ // { id: 1, name: 'Alice', email: 'alice@example.com', age: 28 }
1416
+
1417
+ // Include relationships
1418
+ const userWithPosts = user.toObject({ includeRelationships: true });
1419
+ console.log(userWithPosts);
1420
+ // {
1421
+ // id: 1,
1422
+ // name: 'Alice',
1423
+ // Posts: [
1424
+ // { id: 10, title: 'First Post', content: '...' },
1425
+ // { id: 11, title: 'Second Post', content: '...' }
1426
+ // ]
1427
+ // }
1428
+
1429
+ // Control relationship depth
1430
+ const deep = user.toObject({ includeRelationships: true, depth: 3 });
1431
+
1432
+ // Exclude relationships
1433
+ const shallow = user.toObject({ includeRelationships: false });
1434
+ ```
1435
+
1436
+ **Circular Reference Protection:**
1437
+
1438
+ `.toObject()` automatically prevents infinite loops from circular references:
1439
+
1440
+ ```javascript
1441
+ // Scenario: User → Posts → User creates a cycle
1442
+ const user = await db.User.findById(1);
1443
+ await user.Posts; // Load posts relationship
1444
+
1445
+ const plain = user.toObject({ includeRelationships: true, depth: 2 });
1446
+ // Circular references marked as:
1447
+ // { __circular: true, __entityName: 'User', id: 1 }
1448
+ ```
1449
+
1450
+ **Why It's Needed:**
1451
+
1452
+ MasterRecord entities have internal properties that cause `JSON.stringify()` to fail:
1453
+
1454
+ ```javascript
1455
+ const user = await db.User.findById(1);
1456
+
1457
+ // ❌ FAILS: TypeError: Converting circular structure to JSON
1458
+ JSON.stringify(user);
1459
+
1460
+ // ✅ WORKS: Use toObject() or toJSON()
1461
+ const plain = user.toObject();
1462
+ JSON.stringify(plain); // Success!
1463
+ ```
1464
+
1465
+ ### .toJSON()
1466
+
1467
+ Used automatically by `JSON.stringify()` and Express `res.json()`. Returns the same as `.toObject({ includeRelationships: false })`.
1468
+
1469
+ **Examples:**
1470
+
1471
+ ```javascript
1472
+ // JSON.stringify automatically calls toJSON()
1473
+ const user = await db.User.findById(1);
1474
+ const json = JSON.stringify(user);
1475
+ console.log(json);
1476
+ // '{"id":1,"name":"Alice","email":"alice@example.com"}'
1477
+
1478
+ // Express automatically uses toJSON()
1479
+ app.get('/api/users/:id', async (req, res) => {
1480
+ const user = await db.User.findById(req.params.id);
1481
+ res.json(user); // ✅ Works automatically!
1482
+ });
1483
+
1484
+ // Array of entities
1485
+ app.get('/api/users', async (req, res) => {
1486
+ const users = await db.User.toList();
1487
+ res.json(users); // ✅ Each entity's toJSON() called automatically
1488
+ });
1489
+ ```
1490
+
1491
+ ---
1492
+
1493
+ ## Entity Instance Methods
1494
+
1495
+ ### .delete()
1496
+
1497
+ Delete an entity without manually calling `context.remove()` and `context.saveChanges()`.
1498
+
1499
+ **Example:**
1500
+
1501
+ ```javascript
1502
+ // Before
1503
+ const user = await db.User.findById(1);
1504
+ db.remove(user);
1505
+ await db.saveChanges();
1506
+
1507
+ // After (Active Record style)
1508
+ const user = await db.User.findById(1);
1509
+ await user.delete(); // ✅ Entity deletes itself
1510
+ ```
1511
+
1512
+ **Cascade Deletion:**
1513
+
1514
+ If your entity has cascade delete rules, they will be applied automatically:
1515
+
1516
+ ```javascript
1517
+ class User {
1518
+ constructor() {
1519
+ this.id = { type: 'integer', primary: true, auto: true };
1520
+
1521
+ // Posts will be deleted when user is deleted
1522
+ this.Posts = {
1523
+ type: 'hasMany',
1524
+ model: 'Post',
1525
+ foreignKey: 'user_id',
1526
+ cascade: true // Enable cascade delete
1527
+ };
1528
+ }
1529
+ }
1530
+
1531
+ const user = await db.User.findById(1);
1532
+ await user.delete(); // ✅ Also deletes related Posts automatically
1533
+ ```
1534
+
1535
+ ### .reload()
1536
+
1537
+ Refresh an entity from the database, discarding any unsaved changes.
1538
+
1539
+ **Example:**
1540
+
1541
+ ```javascript
1542
+ const user = await db.User.findById(1);
1543
+ console.log(user.name); // 'Alice'
1544
+
1545
+ user.name = 'Modified';
1546
+ console.log(user.name); // 'Modified'
1547
+
1548
+ await user.reload(); // ✅ Fetch fresh data from database
1549
+ console.log(user.name); // 'Alice' - changes discarded
1550
+ ```
1551
+
1552
+ **Use Cases:**
1553
+ - Discard unsaved changes
1554
+ - Refresh stale data after external updates
1555
+ - Synchronize after concurrent modifications
1556
+ - Reset entity to clean state
1557
+
1558
+ ### .clone()
1559
+
1560
+ Create a copy of an entity for duplication (primary key excluded).
1561
+
1562
+ **Example:**
1563
+
1564
+ ```javascript
1565
+ const user = await db.User.findById(1);
1566
+ const duplicate = user.clone();
1567
+
1568
+ duplicate.name = 'Copy of ' + user.name;
1569
+ duplicate.email = 'copy@example.com';
1570
+
1571
+ await duplicate.save();
1572
+ console.log(duplicate.id); // ✅ New ID (different from original)
1573
+ ```
1574
+
1575
+ **Notes:**
1576
+ - Primary key is automatically excluded
1577
+ - Relationships are not cloned (set manually if needed)
1578
+ - Useful for templates and duplicating records
1579
+
1580
+ ---
1581
+
1582
+ ## Query Helper Methods
1583
+
1584
+ ### .first()
1585
+
1586
+ Get the first record ordered by primary key.
1587
+
1588
+ **Example:**
1589
+
1590
+ ```javascript
1591
+ // Automatically orders by primary key
1592
+ const firstUser = await db.User.first();
1593
+
1594
+ // With custom order (respects existing orderBy)
1595
+ const newestUser = await db.User
1596
+ .orderByDescending(u => u.created_at)
1597
+ .first();
1598
+
1599
+ // With conditions
1600
+ const firstActive = await db.User
1601
+ .where(u => u.status == $$, 'active')
1602
+ .first();
1603
+ ```
1604
+
1605
+ ### .last()
1606
+
1607
+ Get the last record ordered by primary key (descending).
1608
+
1609
+ **Example:**
1610
+
1611
+ ```javascript
1612
+ const lastUser = await db.User.last();
1613
+
1614
+ // With custom order
1615
+ const oldestUser = await db.User
1616
+ .orderBy(u => u.created_at)
1617
+ .last();
1618
+ ```
1619
+
1620
+ ### .exists()
1621
+
1622
+ Check if any records match the query (returns boolean).
1623
+
1624
+ **Example:**
1625
+
1626
+ ```javascript
1627
+ // Before
1628
+ const count = await db.User
1629
+ .where(u => u.email == $$, 'test@example.com')
1630
+ .count();
1631
+ const exists = count > 0;
1632
+
1633
+ // After
1634
+ const exists = await db.User
1635
+ .where(u => u.email == $$, 'test@example.com')
1636
+ .exists();
1637
+
1638
+ if (exists) {
1639
+ throw new Error('Email already registered');
1640
+ }
1641
+
1642
+ // Check if any users exist
1643
+ const hasUsers = await db.User.exists();
1644
+ if (!hasUsers) {
1645
+ // Create default admin user
1646
+ }
1647
+ ```
1648
+
1649
+ ### .pluck(fieldName)
1650
+
1651
+ Extract a single column as an array.
1652
+
1653
+ **Example:**
1654
+
1655
+ ```javascript
1656
+ // Get all active user emails
1657
+ const emails = await db.User
1658
+ .where(u => u.status == $$, 'active')
1659
+ .pluck('email');
1660
+ console.log(emails);
1661
+ // ['alice@example.com', 'bob@example.com', 'charlie@example.com']
1662
+
1663
+ // Get all user IDs
1664
+ const ids = await db.User.pluck('id');
1665
+ console.log(ids); // [1, 2, 3, 4, 5]
1666
+
1667
+ // With sorting
1668
+ const recentEmails = await db.User
1669
+ .orderByDescending(u => u.created_at)
1670
+ .take(10)
1671
+ .pluck('email');
1672
+ ```
1673
+
1674
+ ---
1675
+
1676
+ ## Lifecycle Hooks
1677
+
1678
+ Add lifecycle hooks to your entity definitions to execute logic before/after database operations.
1679
+
1680
+ **Available Hooks:**
1681
+ - `beforeSave()` - Execute before insert or update
1682
+ - `afterSave()` - Execute after insert or update
1683
+ - `beforeDelete()` - Execute before deletion
1684
+ - `afterDelete()` - Execute after deletion
1685
+
1686
+ **Example:**
1687
+
1688
+ ```javascript
1689
+ const bcrypt = require('bcrypt');
1690
+
1691
+ class User {
1692
+ constructor() {
1693
+ this.id = { type: 'integer', primary: true, auto: true };
1694
+ this.email = { type: 'string' };
1695
+ this.password = { type: 'string' };
1696
+ this.created_at = { type: 'timestamp' };
1697
+ this.updated_at = { type: 'timestamp' };
1698
+ this.role = { type: 'string' };
1699
+ }
1700
+
1701
+ // Hash password before saving
1702
+ beforeSave() {
1703
+ // Only hash if password was changed
1704
+ if (this.__dirtyFields.includes('password')) {
1705
+ this.password = bcrypt.hashSync(this.password, 10);
1706
+ }
1707
+ }
1708
+
1709
+ // Set timestamps automatically
1710
+ beforeSave() {
1711
+ if (this.__state === 'insert') {
1712
+ this.created_at = new Date();
1713
+ }
1714
+ this.updated_at = new Date();
1715
+ }
1716
+
1717
+ // Log after successful save
1718
+ afterSave() {
1719
+ console.log(`User ${this.id} saved successfully`);
1720
+ }
1721
+
1722
+ // Prevent deleting admin users
1723
+ beforeDelete() {
1724
+ if (this.role === 'admin') {
1725
+ throw new Error('Cannot delete admin user');
1726
+ }
1727
+ }
1728
+
1729
+ // Cleanup related data after deletion
1730
+ async afterDelete() {
1731
+ console.log(`User ${this.id} deleted, cleaning up related data...`);
1732
+ // Cleanup logic here (e.g., delete user files, clear cache)
1733
+ }
1734
+ }
1735
+ ```
1736
+
1737
+ **Usage:**
1738
+
1739
+ ```javascript
1740
+ // Hooks execute automatically during save
1741
+ const user = db.User.new();
1742
+ user.email = 'alice@example.com';
1743
+ user.password = 'plain-text-password';
1744
+ await user.save();
1745
+ // ✅ beforeSave() hashes password automatically
1746
+ // ✅ afterSave() logs success message
1747
+
1748
+ // Load and update
1749
+ const user = await db.User.findById(1);
1750
+ user.email = 'newemail@example.com';
1751
+ await user.save();
1752
+ // ✅ beforeSave() sets updated_at timestamp
1753
+ // ✅ Password not re-hashed (not in dirtyFields)
1754
+
1755
+ // Hooks can prevent operations
1756
+ const admin = await db.User.where(u => u.role == $$, 'admin').single();
1757
+ try {
1758
+ await admin.delete();
1759
+ } catch (error) {
1760
+ console.log(error.message); // "Cannot delete admin user"
1761
+ }
1762
+ // ✅ beforeDelete() prevented deletion
1763
+ ```
1764
+
1765
+ **Hook Execution Order:**
1766
+
1767
+ ```javascript
1768
+ // Insert:
1769
+ // 1. beforeSave()
1770
+ // 2. SQL INSERT
1771
+ // 3. afterSave()
1772
+
1773
+ // Update:
1774
+ // 1. beforeSave()
1775
+ // 2. SQL UPDATE
1776
+ // 3. afterSave()
1777
+
1778
+ // Delete:
1779
+ // 1. beforeDelete()
1780
+ // 2. SQL DELETE
1781
+ // 3. afterDelete()
1782
+ ```
1783
+
1784
+ **Notes:**
1785
+ - Hooks can be async (use `async` keyword)
1786
+ - Exceptions in `before*` hooks prevent the operation
1787
+ - Hooks execute for each entity during batch operations
1788
+ - Access entity state via `this.__state` ('insert', 'modified', 'delete')
1789
+ - Access changed fields via `this.__dirtyFields` array
1790
+
1791
+ ---
1792
+
1793
+ ## Business Logic Validation
1794
+
1795
+ Add validators to your entity definitions for automatic validation on property assignment.
1796
+
1797
+ **Built-in Validators:**
1798
+ - `required(message)` - Field must have a value
1799
+ - `email(message)` - Must be valid email format
1800
+ - `minLength(length, message)` - Minimum string length
1801
+ - `maxLength(length, message)` - Maximum string length
1802
+ - `pattern(regex, message)` - Must match regex pattern
1803
+ - `min(value, message)` - Minimum numeric value
1804
+ - `max(value, message)` - Maximum numeric value
1805
+ - `custom(fn, message)` - Custom validation function
1806
+
1807
+ **Example:**
1808
+
1809
+ ```javascript
1810
+ class User {
1811
+ id(db) {
1812
+ db.integer().primary().auto();
1813
+ }
1814
+
1815
+ name(db) {
1816
+ db.string()
1817
+ .required('Name is required')
1818
+ .minLength(3, 'Name must be at least 3 characters')
1819
+ .maxLength(50, 'Name cannot exceed 50 characters');
1820
+ }
1821
+
1822
+ email(db) {
1823
+ db.string()
1824
+ .required('Email is required')
1825
+ .email('Must be a valid email address');
1826
+ }
1827
+
1828
+ password(db) {
1829
+ db.string()
1830
+ .required('Password is required')
1831
+ .minLength(8, 'Password must be at least 8 characters')
1832
+ .maxLength(100);
1833
+ }
1834
+
1835
+ username(db) {
1836
+ db.string()
1837
+ .required()
1838
+ .pattern(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores');
1839
+ }
1840
+
1841
+ age(db) {
1842
+ db.integer()
1843
+ .min(18, 'Must be at least 18 years old')
1844
+ .max(120, 'Age cannot exceed 120');
1845
+ }
1846
+
1847
+ status(db) {
1848
+ db.string()
1849
+ .custom((value) => {
1850
+ return ['active', 'inactive', 'pending'].includes(value);
1851
+ }, 'Status must be active, inactive, or pending');
1852
+ }
1853
+ }
1854
+ ```
1855
+
1856
+ **Validation Execution:**
1857
+
1858
+ Validators run automatically when you assign values:
1859
+
1860
+ ```javascript
1861
+ const user = db.User.new();
1862
+
1863
+ // ❌ Validation fails immediately
1864
+ try {
1865
+ user.email = 'invalid-email';
1866
+ } catch (error) {
1867
+ console.log(error.message);
1868
+ // "Validation failed: Must be a valid email address"
1869
+ }
1870
+
1871
+ // ✅ Valid value accepted
1872
+ user.email = 'valid@example.com'; // OK
1873
+
1874
+ // ❌ Length validation
1875
+ try {
1876
+ user.password = 'short';
1877
+ } catch (error) {
1878
+ console.log(error.message);
1879
+ // "Validation failed: Password must be at least 8 characters"
1880
+ }
1881
+
1882
+ // ✅ Valid password
1883
+ user.password = 'secure-password-123'; // OK
1884
+
1885
+ // ❌ Custom validation
1886
+ try {
1887
+ user.status = 'invalid-status';
1888
+ } catch (error) {
1889
+ console.log(error.message);
1890
+ // "Validation failed: Status must be active, inactive, or pending"
1891
+ }
1892
+
1893
+ // ✅ Valid status
1894
+ user.status = 'active'; // OK
1895
+
1896
+ // Save (all fields already validated)
1897
+ await user.save();
1898
+ ```
1899
+
1900
+ **Validator Chaining:**
1901
+
1902
+ Validators can be chained together:
1903
+
1904
+ ```javascript
1905
+ email(db) {
1906
+ db.string()
1907
+ .required('Email is required') // ← First validator
1908
+ .email('Invalid email format') // ← Second validator
1909
+ .minLength(5, 'Email too short') // ← Third validator
1910
+ .maxLength(100, 'Email too long'); // ← Fourth validator
1911
+ }
1912
+ ```
1913
+
1914
+ **Custom Validation:**
1915
+
1916
+ ```javascript
1917
+ discount(db) {
1918
+ db.integer()
1919
+ .min(0, 'Discount cannot be negative')
1920
+ .max(100, 'Discount cannot exceed 100%')
1921
+ .custom((value) => {
1922
+ // Only allow multiples of 5
1923
+ return value % 5 === 0;
1924
+ }, 'Discount must be a multiple of 5');
1925
+ }
1926
+
1927
+ // Usage
1928
+ product.discount = 7; // ❌ Throws: "Discount must be a multiple of 5"
1929
+ product.discount = 10; // ✅ OK
1930
+ ```
1931
+
1932
+ **Nullable Fields:**
1933
+
1934
+ Required validation respects nullable fields:
1935
+
1936
+ ```javascript
1937
+ bio(db) {
1938
+ db.string()
1939
+ .maxLength(500, 'Bio cannot exceed 500 characters');
1940
+ // No .required() = field is optional
1941
+ }
1942
+
1943
+ // Both are valid
1944
+ user.bio = null; // ✅ OK (nullable)
1945
+ user.bio = 'Short bio'; // ✅ OK (with value)
1946
+ user.bio = 'a'.repeat(501); // ❌ Throws: "Bio cannot exceed 500 characters"
1258
1947
  ```
1259
1948
 
1949
+ ---
1950
+
1951
+ ## Bulk Operations API
1952
+
1953
+ Efficiently create, update, or delete multiple entities in a single operation.
1954
+
1955
+ **Available Methods:**
1956
+ - `context.bulkCreate(entityName, data)` - Create multiple entities
1957
+ - `context.bulkUpdate(entityName, updates)` - Update multiple entities
1958
+ - `context.bulkDelete(entityName, ids)` - Delete multiple entities
1959
+
1960
+ ### bulkCreate(entityName, data)
1961
+
1962
+ Create multiple entities efficiently in a batch operation.
1963
+
1964
+ **Example:**
1965
+
1966
+ ```javascript
1967
+ // Create 5 users at once
1968
+ const users = await db.bulkCreate('User', [
1969
+ { name: 'Alice', email: 'alice@example.com', status: 'active' },
1970
+ { name: 'Bob', email: 'bob@example.com', status: 'active' },
1971
+ { name: 'Charlie', email: 'charlie@example.com', status: 'inactive' },
1972
+ { name: 'Dave', email: 'dave@example.com', status: 'active' },
1973
+ { name: 'Eve', email: 'eve@example.com', status: 'pending' }
1974
+ ]);
1975
+
1976
+ console.log(users.length); // 5
1977
+ console.log(users[0].id); // 1 (auto-increment IDs assigned)
1978
+ console.log(users[4].id); // 5
1979
+
1980
+ // Entities are returned in the same order
1981
+ console.log(users.map(u => u.name));
1982
+ // ['Alice', 'Bob', 'Charlie', 'Dave', 'Eve']
1983
+ ```
1984
+
1985
+ **Performance:**
1986
+
1987
+ ```javascript
1988
+ // ❌ SLOW: Multiple individual inserts
1989
+ for (const data of users) {
1990
+ const user = db.User.new();
1991
+ user.name = data.name;
1992
+ user.email = data.email;
1993
+ await user.save(); // Separate database call
1994
+ }
1995
+
1996
+ // ✅ FAST: Single bulk insert
1997
+ await db.bulkCreate('User', users); // One database call
1998
+ ```
1999
+
2000
+ ### bulkUpdate(entityName, updates)
2001
+
2002
+ Update multiple entities by their primary keys.
2003
+
2004
+ **Example:**
2005
+
2006
+ ```javascript
2007
+ // Update multiple users' status
2008
+ await db.bulkUpdate('User', [
2009
+ { id: 1, status: 'inactive' },
2010
+ { id: 2, status: 'inactive' },
2011
+ { id: 4, status: 'inactive' }
2012
+ ]);
2013
+
2014
+ // Verify updates
2015
+ const user1 = await db.User.findById(1);
2016
+ console.log(user1.status); // 'inactive'
2017
+
2018
+ // Other fields unchanged
2019
+ console.log(user1.name); // Original name preserved
2020
+ console.log(user1.email); // Original email preserved
2021
+ ```
2022
+
2023
+ **Partial Updates:**
2024
+
2025
+ Only the fields you specify are updated:
2026
+
2027
+ ```javascript
2028
+ // Update only email for multiple users
2029
+ await db.bulkUpdate('User', [
2030
+ { id: 1, email: 'newemail1@example.com' },
2031
+ { id: 2, email: 'newemail2@example.com' }
2032
+ ]);
2033
+
2034
+ // name, status, age, etc. remain unchanged
2035
+ ```
2036
+
2037
+ ### bulkDelete(entityName, ids)
2038
+
2039
+ Delete multiple entities by their primary keys.
2040
+
2041
+ **Example:**
2042
+
2043
+ ```javascript
2044
+ // Delete multiple users by ID
2045
+ await db.bulkDelete('User', [3, 5, 7]);
2046
+
2047
+ // Verify deletion
2048
+ const user3 = await db.User.findById(3);
2049
+ console.log(user3); // null
2050
+
2051
+ const remaining = await db.User.toList();
2052
+ console.log(remaining.length); // Total users minus 3
2053
+ ```
2054
+
2055
+ **Non-Existent IDs:**
2056
+
2057
+ Bulk delete handles non-existent IDs gracefully:
2058
+
2059
+ ```javascript
2060
+ // Some IDs don't exist
2061
+ await db.bulkDelete('User', [999, 1000, 1001]);
2062
+ // ✅ No error thrown - operation completes successfully
2063
+ ```
2064
+
2065
+ **Error Handling:**
2066
+
2067
+ ```javascript
2068
+ // Empty array throws error
2069
+ try {
2070
+ await db.bulkCreate('User', []);
2071
+ } catch (error) {
2072
+ console.log(error.message);
2073
+ // "bulkCreate requires a non-empty array of data"
2074
+ }
2075
+
2076
+ // Invalid entity name throws error
2077
+ try {
2078
+ await db.bulkUpdate('NonExistentEntity', [{ id: 1 }]);
2079
+ } catch (error) {
2080
+ console.log(error.message);
2081
+ // "Entity NonExistentEntity not found"
2082
+ }
2083
+ ```
2084
+
2085
+ **Lifecycle Hooks:**
2086
+
2087
+ Bulk operations execute lifecycle hooks for each entity:
2088
+
2089
+ ```javascript
2090
+ class User {
2091
+ beforeSave() {
2092
+ console.log(`Saving user: ${this.name}`);
2093
+ }
2094
+ }
2095
+
2096
+ await db.bulkCreate('User', [
2097
+ { name: 'Alice' },
2098
+ { name: 'Bob' }
2099
+ ]);
2100
+ // Console output:
2101
+ // Saving user: Alice
2102
+ // Saving user: Bob
2103
+ ```
2104
+
2105
+ ---
2106
+
1260
2107
  ### Migration Methods
1261
2108
 
1262
2109
  ```javascript
@@ -1315,14 +2162,14 @@ demo();
1315
2162
  async function getUsers(page = 0, pageSize = 10) {
1316
2163
  const db = new AppContext();
1317
2164
 
1318
- const users = db.User
2165
+ const users = await db.User
1319
2166
  .where(u => u.status == $$, 'active')
1320
2167
  .orderBy(u => u.created_at)
1321
2168
  .skip(page * pageSize)
1322
2169
  .take(pageSize)
1323
2170
  .toList();
1324
2171
 
1325
- const total = db.User
2172
+ const total = await db.User
1326
2173
  .where(u => u.status == $$, 'active')
1327
2174
  .count();
1328
2175
 
@@ -1373,7 +2220,7 @@ async function searchUsers(filters) {
1373
2220
  .take(filters.pageSize);
1374
2221
  }
1375
2222
 
1376
- return query.toList();
2223
+ return await query.toList();
1377
2224
  }
1378
2225
  ```
1379
2226
 
@@ -1403,7 +2250,7 @@ post.author_id = author.id;
1403
2250
  await db.saveChanges();
1404
2251
 
1405
2252
  // Query with relationships
1406
- const posts = db.Post
2253
+ const posts = await db.Post
1407
2254
  .where(p => p.author_id == $$, author.id)
1408
2255
  .toList();
1409
2256
 
@@ -1416,15 +2263,15 @@ console.log(`${author.name} has ${posts.length} posts`);
1416
2263
 
1417
2264
  ```javascript
1418
2265
  // ✅ GOOD: Cache reference data that rarely changes
1419
- const categories = db.Categories.cache().toList(); // Opt-in caching
1420
- const settings = db.Settings.cache().toList();
2266
+ const categories = await db.Categories.cache().toList(); // Opt-in caching
2267
+ const settings = await db.Settings.cache().toList();
1421
2268
 
1422
2269
  // ✅ GOOD: Queries without .cache() are always fresh (safe default)
1423
- const user1 = db.User.findById(123); // Always DB query (no cache)
1424
- const user2 = db.User.findById(123); // Always DB query (no cache)
2270
+ const user1 = await db.User.findById(123); // Always DB query (no cache)
2271
+ const user2 = await db.User.findById(123); // Always DB query (no cache)
1425
2272
 
1426
2273
  // ✅ GOOD: Cache expensive queries with stable results
1427
- const revenue2024 = db.Orders
2274
+ const revenue2024 = await db.Orders
1428
2275
  .where(o => o.year == $$, 2024)
1429
2276
  .cache() // Historical data doesn't change
1430
2277
  .count();
@@ -1472,13 +2319,13 @@ class User {
1472
2319
 
1473
2320
  ```javascript
1474
2321
  // ✅ GOOD: Limit results
1475
- const recentUsers = db.User
2322
+ const recentUsers = await db.User
1476
2323
  .orderByDescending(u => u.created_at)
1477
2324
  .take(100)
1478
2325
  .toList();
1479
2326
 
1480
2327
  // ❌ BAD: Load everything
1481
- const allUsers = db.User.toList();
2328
+ const allUsers = await db.User.toList();
1482
2329
  ```
1483
2330
 
1484
2331
  ### 5. Use Connection Pooling (PostgreSQL)
@@ -1500,7 +2347,7 @@ MasterRecord uses **parameterized queries throughout** to prevent SQL injection:
1500
2347
 
1501
2348
  ```javascript
1502
2349
  // ✅ SAFE: Parameterized
1503
- const user = db.User.where(u => u.name == $$, userInput).single();
2350
+ const user = await db.User.where(u => u.name == $$, userInput).single();
1504
2351
 
1505
2352
  // ❌ UNSAFE: Never do this
1506
2353
  // const query = `SELECT * FROM User WHERE name = '${userInput}'`;
@@ -1520,12 +2367,12 @@ While SQL injection is prevented, always validate business logic:
1520
2367
 
1521
2368
  ```javascript
1522
2369
  // Validate input before querying
1523
- function getUser(userId) {
2370
+ async function getUser(userId) {
1524
2371
  if (!Number.isInteger(userId) || userId <= 0) {
1525
2372
  throw new Error('Invalid user ID');
1526
2373
  }
1527
2374
 
1528
- return db.User.findById(userId);
2375
+ return await db.User.findById(userId);
1529
2376
  }
1530
2377
  ```
1531
2378
 
@@ -1652,7 +2499,128 @@ Created by Alexander Rich
1652
2499
 
1653
2500
  ---
1654
2501
 
1655
- ## Recent Improvements (v0.3.13)
2502
+ ## Recent Improvements
2503
+
2504
+ ### v0.3.30 - Mature ORM Features (Latest)
2505
+
2506
+ MasterRecord is now feature-complete with lifecycle hooks, validation, and bulk operations - matching the capabilities of mature ORMs like Sequelize, TypeORM, and Prisma.
2507
+
2508
+ **🎯 Entity Serialization:**
2509
+ - ✅ **`.toObject()`** - Convert entities to plain JavaScript objects with circular reference protection
2510
+ - ✅ **`.toJSON()`** - Automatic JSON.stringify() compatibility for Express responses
2511
+ - ✅ **Circular Reference Handling** - Prevents infinite loops from bidirectional relationships
2512
+ - ✅ **Depth Control** - Configurable relationship traversal depth
2513
+
2514
+ **🎯 Active Record Pattern:**
2515
+ - ✅ **`.delete()`** - Entities can delete themselves (`await user.delete()`)
2516
+ - ✅ **`.reload()`** - Refresh entity from database, discard unsaved changes
2517
+ - ✅ **`.clone()`** - Create entity copies for duplication (excludes primary key)
2518
+ - ✅ **`.save()`** - Already existed, now part of complete Active Record pattern
2519
+
2520
+ **🎯 Query Helpers:**
2521
+ - ✅ **`.first()`** - Get first record ordered by primary key
2522
+ - ✅ **`.last()`** - Get last record ordered by primary key descending
2523
+ - ✅ **`.exists()`** - Check if any records match query (returns boolean)
2524
+ - ✅ **`.pluck(field)`** - Extract single column values as array
2525
+
2526
+ **🎯 Lifecycle Hooks:**
2527
+ - ✅ **`beforeSave()`** - Execute before insert or update (e.g., hash passwords)
2528
+ - ✅ **`afterSave()`** - Execute after successful save (e.g., logging)
2529
+ - ✅ **`beforeDelete()`** - Execute before deletion (can prevent deletion)
2530
+ - ✅ **`afterDelete()`** - Execute after deletion (e.g., cleanup)
2531
+ - ✅ **Hook Execution Order** - Guaranteed execution order with error handling
2532
+ - ✅ **Async Support** - Hooks can be async for database operations
2533
+
2534
+ **🎯 Business Logic Validation:**
2535
+ - ✅ **`.required()`** - Field must have a value
2536
+ - ✅ **`.email()`** - Must be valid email format
2537
+ - ✅ **`.minLength()` / `.maxLength()`** - String length constraints
2538
+ - ✅ **`.min()` / `.max()`** - Numeric value constraints
2539
+ - ✅ **`.pattern()`** - Must match regex pattern
2540
+ - ✅ **`.custom()`** - Custom validation functions
2541
+ - ✅ **Chainable Validators** - Multiple validators per field
2542
+ - ✅ **Immediate Validation** - Errors thrown on property assignment
2543
+
2544
+ **🎯 Bulk Operations API:**
2545
+ - ✅ **`bulkCreate()`** - Create multiple entities efficiently in one transaction
2546
+ - ✅ **`bulkUpdate()`** - Update multiple entities by primary key
2547
+ - ✅ **`bulkDelete()`** - Delete multiple entities by primary key
2548
+ - ✅ **Lifecycle Hook Support** - Hooks execute for each entity in bulk operations
2549
+ - ✅ **Auto-Increment IDs** - IDs properly assigned after bulk inserts
2550
+
2551
+ **🎯 Critical Bug Fixes:**
2552
+ - ✅ **Auto-Increment ID Bug Fixed** - IDs now correctly set on entities after insert (SQLite, MySQL, PostgreSQL)
2553
+ - ✅ **Lifecycle Hook Isolation** - Hooks excluded from SQL queries and INSERT/UPDATE operations
2554
+ - ✅ **Circular Reference Prevention** - WeakSet-based tracking prevents infinite loops
2555
+
2556
+ **Example Usage:**
2557
+
2558
+ ```javascript
2559
+ // Entity serialization
2560
+ const user = await db.User.findById(1);
2561
+ const plain = user.toObject({ includeRelationships: true, depth: 2 });
2562
+ res.json(user); // Works automatically with toJSON()
2563
+
2564
+ // Active Record pattern
2565
+ await user.delete(); // Entity deletes itself
2566
+ await user.reload(); // Discard changes
2567
+ const copy = user.clone(); // Duplicate entity
2568
+
2569
+ // Query helpers
2570
+ const first = await db.User.first();
2571
+ const exists = await db.User.where(u => u.email == $$, 'test@test.com').exists();
2572
+ const emails = await db.User.where(u => u.status == $$, 'active').pluck('email');
2573
+
2574
+ // Lifecycle hooks
2575
+ class User {
2576
+ beforeSave() {
2577
+ if (this.__dirtyFields.includes('password')) {
2578
+ this.password = bcrypt.hashSync(this.password, 10);
2579
+ }
2580
+ this.updated_at = new Date();
2581
+ }
2582
+
2583
+ beforeDelete() {
2584
+ if (this.role === 'admin') {
2585
+ throw new Error('Cannot delete admin user');
2586
+ }
2587
+ }
2588
+ }
2589
+
2590
+ // Business validation
2591
+ class User {
2592
+ email(db) {
2593
+ db.string()
2594
+ .required('Email is required')
2595
+ .email('Must be a valid email address');
2596
+ }
2597
+
2598
+ age(db) {
2599
+ db.integer()
2600
+ .min(18, 'Must be at least 18 years old')
2601
+ .max(120);
2602
+ }
2603
+ }
2604
+
2605
+ // Bulk operations
2606
+ const users = await db.bulkCreate('User', [
2607
+ { name: 'Alice', email: 'alice@example.com' },
2608
+ { name: 'Bob', email: 'bob@example.com' },
2609
+ { name: 'Charlie', email: 'charlie@example.com' }
2610
+ ]);
2611
+ console.log(users.map(u => u.id)); // [1, 2, 3] - IDs assigned
2612
+
2613
+ await db.bulkUpdate('User', [
2614
+ { id: 1, status: 'inactive' },
2615
+ { id: 2, status: 'inactive' }
2616
+ ]);
2617
+
2618
+ await db.bulkDelete('User', [3, 5, 7]);
2619
+ ```
2620
+
2621
+ ---
2622
+
2623
+ ### v0.3.13 - FAANG Engineering Standards
1656
2624
 
1657
2625
  MasterRecord has been upgraded to meet **FAANG engineering standards** (Google/Meta/Amazon) with critical bug fixes and performance improvements:
1658
2626