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.
- package/Entity/entityTrackerModel.js +17 -3
- package/QueryLanguage/queryMethods.js +15 -0
- package/context.js +111 -0
- package/docs/ACTIVE_RECORD_PATTERN.md +477 -0
- package/docs/DETACHED_ENTITIES_GUIDE.md +445 -0
- package/package.json +1 -1
- package/readme.md +37 -4
- package/test/attachDetached.test.js +303 -0
- /package/{QUERY_CACHING_GUIDE.md → docs/QUERY_CACHING_GUIDE.md} +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: Attach Detached Entities
|
|
3
|
+
*
|
|
4
|
+
* Verifies that detached entities can be re-attached and tracked
|
|
5
|
+
* Like Entity Framework's context.Update() or Hibernate's session.merge()
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
console.log("╔════════════════════════════════════════════════════════════════╗");
|
|
9
|
+
console.log("║ Detached Entity Attachment Test ║");
|
|
10
|
+
console.log("╚════════════════════════════════════════════════════════════════╝\n");
|
|
11
|
+
|
|
12
|
+
let passed = 0;
|
|
13
|
+
let failed = 0;
|
|
14
|
+
|
|
15
|
+
// Simulate a context with attach functionality
|
|
16
|
+
class SimulatedContext {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.__trackedEntities = [];
|
|
19
|
+
this.__trackedEntitiesMap = new Map();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
__track(model) {
|
|
23
|
+
if (!model.__ID) {
|
|
24
|
+
model.__ID = Math.floor((Math.random() * 100000) + 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!this.__trackedEntitiesMap.has(model.__ID)) {
|
|
28
|
+
this.__trackedEntities.push(model);
|
|
29
|
+
this.__trackedEntitiesMap.set(model.__ID, model);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return model;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
attach(entity, changes = null) {
|
|
36
|
+
if (!entity) {
|
|
37
|
+
throw new Error('Cannot attach null or undefined entity');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!entity.__entity || !entity.__entity.__name) {
|
|
41
|
+
throw new Error('Entity must have __entity metadata');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Mark entity as modified
|
|
45
|
+
entity.__state = 'modified';
|
|
46
|
+
|
|
47
|
+
// If specific changes provided, mark only those fields as dirty
|
|
48
|
+
if (changes) {
|
|
49
|
+
entity.__dirtyFields = entity.__dirtyFields || [];
|
|
50
|
+
for (const fieldName in changes) {
|
|
51
|
+
entity[fieldName] = changes[fieldName];
|
|
52
|
+
if (!entity.__dirtyFields.includes(fieldName)) {
|
|
53
|
+
entity.__dirtyFields.push(fieldName);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
// Mark all fields as potentially modified
|
|
58
|
+
entity.__dirtyFields = entity.__dirtyFields || [];
|
|
59
|
+
|
|
60
|
+
if (entity.__dirtyFields.length === 0) {
|
|
61
|
+
for (const fieldName in entity.__entity) {
|
|
62
|
+
if (!fieldName.startsWith('__') &&
|
|
63
|
+
entity.__entity[fieldName].type !== 'hasMany' &&
|
|
64
|
+
entity.__entity[fieldName].type !== 'hasOne') {
|
|
65
|
+
entity.__dirtyFields.push(fieldName);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
entity.__context = this;
|
|
72
|
+
this.__track(entity);
|
|
73
|
+
return entity;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
attachAll(entities) {
|
|
77
|
+
if (!Array.isArray(entities)) {
|
|
78
|
+
throw new Error('attachAll() requires an array');
|
|
79
|
+
}
|
|
80
|
+
return entities.map(entity => this.attach(entity));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Create mock entity
|
|
85
|
+
function createMockEntity(id, name, status) {
|
|
86
|
+
return {
|
|
87
|
+
__ID: id,
|
|
88
|
+
__entity: {
|
|
89
|
+
__name: 'Task',
|
|
90
|
+
id: { type: 'integer', primary: true },
|
|
91
|
+
name: { type: 'string' },
|
|
92
|
+
status: { type: 'string' }
|
|
93
|
+
},
|
|
94
|
+
__dirtyFields: [],
|
|
95
|
+
__state: 'track',
|
|
96
|
+
id: id,
|
|
97
|
+
name: name,
|
|
98
|
+
status: status
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Test 1: Attach detached entity
|
|
103
|
+
console.log("📝 Test 1: Attach detached entity");
|
|
104
|
+
console.log("──────────────────────────────────────────────────");
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const ctx = new SimulatedContext();
|
|
108
|
+
const task = createMockEntity(1, 'Task 1', 'pending');
|
|
109
|
+
|
|
110
|
+
// Simulate: entity loaded in different context (detached)
|
|
111
|
+
task.status = 'completed';
|
|
112
|
+
|
|
113
|
+
// Attach to current context
|
|
114
|
+
ctx.attach(task);
|
|
115
|
+
|
|
116
|
+
if (ctx.__trackedEntities.includes(task) &&
|
|
117
|
+
task.__state === 'modified' &&
|
|
118
|
+
task.__dirtyFields.length > 0) {
|
|
119
|
+
console.log(" ✓ Entity attached to context");
|
|
120
|
+
console.log(" ✓ Entity marked as 'modified'");
|
|
121
|
+
console.log(` ✓ Dirty fields marked: ${task.__dirtyFields.join(', ')}`);
|
|
122
|
+
passed++;
|
|
123
|
+
} else {
|
|
124
|
+
console.log(` ✗ Entity not properly attached`);
|
|
125
|
+
failed++;
|
|
126
|
+
}
|
|
127
|
+
} catch(err) {
|
|
128
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
129
|
+
failed++;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Test 2: Attach with specific field changes
|
|
133
|
+
console.log("\n📝 Test 2: Attach with specific field changes");
|
|
134
|
+
console.log("──────────────────────────────────────────────────");
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const ctx = new SimulatedContext();
|
|
138
|
+
const task = createMockEntity(2, 'Task 2', 'pending');
|
|
139
|
+
|
|
140
|
+
// Attach with specific changes
|
|
141
|
+
ctx.attach(task, {
|
|
142
|
+
status: 'completed',
|
|
143
|
+
completed_at: new Date()
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (task.status === 'completed' &&
|
|
147
|
+
task.__dirtyFields.includes('status') &&
|
|
148
|
+
task.__dirtyFields.includes('completed_at') &&
|
|
149
|
+
task.__state === 'modified') {
|
|
150
|
+
console.log(" ✓ Specific fields applied");
|
|
151
|
+
console.log(" ✓ Only specified fields marked dirty");
|
|
152
|
+
console.log(` ✓ Dirty fields: ${task.__dirtyFields.join(', ')}`);
|
|
153
|
+
passed++;
|
|
154
|
+
} else {
|
|
155
|
+
console.log(` ✗ Specific changes not applied correctly`);
|
|
156
|
+
failed++;
|
|
157
|
+
}
|
|
158
|
+
} catch(err) {
|
|
159
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
160
|
+
failed++;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Test 3: attachAll() multiple entities
|
|
164
|
+
console.log("\n📝 Test 3: Attach multiple entities");
|
|
165
|
+
console.log("──────────────────────────────────────────────────");
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const ctx = new SimulatedContext();
|
|
169
|
+
const tasks = [
|
|
170
|
+
createMockEntity(3, 'Task 3', 'pending'),
|
|
171
|
+
createMockEntity(4, 'Task 4', 'pending'),
|
|
172
|
+
createMockEntity(5, 'Task 5', 'pending')
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
// Modify all
|
|
176
|
+
tasks.forEach(t => t.status = 'completed');
|
|
177
|
+
|
|
178
|
+
// Attach all
|
|
179
|
+
ctx.attachAll(tasks);
|
|
180
|
+
|
|
181
|
+
const allAttached = tasks.every(t =>
|
|
182
|
+
ctx.__trackedEntities.includes(t) &&
|
|
183
|
+
t.__state === 'modified'
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
if (allAttached && ctx.__trackedEntities.length === 3) {
|
|
187
|
+
console.log(" ✓ All entities attached");
|
|
188
|
+
console.log(` ✓ Tracked count: ${ctx.__trackedEntities.length}`);
|
|
189
|
+
console.log(" ✓ All marked as modified");
|
|
190
|
+
passed++;
|
|
191
|
+
} else {
|
|
192
|
+
console.log(` ✗ Not all entities attached correctly`);
|
|
193
|
+
failed++;
|
|
194
|
+
}
|
|
195
|
+
} catch(err) {
|
|
196
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
197
|
+
failed++;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Test 4: Attach throws error for invalid entity
|
|
201
|
+
console.log("\n📝 Test 4: Error handling for invalid entities");
|
|
202
|
+
console.log("──────────────────────────────────────────────────");
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const ctx = new SimulatedContext();
|
|
206
|
+
|
|
207
|
+
let error1 = null;
|
|
208
|
+
let error2 = null;
|
|
209
|
+
|
|
210
|
+
// Test null
|
|
211
|
+
try {
|
|
212
|
+
ctx.attach(null);
|
|
213
|
+
} catch(e) {
|
|
214
|
+
error1 = e.message;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Test entity without metadata
|
|
218
|
+
try {
|
|
219
|
+
ctx.attach({ id: 1, name: 'Test' });
|
|
220
|
+
} catch(e) {
|
|
221
|
+
error2 = e.message;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (error1 && error2) {
|
|
225
|
+
console.log(" ✓ Null entity rejected");
|
|
226
|
+
console.log(" ✓ Entity without metadata rejected");
|
|
227
|
+
console.log(` ✓ Error messages provided`);
|
|
228
|
+
passed++;
|
|
229
|
+
} else {
|
|
230
|
+
console.log(` ✗ Invalid entities should throw errors`);
|
|
231
|
+
failed++;
|
|
232
|
+
}
|
|
233
|
+
} catch(err) {
|
|
234
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
235
|
+
failed++;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Test 5: Attach doesn't duplicate entities
|
|
239
|
+
console.log("\n📝 Test 5: No duplicate tracking");
|
|
240
|
+
console.log("──────────────────────────────────────────────────");
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const ctx = new SimulatedContext();
|
|
244
|
+
const task = createMockEntity(6, 'Task 6', 'pending');
|
|
245
|
+
|
|
246
|
+
// Attach twice
|
|
247
|
+
ctx.attach(task);
|
|
248
|
+
ctx.attach(task);
|
|
249
|
+
|
|
250
|
+
if (ctx.__trackedEntities.length === 1) {
|
|
251
|
+
console.log(" ✓ Entity not duplicated in tracking");
|
|
252
|
+
console.log(` ✓ Tracked count: ${ctx.__trackedEntities.length}`);
|
|
253
|
+
passed++;
|
|
254
|
+
} else {
|
|
255
|
+
console.log(` ✗ Entity duplicated: ${ctx.__trackedEntities.length} entries`);
|
|
256
|
+
failed++;
|
|
257
|
+
}
|
|
258
|
+
} catch(err) {
|
|
259
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
260
|
+
failed++;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Test 6: Attach preserves entity reference
|
|
264
|
+
console.log("\n📝 Test 6: Entity reference preserved");
|
|
265
|
+
console.log("──────────────────────────────────────────────────");
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const ctx = new SimulatedContext();
|
|
269
|
+
const task = createMockEntity(7, 'Task 7', 'pending');
|
|
270
|
+
|
|
271
|
+
const returned = ctx.attach(task);
|
|
272
|
+
|
|
273
|
+
if (returned === task) {
|
|
274
|
+
console.log(" ✓ Same entity reference returned");
|
|
275
|
+
console.log(" ✓ No entity cloning");
|
|
276
|
+
passed++;
|
|
277
|
+
} else {
|
|
278
|
+
console.log(` ✗ Different entity reference returned`);
|
|
279
|
+
failed++;
|
|
280
|
+
}
|
|
281
|
+
} catch(err) {
|
|
282
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
283
|
+
failed++;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Summary
|
|
287
|
+
console.log("\n╔════════════════════════════════════════════════════════════════╗");
|
|
288
|
+
console.log("║ Test Summary ║");
|
|
289
|
+
console.log("╚════════════════════════════════════════════════════════════════╝");
|
|
290
|
+
console.log(`\n ✓ Passed: ${passed}`);
|
|
291
|
+
console.log(` ✗ Failed: ${failed}`);
|
|
292
|
+
console.log(` 📊 Total: ${passed + failed}\n`);
|
|
293
|
+
|
|
294
|
+
if(failed === 0) {
|
|
295
|
+
console.log(" 🎉 All tests passed!\n");
|
|
296
|
+
console.log(" ✅ Detached entity attachment works");
|
|
297
|
+
console.log(" ✅ Like Entity Framework's context.Update()");
|
|
298
|
+
console.log(" ✅ Like Hibernate's session.merge()\n");
|
|
299
|
+
process.exit(0);
|
|
300
|
+
} else {
|
|
301
|
+
console.log(" ❌ Some tests failed\n");
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
File without changes
|