@pi-r/redis 0.11.3 → 0.12.0

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/client/index.js CHANGED
@@ -1,12 +1,11 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- var node_crypto = require('node:crypto');
4
- var types = require('@e-mc/types');
5
- var util = require('@e-mc/db/util');
6
-
7
- const redis = require("redis");
8
- const Db = require("@e-mc/db");
9
- const DbPool = require("@pi-r/redis/client/pool");
3
+ const redis = require('redis');
4
+ const { randomUUID } = require('node:crypto');
5
+ const Db = require('@e-mc/db');
6
+ const { DB_TYPE, asFunction, coerceObject, errorMessage, isArray, isError, isObject, isPlainObject, isString } = require('@e-mc/types');
7
+ const { parseConnectionString, parseServerAuth } = require('@e-mc/db/util');
8
+ const DbPool = require('@pi-r/redis/client/pool');
10
9
  const POOL_STATE = {};
11
10
  async function doScan(client, key, index, cursor = [], iterations = [], options) {
12
11
  const target = (Array.isArray(cursor) ? cursor : [cursor]).map(item => Buffer.isBuffer(item) ? item : item.toString());
@@ -21,17 +20,17 @@ async function doScan(client, key, index, cursor = [], iterations = [], options)
21
20
  } while (+current > 0 && --length > 0);
22
21
  return result;
23
22
  }
24
- const convertFloat = (value) => typeof value === 'number' || types.isString(value) && !isNaN(value = +value) ? value : undefined;
23
+ const convertFloat = (value) => typeof value === 'number' || isString(value) && !isNaN(value = +value) ? value : undefined;
25
24
  async function setCredential(item) {
26
25
  const credential = this.getCredential(item);
27
26
  const options = item.options ||= {};
28
- const client = types.isPlainObject(options.client) ? options.client : options.client = {};
27
+ const client = isPlainObject(options.client) ? options.client : options.client = {};
29
28
  let { uri, username, password, database = 0 } = item;
30
29
  if (database > 0) {
31
30
  client.database = database;
32
31
  }
33
32
  if (credential) {
34
- const auth = util.parseServerAuth(credential, 6379);
33
+ const auth = parseServerAuth(credential, 6379);
35
34
  if (auth.database && (database = +auth.database) > 0) {
36
35
  client.database ||= database;
37
36
  }
@@ -43,7 +42,7 @@ async function setCredential(item) {
43
42
  delete item.credential;
44
43
  }
45
44
  if (!uri) {
46
- throw types.errorMessage("redis", "Invalid credentials", 'url');
45
+ throw errorMessage("redis", "Invalid credentials", 'url');
47
46
  }
48
47
  if (client.socket?.tls) {
49
48
  this.readTLSConfig(client.socket);
@@ -62,8 +61,8 @@ async function setCredential(item) {
62
61
  let pool;
63
62
  username = undefined;
64
63
  password = undefined;
65
- if (types.isString(usePool)) {
66
- username = client.username || util.parseConnectionString(uri)?.username;
64
+ if (isString(usePool)) {
65
+ username = client.username || parseConnectionString(uri)?.username;
67
66
  if (username) {
68
67
  [password, pool] = DbPool.validateKey(POOL_STATE, username, usePool);
69
68
  if (pool) {
@@ -79,13 +78,16 @@ async function setCredential(item) {
79
78
  const config = this.getPoolConfig("redis", password);
80
79
  const poolOptions = {};
81
80
  if (config) {
82
- const { min, max, timeout } = config;
81
+ const { min, max, idle, timeout } = config;
83
82
  if (min >= 0) {
84
83
  poolOptions.minimum ??= min;
85
84
  }
86
85
  if (max > 0) {
87
86
  poolOptions.maximum ??= max;
88
87
  }
88
+ if (idle > 0) {
89
+ poolOptions.cleanupDelay ??= idle;
90
+ }
89
91
  if (timeout > 0) {
90
92
  poolOptions.acquireTimeout ??= timeout;
91
93
  }
@@ -117,7 +119,7 @@ async function executeBatchQuery(batch, options = '', outResult) {
117
119
  return [];
118
120
  }
119
121
  let parallel, checkObject, connectOnce, errorQuery, sessionKey, outCacheMiss;
120
- if (types.isPlainObject(options)) {
122
+ if (isPlainObject(options)) {
121
123
  ({ parallel, checkObject, connectOnce, errorQuery, sessionKey, outCacheMiss } = options);
122
124
  }
123
125
  else {
@@ -175,8 +177,8 @@ async function executeBatchQuery(batch, options = '', outResult) {
175
177
  for (let i = 0; i < length; ++i) {
176
178
  const item = batch[i];
177
179
  const { source, key, format = 'HASH', search, aggregate, streams, options: clientOptions = {}, ignoreCache } = item;
178
- let credential = (redisCredential || onceCredential), error;
179
- if (!credential && !types.isPlainObject(credential = clientOptions.client) && (error = types.errorMessage(source, "Invalid credentials")) || !(types.isString(key) || types.isArray(key) || types.isPlainObject(search) && (types.isPlainObject(search.schema) || types.isString(search.query)) || types.isPlainObject(aggregate) && (types.isPlainObject(aggregate.schema) || types.isString(aggregate.query))) && (error = types.errorMessage(source, "Missing database query", item.uri))) {
180
+ let credential = redisCredential || onceCredential, error;
181
+ if (!credential && !isPlainObject(credential = clientOptions.client) && (error = errorMessage(source, "Invalid credentials")) || !(isString(key) || isArray(key) || isPlainObject(search) && (isPlainObject(search.schema) || isString(search.query)) || isPlainObject(aggregate) && (isPlainObject(aggregate.schema) || isString(aggregate.query))) && (error = errorMessage(source, "Missing database query", item.uri))) {
180
182
  if (this.handleFail(error, item, { errorQuery })) {
181
183
  if (!parallel) {
182
184
  tasks.length = 0;
@@ -194,14 +196,14 @@ async function executeBatchQuery(batch, options = '', outResult) {
194
196
  }
195
197
  item.transactionState = 1;
196
198
  const uuidKey = credential.uuidKey ||= (onceCredential ? batch[0] : item).credential?.uuidKey;
197
- const targetObject = typeof checkObject === 'string' ? this.hasCoerce("redis", 'options', uuidKey) && types.asFunction(checkObject) : checkObject;
199
+ const targetObject = typeof checkObject === 'string' ? this.hasCoerce("redis", 'options', uuidKey) ? asFunction(checkObject) : null : checkObject;
198
200
  const cacheValue = ignoreCache === undefined ? sessionKey : Array.isArray(ignoreCache) ? { sessionKey, exclusiveOf: ignoreCache } : { sessionKey, renewCache: ignoreCache === 0 };
199
201
  let queryString = '', rows;
200
202
  if (caching && ignoreCache !== true) {
201
- if (types.isObject(search)) {
203
+ if (isObject(search)) {
202
204
  queryString = Db.asString(search, true);
203
205
  }
204
- else if (types.isObject(aggregate)) {
206
+ else if (isObject(aggregate)) {
205
207
  queryString = Db.asString(aggregate, true);
206
208
  }
207
209
  else if (streams) {
@@ -251,18 +253,18 @@ async function executeBatchQuery(batch, options = '', outResult) {
251
253
  const b = (arg) => typeof arg === 'number' || Buffer.isBuffer(arg) ? arg.toString() : arg;
252
254
  if (item.update) {
253
255
  commandType = this.commandType.UPDATE;
254
- const values = (!Array.isArray(item.update) ? [item.update] : item.update).map(async (target) => {
256
+ const values = (Array.isArray(item.update) ? item.update : [item.update]).map(async (target) => {
255
257
  return new Promise(async (success, failed) => {
256
258
  let { key: k, value: v, format: f, NX, XX, options: setOptions = {} } = target;
257
- f = (types.isString(f) ? f.toUpperCase() : 'HASH');
259
+ f = (isString(f) ? f.toUpperCase() : 'HASH');
258
260
  if (v === undefined && f !== 'JSON') {
259
261
  success(target);
260
262
  return;
261
263
  }
262
264
  const has = (n) => typeof n === 'number' && n > 0;
263
- const failKey = () => failed(types.errorMessage(f, "Invalid key", Db.asString(k) || "Unknown"));
264
- const failValue = () => failed(types.errorMessage(f, "Invalid value", Db.asString(v) || "Unknown"));
265
- if (types.isPlainObject(setOptions.command)) {
265
+ const failKey = () => failed(errorMessage(f, "Invalid key", Db.asString(k) || "Unknown"));
266
+ const failValue = () => failed(errorMessage(f, "Invalid value", Db.asString(v) || "Unknown"));
267
+ if (isPlainObject(setOptions.command)) {
266
268
  client = client.withCommandOptions(setOptions.command);
267
269
  }
268
270
  let pending, codeMin = 0, EX, PX, EXAT, PXAT, KEEPTTL;
@@ -271,16 +273,16 @@ async function executeBatchQuery(batch, options = '', outResult) {
271
273
  failKey();
272
274
  return;
273
275
  }
274
- if (this.hasCoerce("redis", 'options', uuidKey) && types.isString(v) && v.startsWith('new')) {
275
- ({ outV: v } = types.coerceObject({ outV: v }));
276
+ if (this.hasCoerce("redis", 'options', uuidKey) && isString(v) && v.startsWith('new')) {
277
+ ({ outV: v } = coerceObject({ outV: v }));
276
278
  }
277
279
  const { path = '$', command: cmd, start, stop, index } = target;
278
- const name = (types.isString(cmd) ? cmd.toUpperCase() : 'SET');
280
+ const name = (isString(cmd) ? cmd.toUpperCase() : 'SET');
279
281
  switch (name) {
280
282
  case 'ARRAPPEND':
281
283
  case 'ARRINSERT': {
282
284
  let v1, v2;
283
- if (types.isArray(v)) {
285
+ if (isArray(v)) {
284
286
  v1 = v[0];
285
287
  v2 = v.slice(1);
286
288
  }
@@ -343,7 +345,7 @@ async function executeBatchQuery(batch, options = '', outResult) {
343
345
  break;
344
346
  }
345
347
  case 'STRAPPEND':
346
- if (types.isString(v)) {
348
+ if (isString(v)) {
347
349
  pending = client.json.strAppend(k, v, { path });
348
350
  }
349
351
  break;
@@ -389,7 +391,7 @@ async function executeBatchQuery(batch, options = '', outResult) {
389
391
  codeMin = NaN;
390
392
  pending = client.set(k, v, flags);
391
393
  }
392
- else if (types.isObject(v)) {
394
+ else if (isObject(v)) {
393
395
  let valid = true;
394
396
  if (NX || XX) {
395
397
  valid = await client.exists(k) === 1;
@@ -430,7 +432,7 @@ async function executeBatchQuery(batch, options = '', outResult) {
430
432
  }
431
433
  if (pending) {
432
434
  pending.then(async (code) => {
433
- if (code == null || code !== -Infinity && types.isString(code) && code !== 'OK' || typeof code === 'number' && code <= codeMin || Array.isArray(code) && code.every(resp => resp === null || typeof resp === 'number' && resp <= codeMin)) {
435
+ if (code == null || code !== -Infinity && isString(code) && code !== 'OK' || typeof code === 'number' && code <= codeMin || Array.isArray(code) && code.every(resp => resp === null || typeof resp === 'number' && resp <= codeMin)) {
434
436
  failValue();
435
437
  return;
436
438
  }
@@ -468,7 +470,7 @@ async function executeBatchQuery(batch, options = '', outResult) {
468
470
  success(target);
469
471
  }
470
472
  else {
471
- failed(types.errorMessage(f, "Invalid value", has(EX) ? 'EX: ' + EX : has(PX) ? 'PX: ' + PX : has(EXAT) ? 'EXAT: ' + EX : 'PXAT: ' + PXAT));
473
+ failed(errorMessage(f, "Invalid value", has(EX) ? 'EX: ' + EX : has(PX) ? 'PX: ' + PX : has(EXAT) ? 'EXAT: ' + EX : 'PXAT: ' + PXAT));
472
474
  }
473
475
  })
474
476
  .catch(failed);
@@ -499,39 +501,49 @@ async function executeBatchQuery(batch, options = '', outResult) {
499
501
  }
500
502
  });
501
503
  }
502
- const commandOptions = credential.commandOptions || clientOptions.command;
503
- if (types.isPlainObject(commandOptions)) {
504
+ const commandOptions = credential.commandOptions;
505
+ if (isPlainObject(commandOptions)) {
504
506
  commandOptions.abortSignal ||= this.signal;
505
507
  client = client.withCommandOptions(commandOptions);
506
508
  }
507
509
  commandType = this.commandType.SELECT;
508
- const target = types.isObject(search) ? search : types.isObject(aggregate) ? aggregate : null;
510
+ const target = isObject(search) ? search : isObject(aggregate) ? aggregate : null;
509
511
  if (target) {
510
512
  const { query = '', schema, index } = target;
513
+ const ftType = () => target === search ? clientOptions.hybrid ? 'FT.HYBRID' : 'FT.SEARCH' : 'FT.AGGREGATE';
511
514
  let idx = '';
512
- if (types.isObject(schema)) {
513
- idx = index || node_crypto.randomUUID();
515
+ if (isObject(schema)) {
516
+ idx = index || randomUUID();
514
517
  await client.ft.create(idx, schema, target.options);
515
518
  }
516
519
  try {
517
520
  if (target === search) {
518
- const reply = await client.ft.search(index || idx, query, clientOptions.search);
519
- rows = reply.documents.map(doc => (doc.value.__id__ = doc.id) && doc.value);
521
+ if (clientOptions.hybrid) {
522
+ const reply = await client.ft.hybrid(index || idx, clientOptions.hybrid);
523
+ rows = reply.results;
524
+ if (isArray(reply.warnings)) {
525
+ reply.warnings.forEach(msg => this.addLog(3, msg, "redis", 'FT.HYBRID'));
526
+ }
527
+ }
528
+ else {
529
+ const reply = await client.ft.search(index || idx, query, clientOptions.search);
530
+ rows = reply.documents.map(doc => (doc.value.__id__ = doc.id) && doc.value);
531
+ }
520
532
  }
521
533
  else {
522
534
  ({ results: rows } = await client.ft.aggregate(index || idx, query, clientOptions.aggregate));
523
535
  }
524
536
  }
525
537
  catch (err) {
526
- this.addLog(3, err, { source: target === search ? 'FT.SEARCH' : 'FT.AGGREGATE' });
527
- if (err instanceof Error) {
538
+ this.addLog(3, err, "redis", ftType());
539
+ if (isError(err)) {
528
540
  throw err;
529
541
  }
530
542
  }
531
543
  finally {
532
544
  if (idx && idx !== index) {
533
545
  client.ft.dropIndex(idx).catch((err) => {
534
- this.writeFail([`Unable to drop ${target === search ? 'search' : 'aggregate'} index`, idx], err, { type: 65536, fatal: false });
546
+ this.writeFail([`Unable to drop ${ftType()} index`, idx], err, { type: 65536, fatal: false });
535
547
  });
536
548
  }
537
549
  }
@@ -599,22 +611,20 @@ async function executeBatchQuery(batch, options = '', outResult) {
599
611
  rows = (targetObject ? targetObject(item, data) : data);
600
612
  }
601
613
  if (rows == null) {
602
- throw types.errorMessage(source, "Missing database query");
614
+ throw errorMessage(source, "Missing database query");
603
615
  }
604
616
  this.add(item, 4);
605
617
  resolve(this.setQueryResult(source, DbPool.sanitize(redisCredential || credential), queryString, rows, cacheValue));
606
618
  }
607
619
  catch (err) {
608
- if (err instanceof Error) {
609
- switch (err.message) {
610
- case "The client is closed":
611
- case "Disconnects client": {
612
- const pool = item.usePool && DbPool.findKey(POOL_STATE, item.usePool, DbPool.asString(credential));
613
- if (pool) {
614
- await pool.detach(true);
615
- }
616
- break;
620
+ switch (isError(err) && err.message) {
621
+ case "The client is closed":
622
+ case "Disconnects client": {
623
+ const pool = item.usePool && DbPool.findKey(POOL_STATE, item.usePool, DbPool.asString(credential));
624
+ if (pool) {
625
+ await pool.detach(true);
617
626
  }
627
+ break;
618
628
  }
619
629
  }
620
630
  if (this.handleFail(err, item, { errorQuery, commandType })) {
@@ -651,7 +661,7 @@ async function checkTimeout(value, limit = 0) {
651
661
  return DbPool.checkTimeout(POOL_STATE, value, limit);
652
662
  }
653
663
  const DB_SOURCE_CLIENT = true;
654
- const DB_SOURCE_TYPE = types.DB_TYPE.NOSQL | types.DB_TYPE.KEYVALUE;
664
+ const DB_SOURCE_TYPE = DB_TYPE.NOSQL | DB_TYPE.KEYVALUE;
655
665
 
656
666
  exports.DB_SOURCE_CLIENT = DB_SOURCE_CLIENT;
657
667
  exports.DB_SOURCE_TYPE = DB_SOURCE_TYPE;
package/client/pool.js CHANGED
@@ -1,3 +1,5 @@
1
+ "use strict";
2
+
1
3
  const DbPool = require('@e-mc/db/pool');
2
4
  const POOL_ACTIVE = new WeakSet();
3
5
  class RedisPool extends DbPool {
@@ -36,4 +38,5 @@ class RedisPool extends DbPool {
36
38
  return !this.client.isOpen;
37
39
  }
38
40
  }
41
+
39
42
  module.exports = RedisPool;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-r/redis",
3
- "version": "0.11.3",
3
+ "version": "0.12.0",
4
4
  "description": "Redis client driver for E-mc.",
5
5
  "main": "client/index.js",
6
6
  "types": "client/index.d.ts",
@@ -20,8 +20,8 @@
20
20
  "license": "MIT",
21
21
  "homepage": "https://github.com/anpham6/pi-r#readme",
22
22
  "dependencies": {
23
- "@e-mc/db": "^0.13.10",
24
- "@e-mc/types": "^0.13.10",
23
+ "@e-mc/db": "^0.14.0",
24
+ "@e-mc/types": "^0.14.0",
25
25
  "redis": "^5.12.1"
26
26
  }
27
27
  }
package/types/index.d.ts CHANGED
@@ -11,6 +11,7 @@ import type { CommandOptions } from '@redis/client/dist/lib/client/commands-queu
11
11
  import type { ScanOptions } from '@redis/client/dist/lib/commands/SCAN';
12
12
  import type { CreateOptions } from '@redis/search/dist/lib/commands/CREATE';
13
13
  import type { FtAggregateOptions } from '@redis/search/dist/lib/commands/AGGREGATE';
14
+ import type { FtHybridOptions } from '@redis/search/dist/lib/commands/HYBRID';
14
15
  import type { FtSearchOptions } from '@redis/search/dist/lib/commands/SEARCH';
15
16
  import type { JsonGetOptions } from '@redis/json/dist/lib/commands/GET';
16
17
  import type { XReadOptions, XReadStreams } from '@redis/client/dist/lib/commands/XREAD';
@@ -29,10 +30,9 @@ export interface RedisDataSource extends DbDataSource<string, PlainObject, Redis
29
30
  iterations?: number | number[];
30
31
  options?: {
31
32
  client?: RedisClientOptions;
32
- /** @deprecated client.commandOptions */
33
- command?: CommandOptions;
34
33
  get?: JsonGetOptions;
35
34
  search?: FtSearchOptions;
35
+ hybrid?: FtHybridOptions;
36
36
  aggregate?: FtAggregateOptions;
37
37
  xread?: XReadOptions;
38
38
  scan?: ScanOptions;
@@ -91,7 +91,7 @@ export type RedisCommandJSON = "ARRAPPEND" | "ARRINDEX" | "ARRINSERT" | "ARRPOP"
91
91
  export type RedisExpireCondition = "NX" | "XX" | "GT" | "LT";
92
92
  export type RedisCommandValue = RedisArgument | number;
93
93
  export type RedisClientPoolType<T extends RedisModules = RedisDefaultModules> = IRedisClientPoolType<T>;
94
- export type RedisClientPoolInstance = IRedisClientPoolType<any, any, any, any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
94
+ export type RedisClientPoolInstance = IRedisClientPoolType<any, any, any, any, any>;
95
95
  export type RedisHSETObject = Record<number | string, RedisCommandValue>;
96
96
  export type DbPoolCredential = RedisClientOptions & IdentifierAction;
97
97