Skip to content

Commit 09f2eba

Browse files
ytkimirtiCahidArda
andauthored
feat: add withtypes option to the scan command (#1376)
* feat: add withtypes option to the scan command * fix: use describe instead of test and fix tests --------- Co-authored-by: CahidArda <cahidardaooz@hotmail.com>
1 parent c21ca07 commit 09f2eba

File tree

5 files changed

+135
-19
lines changed

5 files changed

+135
-19
lines changed

pkg/commands/scan.test.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { keygen, newHttpClient, randomID } from "../test-utils";
22

3-
import { afterAll, expect, test } from "bun:test";
3+
import { afterAll, describe, expect, test } from "bun:test";
44
import { FlushDBCommand } from "./flushdb";
5+
import type { ScanResultWithType } from "./scan";
56
import { ScanCommand } from "./scan";
67
import { SetCommand } from "./set";
78
import { TypeCommand } from "./type";
@@ -10,7 +11,7 @@ const client = newHttpClient();
1011

1112
const { newKey, cleanup } = keygen();
1213
afterAll(cleanup);
13-
test("without options", () => {
14+
describe("without options", () => {
1415
test("returns cursor and keys", async () => {
1516
const key = newKey();
1617
const value = randomID();
@@ -26,7 +27,7 @@ test("without options", () => {
2627
});
2728
});
2829

29-
test("with match", () => {
30+
describe("with match", () => {
3031
test("returns cursor and keys", async () => {
3132
const key = newKey();
3233
const value = randomID();
@@ -36,7 +37,7 @@ test("with match", () => {
3637
const found: string[] = [];
3738
do {
3839
const res = await new ScanCommand([cursor, { match: key }]).exec(client);
39-
expect(typeof res[0]).toEqual("number");
40+
expect(typeof res[0]).toEqual("string");
4041
cursor = res[0];
4142
found.push(...res[1]);
4243
} while (cursor !== "0");
@@ -45,7 +46,7 @@ test("with match", () => {
4546
});
4647
});
4748

48-
test("with count", () => {
49+
describe("with count", () => {
4950
test("returns cursor and keys", async () => {
5051
const key = newKey();
5152
const value = randomID();
@@ -63,7 +64,7 @@ test("with count", () => {
6364
});
6465
});
6566

66-
test("with type", () => {
67+
describe("with type", () => {
6768
test("returns cursor and keys", async () => {
6869
await new FlushDBCommand([]).exec(client);
6970
const key1 = newKey();
@@ -89,3 +90,46 @@ test("with type", () => {
8990
}
9091
});
9192
});
93+
94+
describe("with withType", () => {
95+
test("returns cursor and keys with types", async () => {
96+
await new FlushDBCommand([]).exec(client);
97+
const stringKey = newKey();
98+
const zsetKey = newKey();
99+
const value = randomID();
100+
101+
// Add different types of keys
102+
await new SetCommand([stringKey, value]).exec(client);
103+
await new ZAddCommand([zsetKey, { score: 1, member: "abc" }]).exec(client);
104+
105+
// Scan with WITHTYPE option
106+
let cursor = "0";
107+
let foundStringKey;
108+
let foundZsetKey;
109+
110+
do {
111+
// Use the generic type parameter to specify the return type
112+
const res = await new ScanCommand<ScanResultWithType>([cursor, { withType: true }]).exec(
113+
client
114+
);
115+
116+
cursor = res[0];
117+
const items = res[1];
118+
119+
// Find our test keys in the results
120+
if (!foundStringKey) {
121+
foundStringKey = items.find((item) => item.key === stringKey && item.type === "string");
122+
}
123+
124+
if (!foundZsetKey) {
125+
foundZsetKey = items.find((item) => item.key === zsetKey && item.type === "zset");
126+
}
127+
} while (cursor !== "0");
128+
129+
// Verify types are correct
130+
expect(foundStringKey).toBeDefined();
131+
expect(foundZsetKey).toBeDefined();
132+
expect(foundStringKey?.type).toEqual("string");
133+
expect(foundZsetKey?.type).toEqual("zset");
134+
});
135+
});

pkg/commands/scan.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,42 @@
1-
import { deserializeScanResponse } from "../util";
1+
import { deserializeScanResponse, deserializeScanWithTypesResponse } from "../util";
22
import type { CommandOptions } from "./command";
33
import { Command } from "./command";
44

5-
export type ScanCommandOptions = {
5+
export type ScanCommandOptionsStandard = {
66
match?: string;
77
count?: number;
88
type?: string;
9+
withType?: false;
910
};
11+
12+
export type ScanCommandOptionsWithType = {
13+
match?: string;
14+
count?: number;
15+
/**
16+
* Includes types of each key in the result
17+
*
18+
* @example
19+
* ```typescript
20+
* await redis.scan("0", { withType: true })
21+
* // ["0", [{ key: "key1", type: "string" }, { key: "key2", type: "list" }]]
22+
* ```
23+
*/
24+
withType: true;
25+
};
26+
27+
export type ScanCommandOptions = ScanCommandOptionsStandard | ScanCommandOptionsWithType;
28+
29+
export type ScanResultStandard = [string, string[]];
30+
31+
export type ScanResultWithType = [string, { key: string; type: string }[]];
32+
1033
/**
1134
* @see https://redis.io/commands/scan
1235
*/
13-
export class ScanCommand extends Command<[string, string[]], [string, string[]]> {
36+
export class ScanCommand<TData = ScanResultStandard> extends Command<[string, string[]], TData> {
1437
constructor(
1538
[cursor, opts]: [cursor: string | number, opts?: ScanCommandOptions],
16-
cmdOpts?: CommandOptions<[string, string[]], [string, string[]]>
39+
cmdOpts?: CommandOptions<[string, string[]], TData>
1740
) {
1841
const command: (number | string)[] = ["scan", cursor];
1942
if (opts?.match) {
@@ -22,11 +45,17 @@ export class ScanCommand extends Command<[string, string[]], [string, string[]]>
2245
if (typeof opts?.count === "number") {
2346
command.push("count", opts.count);
2447
}
25-
if (opts?.type && opts.type.length > 0) {
48+
49+
// Handle type and withType options
50+
if (opts && "withType" in opts && opts.withType === true) {
51+
command.push("withtype");
52+
} else if (opts && "type" in opts && opts.type && opts.type.length > 0) {
2653
command.push("type", opts.type);
2754
}
55+
2856
super(command, {
29-
deserialize: deserializeScanResponse,
57+
// @ts-expect-error ignore types here
58+
deserialize: opts?.withType ? deserializeScanWithTypesResponse : deserializeScanResponse,
3059
...cmdOpts,
3160
});
3261
}

pkg/redis.test.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { keygen, newHttpClient, randomID } from "./test-utils";
33

44
import { afterEach, describe, expect, test } from "bun:test";
55
import { HttpClient } from "./http";
6+
import type { ScanResultStandard, ScanResultWithType } from "./commands/scan";
67
const client = newHttpClient();
78

89
const { newKey, cleanup } = keygen();
@@ -75,7 +76,7 @@ describe("when destructuring the redis class", () => {
7576
});
7677
});
7778

78-
test("zadd", () => {
79+
describe("zadd", () => {
7980
test("adds the set", async () => {
8081
const key = newKey();
8182
const score = 1;
@@ -86,7 +87,7 @@ test("zadd", () => {
8687
});
8788
});
8889

89-
test("zrange", () => {
90+
describe("zrange", () => {
9091
test("returns the range", async () => {
9192
const key = newKey();
9293
const score = 1;
@@ -98,7 +99,7 @@ test("zrange", () => {
9899
});
99100
});
100101

101-
test("middleware", () => {
102+
describe("middleware", () => {
102103
let state = false;
103104
test("before", async () => {
104105
const r = new Redis(client);
@@ -128,7 +129,7 @@ test("middleware", () => {
128129
});
129130
});
130131

131-
test("special data", () => {
132+
describe("special data", () => {
132133
test("with %", async () => {
133134
const key = newKey();
134135
const value = "%%12";
@@ -184,7 +185,7 @@ test("special data", () => {
184185
});
185186
});
186187

187-
test("disable base64 encoding", () => {
188+
describe("disable base64 encoding", () => {
188189
test("emojis", async () => {
189190
const key = newKey();
190191
const value = "😀";
@@ -248,3 +249,19 @@ describe("tests with latency logging", () => {
248249
expect(res).toEqual(value);
249250
});
250251
});
252+
253+
const assertIsType = <T>(_arg: () => T) => {};
254+
255+
describe("return type of scan withType", () => {
256+
test("should return cursor and keys with types", async () => {
257+
const redis = new Redis(client);
258+
259+
assertIsType<Promise<ScanResultStandard>>(() => redis.scan("0"));
260+
261+
assertIsType<Promise<ScanResultStandard>>(() => redis.scan("0", {}));
262+
263+
assertIsType<Promise<ScanResultStandard>>(() => redis.scan("0", { withType: false }));
264+
265+
assertIsType<Promise<ScanResultWithType>>(() => redis.scan("0", { withType: true }));
266+
});
267+
});

pkg/redis.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import type {
55
SetCommandOptions,
66
ZAddCommandOptions,
77
ZRangeCommandOptions,
8+
ScanCommandOptions,
9+
ScanResultStandard,
10+
ScanResultWithType,
811
} from "./commands/mod";
912
import {
1013
AppendCommand,
@@ -1100,8 +1103,17 @@ export class Redis {
11001103
/**
11011104
* @see https://redis.io/commands/scan
11021105
*/
1103-
scan = (...args: CommandArgs<typeof ScanCommand>) =>
1104-
new ScanCommand(args, this.opts).exec(this.client);
1106+
scan(cursor: string | number): Promise<ScanResultStandard>;
1107+
scan<TOptions extends ScanCommandOptions>(
1108+
cursor: string | number,
1109+
opts: TOptions
1110+
): Promise<TOptions extends { withType: true } ? ScanResultWithType : ScanResultStandard>;
1111+
scan<TOptions extends ScanCommandOptions>(
1112+
cursor: string | number,
1113+
opts?: TOptions
1114+
): Promise<TOptions extends { withType: true } ? ScanResultWithType : ScanResultStandard> {
1115+
return new ScanCommand([cursor, opts], this.opts).exec(this.client);
1116+
}
11051117

11061118
/**
11071119
* @see https://redis.io/commands/scard

pkg/util.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ScanResultWithType } from "./commands/scan";
2+
13
function parseRecursive(obj: unknown): unknown {
24
const parsed = Array.isArray(obj)
35
? obj.map((o) => {
@@ -41,6 +43,18 @@ export function deserializeScanResponse<TResult>(result: [string, ...any]): TRes
4143
return [result[0], ...parseResponse<any[]>(result.slice(1))] as TResult;
4244
}
4345

46+
export function deserializeScanWithTypesResponse(result: [string, string[]]): ScanResultWithType {
47+
const [cursor, keys] = result;
48+
49+
const parsedKeys: ScanResultWithType[1] = [];
50+
51+
for (let i = 0; i < keys.length; i += 2) {
52+
parsedKeys.push({ key: keys[i], type: keys[i + 1] });
53+
}
54+
55+
return [cursor, parsedKeys];
56+
}
57+
4458
/**
4559
* Merges multiple Records of headers into a single Record
4660
* Later headers take precedence over earlier ones.

0 commit comments

Comments
 (0)