You've already forked node-redis
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:
@@ -1,20 +1,21 @@
|
||||
# `createClient` configuration
|
||||
|
||||
| Property | Default | Description |
|
||||
|--------------------------|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
|
||||
|--------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 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 | | Object defining socket connection properties |
|
||||
| socket.url | | `[redis[s]:]//[[username][:password]@][host][:port]` |
|
||||
| socket.host | `'localhost'` | Hostname to connect to |
|
||||
| socket.port | `6379` | Port to connect to |
|
||||
| socket.username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
|
||||
| socket.password | | ACL password or the old "--requirepass" password |
|
||||
| socket.connectTimeout | `5000` | The timeout for connecting to the Redis Server (in milliseconds) |
|
||||
| socket.noDelay | `true` | Enable/disable the use of [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) |
|
||||
| socket.keepAlive | `5000` | Enable/disable the [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) functionality |
|
||||
| socket.tls | | Set to `true` to enable [TLS Configuration](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) |
|
||||
| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
|
||||
| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
|
||||
| password | | ACL password or the old "--requirepass" password |
|
||||
| database | | Database number to connect to (see [`SELECT`](https://redis.io/commands/select) command) |
|
||||
| modules | | Object defining which [Redis Modules](https://redis.io/modules) to include (TODO - document) |
|
||||
| scripts | | Object defining Lua scripts to use with this client. See [Lua Scripts](../README.md#lua-scripts) |
|
||||
| scripts | | Object defining Lua Scripts to use with this client (see [Lua Scripts](../README.md#lua-scripts)) |
|
||||
| commandsQueueMaxLength | | Maximum length of the client's internal command queue |
|
||||
| readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode |
|
||||
| legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](v3-to-v4.md)) |
|
||||
|
@@ -18,6 +18,53 @@ export const SQUARE_SCRIPT = defineScript({
|
||||
});
|
||||
|
||||
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', () => {
|
||||
itWithClient(TestRedisServers.PASSWORD, 'Client should be authenticated', async client => {
|
||||
assert.equal(
|
||||
@@ -28,10 +75,8 @@ describe('Client', () => {
|
||||
|
||||
it('should not retry connecting if failed due to wrong auth', async () => {
|
||||
const client = RedisClient.create({
|
||||
socket: {
|
||||
...TEST_REDIS_SERVERS[TestRedisServers.PASSWORD],
|
||||
password: 'wrongpassword'
|
||||
}
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
@@ -49,7 +94,7 @@ describe('Client', () => {
|
||||
|
||||
describe('legacyMode', () => {
|
||||
const client = RedisClient.create({
|
||||
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN],
|
||||
...TEST_REDIS_SERVERS[TestRedisServers.OPEN],
|
||||
scripts: {
|
||||
square: SQUARE_SCRIPT
|
||||
},
|
||||
@@ -173,9 +218,7 @@ describe('Client', () => {
|
||||
|
||||
describe('events', () => {
|
||||
it('connect, ready, end', async () => {
|
||||
const client = RedisClient.create({
|
||||
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN]
|
||||
});
|
||||
const client = RedisClient.create(TEST_REDIS_SERVERS[TestRedisServers.OPEN]);
|
||||
|
||||
await Promise.all([
|
||||
client.connect(),
|
||||
@@ -550,9 +593,7 @@ describe('Client', () => {
|
||||
});
|
||||
|
||||
it('client.quit', async () => {
|
||||
const client = RedisClient.create({
|
||||
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN]
|
||||
});
|
||||
const client = RedisClient.create(TEST_REDIS_SERVERS[TestRedisServers.OPEN]);
|
||||
|
||||
await client.connect();
|
||||
|
||||
|
@@ -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 COMMANDS, { TransformArgumentsReply } 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 { Pool, Options as PoolOptions, createPool } from 'generic-pool';
|
||||
import { ClientClosedError } from './errors';
|
||||
import { URL } from 'url';
|
||||
|
||||
export interface RedisClientOptions<M, S> {
|
||||
url?: string;
|
||||
socket?: RedisSocketOptions;
|
||||
username?: string;
|
||||
password?: string;
|
||||
database?: number;
|
||||
modules?: M;
|
||||
scripts?: S;
|
||||
commandsQueueMaxLength?: number;
|
||||
@@ -71,6 +76,45 @@ export default class RedisClient<M extends RedisModules, S extends RedisLuaScrip
|
||||
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 #socket: RedisSocket;
|
||||
readonly #queue: RedisCommandsQueue;
|
||||
@@ -96,7 +140,7 @@ export default class RedisClient<M extends RedisModules, S extends RedisLuaScrip
|
||||
|
||||
constructor(options?: RedisClientOptions<M, S>) {
|
||||
super();
|
||||
this.#options = options;
|
||||
this.#options = this.#initiateOptions(options);
|
||||
this.#socket = this.#initiateSocket();
|
||||
this.#queue = this.#initiateQueue();
|
||||
this.#isolationPool = createPool({
|
||||
@@ -110,6 +154,23 @@ export default class RedisClient<M extends RedisModules, S extends RedisLuaScrip
|
||||
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 {
|
||||
const socketInitiator = async (): Promise<void> => {
|
||||
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 })));
|
||||
}
|
||||
|
||||
if (this.#options?.socket?.username || this.#options?.socket?.password) {
|
||||
promises.push(v4Commands.auth(RedisClient.commandOptions({ asap: true }), this.#options.socket));
|
||||
if (this.#options?.username || this.#options?.password) {
|
||||
promises.push(v4Commands.auth(RedisClient.commandOptions({ asap: true }), this.#options));
|
||||
}
|
||||
|
||||
const resubscribePromise = this.#queue.resubscribe();
|
||||
|
@@ -6,8 +6,6 @@ import { ConnectionTimeoutError, ClientClosedError } from './errors';
|
||||
import { promiseTimeout } from './utils';
|
||||
|
||||
export interface RedisSocketCommonOptions {
|
||||
username?: string;
|
||||
password?: string;
|
||||
connectTimeout?: number;
|
||||
noDelay?: boolean;
|
||||
keepAlive?: number | false;
|
||||
@@ -19,10 +17,6 @@ export interface RedisNetSocketOptions extends RedisSocketCommonOptions {
|
||||
host?: string;
|
||||
}
|
||||
|
||||
export interface RedisUrlSocketOptions extends RedisSocketCommonOptions {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface RedisUnixSocketOptions extends RedisSocketCommonOptions {
|
||||
path: string;
|
||||
}
|
||||
@@ -31,7 +25,7 @@ export interface RedisTlsSocketOptions extends RedisNetSocketOptions, tls.Secure
|
||||
tls: true;
|
||||
}
|
||||
|
||||
export type RedisSocketOptions = RedisNetSocketOptions | RedisUrlSocketOptions | RedisUnixSocketOptions | RedisTlsSocketOptions;
|
||||
export type RedisSocketOptions = RedisNetSocketOptions | RedisUnixSocketOptions | RedisTlsSocketOptions;
|
||||
|
||||
interface CreateSocketReturn<T> {
|
||||
connectEvent: string;
|
||||
@@ -44,14 +38,6 @@ export default class RedisSocket extends EventEmitter {
|
||||
static #initiateOptions(options?: RedisSocketOptions): RedisSocketOptions {
|
||||
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).host ??= '127.0.0.1';
|
||||
}
|
||||
@@ -67,10 +53,6 @@ export default class RedisSocket extends EventEmitter {
|
||||
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 {
|
||||
return Object.prototype.hasOwnProperty.call(options, 'path');
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import RedisClient, { RedisClientType } from './client';
|
||||
import RedisClient, { RedisClientOptions, RedisClientType } from './client';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import { once } from 'events';
|
||||
import { RedisSocketOptions } from './socket';
|
||||
@@ -9,6 +9,8 @@ import RedisCluster, { RedisClusterType } from './cluster';
|
||||
import { promises as fs } from 'fs';
|
||||
import { Context as MochaContext } from 'mocha';
|
||||
import { promiseTimeout } from './utils';
|
||||
import { RedisModules } from './commands';
|
||||
import { RedisLuaScripts } from './lua-script';
|
||||
|
||||
type RedisVersion = [major: number, minor: number, patch: number];
|
||||
|
||||
@@ -52,7 +54,7 @@ export enum TestRedisServers {
|
||||
PASSWORD
|
||||
}
|
||||
|
||||
export const TEST_REDIS_SERVERS: Record<TestRedisServers, RedisSocketOptions> = <any>{};
|
||||
export const TEST_REDIS_SERVERS: Record<TestRedisServers, RedisClientOptions<RedisModules, RedisLuaScripts>> = <any>{};
|
||||
|
||||
export enum TestRedisClusters {
|
||||
OPEN
|
||||
@@ -226,13 +228,17 @@ export async function spawnGlobalRedisCluster(type: TestRedisClusters | null, nu
|
||||
|
||||
async function spawnOpenServer(): Promise<void> {
|
||||
TEST_REDIS_SERVERS[TestRedisServers.OPEN] = {
|
||||
socket: {
|
||||
port: await spawnGlobalRedisServer()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function spawnPasswordServer(): Promise<void> {
|
||||
TEST_REDIS_SERVERS[TestRedisServers.PASSWORD] = {
|
||||
socket: {
|
||||
port: await spawnGlobalRedisServer(['--requirepass', 'password']),
|
||||
},
|
||||
password: 'password'
|
||||
};
|
||||
|
||||
@@ -285,9 +291,7 @@ export function itWithClient(
|
||||
it(title, async function () {
|
||||
if (handleMinimumRedisVersion(this, options?.minimumRedisVersion)) return;
|
||||
|
||||
const client = RedisClient.create({
|
||||
socket: TEST_REDIS_SERVERS[type]
|
||||
});
|
||||
const client = RedisClient.create(TEST_REDIS_SERVERS[type]);
|
||||
|
||||
await client.connect();
|
||||
|
||||
|
Reference in New Issue
Block a user