diff --git a/lib/client.ts b/lib/client.ts index 2b5ca3e6a3..751b114fc1 100644 --- a/lib/client.ts +++ b/lib/client.ts @@ -373,13 +373,24 @@ export default class RedisClient { + let cursor = 0; + do { + const reply = await (this as any).hScan(key, cursor, options); + cursor = reply.cursor; + for (const key of reply.keys) { + yield key; + } + } while (cursor !== 0) + } + async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable { 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; + for (const member of reply.members) { + yield member; } } while (cursor !== 0) } diff --git a/lib/commands/HGETALL.ts b/lib/commands/HGETALL.ts index 2c46424bdc..8ac14ec496 100644 --- a/lib/commands/HGETALL.ts +++ b/lib/commands/HGETALL.ts @@ -1,14 +1,9 @@ +import { transformReplyTuples } from './generic-transformers'; + export const FIRST_KEY_INDEX = 1; export function transformArguments(key: string): Array { return ['HGETALL', key]; } -export function transformReply(reply: Array): Record { - const obj = Object.create(null); - for (let i = 0; i < reply.length; i += 2) { - obj[reply[i]] = reply[i + 1]; - } - - return obj; -} +export const transformReply = transformReplyTuples; diff --git a/lib/commands/HRANDFIELD.ts b/lib/commands/HRANDFIELD.ts index 558fb7e1fb..e0c6ee392d 100644 --- a/lib/commands/HRANDFIELD.ts +++ b/lib/commands/HRANDFIELD.ts @@ -1,4 +1,4 @@ -import { transformReplyString } from './generic-transformers'; +import { transformReplyStringNull } from './generic-transformers'; export const FIRST_KEY_INDEX = 1; @@ -6,4 +6,4 @@ export function transformArguments(key: string): Array { return ['HRANDFIELD', key]; } -export const transformReply = transformReplyString; \ No newline at end of file +export const transformReply = transformReplyStringNull; \ No newline at end of file diff --git a/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts b/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts index ac299d2a1f..c64043fd99 100644 --- a/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts +++ b/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts @@ -1,4 +1,4 @@ -import { transformReplyTupels, TupelsObject } from './generic-transformers'; +import { transformReplyTuplesNull } from './generic-transformers'; import { transformArguments as transformHRandFieldCountArguments } from './HRANDFIELD_COUNT'; export { FIRST_KEY_INDEX } from './HRANDFIELD_COUNT'; @@ -10,8 +10,4 @@ export function transformArguments(key: string, count: number): Array { ]; } -export function transformReply(reply: Array | null): TupelsObject | null { - if (reply === null) return null; - - return transformReplyTupels(reply); -} +export const transformReply = transformReplyTuplesNull; diff --git a/lib/commands/HSCAN.spec.ts b/lib/commands/HSCAN.spec.ts new file mode 100644 index 0000000000..7441dd48d5 --- /dev/null +++ b/lib/commands/HSCAN.spec.ts @@ -0,0 +1,77 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments, transformReply } from './HSCAN'; + +describe('HSCAN', () => { + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + transformArguments('key', 0), + ['HSCAN', 'key', '0'] + ); + }); + + it('with MATCH', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern' + }), + ['HSCAN', 'key', '0', 'MATCH', 'pattern'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + COUNT: 1 + }), + ['HSCAN', 'key', '0', 'COUNT', '1'] + ); + }); + + it('with MATCH & COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern', + COUNT: 1 + }), + ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1'] + ); + }); + }); + + describe('transformReply', () => { + it('without tuples', () => { + assert.deepEqual( + transformReply(['0', []]), + { + cursor: 0, + tuples: [] + } + ); + }); + + it('with tuples', () => { + assert.deepEqual( + transformReply(['0', ['field', 'value']]), + { + cursor: 0, + tuples: [{ + field: 'field', + value: 'value' + }] + } + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.hScan', async client => { + assert.deepEqual( + await client.hScan('key', 0), + { + cursor: 0, + tuples: [] + } + ); + }); +}); diff --git a/lib/commands/HSCAN.ts b/lib/commands/HSCAN.ts new file mode 100644 index 0000000000..15f27e705a --- /dev/null +++ b/lib/commands/HSCAN.ts @@ -0,0 +1,36 @@ +import { ScanOptions, transformScanArguments } from './generic-transformers'; + +export const FIRST_KEY_INDEX = 1; + +export const IS_READ_ONLY = true; + +export function transformArguments(key: string, cursor: number, options?: ScanOptions): Array { + return [ + 'HSCAN', + key, + ...transformScanArguments(cursor, options) + ]; +} + +interface HScanReply { + cursor: number; + tuples: Array<{ + field: string; + value: string; + }>; +} + +export function transformReply([cursor, rawTuples]: [string, Array]): HScanReply { + const parsedTuples = []; + for (let i = 0; i < rawTuples.length; i += 2) { + parsedTuples.push({ + field: rawTuples[i], + value: rawTuples[i + 1] + }); + } + + return { + cursor: Number(cursor), + tuples: parsedTuples + }; +}; diff --git a/lib/commands/SCAN.ts b/lib/commands/SCAN.ts index 36d20257e3..b51da61ac6 100644 --- a/lib/commands/SCAN.ts +++ b/lib/commands/SCAN.ts @@ -1,4 +1,4 @@ -import { ScanOptions, transformScanArguments, transformScanReply } from './generic-transformers'; +import { ScanOptions, transformScanArguments } from './generic-transformers'; export const IS_READ_ONLY = true; @@ -19,4 +19,14 @@ export function transformArguments(cursor: number, options?: ScanCommandOptions) return args; } -export const transformReply = transformScanReply; +export interface ScanReply { + cursor: number; + keys: Array; +} + +export function transformReply([cursor, keys]: [string, Array]): ScanReply { + return { + cursor: Number(cursor), + keys + }; +} diff --git a/lib/commands/SSCAN.spec.ts b/lib/commands/SSCAN.spec.ts new file mode 100644 index 0000000000..9b203ffb83 --- /dev/null +++ b/lib/commands/SSCAN.spec.ts @@ -0,0 +1,74 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments, transformReply } from './SSCAN'; + +describe('SSCAN', () => { + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + transformArguments('key', 0), + ['SSCAN', 'key', '0'] + ); + }); + + it('with MATCH', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern' + }), + ['SSCAN', 'key', '0', 'MATCH', 'pattern'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + COUNT: 1 + }), + ['SSCAN', 'key', '0', 'COUNT', '1'] + ); + }); + + it('with MATCH & COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern', + COUNT: 1 + }), + ['SSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1'] + ); + }); + }); + + describe('transformReply', () => { + it('without members', () => { + assert.deepEqual( + transformReply(['0', []]), + { + cursor: 0, + members: [] + } + ); + }); + + it('with members', () => { + assert.deepEqual( + transformReply(['0', ['member']]), + { + cursor: 0, + members: ['member'] + } + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.sScan', async client => { + assert.deepEqual( + await client.sScan('key', 0), + { + cursor: 0, + members: [] + } + ); + }); +}); diff --git a/lib/commands/SSCAN.ts b/lib/commands/SSCAN.ts index 24cd290b7b..38f31dc68c 100644 --- a/lib/commands/SSCAN.ts +++ b/lib/commands/SSCAN.ts @@ -1,4 +1,4 @@ -import { ScanOptions, transformScanArguments, transformScanReply } from './generic-transformers'; +import { ScanOptions, transformScanArguments } from './generic-transformers'; export const IS_READ_ONLY = true; @@ -10,4 +10,14 @@ export function transformArguments(key: string, cursor: number, options?: ScanOp ]; } -export const transformReply = transformScanReply; +interface SScanReply { + cursor: number; + members: Array; +} + +export function transformReply([cursor, members]: [string, Array]): SScanReply { + return { + cursor: Number(cursor), + members + }; +} diff --git a/lib/commands/ZSCAN.spec.ts b/lib/commands/ZSCAN.spec.ts index e69de29bb2..3ff0c0a52b 100644 --- a/lib/commands/ZSCAN.spec.ts +++ b/lib/commands/ZSCAN.spec.ts @@ -0,0 +1,77 @@ +import { strict as assert } from 'assert'; +import { TestRedisServers, itWithClient } from '../test-utils'; +import { transformArguments, transformReply } from './ZSCAN'; + +describe('ZSCAN', () => { + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + transformArguments('key', 0), + ['ZSCAN', 'key', '0'] + ); + }); + + it('with MATCH', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern' + }), + ['ZSCAN', 'key', '0', 'MATCH', 'pattern'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + COUNT: 1 + }), + ['ZSCAN', 'key', '0', 'COUNT', '1'] + ); + }); + + it('with MATCH & COUNT', () => { + assert.deepEqual( + transformArguments('key', 0, { + MATCH: 'pattern', + COUNT: 1 + }), + ['ZSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1'] + ); + }); + }); + + describe('transformReply', () => { + it('without members', () => { + assert.deepEqual( + transformReply(['0', []]), + { + cursor: 0, + members: [] + } + ); + }); + + it('with members', () => { + assert.deepEqual( + transformReply(['0', ['member', '-inf']]), + { + cursor: 0, + members: [{ + value: 'member', + score: -Infinity + }] + } + ); + }); + }); + + itWithClient(TestRedisServers.OPEN, 'client.zScan', async client => { + assert.deepEqual( + await client.zScan('key', 0), + { + cursor: 0, + members: [] + } + ); + }); +}); diff --git a/lib/commands/ZSCAN.ts b/lib/commands/ZSCAN.ts index 2f0bebf10c..c56061bda6 100644 --- a/lib/commands/ZSCAN.ts +++ b/lib/commands/ZSCAN.ts @@ -18,7 +18,7 @@ interface ZScanReply { } export function transformReply([cursor, rawMembers]: [string, Array]): ZScanReply { - const parsedMembers:Array = []; + const parsedMembers: Array = []; for (let i = 0; i < rawMembers.length; i += 2) { parsedMembers.push({ value: rawMembers[i], diff --git a/lib/commands/generic-transformers.spec.ts b/lib/commands/generic-transformers.spec.ts new file mode 100644 index 0000000000..71def2d8fb --- /dev/null +++ b/lib/commands/generic-transformers.spec.ts @@ -0,0 +1,274 @@ +import { strict as assert } from 'assert'; +import { + transformReplyBoolean, + transformReplyBooleanArray, + transformScanArguments, + transformReplyNumberInfinity, + transformReplyNumberInfinityArray, + transformReplyNumberInfinityNull, + transformArgumentNumberInfinity, + transformReplyTuples, + transformReplyTuplesNull, + transformReplyStreamMessages, + transformReplyStreamsMessages, + transformReplyStreamsMessagesNull, + transformReplySortedSetWithScores +} from './generic-transformers'; + +describe('Generic Transformers', () => { + describe('transformReplyBoolean', () => { + it('0', () => { + assert.equal( + transformReplyBoolean(0), + false + ); + }); + + it('1', () => { + assert.equal( + transformReplyBoolean(1), + true + ); + }); + }); + + describe('transformReplyBooleanArray', () => { + it('empty array', () => { + assert.deepEqual( + transformReplyBooleanArray([]), + [] + ); + }); + + it('0, 1', () => { + assert.deepEqual( + transformReplyBooleanArray([0, 1]), + [false, true] + ); + }); + }); + + describe('transformScanArguments', () => { + it('cusror only', () => { + assert.deepEqual( + transformScanArguments(0), + ['0'] + ); + }); + + it('with MATCH', () => { + assert.deepEqual( + transformScanArguments(0, { + MATCH: 'pattern' + }), + ['0', 'MATCH', 'pattern'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + transformScanArguments(0, { + COUNT: 1 + }), + ['0', 'COUNT', '1'] + ); + }); + + it('with MATCH & COUNT', () => { + assert.deepEqual( + transformScanArguments(0, { + MATCH: 'pattern', + COUNT: 1 + }), + ['0', 'MATCH', 'pattern', 'COUNT', '1'] + ); + }); + }); + + describe('transformReplyNumberInfinity', () => { + it('0.5', () => { + assert.equal( + transformReplyNumberInfinity('0.5'), + 0.5 + ); + }); + + it('+inf', () => { + assert.equal( + transformReplyNumberInfinity('+inf'), + Infinity + ); + }); + + it('-inf', () => { + assert.equal( + transformReplyNumberInfinity('-inf'), + -Infinity + ); + }); + }); + + describe('transformReplyNumberInfinityArray', () => { + it('empty array', () => { + assert.deepEqual( + transformReplyNumberInfinityArray([]), + [] + ); + }); + + it('0.5, +inf, -inf', () => { + assert.deepEqual( + transformReplyNumberInfinityArray(['0.5', '+inf', '-inf']), + [0.5, Infinity, -Infinity] + ); + }); + }); + + it('transformReplyNumberInfinityNull', () => { + assert.equal( + transformReplyNumberInfinityNull(null), + null + ); + }); + + describe('transformArgumentNumberInfinity', () => { + it('0.5', () => { + assert.equal( + transformArgumentNumberInfinity(0.5), + '0.5' + ); + }); + + it('Infinity', () => { + assert.equal( + transformArgumentNumberInfinity(Infinity), + '+inf' + ); + }); + + it('-Infinity', () => { + assert.equal( + transformArgumentNumberInfinity(-Infinity), + '-inf' + ); + }); + }); + + it('transformReplyTuples', () => { + assert.deepEqual( + transformReplyTuples(['key1', 'value1', 'key2', 'value2']), + Object.create(null, { + key1: { + value: 'value1', + configurable: true, + enumerable: true + }, + key2: { + value: 'value2', + configurable: true, + enumerable: true + } + }) + ); + }); + + it('transformReplyTuplesNull', () => { + assert.equal( + transformReplyTuplesNull(null), + null + ); + }); + + it('transformReplyStreamMessages', () => { + assert.deepEqual( + transformReplyStreamMessages(['0-0', ['0key', '0value'], '1-0', ['1key', '1value']]), + [{ + id: '0-0', + message: Object.create(null, { + '0key': { + value: '0value', + configurable: true, + enumerable: true + } + }) + }, { + id: '1-0', + message: Object.create(null, { + '1key': { + value: '1value', + configurable: true, + enumerable: true + } + }) + }] + ); + }); + + it('transformReplyStreamsMessages', () => { + assert.deepEqual( + transformReplyStreamsMessages([['stream1', ['0-1', ['11key', '11value'], '1-1', ['12key', '12value']]], ['stream2', ['0-2', ['2key1', '2value1', '2key2', '2value2']]]]), + [{ + name: 'stream1', + messages: [{ + id: '0-1', + message: Object.create(null, { + '11key': { + value: '11value', + configurable: true, + enumerable: true + } + }) + }, { + id: '1-1', + message: Object.create(null, { + '12key': { + value: '12value', + configurable: true, + enumerable: true + } + }) + }] + }, { + name: 'stream2', + messages: [{ + id: '0-2', + message: Object.create(null, { + '2key1': { + value: '2value1', + configurable: true, + enumerable: true + }, + '2key2': { + value: '2value2', + configurable: true, + enumerable: true + } + }) + }] + }] + ) + }); + + it('transformReplyStreamsMessagesNull', () => { + assert.equal( + transformReplyStreamsMessagesNull(null), + null + ); + }); + + it('transformReplySortedSetWithScores', () => { + assert.deepEqual( + transformReplySortedSetWithScores(['member1', '0.5', 'member2', '+inf', 'member3', '-inf']), + [{ + value: 'member1', + score: 0.5 + }, { + value: 'member2', + score: Infinity + }, { + value: 'member3', + score: -Infinity + }] + ); + }); +}); diff --git a/lib/commands/generic-transformers.ts b/lib/commands/generic-transformers.ts index 15d65c360a..10c9dbeb79 100644 --- a/lib/commands/generic-transformers.ts +++ b/lib/commands/generic-transformers.ts @@ -49,18 +49,6 @@ export function transformScanArguments(cursor: number, options?: ScanOptions): A return args; } -export interface ScanReply { - cursor: number; - keys: Array; -} - -export function transformScanReply([cursor, keys]: [string, Array]): ScanReply { - return { - cursor: Number(cursor), - keys - }; -} - export function transformReplyNumberInfinity(reply: string): number { switch (reply) { case '+inf': @@ -101,11 +89,11 @@ export function transformArgumentNumberInfinity(num: number): string { } } -export interface TupelsObject { +export interface TuplesObject { [field: string]: string; } -export function transformReplyTupels(reply: Array): TupelsObject { +export function transformReplyTuples(reply: Array): TuplesObject { const message = Object.create(null); for (let i = 0; i < reply.length; i += 2) { @@ -115,9 +103,15 @@ export function transformReplyTupels(reply: Array): TupelsObject { return message; } +export function transformReplyTuplesNull(reply: Array | null): TuplesObject | null { + if (reply === null) return null; + + return transformReplyTuples(reply); +} + export interface StreamMessageReply { id: string; - message: TupelsObject; + message: TuplesObject; } export type StreamMessagesReply = Array; @@ -128,7 +122,7 @@ export function transformReplyStreamMessages(reply: Array): StreamMessagesR for (let i = 0; i < reply.length; i += 2) { messages.push({ id: reply[i], - message: transformReplyTupels(reply[i + 1]) + message: transformReplyTuples(reply[i + 1]) }); } diff --git a/lib/commands/index.ts b/lib/commands/index.ts index 3ff07b0003..16f6476009 100644 --- a/lib/commands/index.ts +++ b/lib/commands/index.ts @@ -38,6 +38,7 @@ import * as HMGET from './HMGET'; import * as HRANDFIELD_COUNT_WITHVALUES from './HRANDFIELD_COUNT_WITHVALUES'; import * as HRANDFIELD_COUNT from './HRANDFIELD_COUNT'; import * as HRANDFIELD from './HRANDFIELD'; +import * as HSCAN from './HSCAN'; import * as HSET from './HSET'; import * as HSETNX from './HSETNX'; import * as HSTRLEN from './HSTRLEN'; @@ -244,6 +245,8 @@ export default { hRandFieldCount: HRANDFIELD_COUNT, HRANDFIELD, hRandField: HRANDFIELD, + HSCAN, + hScan: HSCAN, HSET, hSet: HSET, HSETNX,