masterrecord 0.3.8 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,445 @@
1
+ # Detached Entity Problem - Solutions
2
+
3
+ ## The Problem
4
+
5
+ ```javascript
6
+ // Controller loads task
7
+ const task = await this._taskService.getTask(taskId);
8
+
9
+ // Service receives task and modifies it
10
+ task.status = 'completed';
11
+
12
+ // Try to save - BUT IT DOESN'T WORK! ❌
13
+ this._qaContext.saveChanges(); // Task change NOT saved
14
+
15
+ // Why? The task is "detached" - not tracked by _qaContext
16
+ ```
17
+
18
+ **Root Cause:** The task was loaded in a different context (`taskService`) and is now **detached** from the current context (`_qaContext`). MasterRecord's change tracking doesn't see the modification.
19
+
20
+ ---
21
+
22
+ ## How Other ORMs Solve This
23
+
24
+ ### Entity Framework (.NET)
25
+ ```csharp
26
+ // Solution 1: Attach
27
+ context.Attach(task);
28
+ context.Entry(task).State = EntityState.Modified;
29
+ context.SaveChanges();
30
+
31
+ // Solution 2: Update (simpler)
32
+ context.Update(task);
33
+ context.SaveChanges();
34
+ ```
35
+
36
+ ### Hibernate (Java)
37
+ ```java
38
+ // Solution: Merge
39
+ session.merge(task);
40
+ session.flush();
41
+ ```
42
+
43
+ ### Active Record (Rails)
44
+ ```ruby
45
+ # No problem - entities have .save()
46
+ task.status = 'completed'
47
+ task.save
48
+ ```
49
+
50
+ ### Sequelize (Node.js)
51
+ ```javascript
52
+ // No problem - entities have .save()
53
+ task.status = 'completed';
54
+ await task.save();
55
+ ```
56
+
57
+ ---
58
+
59
+ ## MasterRecord Solutions
60
+
61
+ ### Solution 1: **attach()** Method (Recommended)
62
+
63
+ Like Entity Framework's `Update()`:
64
+
65
+ ```javascript
66
+ // Your original code (BROKEN)
67
+ const task = await this._taskService.getTask(taskId);
68
+ task.status = 'completed';
69
+ this._qaContext.saveChanges(); // ❌ Doesn't work
70
+
71
+ // FIX: Attach the detached entity
72
+ const task = await this._taskService.getTask(taskId);
73
+ task.status = 'completed';
74
+ this._qaContext.attach(task); // ✅ Mark as modified
75
+ await this._qaContext.saveChanges(); // ✅ Now it works!
76
+ ```
77
+
78
+ ### Solution 2: **Specific Field Changes**
79
+
80
+ Only mark specific fields as dirty:
81
+
82
+ ```javascript
83
+ const task = await this._taskService.getTask(taskId);
84
+
85
+ // Attach with specific changes
86
+ this._qaContext.attach(task, {
87
+ status: 'completed',
88
+ completed_at: new Date()
89
+ });
90
+
91
+ await this._qaContext.saveChanges();
92
+ ```
93
+
94
+ ### Solution 3: **attachAll()** for Multiple Entities
95
+
96
+ ```javascript
97
+ const tasks = await this._taskService.getTasks();
98
+
99
+ // Modify all tasks
100
+ tasks.forEach(task => {
101
+ task.status = 'completed';
102
+ });
103
+
104
+ // Attach all at once
105
+ this._qaContext.attachAll(tasks);
106
+ await this._qaContext.saveChanges();
107
+ ```
108
+
109
+ ### Solution 4: **update()** Helper
110
+
111
+ Update by primary key without loading first:
112
+
113
+ ```javascript
114
+ // No need to load task first
115
+ await this._qaContext.update('Task', taskId, {
116
+ status: 'completed',
117
+ completed_at: new Date()
118
+ });
119
+
120
+ await this._qaContext.saveChanges();
121
+ ```
122
+
123
+ ---
124
+
125
+ ## Complete Example: Your Annotation Service
126
+
127
+ ### Before (BROKEN) ❌
128
+
129
+ ```javascript
130
+ class AnnotationService {
131
+ constructor() {
132
+ this._qaContext = new QAContext();
133
+ this._taskService = new TaskService();
134
+ }
135
+
136
+ async createAnnotation(taskId, data) {
137
+ // Load task from different service
138
+ const task = await this._taskService.getTask(taskId);
139
+
140
+ // Create annotation
141
+ const annotation = this._qaContext.Annotation.new();
142
+ annotation.task_id = taskId;
143
+ annotation.data = data;
144
+ this._qaContext.Annotation.add(annotation);
145
+
146
+ // Modify task - BUT NOT TRACKED! ❌
147
+ task.status = 'completed';
148
+
149
+ // Only annotation is saved, task change is ignored ❌
150
+ await this._qaContext.saveChanges();
151
+
152
+ return annotation;
153
+ }
154
+ }
155
+ ```
156
+
157
+ ### After (FIXED) ✅
158
+
159
+ **Option A: Use attach()**
160
+
161
+ ```javascript
162
+ class AnnotationService {
163
+ constructor() {
164
+ this._qaContext = new QAContext();
165
+ this._taskService = new TaskService();
166
+ }
167
+
168
+ async createAnnotation(taskId, data) {
169
+ // Load task from different service
170
+ const task = await this._taskService.getTask(taskId);
171
+
172
+ // Create annotation
173
+ const annotation = this._qaContext.Annotation.new();
174
+ annotation.task_id = taskId;
175
+ annotation.data = data;
176
+ this._qaContext.Annotation.add(annotation);
177
+
178
+ // Modify task
179
+ task.status = 'completed';
180
+
181
+ // FIX: Attach detached task ✅
182
+ this._qaContext.attach(task);
183
+
184
+ // Both annotation and task are saved ✅
185
+ await this._qaContext.saveChanges();
186
+
187
+ return annotation;
188
+ }
189
+ }
190
+ ```
191
+
192
+ **Option B: Use attach() with specific fields**
193
+
194
+ ```javascript
195
+ async createAnnotation(taskId, data) {
196
+ const task = await this._taskService.getTask(taskId);
197
+
198
+ // Create annotation
199
+ const annotation = this._qaContext.Annotation.new();
200
+ annotation.task_id = taskId;
201
+ annotation.data = data;
202
+ this._qaContext.Annotation.add(annotation);
203
+
204
+ // Attach with specific changes ✅
205
+ this._qaContext.attach(task, {
206
+ status: 'completed',
207
+ completed_at: new Date()
208
+ });
209
+
210
+ await this._qaContext.saveChanges();
211
+ return annotation;
212
+ }
213
+ ```
214
+
215
+ **Option C: Use update() helper**
216
+
217
+ ```javascript
218
+ async createAnnotation(taskId, data) {
219
+ // No need to load task first
220
+
221
+ // Create annotation
222
+ const annotation = this._qaContext.Annotation.new();
223
+ annotation.task_id = taskId;
224
+ annotation.data = data;
225
+ this._qaContext.Annotation.add(annotation);
226
+
227
+ // Update task by ID ✅
228
+ await this._qaContext.update('Task', taskId, {
229
+ status: 'completed',
230
+ completed_at: new Date()
231
+ });
232
+
233
+ await this._qaContext.saveChanges();
234
+ return annotation;
235
+ }
236
+ ```
237
+
238
+ ---
239
+
240
+ ## Best Practices
241
+
242
+ ### ✅ DO: Use One Context Per Request
243
+
244
+ ```javascript
245
+ // Express middleware
246
+ app.use((req, res, next) => {
247
+ req.db = new AppContext(); // One context per request
248
+ res.on('finish', () => req.db.endRequest());
249
+ next();
250
+ });
251
+
252
+ // Service uses the request context
253
+ class AnnotationService {
254
+ async createAnnotation(db, taskId, data) {
255
+ // Use passed-in context
256
+ const task = db.Task.findById(taskId); // Loaded in same context ✅
257
+ task.status = 'completed'; // Already tracked ✅
258
+
259
+ const annotation = db.Annotation.new();
260
+ annotation.task_id = taskId;
261
+ db.Annotation.add(annotation);
262
+
263
+ await db.saveChanges(); // Both changes saved ✅
264
+ return annotation;
265
+ }
266
+ }
267
+
268
+ // Controller
269
+ app.post('/annotations', async (req, res) => {
270
+ const annotationService = new AnnotationService();
271
+ const annotation = await annotationService.createAnnotation(
272
+ req.db, // Pass request context ✅
273
+ req.body.taskId,
274
+ req.body.data
275
+ );
276
+ res.json(annotation);
277
+ });
278
+ ```
279
+
280
+ ### ✅ DO: Load in Same Context
281
+
282
+ ```javascript
283
+ // ❌ BAD: Different contexts
284
+ const taskService = new TaskService(); // Has own context
285
+ const qaContext = new QAContext(); // Different context
286
+ const task = await taskService.getTask(taskId); // Loaded in taskService context
287
+ task.status = 'completed';
288
+ qaContext.saveChanges(); // Doesn't see change ❌
289
+
290
+ // ✅ GOOD: Same context
291
+ const db = new AppContext();
292
+ const task = db.Task.findById(taskId); // Loaded in db context
293
+ task.status = 'completed'; // Already tracked
294
+ await db.saveChanges(); // Change saved ✅
295
+ ```
296
+
297
+ ### ✅ DO: Use attach() for Detached Entities
298
+
299
+ ```javascript
300
+ // When you must use entities from different contexts
301
+ const task = await externalService.getTask(taskId); // Detached
302
+ task.status = 'completed';
303
+ db.attach(task); // Re-attach ✅
304
+ await db.saveChanges();
305
+ ```
306
+
307
+ ### ❌ DON'T: Modify Detached Entities Without Attaching
308
+
309
+ ```javascript
310
+ // ❌ BAD
311
+ const task = await someService.getTask(taskId);
312
+ task.status = 'completed';
313
+ db.saveChanges(); // Won't work!
314
+
315
+ // ✅ GOOD
316
+ const task = await someService.getTask(taskId);
317
+ task.status = 'completed';
318
+ db.attach(task); // Attach first!
319
+ await db.saveChanges();
320
+ ```
321
+
322
+ ---
323
+
324
+ ## Comparison Table
325
+
326
+ | ORM | Method | Example |
327
+ |-----|--------|---------|
328
+ | **Entity Framework** | `context.Update(entity)` | `context.Update(task);` |
329
+ | **Hibernate** | `session.merge(entity)` | `session.merge(task);` |
330
+ | **Active Record** | `entity.save()` | `task.save` |
331
+ | **Sequelize** | `entity.save()` | `await task.save()` |
332
+ | **MasterRecord** | `context.attach(entity)` | `db.attach(task);` |
333
+
334
+ ---
335
+
336
+ ## API Reference
337
+
338
+ ### attach(entity, changes?)
339
+
340
+ Attach a detached entity and mark as modified.
341
+
342
+ **Parameters:**
343
+ - `entity` - The detached entity
344
+ - `changes` (optional) - Object with specific field changes
345
+
346
+ **Returns:** The attached entity
347
+
348
+ **Example:**
349
+ ```javascript
350
+ // Attach entire entity
351
+ db.attach(task);
352
+
353
+ // Attach with specific changes
354
+ db.attach(task, { status: 'completed' });
355
+ ```
356
+
357
+ ### attachAll(entities)
358
+
359
+ Attach multiple entities at once.
360
+
361
+ **Parameters:**
362
+ - `entities` - Array of detached entities
363
+
364
+ **Returns:** Array of attached entities
365
+
366
+ **Example:**
367
+ ```javascript
368
+ const tasks = await getTasks();
369
+ tasks.forEach(t => t.status = 'completed');
370
+ db.attachAll(tasks);
371
+ await db.saveChanges();
372
+ ```
373
+
374
+ ### update(entityName, primaryKey, changes)
375
+
376
+ Update entity by primary key without loading.
377
+
378
+ **Parameters:**
379
+ - `entityName` - Name of entity (e.g., 'Task')
380
+ - `primaryKey` - Primary key value
381
+ - `changes` - Object with field changes
382
+
383
+ **Returns:** The attached entity
384
+
385
+ **Example:**
386
+ ```javascript
387
+ await db.update('Task', taskId, {
388
+ status: 'completed',
389
+ completed_at: new Date()
390
+ });
391
+ await db.saveChanges();
392
+ ```
393
+
394
+ ---
395
+
396
+ ## Troubleshooting
397
+
398
+ ### Changes not saving?
399
+
400
+ **Check:**
401
+ 1. Is entity tracked? `console.log(db.__trackedEntities.includes(entity))`
402
+ 2. Is entity state correct? `console.log(entity.__state)` (should be "modified")
403
+ 3. Are dirty fields marked? `console.log(entity.__dirtyFields)`
404
+
405
+ **Fix:**
406
+ ```javascript
407
+ // If not tracked, attach it
408
+ if (!db.__trackedEntities.includes(entity)) {
409
+ db.attach(entity);
410
+ }
411
+ ```
412
+
413
+ ### "Entity must have __entity metadata" error?
414
+
415
+ **Cause:** Entity wasn't loaded through MasterRecord
416
+
417
+ **Fix:**
418
+ ```javascript
419
+ // ❌ BAD: Plain object
420
+ const task = { id: 1, status: 'completed' };
421
+ db.attach(task); // Error!
422
+
423
+ // ✅ GOOD: Load through MasterRecord
424
+ const task = db.Task.findById(1);
425
+ task.status = 'completed';
426
+ db.attach(task); // Works!
427
+ ```
428
+
429
+ ---
430
+
431
+ ## Summary
432
+
433
+ **The detached entity problem** happens when:
434
+ 1. Entity loaded in one context
435
+ 2. Passed to different context/service
436
+ 3. Modified
437
+ 4. saveChanges() called but doesn't see modification
438
+
439
+ **Solutions:**
440
+ 1. ✅ Use **`attach()`** to re-attach detached entities (like Entity Framework)
441
+ 2. ✅ Use **same context** throughout request (best practice)
442
+ 3. ✅ Pass context to services instead of creating multiple contexts
443
+ 4. ✅ Use **`update()`** helper for simple updates
444
+
445
+ **MasterRecord now handles this like Entity Framework!** 🎉
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "An Object-relational mapping for the Master framework. Master Record connects classes to relational database tables to establish a database with almost zero-configuration ",
5
5
  "main": "MasterRecord.js",
6
6
  "bin": {
package/readme.md CHANGED
@@ -68,6 +68,30 @@ MasterRecord includes the following database drivers by default:
68
68
  - `sync-mysql2@^1.0.8` - MySQL
69
69
  - `better-sqlite3@^12.6.0` - SQLite
70
70
 
71
+ ## Two Patterns: Entity Framework & Active Record
72
+
73
+ MasterRecord supports **both** ORM patterns - choose what feels natural:
74
+
75
+ ### Active Record Style (Recommended for beginners)
76
+ ```javascript
77
+ // Entity saves itself
78
+ const user = db.User.findById(1);
79
+ user.name = 'Updated';
80
+ await user.save(); // ✅ Entity knows how to save
81
+ ```
82
+
83
+ ### Entity Framework Style (Efficient for batch operations)
84
+ ```javascript
85
+ // Context saves all tracked entities
86
+ const user = db.User.findById(1);
87
+ user.name = 'Updated';
88
+ await db.saveChanges(); // ✅ Batch save
89
+ ```
90
+
91
+ **Read more:** [Active Record Pattern Guide](./ACTIVE_RECORD_PATTERN.md) | [Detached Entities Guide](./DETACHED_ENTITIES_GUIDE.md)
92
+
93
+ ---
94
+
71
95
  ## Quick Start
72
96
 
73
97
  ### 1. Create a Context
@@ -137,21 +161,21 @@ masterrecord migrate AppContext
137
161
  const AppContext = require('./app/models/context');
138
162
  const db = new AppContext();
139
163
 
140
- // Create
164
+ // Create (Active Record style)
141
165
  const user = db.User.new();
142
166
  user.name = 'Alice';
143
167
  user.email = 'alice@example.com';
144
168
  user.age = 28;
145
- await db.saveChanges();
169
+ await user.save(); // Entity saves itself!
146
170
 
147
171
  // Read with parameterized query
148
172
  const alice = db.User
149
173
  .where(u => u.email == $$, 'alice@example.com')
150
174
  .single();
151
175
 
152
- // Update
176
+ // Update (Active Record style)
153
177
  alice.age = 29;
154
- await db.saveChanges();
178
+ await alice.save(); // Entity saves itself!
155
179
 
156
180
  // Delete
157
181
  db.remove(alice);
@@ -1172,6 +1196,12 @@ context.saveChanges() // MySQL/SQLite (sync)
1172
1196
  context.EntityName.add(entity)
1173
1197
  context.remove(entity)
1174
1198
 
1199
+ // Attach detached entities (like Entity Framework's Update())
1200
+ context.attach(entity) // Attach and mark as modified
1201
+ context.attach(entity, { field: value }) // Attach with specific changes
1202
+ context.attachAll([entity1, entity2]) // Attach multiple entities
1203
+ await context.update('Entity', id, changes) // Update by primary key
1204
+
1175
1205
  // Cache management
1176
1206
  context.getCacheStats() // Get cache statistics
1177
1207
  context.clearQueryCache() // Clear all cached queries
@@ -1203,6 +1233,9 @@ context.setQueryCacheEnabled(bool) // Enable/disable caching
1203
1233
  // Convenience methods
1204
1234
  .findById(id) // Find by primary key
1205
1235
  .new() // Create new entity instance
1236
+
1237
+ // Entity methods (Active Record style)
1238
+ await entity.save() // Save this entity (and all tracked changes)
1206
1239
  ```
1207
1240
 
1208
1241
  ### Migration Methods