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

fix #1659 - add support for db-number in client options url

This commit is contained in:
leibale
2021-09-21 15:30:25 -04:00
parent d79bc55df6
commit 1819b9c1c4
5 changed files with 149 additions and 60 deletions

View File

@@ -1,24 +1,25 @@
# `createClient` configuration # `createClient` configuration
| Property | Default | Description | | Property | Default | Description |
|--------------------------|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------| |--------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| socket | | Object defining socket connection properties | | url | | `redis[s]://[[username][:password]@][host][:port][/db-number]` (see [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details) |
| socket.url | | `[redis[s]:]//[[username][:password]@][host][:port]` | | socket | | Object defining socket connection properties |
| socket.host | `'localhost'` | Hostname to connect to | | socket.host | `'localhost'` | Hostname to connect to |
| socket.port | `6379` | Port to connect to | | socket.port | `6379` | Port to connect to |
| socket.username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) | | socket.connectTimeout | `5000` | The timeout for connecting to the Redis Server (in milliseconds) |
| socket.password | | ACL password or the old "--requirepass" password | | socket.noDelay | `true` | Enable/disable the use of [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) |
| socket.connectTimeout | `5000` | The timeout for connecting to the Redis Server (in milliseconds) | | socket.keepAlive | `5000` | Enable/disable the [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) functionality |
| socket.noDelay | `true` | Enable/disable the use of [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) | | socket.tls | | Set to `true` to enable [TLS Configuration](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) |
| socket.keepAlive | `5000` | Enable/disable the [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) functionality | | socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
| socket.tls | | Set to `true` to enable [TLS Configuration](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) | | username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic | | password | | ACL password or the old "--requirepass" password |
| modules | | Object defining which [Redis Modules](https://redis.io/modules) to include (TODO - document) | | database | | Database number to connect to (see [`SELECT`](https://redis.io/commands/select) command) |
| scripts | | Object defining Lua scripts to use with this client. See [Lua Scripts](../README.md#lua-scripts) | | modules | | Object defining which [Redis Modules](https://redis.io/modules) to include (TODO - document) |
| commandsQueueMaxLength | | Maximum length of the client's internal command queue | | scripts | | Object defining Lua Scripts to use with this client (see [Lua Scripts](../README.md#lua-scripts)) |
| readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode | | commandsQueueMaxLength | | Maximum length of the client's internal command queue |
| legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](v3-to-v4.md)) | | readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode |
| isolationPoolOptions | | See the [Isolated Execution Guide](./isolated-execution.md) | | legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](v3-to-v4.md)) |
| isolationPoolOptions | | See the [Isolated Execution Guide](./isolated-execution.md) |
## Reconnect Strategy ## Reconnect Strategy

View File

@@ -18,6 +18,53 @@ export const SQUARE_SCRIPT = defineScript({
}); });
describe('Client', () => { describe('Client', () => {
describe('parseURL', () => {
it('redis://user:secret@localhost:6379/0', () => {
assert.deepEqual(
RedisClient.parseURL('redis://user:secret@localhost:6379/0'),
{
socket: {
host: 'localhost',
port: 6379
},
username: 'user',
password: 'secret',
database: 0
}
);
});
it('rediss://user:secret@localhost:6379/0', () => {
assert.deepEqual(
RedisClient.parseURL('rediss://user:secret@localhost:6379/0'),
{
socket: {
host: 'localhost',
port: 6379,
tls: true
},
username: 'user',
password: 'secret',
database: 0
}
);
});
it('Invalid protocol', () => {
assert.throws(
() => RedisClient.parseURL('redi://user:secret@localhost:6379/0'),
TypeError
);
});
it('Invalid pathname', () => {
assert.throws(
() => RedisClient.parseURL('redis://user:secret@localhost:6379/NaN'),
TypeError
);
});
});
describe('authentication', () => { describe('authentication', () => {
itWithClient(TestRedisServers.PASSWORD, 'Client should be authenticated', async client => { itWithClient(TestRedisServers.PASSWORD, 'Client should be authenticated', async client => {
assert.equal( assert.equal(
@@ -28,10 +75,8 @@ describe('Client', () => {
it('should not retry connecting if failed due to wrong auth', async () => { it('should not retry connecting if failed due to wrong auth', async () => {
const client = RedisClient.create({ const client = RedisClient.create({
socket: { ...TEST_REDIS_SERVERS[TestRedisServers.PASSWORD],
...TEST_REDIS_SERVERS[TestRedisServers.PASSWORD], password: 'wrongpassword'
password: 'wrongpassword'
}
}); });
await assert.rejects( await assert.rejects(
@@ -49,7 +94,7 @@ describe('Client', () => {
describe('legacyMode', () => { describe('legacyMode', () => {
const client = RedisClient.create({ const client = RedisClient.create({
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN], ...TEST_REDIS_SERVERS[TestRedisServers.OPEN],
scripts: { scripts: {
square: SQUARE_SCRIPT square: SQUARE_SCRIPT
}, },
@@ -173,9 +218,7 @@ describe('Client', () => {
describe('events', () => { describe('events', () => {
it('connect, ready, end', async () => { it('connect, ready, end', async () => {
const client = RedisClient.create({ const client = RedisClient.create(TEST_REDIS_SERVERS[TestRedisServers.OPEN]);
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN]
});
await Promise.all([ await Promise.all([
client.connect(), client.connect(),
@@ -550,9 +593,7 @@ describe('Client', () => {
}); });
it('client.quit', async () => { it('client.quit', async () => {
const client = RedisClient.create({ const client = RedisClient.create(TEST_REDIS_SERVERS[TestRedisServers.OPEN]);
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN]
});
await client.connect(); await client.connect();

View File

@@ -1,4 +1,4 @@
import RedisSocket, { RedisSocketOptions } from './socket'; import RedisSocket, { RedisSocketOptions, RedisNetSocketOptions, RedisTlsSocketOptions } from './socket';
import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue'; import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
import COMMANDS, { TransformArgumentsReply } from './commands'; import COMMANDS, { TransformArgumentsReply } from './commands';
import { RedisCommand, RedisModules, RedisReply } from './commands'; import { RedisCommand, RedisModules, RedisReply } from './commands';
@@ -12,9 +12,14 @@ import { HScanTuple } from './commands/HSCAN';
import { encodeCommand, extendWithDefaultCommands, extendWithModulesAndScripts, transformCommandArguments } from './commander'; import { encodeCommand, extendWithDefaultCommands, extendWithModulesAndScripts, transformCommandArguments } from './commander';
import { Pool, Options as PoolOptions, createPool } from 'generic-pool'; import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
import { ClientClosedError } from './errors'; import { ClientClosedError } from './errors';
import { URL } from 'url';
export interface RedisClientOptions<M, S> { export interface RedisClientOptions<M, S> {
url?: string;
socket?: RedisSocketOptions; socket?: RedisSocketOptions;
username?: string;
password?: string;
database?: number;
modules?: M; modules?: M;
scripts?: S; scripts?: S;
commandsQueueMaxLength?: number; commandsQueueMaxLength?: number;
@@ -71,6 +76,45 @@ export default class RedisClient<M extends RedisModules, S extends RedisLuaScrip
return new Client(options); return new Client(options);
} }
static parseURL(url: string): RedisClientOptions<{}, {}> {
// https://www.iana.org/assignments/uri-schemes/prov/redis
const { hostname, port, protocol, username, password, pathname } = new URL(url),
parsed: RedisClientOptions<{}, {}> = {
socket: {
host: hostname
}
};
if (protocol === 'rediss:') {
(parsed.socket as RedisTlsSocketOptions).tls = true;
} else if (protocol !== 'redis:') {
throw new TypeError('Invalid protocol');
}
if (port) {
(parsed.socket as RedisNetSocketOptions).port = Number(port);
}
if (username) {
parsed.username = username;
}
if (password) {
parsed.password = password;
}
if (pathname.length > 1) {
const database = Number(pathname.substring(1));
if (isNaN(database)) {
throw new TypeError('Invalid pathname');
}
parsed.database = database;
}
return parsed;
}
readonly #options?: RedisClientOptions<M, S>; readonly #options?: RedisClientOptions<M, S>;
readonly #socket: RedisSocket; readonly #socket: RedisSocket;
readonly #queue: RedisCommandsQueue; readonly #queue: RedisCommandsQueue;
@@ -96,7 +140,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisLuaScrip
constructor(options?: RedisClientOptions<M, S>) { constructor(options?: RedisClientOptions<M, S>) {
super(); super();
this.#options = options; this.#options = this.#initiateOptions(options);
this.#socket = this.#initiateSocket(); this.#socket = this.#initiateSocket();
this.#queue = this.#initiateQueue(); this.#queue = this.#initiateQueue();
this.#isolationPool = createPool({ this.#isolationPool = createPool({
@@ -110,6 +154,23 @@ export default class RedisClient<M extends RedisModules, S extends RedisLuaScrip
this.#legacyMode(); this.#legacyMode();
} }
#initiateOptions(options?: RedisClientOptions<M, S>): RedisClientOptions<M, S> | undefined {
if (options?.url) {
const parsed = RedisClient.parseURL(options.url);
if (options.socket) {
parsed.socket = Object.assign(options.socket, parsed.socket);
}
Object.assign(options, parsed);
}
if (options?.database) {
this.#selectedDB = options.database;
}
return options;
}
#initiateSocket(): RedisSocket { #initiateSocket(): RedisSocket {
const socketInitiator = async (): Promise<void> => { const socketInitiator = async (): Promise<void> => {
const v4Commands = this.#options?.legacyMode ? this.#v4 : this, const v4Commands = this.#options?.legacyMode ? this.#v4 : this,
@@ -123,8 +184,8 @@ export default class RedisClient<M extends RedisModules, S extends RedisLuaScrip
promises.push(v4Commands.readonly(RedisClient.commandOptions({ asap: true }))); promises.push(v4Commands.readonly(RedisClient.commandOptions({ asap: true })));
} }
if (this.#options?.socket?.username || this.#options?.socket?.password) { if (this.#options?.username || this.#options?.password) {
promises.push(v4Commands.auth(RedisClient.commandOptions({ asap: true }), this.#options.socket)); promises.push(v4Commands.auth(RedisClient.commandOptions({ asap: true }), this.#options));
} }
const resubscribePromise = this.#queue.resubscribe(); const resubscribePromise = this.#queue.resubscribe();

View File

@@ -6,8 +6,6 @@ import { ConnectionTimeoutError, ClientClosedError } from './errors';
import { promiseTimeout } from './utils'; import { promiseTimeout } from './utils';
export interface RedisSocketCommonOptions { export interface RedisSocketCommonOptions {
username?: string;
password?: string;
connectTimeout?: number; connectTimeout?: number;
noDelay?: boolean; noDelay?: boolean;
keepAlive?: number | false; keepAlive?: number | false;
@@ -19,10 +17,6 @@ export interface RedisNetSocketOptions extends RedisSocketCommonOptions {
host?: string; host?: string;
} }
export interface RedisUrlSocketOptions extends RedisSocketCommonOptions {
url: string;
}
export interface RedisUnixSocketOptions extends RedisSocketCommonOptions { export interface RedisUnixSocketOptions extends RedisSocketCommonOptions {
path: string; path: string;
} }
@@ -31,7 +25,7 @@ export interface RedisTlsSocketOptions extends RedisNetSocketOptions, tls.Secure
tls: true; tls: true;
} }
export type RedisSocketOptions = RedisNetSocketOptions | RedisUrlSocketOptions | RedisUnixSocketOptions | RedisTlsSocketOptions; export type RedisSocketOptions = RedisNetSocketOptions | RedisUnixSocketOptions | RedisTlsSocketOptions;
interface CreateSocketReturn<T> { interface CreateSocketReturn<T> {
connectEvent: string; connectEvent: string;
@@ -44,14 +38,6 @@ export default class RedisSocket extends EventEmitter {
static #initiateOptions(options?: RedisSocketOptions): RedisSocketOptions { static #initiateOptions(options?: RedisSocketOptions): RedisSocketOptions {
options ??= {}; options ??= {};
if (!RedisSocket.#isUnixSocket(options)) { if (!RedisSocket.#isUnixSocket(options)) {
if (RedisSocket.#isUrlSocket(options)) {
const url = new URL(options.url);
(options as RedisNetSocketOptions).port = Number(url.port);
(options as RedisNetSocketOptions).host = url.hostname;
options.username = url.username;
options.password = url.password;
}
(options as RedisNetSocketOptions).port ??= 6379; (options as RedisNetSocketOptions).port ??= 6379;
(options as RedisNetSocketOptions).host ??= '127.0.0.1'; (options as RedisNetSocketOptions).host ??= '127.0.0.1';
} }
@@ -67,10 +53,6 @@ export default class RedisSocket extends EventEmitter {
return Math.min(retries * 50, 500); return Math.min(retries * 50, 500);
} }
static #isUrlSocket(options: RedisSocketOptions): options is RedisUrlSocketOptions {
return Object.prototype.hasOwnProperty.call(options, 'url');
}
static #isUnixSocket(options: RedisSocketOptions): options is RedisUnixSocketOptions { static #isUnixSocket(options: RedisSocketOptions): options is RedisUnixSocketOptions {
return Object.prototype.hasOwnProperty.call(options, 'path'); return Object.prototype.hasOwnProperty.call(options, 'path');
} }

View File

@@ -1,5 +1,5 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import RedisClient, { RedisClientType } from './client'; import RedisClient, { RedisClientOptions, RedisClientType } from './client';
import { execSync, spawn } from 'child_process'; import { execSync, spawn } from 'child_process';
import { once } from 'events'; import { once } from 'events';
import { RedisSocketOptions } from './socket'; import { RedisSocketOptions } from './socket';
@@ -9,6 +9,8 @@ import RedisCluster, { RedisClusterType } from './cluster';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { Context as MochaContext } from 'mocha'; import { Context as MochaContext } from 'mocha';
import { promiseTimeout } from './utils'; import { promiseTimeout } from './utils';
import { RedisModules } from './commands';
import { RedisLuaScripts } from './lua-script';
type RedisVersion = [major: number, minor: number, patch: number]; type RedisVersion = [major: number, minor: number, patch: number];
@@ -52,7 +54,7 @@ export enum TestRedisServers {
PASSWORD PASSWORD
} }
export const TEST_REDIS_SERVERS: Record<TestRedisServers, RedisSocketOptions> = <any>{}; export const TEST_REDIS_SERVERS: Record<TestRedisServers, RedisClientOptions<RedisModules, RedisLuaScripts>> = <any>{};
export enum TestRedisClusters { export enum TestRedisClusters {
OPEN OPEN
@@ -226,13 +228,17 @@ export async function spawnGlobalRedisCluster(type: TestRedisClusters | null, nu
async function spawnOpenServer(): Promise<void> { async function spawnOpenServer(): Promise<void> {
TEST_REDIS_SERVERS[TestRedisServers.OPEN] = { TEST_REDIS_SERVERS[TestRedisServers.OPEN] = {
port: await spawnGlobalRedisServer() socket: {
port: await spawnGlobalRedisServer()
}
}; };
} }
async function spawnPasswordServer(): Promise<void> { async function spawnPasswordServer(): Promise<void> {
TEST_REDIS_SERVERS[TestRedisServers.PASSWORD] = { TEST_REDIS_SERVERS[TestRedisServers.PASSWORD] = {
port: await spawnGlobalRedisServer(['--requirepass', 'password']), socket: {
port: await spawnGlobalRedisServer(['--requirepass', 'password']),
},
password: 'password' password: 'password'
}; };
@@ -285,9 +291,7 @@ export function itWithClient(
it(title, async function () { it(title, async function () {
if (handleMinimumRedisVersion(this, options?.minimumRedisVersion)) return; if (handleMinimumRedisVersion(this, options?.minimumRedisVersion)) return;
const client = RedisClient.create({ const client = RedisClient.create(TEST_REDIS_SERVERS[type]);
socket: TEST_REDIS_SERVERS[type]
});
await client.connect(); await client.connect();