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.
@@ -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
+ }