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/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 –
|
|
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
|
|
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
|
-
|
|
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** –
|
|
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
|
-
* ✅
|
|
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
|
|
150
|
-
QuaryCache(sql, params
|
|
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
|
|
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', [], '
|
|
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
|
|
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()`
|
|
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](
|
|
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
|
|
335
|
+
MIT © [Hayati Ali Keleş](https://github.com/hayatialikeles)
|
|
288
336
|
|
|
289
337
|
---
|
|
290
338
|
|
package/docs/USE-CASE-BULK.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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) =>
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
73
|
-
|
|
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
|
```
|
package/docs/USE-CASE-SAAS.md
CHANGED
|
@@ -5,58 +5,67 @@
|
|
|
5
5
|
```javascript
|
|
6
6
|
const { getCacheQuery, QuaryCache } = require('sqlcache');
|
|
7
7
|
|
|
8
|
-
// Middleware:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 = [
|
|
64
|
+
const tenants = [...TENANT_DATABASES];
|
|
56
65
|
|
|
57
66
|
const results = await Promise.all(
|
|
58
67
|
tenants.map(db =>
|
|
59
|
-
getCacheQuery('SELECT * FROM users', [],
|
|
68
|
+
getCacheQuery('SELECT * FROM users', [], `${db}:users:all`, db)
|
|
60
69
|
)
|
|
61
70
|
);
|
|
62
71
|
|