Skip to content

Commit 2fdb6de

Browse files
authored
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
1 parent 5a0a06d commit 2fdb6de

File tree

7 files changed

+244
-1
lines changed

7 files changed

+244
-1
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { strict as assert } from "node:assert";
2+
import DELEX, { DelexCondition } from "./DELEX";
3+
import { parseArgs } from "./generic-transformers";
4+
import testUtils, { GLOBAL } from "../test-utils";
5+
6+
describe("DELEX", () => {
7+
describe("transformArguments", () => {
8+
it("no condition", () => {
9+
assert.deepEqual(parseArgs(DELEX, "key"), ["DELEX", "key"]);
10+
});
11+
12+
it("with condition", () => {
13+
assert.deepEqual(
14+
parseArgs(DELEX, "key", {
15+
condition: DelexCondition.IFEQ,
16+
matchValue: "some-value",
17+
}),
18+
["DELEX", "key", "IFEQ", "some-value"]
19+
);
20+
});
21+
});
22+
23+
testUtils.testAll(
24+
"non-existing key",
25+
async (client) => {
26+
assert.equal(await client.delEx("key{tag}"), 0);
27+
},
28+
{
29+
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
30+
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
31+
}
32+
);
33+
34+
testUtils.testAll(
35+
"non-existing key with condition",
36+
async (client) => {
37+
assert.equal(
38+
await client.delEx("key{tag}", {
39+
condition: DelexCondition.IFDEQ,
40+
matchValue: "digest",
41+
}),
42+
0
43+
);
44+
},
45+
{
46+
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
47+
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
48+
}
49+
);
50+
51+
testUtils.testAll(
52+
"existing key no condition",
53+
async (client) => {
54+
await client.set("key{tag}", "value");
55+
assert.equal(await client.delEx("key{tag}"), 1);
56+
},
57+
{
58+
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
59+
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
60+
}
61+
);
62+
63+
testUtils.testAll(
64+
"existing key and condition",
65+
async (client) => {
66+
await client.set("key{tag}", "some-value");
67+
68+
assert.equal(
69+
await client.delEx("key{tag}", {
70+
condition: DelexCondition.IFEQ,
71+
matchValue: "some-value",
72+
}),
73+
1
74+
);
75+
},
76+
{
77+
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
78+
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
79+
}
80+
);
81+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { CommandParser } from "../client/parser";
2+
import { NumberReply, Command, RedisArgument } from "../RESP/types";
3+
4+
export const DelexCondition = {
5+
/**
6+
* Delete if value equals match-value.
7+
*/
8+
IFEQ: "IFEQ",
9+
/**
10+
* Delete if value does not equal match-value.
11+
*/
12+
IFNE: "IFNE",
13+
/**
14+
* Delete if value digest equals match-digest.
15+
*/
16+
IFDEQ: "IFDEQ",
17+
/**
18+
* Delete if value digest does not equal match-digest.
19+
*/
20+
IFDNE: "IFDNE",
21+
} as const;
22+
23+
type DelexCondition = (typeof DelexCondition)[keyof typeof DelexCondition];
24+
25+
export default {
26+
IS_READ_ONLY: false,
27+
/**
28+
* Conditionally removes the specified key based on value or digest comparison.
29+
*
30+
* @param parser - The Redis command parser
31+
* @param key - Key to delete
32+
*/
33+
parseCommand(
34+
parser: CommandParser,
35+
key: RedisArgument,
36+
options?: {
37+
/**
38+
* The condition to apply when deleting the key.
39+
* - `IFEQ` - Delete if value equals match-value
40+
* - `IFNE` - Delete if value does not equal match-value
41+
* - `IFDEQ` - Delete if value digest equals match-digest
42+
* - `IFDNE` - Delete if value digest does not equal match-digest
43+
*/
44+
condition: DelexCondition;
45+
/**
46+
* The value or digest to compare against
47+
*/
48+
matchValue: RedisArgument;
49+
}
50+
) {
51+
parser.push("DELEX");
52+
parser.pushKey(key);
53+
54+
if (options) {
55+
parser.push(options.condition);
56+
parser.push(options.matchValue);
57+
}
58+
},
59+
transformReply: undefined as unknown as () => NumberReply<1 | 0>,
60+
} as const satisfies Command;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { strict as assert } from "node:assert";
2+
import DIGEST from "./DIGEST";
3+
import { parseArgs } from "./generic-transformers";
4+
import testUtils, { GLOBAL } from "../test-utils";
5+
6+
describe("DIGEST", () => {
7+
describe("transformArguments", () => {
8+
it("digest", () => {
9+
assert.deepEqual(parseArgs(DIGEST, "key"), ["DIGEST", "key"]);
10+
});
11+
});
12+
13+
testUtils.testAll(
14+
"existing key",
15+
async (client) => {
16+
await client.set("key{tag}", "value");
17+
assert.equal(typeof await client.digest("key{tag}"), "string");
18+
},
19+
{
20+
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
21+
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
22+
}
23+
);
24+
25+
testUtils.testAll(
26+
"non-existing key",
27+
async (client) => {
28+
assert.equal(await client.digest("key{tag}"), null);
29+
},
30+
{
31+
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
32+
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
33+
}
34+
);
35+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { CommandParser } from "../client/parser";
2+
import { Command, RedisArgument, SimpleStringReply } from "../RESP/types";
3+
4+
export default {
5+
IS_READ_ONLY: true,
6+
/**
7+
* Returns the XXH3 hash of a string value.
8+
*
9+
* @param parser - The Redis command parser
10+
* @param key - Key to get the digest of
11+
*/
12+
parseCommand(parser: CommandParser, key: RedisArgument) {
13+
parser.push("DIGEST");
14+
parser.pushKey(key);
15+
},
16+
transformReply: undefined as unknown as () => SimpleStringReply,
17+
} as const satisfies Command;

packages/client/lib/commands/SET.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
import { strict as assert } from 'node:assert';
23
import testUtils, { GLOBAL } from '../test-utils';
34
import SET from './SET';
@@ -127,6 +128,16 @@ describe('SET', () => {
127128
['SET', 'key', 'value', 'XX']
128129
);
129130
});
131+
132+
it('with IFDEQ condition', () => {
133+
assert.deepEqual(
134+
parseArgs(SET, 'key', 'value', {
135+
condition: 'IFDEQ',
136+
matchValue: 'some-value'
137+
}),
138+
['SET', 'key', 'value', 'IFDEQ', 'some-value']
139+
);
140+
});
130141
});
131142

132143
it('with GET', () => {
@@ -162,4 +173,19 @@ describe('SET', () => {
162173
client: GLOBAL.SERVERS.OPEN,
163174
cluster: GLOBAL.CLUSTERS.OPEN
164175
});
176+
177+
testUtils.testAll('set with IFEQ', async client => {
178+
await client.set('key{tag}', 'some-value');
179+
180+
assert.equal(
181+
await client.set('key{tag}', 'some-value', {
182+
condition: 'IFEQ',
183+
matchValue: 'some-value'
184+
}),
185+
'OK'
186+
);
187+
}, {
188+
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
189+
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
190+
});
165191
});

packages/client/lib/commands/SET.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,22 @@ export interface SetOptions {
2929
*/
3030
KEEPTTL?: boolean;
3131

32-
condition?: 'NX' | 'XX';
32+
/**
33+
* Condition for setting the key:
34+
* - `NX` - Set if key does not exist
35+
* - `XX` - Set if key already exists
36+
* - `IFEQ` - Set if current value equals match-value (since 8.4, requires `matchValue`)
37+
* - `IFNE` - Set if current value does not equal match-value (since 8.4, requires `matchValue`)
38+
* - `IFDEQ` - Set if current value digest equals match-digest (since 8.4, requires `matchValue`)
39+
* - `IFDNE` - Set if current value digest does not equal match-digest (since 8.4, requires `matchValue`)
40+
*/
41+
condition?: 'NX' | 'XX' | 'IFEQ' | 'IFNE' | 'IFDEQ' | 'IFDNE';
42+
43+
/**
44+
* Value or digest to compare against. Required when using `IFEQ`, `IFNE`, `IFDEQ`, or `IFDNE` conditions.
45+
*/
46+
matchValue?: RedisArgument;
47+
3348
/**
3449
* @deprecated Use `{ condition: 'NX' }` instead.
3550
*/
@@ -82,6 +97,9 @@ export default {
8297

8398
if (options?.condition) {
8499
parser.push(options.condition);
100+
if (options?.matchValue !== undefined) {
101+
parser.push(options.matchValue);
102+
}
85103
} else if (options?.NX) {
86104
parser.push('NX');
87105
} else if (options?.XX) {

packages/client/lib/commands/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ import DBSIZE from './DBSIZE';
8484
import DECR from './DECR';
8585
import DECRBY from './DECRBY';
8686
import DEL from './DEL';
87+
import DELEX from './DELEX';
88+
import DIGEST from './DIGEST';
8789
import DUMP from './DUMP';
8890
import ECHO from './ECHO';
8991
import EVAL_RO from './EVAL_RO';
@@ -543,6 +545,10 @@ export default {
543545
decrBy: DECRBY,
544546
DEL,
545547
del: DEL,
548+
DELEX,
549+
delEx: DELEX,
550+
DIGEST,
551+
digest: DIGEST,
546552
DUMP,
547553
dump: DUMP,
548554
ECHO,

0 commit comments

Comments
 (0)