1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-17 19:41:06 +03:00

add support for all set commands (including sScanIterator)

This commit is contained in:
leibale
2021-06-09 13:57:39 -04:00
parent b3eeb41f58
commit c15a8c4304
36 changed files with 729 additions and 28 deletions

View File

@@ -323,4 +323,23 @@ describe('Client', () => {
keys.sort()
);
});
itWithClient(TestRedisServers.OPEN, 'sScanIterator', async client => {
const keys = [];
for (let i = 0; i < 100; i++) {
keys.push(i.toString());
}
await client.sAdd('key', keys);
const set = new Set();
for await (const key of client.sScanIterator('key')) {
set.add(key);
}
assert.deepEqual(
[...set].sort(),
keys.sort()
);
});
});

View File

@@ -6,7 +6,8 @@ import RedisMultiCommand, { MultiQueuedCommand, RedisMultiCommandType } from './
import EventEmitter from 'events';
import { CommandOptions, commandOptions, isCommandOptions } from './command-options';
import { RedisLuaScript, RedisLuaScripts } from './lua-script';
import { ScanOptions } from './commands/SCAN';
import { ScanOptions } from './commands/generic-transformers';
import { ScanCommandOptions } from './commands/SCAN';
export interface RedisClientOptions<M = RedisModules, S = RedisLuaScripts> {
socket?: RedisSocketOptions;
@@ -348,7 +349,7 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
return new this.#Multi();
}
async* scanIterator(options?: ScanOptions): AsyncIterable<string> {
async* scanIterator(options?: ScanCommandOptions): AsyncIterable<string> {
let cursor = 0;
do {
const reply = await (this as any).scan(cursor, options);
@@ -357,7 +358,18 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
yield key;
}
} while (cursor !== 0)
}
}
async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable<string> {
let cursor = 0;
do {
const reply = await (this as any).sScan(key, cursor, options);
cursor = reply.cursor;
for (const key of reply.keys) {
yield key;
}
} while (cursor !== 0)
}
disconnect(): Promise<void> {
this.#queue.flushAll(new Error('Disconnecting'));

28
lib/commands/SADD.spec.ts Normal file
View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SADD';
describe('SADD', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('key', 'member'),
['SADD', 'key', 'member']
);
});
it('array', () => {
assert.deepEqual(
transformArguments('key', ['1', '2']),
['SADD', 'key', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.sAdd', async client => {
assert.equal(
await client.sAdd('key', 'member'),
1
);
});
});

17
lib/commands/SADD.ts Normal file
View File

@@ -0,0 +1,17 @@
import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, members: string | Array<string>): Array<string> {
const args = ['SADD', key];
if (typeof members === 'string') {
args.push(members);
} else {
args.push(...members);
}
return args;
}
export const transformReply = transformReplyNumber;

View File

@@ -1,22 +1,17 @@
import { ScanOptions, transformScanArguments, transformScanReply } from './generic-transformers';
export const IS_READ_ONLY = true;
export interface ScanOptions {
MATCH?: string;
COUNT?: number;
export interface ScanCommandOptions extends ScanOptions {
TYPE?: string;
}
export function transformArguments(cursor: number, options?: ScanOptions): Array<string> {
const args = ['SCAN', cursor.toString()];
if (options?.MATCH) {
args.push('MATCH', options.MATCH);
}
if (options?.COUNT) {
args.push('COUNT', options.COUNT.toString());
}
export function transformArguments(cursor: number, options?: ScanCommandOptions): Array<string> {
const args = [
'SCAN',
...transformScanArguments(cursor, options)
];
if (options?.TYPE) {
args.push('TYPE', options.TYPE);
}
@@ -24,14 +19,4 @@ export function transformArguments(cursor: number, options?: ScanOptions): Array
return args;
}
interface ScanReply {
cursor: number;
keys: Array<string>
}
export function transformReply(reply: [string, Array<string>]): ScanReply {
return {
cursor: Number(reply[0]),
keys: reply[1]
};
}
export const transformReply = transformScanReply;

View File

@@ -0,0 +1,19 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SCARD';
describe('SCARD', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key'),
['SCARD', 'key']
);
});
itWithClient(TestRedisServers.OPEN, 'client.sCard', async client => {
assert.equal(
await client.sCard('key'),
0
);
});
});

9
lib/commands/SCARD.ts Normal file
View File

@@ -0,0 +1,9 @@
import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string): Array<string> {
return ['SCARD', key];
}
export const transformReply = transformReplyNumber;

View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SDIFF';
describe('SDIFF', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('key'),
['SDIFF', 'key']
);
});
it('array', () => {
assert.deepEqual(
transformArguments(['1', '2']),
['SDIFF', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.sDiff', async client => {
assert.deepEqual(
await client.sDiff('key'),
[]
);
});
});

17
lib/commands/SDIFF.ts Normal file
View File

@@ -0,0 +1,17 @@
import { transformReplyStringArray } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(keys: string | Array<string>): Array<string> {
const args = ['SDIFF'];
if (typeof keys === 'string') {
args.push(keys);
} else {
args.push(...keys);
}
return args;
}
export const transformReply = transformReplyStringArray;

View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SDIFFSTORE';
describe('SDIFFSTORE', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('destination', 'key'),
['SDIFFSTORE', 'destination', 'key']
);
});
it('array', () => {
assert.deepEqual(
transformArguments('destination', ['1', '2']),
['SDIFFSTORE', 'destination', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.sDiffStore', async client => {
assert.equal(
await client.sDiffStore('destination', 'key'),
0
);
});
});

View File

@@ -0,0 +1,17 @@
import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(destination: string, keys: string | Array<string>): Array<string> {
const args = ['SDIFFSTORE', destination];
if (typeof keys === 'string') {
args.push(keys);
} else {
args.push(...keys);
}
return args;
}
export const transformReply = transformReplyNumber;

View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SINTER';
describe('SINTER', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('key'),
['SINTER', 'key']
);
});
it('array', () => {
assert.deepEqual(
transformArguments(['1', '2']),
['SINTER', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.sInter', async client => {
assert.deepEqual(
await client.sInter('key'),
[]
);
});
});

17
lib/commands/SINTER.ts Normal file
View File

@@ -0,0 +1,17 @@
import { transformReplyStringArray } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(keys: string | Array<string>): Array<string> {
const args = ['SINTER'];
if (typeof keys === 'string') {
args.push(keys);
} else {
args.push(...keys);
}
return args;
}
export const transformReply = transformReplyStringArray;

View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SINTERSTORE';
describe('SINTERSTORE', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('destination', 'key'),
['SINTERSTORE', 'destination', 'key']
);
});
it('array', () => {
assert.deepEqual(
transformArguments('destination', ['1', '2']),
['SINTERSTORE', 'destination', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.sInterStore', async client => {
assert.equal(
await client.sInterStore('destination', 'key'),
0
);
});
});

View File

@@ -0,0 +1,17 @@
import { transformReplyStringArray } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(destination: string, keys: string | Array<string>): Array<string> {
const args = ['SINTERSTORE', destination];
if (typeof keys === 'string') {
args.push(keys);
} else {
args.push(...keys);
}
return args;
}
export const transformReply = transformReplyStringArray;

View File

@@ -0,0 +1,19 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SISMEMBER';
describe('SISMEMBER', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'member'),
['SISMEMBER', 'key', 'member']
);
});
itWithClient(TestRedisServers.OPEN, 'client.sIsMember', async client => {
assert.equal(
await client.sIsMember('key', 'member'),
false
);
});
});

View File

@@ -0,0 +1,9 @@
import { transformReplyBoolean, transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, member: string): Array<string> {
return ['SISMEMBER', key, member];
}
export const transformReply = transformReplyBoolean;

View File

@@ -0,0 +1,19 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SMEMBERS';
describe('SMEMBERS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key'),
['SMEMBERS', 'key']
);
});
itWithClient(TestRedisServers.OPEN, 'client.sMembers', async client => {
assert.deepEqual(
await client.sMembers('key'),
[]
);
});
});

9
lib/commands/SMEMBERS.ts Normal file
View File

@@ -0,0 +1,9 @@
import { transformReplyNumber, transformReplyStringArray } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string): Array<string> {
return ['SMEMBERS', key];
}
export const transformReply = transformReplyStringArray;

View File

@@ -0,0 +1,19 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SMISMEMBER';
describe('SMISMEMBER', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', ['1', '2']),
['SMISMEMBER', 'key', '1', '2']
);
});
itWithClient(TestRedisServers.OPEN, 'client.smIsMember', async client => {
assert.deepEqual(
await client.smIsMember('key', ['1', '2']),
[false, false]
);
});
});

View File

@@ -0,0 +1,9 @@
import { transformReplyBooleanArray } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, members: Array<string>): Array<string> {
return ['SMISMEMBER', key, ...members];
}
export const transformReply = transformReplyBooleanArray;

View File

@@ -0,0 +1,19 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SMOVE';
describe('SMOVE', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('source', 'destination', 'member'),
['SMOVE', 'source', 'destination', 'member']
);
});
itWithClient(TestRedisServers.OPEN, 'client.sMove', async client => {
assert.equal(
await client.sMove('source', 'destination', 'member'),
false
);
});
});

9
lib/commands/SMOVE.ts Normal file
View File

@@ -0,0 +1,9 @@
import { transformReplyBoolean } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(source: string, destination: string, member: string): Array<string> {
return ['SMOVE', source, destination, member];
}
export const transformReply = transformReplyBoolean;

28
lib/commands/SPOP.spec.ts Normal file
View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SPOP';
describe('SPOP', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments('key'),
['SPOP', 'key']
);
});
it('with count', () => {
assert.deepEqual(
transformArguments('key', 2),
['SPOP', 'key', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.sPop', async client => {
assert.equal(
await client.sPop('key'),
null
);
});
});

15
lib/commands/SPOP.ts Normal file
View File

@@ -0,0 +1,15 @@
import { transformReplyStringArray } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, count?: number): Array<string> {
const args = ['SPOP', key];
if (typeof count === 'number') {
args.push(count.toString());
}
return args;
}
export const transformReply = transformReplyStringArray;

View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SRANDMEMBER';
describe('SRANDMEMBER', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments('key'),
['SRANDMEMBER', 'key']
);
});
it('with count', () => {
assert.deepEqual(
transformArguments('key', 2),
['SRANDMEMBER', 'key', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.sRandMember', async client => {
assert.equal(
await client.sRandMember('key'),
null
);
});
});

View File

@@ -0,0 +1,16 @@
import { transformReplyStringArray } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, count?: number): Array<string> {
const args = ['SRANDMEMBER', key];
if (typeof count === 'number') {
args.push(count.toString());
}
return args;
}
// TODO: without `count` it'll return "bulk string" and not "array"
export const transformReply = transformReplyStringArray;

28
lib/commands/SREM.spec.ts Normal file
View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SREM';
describe('SREM', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('key', 'member'),
['SREM', 'key', 'member']
);
});
it('array', () => {
assert.deepEqual(
transformArguments('key', ['1', '2']),
['SREM', 'key', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.sRem', async client => {
assert.equal(
await client.sRem('key', 'member'),
0
);
});
});

17
lib/commands/SREM.ts Normal file
View File

@@ -0,0 +1,17 @@
import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, members: string | Array<string>): Array<string> {
const args = ['SREM', key];
if (typeof members === 'string') {
args.push(members);
} else {
args.push(...members);
}
return args;
}
export const transformReply = transformReplyNumber;

13
lib/commands/SSCAN.ts Normal file
View File

@@ -0,0 +1,13 @@
import { ScanOptions, transformScanArguments, transformScanReply } from './generic-transformers';
export const IS_READ_ONLY = true;
export function transformArguments(key: string, cursor: number, options?: ScanOptions): Array<string> {
return [
'SSCAN',
key,
...transformScanArguments(cursor, options)
];
}
export const transformReply = transformScanReply;

View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SUNION';
describe('SUNION', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('key'),
['SUNION', 'key']
);
});
it('array', () => {
assert.deepEqual(
transformArguments(['1', '2']),
['SUNION', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.sUnion', async client => {
assert.deepEqual(
await client.sUnion('key'),
[]
);
});
});

19
lib/commands/SUNION.ts Normal file
View File

@@ -0,0 +1,19 @@
import { transformReplyStringArray } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
export function transformArguments(keys: string | Array<string>): Array<string> {
const args = ['SUNION'];
if (typeof keys === 'string') {
args.push(keys);
} else {
args.push(...keys);
}
return args;
}
export const transformReply = transformReplyStringArray;

View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './SUNIONSTORE';
describe('SUNIONSTORE', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('destination', 'key'),
['SUNIONSTORE', 'destination', 'key']
);
});
it('array', () => {
assert.deepEqual(
transformArguments('destination', ['1', '2']),
['SUNIONSTORE', 'destination', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.sUnionStore', async client => {
assert.equal(
await client.sUnionStore('destination', 'key'),
0
);
});
});

View File

@@ -0,0 +1,17 @@
import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(destination: string, keys: string | Array<string>): Array<string> {
const args = ['SUNIONSTORE', destination];
if (typeof keys === 'string') {
args.push(keys);
} else {
args.push(...keys);
}
return args;
}
export const transformReply = transformReplyNumber;

View File

@@ -13,3 +13,38 @@ export function transformReplyStringArray(reply: Array<string>): Array<string> {
export function transformReplyBoolean(reply: number): boolean {
return reply === 1;
}
export function transformReplyBooleanArray(reply: Array<number>): Array<boolean> {
return reply.map(transformReplyBoolean);
}
export interface ScanOptions {
MATCH?: string;
COUNT?: number;
}
export function transformScanArguments(cursor: number, options?: ScanOptions): Array<string> {
const args = [cursor.toString()];
if (options?.MATCH) {
args.push('MATCH', options.MATCH);
}
if (options?.COUNT) {
args.push('COUNT', options.COUNT.toString());
}
return args;
}
export interface ScanReply {
cursor: number;
keys: Array<string>
}
export function transformScanReply([cursor, keys]: [string, Array<string>]): ScanReply {
return {
cursor: Number(cursor),
keys
};
}

View File

@@ -35,7 +35,23 @@ import * as LPUSH from './LPUSH';
import * as PING from './PING';
import * as PUBLISH from './PUBLISH';
import * as READONLY from './READONLY';
import * as SADD from './SADD';
import * as SCAN from './SCAN';
import * as SCARD from './SCARD';
import * as SDIFF from './SDIFF';
import * as SDIFFSTORE from './SDIFFSTORE';
import * as SINTER from './SINTER';
import * as SINTERSTORE from './SINTERSTORE';
import * as SISMEMBER from './SISMEMBER';
import * as SMEMBERS from './SMEMBERS';
import * as SMISMEMBER from './SMISMEMBER';
import * as SMOVE from './SMOVE';
import * as SPOP from './SPOP';
import * as SRANDMEMBER from './SRANDMEMBER';
import * as SREM from './SREM';
import * as SSCAN from './SSCAN';
import * as SUNION from './SUNION';
import * as SUNIONSTORE from './SUNIONSTORE';
import * as SET from './SET';
export default {
@@ -113,8 +129,40 @@ export default {
publish: PUBLISH,
READONLY,
readOnly: READONLY,
SADD,
sAdd: SADD,
SCAN,
scan: SCAN,
SCARD,
sCard: SCARD,
SDIFF,
sDiff: SDIFF,
SDIFFSTORE,
sDiffStore: SDIFFSTORE,
SINTER,
sInter: SINTER,
SINTERSTORE,
sInterStore: SINTERSTORE,
SISMEMBER,
sIsMember: SISMEMBER,
SMEMBERS,
sMembers: SMEMBERS,
SMISMEMBER,
smIsMember: SMISMEMBER,
SMOVE,
sMove: SMOVE,
SPOP,
sPop: SPOP,
SRANDMEMBER,
sRandMember: SRANDMEMBER,
SREM,
sRem: SREM,
SSCAN,
sScan: SSCAN,
SUNION,
sUnion: SUNION,
SUNIONSTORE,
sUnionStore: SUNIONSTORE,
SET,
set: SET
};