masterrecord 0.3.7 → 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 +28 -5
- package/context.js +130 -1
- package/docs/ACTIVE_RECORD_PATTERN.md +477 -0
- package/docs/DETACHED_ENTITIES_GUIDE.md +445 -0
- package/docs/QUERY_CACHING_GUIDE.md +445 -0
- package/package.json +1 -1
- package/readme.md +191 -76
- package/test/attachDetached.test.js +303 -0
- package/test/optInCache.test.js +221 -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
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: Opt-In Caching Behavior
|
|
3
|
+
*
|
|
4
|
+
* Verifies that caching is disabled by default and only enabled with .cache()
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const QueryCache = require('../Cache/QueryCache');
|
|
8
|
+
|
|
9
|
+
console.log("╔════════════════════════════════════════════════════════════════╗");
|
|
10
|
+
console.log("║ Opt-In Caching Test ║");
|
|
11
|
+
console.log("╚════════════════════════════════════════════════════════════════╝\n");
|
|
12
|
+
|
|
13
|
+
let passed = 0;
|
|
14
|
+
let failed = 0;
|
|
15
|
+
|
|
16
|
+
// Simulate queryMethods with opt-in caching
|
|
17
|
+
class SimulatedQuery {
|
|
18
|
+
constructor(context) {
|
|
19
|
+
this._context = context;
|
|
20
|
+
this.__useCache = false; // Default: caching disabled
|
|
21
|
+
this._queryString = 'SELECT * FROM test';
|
|
22
|
+
this._tableName = 'Test';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Enable caching for this query
|
|
26
|
+
cache() {
|
|
27
|
+
this.__useCache = true;
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Execute query
|
|
32
|
+
toList() {
|
|
33
|
+
const cacheKey = this._context.cache.generateKey(this._queryString, [], this._tableName);
|
|
34
|
+
|
|
35
|
+
// Check cache if enabled
|
|
36
|
+
if (this.__useCache) {
|
|
37
|
+
const cached = this._context.cache.get(cacheKey);
|
|
38
|
+
if (cached) {
|
|
39
|
+
return cached;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Simulate DB query
|
|
44
|
+
const result = [{ id: 1, name: 'Test' }];
|
|
45
|
+
|
|
46
|
+
// Store in cache if enabled
|
|
47
|
+
if (this.__useCache) {
|
|
48
|
+
this._context.cache.set(cacheKey, result, this._tableName);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class SimulatedContext {
|
|
56
|
+
constructor() {
|
|
57
|
+
this.cache = new QueryCache({ ttl: 60000, maxSize: 100 });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
createQuery() {
|
|
61
|
+
return new SimulatedQuery(this);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Test 1: Default behavior - no caching
|
|
66
|
+
console.log("📝 Test 1: Queries without .cache() are NOT cached");
|
|
67
|
+
console.log("──────────────────────────────────────────────────");
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const ctx = new SimulatedContext();
|
|
71
|
+
ctx.cache.clear();
|
|
72
|
+
|
|
73
|
+
// Execute same query twice WITHOUT .cache()
|
|
74
|
+
const result1 = ctx.createQuery().toList();
|
|
75
|
+
const result2 = ctx.createQuery().toList();
|
|
76
|
+
|
|
77
|
+
const stats = ctx.cache.getStats();
|
|
78
|
+
|
|
79
|
+
if(stats.size === 0 && stats.hits === 0) {
|
|
80
|
+
console.log(" ✓ Queries without .cache() do not store results");
|
|
81
|
+
console.log(" ✓ No cache hits recorded");
|
|
82
|
+
console.log(" ✓ Cache size remains 0");
|
|
83
|
+
passed++;
|
|
84
|
+
} else {
|
|
85
|
+
console.log(` ✗ Queries were cached without .cache() call`);
|
|
86
|
+
console.log(` ✗ Cache size: ${stats.size}, hits: ${stats.hits}`);
|
|
87
|
+
failed++;
|
|
88
|
+
}
|
|
89
|
+
} catch(err) {
|
|
90
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
91
|
+
failed++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Test 2: Opt-in with .cache() enables caching
|
|
95
|
+
console.log("\n📝 Test 2: Queries with .cache() ARE cached");
|
|
96
|
+
console.log("──────────────────────────────────────────────────");
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const ctx = new SimulatedContext();
|
|
100
|
+
ctx.cache.clear();
|
|
101
|
+
|
|
102
|
+
// Execute same query twice WITH .cache()
|
|
103
|
+
const result1 = ctx.createQuery().cache().toList();
|
|
104
|
+
const result2 = ctx.createQuery().cache().toList();
|
|
105
|
+
|
|
106
|
+
const stats = ctx.cache.getStats();
|
|
107
|
+
|
|
108
|
+
if(stats.size === 1 && stats.hits === 1 && stats.misses === 1) {
|
|
109
|
+
console.log(" ✓ First query with .cache() stored result (miss)");
|
|
110
|
+
console.log(" ✓ Second query with .cache() hit cache (hit)");
|
|
111
|
+
console.log(` ✓ Cache stats: ${stats.hits} hit, ${stats.misses} miss`);
|
|
112
|
+
passed++;
|
|
113
|
+
} else {
|
|
114
|
+
console.log(` ✗ Caching with .cache() didn't work properly`);
|
|
115
|
+
console.log(` ✗ Expected: 1 hit, 1 miss. Got: ${stats.hits} hits, ${stats.misses} misses`);
|
|
116
|
+
failed++;
|
|
117
|
+
}
|
|
118
|
+
} catch(err) {
|
|
119
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
120
|
+
failed++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Test 3: Mixed queries - cached and non-cached
|
|
124
|
+
console.log("\n📝 Test 3: Mixed cached and non-cached queries");
|
|
125
|
+
console.log("──────────────────────────────────────────────────");
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const ctx = new SimulatedContext();
|
|
129
|
+
ctx.cache.clear();
|
|
130
|
+
|
|
131
|
+
// Non-cached query (no .cache())
|
|
132
|
+
ctx.createQuery().toList();
|
|
133
|
+
ctx.createQuery().toList();
|
|
134
|
+
|
|
135
|
+
// Cached query (with .cache())
|
|
136
|
+
ctx.createQuery().cache().toList();
|
|
137
|
+
ctx.createQuery().cache().toList();
|
|
138
|
+
|
|
139
|
+
const stats = ctx.cache.getStats();
|
|
140
|
+
|
|
141
|
+
if(stats.size === 1 && stats.hits === 1 && stats.misses === 1) {
|
|
142
|
+
console.log(" ✓ Non-cached queries didn't affect cache");
|
|
143
|
+
console.log(" ✓ Cached queries stored and retrieved correctly");
|
|
144
|
+
console.log(` ✓ Cache contains only .cache() queries: ${stats.size} entry`);
|
|
145
|
+
passed++;
|
|
146
|
+
} else {
|
|
147
|
+
console.log(` ✗ Mixed query handling incorrect`);
|
|
148
|
+
console.log(` ✗ Cache size: ${stats.size}, expected: 1`);
|
|
149
|
+
failed++;
|
|
150
|
+
}
|
|
151
|
+
} catch(err) {
|
|
152
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
153
|
+
failed++;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Test 4: Default __useCache flag is false
|
|
157
|
+
console.log("\n📝 Test 4: Default __useCache flag is false");
|
|
158
|
+
console.log("──────────────────────────────────────────────────");
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const ctx = new SimulatedContext();
|
|
162
|
+
const query = ctx.createQuery();
|
|
163
|
+
|
|
164
|
+
if(query.__useCache === false) {
|
|
165
|
+
console.log(" ✓ Default __useCache is false");
|
|
166
|
+
console.log(" ✓ Caching is opt-in by default");
|
|
167
|
+
passed++;
|
|
168
|
+
} else {
|
|
169
|
+
console.log(` ✗ Default __useCache is ${query.__useCache}, expected false`);
|
|
170
|
+
failed++;
|
|
171
|
+
}
|
|
172
|
+
} catch(err) {
|
|
173
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
174
|
+
failed++;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Test 5: .cache() sets __useCache to true
|
|
178
|
+
console.log("\n📝 Test 5: .cache() enables caching flag");
|
|
179
|
+
console.log("──────────────────────────────────────────────────");
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const ctx = new SimulatedContext();
|
|
183
|
+
const query = ctx.createQuery();
|
|
184
|
+
|
|
185
|
+
const beforeCache = query.__useCache;
|
|
186
|
+
query.cache();
|
|
187
|
+
const afterCache = query.__useCache;
|
|
188
|
+
|
|
189
|
+
if(beforeCache === false && afterCache === true) {
|
|
190
|
+
console.log(" ✓ __useCache starts as false");
|
|
191
|
+
console.log(" ✓ .cache() sets __useCache to true");
|
|
192
|
+
console.log(" ✓ Caching is explicitly enabled");
|
|
193
|
+
passed++;
|
|
194
|
+
} else {
|
|
195
|
+
console.log(` ✗ Flag transition incorrect`);
|
|
196
|
+
console.log(` ✗ Before: ${beforeCache}, After: ${afterCache}`);
|
|
197
|
+
failed++;
|
|
198
|
+
}
|
|
199
|
+
} catch(err) {
|
|
200
|
+
console.log(` ✗ Error: ${err.message}`);
|
|
201
|
+
failed++;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Summary
|
|
205
|
+
console.log("\n╔════════════════════════════════════════════════════════════════╗");
|
|
206
|
+
console.log("║ Test Summary ║");
|
|
207
|
+
console.log("╚════════════════════════════════════════════════════════════════╝");
|
|
208
|
+
console.log(`\n ✓ Passed: ${passed}`);
|
|
209
|
+
console.log(` ✗ Failed: ${failed}`);
|
|
210
|
+
console.log(` 📊 Total: ${passed + failed}\n`);
|
|
211
|
+
|
|
212
|
+
if(failed === 0) {
|
|
213
|
+
console.log(" 🎉 All tests passed!\n");
|
|
214
|
+
console.log(" ✅ Opt-in caching behavior verified");
|
|
215
|
+
console.log(" ✅ Default is safe (no caching)");
|
|
216
|
+
console.log(" ✅ .cache() explicitly enables caching\n");
|
|
217
|
+
process.exit(0);
|
|
218
|
+
} else {
|
|
219
|
+
console.log(" ❌ Some tests failed\n");
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|