sqlcache 2.9.0 → 2.9.1
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/.env.example +31 -17
- package/{readme.md → README.md} +106 -28
- package/core/autoInvalidate.js +14 -4
- package/core/autoKey.js +44 -8
- package/dbConnector.js +418 -65
- package/docs/API.md +161 -61
- package/docs/CONFIGURATION.md +155 -47
- package/docs/CONTRIBUTING.md +39 -29
- package/docs/EXAMPLES.md +70 -29
- package/docs/MIGRATION.md +141 -42
- package/docs/README.tr.md +75 -27
- package/docs/USE-CASE-BULK.md +48 -17
- package/docs/USE-CASE-ECOMMERCE.md +8 -2
- package/docs/USE-CASE-REALTIME.md +18 -6
- package/docs/USE-CASE-SAAS.md +24 -15
- package/index.d.ts +163 -50
- package/index.js +50 -23
- package/package.json +11 -5
- package/redis.Connector.js +98 -5
package/docs/EXAMPLES.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Comprehensive examples for common use cases.
|
|
4
4
|
|
|
5
|
+
Most examples omit cache names and invalidation prefixes, so they assume both automatic features are
|
|
6
|
+
on:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# .env
|
|
10
|
+
CORE_AUTO_FEATURES=true # automatic cache keys: cacheName may be omitted
|
|
11
|
+
CORE_AUTO_INVALIDATION=true # automatic invalidation: writes without resetCacheName clear <table>:* and <table>_*
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Without `CORE_AUTO_FEATURES`, a call without a cache name throws `cacheName is required...`; without
|
|
15
|
+
`CORE_AUTO_INVALIDATION`, a write without `resetCacheName` clears nothing. The handlers are
|
|
16
|
+
Express-style, with `app`, `req` and `res` defined by your application.
|
|
17
|
+
|
|
5
18
|
---
|
|
6
19
|
|
|
7
20
|
## Table of Contents
|
|
@@ -41,11 +54,11 @@ app.post('/orders', async (req, res) => {
|
|
|
41
54
|
|
|
42
55
|
// 2. Process each item
|
|
43
56
|
for (const item of items) {
|
|
44
|
-
// Check stock
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
// Check stock: read and lock the row inside the transaction, never from the
|
|
58
|
+
// cache (a cached stock level can be hours old)
|
|
59
|
+
const [product] = await tx.query(
|
|
60
|
+
'SELECT id, name, price, stock FROM products WHERE id = ? FOR UPDATE',
|
|
61
|
+
[item.productId]
|
|
49
62
|
);
|
|
50
63
|
|
|
51
64
|
if (!product || product.stock < item.quantity) {
|
|
@@ -58,7 +71,7 @@ app.post('/orders', async (req, res) => {
|
|
|
58
71
|
[orderId, item.productId, item.quantity, product.price]
|
|
59
72
|
);
|
|
60
73
|
|
|
61
|
-
// Update stock
|
|
74
|
+
// Update stock (products:* and products_* are cleared after the commit)
|
|
62
75
|
await tx.query(
|
|
63
76
|
'UPDATE products SET stock = stock - ? WHERE id = ?',
|
|
64
77
|
[item.quantity, item.productId]
|
|
@@ -94,7 +107,9 @@ app.post('/orders', async (req, res) => {
|
|
|
94
107
|
### Product Catalog with Caching
|
|
95
108
|
|
|
96
109
|
```javascript
|
|
97
|
-
// Get products by category (cached)
|
|
110
|
+
// Get products by category (cached under products:category_id:status:<hash>)
|
|
111
|
+
// Only the first table counts for keys and invalidation: when a category is renamed,
|
|
112
|
+
// pass 'products:' as resetCacheName so these listings are refreshed.
|
|
98
113
|
app.get('/products/category/:categoryId', async (req, res) => {
|
|
99
114
|
const { categoryId } = req.params;
|
|
100
115
|
const { page = 1, limit = 20 } = req.query;
|
|
@@ -121,7 +136,7 @@ app.put('/products/:id', async (req, res) => {
|
|
|
121
136
|
'UPDATE products SET name = ?, price = ?, stock = ?, category_id = ? WHERE id = ?',
|
|
122
137
|
[name, price, stock, category_id, id]
|
|
123
138
|
);
|
|
124
|
-
// ✅
|
|
139
|
+
// ✅ products:* and products_* automatically cleared (CORE_AUTO_INVALIDATION=true)
|
|
125
140
|
|
|
126
141
|
res.json({ success: true });
|
|
127
142
|
});
|
|
@@ -237,13 +252,18 @@ app.post('/posts/:id/like', async (req, res) => {
|
|
|
237
252
|
### Tenant Database Switching
|
|
238
253
|
|
|
239
254
|
```javascript
|
|
240
|
-
//
|
|
255
|
+
const bcrypt = require('bcrypt'); // or another password-hashing library
|
|
256
|
+
|
|
257
|
+
// Middleware to get the tenant database: take the tenant from the authenticated session
|
|
258
|
+
// (set by your auth middleware) and check it against an allowlist, never from a raw header
|
|
259
|
+
const TENANT_DATABASES = new Set(['tenant_1', 'tenant_2', 'tenant_3']);
|
|
260
|
+
|
|
241
261
|
const getTenantDb = (req, res, next) => {
|
|
242
|
-
const
|
|
243
|
-
if (!
|
|
244
|
-
return res.status(
|
|
262
|
+
const tenantDb = req.user && `tenant_${req.user.tenantId}`;
|
|
263
|
+
if (!tenantDb || !TENANT_DATABASES.has(tenantDb)) {
|
|
264
|
+
return res.status(403).json({ error: 'Unknown tenant' });
|
|
245
265
|
}
|
|
246
|
-
req.tenantDb =
|
|
266
|
+
req.tenantDb = tenantDb;
|
|
247
267
|
next();
|
|
248
268
|
};
|
|
249
269
|
|
|
@@ -252,20 +272,23 @@ app.get('/users', getTenantDb, async (req, res) => {
|
|
|
252
272
|
const users = await getCacheQuery(
|
|
253
273
|
'SELECT id, name, email, role, created_at FROM users WHERE status = ?',
|
|
254
274
|
['active'],
|
|
255
|
-
null, // Auto-generated cache key
|
|
275
|
+
null, // Auto-generated cache key, ends with @<tenant database>
|
|
256
276
|
req.tenantDb // Tenant-specific database
|
|
257
277
|
);
|
|
258
278
|
|
|
259
279
|
res.json(users);
|
|
260
280
|
});
|
|
261
281
|
|
|
262
|
-
// Create user in tenant database
|
|
263
|
-
app.post('/users', getTenantDb, async (req, res) => {
|
|
264
|
-
const { name, email, password
|
|
282
|
+
// Create user in tenant database (requireAdmin: your own authorization middleware)
|
|
283
|
+
app.post('/users', getTenantDb, requireAdmin, async (req, res) => {
|
|
284
|
+
const { name, email, password } = req.body;
|
|
285
|
+
// Never take a role from the request as-is, and never store a plain-text password
|
|
286
|
+
const role = ['member', 'editor'].includes(req.body.role) ? req.body.role : 'member';
|
|
287
|
+
const passwordHash = await bcrypt.hash(password, 10);
|
|
265
288
|
|
|
266
289
|
const result = await QuaryCache(
|
|
267
290
|
'INSERT INTO users (name, email, password, role, created_at) VALUES (?, ?, ?, ?, NOW())',
|
|
268
|
-
[name, email,
|
|
291
|
+
[name, email, passwordHash, role],
|
|
269
292
|
null, // Auto-invalidation
|
|
270
293
|
req.tenantDb
|
|
271
294
|
);
|
|
@@ -278,21 +301,30 @@ app.post('/users', getTenantDb, async (req, res) => {
|
|
|
278
301
|
|
|
279
302
|
```javascript
|
|
280
303
|
app.get('/dashboard/stats', getTenantDb, async (req, res) => {
|
|
304
|
+
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
305
|
+
|
|
281
306
|
const stats = await withTransaction(async (tx) => {
|
|
282
|
-
//
|
|
307
|
+
// Several stats, one after another (a transaction runs on one connection).
|
|
308
|
+
// Aggregates get explicit cache names: an automatic key would be users:all@<db>, the same
|
|
309
|
+
// as for any other query on the table without a WHERE clause. Names under the
|
|
310
|
+
// '<table>:' prefix are still cleared by automatic invalidation.
|
|
283
311
|
const [userStats] = await tx.getCacheQuery(
|
|
284
312
|
'SELECT COUNT(*) as total, SUM(CASE WHEN status="active" THEN 1 ELSE 0 END) as active FROM users',
|
|
285
|
-
[]
|
|
313
|
+
[],
|
|
314
|
+
`users:stats@${req.tenantDb}`
|
|
286
315
|
);
|
|
287
316
|
|
|
317
|
+
// The day is a parameter and part of the name, so yesterday's entry is not reused
|
|
288
318
|
const [orderStats] = await tx.getCacheQuery(
|
|
289
|
-
'SELECT COUNT(*) as total, SUM(total_amount) as revenue FROM orders WHERE
|
|
290
|
-
[]
|
|
319
|
+
'SELECT COUNT(*) as total, SUM(total_amount) as revenue FROM orders WHERE created_at >= ? AND created_at < ? + INTERVAL 1 DAY',
|
|
320
|
+
[today, today],
|
|
321
|
+
`orders:stats:${today}@${req.tenantDb}`
|
|
291
322
|
);
|
|
292
323
|
|
|
293
324
|
const [productStats] = await tx.getCacheQuery(
|
|
294
325
|
'SELECT COUNT(*) as total, SUM(CASE WHEN stock < 10 THEN 1 ELSE 0 END) as low_stock FROM products',
|
|
295
|
-
[]
|
|
326
|
+
[],
|
|
327
|
+
`products:stats@${req.tenantDb}`
|
|
296
328
|
);
|
|
297
329
|
|
|
298
330
|
return {
|
|
@@ -319,9 +351,12 @@ const { bulkInsert } = require('sqlcache');
|
|
|
319
351
|
app.post('/analytics/events/bulk', async (req, res) => {
|
|
320
352
|
const events = req.body.events; // Array of events
|
|
321
353
|
|
|
322
|
-
//
|
|
354
|
+
// Map each event onto a fixed set of columns: bulkInsert uses the keys of the first
|
|
355
|
+
// record as column names, so never pass request objects (or spread them) as they are
|
|
323
356
|
const enrichedEvents = events.map(event => ({
|
|
324
|
-
|
|
357
|
+
event_type: String(event.type),
|
|
358
|
+
user_id: event.userId,
|
|
359
|
+
data: JSON.stringify(event.data),
|
|
325
360
|
created_at: new Date(),
|
|
326
361
|
ip_address: req.ip
|
|
327
362
|
}));
|
|
@@ -459,6 +494,9 @@ app.post('/chat/rooms/:roomId/read', async (req, res) => {
|
|
|
459
494
|
### User Registration with Transaction
|
|
460
495
|
|
|
461
496
|
```javascript
|
|
497
|
+
const bcrypt = require('bcrypt'); // or another password-hashing library
|
|
498
|
+
const { withTransaction } = require('sqlcache');
|
|
499
|
+
|
|
462
500
|
app.post('/auth/register', async (req, res) => {
|
|
463
501
|
const { email, password, name } = req.body;
|
|
464
502
|
|
|
@@ -508,16 +546,19 @@ app.post('/auth/register', async (req, res) => {
|
|
|
508
546
|
### Session Management
|
|
509
547
|
|
|
510
548
|
```javascript
|
|
511
|
-
const
|
|
549
|
+
const crypto = require('crypto');
|
|
550
|
+
const bcrypt = require('bcrypt');
|
|
551
|
+
const { QuaryCache, addArrayItem, getArrayItem, delKeyItem } = require('sqlcache');
|
|
512
552
|
|
|
513
553
|
// Create session
|
|
514
554
|
app.post('/auth/login', async (req, res) => {
|
|
515
555
|
const { email, password } = req.body;
|
|
516
556
|
|
|
517
|
-
|
|
557
|
+
// Read credentials from MySQL, not through the cache: a cached password hash would keep an
|
|
558
|
+
// old password valid until the entry expires (QuaryCache neither reads the cache nor stores results)
|
|
559
|
+
const [user] = await QuaryCache(
|
|
518
560
|
'SELECT id, email, password, name FROM users WHERE email = ?',
|
|
519
|
-
[email]
|
|
520
|
-
`user-email-${email}`
|
|
561
|
+
[email]
|
|
521
562
|
);
|
|
522
563
|
|
|
523
564
|
if (!user || !(await bcrypt.compare(password, user.password))) {
|
package/docs/MIGRATION.md
CHANGED
|
@@ -1,6 +1,83 @@
|
|
|
1
1
|
# Migration Guide
|
|
2
2
|
|
|
3
|
-
Guide for
|
|
3
|
+
Guide for upgrading between sqlcache versions and adopting the automatic features.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## v2.9.0 → v2.9.1
|
|
8
|
+
|
|
9
|
+
A patch release. The API is unchanged; some TypeScript types accept more than before. The
|
|
10
|
+
differences:
|
|
11
|
+
|
|
12
|
+
### Requirements
|
|
13
|
+
|
|
14
|
+
- Node.js 14 or newer (tested on 18–26); `engines.node` is `>=14`.
|
|
15
|
+
- mysql2 3.23.1 or newer (the dependency is now `^3.23.1`). Older versions have security
|
|
16
|
+
advisories, including critical ones; update lockfiles that pin an older mysql2.
|
|
17
|
+
|
|
18
|
+
### Imports
|
|
19
|
+
|
|
20
|
+
- Named ESM imports work: `import { getCacheQuery } from 'sqlcache'`. On 2.9.0 and earlier, native
|
|
21
|
+
ESM could only import `QuaryCache` by name; use the default import there.
|
|
22
|
+
- `import sqlcache from 'sqlcache'` works without `esModuleInterop`: the module now has a
|
|
23
|
+
non-enumerable `default` property, as the type definitions always declared.
|
|
24
|
+
- Types: `QuaryCache<T>()` takes the result type (default `QueryResult`, which gained `info` and
|
|
25
|
+
`warningStatus`); `resetCacheName` accepts `string[]` in `QuaryCache` and `bulkInsert`; `parameters`
|
|
26
|
+
is optional except in `getCacheQueryPagination`; the cache name may be omitted in
|
|
27
|
+
`getCacheQueryWithTimeout` and passed as `null` to `getCacheQueryPagination`; a `withTransaction`
|
|
28
|
+
callback may return a plain value; `configure`, `enableAutoKey` and `enableAutoInvalidation` accept
|
|
29
|
+
booleans.
|
|
30
|
+
|
|
31
|
+
### Caching
|
|
32
|
+
|
|
33
|
+
- Empty results are no longer written to the cache; they were never served from it.
|
|
34
|
+
- A cached value that is not a non-empty array is treated as a miss. For pagination, it must be a
|
|
35
|
+
page object.
|
|
36
|
+
- A read that started before an invalidation in the same process no longer writes its possibly stale
|
|
37
|
+
result to the cache. Across processes a short window remains.
|
|
38
|
+
- `tx.getCacheQuery` still reads the cache first, but writes to it only after the transaction commits.
|
|
39
|
+
Nothing is cached from a rolled-back transaction.
|
|
40
|
+
- `getCacheQueryPagination` and `getCacheQueryWithTimeout` without a cache name use an automatic key
|
|
41
|
+
when automatic keys are enabled; otherwise the result is not cached and a one-time warning is
|
|
42
|
+
logged. Before 2.9.1 all such calls shared one cache key.
|
|
43
|
+
- Automatic keys of queries without a detectable table (`CALL`, `SELECT fn(?)`) now include the
|
|
44
|
+
parameters: `query:<sqlhash>:<paramhash>`. `BigInt` parameters no longer throw.
|
|
45
|
+
- New `DB_COALESCE_MAX_WAIT` (ms, default 10000): how long concurrent cache misses wait for a shared
|
|
46
|
+
query before starting a fresh one, which protects against a stuck connection. A waiting caller
|
|
47
|
+
still takes the original result if it arrives first. When a shared query fails with
|
|
48
|
+
`ER_QUERY_TIMEOUT` (`MAX_EXECUTION_TIME`), that error goes to every caller waiting on it.
|
|
49
|
+
|
|
50
|
+
### Turning features off
|
|
51
|
+
|
|
52
|
+
- `enableAutoKey(false)`, `enableAutoInvalidation(false)` and `configure({ autoKey: false })` now
|
|
53
|
+
disable the feature. Before 2.9.1 they enabled it; check calls that pass `false`.
|
|
54
|
+
|
|
55
|
+
### bulkInsert
|
|
56
|
+
|
|
57
|
+
- Invalid table or column names are rejected before anything is sent, with a `TypeError`:
|
|
58
|
+
`bulkInsert: invalid table name "<name>"` or `bulkInsert: invalid column name "<name>"`. Valid names
|
|
59
|
+
are unchanged.
|
|
60
|
+
- Auto-invalidation (`CORE_AUTO_INVALIDATION` / `enableAutoInvalidation`) now also applies to
|
|
61
|
+
`bulkInsert` when no `resetCacheName` is given.
|
|
62
|
+
- Invalidation also runs when `bulkInsert` fails after rows may have been written.
|
|
63
|
+
- With `transaction: true`, an error from `COMMIT` is not retried and carries
|
|
64
|
+
`err.commitOutcomeUnknown = true`.
|
|
65
|
+
|
|
66
|
+
### Queries and connections
|
|
67
|
+
|
|
68
|
+
- `cancelOnTimeout`: the connection whose query was killed is discarded instead of being returned to
|
|
69
|
+
the pool, at most 4 cancellations run at once, and a timeout during the database switch also stops
|
|
70
|
+
the query from being sent.
|
|
71
|
+
- Pagination appends `LIMIT` on a new line, so SQL ending in a `-- comment` paginates correctly.
|
|
72
|
+
- `DROP DATABASE` / `DROP SCHEMA` in your SQL is handled like `USE`: the next call on that pooled
|
|
73
|
+
connection selects its database again.
|
|
74
|
+
|
|
75
|
+
### Redis
|
|
76
|
+
|
|
77
|
+
- `isRedisConnected()` returns `false` without opening a connection when `REDIS_ENABLED=false`.
|
|
78
|
+
- One log line per reconnect attempt: `Redis reconnecting... (attempt N, delay Xms)`.
|
|
79
|
+
- A log line appears if more than 1000 invalidations are missed during an outage.
|
|
80
|
+
- A one-time warning appears if `REDIS_VHOST` changes at runtime.
|
|
4
81
|
|
|
5
82
|
---
|
|
6
83
|
|
|
@@ -52,9 +129,10 @@ app.post('/users', async (req, res) => {
|
|
|
52
129
|
const { getCacheQuery, QuaryCache } = require('sqlcache');
|
|
53
130
|
|
|
54
131
|
// .env
|
|
55
|
-
CORE_AUTO_FEATURES=true
|
|
132
|
+
CORE_AUTO_FEATURES=true // automatic cache keys
|
|
133
|
+
CORE_AUTO_INVALIDATION=true // automatic invalidation (CORE_AUTO_FEATURES alone does not enable it)
|
|
56
134
|
|
|
57
|
-
// Read with auto-cache
|
|
135
|
+
// Read with auto-cache (key users:id:<hash>)
|
|
58
136
|
app.get('/users/:id', async (req, res) => {
|
|
59
137
|
const users = await getCacheQuery('SELECT * FROM users WHERE id = ?', [req.params.id]);
|
|
60
138
|
res.json(users[0]);
|
|
@@ -63,16 +141,19 @@ app.get('/users/:id', async (req, res) => {
|
|
|
63
141
|
// Write with auto-invalidation
|
|
64
142
|
app.post('/users', async (req, res) => {
|
|
65
143
|
await QuaryCache('INSERT INTO users (name, email) VALUES (?, ?)', [req.body.name, req.body.email]);
|
|
66
|
-
// ✅
|
|
144
|
+
// ✅ users:* and users_* automatically cleared
|
|
67
145
|
res.json({ success: true });
|
|
68
146
|
});
|
|
69
147
|
```
|
|
70
148
|
|
|
71
149
|
**Benefits:**
|
|
72
150
|
- 10+ lines → 3 lines
|
|
73
|
-
- No cache key management
|
|
74
|
-
- No manual invalidation
|
|
75
|
-
|
|
151
|
+
- No cache key management for simple queries
|
|
152
|
+
- No manual invalidation for writes to the cached table
|
|
153
|
+
|
|
154
|
+
Automatic keys and invalidation follow the first table of each statement only; see the README's
|
|
155
|
+
[Cache keys and invalidation](../README.md#cache-keys-and-invalidation) for what needs an explicit
|
|
156
|
+
cache name or `resetCacheName` (joins, aggregates, `CALL`, writes from other applications, ...).
|
|
76
157
|
|
|
77
158
|
---
|
|
78
159
|
|
|
@@ -95,8 +176,8 @@ const [users] = await pool.query('SELECT * FROM users WHERE id = ?', [123]);
|
|
|
95
176
|
// After
|
|
96
177
|
const { getCacheQuery } = require('sqlcache');
|
|
97
178
|
|
|
98
|
-
const users = await getCacheQuery('SELECT * FROM users WHERE id = ?', [123]);
|
|
99
|
-
// ✅ Same
|
|
179
|
+
const users = await getCacheQuery('SELECT * FROM users WHERE id = ?', [123], 'user:123');
|
|
180
|
+
// ✅ Same SQL and parameters + automatic caching; returns the rows directly (no [rows, fields])
|
|
100
181
|
```
|
|
101
182
|
|
|
102
183
|
### From `knex`
|
|
@@ -118,13 +199,15 @@ const users = await knex('users').where('id', 123).select('*');
|
|
|
118
199
|
// After
|
|
119
200
|
const { getCacheQuery } = require('sqlcache');
|
|
120
201
|
|
|
121
|
-
const users = await getCacheQuery('SELECT * FROM users WHERE id = ?', [123]);
|
|
202
|
+
const users = await getCacheQuery('SELECT * FROM users WHERE id = ?', [123], 'user:123');
|
|
122
203
|
```
|
|
123
204
|
|
|
124
205
|
---
|
|
125
206
|
|
|
126
207
|
## Version Migration
|
|
127
208
|
|
|
209
|
+
For v2.9.0 → v2.9.1 see [the top of this guide](#v290--v291).
|
|
210
|
+
|
|
128
211
|
### Package renamed to `sqlcache` (v2.9.0)
|
|
129
212
|
|
|
130
213
|
From v2.9.0 the package is published as `sqlcache`; the API is unchanged.
|
|
@@ -144,7 +227,8 @@ const { getCacheQuery } = require('sqlcache');
|
|
|
144
227
|
Deep imports (`/dbConnector`, `/redis.Connector`, `/core/...`) and TypeScript imports change the same
|
|
145
228
|
way. You do not have to switch right away: `node-caching-mysql-connector-with-redis@2.9.0` only
|
|
146
229
|
forwards to `sqlcache@^2.9.0` (including those deep paths and the types), so `npm update` on the old
|
|
147
|
-
name already runs the same code. The old name is deprecated and gets no new major versions.
|
|
230
|
+
name already runs the same code. The old name is deprecated and gets no new major versions. Deep
|
|
231
|
+
imports have no type declarations of their own; TypeScript code should import from `'sqlcache'`.
|
|
148
232
|
|
|
149
233
|
### v2.8.x → v2.9.0 (performance & resilience)
|
|
150
234
|
|
|
@@ -181,14 +265,17 @@ can be switched back with an environment variable.
|
|
|
181
265
|
they write to (previously no table, or a wrong one).
|
|
182
266
|
- **`bulkInsert` no longer re-inserts committed chunks** when a connection error triggers a retry.
|
|
183
267
|
- **`closeConnections()` no longer hangs the process.** With mysql2 3.3.x the pool's idle reaper
|
|
184
|
-
kept running after `end()` and later spun in a synchronous loop at 100% CPU.
|
|
268
|
+
kept running after `end()` and later spun in a synchronous loop at 100% CPU. It closes only the
|
|
269
|
+
MySQL pool: with Redis enabled, also call `getRedisClient().quit()`, or the open Redis connection
|
|
270
|
+
keeps the process alive.
|
|
185
271
|
- **The Redis client really never stops reconnecting.** node-redis gave up after one hour of retries,
|
|
186
272
|
leaving the cache disabled until a restart.
|
|
187
273
|
- **Invalidations missed during a Redis outage are replayed** when Redis is ready again, instead of
|
|
188
274
|
leaving stale entries for the rest of their TTL.
|
|
189
275
|
- A failed command sent without a callback through `getRedisClient()` no longer marks Redis as down.
|
|
190
276
|
- `REDIS_ENABLED=false` no longer opens a Redis connection (and reconnect loop) at startup; it is
|
|
191
|
-
opened on the first direct Redis call, including `isRedisConnected()
|
|
277
|
+
opened on the first direct Redis call, including `isRedisConnected()` (no longer the case for
|
|
278
|
+
`isRedisConnected()` since v2.9.1).
|
|
192
279
|
- An empty `REDIS_PASSWORD` no longer sends `AUTH ""`.
|
|
193
280
|
|
|
194
281
|
#### Smaller observable differences (no switch)
|
|
@@ -224,6 +311,8 @@ return the same results with less work.
|
|
|
224
311
|
### v2.5.x → v2.6.0
|
|
225
312
|
|
|
226
313
|
#### What's New
|
|
314
|
+
- Automatic cache keys and invalidation, opt-in (`CORE_AUTO_FEATURES`, `CORE_AUTO_INVALIDATION`,
|
|
315
|
+
`configure()`, `enableAutoKey()`, `enableAutoInvalidation()`)
|
|
227
316
|
- Transaction support (`withTransaction`)
|
|
228
317
|
- Enhanced documentation
|
|
229
318
|
- More comprehensive tests
|
|
@@ -241,7 +330,7 @@ return the same results with less work.
|
|
|
241
330
|
2. **No code changes required**
|
|
242
331
|
```javascript
|
|
243
332
|
// Your v2.5.x code works exactly the same
|
|
244
|
-
await getCacheQuery('SELECT * FROM users', [], '
|
|
333
|
+
await getCacheQuery('SELECT * FROM users', [], 'users_all');
|
|
245
334
|
await QuaryCache('INSERT INTO users...', [data], 'users_*');
|
|
246
335
|
```
|
|
247
336
|
|
|
@@ -256,6 +345,7 @@ return the same results with less work.
|
|
|
256
345
|
await connection.commit();
|
|
257
346
|
} catch (error) {
|
|
258
347
|
await connection.rollback();
|
|
348
|
+
throw error;
|
|
259
349
|
} finally {
|
|
260
350
|
connection.release();
|
|
261
351
|
}
|
|
@@ -280,21 +370,27 @@ npm install sqlcache
|
|
|
280
370
|
// Use with manual keys (no changes to existing logic)
|
|
281
371
|
const { getCacheQuery, QuaryCache } = require('sqlcache');
|
|
282
372
|
|
|
283
|
-
await getCacheQuery('SELECT * FROM users', [], '
|
|
284
|
-
await QuaryCache('INSERT INTO users...', [data], 'users_*');
|
|
373
|
+
await getCacheQuery('SELECT * FROM users', [], 'users_all');
|
|
374
|
+
await QuaryCache('INSERT INTO users...', [data], 'users_*'); // clears users_all
|
|
285
375
|
```
|
|
286
376
|
|
|
287
377
|
### Phase 2: Enable Auto Features
|
|
288
378
|
|
|
289
379
|
```javascript
|
|
290
380
|
// .env
|
|
291
|
-
CORE_AUTO_FEATURES=true
|
|
381
|
+
CORE_AUTO_FEATURES=true // automatic keys
|
|
382
|
+
CORE_AUTO_INVALIDATION=true // automatic invalidation (not enabled by CORE_AUTO_FEATURES)
|
|
292
383
|
|
|
293
384
|
// Remove cache keys from new code
|
|
294
|
-
await getCacheQuery('SELECT * FROM users', []); // Auto key ✨
|
|
385
|
+
await getCacheQuery('SELECT * FROM users', []); // Auto key users:all ✨
|
|
295
386
|
|
|
296
387
|
// Old code with manual keys still works
|
|
297
|
-
await getCacheQuery('SELECT * FROM orders', [], '
|
|
388
|
+
await getCacheQuery('SELECT * FROM orders', [], 'orders_all'); // Manual key
|
|
389
|
+
|
|
390
|
+
// But a resetCacheName replaces the automatic patterns, so old writes that clear only
|
|
391
|
+
// 'users_*' leave the new users:... keys stale. Drop the resetCacheName from such writes,
|
|
392
|
+
// or clear both prefixes:
|
|
393
|
+
await QuaryCache('INSERT INTO users...', [data], ['users_', 'users:']);
|
|
298
394
|
```
|
|
299
395
|
|
|
300
396
|
### Phase 3: Adopt Transactions
|
|
@@ -312,14 +408,17 @@ await withTransaction(async (tx) => {
|
|
|
312
408
|
|
|
313
409
|
```javascript
|
|
314
410
|
// Before
|
|
315
|
-
await getCacheQuery('SELECT * FROM users', [], '
|
|
316
|
-
await getCacheQuery('SELECT * FROM posts', [], '
|
|
411
|
+
await getCacheQuery('SELECT * FROM users', [], 'users_all');
|
|
412
|
+
await getCacheQuery('SELECT * FROM posts', [], 'posts_recent');
|
|
317
413
|
|
|
318
414
|
// After (once confident in auto features)
|
|
319
415
|
await getCacheQuery('SELECT * FROM users', []);
|
|
320
416
|
await getCacheQuery('SELECT * FROM posts', []);
|
|
321
417
|
```
|
|
322
418
|
|
|
419
|
+
Keep explicit names where automatic keys would collide, such as aggregates, joins and
|
|
420
|
+
`LIMIT`/`ORDER BY` variants (see the README's caveats).
|
|
421
|
+
|
|
323
422
|
---
|
|
324
423
|
|
|
325
424
|
## Common Migration Scenarios
|
|
@@ -342,7 +441,7 @@ app.get('/users', async (req, res) => {
|
|
|
342
441
|
const { getCacheQuery } = require('sqlcache');
|
|
343
442
|
|
|
344
443
|
app.get('/users', async (req, res) => {
|
|
345
|
-
const users = await getCacheQuery('SELECT * FROM users', []);
|
|
444
|
+
const users = await getCacheQuery('SELECT * FROM users', []); // auto key users:all (CORE_AUTO_FEATURES=true)
|
|
346
445
|
res.json(users);
|
|
347
446
|
});
|
|
348
447
|
```
|
|
@@ -365,11 +464,12 @@ async function processOrders() {
|
|
|
365
464
|
const { QuaryCache } = require('sqlcache');
|
|
366
465
|
|
|
367
466
|
async function processOrders() {
|
|
368
|
-
|
|
467
|
+
// A work queue must see the current rows: QuaryCache reads MySQL without the cache
|
|
468
|
+
const orders = await QuaryCache('SELECT * FROM orders WHERE status = ?', ['pending']);
|
|
369
469
|
|
|
370
470
|
for (const order of orders) {
|
|
371
471
|
await QuaryCache('UPDATE orders SET status = ? WHERE id = ?', ['processing', order.id]);
|
|
372
|
-
// ✅
|
|
472
|
+
// ✅ orders:* and orders_* cleared (CORE_AUTO_INVALIDATION=true)
|
|
373
473
|
}
|
|
374
474
|
}
|
|
375
475
|
```
|
|
@@ -408,7 +508,8 @@ async function createUserWithProfile(userData, profileData) {
|
|
|
408
508
|
const userResult = await tx.query('INSERT INTO users...', [userData]);
|
|
409
509
|
await tx.query('INSERT INTO profiles...', [userResult.insertId, profileData]);
|
|
410
510
|
return userResult.insertId;
|
|
411
|
-
// ✅ Auto commit, auto rollback,
|
|
511
|
+
// ✅ Auto commit, auto rollback, cache invalidation after the commit
|
|
512
|
+
// (automatic with CORE_AUTO_INVALIDATION=true, or pass resetCacheName to tx.query)
|
|
412
513
|
});
|
|
413
514
|
}
|
|
414
515
|
```
|
|
@@ -433,7 +534,7 @@ const { getCacheQuery } = require('sqlcache');
|
|
|
433
534
|
|
|
434
535
|
describe('User API', () => {
|
|
435
536
|
it('should fetch users with cache', async () => {
|
|
436
|
-
const users = await getCacheQuery('SELECT * FROM users', []);
|
|
537
|
+
const users = await getCacheQuery('SELECT * FROM users', [], 'users_all');
|
|
437
538
|
expect(users).to.be.an('array');
|
|
438
539
|
});
|
|
439
540
|
});
|
|
@@ -441,12 +542,16 @@ describe('User API', () => {
|
|
|
441
542
|
|
|
442
543
|
### Integration Tests
|
|
443
544
|
|
|
545
|
+
`REDIS_ENABLED`, like the pool and coalescing settings, is read once, when sqlcache is first
|
|
546
|
+
required. Changing it afterwards has no effect, so set it before the first `require` and run the
|
|
547
|
+
Redis and no-Redis variants as separate test processes:
|
|
548
|
+
|
|
444
549
|
```javascript
|
|
445
|
-
//
|
|
550
|
+
// test/setup-redis.js (one test run)
|
|
446
551
|
process.env.REDIS_ENABLED = 'true';
|
|
447
552
|
process.env.CORE_AUTO_FEATURES = 'true';
|
|
448
553
|
|
|
449
|
-
//
|
|
554
|
+
// test/setup-no-redis.js (another test run): MySQL only, no caching
|
|
450
555
|
process.env.REDIS_ENABLED = 'false';
|
|
451
556
|
```
|
|
452
557
|
|
|
@@ -459,11 +564,12 @@ If you need to rollback:
|
|
|
459
564
|
1. **Disable auto features**
|
|
460
565
|
```bash
|
|
461
566
|
CORE_AUTO_FEATURES=false
|
|
567
|
+
CORE_AUTO_INVALIDATION=false
|
|
462
568
|
```
|
|
463
569
|
|
|
464
570
|
2. **Add manual keys back**
|
|
465
571
|
```javascript
|
|
466
|
-
await getCacheQuery('SELECT * FROM users', [], '
|
|
572
|
+
await getCacheQuery('SELECT * FROM users', [], 'users_all');
|
|
467
573
|
await QuaryCache('INSERT INTO users...', [data], 'users_*');
|
|
468
574
|
```
|
|
469
575
|
|
|
@@ -478,23 +584,17 @@ If you need to rollback:
|
|
|
478
584
|
|
|
479
585
|
### Before Migration
|
|
480
586
|
|
|
481
|
-
- Manual cache management: ~50ms per request
|
|
482
587
|
- Risk of stale data due to missed invalidations
|
|
483
588
|
- Complex code maintenance
|
|
484
589
|
|
|
485
590
|
### After Migration
|
|
486
591
|
|
|
487
|
-
-
|
|
488
|
-
|
|
489
|
-
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
```javascript
|
|
494
|
-
// Manual approach: 10 lines, 50ms
|
|
495
|
-
// Auto approach: 2 lines, 52ms
|
|
496
|
-
// Trade-off: +2ms for huge DX improvement ✅
|
|
497
|
-
```
|
|
592
|
+
- Cache hits are served from Redis; a miss adds a Redis read to the query, and a Redis write when
|
|
593
|
+
the result is not empty
|
|
594
|
+
- Invalidation is automatic for writes made through sqlcache to the first table of each statement;
|
|
595
|
+
joins, aggregates, `CALL`, writes from other applications and the cases listed in the README still
|
|
596
|
+
need explicit cache names or `resetCacheName`
|
|
597
|
+
- Simpler code
|
|
498
598
|
|
|
499
599
|
---
|
|
500
600
|
|
|
@@ -502,7 +602,6 @@ If you need to rollback:
|
|
|
502
602
|
|
|
503
603
|
- **Documentation**: [API Reference](API.md) | [Examples](EXAMPLES.md)
|
|
504
604
|
- **Issues**: [GitHub Issues](https://github.com/hayatialikeles/sqlcache/issues)
|
|
505
|
-
- **Community**: [Discussions](https://github.com/hayatialikeles/sqlcache/discussions)
|
|
506
605
|
|
|
507
606
|
---
|
|
508
607
|
|