masterrecord 0.3.8 → 0.3.10
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/.eslintrc.js +290 -0
- package/.prettierrc.js +109 -0
- package/CHANGES.md +170 -0
- package/Entity/entityTrackerModel.js +17 -3
- package/Migrations/cli.js +4 -2
- package/Migrations/migrations.js +13 -10
- package/Migrations/pathUtils.js +76 -0
- package/Migrations/pathUtils.test.js +53 -0
- package/QueryLanguage/queryMethods.js +15 -0
- package/context.js +1186 -398
- package/deleteManager.js +137 -40
- package/docs/ACTIVE_RECORD_PATTERN.md +477 -0
- package/docs/DETACHED_ENTITIES_GUIDE.md +445 -0
- package/insertManager.js +358 -200
- package/package.json +1 -1
- package/readme.md +217 -7
- package/test/attachDetached.test.js +303 -0
- /package/{QUERY_CACHING_GUIDE.md → docs/QUERY_CACHING_GUIDE.md} +0 -0
package/deleteManager.js
CHANGED
|
@@ -1,60 +1,157 @@
|
|
|
1
|
-
// version 0.0
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// version 1.0.0 - FAANG-Level Refactored
|
|
2
|
+
const tools = require('./Tools');
|
|
3
|
+
|
|
4
|
+
// Constants for relationship types
|
|
5
|
+
const RELATIONSHIP_TYPES = {
|
|
6
|
+
HAS_ONE: 'hasOne',
|
|
7
|
+
HAS_MANY: 'hasMany',
|
|
8
|
+
HAS_MANY_THROUGH: 'hasManyThrough'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Manages cascade deletion of entities and their relationships
|
|
13
|
+
* @class DeleteManager
|
|
14
|
+
*/
|
|
15
|
+
class DeleteManager {
|
|
16
|
+
constructor(sqlEngine, entities) {
|
|
17
|
+
if (!sqlEngine) {
|
|
18
|
+
throw new Error('DeleteManager requires a valid SQL engine');
|
|
19
|
+
}
|
|
5
20
|
this._SQLEngine = sqlEngine;
|
|
6
|
-
this._allEntities = entities;
|
|
21
|
+
this._allEntities = entities || [];
|
|
7
22
|
}
|
|
8
23
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Initialize deletion for an entity or array of entities
|
|
26
|
+
* @param {Object|Array} currentModel - Entity or entities to delete
|
|
27
|
+
* @throws {Error} If deletion fails
|
|
28
|
+
*/
|
|
29
|
+
init(currentModel) {
|
|
30
|
+
// Input validation
|
|
31
|
+
if (!currentModel) {
|
|
32
|
+
throw new Error('DeleteManager.init() requires a valid model');
|
|
13
33
|
}
|
|
14
|
-
|
|
15
|
-
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
this.cascadeDelete(currentModel);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Add context to error
|
|
39
|
+
const entityName = currentModel.__entity?.__name || 'unknown';
|
|
40
|
+
throw new Error(`Failed to delete ${entityName}: ${error.message}`, { cause: error });
|
|
16
41
|
}
|
|
17
42
|
}
|
|
18
43
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Recursively cascade delete an entity and its relationships
|
|
46
|
+
* @param {Object|Array} currentModel - Entity or entities to delete
|
|
47
|
+
* @throws {Error} If cascade deletion fails
|
|
48
|
+
*/
|
|
49
|
+
cascadeDelete(currentModel) {
|
|
50
|
+
if (!currentModel) {
|
|
51
|
+
return; // Nothing to delete
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!Array.isArray(currentModel)) {
|
|
55
|
+
this._deleteSingleEntity(currentModel);
|
|
56
|
+
} else {
|
|
57
|
+
this._deleteMultipleEntities(currentModel);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Delete a single entity with cascade
|
|
63
|
+
* @private
|
|
64
|
+
* @param {Object} entity - Entity to delete
|
|
65
|
+
*/
|
|
66
|
+
_deleteSingleEntity(entity) {
|
|
67
|
+
// Validate entity structure
|
|
68
|
+
if (!entity.__entity) {
|
|
69
|
+
throw new Error('Entity missing __entity metadata');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const entityKeys = Object.keys(entity.__entity);
|
|
73
|
+
|
|
74
|
+
// Loop through all entity properties to find relationships
|
|
75
|
+
for (const property of entityKeys) {
|
|
76
|
+
const propertyConfig = entity.__entity[property];
|
|
77
|
+
|
|
78
|
+
// Check if this is a relationship that needs cascade deletion
|
|
79
|
+
if (this._isRelationshipType(propertyConfig.type)) {
|
|
80
|
+
const relatedModel = entity[property];
|
|
81
|
+
|
|
82
|
+
if (relatedModel === null || relatedModel === undefined) {
|
|
83
|
+
// Check if relationship is required (not nullable)
|
|
84
|
+
if (!propertyConfig.nullable) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Cannot delete ${entity.__entity.__name}: ` +
|
|
87
|
+
`required relationship '${property}' is null. ` +
|
|
88
|
+
`Set nullable: true if this is intentional.`
|
|
89
|
+
);
|
|
37
90
|
}
|
|
91
|
+
} else {
|
|
92
|
+
// Recursively delete related entities
|
|
93
|
+
this.cascadeDelete(relatedModel);
|
|
38
94
|
}
|
|
39
95
|
}
|
|
40
|
-
this._SQLEngine.delete(currentModel);
|
|
41
96
|
}
|
|
42
|
-
else{
|
|
43
97
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
98
|
+
// Delete the entity itself after cascading
|
|
99
|
+
this._SQLEngine.delete(entity);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Delete multiple entities with cascade
|
|
104
|
+
* @private
|
|
105
|
+
* @param {Array} entities - Array of entities to delete
|
|
106
|
+
*/
|
|
107
|
+
_deleteMultipleEntities(entities) {
|
|
108
|
+
if (entities.length === 0) {
|
|
109
|
+
return; // Nothing to delete
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Process each entity
|
|
113
|
+
for (let i = 0; i < entities.length; i++) {
|
|
114
|
+
const entity = entities[i];
|
|
115
|
+
|
|
116
|
+
if (!entity) {
|
|
117
|
+
console.warn(`DeleteManager: Skipping null entity at index ${i}`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!entity.__entity) {
|
|
122
|
+
throw new Error(`Entity at index ${i} missing __entity metadata`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const entityKeys = Object.keys(entity.__entity);
|
|
126
|
+
|
|
127
|
+
// Loop through relationships for cascade
|
|
128
|
+
for (const property of entityKeys) {
|
|
129
|
+
const propertyConfig = entity.__entity[property];
|
|
130
|
+
|
|
131
|
+
if (this._isRelationshipType(propertyConfig.type)) {
|
|
132
|
+
const relatedModel = entity[property];
|
|
133
|
+
|
|
134
|
+
if (relatedModel !== null && relatedModel !== undefined) {
|
|
135
|
+
this.cascadeDelete(relatedModel);
|
|
51
136
|
}
|
|
52
137
|
}
|
|
53
|
-
this._SQLEngine.delete(currentModel[i]);
|
|
54
138
|
}
|
|
139
|
+
|
|
140
|
+
// Delete the entity
|
|
141
|
+
this._SQLEngine.delete(entity);
|
|
55
142
|
}
|
|
56
|
-
|
|
143
|
+
}
|
|
57
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Check if a property type is a relationship that requires cascade deletion
|
|
147
|
+
* @private
|
|
148
|
+
* @param {string} type - Property type
|
|
149
|
+
* @returns {boolean} True if relationship type
|
|
150
|
+
*/
|
|
151
|
+
_isRelationshipType(type) {
|
|
152
|
+
return type === RELATIONSHIP_TYPES.HAS_ONE ||
|
|
153
|
+
type === RELATIONSHIP_TYPES.HAS_MANY ||
|
|
154
|
+
type === RELATIONSHIP_TYPES.HAS_MANY_THROUGH;
|
|
58
155
|
}
|
|
59
156
|
}
|
|
60
157
|
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# Active Record Pattern - entity.save()
|
|
2
|
+
|
|
3
|
+
MasterRecord now supports **both** patterns:
|
|
4
|
+
|
|
5
|
+
1. **Entity Framework style** - `context.saveChanges()`
|
|
6
|
+
2. **Active Record style** - `entity.save()` ✨ NEW!
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Active Record Style (NEW)
|
|
11
|
+
|
|
12
|
+
### Basic Usage
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
// Load entity
|
|
16
|
+
const task = db.Task.findById(1);
|
|
17
|
+
|
|
18
|
+
// Modify
|
|
19
|
+
task.status = 'completed';
|
|
20
|
+
task.completed_at = new Date();
|
|
21
|
+
|
|
22
|
+
// Save (like Active Record!)
|
|
23
|
+
await task.save(); // ✅ Entity saves itself!
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Create New Entity
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
// Create
|
|
30
|
+
const task = db.Task.new();
|
|
31
|
+
task.name = 'New Task';
|
|
32
|
+
task.status = 'pending';
|
|
33
|
+
|
|
34
|
+
// Save
|
|
35
|
+
await task.save(); // ✅ Like Active Record!
|
|
36
|
+
|
|
37
|
+
console.log(task.id); // Auto-generated ID
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Update Multiple Fields
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
const user = db.User.findById(userId);
|
|
44
|
+
user.name = 'Updated Name';
|
|
45
|
+
user.email = 'new@example.com';
|
|
46
|
+
user.updated_at = new Date();
|
|
47
|
+
|
|
48
|
+
await user.save(); // ✅ All changes saved
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Solving the Detached Entity Problem
|
|
54
|
+
|
|
55
|
+
### Before (Manual attach)
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
// ❌ Required manual attach
|
|
59
|
+
const task = await taskService.getTask(taskId);
|
|
60
|
+
task.status = 'completed';
|
|
61
|
+
db.attach(task); // Manual attach
|
|
62
|
+
await db.saveChanges();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### After (Active Record style)
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
// ✅ Just call .save()!
|
|
69
|
+
const task = await taskService.getTask(taskId);
|
|
70
|
+
task.status = 'completed';
|
|
71
|
+
await task.save(); // Entity saves itself!
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Why this works:**
|
|
75
|
+
- Entity has `__context` reference
|
|
76
|
+
- `.save()` automatically tracks the entity
|
|
77
|
+
- No manual `attach()` needed!
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Your Original Problem - SOLVED
|
|
82
|
+
|
|
83
|
+
### Your Original Code (BROKEN)
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
class AnnotationService {
|
|
87
|
+
async createAnnotation(taskId, data) {
|
|
88
|
+
const task = await this._taskService.getTask(taskId);
|
|
89
|
+
|
|
90
|
+
const annotation = this._qaContext.Annotation.new();
|
|
91
|
+
annotation.task_id = taskId;
|
|
92
|
+
this._qaContext.Annotation.add(annotation);
|
|
93
|
+
|
|
94
|
+
task.status = 'completed'; // ❌ Not tracked
|
|
95
|
+
|
|
96
|
+
await this._qaContext.saveChanges(); // ❌ Task not saved
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Solution 1: Active Record Style (EASIEST) ✨
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
class AnnotationService {
|
|
105
|
+
async createAnnotation(taskId, data) {
|
|
106
|
+
const task = await this._taskService.getTask(taskId);
|
|
107
|
+
|
|
108
|
+
const annotation = this._qaContext.Annotation.new();
|
|
109
|
+
annotation.task_id = taskId;
|
|
110
|
+
annotation.data = data;
|
|
111
|
+
|
|
112
|
+
task.status = 'completed';
|
|
113
|
+
|
|
114
|
+
// Save both (Active Record style!)
|
|
115
|
+
await annotation.save(); // ✅ Saves annotation
|
|
116
|
+
await task.save(); // ✅ Saves task
|
|
117
|
+
|
|
118
|
+
return annotation;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Solution 2: Entity Framework Style
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
class AnnotationService {
|
|
127
|
+
async createAnnotation(taskId, data) {
|
|
128
|
+
const task = await this._taskService.getTask(taskId);
|
|
129
|
+
|
|
130
|
+
const annotation = this._qaContext.Annotation.new();
|
|
131
|
+
annotation.task_id = taskId;
|
|
132
|
+
this._qaContext.Annotation.add(annotation);
|
|
133
|
+
|
|
134
|
+
task.status = 'completed';
|
|
135
|
+
this._qaContext.attach(task); // Attach detached entity
|
|
136
|
+
|
|
137
|
+
await this._qaContext.saveChanges(); // Save all
|
|
138
|
+
return annotation;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Solution 3: Pure Active Record
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
class AnnotationService {
|
|
147
|
+
async createAnnotation(taskId, data) {
|
|
148
|
+
const task = await this._taskService.getTask(taskId);
|
|
149
|
+
task.status = 'completed';
|
|
150
|
+
await task.save(); // Save task first
|
|
151
|
+
|
|
152
|
+
const annotation = this._qaContext.Annotation.new();
|
|
153
|
+
annotation.task_id = taskId;
|
|
154
|
+
annotation.data = data;
|
|
155
|
+
await annotation.save(); // Save annotation
|
|
156
|
+
|
|
157
|
+
return annotation;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Comparison: Both Patterns
|
|
165
|
+
|
|
166
|
+
### Entity Framework Style
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
// Load entities
|
|
170
|
+
const user = db.User.findById(1);
|
|
171
|
+
const task = db.Task.findById(2);
|
|
172
|
+
|
|
173
|
+
// Modify both
|
|
174
|
+
user.name = 'Updated';
|
|
175
|
+
task.status = 'completed';
|
|
176
|
+
|
|
177
|
+
// Save all at once
|
|
178
|
+
await db.saveChanges(); // Batch save
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Pros:**
|
|
182
|
+
- ✅ Batch operations (efficient)
|
|
183
|
+
- ✅ Transaction-like behavior
|
|
184
|
+
- ✅ One save call for multiple entities
|
|
185
|
+
|
|
186
|
+
**Cons:**
|
|
187
|
+
- ❌ Less intuitive
|
|
188
|
+
- ❌ Need to track which context has which entities
|
|
189
|
+
|
|
190
|
+
### Active Record Style (NEW)
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
// Load entities
|
|
194
|
+
const user = db.User.findById(1);
|
|
195
|
+
const task = db.Task.findById(2);
|
|
196
|
+
|
|
197
|
+
// Modify and save individually
|
|
198
|
+
user.name = 'Updated';
|
|
199
|
+
await user.save(); // Save user
|
|
200
|
+
|
|
201
|
+
task.status = 'completed';
|
|
202
|
+
await task.save(); // Save task
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Pros:**
|
|
206
|
+
- ✅ More intuitive (entity saves itself)
|
|
207
|
+
- ✅ No detached entity issues
|
|
208
|
+
- ✅ Works across contexts
|
|
209
|
+
- ✅ Familiar to Rails developers
|
|
210
|
+
|
|
211
|
+
**Cons:**
|
|
212
|
+
- ❌ Multiple database calls
|
|
213
|
+
- ❌ No automatic batching
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## When to Use Each Pattern
|
|
218
|
+
|
|
219
|
+
### Use `entity.save()` when:
|
|
220
|
+
|
|
221
|
+
✅ Working with single entities
|
|
222
|
+
✅ Entity from external service (detached)
|
|
223
|
+
✅ Quick updates to one entity
|
|
224
|
+
✅ Familiar with Active Record (Rails)
|
|
225
|
+
✅ Want explicit control
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
// Single entity updates
|
|
229
|
+
const user = db.User.findById(userId);
|
|
230
|
+
user.last_login = new Date();
|
|
231
|
+
await user.save(); // Clear and simple
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Use `context.saveChanges()` when:
|
|
235
|
+
|
|
236
|
+
✅ Batch operations (multiple entities)
|
|
237
|
+
✅ Need transaction-like behavior
|
|
238
|
+
✅ Performance critical (fewer DB calls)
|
|
239
|
+
✅ Familiar with Entity Framework
|
|
240
|
+
|
|
241
|
+
```javascript
|
|
242
|
+
// Batch updates
|
|
243
|
+
const users = db.User.where(u => u.active == false).toList();
|
|
244
|
+
users.forEach(u => u.deleted_at = new Date());
|
|
245
|
+
await db.saveChanges(); // One batch save
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Complete Examples
|
|
251
|
+
|
|
252
|
+
### Example 1: User Registration
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
async function registerUser(data) {
|
|
256
|
+
const db = new AppContext();
|
|
257
|
+
|
|
258
|
+
// Create user
|
|
259
|
+
const user = db.User.new();
|
|
260
|
+
user.email = data.email;
|
|
261
|
+
user.name = data.name;
|
|
262
|
+
user.password_hash = await hash(data.password);
|
|
263
|
+
|
|
264
|
+
// Save (Active Record style)
|
|
265
|
+
await user.save(); // ✅
|
|
266
|
+
|
|
267
|
+
// Send welcome email
|
|
268
|
+
await sendWelcomeEmail(user);
|
|
269
|
+
|
|
270
|
+
return user;
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Example 2: Task Completion
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
async function completeTask(taskId) {
|
|
278
|
+
const db = new AppContext();
|
|
279
|
+
|
|
280
|
+
const task = db.Task.findById(taskId);
|
|
281
|
+
task.status = 'completed';
|
|
282
|
+
task.completed_at = new Date();
|
|
283
|
+
|
|
284
|
+
// Save (Active Record style)
|
|
285
|
+
await task.save(); // ✅
|
|
286
|
+
|
|
287
|
+
// Notify user
|
|
288
|
+
await notifyUser(task.user_id, 'Task completed');
|
|
289
|
+
|
|
290
|
+
return task;
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Example 3: Bulk Update (Entity Framework style)
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
async function archiveOldTasks() {
|
|
298
|
+
const db = new AppContext();
|
|
299
|
+
|
|
300
|
+
const oldTasks = db.Task
|
|
301
|
+
.where(t => t.status == $$, 'completed')
|
|
302
|
+
.where(t => t.completed_at < $$, thirtyDaysAgo)
|
|
303
|
+
.toList();
|
|
304
|
+
|
|
305
|
+
oldTasks.forEach(task => {
|
|
306
|
+
task.archived = true;
|
|
307
|
+
task.archived_at = new Date();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Batch save (Entity Framework style)
|
|
311
|
+
await db.saveChanges(); // ✅ Efficient batch update
|
|
312
|
+
|
|
313
|
+
return oldTasks.length;
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Example 4: Mixed Pattern
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
async function processOrder(orderId) {
|
|
321
|
+
const db = new AppContext();
|
|
322
|
+
|
|
323
|
+
// Load order
|
|
324
|
+
const order = db.Order.findById(orderId);
|
|
325
|
+
order.status = 'processing';
|
|
326
|
+
await order.save(); // Save immediately (Active Record)
|
|
327
|
+
|
|
328
|
+
// Create line items
|
|
329
|
+
const items = order.items.map(item => {
|
|
330
|
+
const lineItem = db.LineItem.new();
|
|
331
|
+
lineItem.order_id = orderId;
|
|
332
|
+
lineItem.product_id = item.productId;
|
|
333
|
+
lineItem.quantity = item.quantity;
|
|
334
|
+
return lineItem;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Batch save line items (Entity Framework)
|
|
338
|
+
await db.saveChanges(); // ✅ Efficient for multiple items
|
|
339
|
+
|
|
340
|
+
return order;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Rails vs MasterRecord
|
|
347
|
+
|
|
348
|
+
### Rails (Active Record)
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
# Create
|
|
352
|
+
user = User.new
|
|
353
|
+
user.name = 'John'
|
|
354
|
+
user.save
|
|
355
|
+
|
|
356
|
+
# Update
|
|
357
|
+
user = User.find(1)
|
|
358
|
+
user.name = 'Jane'
|
|
359
|
+
user.save
|
|
360
|
+
|
|
361
|
+
# Delete
|
|
362
|
+
user.destroy
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### MasterRecord (Active Record style)
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
// Create
|
|
369
|
+
const user = db.User.new();
|
|
370
|
+
user.name = 'John';
|
|
371
|
+
await user.save();
|
|
372
|
+
|
|
373
|
+
// Update
|
|
374
|
+
const user = db.User.findById(1);
|
|
375
|
+
user.name = 'Jane';
|
|
376
|
+
await user.save();
|
|
377
|
+
|
|
378
|
+
// Delete
|
|
379
|
+
db.remove(user);
|
|
380
|
+
await db.saveChanges(); // Or await user.save()
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Important Notes
|
|
386
|
+
|
|
387
|
+
### 1. `.save()` saves ALL tracked changes
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
const db = new AppContext();
|
|
391
|
+
|
|
392
|
+
const user1 = db.User.findById(1);
|
|
393
|
+
user1.name = 'Updated 1';
|
|
394
|
+
|
|
395
|
+
const user2 = db.User.findById(2);
|
|
396
|
+
user2.name = 'Updated 2';
|
|
397
|
+
|
|
398
|
+
// Calling save() on either entity saves BOTH
|
|
399
|
+
await user1.save(); // Saves user1 AND user2!
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Why?** `.save()` calls `context.saveChanges()`, which saves all tracked entities in that context.
|
|
403
|
+
|
|
404
|
+
**To save only one entity:** Use separate contexts or save immediately:
|
|
405
|
+
|
|
406
|
+
```javascript
|
|
407
|
+
// Option 1: Separate contexts
|
|
408
|
+
const db1 = new AppContext();
|
|
409
|
+
const user1 = db1.User.findById(1);
|
|
410
|
+
user1.name = 'Updated 1';
|
|
411
|
+
await user1.save(); // Only saves user1
|
|
412
|
+
|
|
413
|
+
const db2 = new AppContext();
|
|
414
|
+
const user2 = db2.User.findById(2);
|
|
415
|
+
user2.name = 'Updated 2';
|
|
416
|
+
await user2.save(); // Only saves user2
|
|
417
|
+
|
|
418
|
+
// Option 2: Save immediately
|
|
419
|
+
const db = new AppContext();
|
|
420
|
+
const user1 = db.User.findById(1);
|
|
421
|
+
user1.name = 'Updated 1';
|
|
422
|
+
await user1.save(); // Saves and clears tracking
|
|
423
|
+
|
|
424
|
+
const user2 = db.User.findById(2);
|
|
425
|
+
user2.name = 'Updated 2';
|
|
426
|
+
await user2.save(); // Only user2 tracked at this point
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### 2. Entity must have `__context`
|
|
430
|
+
|
|
431
|
+
```javascript
|
|
432
|
+
// ✅ WORKS: Entity loaded through MasterRecord
|
|
433
|
+
const user = db.User.findById(1);
|
|
434
|
+
await user.save(); // Has __context reference
|
|
435
|
+
|
|
436
|
+
// ❌ ERROR: Plain object
|
|
437
|
+
const user = { id: 1, name: 'Test' };
|
|
438
|
+
await user.save(); // Error: no __context
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### 3. Async/await required
|
|
442
|
+
|
|
443
|
+
```javascript
|
|
444
|
+
// ✅ GOOD
|
|
445
|
+
await user.save();
|
|
446
|
+
|
|
447
|
+
// ❌ BAD: Doesn't wait for save
|
|
448
|
+
user.save();
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Summary
|
|
454
|
+
|
|
455
|
+
**MasterRecord now supports Active Record style!** 🎉
|
|
456
|
+
|
|
457
|
+
```javascript
|
|
458
|
+
// Both patterns work:
|
|
459
|
+
|
|
460
|
+
// Active Record (NEW)
|
|
461
|
+
await entity.save();
|
|
462
|
+
|
|
463
|
+
// Entity Framework
|
|
464
|
+
await context.saveChanges();
|
|
465
|
+
|
|
466
|
+
// Choose what feels natural for your use case!
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**Solves the detached entity problem:**
|
|
470
|
+
- No need for manual `attach()`
|
|
471
|
+
- Entity knows its context
|
|
472
|
+
- Just call `.save()`!
|
|
473
|
+
|
|
474
|
+
**Best of both worlds:** ✨
|
|
475
|
+
- Active Record: Intuitive entity-level saves
|
|
476
|
+
- Entity Framework: Efficient batch operations
|
|
477
|
+
- You choose which to use!
|