You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-12-14 09:42:12 +03:00
feat(client): add CAS/CAD, DELEX, DIGEST support (#3123)
* feat: add digest command and tests * feat: add delex command and tests * feat: add more conditional options to SET update tests
This commit is contained in:
81
packages/client/lib/commands/DELEX.spec.ts
Normal file
81
packages/client/lib/commands/DELEX.spec.ts
Normal file
@@ -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] },
|
||||
}
|
||||
);
|
||||
});
|
||||
60
packages/client/lib/commands/DELEX.ts
Normal file
60
packages/client/lib/commands/DELEX.ts
Normal file
@@ -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;
|
||||
35
packages/client/lib/commands/DIGEST.spec.ts
Normal file
35
packages/client/lib/commands/DIGEST.spec.ts
Normal file
@@ -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] },
|
||||
}
|
||||
);
|
||||
});
|
||||
17
packages/client/lib/commands/DIGEST.ts
Normal file
17
packages/client/lib/commands/DIGEST.ts
Normal file
@@ -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;
|
||||
@@ -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] },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user