1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-11 22:42:42 +03:00

implement some GEO commands, improve scan generic transformer, expose RPUSHX

This commit is contained in:
leibale
2021-07-19 16:39:09 -04:00
parent c72aab2fc2
commit a10b276144
24 changed files with 924 additions and 59 deletions

View File

@@ -179,12 +179,20 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
for (const [name, script] of Object.entries(this.#options.scripts)) { for (const [name, script] of Object.entries(this.#options.scripts)) {
(this as any)[name] = async function (...args: Parameters<typeof script.transformArguments>): Promise<ReturnType<typeof script.transformReply>> { (this as any)[name] = async function (...args: Parameters<typeof script.transformArguments>): Promise<ReturnType<typeof script.transformReply>> {
const options = isCommandOptions(args[0]) && args[0]; let options;
if (isCommandOptions<ClientCommandOptions>(args[0])) {
options = args[0];
args = args.slice(1);
}
const transformedArguments = script.transformArguments(...args);
return script.transformReply( return script.transformReply(
await this.executeScript( await this.executeScript(
script, script,
...script.transformArguments(...(options ? args.slice(1) : args)) transformedArguments,
) options
),
transformedArguments.preserve
); );
}; };
} }
@@ -373,11 +381,13 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
args = args.slice(1); args = args.slice(1);
} }
const transformedArguments = command.transformArguments(...args);
return command.transformReply( return command.transformReply(
await this.#sendCommand( await this.#sendCommand(
command.transformArguments(...args), transformedArguments,
options options
) ),
transformedArguments.preserve
); );
} }

View File

@@ -67,14 +67,21 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
for (const [name, script] of Object.entries(this.#options.scripts)) { for (const [name, script] of Object.entries(this.#options.scripts)) {
(this as any)[name] = async function (...args: Parameters<typeof script.transformArguments>): Promise<ReturnType<typeof script.transformReply>> { (this as any)[name] = async function (...args: Parameters<typeof script.transformArguments>): Promise<ReturnType<typeof script.transformReply>> {
const options = isCommandOptions(args[0]) && args[0]; let options;
if (isCommandOptions<ClientCommandOptions>(args[0])) {
options = args[0];
args = args.slice(1);
}
const transformedArguments = script.transformArguments(...args);
return script.transformReply( return script.transformReply(
await this.executeScript( await this.executeScript(
script, script,
args, args,
script.transformArguments(...(options ? args.slice(1) : args)), transformedArguments,
options options
) ),
transformedArguments.preserve
); );
}; };
} }
@@ -121,7 +128,8 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
command.IS_READ_ONLY, command.IS_READ_ONLY,
redisArgs, redisArgs,
options options
) ),
redisArgs.preserve
); );
} }

View File

@@ -0,0 +1,95 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils';
import { transformArguments } from './GEOADD';
describe('GEOADD', () => {
describe('transformArguments', () => {
it('one member', () => {
assert.deepEqual(
transformArguments('key', {
member: 'member',
longitude: 1,
latitude: 2
}),
['GEOADD', 'key', '1', '2', 'member']
);
});
it('multiple members', () => {
assert.deepEqual(
transformArguments('key', [{
longitude: 1,
latitude: 2,
member: '3',
}, {
longitude: 4,
latitude: 5,
member: '6',
}]),
['GEOADD', 'key', '1', '2', '3', '4', '5', '6']
);
});
it('with NX', () => {
assert.deepEqual(
transformArguments('key', {
longitude: 1,
latitude: 2,
member: 'member'
}, {
NX: true
}),
['GEOADD', 'key', 'NX', '1', '2', 'member']
);
});
it('with CH', () => {
assert.deepEqual(
transformArguments('key', {
longitude: 1,
latitude: 2,
member: 'member'
}, {
CH: true
}),
['GEOADD', 'key', 'CH', '1', '2', 'member']
);
});
it('with XX, CH', () => {
assert.deepEqual(
transformArguments('key', {
longitude: 1,
latitude: 2,
member: 'member'
}, {
XX: true,
CH: true
}),
['GEOADD', 'key', 'XX', 'CH', '1', '2', 'member']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.geoAdd', async client => {
assert.equal(
await client.geoAdd('key', {
member: 'member',
longitude: 1,
latitude: 2
}),
1
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.geoAdd', async cluster => {
assert.equal(
await cluster.geoAdd('key', {
member: 'member',
longitude: 1,
latitude: 2
}),
1
);
});
});

49
lib/commands/GEOADD.ts Normal file
View File

@@ -0,0 +1,49 @@
import { GeoCoordinates, transformReplyNumber } from './generic-transformers';
interface GeoMember extends GeoCoordinates {
member: string;
}
interface NX {
NX?: true;
}
interface XX {
XX?: true;
}
type SetGuards = NX | XX;
interface GeoAddCommonOptions {
CH?: true;
}
type GeoAddOptions = SetGuards & GeoAddCommonOptions;
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, toAdd: GeoMember | Array<GeoMember>, options?: GeoAddOptions): Array<string> {
const args = ['GEOADD', key];
if ((options as NX)?.NX) {
args.push('NX');
} else if ((options as XX)?.XX) {
args.push('XX');
}
if (options?.CH) {
args.push('CH');
}
for (const { longitude, latitude, member } of (Array.isArray(toAdd) ? toAdd : [toAdd])) {
args.push(
longitude.toString(),
latitude.toString(),
member
);
}
return args;
}
export const transformReply = transformReplyNumber;

View File

@@ -0,0 +1,26 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils';
import { transformArguments } from './GEODIST';
describe('GEODIST', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', '1', '2'),
['GEODIST', 'key', '1', '2']
);
});
itWithClient(TestRedisServers.OPEN, 'client.geoDist', async client => {
assert.equal(
await client.geoDist('key', '1', '2'),
null
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.geoDist', async cluster => {
assert.equal(
await cluster.geoDist('key', '1', '2'),
null
);
});
});

24
lib/commands/GEODIST.ts Normal file
View File

@@ -0,0 +1,24 @@
import { GeoUnits } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
export function transformArguments(
key: string,
member1: string,
member2: string,
unit?: GeoUnits
): Array<string> {
const args = ['GEODIST', key, member1, member2];
if (unit) {
args.push(unit);
}
return args;
}
export function transformReply(reply: string | null): number | null {
return reply === null ? null : Number(reply);
}

View File

@@ -0,0 +1,35 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils';
import { transformArguments } from './GEOHASH';
describe('GEOHASH', () => {
describe('transformArguments', () => {
it('single member', () => {
assert.deepEqual(
transformArguments('key', 'member'),
['GEOHASH', 'key', 'member']
);
});
it('multiple members', () => {
assert.deepEqual(
transformArguments('key', ['1', '2']),
['GEOHASH', 'key', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.geoHash', async client => {
assert.deepEqual(
await client.geoHash('key', 'member'),
[null]
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.geoHash', async cluster => {
assert.deepEqual(
await cluster.geoHash('key', 'member'),
[null]
);
});
});

19
lib/commands/GEOHASH.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(key: string, member: string | Array<string>): Array<string> {
const args = ['GEOHASH', key];
if (typeof member === 'string') {
args.push(member);
} else {
args.push(...member);
}
return args;
}
export const transformReply = transformReplyStringArray;

View File

@@ -0,0 +1,35 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils';
import { transformArguments } from './GEOPOS';
describe('GEOPOS', () => {
describe('transformArguments', () => {
it('single member', () => {
assert.deepEqual(
transformArguments('key', 'member'),
['GEOPOS', 'key', 'member']
);
});
it('multiple members', () => {
assert.deepEqual(
transformArguments('key', ['1', '2']),
['GEOPOS', 'key', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.geoPos', async client => {
assert.deepEqual(
await client.geoPos('key', 'member'),
[null]
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.geoPos', async cluster => {
assert.deepEqual(
await cluster.geoPos('key', 'member'),
[null]
);
});
});

27
lib/commands/GEOPOS.ts Normal file
View File

@@ -0,0 +1,27 @@
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
export function transformArguments(key: string, member: string | Array<string>): Array<string> {
const args = ['GEOPOS', key];
if (typeof member === 'string') {
args.push(member);
} else {
args.push(...member);
}
return args;
}
interface GeoCoordinates {
longitude: string;
latitude: string;
}
export function transformReply(reply: Array<[string, string] | null>): Array<GeoCoordinates | null> {
return reply.map(coordinates => coordinates === null ? null : {
longitude: coordinates[0],
latitude: coordinates[1]
});
}

View File

@@ -0,0 +1,35 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils';
import { transformArguments } from './GEOSEARCH';
describe('GEOSEARCH', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'member', {
radius: 1,
unit: 'm'
}),
['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm']
);
});
itWithClient(TestRedisServers.OPEN, 'client.geoSearch', async client => {
assert.deepEqual(
await client.geoSearch('key', 'member', {
radius: 1,
unit: 'm'
}),
[]
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.geoSearch', async cluster => {
assert.deepEqual(
await cluster.geoSearch('key', 'member', {
radius: 1,
unit: 'm'
}),
[]
);
});
});

16
lib/commands/GEOSEARCH.ts Normal file
View File

@@ -0,0 +1,16 @@
import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, pushGeoSearchArguments, transformReplyStringArray } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
export function transformArguments(
key: string,
from: GeoSearchFrom,
by: GeoSearchBy,
options?: GeoSearchOptions
): Array<string> {
return pushGeoSearchArguments(['GEOSEARCH'], key, from, by, options);
}
export const transformReply = transformReplyStringArray;

View File

@@ -0,0 +1,41 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils';
import { transformArguments } from './GEOSEARCHSTORE';
describe('GEOSEARCHSTORE', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('destination', 'source', 'member', {
radius: 1,
unit: 'm'
}, {
SORT: 'ASC',
COUNT: {
value: 1,
ANY: true
}
}),
['GEOSEARCHSTORE', 'destination', 'source', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'ASC', 'COUNT', '1', 'ANY']
);
});
itWithClient(TestRedisServers.OPEN, 'client.geoSearchStore', async client => {
assert.equal(
await client.geoSearchStore('source', 'destination', 'member', {
radius: 1,
unit: 'm'
}),
0
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.geoSearchStore', async cluster => {
assert.equal(
await cluster.geoSearchStore('{tag}source', '{tag}destination', 'member', {
radius: 1,
unit: 'm'
}),
0
);
});
});

View File

@@ -0,0 +1,40 @@
import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, pushGeoSearchArguments, transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
interface GeoSearchStoreOptions extends GeoSearchOptions {
STOREDIST?: true;
}
export function transformArguments(
destination: string,
source: string,
from: GeoSearchFrom,
by: GeoSearchBy,
options?: GeoSearchStoreOptions
): Array<string> {
const args = pushGeoSearchArguments(
['GEOSEARCHSTORE', destination],
source,
from,
by,
options
);
if (options?.STOREDIST) {
args.push('STOREDIST');
}
return args;
}
// in versions 6.2.0-6.2.4 Redis will return an empty array when `src` is empty
// TODO: issue/PR
export function transformReply(reply: number | []): number {
if (typeof reply === 'number') return reply;
return 0;
}

View File

@@ -0,0 +1,40 @@
import { strict as assert } from 'assert';
import { TransformArgumentsReply } from '.';
import { TestRedisServers, itWithClient, TestRedisClusters, itWithCluster } from '../test-utils';
import { GeoReplyWith } from './generic-transformers';
import { transformArguments } from './GEOSEARCH_WITH';
describe('GEOSEARCH WITH', () => {
it('transformArguments', () => {
const expectedReply: TransformArgumentsReply = ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'WITHDIST']
expectedReply.preserve = ['WITHDIST'];
assert.deepEqual(
transformArguments('key', 'member', {
radius: 1,
unit: 'm'
}, [GeoReplyWith.DISTANCE]),
expectedReply
);
});
itWithClient(TestRedisServers.OPEN, 'client.geoSearchWith', async client => {
assert.deepEqual(
await client.geoSearchWith('key', 'member', {
radius: 1,
unit: 'm'
}, [GeoReplyWith.DISTANCE]),
[]
);
});
itWithCluster(TestRedisClusters.OPEN, 'cluster.geoSearchWith', async cluster => {
assert.deepEqual(
await cluster.geoSearchWith('key', 'member', {
radius: 1,
unit: 'm'
}, [GeoReplyWith.DISTANCE]),
[]
);
});
});

View File

@@ -0,0 +1,23 @@
import { TransformArgumentsReply } from '.';
import { GeoSearchFrom, GeoSearchBy, GeoReplyWith, GeoSearchOptions, transformGeoMembersWithReply } from './generic-transformers';
import { transformArguments as geoSearchTransformArguments } from './GEOSEARCH';
export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEOSEARCH';
export function transformArguments(
key: string,
from: GeoSearchFrom,
by: GeoSearchBy,
replyWith: Array<GeoReplyWith>,
options?: GeoSearchOptions
): TransformArgumentsReply {
const args: TransformArgumentsReply = geoSearchTransformArguments(key, from, by, options);
args.push(...replyWith);
args.preserve = replyWith;
return args;
}
export const transformReply = transformGeoMembersWithReply;

View File

@@ -1,15 +1,14 @@
import { ScanOptions, transformScanArguments } from './generic-transformers'; import { ScanOptions, pushScanArguments } from './generic-transformers';
export const FIRST_KEY_INDEX = 1; export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true; export const IS_READ_ONLY = true;
export function transformArguments(key: string, cursor: number, options?: ScanOptions): Array<string> { export function transformArguments(key: string, cursor: number, options?: ScanOptions): Array<string> {
return [ return pushScanArguments([
'HSCAN', 'HSCAN',
key, key
...transformScanArguments(cursor, options) ], cursor, options);
];
} }
export interface HScanTuple { export interface HScanTuple {

View File

@@ -1,16 +1,12 @@
import { ScanOptions, transformScanArguments } from './generic-transformers'; import { ScanOptions, pushScanArguments } from './generic-transformers';
export const IS_READ_ONLY = true; export const IS_READ_ONLY = true;
export interface ScanCommandOptions extends ScanOptions { export interface ScanCommandOptions extends ScanOptions {
TYPE?: string; TYPE?: string;
} }
export function transformArguments(cursor: number, options?: ScanCommandOptions): Array<string> { export function transformArguments(cursor: number, options?: ScanCommandOptions): Array<string> {
const args = [ const args = pushScanArguments(['SCAN'], cursor, options);
'SCAN',
...transformScanArguments(cursor, options)
];
if (options?.TYPE) { if (options?.TYPE) {
args.push('TYPE', options.TYPE); args.push('TYPE', options.TYPE);

View File

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

View File

@@ -1,15 +1,14 @@
import { ScanOptions, transformReplyNumberInfinity, transformScanArguments, ZMember } from './generic-transformers'; import { ScanOptions, transformReplyNumberInfinity, pushScanArguments, ZMember } from './generic-transformers';
export const FIRST_KEY_INDEX = 1; export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true; export const IS_READ_ONLY = true;
export function transformArguments(key: string, cursor: number, options?: ScanOptions): Array<string> { export function transformArguments(key: string, cursor: number, options?: ScanOptions): Array<string> {
return [ return pushScanArguments([
'ZSCAN', 'ZSCAN',
key, key
...transformScanArguments(cursor, options) ], cursor, options);
];
} }
interface ZScanReply { interface ZScanReply {

View File

@@ -1,8 +1,9 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import { isKeyObject } from 'util/types';
import { import {
transformReplyBoolean, transformReplyBoolean,
transformReplyBooleanArray, transformReplyBooleanArray,
transformScanArguments, pushScanArguments,
transformReplyNumberInfinity, transformReplyNumberInfinity,
transformReplyNumberInfinityArray, transformReplyNumberInfinityArray,
transformReplyNumberInfinityNull, transformReplyNumberInfinityNull,
@@ -12,7 +13,11 @@ import {
transformReplyStreamMessages, transformReplyStreamMessages,
transformReplyStreamsMessages, transformReplyStreamsMessages,
transformReplyStreamsMessagesNull, transformReplyStreamsMessagesNull,
transformReplySortedSetWithScores transformReplySortedSetWithScores,
pushGeoCountArgument,
pushGeoSearchArguments,
transformGeoMembersWithReply,
GeoReplyWith
} from './generic-transformers'; } from './generic-transformers';
describe('Generic Transformers', () => { describe('Generic Transformers', () => {
@@ -48,17 +53,17 @@ describe('Generic Transformers', () => {
}); });
}); });
describe('transformScanArguments', () => { describe('pushScanArguments', () => {
it('cusror only', () => { it('cusror only', () => {
assert.deepEqual( assert.deepEqual(
transformScanArguments(0), pushScanArguments([], 0),
['0'] ['0']
); );
}); });
it('with MATCH', () => { it('with MATCH', () => {
assert.deepEqual( assert.deepEqual(
transformScanArguments(0, { pushScanArguments([], 0, {
MATCH: 'pattern' MATCH: 'pattern'
}), }),
['0', 'MATCH', 'pattern'] ['0', 'MATCH', 'pattern']
@@ -67,7 +72,7 @@ describe('Generic Transformers', () => {
it('with COUNT', () => { it('with COUNT', () => {
assert.deepEqual( assert.deepEqual(
transformScanArguments(0, { pushScanArguments([], 0, {
COUNT: 1 COUNT: 1
}), }),
['0', 'COUNT', '1'] ['0', 'COUNT', '1']
@@ -76,7 +81,7 @@ describe('Generic Transformers', () => {
it('with MATCH & COUNT', () => { it('with MATCH & COUNT', () => {
assert.deepEqual( assert.deepEqual(
transformScanArguments(0, { pushScanArguments([], 0, {
MATCH: 'pattern', MATCH: 'pattern',
COUNT: 1 COUNT: 1
}), }),
@@ -271,4 +276,190 @@ describe('Generic Transformers', () => {
}] }]
); );
}); });
describe('pushGeoCountArgument', () => {
it('undefined', () => {
assert.deepEqual(
pushGeoCountArgument([], undefined),
[]
);
});
it('number', () => {
assert.deepEqual(
pushGeoCountArgument([], 1),
['COUNT', '1']
);
});
it('with ANY', () => {
assert.deepEqual(
pushGeoCountArgument([], {
value: 1,
ANY: true
}),
['COUNT', '1', 'ANY']
);
});
});
describe('pushGeoSearchArguments', () => {
it('FROMMEMBER, BYRADIUS', () => {
assert.deepEqual(
pushGeoSearchArguments([], 'key', 'member', {
radius: 1,
unit: 'm'
}),
['key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm']
);
});
it('FROMLONLAT, BYBOX', () => {
assert.deepEqual(
pushGeoSearchArguments([], 'key', {
longitude: 1,
latitude: 2
}, {
width: 1,
height: 2,
unit: 'm'
}),
['key', 'FROMLONLAT', '1', '2', 'BYBOX', '1', '2', 'm']
);
});
it('with SORT', () => {
assert.deepEqual(
pushGeoSearchArguments([], 'key', 'member', {
radius: 1,
unit: 'm'
}, {
SORT: 'ASC'
}),
['key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'ASC']
);
});
});
describe('transformGeoMembersWithReply', () => {
it('DISTANCE', () => {
assert.deepEqual(
transformGeoMembersWithReply([
[
'1',
'2'
],
[
'3',
'4'
]
], [GeoReplyWith.DISTANCE]),
[{
member: '1',
distance: '2'
}, {
member: '3',
distance: '4'
}]
);
});
it('HASH', () => {
assert.deepEqual(
transformGeoMembersWithReply([
[
'1',
2
],
[
'3',
4
]
], [GeoReplyWith.HASH]),
[{
member: '1',
hash: 2
}, {
member: '3',
hash: 4
}]
);
});
it('COORDINATES', () => {
assert.deepEqual(
transformGeoMembersWithReply([
[
'1',
[
'2',
'3'
]
],
[
'4',
[
'5',
'6'
]
]
], [GeoReplyWith.COORDINATES]),
[{
member: '1',
coordinates: {
longitude: '2',
latitude: '3'
}
}, {
member: '4',
coordinates: {
longitude: '5',
latitude: '6'
}
}]
);
});
it('DISTANCE, HASH, COORDINATES', () => {
assert.deepEqual(
transformGeoMembersWithReply([
[
'1',
'2',
3,
[
'4',
'5'
]
],
[
'6',
'7',
8,
[
'9',
'10'
]
]
], [GeoReplyWith.DISTANCE, GeoReplyWith.HASH, GeoReplyWith.COORDINATES]),
[{
member: '1',
distance: '2',
hash: 3,
coordinates: {
longitude: '4',
latitude: '5'
}
}, {
member: '6',
distance: '7',
hash: 8,
coordinates: {
longitude: '9',
latitude: '10'
}
}]
);
});
});
}); });

View File

@@ -51,8 +51,8 @@ export interface ScanOptions {
COUNT?: number; COUNT?: number;
} }
export function transformScanArguments(cursor: number, options?: ScanOptions): Array<string> { export function pushScanArguments(args: Array<string>, cursor: number, options?: ScanOptions): Array<string> {
const args = [cursor.toString()]; args.push(cursor.toString());
if (options?.MATCH) { if (options?.MATCH) {
args.push('MATCH', options.MATCH); args.push('MATCH', options.MATCH);
@@ -180,3 +180,134 @@ export function transformReplySortedSetWithScores(reply: Array<string>): Array<Z
return members; return members;
} }
type GeoCountArgument = number | {
value: number;
ANY?: true
};
export function pushGeoCountArgument(args: Array<string>, count: GeoCountArgument | undefined): Array<string> {
if (typeof count === 'number') {
args.push('COUNT', count.toString());
} else if (count) {
args.push('COUNT', count.value.toString());
if (count.ANY) {
args.push('ANY');
}
}
return args;
}
export type GeoUnits = 'm' | 'km' | 'mi' | 'ft';
export interface GeoCoordinates {
longitude: string | number;
latitude: string | number;
}
type GeoSearchFromMember = string;
export type GeoSearchFrom = GeoSearchFromMember | GeoCoordinates;
interface GeoSearchByRadius {
radius: number;
unit: GeoUnits;
}
interface GeoSearchByBox {
width: number;
height: number;
unit: GeoUnits;
}
export type GeoSearchBy = GeoSearchByRadius | GeoSearchByBox;
export interface GeoSearchOptions {
SORT?: 'ASC' | 'DESC';
COUNT?: GeoCountArgument;
}
export function pushGeoSearchArguments(
args: Array<string>,
key: string,
from: GeoSearchFrom,
by: GeoSearchBy,
options?: GeoSearchOptions
): Array<string> {
args.push(key);
if (typeof from === 'string') {
args.push('FROMMEMBER', from);
} else {
args.push('FROMLONLAT', from.longitude.toString(), from.latitude.toString());
}
if ('radius' in by) {
args.push('BYRADIUS', by.radius.toString());
} else {
args.push('BYBOX', by.width.toString(), by.height.toString());
}
if (by.unit) {
args.push(by.unit);
}
if (options?.SORT) {
args.push(options?.SORT);
}
pushGeoCountArgument(args, options?.COUNT);
return args;
}
export enum GeoReplyWith {
DISTANCE = 'WITHDIST',
HASH = 'WITHHASH',
COORDINATES = 'WITHCOORD'
}
export interface GeoReplyWithMember {
member: string;
distance?: number;
hash?: string;
coordinates?: {
longitude: string;
latitude: string;
};
}
export function transformGeoMembersWithReply(reply: Array<Array<any>>, replyWith: Array<GeoReplyWith>): Array<GeoReplyWithMember> {
const replyWithSet = new Set(replyWith);
let index = 0,
distanceIndex = replyWithSet.has(GeoReplyWith.DISTANCE) && ++index,
hashIndex = replyWithSet.has(GeoReplyWith.HASH) && ++index,
coordinatesIndex = replyWithSet.has(GeoReplyWith.COORDINATES) && ++index;
return reply.map(member => {
const transformedMember: GeoReplyWithMember = {
member: member[0]
};
if (distanceIndex) {
transformedMember.distance = member[distanceIndex];
}
if (hashIndex) {
transformedMember.hash = member[hashIndex];
}
if (coordinatesIndex) {
const [longitude, latitude] = member[coordinatesIndex];
transformedMember.coordinates = {
longitude,
latitude
};
}
return transformedMember;
});
}

View File

@@ -50,6 +50,13 @@ import * as EXPIREAT from './EXPIREAT';
import * as FAILOVER from './FAILOVER'; import * as FAILOVER from './FAILOVER';
import * as FLUSHALL from './FLUSHALL'; import * as FLUSHALL from './FLUSHALL';
import * as FLUSHDB from './FLUSHDB'; import * as FLUSHDB from './FLUSHDB';
import * as GEOADD from './GEOADD';
import * as GEODIST from './GEODIST';
import * as GEOHASH from './GEOHASH';
import * as GEOPOS from './GEOPOS';
import * as GEOSEARCH_WITH from './GEOSEARCH_WITH';
import * as GEOSEARCH from './GEOSEARCH';
import * as GEOSEARCHSTORE from './GEOSEARCHSTORE';
import * as GET from './GET'; import * as GET from './GET';
import * as GETBIT from './GETBIT'; import * as GETBIT from './GETBIT';
import * as GETDEL from './GETDEL'; import * as GETDEL from './GETDEL';
@@ -311,6 +318,20 @@ export default {
flushAll: FLUSHALL, flushAll: FLUSHALL,
FLUSHDB, FLUSHDB,
flushDb: FLUSHDB, flushDb: FLUSHDB,
GEOADD,
geoAdd: GEOADD,
GEODIST,
geoDist: GEODIST,
GEOHASH,
geoHash: GEOHASH,
GEOPOS,
geoPos: GEOPOS,
GEOSEARCH_WITH,
geoSearchWith: GEOSEARCH_WITH,
GEOSEARCH,
geoSearch: GEOSEARCH,
GEOSEARCHSTORE,
geoSearchStore: GEOSEARCHSTORE,
GET, GET,
get: GET, get: GET,
GETBIT, GETBIT,
@@ -624,11 +645,13 @@ export default {
export type RedisReply = string | number | Array<RedisReply> | null | undefined; export type RedisReply = string | number | Array<RedisReply> | null | undefined;
export type TransformArgumentsReply = Array<string> & { preserve?: unknown };
export interface RedisCommand { export interface RedisCommand {
FIRST_KEY_INDEX?: number | ((...args: Array<any>) => string); FIRST_KEY_INDEX?: number | ((...args: Array<any>) => string);
IS_READ_ONLY?: boolean; IS_READ_ONLY?: boolean;
transformArguments(...args: Array<any>): Array<string>; transformArguments(...args: Array<any>): TransformArgumentsReply;
transformReply(reply: RedisReply): any; transformReply(reply: RedisReply, preserved: unknown): any;
} }
export interface RedisCommands { export interface RedisCommands {

View File

@@ -1,4 +1,4 @@
import COMMANDS from './commands'; import COMMANDS, { TransformArgumentsReply } from './commands';
import { RedisCommand, RedisModules, RedisReply } from './commands'; import { RedisCommand, RedisModules, RedisReply } from './commands';
import RedisCommandsQueue from './commands-queue'; import RedisCommandsQueue from './commands-queue';
import { RedisLuaScript, RedisLuaScripts } from './lua-script'; import { RedisLuaScript, RedisLuaScripts } from './lua-script';
@@ -24,6 +24,7 @@ export type RedisMultiCommandType<M extends RedisModules, S extends RedisLuaScri
export interface MultiQueuedCommand { export interface MultiQueuedCommand {
encodedCommand: string; encodedCommand: string;
preservedArguments?: unknown;
transformReply?: RedisCommand['transformReply']; transformReply?: RedisCommand['transformReply'];
} }
@@ -166,9 +167,10 @@ export default class RedisMultiCommand<M extends RedisModules = RedisModules, S
} }
} }
addCommand(args: Array<string>, transformReply?: RedisCommand['transformReply']): RedisMultiCommandType<M, S> { addCommand(args: TransformArgumentsReply, transformReply?: RedisCommand['transformReply']): RedisMultiCommandType<M, S> {
this.#queue.push({ this.#queue.push({
encodedCommand: RedisCommandsQueue.encodeCommand(args), encodedCommand: RedisCommandsQueue.encodeCommand(args),
preservedArguments: args.preserve,
transformReply transformReply
}); });
@@ -224,8 +226,8 @@ export default class RedisMultiCommand<M extends RedisModules = RedisModules, S
const rawReplies = await this.#executor(queue, Symbol('[RedisMultiCommand] Chain ID')); const rawReplies = await this.#executor(queue, Symbol('[RedisMultiCommand] Chain ID'));
return (rawReplies[rawReplies.length - 1]! as Array<RedisReply>).map((reply, i) => { return (rawReplies[rawReplies.length - 1]! as Array<RedisReply>).map((reply, i) => {
const { transformReply } = queue[i + 1]; const { transformReply, preservedArguments } = queue[i + 1];
return transformReply ? transformReply(reply) : reply; return transformReply ? transformReply(reply, preservedArguments) : reply;
}); });
} }