diff --git a/packages/client/lib/commands/DELEX.spec.ts b/packages/client/lib/commands/DELEX.spec.ts new file mode 100644 index 0000000000..ec948801d6 --- /dev/null +++ b/packages/client/lib/commands/DELEX.spec.ts @@ -0,0 +1,81 @@ +import { strict as assert } from "node:assert"; +import DELEX, { DelexCondition } from "./DELEX"; +import { parseArgs } from "./generic-transformers"; +import testUtils, { GLOBAL } from "../test-utils"; + +describe("DELEX", () => { + describe("transformArguments", () => { + it("no condition", () => { + assert.deepEqual(parseArgs(DELEX, "key"), ["DELEX", "key"]); + }); + + it("with condition", () => { + assert.deepEqual( + parseArgs(DELEX, "key", { + condition: DelexCondition.IFEQ, + matchValue: "some-value", + }), + ["DELEX", "key", "IFEQ", "some-value"] + ); + }); + }); + + testUtils.testAll( + "non-existing key", + async (client) => { + assert.equal(await client.delEx("key{tag}"), 0); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] }, + } + ); + + testUtils.testAll( + "non-existing key with condition", + async (client) => { + assert.equal( + await client.delEx("key{tag}", { + condition: DelexCondition.IFDEQ, + matchValue: "digest", + }), + 0 + ); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] }, + } + ); + + testUtils.testAll( + "existing key no condition", + async (client) => { + await client.set("key{tag}", "value"); + assert.equal(await client.delEx("key{tag}"), 1); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] }, + } + ); + + testUtils.testAll( + "existing key and condition", + async (client) => { + await client.set("key{tag}", "some-value"); + + assert.equal( + await client.delEx("key{tag}", { + condition: DelexCondition.IFEQ, + matchValue: "some-value", + }), + 1 + ); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] }, + } + ); +}); diff --git a/packages/client/lib/commands/DELEX.ts b/packages/client/lib/commands/DELEX.ts new file mode 100644 index 0000000000..e32c292f22 --- /dev/null +++ b/packages/client/lib/commands/DELEX.ts @@ -0,0 +1,60 @@ +import { CommandParser } from "../client/parser"; +import { NumberReply, Command, RedisArgument } from "../RESP/types"; + +export const DelexCondition = { + /** + * Delete if value equals match-value. + */ + IFEQ: "IFEQ", + /** + * Delete if value does not equal match-value. + */ + IFNE: "IFNE", + /** + * Delete if value digest equals match-digest. + */ + IFDEQ: "IFDEQ", + /** + * Delete if value digest does not equal match-digest. + */ + IFDNE: "IFDNE", +} as const; + +type DelexCondition = (typeof DelexCondition)[keyof typeof DelexCondition]; + +export default { + IS_READ_ONLY: false, + /** + * Conditionally removes the specified key based on value or digest comparison. + * + * @param parser - The Redis command parser + * @param key - Key to delete + */ + parseCommand( + parser: CommandParser, + key: RedisArgument, + options?: { + /** + * The condition to apply when deleting the key. + * - `IFEQ` - Delete if value equals match-value + * - `IFNE` - Delete if value does not equal match-value + * - `IFDEQ` - Delete if value digest equals match-digest + * - `IFDNE` - Delete if value digest does not equal match-digest + */ + condition: DelexCondition; + /** + * The value or digest to compare against + */ + matchValue: RedisArgument; + } + ) { + parser.push("DELEX"); + parser.pushKey(key); + + if (options) { + parser.push(options.condition); + parser.push(options.matchValue); + } + }, + transformReply: undefined as unknown as () => NumberReply<1 | 0>, +} as const satisfies Command; diff --git a/packages/client/lib/commands/DIGEST.spec.ts b/packages/client/lib/commands/DIGEST.spec.ts new file mode 100644 index 0000000000..d89ba9d05a --- /dev/null +++ b/packages/client/lib/commands/DIGEST.spec.ts @@ -0,0 +1,35 @@ +import { strict as assert } from "node:assert"; +import DIGEST from "./DIGEST"; +import { parseArgs } from "./generic-transformers"; +import testUtils, { GLOBAL } from "../test-utils"; + +describe("DIGEST", () => { + describe("transformArguments", () => { + it("digest", () => { + assert.deepEqual(parseArgs(DIGEST, "key"), ["DIGEST", "key"]); + }); + }); + + testUtils.testAll( + "existing key", + async (client) => { + await client.set("key{tag}", "value"); + assert.equal(typeof await client.digest("key{tag}"), "string"); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] }, + } + ); + + testUtils.testAll( + "non-existing key", + async (client) => { + assert.equal(await client.digest("key{tag}"), null); + }, + { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] }, + } + ); +}); diff --git a/packages/client/lib/commands/DIGEST.ts b/packages/client/lib/commands/DIGEST.ts new file mode 100644 index 0000000000..249bb78740 --- /dev/null +++ b/packages/client/lib/commands/DIGEST.ts @@ -0,0 +1,17 @@ +import { CommandParser } from "../client/parser"; +import { Command, RedisArgument, SimpleStringReply } from "../RESP/types"; + +export default { + IS_READ_ONLY: true, + /** + * Returns the XXH3 hash of a string value. + * + * @param parser - The Redis command parser + * @param key - Key to get the digest of + */ + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push("DIGEST"); + parser.pushKey(key); + }, + transformReply: undefined as unknown as () => SimpleStringReply, +} as const satisfies Command; diff --git a/packages/client/lib/commands/SET.spec.ts b/packages/client/lib/commands/SET.spec.ts index b8aa57fe77..61fe986320 100644 --- a/packages/client/lib/commands/SET.spec.ts +++ b/packages/client/lib/commands/SET.spec.ts @@ -1,3 +1,4 @@ + import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SET from './SET'; @@ -127,6 +128,16 @@ describe('SET', () => { ['SET', 'key', 'value', 'XX'] ); }); + + it('with IFDEQ condition', () => { + assert.deepEqual( + parseArgs(SET, 'key', 'value', { + condition: 'IFDEQ', + matchValue: 'some-value' + }), + ['SET', 'key', 'value', 'IFDEQ', 'some-value'] + ); + }); }); it('with GET', () => { @@ -162,4 +173,19 @@ describe('SET', () => { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN }); + + testUtils.testAll('set with IFEQ', async client => { + await client.set('key{tag}', 'some-value'); + + assert.equal( + await client.set('key{tag}', 'some-value', { + condition: 'IFEQ', + matchValue: 'some-value' + }), + 'OK' + ); + }, { + client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] }, + cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] }, + }); }); diff --git a/packages/client/lib/commands/SET.ts b/packages/client/lib/commands/SET.ts index d138425567..16a2a0216c 100644 --- a/packages/client/lib/commands/SET.ts +++ b/packages/client/lib/commands/SET.ts @@ -29,7 +29,22 @@ export interface SetOptions { */ KEEPTTL?: boolean; - condition?: 'NX' | 'XX'; + /** + * Condition for setting the key: + * - `NX` - Set if key does not exist + * - `XX` - Set if key already exists + * - `IFEQ` - Set if current value equals match-value (since 8.4, requires `matchValue`) + * - `IFNE` - Set if current value does not equal match-value (since 8.4, requires `matchValue`) + * - `IFDEQ` - Set if current value digest equals match-digest (since 8.4, requires `matchValue`) + * - `IFDNE` - Set if current value digest does not equal match-digest (since 8.4, requires `matchValue`) + */ + condition?: 'NX' | 'XX' | 'IFEQ' | 'IFNE' | 'IFDEQ' | 'IFDNE'; + + /** + * Value or digest to compare against. Required when using `IFEQ`, `IFNE`, `IFDEQ`, or `IFDNE` conditions. + */ + matchValue?: RedisArgument; + /** * @deprecated Use `{ condition: 'NX' }` instead. */ @@ -82,6 +97,9 @@ export default { if (options?.condition) { parser.push(options.condition); + if (options?.matchValue !== undefined) { + parser.push(options.matchValue); + } } else if (options?.NX) { parser.push('NX'); } else if (options?.XX) { diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index 54ede43d01..27d3d329ed 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -84,6 +84,8 @@ import DBSIZE from './DBSIZE'; import DECR from './DECR'; import DECRBY from './DECRBY'; import DEL from './DEL'; +import DELEX from './DELEX'; +import DIGEST from './DIGEST'; import DUMP from './DUMP'; import ECHO from './ECHO'; import EVAL_RO from './EVAL_RO'; @@ -543,6 +545,10 @@ export default { decrBy: DECRBY, DEL, del: DEL, + DELEX, + delEx: DELEX, + DIGEST, + digest: DIGEST, DUMP, dump: DUMP, ECHO,