masterrecord 0.3.5 → 0.3.6
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/Cache/QueryCache.js +175 -0
- package/Cache/RedisQueryCache.js +155 -0
- package/QueryLanguage/queryMethods.js +72 -25
- package/context.js +43 -0
- package/package.json +1 -1
- package/readme.md +262 -4
- package/test/cacheIntegration.test.js +319 -0
- package/test/queryCache.test.js +148 -0
- package/examples/jsonArrayTransformer.js +0 -215
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const QueryCache = require('../Cache/QueryCache');
|
|
3
|
+
|
|
4
|
+
describe('QueryCache', () => {
|
|
5
|
+
let cache;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
cache = new QueryCache({ ttl: 1000, maxSize: 3 });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('cache miss returns null', () => {
|
|
12
|
+
const key = cache.generateKey('SELECT * FROM users', [], 'users');
|
|
13
|
+
assert.strictEqual(cache.get(key), null);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('cache hit returns stored data', () => {
|
|
17
|
+
const key = cache.generateKey('SELECT * FROM users', [], 'users');
|
|
18
|
+
const data = [{ id: 1, name: 'John' }];
|
|
19
|
+
|
|
20
|
+
cache.set(key, data, 'users');
|
|
21
|
+
const cached = cache.get(key);
|
|
22
|
+
|
|
23
|
+
assert.deepStrictEqual(cached, data);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('TTL expiration removes entries', (done) => {
|
|
27
|
+
const key = cache.generateKey('SELECT * FROM users', [], 'users');
|
|
28
|
+
cache.set(key, { id: 1 }, 'users');
|
|
29
|
+
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
assert.strictEqual(cache.get(key), null);
|
|
32
|
+
done();
|
|
33
|
+
}, 1100); // Wait for TTL expiration
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('LRU eviction removes oldest entry', () => {
|
|
37
|
+
cache.set('key1', 'data1', 'table1');
|
|
38
|
+
cache.set('key2', 'data2', 'table2');
|
|
39
|
+
cache.set('key3', 'data3', 'table3');
|
|
40
|
+
|
|
41
|
+
// Access key1 to make it recently used
|
|
42
|
+
cache.get('key1');
|
|
43
|
+
|
|
44
|
+
// Add key4 - should evict key2 (least recently used)
|
|
45
|
+
cache.set('key4', 'data4', 'table4');
|
|
46
|
+
|
|
47
|
+
assert.strictEqual(cache.get('key1'), 'data1'); // Still in cache
|
|
48
|
+
assert.strictEqual(cache.get('key2'), null); // Evicted
|
|
49
|
+
assert.strictEqual(cache.get('key3'), 'data3'); // Still in cache
|
|
50
|
+
assert.strictEqual(cache.get('key4'), 'data4'); // Newly added
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('invalidateTable removes all entries for table', () => {
|
|
54
|
+
cache.set('key1', 'data1', 'users');
|
|
55
|
+
cache.set('key2', 'data2', 'users');
|
|
56
|
+
cache.set('key3', 'data3', 'posts');
|
|
57
|
+
|
|
58
|
+
cache.invalidateTable('users');
|
|
59
|
+
|
|
60
|
+
assert.strictEqual(cache.get('key1'), null);
|
|
61
|
+
assert.strictEqual(cache.get('key2'), null);
|
|
62
|
+
assert.strictEqual(cache.get('key3'), 'data3'); // Different table
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('getStats returns accurate metrics', () => {
|
|
66
|
+
const key1 = cache.generateKey('query1', [], 'users');
|
|
67
|
+
const key2 = cache.generateKey('query2', [], 'posts');
|
|
68
|
+
|
|
69
|
+
cache.set(key1, 'data1', 'users');
|
|
70
|
+
cache.get(key1); // Hit
|
|
71
|
+
cache.get(key2); // Miss
|
|
72
|
+
|
|
73
|
+
const stats = cache.getStats();
|
|
74
|
+
assert.strictEqual(stats.size, 1);
|
|
75
|
+
assert.strictEqual(stats.hits, 1);
|
|
76
|
+
assert.strictEqual(stats.misses, 1);
|
|
77
|
+
assert.strictEqual(stats.hitRate, '50.00%');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('clear removes all cache entries', () => {
|
|
81
|
+
const key1 = cache.generateKey('query1', [], 'users');
|
|
82
|
+
const key2 = cache.generateKey('query2', [], 'posts');
|
|
83
|
+
|
|
84
|
+
cache.set(key1, 'data1', 'users');
|
|
85
|
+
cache.set(key2, 'data2', 'posts');
|
|
86
|
+
|
|
87
|
+
assert.strictEqual(cache.cache.size, 2);
|
|
88
|
+
|
|
89
|
+
cache.clear();
|
|
90
|
+
|
|
91
|
+
assert.strictEqual(cache.cache.size, 0);
|
|
92
|
+
assert.strictEqual(cache.hitCount, 0);
|
|
93
|
+
assert.strictEqual(cache.missCount, 0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('disabled cache does not store or retrieve data', () => {
|
|
97
|
+
cache.enabled = false;
|
|
98
|
+
|
|
99
|
+
const key = cache.generateKey('SELECT * FROM users', [], 'users');
|
|
100
|
+
cache.set(key, 'data', 'users');
|
|
101
|
+
|
|
102
|
+
assert.strictEqual(cache.get(key), null);
|
|
103
|
+
assert.strictEqual(cache.cache.size, 0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('generateKey creates consistent keys for same query', () => {
|
|
107
|
+
const key1 = cache.generateKey('SELECT * FROM users WHERE id = ?', [1], 'users');
|
|
108
|
+
const key2 = cache.generateKey('SELECT * FROM users WHERE id = ?', [1], 'users');
|
|
109
|
+
const key3 = cache.generateKey('SELECT * FROM users WHERE id = ?', [2], 'users');
|
|
110
|
+
|
|
111
|
+
assert.strictEqual(key1, key2); // Same query + params = same key
|
|
112
|
+
assert.notStrictEqual(key1, key3); // Different params = different key
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('generateKey normalizes whitespace', () => {
|
|
116
|
+
const key1 = cache.generateKey('SELECT * FROM users', [], 'users');
|
|
117
|
+
const key2 = cache.generateKey('SELECT * FROM users', [], 'users');
|
|
118
|
+
|
|
119
|
+
assert.strictEqual(key1, key2); // Whitespace normalized
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('cache updates lastAccess on hit', () => {
|
|
123
|
+
const key = cache.generateKey('query', [], 'users');
|
|
124
|
+
cache.set(key, 'data', 'users');
|
|
125
|
+
|
|
126
|
+
const entry1 = cache.cache.get(key);
|
|
127
|
+
const originalAccess = entry1.lastAccess;
|
|
128
|
+
|
|
129
|
+
// Wait a bit
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
cache.get(key);
|
|
132
|
+
const entry2 = cache.cache.get(key);
|
|
133
|
+
assert(entry2.lastAccess > originalAccess);
|
|
134
|
+
}, 10);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('cache tracks hit count per entry', () => {
|
|
138
|
+
const key = cache.generateKey('query', [], 'users');
|
|
139
|
+
cache.set(key, 'data', 'users');
|
|
140
|
+
|
|
141
|
+
cache.get(key);
|
|
142
|
+
cache.get(key);
|
|
143
|
+
cache.get(key);
|
|
144
|
+
|
|
145
|
+
const entry = cache.cache.get(key);
|
|
146
|
+
assert.strictEqual(entry.hits, 3);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Real-World Example: Storing JavaScript Arrays as JSON Strings
|
|
3
|
-
*
|
|
4
|
-
* This example demonstrates how to use field transformers to store
|
|
5
|
-
* JavaScript arrays in database string columns, solving the common
|
|
6
|
-
* problem of "Type validation blocking array-to-JSON transformation"
|
|
7
|
-
*
|
|
8
|
-
* BEFORE (using raw SQL - not ideal):
|
|
9
|
-
* - Some fields saved through ORM
|
|
10
|
-
* - Array fields saved via raw SQL to bypass validation
|
|
11
|
-
* - Inconsistent, error-prone, loses ORM benefits
|
|
12
|
-
*
|
|
13
|
-
* AFTER (using transformers - production-ready):
|
|
14
|
-
* - All fields saved through ORM consistently
|
|
15
|
-
* - Arrays automatically transformed to/from JSON
|
|
16
|
-
* - Type-safe, maintainable, elegant
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const masterrecord = require('masterrecord');
|
|
20
|
-
|
|
21
|
-
// ============================================================================
|
|
22
|
-
// 1. Define Entity with Transformers
|
|
23
|
-
// ============================================================================
|
|
24
|
-
|
|
25
|
-
class User {
|
|
26
|
-
constructor() {
|
|
27
|
-
// Regular fields - no transformation needed
|
|
28
|
-
this.id = { type: "integer", primary: true, auto: true };
|
|
29
|
-
this.name = { type: "string" };
|
|
30
|
-
this.email = { type: "string" };
|
|
31
|
-
this.role = { type: "string" };
|
|
32
|
-
|
|
33
|
-
// 🔥 ARRAY FIELDS WITH TRANSFORMERS
|
|
34
|
-
// These fields store arrays as JSON strings in the database
|
|
35
|
-
this.certified_models = {
|
|
36
|
-
type: "string", // Database column type
|
|
37
|
-
nullable: true,
|
|
38
|
-
transform: {
|
|
39
|
-
// Transform JavaScript array → JSON string for database
|
|
40
|
-
toDatabase: (value) => {
|
|
41
|
-
if (value === null || value === undefined) return null;
|
|
42
|
-
if (Array.isArray(value)) return JSON.stringify(value);
|
|
43
|
-
// Already a string (maybe from edit scenario)
|
|
44
|
-
return value;
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
// Transform JSON string → JavaScript array from database
|
|
48
|
-
fromDatabase: (value) => {
|
|
49
|
-
if (!value) return [];
|
|
50
|
-
if (Array.isArray(value)) return value; // Already parsed
|
|
51
|
-
try {
|
|
52
|
-
return JSON.parse(value);
|
|
53
|
-
} catch {
|
|
54
|
-
console.warn(`Failed to parse certified_models: ${value}`);
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
this.certified_agent_types = {
|
|
62
|
-
type: "string",
|
|
63
|
-
nullable: true,
|
|
64
|
-
transform: {
|
|
65
|
-
toDatabase: (value) => {
|
|
66
|
-
if (value === null || value === undefined) return null;
|
|
67
|
-
if (Array.isArray(value)) return JSON.stringify(value);
|
|
68
|
-
return value;
|
|
69
|
-
},
|
|
70
|
-
fromDatabase: (value) => {
|
|
71
|
-
if (!value) return [];
|
|
72
|
-
if (Array.isArray(value)) return value;
|
|
73
|
-
try {
|
|
74
|
-
return JSON.parse(value);
|
|
75
|
-
} catch {
|
|
76
|
-
console.warn(`Failed to parse certified_agent_types: ${value}`);
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// Regular numeric field
|
|
84
|
-
this.calibration_score = { type: "integer", nullable: true };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ============================================================================
|
|
89
|
-
// 2. Create Context
|
|
90
|
-
// ============================================================================
|
|
91
|
-
|
|
92
|
-
class AppContext extends masterrecord.context {
|
|
93
|
-
constructor(config) {
|
|
94
|
-
super(config);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
onConfig(db) {
|
|
98
|
-
this.User = this.dbset(User, "User");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ============================================================================
|
|
103
|
-
// 3. Usage Example - Creating a User with Arrays
|
|
104
|
-
// ============================================================================
|
|
105
|
-
|
|
106
|
-
console.log("╔════════════════════════════════════════════════════════════════╗");
|
|
107
|
-
console.log("║ JSON Array Transformer - Real-World Example ║");
|
|
108
|
-
console.log("╚════════════════════════════════════════════════════════════════╝\n");
|
|
109
|
-
|
|
110
|
-
console.log("📝 Scenario: User certification management system");
|
|
111
|
-
console.log(" - Users can be certified for multiple AI models");
|
|
112
|
-
console.log(" - Users can handle multiple agent types");
|
|
113
|
-
console.log(" - Arrays stored as JSON strings in database\n");
|
|
114
|
-
|
|
115
|
-
// Simulated context (in real app, this would connect to actual database)
|
|
116
|
-
console.log("1️⃣ Creating new user with array fields");
|
|
117
|
-
console.log("──────────────────────────────────────────────────");
|
|
118
|
-
|
|
119
|
-
const user = new User();
|
|
120
|
-
user.name = "Alex Rich";
|
|
121
|
-
user.email = "alex@example.com";
|
|
122
|
-
user.role = "calibrator";
|
|
123
|
-
|
|
124
|
-
// ✨ Arrays assigned naturally - NO raw SQL needed!
|
|
125
|
-
user.certified_models = [1, 2, 5, 8]; // Array of model IDs
|
|
126
|
-
user.certified_agent_types = [10, 20, 30]; // Array of agent type IDs
|
|
127
|
-
user.calibration_score = 95;
|
|
128
|
-
|
|
129
|
-
console.log(` Name: ${user.name}`);
|
|
130
|
-
console.log(` Certified Models (array): [${user.certified_models.join(', ')}]`);
|
|
131
|
-
console.log(` Certified Agent Types (array): [${user.certified_agent_types.join(', ')}]`);
|
|
132
|
-
console.log(` Calibration Score: ${user.calibration_score}\n`);
|
|
133
|
-
|
|
134
|
-
// When saved, transformers automatically convert:
|
|
135
|
-
// [1, 2, 5, 8] → "[1,2,5,8]" (stored in DB)
|
|
136
|
-
console.log("2️⃣ What happens when saving");
|
|
137
|
-
console.log("──────────────────────────────────────────────────");
|
|
138
|
-
console.log(" User provides: [1, 2, 5, 8]");
|
|
139
|
-
console.log(" ↓");
|
|
140
|
-
console.log(" Transformer (toDatabase): [1, 2, 5, 8] → '[1,2,5,8]'");
|
|
141
|
-
console.log(" ↓");
|
|
142
|
-
console.log(" Type Validation: string '[1,2,5,8]' ✓ matches type: 'string'");
|
|
143
|
-
console.log(" ↓");
|
|
144
|
-
console.log(" Database Stores: '[1,2,5,8]' (as string column)\n");
|
|
145
|
-
|
|
146
|
-
// Standard save - no raw SQL required!
|
|
147
|
-
// context.User.add(user);
|
|
148
|
-
// context.saveChanges();
|
|
149
|
-
|
|
150
|
-
console.log("3️⃣ What happens when loading");
|
|
151
|
-
console.log("──────────────────────────────────────────────────");
|
|
152
|
-
console.log(" Database Returns: '[1,2,5,8]' (string)");
|
|
153
|
-
console.log(" ↓");
|
|
154
|
-
console.log(" Transformer (fromDatabase): '[1,2,5,8]' → [1, 2, 5, 8]");
|
|
155
|
-
console.log(" ↓");
|
|
156
|
-
console.log(" Application Receives: [1, 2, 5, 8] (JavaScript array)");
|
|
157
|
-
console.log(" ↓");
|
|
158
|
-
console.log(" Code: user.certified_models.includes(2) → true ✓\n");
|
|
159
|
-
|
|
160
|
-
// When loaded from DB, transformers automatically convert back:
|
|
161
|
-
// "[1,2,5,8]" → [1, 2, 5, 8] (JavaScript array)
|
|
162
|
-
// const users = context.User.where(u => u.id == $$, userId).toList();
|
|
163
|
-
// console.log(users[0].certified_models); // [1, 2, 5, 8]
|
|
164
|
-
|
|
165
|
-
console.log("4️⃣ Updating existing user");
|
|
166
|
-
console.log("──────────────────────────────────────────────────");
|
|
167
|
-
console.log(" const user = context.User.where(u => u.id == $$, 14).single();");
|
|
168
|
-
console.log(" user.certified_models = [1, 2, 5, 8, 12]; // Add model 12");
|
|
169
|
-
console.log(" context.saveChanges(); // ✓ Works perfectly!\n");
|
|
170
|
-
|
|
171
|
-
console.log("5️⃣ Benefits over raw SQL approach");
|
|
172
|
-
console.log("──────────────────────────────────────────────────");
|
|
173
|
-
console.log(" ✅ Consistent ORM usage (no raw SQL needed)");
|
|
174
|
-
console.log(" ✅ Automatic transformation (transparent to application code)");
|
|
175
|
-
console.log(" ✅ Type-safe (validation happens after transformation)");
|
|
176
|
-
console.log(" ✅ Maintainable (transformation logic in one place)");
|
|
177
|
-
console.log(" ✅ Testable (transformers are pure functions)");
|
|
178
|
-
console.log(" ✅ Works with all ORM features (tracking, relationships, etc.)\n");
|
|
179
|
-
|
|
180
|
-
console.log("6️⃣ Common Patterns");
|
|
181
|
-
console.log("──────────────────────────────────────────────────");
|
|
182
|
-
|
|
183
|
-
console.log("\n Pattern A: Simple Arrays");
|
|
184
|
-
console.log(" ─────────────────────────");
|
|
185
|
-
console.log(" certified_models: [1, 2, 3] → '[1,2,3]'");
|
|
186
|
-
|
|
187
|
-
console.log("\n Pattern B: String Arrays");
|
|
188
|
-
console.log(" ─────────────────────────");
|
|
189
|
-
console.log(" tags: ['urgent', 'review'] → '[\"urgent\",\"review\"]'");
|
|
190
|
-
|
|
191
|
-
console.log("\n Pattern C: Complex Objects");
|
|
192
|
-
console.log(" ─────────────────────────");
|
|
193
|
-
console.log(" metadata: {key: 'value'} → '{\"key\":\"value\"}'");
|
|
194
|
-
console.log(" transform: { toDatabase: JSON.stringify, fromDatabase: JSON.parse }");
|
|
195
|
-
|
|
196
|
-
console.log("\n Pattern D: Defaults for Null");
|
|
197
|
-
console.log(" ─────────────────────────");
|
|
198
|
-
console.log(" fromDatabase: (v) => v ? JSON.parse(v) : []");
|
|
199
|
-
|
|
200
|
-
console.log("\n\n╔════════════════════════════════════════════════════════════════╗");
|
|
201
|
-
console.log("║ Summary ║");
|
|
202
|
-
console.log("╚════════════════════════════════════════════════════════════════╝\n");
|
|
203
|
-
|
|
204
|
-
console.log("✨ PROBLEM SOLVED!");
|
|
205
|
-
console.log("\nBefore: Had to use raw SQL to bypass type validation");
|
|
206
|
-
console.log(" const sql = `UPDATE User SET certified_models = ? WHERE id = ?`;");
|
|
207
|
-
console.log(" context.User.raw(sql, [jsonString, userId]);");
|
|
208
|
-
console.log("\nAfter: Use ORM naturally with automatic transformation");
|
|
209
|
-
console.log(" user.certified_models = [1, 2, 3];");
|
|
210
|
-
console.log(" context.saveChanges();");
|
|
211
|
-
|
|
212
|
-
console.log("\n🎯 Use Case: This example solves the exact problem from the");
|
|
213
|
-
console.log(" BookBag calibration system where arrays needed to bypass ORM.\n");
|
|
214
|
-
|
|
215
|
-
console.log("📖 See readme.md 'Field Transformers' section for full documentation.\n");
|