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/docs/README.tr.md CHANGED
@@ -26,7 +26,7 @@ if (!cachedUsers) {
26
26
  await db.query('UPDATE users SET status = ? WHERE id = ?', ['inactive', 123]);
27
27
  await redis.del('users-active'); // Unutmak çok kolay!
28
28
 
29
- // ✅ Bu paketle – Sıfır konfigürasyon
29
+ // ✅ Bu paketle – CORE_AUTO_FEATURES=true ve CORE_AUTO_INVALIDATION=true ile
30
30
  const users = await getCacheQuery('SELECT * FROM users WHERE status = ?', ['active']);
31
31
  await QuaryCache('UPDATE users SET status = ? WHERE id = ?', ['inactive', 123]);
32
32
  // Cache otomatik temizlenir! ✨
@@ -42,6 +42,13 @@ await QuaryCache('UPDATE users SET status = ? WHERE id = ?', ['inactive', 123]);
42
42
  npm install sqlcache
43
43
  ```
44
44
 
45
+ ### Gereksinimler
46
+
47
+ - Node.js 14 veya üzeri (Node 18–26 üzerinde test edildi)
48
+ - mysql2 3.23.1 veya üzeri (bağımlılık olarak kurulur; eski sürümlerde kritik olanlar dahil güvenlik
49
+ açıkları var)
50
+ - MySQL 5.7 veya 8.x; Redis 2.8 veya üzeri (opsiyonel, bkz. `REDIS_ENABLED`)
51
+
45
52
  ### Temel Kullanım
46
53
 
47
54
  ```bash
@@ -52,9 +59,15 @@ DB_PASSWORD=secret
52
59
  DB_NAME=mydb
53
60
  REDIS_SERVER=localhost
54
61
  REDIS_PORT=6379
55
- CORE_AUTO_FEATURES=true # Akıllı özellikleri etkinleştir 🎯
62
+ CORE_AUTO_FEATURES=true # Otomatik cache anahtarları 🎯
63
+ CORE_AUTO_INVALIDATION=true # Yazmalardan sonra otomatik cache temizleme
56
64
  ```
57
65
 
66
+ `CORE_AUTO_FEATURES` yalnızca otomatik cache anahtarlarını açar; otomatik temizleme için
67
+ `CORE_AUTO_INVALIDATION=true` gerekir. Paket yalnızca çalışma dizinindeki `.env` dosyasını okur
68
+ (`${VAR}` ifadeleri açılmaz); tüm ayarların açıklamalı şablonu `node_modules/sqlcache/.env.example`
69
+ dosyasındadır.
70
+
58
71
  ```javascript
59
72
  const { getCacheQuery, QuaryCache, withTransaction } = require('sqlcache');
60
73
 
@@ -72,7 +85,10 @@ await withTransaction(async (tx) => {
72
85
  ```
73
86
 
74
87
  **Hepsi bu kadar!**
75
- Manuel cache key, manuel invalidation veya transaction yönetimi gerekmez.
88
+ Basit sorgular için manuel cache key veya manuel invalidation, transaction için de manuel
89
+ BEGIN/COMMIT gerekmez. Join, aggregate (`COUNT(*)` vb.), `LIMIT`/`ORDER BY` varyantları ve kullanıcıya
90
+ özel veya hassas veriler için açık cache adı verin: otomatik anahtar yalnızca tabloyu, `WHERE`
91
+ sütunlarını ve parametrelerin 32 bitlik özetini içerir.
76
92
 
77
93
  ---
78
94
 
@@ -92,9 +108,10 @@ Manuel cache key, manuel invalidation veya transaction yönetimi gerekmez.
92
108
  * ✅ **Bulk İşlemler** – 100K+ kayıt için otomatik parçalara bölme
93
109
  * ✅ **Timeout Koruması** – Uzun süren sorguların sistemi bloklamasını önler
94
110
  * ✅ **Connection Pool İzleme** – Aktif bağlantı sayısı ve durum takibi
95
- * ✅ **Graceful Shutdown** – Kapanışta temiz bağlantı kapatma
111
+ * ✅ **Graceful Shutdown** – `closeConnections()` MySQL havuzunu kapatır; Redis açıksa ardından
112
+ `getRedisClient().quit()` çağırın, yoksa açık Redis bağlantısı süreci ayakta tutar
96
113
  * ✅ **TypeScript Desteği** – Tam IntelliSense ve tip tanımları
97
- * ✅ **%100 Geriye Dönük Uyumluluk** – Hiçbir breaking change yok
114
+ * ✅ **API Uyumluluğu** – API değişmedi; sürümler arası davranış farkları [MIGRATION.md](MIGRATION.md) dosyasında
98
115
 
99
116
  ---
100
117
 
@@ -136,7 +153,7 @@ const posts = await getCacheQuery(
136
153
  [userId]
137
154
  );
138
155
 
139
- // Yeni gönderi oluştur – cache otomatik temizlenir
156
+ // Yeni gönderi oluştur – posts:* cache'i otomatik temizlenir (CORE_AUTO_INVALIDATION=true)
140
157
  await QuaryCache('INSERT INTO posts (user_id, content) VALUES (?, ?)', [userId, content]);
141
158
  ```
142
159
 
@@ -146,14 +163,14 @@ await QuaryCache('INSERT INTO posts (user_id, content) VALUES (?, ?)', [userId,
146
163
 
147
164
  ```javascript
148
165
  // Ana fonksiyonlar
149
- getCacheQuery(sql, params, cacheName?, database?)
150
- QuaryCache(sql, params, resetCacheName?, database?)
166
+ getCacheQuery(sql, params?, cacheName?, database?)
167
+ QuaryCache(sql, params?, resetCacheName?, database?) // cache okumaz; başarılı yazmadan sonra prefix ile temizler
151
168
  withTransaction(callback, options?)
152
169
 
153
170
  // Yardımcılar
154
171
  bulkInsert(table, records, options?)
155
- getCacheQueryWithTimeout(sql, params, cacheName, options?)
156
- getCacheQueryPagination(sql, params, cacheName, page, pageSize?, database?)
172
+ getCacheQueryWithTimeout(sql, params?, cacheName?, options?)
173
+ getCacheQueryPagination(sql, params, cacheName, page, pageSize?, database?) // page 0'dan başlar → { totalCount, pageCount, detail }
157
174
  closeConnections()
158
175
  getPoolStats()
159
176
 
@@ -162,6 +179,13 @@ getArrayItem(key)
162
179
  addArrayItem(key, data, expiry?)
163
180
  delKeyItem(keys)
164
181
  delPrefixKeyItem(patterns)
182
+ isRedisConnected()
183
+ getRedisClient()
184
+
185
+ // Otomatik özellikler
186
+ configure({ autoKey, autoInvalidation })
187
+ enableAutoKey(config?)
188
+ enableAutoInvalidation(config?)
165
189
  ```
166
190
 
167
191
  📚 **[Detaylı API Dokümantasyonu →](API.md)**
@@ -175,28 +199,31 @@ delPrefixKeyItem(patterns)
175
199
  ```bash
176
200
  DB_HOST=localhost
177
201
  DB_USERNAME=root
178
- DB_PASSWORD=secret
179
202
  DB_NAME=mydb
180
203
 
181
- REDIS_SERVER=localhost
182
- REDIS_PORT=6379
204
+ REDIS_SERVER=localhost # REDIS_ENABLED=false değilse zorunlu
183
205
  ```
184
206
 
185
207
  ### Opsiyonel Akıllı Özellikler
186
208
 
187
209
  ```bash
188
- CORE_AUTO_FEATURES=true
210
+ CORE_AUTO_FEATURES=true # Otomatik cache anahtarları (temizlemeyi açmaz)
211
+ CORE_AUTO_INVALIDATION=true # Yazmalardan sonra otomatik cache temizleme
189
212
  ```
190
213
 
191
214
  ### Gelişmiş Ayarlar
192
215
 
193
216
  ```bash
217
+ DB_PASSWORD=secret # Varsayılan: boş
194
218
  DB_PORT=3306
195
- DB_CONNECTION_LIMIT=10
219
+ DB_CONNECTION_LIMIT=10 # Süreç başına (varsayılan: 151); örnek × worker × limit + diğer istemciler, MySQL max_connections (varsayılan 151) altında kalmalı
196
220
  DB_QUEUE_LIMIT=0
197
221
  DB_CONNECT_TIMEOUT=10000
222
+ DB_MULTIPLE_STATEMENTS=false # Varsayılan: true (geriye uyumluluk); yeni projelerde false önerilir
198
223
  TIMEZONE=+00:00
224
+ REDIS_PORT=6379
199
225
  REDIS_PASSWORD=secret
226
+ REDIS_VHOST=my_app # Tüm anahtarların öneki; süreç başına bir kez ayarlayın, istek başına değiştirmeyin
200
227
  REDIS_ENABLED=true
201
228
  ```
202
229
 
@@ -204,6 +231,7 @@ REDIS_ENABLED=true
204
231
 
205
232
  ```bash
206
233
  DB_COALESCE_QUERIES=true # Aynı anda gelen aynı cache-miss'ler tek sorguyu paylaşır (varsayılan: true)
234
+ DB_COALESCE_MAX_WAIT=10000 # Paylaşılan sorgu için en fazla bekleme, sonra yeni sorgu başlar, ms (varsayılan: 10000, v2.9.1)
207
235
  DB_PAGINATION_REUSE_TOTAL=false # Sayfalama toplamını aynı sorgunun diğer sayfalarında yeniden kullan (varsayılan: false)
208
236
  REDIS_WAIT_TIMEOUT=10000 # Redis'in (yeniden) bağlanması için en fazla bekleme, ms (varsayılan: 10000)
209
237
  REDIS_FAIL_FAST=true # Bu süre kadar kesintiden sonra cache okuma/yazma beklemeden devam eder (varsayılan: true)
@@ -214,6 +242,9 @@ REDIS_SCAN_COUNT=1000 # Prefix silmede SCAN başına incelenen anaht
214
242
 
215
243
  v2.8.x davranışına dönmek ve değişikliklerin listesi için: [MIGRATION.md](MIGRATION.md#v28x--v290-performance--resilience)
216
244
 
245
+ v2.9.1 değişiklikleri (Node.js 14+, mysql2 3.23.1+, ESM/TypeScript import düzeltmeleri, cache
246
+ ayrıntıları): [MIGRATION.md](MIGRATION.md#v290--v291)
247
+
217
248
  ---
218
249
 
219
250
  ## 🔄 Geriye Dönük Uyumluluk
@@ -221,34 +252,36 @@ v2.8.x davranışına dönmek ve değişikliklerin listesi için: [MIGRATION.md]
221
252
  v2.5.3 kodunuz **hiçbir değişiklik olmadan** çalışmaya devam eder:
222
253
 
223
254
  ```javascript
224
- await getCacheQuery('SELECT * FROM users', [], 'my-key');
255
+ await getCacheQuery('SELECT * FROM users', [], 'users_all');
225
256
  await QuaryCache('INSERT INTO users...', [data], 'users_*');
226
257
  ```
227
258
 
228
- Yeni sürümde `CORE_AUTO_FEATURES=true` yaparsanız:
259
+ Yeni sürümde `CORE_AUTO_FEATURES=true` ve `CORE_AUTO_INVALIDATION=true` yaparsanız:
229
260
 
230
261
  ```javascript
231
- await getCacheQuery('SELECT * FROM users', []); // Otomatik cache key ✨
232
- await QuaryCache('INSERT INTO users...', [data]); // Otomatik invalidation ✨
262
+ await getCacheQuery('SELECT * FROM users', []); // Otomatik cache key (users:all) ✨
263
+ await QuaryCache('INSERT INTO users...', [data]); // Otomatik invalidation (users:* ve users_*) ✨
233
264
  ```
234
265
 
266
+ Bir yazmaya `resetCacheName` verirseniz o çağrıda otomatik desenlerin yerine geçer: yalnızca
267
+ `'users_*'` temizleyen eski yazmalar yeni `users:...` anahtarlarını silmez.
268
+
235
269
  ---
236
270
 
237
271
  ## 🧪 Test
238
272
 
239
273
  ```bash
274
+ npm ci
240
275
  npm test
241
276
  npm run coverage
242
277
  ```
243
278
 
244
- **Kapsama:** %98.07 | **Test:** 215 geçiyor ✅
245
-
246
279
  ---
247
280
 
248
281
  ## 📝 TypeScript Desteği
249
282
 
250
283
  ```typescript
251
- import { getCacheQuery, withTransaction, TransactionContext } from 'sqlcache';
284
+ import { getCacheQuery, withTransaction, type TransactionContext } from 'sqlcache';
252
285
 
253
286
  interface User {
254
287
  id: number;
@@ -256,7 +289,7 @@ interface User {
256
289
  email: string;
257
290
  }
258
291
 
259
- const users = await getCacheQuery<User[]>('SELECT * FROM users WHERE id = ?', [123]);
292
+ const users = await getCacheQuery<User>('SELECT * FROM users WHERE id = ?', [123], 'user:123'); // User[]
260
293
 
261
294
  await withTransaction(async (tx: TransactionContext) => {
262
295
  const result = await tx.query<{ insertId: number }>('INSERT INTO users...', [data]);
@@ -264,27 +297,42 @@ await withTransaction(async (tx: TransactionContext) => {
264
297
  });
265
298
  ```
266
299
 
300
+ v2.9.1'den itibaren isimli import'lar native ESM'de de çalışır (`import { getCacheQuery } from
301
+ 'sqlcache'`) ve `import sqlcache from 'sqlcache'` için `esModuleInterop` gerekmez. 2.9.0 ve öncesinde
302
+ native ESM'de varsayılan import'u (`import sqlcache from 'sqlcache'`) kullanın.
303
+
267
304
  ---
268
305
 
269
306
  ## ⚠️ Önemli Notlar
270
307
 
271
308
  * Okuma ağırlıklı sistemler için idealdir
272
- * Varsayılan TTL: 40.000 saniye (~11 saat)
309
+ * Varsayılan TTL: 40.000 saniye (~11 saat); çağrı başına TTL yoktur
310
+ * Cache isabetleri JSON'dan döner: `Date` ISO metne, `Buffer` `{ type, data }` nesnesine dönüşür;
311
+ cache'te bulunmayıp MySQL'den okunan sonuçlar ise mysql2 tiplerini taşır
273
312
  * `REDIS_ENABLED=false` ile cache devre dışı bırakılabilir
274
- * Transaction içinde `tx.query()` kullanarak veri bütünlüğü sağlanır
313
+ * Transaction içinde kararı etkileyen okumalar için `tx.query()` kullanın: `tx.getCacheQuery()` önce
314
+ cache'e bakar (v2.9.1'den itibaren okuduğunu yalnızca commit'ten sonra cache'e yazar)
315
+ * `bulkInsert` tablo adını ve ilk kaydın anahtarlarını (sütun adları) SQL'e olduğu gibi koyar:
316
+ istek veya CSV verisini sabit bir sütun listesine eşleyin
317
+ * Çok kiracılı uygulamalarda kiracıyı oturumdan alıp izin listesiyle doğrulayın, `database`
318
+ parametresini çağrı başına verin; `REDIS_VHOST` değerini istek başına değiştirmeyin
319
+ * Redis güvenilen bir depodur: AUTH ve özel ağ kullanın. `resetCacheName` / `delPrefixKeyItem`
320
+ önekleri Redis glob desenidir; kullanıcı girdisindeki `* ? [ ] \` karakterlerini kaçırın
321
+ * mysql2 hataları parametre değerleri doldurulmuş `err.sql` içerir; hata nesnelerini güvenmediğiniz
322
+ log'lara veya istemcilere göndermeyin
275
323
 
276
324
  ---
277
325
 
278
326
  ## 🤝 Katkıda Bulunma
279
327
 
280
328
  Katkılar memnuniyetle karşılanır!
281
- Ayrıntılar için [CONTRIBUTING.md](../CONTRIBUTING.md) dosyasına göz atın.
329
+ Ayrıntılar için [CONTRIBUTING.md](CONTRIBUTING.md) dosyasına göz atın.
282
330
 
283
331
  ---
284
332
 
285
333
  ## 📄 Lisans
286
334
 
287
- MIT © [Ali Hayati Keleş](https://github.com/hayatialikeles)
335
+ MIT © [Hayati Ali Keleş](https://github.com/hayatialikeles)
288
336
 
289
337
  ---
290
338
 
@@ -1,16 +1,29 @@
1
1
  # Bulk Operations Use Case
2
2
 
3
+ `bulkInsert` takes its columns from the keys of the first record and puts the table and column
4
+ names into the SQL as-is (since v2.9.1 an invalid name is rejected with a `TypeError` before anything
5
+ is sent). Never pass untrusted keys: map request or CSV input onto a fixed list of columns, as below.
6
+
7
+ Without `transaction: true`, each chunk commits on its own and a connection error retries from the
8
+ first unacknowledged chunk, so a chunk can be inserted twice. With `transaction: true`, all chunks
9
+ commit together; an error from `COMMIT` is not retried and carries `err.commitOutcomeUnknown = true`.
10
+
3
11
  ## Import Large Dataset
4
12
 
5
13
  ```javascript
6
14
  const { bulkInsert } = require('sqlcache');
7
15
 
8
16
  app.post('/import/products', async (req, res) => {
9
- const products = req.body.products; // 100K+ records
17
+ // Copy only the known columns: bulkInsert uses the first record's keys as column names
18
+ const products = req.body.products.map(p => ({ // 100K+ records
19
+ sku: String(p.sku),
20
+ name: String(p.name),
21
+ price: Number(p.price)
22
+ }));
10
23
 
11
24
  const result = await bulkInsert('products', products, {
12
25
  chunkSize: 1000, // Insert 1000 at a time
13
- resetCacheName: 'products' // Clear product cache after
26
+ resetCacheName: 'products' // Clear product cache after (every key starting with "products")
14
27
  });
15
28
 
16
29
  res.json({
@@ -33,12 +46,17 @@ setInterval(async () => {
33
46
  const events = eventBuffer;
34
47
  eventBuffer = [];
35
48
 
36
- await bulkInsert('analytics_events', events, {
37
- chunkSize: 5000
38
- });
49
+ try {
50
+ await bulkInsert('analytics_events', events, {
51
+ chunkSize: 5000
52
+ });
53
+ } catch (err) {
54
+ // An unhandled rejection would stop the process; log without err.sql (it holds the values)
55
+ console.error('Event flush failed:', err.code, err.message);
56
+ }
39
57
  }, 10000);
40
58
 
41
- // Track event
59
+ // Track event (the record has fixed keys; only the values come from the request)
42
60
  app.post('/track', (req, res) => {
43
61
  eventBuffer.push({
44
62
  event_type: req.body.type,
@@ -56,17 +74,29 @@ app.post('/track', (req, res) => {
56
74
  const csv = require('csv-parser');
57
75
  const fs = require('fs');
58
76
 
77
+ // The only columns written. The keys of a parsed CSV row come from the uploaded file's
78
+ // header line, so they must never reach bulkInsert as they are.
79
+ const IMPORT_COLUMNS = ['name', 'email', 'city'];
80
+
59
81
  app.post('/import/csv', async (req, res) => {
60
82
  const records = [];
61
83
 
62
84
  fs.createReadStream(req.file.path)
63
85
  .pipe(csv())
64
- .on('data', (row) => records.push(row))
86
+ .on('data', (row) => {
87
+ const record = {};
88
+ for (const column of IMPORT_COLUMNS) record[column] = row[column] ?? null;
89
+ records.push(record);
90
+ })
65
91
  .on('end', async () => {
66
- const result = await bulkInsert('imports', records, {
67
- chunkSize: 2000
68
- });
69
- res.json(result);
92
+ try {
93
+ const result = await bulkInsert('imports', records, {
94
+ chunkSize: 2000
95
+ });
96
+ res.json(result);
97
+ } catch (err) {
98
+ res.status(500).json({ error: 'Import failed' });
99
+ }
70
100
  });
71
101
  });
72
102
  ```
@@ -80,11 +110,12 @@ app.post('/import/csv', async (req, res) => {
80
110
  | 100K+ | 5000 | High |
81
111
 
82
112
  ```javascript
83
- // For very large imports, process in streams
84
- const chunkSize = 5000;
85
- for (let i = 0; i < records.length; i += chunkSize) {
86
- const chunk = records.slice(i, i + chunkSize);
87
- await bulkInsert('table', chunk);
88
- console.log(`Processed ${i + chunk.length}/${records.length}`);
113
+ // For very large imports, insert in batches to report progress
114
+ // (each bulkInsert call is retried and invalidated on its own)
115
+ const batchSize = 5000;
116
+ for (let i = 0; i < records.length; i += batchSize) {
117
+ const batch = records.slice(i, i + batchSize);
118
+ await bulkInsert('table', batch);
119
+ console.log(`Processed ${i + batch.length}/${records.length}`);
89
120
  }
90
121
  ```
@@ -1,5 +1,8 @@
1
1
  # E-Commerce Use Case
2
2
 
3
+ The writes below rely on automatic invalidation, so set `CORE_AUTO_INVALIDATION=true` (it is not
4
+ enabled by `CORE_AUTO_FEATURES`, which only generates cache keys).
5
+
3
6
  ## Order Creation with Transaction
4
7
 
5
8
  ```javascript
@@ -42,7 +45,9 @@ const { getCacheQuery } = require('sqlcache');
42
45
 
43
46
  app.get('/products', async (req, res) => {
44
47
  const products = await getCacheQuery(
45
- 'SELECT * FROM products WHERE active = 1 ORDER BY created_at DESC'
48
+ 'SELECT * FROM products WHERE active = 1 ORDER BY created_at DESC',
49
+ [],
50
+ 'products:active' // under products:, so product writes clear it
46
51
  );
47
52
  res.json(products);
48
53
  });
@@ -64,4 +69,5 @@ app.post('/cart/add', async (req, res) => {
64
69
 
65
70
  - **Transaction**: Order + items + stock update all succeed or all fail
66
71
  - **Cache**: Product listings served from Redis
67
- - **Auto-invalidation**: Cart/product changes clear related cache
72
+ - **Auto-invalidation** (`CORE_AUTO_INVALIDATION=true`): writes to `cart` and `products` clear
73
+ `cart:*`/`cart_*` and `products:*`/`products_*`; inside the transaction, after the commit
@@ -1,9 +1,14 @@
1
1
  # Real-Time Apps Use Case
2
2
 
3
+ These examples omit cache names and invalidation prefixes, so they need both
4
+ `CORE_AUTO_FEATURES=true` (automatic keys) and `CORE_AUTO_INVALIDATION=true` (automatic
5
+ invalidation). Without the second, new messages would not show up until the cached list expires
6
+ (40000 s).
7
+
3
8
  ## Chat Messages
4
9
 
5
10
  ```javascript
6
- const { getCacheQuery, QuaryCache } = require('sqlcache');
11
+ const { getCacheQuery, QuaryCache, getArrayItem, addArrayItem } = require('sqlcache');
7
12
 
8
13
  // Get messages (cached)
9
14
  app.get('/chat/:roomId/messages', async (req, res) => {
@@ -58,7 +63,8 @@ app.post('/notifications/:id/read', async (req, res) => {
58
63
  ## Online Status
59
64
 
60
65
  ```javascript
61
- // Update last seen
66
+ // Update last seen. With automatic invalidation every heartbeat clears all users:* and users_*
67
+ // keys; if you cache other user queries, keep presence in a table of its own.
62
68
  app.post('/heartbeat', async (req, res) => {
63
69
  await QuaryCache(
64
70
  'UPDATE users SET last_seen = NOW() WHERE id = ?',
@@ -67,11 +73,17 @@ app.post('/heartbeat', async (req, res) => {
67
73
  res.json({ ok: true });
68
74
  });
69
75
 
70
- // Get online users (cached 30s)
76
+ // Get online users, cached for 30 s. getCacheQuery has no per-call TTL (it caches for 40000 s),
77
+ // so read with QuaryCache, which does not use the cache, and cache the rows by hand.
71
78
  app.get('/online', async (req, res) => {
72
- const users = await getCacheQuery(
73
- 'SELECT id, name FROM users WHERE last_seen > DATE_SUB(NOW(), INTERVAL 5 MINUTE)'
74
- );
79
+ let users = await getArrayItem('online-users');
80
+ if (users.length === 0) {
81
+ users = await QuaryCache(
82
+ 'SELECT id, name FROM users WHERE last_seen > DATE_SUB(NOW(), INTERVAL 5 MINUTE)',
83
+ []
84
+ );
85
+ await addArrayItem('online-users', users, 30); // TTL in seconds
86
+ }
75
87
  res.json(users);
76
88
  });
77
89
  ```
@@ -5,58 +5,67 @@
5
5
  ```javascript
6
6
  const { getCacheQuery, QuaryCache } = require('sqlcache');
7
7
 
8
- // Middleware: Extract tenant
8
+ // Middleware: take the tenant from the authenticated session (set by your auth middleware)
9
+ // and check it against an allowlist. Never build the database name from a raw request header.
10
+ const TENANT_DATABASES = new Set(['tenant_1', 'tenant_2', 'tenant_3']);
11
+
9
12
  app.use((req, res, next) => {
10
- req.tenantDb = `tenant_${req.headers['x-tenant-id']}`;
13
+ const tenantDb = req.user && `tenant_${req.user.tenantId}`;
14
+ if (!tenantDb || !TENANT_DATABASES.has(tenantDb)) {
15
+ return res.status(403).json({ error: 'Unknown tenant' });
16
+ }
17
+ req.tenantDb = tenantDb;
11
18
  next();
12
19
  });
13
20
 
14
- // Read from tenant database
21
+ // Read from tenant database, cached under a tenant-prefixed name
15
22
  app.get('/users', async (req, res) => {
16
23
  const users = await getCacheQuery(
17
24
  'SELECT * FROM users WHERE active = 1',
18
25
  [],
19
- null,
26
+ `${req.tenantDb}:users:active`,
20
27
  req.tenantDb // 4th param = database
21
28
  );
22
29
  res.json(users);
23
30
  });
24
31
 
25
- // Write to tenant database
32
+ // Write to tenant database and clear only this tenant's user caches
26
33
  app.post('/users', async (req, res) => {
27
34
  await QuaryCache(
28
35
  'INSERT INTO users (name, email) VALUES (?, ?)',
29
36
  [req.body.name, req.body.email],
30
- null,
37
+ `${req.tenantDb}:users`,
31
38
  req.tenantDb
32
39
  );
33
40
  res.json({ success: true });
34
41
  });
35
42
  ```
36
43
 
44
+ With automatic keys (`null` as the cache name and `CORE_AUTO_FEATURES=true`), keys end with
45
+ `@<database>`, for example `users:active:all@tenant_1`, so tenants never share an entry; automatic
46
+ invalidation then clears `users:*` of every tenant.
47
+
37
48
  ## Namespace per Tenant
38
49
 
39
50
  ```bash
40
- # .env
51
+ # .env, only when each tenant runs in its own process
41
52
  REDIS_VHOST=tenant_123
42
53
  ```
43
54
 
44
- Or dynamically:
45
-
46
- ```javascript
47
- // Each tenant gets isolated cache namespace
48
- process.env.REDIS_VHOST = `tenant_${tenantId}`;
49
- ```
55
+ `REDIS_VHOST` applies to the whole process. Never change it per request
56
+ (`process.env.REDIS_VHOST = ...`): concurrent requests share it, so one tenant's request can read or
57
+ write another tenant's entries. v2.9.1 logs a warning when it changes at runtime. Within one process,
58
+ use the `database` argument and tenant-prefixed cache names as shown above.
50
59
 
51
60
  ## Cross-Tenant Admin Query
52
61
 
53
62
  ```javascript
54
63
  app.get('/admin/all-users', async (req, res) => {
55
- const tenants = ['tenant_1', 'tenant_2', 'tenant_3'];
64
+ const tenants = [...TENANT_DATABASES];
56
65
 
57
66
  const results = await Promise.all(
58
67
  tenants.map(db =>
59
- getCacheQuery('SELECT * FROM users', [], null, db)
68
+ getCacheQuery('SELECT * FROM users', [], `${db}:users:all`, db)
60
69
  )
61
70
  );
62
71