You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-10 11:43:01 +03:00
implement some GEO commands, improve scan generic transformer, expose RPUSHX
This commit is contained in:
@@ -111,7 +111,7 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
|
||||
promises.push(resubscribePromise);
|
||||
this.#tick();
|
||||
}
|
||||
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
@@ -179,12 +179,20 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
|
||||
|
||||
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>> {
|
||||
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(
|
||||
await this.executeScript(
|
||||
script,
|
||||
...script.transformArguments(...(options ? args.slice(1) : args))
|
||||
)
|
||||
transformedArguments,
|
||||
options
|
||||
),
|
||||
transformedArguments.preserve
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -197,7 +205,7 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
|
||||
script.SHA,
|
||||
script.NUMBER_OF_KEYS.toString(),
|
||||
...args
|
||||
], options);
|
||||
], options);
|
||||
} catch (err: any) {
|
||||
if (!err?.message?.startsWith?.('NOSCRIPT')) {
|
||||
throw err;
|
||||
@@ -274,7 +282,7 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
|
||||
const handler = (...args: Array<unknown>): void => {
|
||||
(this as any).sendCommand(name, ...args);
|
||||
};
|
||||
|
||||
|
||||
if (moduleName) {
|
||||
(this as any).#v4[moduleName][name] = (this as any)[moduleName][name];
|
||||
(this as any)[moduleName][name] = handler;
|
||||
@@ -372,12 +380,14 @@ export default class RedisClient<M extends RedisModules = RedisModules, S extend
|
||||
options = args[0];
|
||||
args = args.slice(1);
|
||||
}
|
||||
|
||||
|
||||
const transformedArguments = command.transformArguments(...args);
|
||||
return command.transformReply(
|
||||
await this.#sendCommand(
|
||||
command.transformArguments(...args),
|
||||
transformedArguments,
|
||||
options
|
||||
)
|
||||
),
|
||||
transformedArguments.preserve
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -67,14 +67,21 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
|
||||
|
||||
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>> {
|
||||
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(
|
||||
await this.executeScript(
|
||||
script,
|
||||
args,
|
||||
script.transformArguments(...(options ? args.slice(1) : args)),
|
||||
transformedArguments,
|
||||
options
|
||||
)
|
||||
),
|
||||
transformedArguments.preserve
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -121,7 +128,8 @@ export default class RedisCluster<M extends RedisModules = RedisModules, S exten
|
||||
command.IS_READ_ONLY,
|
||||
redisArgs,
|
||||
options
|
||||
)
|
||||
),
|
||||
redisArgs.preserve
|
||||
);
|
||||
}
|
||||
|
||||
|
95
lib/commands/GEOADD.spec.ts
Normal file
95
lib/commands/GEOADD.spec.ts
Normal 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
49
lib/commands/GEOADD.ts
Normal 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;
|
26
lib/commands/GEODIST.spec.ts
Normal file
26
lib/commands/GEODIST.spec.ts
Normal 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
24
lib/commands/GEODIST.ts
Normal 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);
|
||||
}
|
35
lib/commands/GEOHASH.spec.ts
Normal file
35
lib/commands/GEOHASH.spec.ts
Normal 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
19
lib/commands/GEOHASH.ts
Normal 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;
|
35
lib/commands/GEOPOS.spec.ts
Normal file
35
lib/commands/GEOPOS.spec.ts
Normal 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
27
lib/commands/GEOPOS.ts
Normal 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]
|
||||
});
|
||||
}
|
35
lib/commands/GEOSEARCH.spec.ts
Normal file
35
lib/commands/GEOSEARCH.spec.ts
Normal 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
16
lib/commands/GEOSEARCH.ts
Normal 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;
|
41
lib/commands/GEOSEARCHSTORE.spec.ts
Normal file
41
lib/commands/GEOSEARCHSTORE.spec.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
40
lib/commands/GEOSEARCHSTORE.ts
Normal file
40
lib/commands/GEOSEARCHSTORE.ts
Normal 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;
|
||||
}
|
40
lib/commands/GEOSEARCH_WITH.spec.ts
Normal file
40
lib/commands/GEOSEARCH_WITH.spec.ts
Normal 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]),
|
||||
[]
|
||||
);
|
||||
});
|
||||
});
|
23
lib/commands/GEOSEARCH_WITH.ts
Normal file
23
lib/commands/GEOSEARCH_WITH.ts
Normal 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;
|
@@ -1,15 +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 function transformArguments(key: string, cursor: number, options?: ScanOptions): Array<string> {
|
||||
return [
|
||||
return pushScanArguments([
|
||||
'HSCAN',
|
||||
key,
|
||||
...transformScanArguments(cursor, options)
|
||||
];
|
||||
key
|
||||
], cursor, options);
|
||||
}
|
||||
|
||||
export interface HScanTuple {
|
||||
@@ -30,7 +29,7 @@ export function transformReply([cursor, rawTuples]: [string, Array<string>]): HS
|
||||
value: rawTuples[i + 1]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
cursor: Number(cursor),
|
||||
tuples: parsedTuples
|
||||
|
@@ -1,21 +1,17 @@
|
||||
import { ScanOptions, transformScanArguments } from './generic-transformers';
|
||||
import { ScanOptions, pushScanArguments } from './generic-transformers';
|
||||
|
||||
export const IS_READ_ONLY = true;
|
||||
|
||||
export interface ScanCommandOptions extends ScanOptions {
|
||||
TYPE?: string;
|
||||
}
|
||||
|
||||
export function transformArguments(cursor: number, options?: ScanCommandOptions): Array<string> {
|
||||
const args = [
|
||||
'SCAN',
|
||||
...transformScanArguments(cursor, options)
|
||||
];
|
||||
|
||||
const args = pushScanArguments(['SCAN'], cursor, options);
|
||||
|
||||
if (options?.TYPE) {
|
||||
args.push('TYPE', options.TYPE);
|
||||
}
|
||||
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
|
@@ -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 function transformArguments(key: string, cursor: number, options?: ScanOptions): Array<string> {
|
||||
return [
|
||||
return pushScanArguments([
|
||||
'SSCAN',
|
||||
key,
|
||||
...transformScanArguments(cursor, options)
|
||||
];
|
||||
], cursor, options);
|
||||
}
|
||||
|
||||
interface SScanReply {
|
||||
|
@@ -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 IS_READ_ONLY = true;
|
||||
|
||||
export function transformArguments(key: string, cursor: number, options?: ScanOptions): Array<string> {
|
||||
return [
|
||||
return pushScanArguments([
|
||||
'ZSCAN',
|
||||
key,
|
||||
...transformScanArguments(cursor, options)
|
||||
];
|
||||
key
|
||||
], cursor, options);
|
||||
}
|
||||
|
||||
interface ZScanReply {
|
||||
@@ -25,7 +24,7 @@ export function transformReply([cursor, rawMembers]: [string, Array<string>]): Z
|
||||
score: transformReplyNumberInfinity(rawMembers[i + 1])
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
cursor: Number(cursor),
|
||||
members: parsedMembers
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { isKeyObject } from 'util/types';
|
||||
import {
|
||||
transformReplyBoolean,
|
||||
transformReplyBooleanArray,
|
||||
transformScanArguments,
|
||||
pushScanArguments,
|
||||
transformReplyNumberInfinity,
|
||||
transformReplyNumberInfinityArray,
|
||||
transformReplyNumberInfinityNull,
|
||||
@@ -12,7 +13,11 @@ import {
|
||||
transformReplyStreamMessages,
|
||||
transformReplyStreamsMessages,
|
||||
transformReplyStreamsMessagesNull,
|
||||
transformReplySortedSetWithScores
|
||||
transformReplySortedSetWithScores,
|
||||
pushGeoCountArgument,
|
||||
pushGeoSearchArguments,
|
||||
transformGeoMembersWithReply,
|
||||
GeoReplyWith
|
||||
} from './generic-transformers';
|
||||
|
||||
describe('Generic Transformers', () => {
|
||||
@@ -48,17 +53,17 @@ describe('Generic Transformers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformScanArguments', () => {
|
||||
describe('pushScanArguments', () => {
|
||||
it('cusror only', () => {
|
||||
assert.deepEqual(
|
||||
transformScanArguments(0),
|
||||
pushScanArguments([], 0),
|
||||
['0']
|
||||
);
|
||||
});
|
||||
|
||||
it('with MATCH', () => {
|
||||
assert.deepEqual(
|
||||
transformScanArguments(0, {
|
||||
pushScanArguments([], 0, {
|
||||
MATCH: 'pattern'
|
||||
}),
|
||||
['0', 'MATCH', 'pattern']
|
||||
@@ -67,7 +72,7 @@ describe('Generic Transformers', () => {
|
||||
|
||||
it('with COUNT', () => {
|
||||
assert.deepEqual(
|
||||
transformScanArguments(0, {
|
||||
pushScanArguments([], 0, {
|
||||
COUNT: 1
|
||||
}),
|
||||
['0', 'COUNT', '1']
|
||||
@@ -76,7 +81,7 @@ describe('Generic Transformers', () => {
|
||||
|
||||
it('with MATCH & COUNT', () => {
|
||||
assert.deepEqual(
|
||||
transformScanArguments(0, {
|
||||
pushScanArguments([], 0, {
|
||||
MATCH: 'pattern',
|
||||
COUNT: 1
|
||||
}),
|
||||
@@ -99,7 +104,7 @@ describe('Generic Transformers', () => {
|
||||
Infinity
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('-inf', () => {
|
||||
assert.equal(
|
||||
transformReplyNumberInfinity('-inf'),
|
||||
@@ -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'
|
||||
}
|
||||
}]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -51,8 +51,8 @@ export interface ScanOptions {
|
||||
COUNT?: number;
|
||||
}
|
||||
|
||||
export function transformScanArguments(cursor: number, options?: ScanOptions): Array<string> {
|
||||
const args = [cursor.toString()];
|
||||
export function pushScanArguments(args: Array<string>, cursor: number, options?: ScanOptions): Array<string> {
|
||||
args.push(cursor.toString());
|
||||
|
||||
if (options?.MATCH) {
|
||||
args.push('MATCH', options.MATCH);
|
||||
@@ -61,7 +61,7 @@ export function transformScanArguments(cursor: number, options?: ScanOptions): A
|
||||
if (options?.COUNT) {
|
||||
args.push('COUNT', options.COUNT.toString());
|
||||
}
|
||||
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export function transformReplyNumberInfinity(reply: string): number {
|
||||
switch (reply) {
|
||||
case '+inf':
|
||||
return Infinity;
|
||||
|
||||
|
||||
case '-inf':
|
||||
return -Infinity;
|
||||
|
||||
@@ -96,7 +96,7 @@ export function transformArgumentNumberInfinity(num: number): string {
|
||||
switch (num) {
|
||||
case Infinity:
|
||||
return '+inf';
|
||||
|
||||
|
||||
case -Infinity:
|
||||
return '-inf';
|
||||
|
||||
@@ -180,3 +180,134 @@ export function transformReplySortedSetWithScores(reply: Array<string>): Array<Z
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
@@ -50,6 +50,13 @@ import * as EXPIREAT from './EXPIREAT';
|
||||
import * as FAILOVER from './FAILOVER';
|
||||
import * as FLUSHALL from './FLUSHALL';
|
||||
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 GETBIT from './GETBIT';
|
||||
import * as GETDEL from './GETDEL';
|
||||
@@ -116,7 +123,7 @@ import * as RPOP_COUNT from './RPOP_COUNT';
|
||||
import * as RPOP from './RPOP';
|
||||
import * as RPOPLPUSH from './RPOPLPUSH';
|
||||
import * as RPUSH from './RPUSH';
|
||||
import * as RPUSHX from './RPUSHX';
|
||||
import * as RPUSHX from './RPUSHX';
|
||||
import * as SADD from './SADD';
|
||||
import * as SAVE from './SAVE';
|
||||
import * as SCAN from './SCAN';
|
||||
@@ -311,6 +318,20 @@ export default {
|
||||
flushAll: FLUSHALL,
|
||||
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,
|
||||
GETBIT,
|
||||
@@ -561,7 +582,7 @@ export default {
|
||||
ZCOUNT,
|
||||
zCount: ZCOUNT,
|
||||
ZDIFF_WITHSCORES,
|
||||
zDiffWithScores: ZDIFF_WITHSCORES,
|
||||
zDiffWithScores: ZDIFF_WITHSCORES,
|
||||
ZDIFF,
|
||||
zDiff: ZDIFF,
|
||||
ZDIFFSTORE,
|
||||
@@ -624,11 +645,13 @@ export default {
|
||||
|
||||
export type RedisReply = string | number | Array<RedisReply> | null | undefined;
|
||||
|
||||
export type TransformArgumentsReply = Array<string> & { preserve?: unknown };
|
||||
|
||||
export interface RedisCommand {
|
||||
FIRST_KEY_INDEX?: number | ((...args: Array<any>) => string);
|
||||
IS_READ_ONLY?: boolean;
|
||||
transformArguments(...args: Array<any>): Array<string>;
|
||||
transformReply(reply: RedisReply): any;
|
||||
transformArguments(...args: Array<any>): TransformArgumentsReply;
|
||||
transformReply(reply: RedisReply, preserved: unknown): any;
|
||||
}
|
||||
|
||||
export interface RedisCommands {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import COMMANDS from './commands';
|
||||
import COMMANDS, { TransformArgumentsReply } from './commands';
|
||||
import { RedisCommand, RedisModules, RedisReply } from './commands';
|
||||
import RedisCommandsQueue from './commands-queue';
|
||||
import { RedisLuaScript, RedisLuaScripts } from './lua-script';
|
||||
@@ -24,6 +24,7 @@ export type RedisMultiCommandType<M extends RedisModules, S extends RedisLuaScri
|
||||
|
||||
export interface MultiQueuedCommand {
|
||||
encodedCommand: string;
|
||||
preservedArguments?: unknown;
|
||||
transformReply?: RedisCommand['transformReply'];
|
||||
}
|
||||
|
||||
@@ -94,7 +95,7 @@ export default class RedisMultiCommand<M extends RedisModules = RedisModules, S
|
||||
script.SCRIPT
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return this.addCommand(
|
||||
[
|
||||
...evalArgs,
|
||||
@@ -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({
|
||||
encodedCommand: RedisCommandsQueue.encodeCommand(args),
|
||||
preservedArguments: args.preserve,
|
||||
transformReply
|
||||
});
|
||||
|
||||
@@ -224,8 +226,8 @@ export default class RedisMultiCommand<M extends RedisModules = RedisModules, S
|
||||
|
||||
const rawReplies = await this.#executor(queue, Symbol('[RedisMultiCommand] Chain ID'));
|
||||
return (rawReplies[rawReplies.length - 1]! as Array<RedisReply>).map((reply, i) => {
|
||||
const { transformReply } = queue[i + 1];
|
||||
return transformReply ? transformReply(reply) : reply;
|
||||
const { transformReply, preservedArguments } = queue[i + 1];
|
||||
return transformReply ? transformReply(reply, preservedArguments) : reply;
|
||||
});
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user