masterrecord 0.3.27 → 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
 
@@ -1256,8 +1386,724 @@ await .findById(id) // Find by primary key
1256
1386
 
1257
1387
  // Entity methods (Active Record style - REQUIRE AWAIT)
1258
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
+ }
1259
1912
  ```
1260
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"
1947
+ ```
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
+
1261
2107
  ### Migration Methods
1262
2108
 
1263
2109
  ```javascript
@@ -1653,7 +2499,128 @@ Created by Alexander Rich
1653
2499
 
1654
2500
  ---
1655
2501
 
1656
- ## 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
1657
2624
 
1658
2625
  MasterRecord has been upgraded to meet **FAANG engineering standards** (Google/Meta/Amazon) with critical bug fixes and performance improvements:
1659
2626