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,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production-Grade Query Result Cache
|
|
3
|
+
* Similar to EF Core's EFCoreSecondLevelCacheInterceptor
|
|
4
|
+
*/
|
|
5
|
+
class QueryCache {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.cache = new Map();
|
|
8
|
+
this.ttl = options.ttl || 5 * 60 * 1000; // 5 minutes default
|
|
9
|
+
this.maxSize = options.maxSize || 1000; // LRU eviction
|
|
10
|
+
this.enabled = options.enabled !== false;
|
|
11
|
+
this.hitCount = 0;
|
|
12
|
+
this.missCount = 0;
|
|
13
|
+
|
|
14
|
+
// Start cleanup timer
|
|
15
|
+
this._startCleanupTimer();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate deterministic cache key from query + params
|
|
20
|
+
* Like Hibernate's query cache regions
|
|
21
|
+
*/
|
|
22
|
+
generateKey(query, params, tableName) {
|
|
23
|
+
const crypto = require('crypto');
|
|
24
|
+
const normalizedQuery = query.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
25
|
+
const keyData = {
|
|
26
|
+
query: normalizedQuery,
|
|
27
|
+
params: params || [],
|
|
28
|
+
table: tableName
|
|
29
|
+
};
|
|
30
|
+
return crypto.createHash('sha256')
|
|
31
|
+
.update(JSON.stringify(keyData))
|
|
32
|
+
.digest('hex');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get cached query result
|
|
37
|
+
*/
|
|
38
|
+
get(cacheKey) {
|
|
39
|
+
if (!this.enabled) return null;
|
|
40
|
+
|
|
41
|
+
const entry = this.cache.get(cacheKey);
|
|
42
|
+
if (!entry) {
|
|
43
|
+
this.missCount++;
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check TTL expiration
|
|
48
|
+
if (Date.now() - entry.timestamp > this.ttl) {
|
|
49
|
+
this.cache.delete(cacheKey);
|
|
50
|
+
this.missCount++;
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Update LRU metadata
|
|
55
|
+
entry.hits++;
|
|
56
|
+
entry.lastAccess = Date.now();
|
|
57
|
+
this.hitCount++;
|
|
58
|
+
|
|
59
|
+
// Log cache hit in dev
|
|
60
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
61
|
+
console.debug(`[QueryCache HIT] Key: ${cacheKey.substring(0, 8)}... (${entry.hits} hits)`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return entry.data;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Store query result in cache
|
|
69
|
+
*/
|
|
70
|
+
set(cacheKey, data, tableName) {
|
|
71
|
+
if (!this.enabled) return;
|
|
72
|
+
|
|
73
|
+
// LRU eviction if at capacity
|
|
74
|
+
if (this.cache.size >= this.maxSize) {
|
|
75
|
+
this._evictLRU();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.cache.set(cacheKey, {
|
|
79
|
+
data: data,
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
lastAccess: Date.now(),
|
|
82
|
+
hits: 0,
|
|
83
|
+
tableName: tableName
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Log cache set in dev
|
|
87
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
88
|
+
console.debug(`[QueryCache SET] Key: ${cacheKey.substring(0, 8)}... Table: ${tableName}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Invalidate all cache entries for a table
|
|
94
|
+
* Called automatically on INSERT/UPDATE/DELETE
|
|
95
|
+
*/
|
|
96
|
+
invalidateTable(tableName) {
|
|
97
|
+
let invalidated = 0;
|
|
98
|
+
for (const [key, entry] of this.cache) {
|
|
99
|
+
if (entry.tableName === tableName) {
|
|
100
|
+
this.cache.delete(key);
|
|
101
|
+
invalidated++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
106
|
+
console.debug(`[QueryCache INVALIDATE] Table: ${tableName} (${invalidated} entries)`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Clear all cache entries
|
|
112
|
+
*/
|
|
113
|
+
clear() {
|
|
114
|
+
this.cache.clear();
|
|
115
|
+
this.hitCount = 0;
|
|
116
|
+
this.missCount = 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get cache statistics
|
|
121
|
+
*/
|
|
122
|
+
getStats() {
|
|
123
|
+
const total = this.hitCount + this.missCount;
|
|
124
|
+
return {
|
|
125
|
+
size: this.cache.size,
|
|
126
|
+
maxSize: this.maxSize,
|
|
127
|
+
hits: this.hitCount,
|
|
128
|
+
misses: this.missCount,
|
|
129
|
+
hitRate: total > 0 ? (this.hitCount / total * 100).toFixed(2) + '%' : '0%',
|
|
130
|
+
enabled: this.enabled
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* LRU eviction - remove least recently used entry
|
|
136
|
+
*/
|
|
137
|
+
_evictLRU() {
|
|
138
|
+
let oldestKey = null;
|
|
139
|
+
let oldestTime = Infinity;
|
|
140
|
+
|
|
141
|
+
for (const [key, entry] of this.cache) {
|
|
142
|
+
if (entry.lastAccess < oldestTime) {
|
|
143
|
+
oldestTime = entry.lastAccess;
|
|
144
|
+
oldestKey = key;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (oldestKey) {
|
|
149
|
+
this.cache.delete(oldestKey);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Background cleanup of expired entries
|
|
155
|
+
*/
|
|
156
|
+
_startCleanupTimer() {
|
|
157
|
+
setInterval(() => {
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
let cleaned = 0;
|
|
160
|
+
|
|
161
|
+
for (const [key, entry] of this.cache) {
|
|
162
|
+
if (now - entry.timestamp > this.ttl) {
|
|
163
|
+
this.cache.delete(key);
|
|
164
|
+
cleaned++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (cleaned > 0 && process.env.NODE_ENV !== 'production') {
|
|
169
|
+
console.debug(`[QueryCache CLEANUP] Removed ${cleaned} expired entries`);
|
|
170
|
+
}
|
|
171
|
+
}, 60000); // Run every minute
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = QueryCache;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Distributed Query Cache using Redis
|
|
3
|
+
* For multi-process/clustered deployments
|
|
4
|
+
*/
|
|
5
|
+
class RedisQueryCache {
|
|
6
|
+
constructor(redisClient, options = {}) {
|
|
7
|
+
this.redis = redisClient;
|
|
8
|
+
this.localCache = new Map(); // L1 cache (in-memory)
|
|
9
|
+
this.ttl = options.ttl || 5 * 60; // Redis uses seconds
|
|
10
|
+
this.enabled = options.enabled !== false;
|
|
11
|
+
this.prefix = options.prefix || 'qcache:';
|
|
12
|
+
|
|
13
|
+
// Subscribe to invalidation events
|
|
14
|
+
this._setupInvalidationSubscription();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
generateKey(query, params, tableName) {
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
const hash = crypto.createHash('sha256')
|
|
20
|
+
.update(JSON.stringify({query, params}))
|
|
21
|
+
.digest('hex');
|
|
22
|
+
return `${this.prefix}${tableName}:${hash}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async get(cacheKey) {
|
|
26
|
+
if (!this.enabled) return null;
|
|
27
|
+
|
|
28
|
+
// L1: Check local cache first (fast)
|
|
29
|
+
if (this.localCache.has(cacheKey)) {
|
|
30
|
+
const entry = this.localCache.get(cacheKey);
|
|
31
|
+
if (Date.now() - entry.timestamp < 30000) { // 30s local TTL
|
|
32
|
+
return entry.data;
|
|
33
|
+
}
|
|
34
|
+
this.localCache.delete(cacheKey);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// L2: Check Redis (distributed)
|
|
38
|
+
try {
|
|
39
|
+
const cached = await this.redis.get(cacheKey);
|
|
40
|
+
if (cached) {
|
|
41
|
+
const data = JSON.parse(cached);
|
|
42
|
+
|
|
43
|
+
// Populate L1
|
|
44
|
+
this.localCache.set(cacheKey, {
|
|
45
|
+
data: data,
|
|
46
|
+
timestamp: Date.now()
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return data;
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('[RedisQueryCache] Get error:', error);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async set(cacheKey, data, tableName) {
|
|
59
|
+
if (!this.enabled) return;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Store in Redis with TTL
|
|
63
|
+
await this.redis.setex(cacheKey, this.ttl, JSON.stringify(data));
|
|
64
|
+
|
|
65
|
+
// Store in L1
|
|
66
|
+
this.localCache.set(cacheKey, {
|
|
67
|
+
data: data,
|
|
68
|
+
timestamp: Date.now()
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('[RedisQueryCache] Set error:', error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async invalidateTable(tableName) {
|
|
76
|
+
try {
|
|
77
|
+
// Find all keys for this table
|
|
78
|
+
const pattern = `${this.prefix}${tableName}:*`;
|
|
79
|
+
const keys = await this.redis.keys(pattern);
|
|
80
|
+
|
|
81
|
+
if (keys.length > 0) {
|
|
82
|
+
await this.redis.del(...keys);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Clear L1
|
|
86
|
+
for (const [key] of this.localCache) {
|
|
87
|
+
if (key.includes(tableName)) {
|
|
88
|
+
this.localCache.delete(key);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Publish invalidation to other processes
|
|
93
|
+
await this.redis.publish('cache-invalidate', JSON.stringify({
|
|
94
|
+
table: tableName,
|
|
95
|
+
timestamp: Date.now()
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('[RedisQueryCache] Invalidate error:', error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
_setupInvalidationSubscription() {
|
|
104
|
+
// Subscribe to invalidation messages from other processes
|
|
105
|
+
const subscriber = this.redis.duplicate();
|
|
106
|
+
subscriber.subscribe('cache-invalidate', (message) => {
|
|
107
|
+
try {
|
|
108
|
+
const event = JSON.parse(message);
|
|
109
|
+
|
|
110
|
+
// Clear local cache for this table
|
|
111
|
+
for (const [key] of this.localCache) {
|
|
112
|
+
if (key.includes(event.table)) {
|
|
113
|
+
this.localCache.delete(key);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('[RedisQueryCache] Subscription error:', error);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async clear() {
|
|
123
|
+
try {
|
|
124
|
+
const keys = await this.redis.keys(`${this.prefix}*`);
|
|
125
|
+
if (keys.length > 0) {
|
|
126
|
+
await this.redis.del(...keys);
|
|
127
|
+
}
|
|
128
|
+
this.localCache.clear();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('[RedisQueryCache] Clear error:', error);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getStats() {
|
|
135
|
+
try {
|
|
136
|
+
const keys = await this.redis.keys(`${this.prefix}*`);
|
|
137
|
+
return {
|
|
138
|
+
size: keys.length,
|
|
139
|
+
localSize: this.localCache.size,
|
|
140
|
+
enabled: this.enabled,
|
|
141
|
+
ttl: this.ttl
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('[RedisQueryCache] GetStats error:', error);
|
|
145
|
+
return {
|
|
146
|
+
size: 0,
|
|
147
|
+
localSize: this.localCache.size,
|
|
148
|
+
enabled: this.enabled,
|
|
149
|
+
ttl: this.ttl
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = RedisQueryCache;
|
|
@@ -10,6 +10,7 @@ class queryMethods{
|
|
|
10
10
|
this.__entity = entity;
|
|
11
11
|
this.__context = context;
|
|
12
12
|
this.__queryObject = new queryScript();
|
|
13
|
+
this.__useCache = true; // Enable caching by default
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
// build a single entity
|
|
@@ -87,6 +88,15 @@ class queryMethods{
|
|
|
87
88
|
return this;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Disable query caching for this query
|
|
93
|
+
* Use for queries that should always hit database
|
|
94
|
+
*/
|
|
95
|
+
noCache() {
|
|
96
|
+
this.__useCache = false;
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
|
|
90
100
|
raw(query){
|
|
91
101
|
this.__queryObject.raw(query);
|
|
92
102
|
return this;
|
|
@@ -321,47 +331,84 @@ class queryMethods{
|
|
|
321
331
|
this.__queryObject.script.take = 1;
|
|
322
332
|
}
|
|
323
333
|
|
|
334
|
+
// Generate cache key
|
|
335
|
+
const tableName = this.__entity.__name;
|
|
336
|
+
const queryString = JSON.stringify(this.__queryObject.script);
|
|
337
|
+
const params = this.__queryObject.script.parameters ? this.__queryObject.script.parameters.getParams() : [];
|
|
338
|
+
const cacheKey = this.__context._queryCache.generateKey(queryString, params, tableName);
|
|
339
|
+
|
|
340
|
+
// Check cache first (if enabled for this query)
|
|
341
|
+
if (this.__useCache) {
|
|
342
|
+
const cached = this.__context._queryCache.get(cacheKey);
|
|
343
|
+
if (cached) {
|
|
344
|
+
this.__reset();
|
|
345
|
+
return cached;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Cache miss - execute query
|
|
350
|
+
var result = null;
|
|
324
351
|
if(this.__context.isSQLite){
|
|
325
352
|
var entityValue = this.__context._SQLEngine.get(this.__queryObject.script, this.__entity, this.__context);
|
|
326
|
-
|
|
327
|
-
this.__reset();
|
|
328
|
-
return sing;
|
|
353
|
+
result = this.__singleEntityBuilder(entityValue);
|
|
329
354
|
}
|
|
330
|
-
|
|
355
|
+
|
|
331
356
|
if(this.__context.isMySQL){
|
|
332
357
|
var entityValue = this.__context._SQLEngine.get(this.__queryObject.script, this.__entity, this.__context);
|
|
333
|
-
|
|
334
|
-
this.__reset();
|
|
335
|
-
return sing;
|
|
358
|
+
result = this.__singleEntityBuilder(entityValue[0]);
|
|
336
359
|
}
|
|
360
|
+
|
|
361
|
+
// Store in cache
|
|
362
|
+
if (this.__useCache && result) {
|
|
363
|
+
this.__context._queryCache.set(cacheKey, result, tableName);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
this.__reset();
|
|
367
|
+
return result;
|
|
337
368
|
}
|
|
338
369
|
|
|
339
370
|
toList(){
|
|
340
|
-
if(this.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
371
|
+
if(this.__queryObject.script.entityMap.length === 0){
|
|
372
|
+
this.__queryObject.skipClause( this.__entity.__name);
|
|
373
|
+
if(!this.__queryObject.script.take || this.__queryObject.script.take === 0){
|
|
374
|
+
this.__queryObject.script.take = 1000;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Generate cache key
|
|
379
|
+
const tableName = this.__entity.__name;
|
|
380
|
+
const queryString = JSON.stringify(this.__queryObject.script);
|
|
381
|
+
const params = this.__queryObject.script.parameters ? this.__queryObject.script.parameters.getParams() : [];
|
|
382
|
+
const cacheKey = this.__context._queryCache.generateKey(queryString, params, tableName);
|
|
383
|
+
|
|
384
|
+
// Check cache first (if enabled for this query)
|
|
385
|
+
if (this.__useCache) {
|
|
386
|
+
const cached = this.__context._queryCache.get(cacheKey);
|
|
387
|
+
if (cached) {
|
|
388
|
+
this.__reset();
|
|
389
|
+
return cached;
|
|
346
390
|
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Cache miss - execute query
|
|
394
|
+
var result = [];
|
|
395
|
+
if(this.__context.isSQLite){
|
|
347
396
|
var entityValue = this.__context._SQLEngine.all(this.__queryObject.script, this.__entity, this.__context);
|
|
348
|
-
|
|
349
|
-
this.__reset();
|
|
350
|
-
return toLi;
|
|
397
|
+
result = this.__multipleEntityBuilder(entityValue);
|
|
351
398
|
}
|
|
352
399
|
|
|
353
400
|
if(this.__context.isMySQL){
|
|
354
|
-
if(this.__queryObject.script.entityMap.length === 0){
|
|
355
|
-
this.__queryObject.skipClause( this.__entity.__name);
|
|
356
|
-
if(!this.__queryObject.script.take || this.__queryObject.script.take === 0){
|
|
357
|
-
this.__queryObject.script.take = 1000;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
401
|
var entityValue = this.__context._SQLEngine.all(this.__queryObject.script, this.__entity, this.__context);
|
|
361
|
-
|
|
362
|
-
this.__reset();
|
|
363
|
-
return toLi;
|
|
402
|
+
result = this.__multipleEntityBuilder(entityValue);
|
|
364
403
|
}
|
|
404
|
+
|
|
405
|
+
// Store in cache
|
|
406
|
+
if (this.__useCache && result) {
|
|
407
|
+
this.__context._queryCache.set(cacheKey, result, tableName);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
this.__reset();
|
|
411
|
+
return result;
|
|
365
412
|
}
|
|
366
413
|
|
|
367
414
|
// ------------------------------- FUNCTIONS THAT UPDATE SQL START FROM HERE -----------------------------------------------------
|
package/context.js
CHANGED
|
@@ -14,6 +14,7 @@ var path = require('path');
|
|
|
14
14
|
const appRoot = require('app-root-path');
|
|
15
15
|
const MySQLClient = require('masterrecord/mySQLSyncConnect');
|
|
16
16
|
const PostgresClient = require('masterrecord/postgresSyncConnect');
|
|
17
|
+
const QueryCache = require('./Cache/QueryCache');
|
|
17
18
|
|
|
18
19
|
class context {
|
|
19
20
|
_isModelValid = {
|
|
@@ -37,6 +38,13 @@ class context {
|
|
|
37
38
|
this.__name = this.constructor.name;
|
|
38
39
|
this._SQLEngine = "";
|
|
39
40
|
this.__trackedEntitiesMap = new Map(); // Initialize Map for O(1) lookups
|
|
41
|
+
|
|
42
|
+
// Initialize query cache
|
|
43
|
+
this._queryCache = new QueryCache({
|
|
44
|
+
ttl: process.env.QUERY_CACHE_TTL || 5 * 60 * 1000, // 5 min default
|
|
45
|
+
maxSize: process.env.QUERY_CACHE_SIZE || 1000,
|
|
46
|
+
enabled: process.env.QUERY_CACHE_ENABLED !== 'false'
|
|
47
|
+
});
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/*
|
|
@@ -549,6 +557,15 @@ class context {
|
|
|
549
557
|
const tracked = this.__trackedEntities;
|
|
550
558
|
|
|
551
559
|
if(tracked.length > 0){
|
|
560
|
+
// Collect affected tables for cache invalidation
|
|
561
|
+
const affectedTables = new Set();
|
|
562
|
+
for (let i = 0; i < tracked.length; i++) {
|
|
563
|
+
const entity = tracked[i];
|
|
564
|
+
if (entity.__entity && entity.__entity.__name) {
|
|
565
|
+
affectedTables.add(entity.__entity.__name);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
552
569
|
// Handle transactions based on database type
|
|
553
570
|
if(this.isSQLite){
|
|
554
571
|
this._SQLEngine.startTransaction();
|
|
@@ -568,6 +585,11 @@ class context {
|
|
|
568
585
|
this._processTrackedEntities(tracked);
|
|
569
586
|
this.__clearErrorHandler();
|
|
570
587
|
}
|
|
588
|
+
|
|
589
|
+
// Invalidate query cache for affected tables
|
|
590
|
+
for (const tableName of affectedTables) {
|
|
591
|
+
this._queryCache.invalidateTable(tableName);
|
|
592
|
+
}
|
|
571
593
|
}
|
|
572
594
|
else{
|
|
573
595
|
console.log("save changes has no tracked entities");
|
|
@@ -593,6 +615,27 @@ class context {
|
|
|
593
615
|
this._SQLEngine._execute(query);
|
|
594
616
|
}
|
|
595
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Get query cache statistics
|
|
620
|
+
*/
|
|
621
|
+
getCacheStats() {
|
|
622
|
+
return this._queryCache.getStats();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Clear query cache manually
|
|
627
|
+
*/
|
|
628
|
+
clearQueryCache() {
|
|
629
|
+
this._queryCache.clear();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Enable/disable query caching
|
|
634
|
+
*/
|
|
635
|
+
setQueryCacheEnabled(enabled) {
|
|
636
|
+
this._queryCache.enabled = enabled;
|
|
637
|
+
}
|
|
638
|
+
|
|
596
639
|
// __track(model){
|
|
597
640
|
// this.__trackedEntities.push(model);
|
|
598
641
|
// return model;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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": {
|