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/API.md CHANGED
@@ -21,6 +21,7 @@ Complete API documentation for all functions.
21
21
  - [addArrayItem](#addarrayitem)
22
22
  - [delKeyItem](#delkeyitem)
23
23
  - [delPrefixKeyItem](#delprefixkeyitem)
24
+ - [isRedisConnected](#isredisconnected)
24
25
  - [getRedisClient](#getredisclient)
25
26
  - [Configuration](#configuration)
26
27
  - [configure](#configure)
@@ -40,7 +41,7 @@ Execute a SELECT query with automatic Redis caching.
40
41
  ```typescript
41
42
  getCacheQuery<T = any>(
42
43
  sql: string,
43
- parameters: any[],
44
+ parameters?: any[],
44
45
  cacheName?: string | null,
45
46
  database?: string | null
46
47
  ): Promise<T[]>
@@ -49,14 +50,28 @@ getCacheQuery<T = any>(
49
50
  #### Parameters
50
51
 
51
52
  - `sql` (string): SQL SELECT query
52
- - `parameters` (any[]): Query parameters (use `?` placeholders)
53
+ - `parameters` (any[], optional): Query parameters (use `?` placeholders)
53
54
  - `cacheName` (string | null, optional): Cache key. Auto-generated if `CORE_AUTO_FEATURES=true`
55
+ (or `enableAutoKey()`); otherwise the call throws `cacheName is required...`
54
56
  - `database` (string | null, optional): Database name to switch to
55
57
 
56
58
  #### Returns
57
59
 
58
60
  Promise resolving to array of query results
59
61
 
62
+ #### Notes
63
+
64
+ - Results are cached for 40000 s. Empty results are not served from the cache, and since v2.9.1 not
65
+ written to it; a cached value that is not a non-empty array is treated as a miss.
66
+ - A cache hit is a JSON round trip: a `Date` comes back as an ISO string and a `Buffer` as
67
+ `{ type, data }`, while a miss returns mysql2's types.
68
+ - Concurrent misses for the same key, database, SQL and parameters share one query
69
+ (`DB_COALESCE_QUERIES`). Since v2.9.1 a caller that has waited `DB_COALESCE_MAX_WAIT` ms (default
70
+ 10000) starts its own query, but still takes the shared result if that arrives first; an
71
+ `ER_QUERY_TIMEOUT` error of the shared query (`MAX_EXECUTION_TIME`) goes to every caller waiting on it.
72
+ - Since v2.9.1 a read that started before an invalidation in the same process does not write its
73
+ possibly stale result to the cache. Across processes a short window remains.
74
+
60
75
  #### Examples
61
76
 
62
77
  ```javascript
@@ -90,7 +105,8 @@ interface User {
90
105
 
91
106
  const users = await getCacheQuery<User>(
92
107
  'SELECT * FROM users WHERE id = ?',
93
- [123]
108
+ [123],
109
+ 'user:123'
94
110
  );
95
111
  ```
96
112
 
@@ -105,43 +121,56 @@ try {
105
121
  } else if (error.message.includes('cacheName is required')) {
106
122
  console.error('Auto key disabled, provide cacheName');
107
123
  } else {
108
- console.error('Query failed:', error);
124
+ console.error('Query failed:', error.code, error.message);
109
125
  }
110
126
  }
111
127
  ```
112
128
 
129
+ mysql2 errors carry `err.sql` with the parameter values filled in, so do not send whole error objects
130
+ to logs or clients you do not trust.
131
+
113
132
  ---
114
133
 
115
134
  ### QuaryCache
116
135
 
117
- Execute INSERT/UPDATE/DELETE queries with automatic cache invalidation.
136
+ Run a statement on MySQL without reading the cache, then invalidate cached keys. Use it for
137
+ INSERT/UPDATE/DELETE, and for reads that must not be cached.
118
138
 
119
139
  #### Signature
120
140
 
121
141
  ```typescript
122
- QuaryCache(
142
+ QuaryCache<T = QueryResult>(
123
143
  sql: string,
124
- parameters: any[],
144
+ parameters?: any[],
125
145
  resetCacheName?: string | string[] | null,
126
146
  database?: string | null
127
- ): Promise<any>
147
+ ): Promise<T>
128
148
  ```
129
149
 
130
150
  #### Parameters
131
151
 
132
- - `sql` (string): SQL query (INSERT/UPDATE/DELETE)
133
- - `parameters` (any[]): Query parameters
134
- - `resetCacheName` (string | string[] | null, optional): Cache patterns to invalidate. Auto-detected if `CORE_AUTO_FEATURES=true`
152
+ - `sql` (string): SQL statement
153
+ - `parameters` (any[], optional): Query parameters
154
+ - `resetCacheName` (string | string[] | null, optional): Prefixes whose keys are deleted after the
155
+ statement succeeds. Without it, automatic invalidation applies if `CORE_AUTO_INVALIDATION=true`
156
+ (or `enableAutoInvalidation()`); a `resetCacheName` replaces the automatic patterns for the call
135
157
  - `database` (string | null, optional): Database name to switch to
136
158
 
159
+ Prefixes are Redis glob patterns: escape `*`, `?`, `[`, `]` and `\` in any user-supplied part. An
160
+ empty prefix without `REDIS_VHOST` deletes every key in that Redis database.
161
+
137
162
  #### Returns
138
163
 
139
- Promise resolving to query result (insertId, affectedRows, etc.)
164
+ Promise resolving to the mysql2 result: rows for a `SELECT`, or `{ insertId, affectedRows, ... }`
165
+ for a write. In TypeScript, pass the type for reads: `QuaryCache<User[]>('SELECT ...', [id])`.
166
+
167
+ The statement is retried after connection errors (see the README's Failure handling), so a write
168
+ can run twice: make writes idempotent.
140
169
 
141
170
  #### Examples
142
171
 
143
172
  ```javascript
144
- // Auto invalidation (requires CORE_AUTO_FEATURES=true)
173
+ // Auto invalidation (requires CORE_AUTO_INVALIDATION=true)
145
174
  const result = await QuaryCache(
146
175
  'INSERT INTO users (name, email) VALUES (?, ?)',
147
176
  ['John', 'john@example.com']
@@ -184,13 +213,17 @@ bounds the count. Concurrent cache misses for the same page share one database q
184
213
  `DB_PAGINATION_REUSE_TOTAL=true` the total of one page is reused for the other pages of the same
185
214
  query until `cacheName` is invalidated.
186
215
 
216
+ Each page is cached as `<cacheName>:page<n>:size<m>`. Since v2.9.1 the `LIMIT` is appended on a new
217
+ line, so SQL that ends with a `-- comment` paginates correctly, and a cached value is only used if it
218
+ is a page object.
219
+
187
220
  #### Signature
188
221
 
189
222
  ```typescript
190
223
  getCacheQueryPagination<T = any>(
191
224
  sql: string,
192
225
  parameters: any[],
193
- cacheName: string,
226
+ cacheName: string | null | undefined,
194
227
  page: number,
195
228
  pageSize?: number,
196
229
  database?: string | null
@@ -200,21 +233,21 @@ getCacheQueryPagination<T = any>(
200
233
  #### Parameters
201
234
 
202
235
  - `sql` (string): SQL SELECT query
203
- - `parameters` (any[]): Query parameters
204
- - `cacheName` (string): Cache key prefix
205
- - `page` (number): Page number (1-indexed)
206
- - `pageSize` (number, optional): Items per page (default: 10)
236
+ - `parameters` (any[]): Query parameters (required, use `[]` for none)
237
+ - `cacheName` (string | null): Cache key prefix. Since v2.9.1, without one an automatic key is used when
238
+ automatic keys are enabled; otherwise the result is not cached and a one-time warning is logged
239
+ (earlier versions shared one cache key between all such calls)
240
+ - `page` (number): Page number, 0-based (`0` is the first page; invalid values become `0`)
241
+ - `pageSize` (number, optional): Items per page (default: 30); must be greater than 0
207
242
  - `database` (string | null, optional): Database to switch to
208
243
 
209
244
  #### Returns
210
245
 
211
246
  ```typescript
212
247
  interface PaginationResult<T> {
213
- data: T[];
214
- page: number;
215
- pageSize: number;
216
- totalCount: number;
217
- pageCount: number;
248
+ totalCount: number; // rows in the whole result
249
+ pageCount: number; // Math.ceil(totalCount / pageSize)
250
+ detail: T[]; // rows of the requested page
218
251
  }
219
252
  ```
220
253
 
@@ -222,21 +255,19 @@ interface PaginationResult<T> {
222
255
 
223
256
  ```javascript
224
257
  const result = await getCacheQueryPagination(
225
- 'SELECT * FROM users WHERE status = ?',
258
+ 'SELECT * FROM users WHERE status = ? ORDER BY id',
226
259
  ['active'],
227
260
  'active-users',
228
- 1, // page 1
261
+ 0, // first page
229
262
  20 // 20 items per page
230
263
  );
231
264
 
232
265
  console.log(result);
233
266
  /*
234
267
  {
235
- data: [...],
236
- page: 1,
237
- pageSize: 20,
238
268
  totalCount: 157,
239
- pageCount: 8
269
+ pageCount: 8,
270
+ detail: [...] // up to 20 rows
240
271
  }
241
272
  */
242
273
  ```
@@ -251,14 +282,14 @@ Execute queries within a database transaction with automatic commit/rollback.
251
282
 
252
283
  ```typescript
253
284
  withTransaction<T = any>(
254
- callback: (tx: TransactionContext) => Promise<T>,
285
+ callback: (tx: TransactionContext) => T | Promise<T>,
255
286
  options?: TransactionOptions
256
287
  ): Promise<T>
257
288
  ```
258
289
 
259
290
  #### Parameters
260
291
 
261
- - `callback` (function): Async function receiving transaction context
292
+ - `callback` (function): Function (usually async) receiving the transaction context
262
293
  - `options` (object, optional):
263
294
  - `database` (string | null): Database to switch to
264
295
 
@@ -266,8 +297,8 @@ withTransaction<T = any>(
266
297
 
267
298
  ```typescript
268
299
  interface TransactionContext {
269
- query<T>(sql: string, parameters: any[], resetCacheName?: string | string[] | null): Promise<T>;
270
- getCacheQuery<T>(sql: string, parameters: any[], cacheName?: string | null): Promise<T[]>;
300
+ query<T>(sql: string, parameters?: any[], resetCacheName?: string | string[] | null): Promise<T>;
301
+ getCacheQuery<T>(sql: string, parameters?: any[], cacheName?: string | null): Promise<T[]>;
271
302
  getConnection(): any;
272
303
  }
273
304
  ```
@@ -320,10 +351,11 @@ await withTransaction(async (tx) => {
320
351
  'user-123'
321
352
  );
322
353
 
323
- // Update user
354
+ // Update user, and drop the cached row once the transaction commits
324
355
  await tx.query(
325
356
  'UPDATE users SET balance = balance + ? WHERE id = ?',
326
- [100, user.id]
357
+ [100, user.id],
358
+ 'user-123'
327
359
  );
328
360
  });
329
361
  ```
@@ -331,7 +363,9 @@ await withTransaction(async (tx) => {
331
363
  #### Important Notes
332
364
 
333
365
  - Cache invalidation is buffered and applied **only on commit**
334
- - `tx.getCacheQuery()` reads from cache but **doesn't guarantee transaction isolation**
366
+ - `tx.getCacheQuery()` checks the cache first, so a hit **doesn't guarantee transaction isolation**
367
+ - Since v2.9.1 `tx.getCacheQuery()` writes what it read to the cache only after the commit; nothing
368
+ is cached from a rolled-back transaction (v2.9.0 cached it immediately)
335
369
  - For critical reads, use `tx.query()` instead
336
370
  - Transactions auto-rollback on any error
337
371
 
@@ -355,15 +389,31 @@ bulkInsert<T = any>(
355
389
 
356
390
  #### Parameters
357
391
 
358
- - `table` (string): Table name
359
- - `records` (array): Array of objects with column-value pairs
392
+ - `table` (string): Table name, used in the SQL as-is
393
+ - `records` (array): Array of objects with column-value pairs. The columns are the keys of the first
394
+ record, used in the SQL as-is; later records are read for those columns only (a missing key is
395
+ inserted as `NULL`, extra keys are ignored)
360
396
  - `options` (object, optional):
361
397
  - `chunkSize` (number): Records per chunk (default: 1000)
362
398
  - `database` (string | null): Database to switch to
363
- - `resetCacheName` (string | null): Cache pattern to invalidate
399
+ - `resetCacheName` (string | string[] | null): Prefixes to invalidate after the insert
364
400
  - `transaction` (boolean, v2.9.0): insert all chunks in one transaction, all or nothing
365
401
  (default: false, each chunk commits on its own as before)
366
402
 
403
+ #### Notes
404
+
405
+ - **Never pass untrusted keys.** Map request or CSV input onto a fixed list of columns. Since v2.9.1
406
+ an invalid table or column name is rejected before anything is sent, with a `TypeError`:
407
+ `bulkInsert: invalid table name "<name>"` or `bulkInsert: invalid column name "<name>"`. Valid names
408
+ are unchanged.
409
+ - **Invalidation:** `resetCacheName` prefixes are deleted after the insert. Since v2.9.1, without
410
+ `resetCacheName`, auto-invalidation (`CORE_AUTO_INVALIDATION` / `enableAutoInvalidation`) also applies
411
+ to `bulkInsert`, and invalidation also runs when the insert fails after rows may have been written.
412
+ - **Retries:** after a connection error the insert is retried from the first chunk that was not
413
+ acknowledged, so a chunk whose acknowledgement was lost can be inserted twice. With
414
+ `transaction: true` a retry starts over; an error from `COMMIT` is not retried and carries
415
+ `err.commitOutcomeUnknown = true` (since v2.9.1), because the rows may or may not have been written.
416
+
367
417
  #### Returns
368
418
 
369
419
  ```typescript
@@ -403,8 +453,8 @@ Execute query with timeout protection.
403
453
  ```typescript
404
454
  getCacheQueryWithTimeout<T = any>(
405
455
  sql: string,
406
- parameters: any[],
407
- cacheName: string,
456
+ parameters?: any[],
457
+ cacheName?: string | null,
408
458
  options?: QueryTimeoutOptions
409
459
  ): Promise<T[]>
410
460
  ```
@@ -412,8 +462,10 @@ getCacheQueryWithTimeout<T = any>(
412
462
  #### Parameters
413
463
 
414
464
  - `sql` (string): SQL query
415
- - `parameters` (any[]): Query parameters
416
- - `cacheName` (string): Cache key
465
+ - `parameters` (any[], optional): Query parameters
466
+ - `cacheName` (string | null): Cache key. Since v2.9.1, without one an automatic key is used when
467
+ automatic keys are enabled; otherwise the result is not cached and a one-time warning is logged
468
+ (earlier versions shared one cache key between all such calls)
417
469
  - `options` (object, optional):
418
470
  - `timeout` (number): Timeout in milliseconds (default: 30000)
419
471
  - `database` (string | null): Database to switch to
@@ -422,7 +474,10 @@ getCacheQueryWithTimeout<T = any>(
422
474
  its result (default: false)
423
475
 
424
476
  Since v2.9.0 a call whose timeout fires before its query started (e.g. while waiting for a pool
425
- connection) no longer runs the query afterwards.
477
+ connection) no longer runs the query afterwards; since v2.9.1 this also holds when the timeout fires
478
+ while the call switches to `database`. With `cancelOnTimeout`, since v2.9.1 the connection whose query
479
+ was killed is discarded instead of being returned to the pool, and at most 4 cancellations run at
480
+ once.
426
481
 
427
482
  #### Returns
428
483
 
@@ -452,7 +507,8 @@ try {
452
507
 
453
508
  ### closeConnections
454
509
 
455
- Gracefully close all database connections.
510
+ Gracefully close the MySQL connection pool. New queries are rejected afterwards. The Redis client is
511
+ not closed: quit it yourself when Redis is enabled, or its open connection keeps the process alive.
456
512
 
457
513
  #### Signature
458
514
 
@@ -463,16 +519,23 @@ closeConnections(): Promise<void>
463
519
  #### Examples
464
520
 
465
521
  ```javascript
522
+ const { closeConnections, getRedisClient } = require('sqlcache');
523
+
524
+ async function shutdown() {
525
+ await closeConnections();
526
+ if (process.env.REDIS_ENABLED !== 'false') getRedisClient().quit();
527
+ }
528
+
466
529
  // In app shutdown
467
530
  process.on('SIGTERM', async () => {
468
531
  console.log('Shutting down...');
469
- await closeConnections();
532
+ await shutdown();
470
533
  process.exit(0);
471
534
  });
472
535
 
473
536
  // In tests
474
537
  after(async () => {
475
- await closeConnections();
538
+ await shutdown();
476
539
  });
477
540
  ```
478
541
 
@@ -513,11 +576,12 @@ console.log(stats);
513
576
  }
514
577
  */
515
578
 
516
- // Monitor pool usage
579
+ // Monitor pool usage. Connections are opened on demand (an idle pool reports 0 of 0),
580
+ // so requests waiting for a connection are what shows an exhausted pool.
517
581
  setInterval(() => {
518
582
  const stats = getPoolStats();
519
- if (stats.activeConnections === stats.totalConnections) {
520
- console.warn('Connection pool exhausted!');
583
+ if (stats.queuedRequests > 0) {
584
+ console.warn('Connection pool exhausted: requests are waiting for a connection');
521
585
  }
522
586
  }, 60000);
523
587
  ```
@@ -609,6 +673,9 @@ and deletes while scanning; several prefixes are scanned concurrently, and prefi
609
673
  one pass (e.g. `users_` and `users:`) are merged. Waits like `delKeyItem`, and invalidations missed
610
674
  during an outage are replayed when Redis is ready again.
611
675
 
676
+ Prefixes are Redis glob patterns (`*` is appended): escape `*`, `?`, `[`, `]` and `\` in any
677
+ user-supplied part. An empty prefix without `REDIS_VHOST` deletes every key in that Redis database.
678
+
612
679
  #### Signature
613
680
 
614
681
  ```typescript
@@ -627,6 +694,27 @@ await delPrefixKeyItem(['users_', 'profiles_', 'sessions_']);
627
694
 
628
695
  ---
629
696
 
697
+ ### isRedisConnected
698
+
699
+ Check whether the Redis connection is ready, for health checks. Since v2.9.1 it returns `false`
700
+ without opening a connection when `REDIS_ENABLED=false` (v2.9.0 opened one on the first call).
701
+
702
+ #### Signature
703
+
704
+ ```typescript
705
+ isRedisConnected(): boolean
706
+ ```
707
+
708
+ #### Examples
709
+
710
+ ```javascript
711
+ app.get('/health', (req, res) => {
712
+ res.json({ redis: isRedisConnected() });
713
+ });
714
+ ```
715
+
716
+ ---
717
+
630
718
  ### getRedisClient
631
719
 
632
720
  Get raw Redis client instance.
@@ -656,23 +744,26 @@ Configure all v2.6.0 features at once.
656
744
  #### Signature
657
745
 
658
746
  ```typescript
659
- configure(config?: CoreConfig): void
747
+ configure(config?: ConfigureOptions): void
660
748
  ```
661
749
 
662
750
  #### Parameters
663
751
 
664
752
  ```typescript
665
- interface CoreConfig {
666
- autoKey?: {
753
+ interface ConfigureOptions {
754
+ autoKey?: boolean | {
667
755
  enabled?: boolean;
668
756
  };
669
- autoInvalidation?: {
757
+ autoInvalidation?: boolean | {
670
758
  enabled?: boolean;
671
759
  tables?: Record<string, string | string[]>;
672
760
  };
673
761
  }
674
762
  ```
675
763
 
764
+ Each option is passed to `enableAutoKey` / `enableAutoInvalidation`. `{ enabled: false }` turns the
765
+ feature off, and since v2.9.1 so does `false` (earlier versions turned it on for `false`).
766
+
676
767
  #### Examples
677
768
 
678
769
  ```javascript
@@ -683,23 +774,26 @@ configure({
683
774
  autoInvalidation: {
684
775
  enabled: true,
685
776
  tables: {
686
- users: ['users_*', 'profiles_*'],
687
- orders: ['orders_*', 'cart_*']
777
+ users: ['users:*', 'users_*', 'profiles_*'],
778
+ orders: ['orders:*', 'orders_*', 'cart_*']
688
779
  }
689
780
  }
690
781
  });
691
782
  ```
692
783
 
784
+ A `tables` rule replaces the default `<table>_*` and `<table>:*` patterns for that table; keep
785
+ `<table>:*` when you use automatic keys, which start with `<table>:`.
786
+
693
787
  ---
694
788
 
695
789
  ### enableAutoKey
696
790
 
697
- Enable automatic cache key generation.
791
+ Enable automatic cache key generation, used when a call has no cache name.
698
792
 
699
793
  #### Signature
700
794
 
701
795
  ```typescript
702
- enableAutoKey(config?: AutoKeyConfig): void
796
+ enableAutoKey(config?: AutoKeyConfig | boolean): void
703
797
  ```
704
798
 
705
799
  #### Examples
@@ -708,8 +802,12 @@ enableAutoKey(config?: AutoKeyConfig): void
708
802
  const { enableAutoKey } = require('sqlcache');
709
803
 
710
804
  enableAutoKey({ enabled: true });
805
+ enableAutoKey(false); // turns it off (v2.9.1; earlier versions turned it on)
711
806
  ```
712
807
 
808
+ Since v2.9.1 queries without a detectable table (`CALL`, `SELECT fn(?)`) get the key
809
+ `query:<sqlhash>:<paramhash>`, and `BigInt` parameters no longer throw.
810
+
713
811
  ---
714
812
 
715
813
  ### enableAutoInvalidation
@@ -719,7 +817,7 @@ Enable automatic cache invalidation.
719
817
  #### Signature
720
818
 
721
819
  ```typescript
722
- enableAutoInvalidation(config?: AutoInvalidationConfig): void
820
+ enableAutoInvalidation(config?: AutoInvalidationConfig | boolean): void
723
821
  ```
724
822
 
725
823
  #### Examples
@@ -730,10 +828,12 @@ const { enableAutoInvalidation } = require('sqlcache');
730
828
  enableAutoInvalidation({
731
829
  enabled: true,
732
830
  tables: {
733
- users: ['users_*', 'profiles_*'],
734
- orders: 'orders_*'
831
+ users: ['users:*', 'users_*', 'profiles_*'],
832
+ orders: ['orders:*', 'orders_*']
735
833
  }
736
834
  });
835
+
836
+ enableAutoInvalidation(false); // turns it off (v2.9.1; earlier versions turned it on)
737
837
  ```
738
838
 
739
839
  ---