diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9753535b53..f522282d28 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: node-version: ["18", "20", "22"] - redis-version: ["rs-7.4.0-v1", "8.0.2", "8.2"] + redis-version: ["rs-7.4.0-v1", "8.0.2", "8.2", "8.2.1-pre"] steps: - uses: actions/checkout@v4 with: diff --git a/README.md b/README.md index 9948858db9..05c55985b3 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,8 @@ await client.sendCommand(["SET", "key", "value", "NX"]); // 'OK' await client.sendCommand(["HGETALL", "key"]); // ['key1', 'field1', 'key2', 'field2'] ``` +_Note: the [API is different when using a cluster](https://github.com/redis/node-redis/blob/master/docs/clustering.md#unsupported-redis-commands)._ + ### Transactions (Multi/Exec) Start a [transaction](https://redis.io/topics/transactions) by calling `.multi()`, then chaining your commands. When diff --git a/docs/clustering.md b/docs/clustering.md index 6803583fa5..3e4f8446b6 100644 --- a/docs/clustering.md +++ b/docs/clustering.md @@ -38,6 +38,25 @@ await cluster.close(); | scripts | | Script definitions (see [Lua Scripts](./programmability.md#lua-scripts)) | | functions | | Function definitions (see [Functions](./programmability.md#functions)) | +## Usage + +Most redis commands are the same as with individual clients. + +### Unsupported Redis Commands + +If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`. + +When clustering, `sendCommand` takes 3 arguments to help with routing to the correct redis node: +* `firstKey`: the key that is being operated on, or `undefined` to route to a random node. +* `isReadOnly`: determines if the command needs to go to the master or may go to a replica. +* `args`: the command and all arguments (including the key), as an array of strings. + +```javascript +await cluster.sendCommand("key", false, ["SET", "key", "value", "NX"]); // 'OK' + +await cluster.sendCommand("key", true, ["HGETALL", "key"]); // ['key1', 'field1', 'key2', 'field2'] +``` + ## Auth with password and username Specifying the password in the URL or a root node will only affect the connection to that specific node. In case you want to set the password for all the connections being created from a cluster instance, use the `defaults` option. @@ -114,3 +133,4 @@ Admin commands such as `MEMORY STATS`, `FLUSHALL`, etc. are not attached to the ### "Forwarded Commands" Certain commands (e.g. `PUBLISH`) are forwarded to other cluster nodes by the Redis server. The client sends these commands to a random node in order to spread the load across the cluster. + diff --git a/package-lock.json b/package-lock.json index 8585231103..c2a56173c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7337,7 +7337,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7346,12 +7346,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" } }, "packages/client": { "name": "@redis/client", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -7367,7 +7367,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -7386,7 +7386,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" } }, "packages/entraid/node_modules/@types/node": { @@ -7423,7 +7423,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7432,39 +7432,39 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" } }, "packages/redis": { - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "dependencies": { - "@redis/bloom": "5.8.0", - "@redis/client": "5.8.0", - "@redis/json": "5.8.0", - "@redis/search": "5.8.0", - "@redis/time-series": "5.8.0" + "@redis/bloom": "5.8.1", + "@redis/client": "5.8.1", + "@redis/json": "5.8.1", + "@redis/search": "5.8.1", + "@redis/time-series": "5.8.1" }, "engines": { "node": ">= 18" } }, "packages/redis/node_modules/@redis/bloom": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.0.tgz", - "integrity": "sha512-kpKZzAAjGiGYn88Bqq6+ozxPg6kGYWRZH9vnOwGcoSCbrW14SZpZVMYMFSio8FH9ZJUdUcmT/RLGlA1W1t0UWQ==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.1.tgz", + "integrity": "sha512-hJOJr/yX6BttnyZ+nxD3Ddiu2lPig4XJjyAK1v7OSHOJNUTfn3RHBryB9wgnBMBdkg9glVh2AjItxIXmr600MA==", "license": "MIT", "engines": { "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" } }, "packages/redis/node_modules/@redis/client": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.0.tgz", - "integrity": "sha512-ywZjKGoSSAECGYOd9bJpws6d4867SN686obUWT/sRmo1c/Q8V+jWyInvlqwKa0BOvTHHwYeB2WFUEvd6PADeOQ==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.1.tgz", + "integrity": "sha512-hD5Tvv7G0t8b3w8ao3kQ4jEPUmUUC6pqA18c8ciYF5xZGfUGBg0olQHW46v6qSt4O5bxOuB3uV7pM6H5wEjBwA==", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -7474,20 +7474,20 @@ } }, "packages/redis/node_modules/@redis/json": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.0.tgz", - "integrity": "sha512-xPBpwY6aKoRzMSu67MpwrBiSliON9bfHo9Y/pSPBjW8/KoOm1MzGqwJUO20qdjXpFoKJsDWwxIE1LHdBNzcImw==", + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.1.tgz", + "integrity": "sha512-kyvM8Vn+WjJI++nRsIoI9TbdfCs1/TgD0Hp7Z7GiG6W4IEBzkXGQakli+R5BoJzUfgh7gED2fkncYy1NLprMNg==", "license": "MIT", "engines": { "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" } }, "packages/search": { "name": "@redis/search", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7496,7 +7496,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" } }, "packages/test-utils": { @@ -7565,7 +7565,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -7574,7 +7574,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" } } } diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index 64bc348409..c2991de60d 100644 --- a/packages/bloom/lib/test-utils.ts +++ b/packages/bloom/lib/test-utils.ts @@ -4,7 +4,7 @@ import RedisBloomModules from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2' + defaultDockerVersion: '8.2.1-pre' }); export const GLOBAL = { diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 35e65009ce..e2ff5a8b42 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" }, "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/client/lib/commands/XTRIM.spec.ts b/packages/client/lib/commands/XTRIM.spec.ts index b88cf84676..38254d565e 100644 --- a/packages/client/lib/commands/XTRIM.spec.ts +++ b/packages/client/lib/commands/XTRIM.spec.ts @@ -18,6 +18,11 @@ describe('XTRIM', () => { parseArgs(XTRIM, 'key', 'MINID', 123), ['XTRIM', 'key', 'MINID', '123'] ); + + assert.deepEqual( + parseArgs(XTRIM, 'key', 'MINID', '0-0'), + ['XTRIM', 'key', 'MINID', '0-0'] + ); }); it('with strategyModifier', () => { @@ -89,6 +94,16 @@ describe('XTRIM', () => { cluster: GLOBAL.CLUSTERS.OPEN, }); + testUtils.testAll('xTrim with string MINID', async client => { + assert.equal( + typeof await client.xTrim('key', 'MINID', '0-0'), + 'number' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN, + }); + testUtils.testAll( 'xTrim with LIMIT', async (client) => { diff --git a/packages/client/lib/commands/XTRIM.ts b/packages/client/lib/commands/XTRIM.ts index 34171d4611..8d40824d79 100644 --- a/packages/client/lib/commands/XTRIM.ts +++ b/packages/client/lib/commands/XTRIM.ts @@ -37,7 +37,7 @@ export default { parser: CommandParser, key: RedisArgument, strategy: 'MAXLEN' | 'MINID', - threshold: number, + threshold: number | string, options?: XTrimOptions ) { parser.push('XTRIM') diff --git a/packages/client/lib/errors.ts b/packages/client/lib/errors.ts index db37ec1a9b..5cb9166df0 100644 --- a/packages/client/lib/errors.ts +++ b/packages/client/lib/errors.ts @@ -63,12 +63,7 @@ export class ReconnectStrategyError extends Error { } } -export class ErrorReply extends Error { - constructor(message: string) { - super(message); - this.stack = undefined; - } -} +export class ErrorReply extends Error {} export class SimpleError extends ErrorReply {} diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index b3f3bbf0b8..63c4586293 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -716,7 +716,7 @@ class RedisSentinelInternal< ); } - #createClient(node: RedisNode, clientOptions: RedisClientOptions, reconnectStrategy?: undefined | false) { + #createClient(node: RedisNode, clientOptions: RedisClientOptions, reconnectStrategy?: false) { return RedisClient.create({ //first take the globally set RESP RESP: this.#RESP, @@ -726,7 +726,7 @@ class RedisSentinelInternal< ...clientOptions.socket, host: node.host, port: node.port, - reconnectStrategy + ...(reconnectStrategy !== undefined && { reconnectStrategy }) } }); } diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 1f8d75a76d..33e8e330c2 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -174,7 +174,7 @@ export class SentinelFramework extends DockerBase { this.#testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2' + defaultDockerVersion: '8.2.1-pre' }); this.#nodeMap = new Map>>>(); this.#sentinelMap = new Map>>>(); diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index d6cb67aa01..e54a7d7647 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -9,7 +9,7 @@ import RedisBloomModules from '@redis/bloom'; const utils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2' + defaultDockerVersion: '8.2.1-pre' }); export default utils; diff --git a/packages/client/package.json b/packages/client/package.json index c685f60e77..1332083bf1 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts index 2a240db341..7a624902f5 100644 --- a/packages/entraid/lib/test-utils.ts +++ b/packages/entraid/lib/test-utils.ts @@ -6,7 +6,7 @@ import { EntraidCredentialsProvider } from './entraid-credentials-provider'; export const testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2' + defaultDockerVersion: '8.2.1-pre' }); const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ? diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 2ef4cfca79..2eee504a1f 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -22,7 +22,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" }, "devDependencies": { "@types/express": "^4.17.21", diff --git a/packages/json/lib/test-utils.ts b/packages/json/lib/test-utils.ts index 629c2a5fd6..2cc3804fe9 100644 --- a/packages/json/lib/test-utils.ts +++ b/packages/json/lib/test-utils.ts @@ -4,7 +4,7 @@ import RedisJSON from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2' + defaultDockerVersion: '8.2.1-pre' }); export const GLOBAL = { diff --git a/packages/json/package.json b/packages/json/package.json index 2879186a9c..ff689dd17e 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.8.1", + "version": "5.8.2", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.8.1" + "@redis/client": "^5.8.2" }, "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/redis/package.json b/packages/redis/package.json index c3cbc01442..de4ac275ed 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -1,7 +1,7 @@ { "name": "redis", "description": "A modern, high performance Redis client", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -13,11 +13,11 @@ "release": "release-it" }, "dependencies": { - "@redis/bloom": "5.8.0", - "@redis/client": "5.8.0", - "@redis/json": "5.8.0", - "@redis/search": "5.8.0", - "@redis/time-series": "5.8.0" + "@redis/bloom": "5.8.1", + "@redis/client": "5.8.1", + "@redis/json": "5.8.1", + "@redis/search": "5.8.1", + "@redis/time-series": "5.8.1" }, "engines": { "node": ">= 18" diff --git a/packages/search/lib/commands/SEARCH.spec.ts b/packages/search/lib/commands/SEARCH.spec.ts index ab480808ff..97e1a9a988 100644 --- a/packages/search/lib/commands/SEARCH.spec.ts +++ b/packages/search/lib/commands/SEARCH.spec.ts @@ -326,5 +326,84 @@ describe('FT.SEARCH', () => { } ); }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('properly parse content/nocontent scenarios', async client => { + + const indexName = 'foo'; + await client.ft.create( + indexName, + { + itemOrder: { + type: 'NUMERIC', + SORTABLE: true, + }, + name: { + type: 'TEXT', + }, + }, + { + ON: 'HASH', + PREFIX: 'item:', + } + ); + + await client.hSet("item:1", { + itemOrder: 1, + name: "First item", + }); + + await client.hSet("item:2", { + itemOrder: 2, + name: "Second item", + }); + + await client.hSet("item:3", { + itemOrder: 3, + name: "Third item", + }); + + // Search with SORTBY and LIMIT + let result = await client.ft.search(indexName, "@itemOrder:[0 10]", { + SORTBY: { + BY: "itemOrder", + DIRECTION: "ASC", + }, + LIMIT: { + from: 0, + size: 1, // only get first result + }, + }); + + assert.equal(result.total, 3, "Result's `total` value reflects the total scanned documents"); + assert.equal(result.documents.length, 1); + let doc = result.documents[0]; + assert.equal(doc.id, 'item:1'); + assert.equal(doc.value.itemOrder, '1'); + assert.equal(doc.value.name, 'First item'); + + await client.del("item:3"); + + // Search again after removing item:3 + result = await client.ft.search(indexName, "@itemOrder:[0 10]", { + SORTBY: { + BY: "itemOrder", + DIRECTION: "ASC", + }, + LIMIT: { + from: 0, + size: 1, // only get first result + }, + }); + + assert.equal(result.total, 2, "Result's `total` value reflects the total scanned documents"); + assert.equal(result.documents.length, 1); + doc = result.documents[0]; + assert.equal(doc.id, 'item:1'); + assert.equal(doc.value.itemOrder, '1'); + assert.equal(doc.value.name, 'First item'); + + + }, GLOBAL.SERVERS.OPEN); + }); }); diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts index 61e1d8d84d..03779a446c 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -183,7 +183,8 @@ export default { }, transformReply: { 2: (reply: SearchRawReply): SearchReply => { - const withoutDocuments = (reply[0] + 1 == reply.length) + // if reply[2] is array, then we have content/documents. Otherwise, only ids + const withoutDocuments = reply.length > 2 && !Array.isArray(reply[2]); const documents = []; let i = 1; diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index ed1f864ef2..9b82816cd4 100644 --- a/packages/search/lib/test-utils.ts +++ b/packages/search/lib/test-utils.ts @@ -5,7 +5,7 @@ import { RespVersions } from '@redis/client'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2' + defaultDockerVersion: '8.2.1-pre' }); export const GLOBAL = { diff --git a/packages/search/package.json b/packages/search/package.json index 8095a418d6..866371ba11 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -14,7 +14,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" }, "devDependencies": { "@redis/test-utils": "*" diff --git a/packages/test-utils/lib/test-utils.ts b/packages/test-utils/lib/test-utils.ts index fe27bd93d3..1136449309 100644 --- a/packages/test-utils/lib/test-utils.ts +++ b/packages/test-utils/lib/test-utils.ts @@ -3,7 +3,7 @@ import TestUtils from './index' export const testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2-M01-pre' + defaultDockerVersion: '8.2.1-pre' }); diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index d454a3c6b6..2c34596163 100644 --- a/packages/time-series/lib/test-utils.ts +++ b/packages/time-series/lib/test-utils.ts @@ -4,7 +4,7 @@ import TimeSeries from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.2' + defaultDockerVersion: '8.2.1-pre' }); export const GLOBAL = { diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 0ca01943a5..b22bbd9cb0 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.8.0", + "version": "5.8.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", @@ -13,7 +13,7 @@ "release": "release-it" }, "peerDependencies": { - "@redis/client": "^5.8.0" + "@redis/client": "^5.8.1" }, "devDependencies": { "@redis/test-utils": "*"