first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,25 @@
{
"node": "n8n-nodes-base.redis",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Data & Storage"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/redis/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.redis/"
}
],
"generic": [
{
"label": "Why this Product Manager loves workflow automation with n8n",
"icon": "🧠",
"url": "https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/"
}
]
}
}
@@ -0,0 +1,715 @@
import set from 'lodash/set';
import type {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import type { RedisCredential } from './types';
import {
setupRedisClient,
redisConnectionTest,
convertInfoToObject,
getValue,
setValue,
} from './utils';
export class Redis implements INodeType {
description: INodeTypeDescription = {
displayName: 'Redis',
name: 'redis',
icon: 'file:redis.svg',
group: ['input'],
version: 1,
description: 'Get, send and update data in Redis',
defaults: {
name: 'Redis',
},
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
usableAsTool: true,
credentials: [
{
name: 'redis',
required: true,
testedBy: 'redisConnectionTest',
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Delete',
value: 'delete',
description: 'Delete a key from Redis',
action: 'Delete a key from Redis',
},
{
name: 'Get',
value: 'get',
description: 'Get the value of a key from Redis',
action: 'Get the value of a key from Redis',
},
{
name: 'Increment',
value: 'incr',
description: 'Atomically increments a key by 1. Creates the key if it does not exist.',
action: 'Atomically increment a key by 1. Creates the key if it does not exist.',
},
{
name: 'Info',
value: 'info',
description: 'Returns generic information about the Redis instance',
action: 'Return generic information about the Redis instance',
},
{
name: 'Keys',
value: 'keys',
description: 'Returns all the keys matching a pattern',
action: 'Return all keys matching a pattern',
},
{
name: 'List Length',
value: 'llen',
description: 'Returns the length of a list',
action: 'Return the length of a list',
},
{
name: 'Pop',
value: 'pop',
description: 'Pop data from a redis list',
action: 'Pop data from a redis list',
},
{
name: 'Publish',
value: 'publish',
description: 'Publish message to redis channel',
action: 'Publish message to redis channel',
},
{
name: 'Push',
value: 'push',
description: 'Push data to a redis list',
action: 'Push data to a redis list',
},
{
name: 'Set',
value: 'set',
description: 'Set the value of a key in redis',
action: 'Set the value of a key in redis',
},
],
default: 'info',
},
// ----------------------------------
// delete
// ----------------------------------
{
displayName: 'Key',
name: 'key',
type: 'string',
displayOptions: {
show: {
operation: ['delete'],
},
},
default: '',
required: true,
description: 'Name of the key to delete from Redis',
},
// ----------------------------------
// get
// ----------------------------------
{
displayName: 'Name',
name: 'propertyName',
type: 'string',
displayOptions: {
show: {
operation: ['get'],
},
},
default: 'propertyName',
required: true,
description:
'Name of the property to write received data to. Supports dot-notation. Example: "data.person[0].name".',
},
{
displayName: 'Key',
name: 'key',
type: 'string',
displayOptions: {
show: {
operation: ['get'],
},
},
default: '',
required: true,
description: 'Name of the key to get from Redis',
},
{
displayName: 'Key Type',
name: 'keyType',
type: 'options',
displayOptions: {
show: {
operation: ['get'],
},
},
options: [
{
name: 'Automatic',
value: 'automatic',
description: 'Requests the type before requesting the data (slower)',
},
{
name: 'Hash',
value: 'hash',
description: "Data in key is of type 'hash'",
},
{
name: 'List',
value: 'list',
description: "Data in key is of type 'lists'",
},
{
name: 'Sets',
value: 'sets',
description: "Data in key is of type 'sets'",
},
{
name: 'String',
value: 'string',
description: "Data in key is of type 'string'",
},
],
default: 'automatic',
description: 'The type of the key to get',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
operation: ['get'],
},
},
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Dot Notation',
name: 'dotNotation',
type: 'boolean',
default: true,
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
description:
'<p>By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.<p></p>If that is not intended this can be deactivated, it will then set { "a.b": value } instead.</p>.',
},
],
},
// ----------------------------------
// incr
// ----------------------------------
{
displayName: 'Key',
name: 'key',
type: 'string',
displayOptions: {
show: {
operation: ['incr'],
},
},
default: '',
required: true,
description: 'Name of the key to increment',
},
{
displayName: 'Expire',
name: 'expire',
type: 'boolean',
displayOptions: {
show: {
operation: ['incr'],
},
},
default: false,
description: 'Whether to set a timeout on key',
},
{
displayName: 'TTL',
name: 'ttl',
type: 'number',
typeOptions: {
minValue: 1,
},
displayOptions: {
show: {
operation: ['incr'],
expire: [true],
},
},
default: 60,
description: 'Number of seconds before key expiration',
},
// ----------------------------------
// keys
// ----------------------------------
{
displayName: 'Key Pattern',
name: 'keyPattern',
type: 'string',
displayOptions: {
show: {
operation: ['keys'],
},
},
default: '',
required: true,
description: 'The key pattern for the keys to return',
},
{
displayName: 'Get Values',
name: 'getValues',
type: 'boolean',
displayOptions: {
show: {
operation: ['keys'],
},
},
default: true,
description: 'Whether to get the value of matching keys',
},
{
displayName: 'List',
name: 'list',
type: 'string',
displayOptions: {
show: {
operation: ['llen'],
},
},
default: '',
required: true,
description: 'Name of the list in Redis',
},
// ----------------------------------
// llen
// ----------------------------------
{
displayName: 'List',
name: 'list',
type: 'string',
displayOptions: {
show: {
operation: ['llen'],
},
},
default: '',
required: true,
description: 'Name of the list in Redis',
},
// ----------------------------------
// set
// ----------------------------------
{
displayName: 'Key',
name: 'key',
type: 'string',
displayOptions: {
show: {
operation: ['set'],
},
},
default: '',
required: true,
description: 'Name of the key to set in Redis',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
displayOptions: {
show: {
operation: ['set'],
},
},
default: '',
description: 'The value to write in Redis',
},
{
displayName: 'Key Type',
name: 'keyType',
type: 'options',
displayOptions: {
show: {
operation: ['set'],
},
},
options: [
{
name: 'Automatic',
value: 'automatic',
description: 'Tries to figure out the type automatically depending on the data',
},
{
name: 'Hash',
value: 'hash',
description: "Data in key is of type 'hash'",
},
{
name: 'List',
value: 'list',
description: "Data in key is of type 'lists'",
},
{
name: 'Sets',
value: 'sets',
description: "Data in key is of type 'sets'",
},
{
name: 'String',
value: 'string',
description: "Data in key is of type 'string'",
},
],
default: 'automatic',
description: 'The type of the key to set',
},
{
displayName: 'Value Is JSON',
name: 'valueIsJSON',
type: 'boolean',
displayOptions: {
show: {
keyType: ['hash'],
},
},
default: true,
description: 'Whether the value is JSON or key value pairs',
},
{
displayName: 'Expire',
name: 'expire',
type: 'boolean',
displayOptions: {
show: {
operation: ['set'],
},
},
default: false,
description: 'Whether to set a timeout on key',
},
{
displayName: 'TTL',
name: 'ttl',
type: 'number',
typeOptions: {
minValue: 1,
},
displayOptions: {
show: {
operation: ['set'],
expire: [true],
},
},
default: 60,
description: 'Number of seconds before key expiration',
},
// ----------------------------------
// publish
// ----------------------------------
{
displayName: 'Channel',
name: 'channel',
type: 'string',
displayOptions: {
show: {
operation: ['publish'],
},
},
default: '',
required: true,
description: 'Channel name',
},
{
displayName: 'Data',
name: 'messageData',
type: 'string',
displayOptions: {
show: {
operation: ['publish'],
},
},
default: '',
required: true,
description: 'Data to publish',
},
// ----------------------------------
// push/pop
// ----------------------------------
{
displayName: 'List',
name: 'list',
type: 'string',
displayOptions: {
show: {
operation: ['push', 'pop'],
},
},
default: '',
required: true,
description: 'Name of the list in Redis',
},
{
displayName: 'Data',
name: 'messageData',
type: 'string',
displayOptions: {
show: {
operation: ['push'],
},
},
default: '',
required: true,
description: 'Data to push',
},
{
displayName: 'Tail',
name: 'tail',
type: 'boolean',
displayOptions: {
show: {
operation: ['push', 'pop'],
},
},
default: false,
description: 'Whether to push or pop data from the end of the list',
},
{
displayName: 'Name',
name: 'propertyName',
type: 'string',
displayOptions: {
show: {
operation: ['pop'],
},
},
default: 'propertyName',
description:
'Optional name of the property to write received data to. Supports dot-notation. Example: "data.person[0].name".',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
operation: ['pop'],
},
},
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Dot Notation',
name: 'dotNotation',
type: 'boolean',
default: true,
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
description:
'<p>By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.<p></p>If that is not intended this can be deactivated, it will then set { "a.b": value } instead.</p>.',
},
],
},
],
};
methods = {
credentialTest: { redisConnectionTest },
};
async execute(this: IExecuteFunctions) {
// TODO: For array and object fields it should not have a "value" field it should
// have a parameter field for a path. Because it is not possible to set
// array, object via parameter directly (should maybe be possible?!?!)
// Should maybe have a parameter which is JSON.
const credentials = await this.getCredentials<RedisCredential>('redis');
const client = setupRedisClient(credentials);
try {
await client.connect();
await client.ping();
const operation = this.getNodeParameter('operation', 0);
const returnItems: INodeExecutionData[] = [];
if (operation === 'info') {
try {
const result = await client.info();
returnItems.push({ json: convertInfoToObject(result) });
} catch (error) {
if (this.continueOnFail()) {
returnItems.push({
json: {
error: error.message,
},
});
} else {
throw new NodeOperationError(this.getNode(), error);
}
}
} else if (
['delete', 'get', 'keys', 'llen', 'set', 'incr', 'publish', 'push', 'pop'].includes(
operation,
)
) {
const items = this.getInputData();
let item: INodeExecutionData;
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
try {
item = { json: {}, pairedItem: { item: itemIndex } };
if (operation === 'delete') {
const keyDelete = this.getNodeParameter('key', itemIndex) as string;
await client.del(keyDelete);
returnItems.push(items[itemIndex]);
} else if (operation === 'get') {
const propertyName = this.getNodeParameter('propertyName', itemIndex) as string;
const keyGet = this.getNodeParameter('key', itemIndex) as string;
const keyType = this.getNodeParameter('keyType', itemIndex) as string;
const value = (await getValue(client, keyGet, keyType)) ?? null;
const options = this.getNodeParameter('options', itemIndex, {});
if (options.dotNotation === false) {
item.json[propertyName] = value;
} else {
set(item.json, propertyName, value);
}
returnItems.push(item);
} else if (operation === 'keys') {
const keyPattern = this.getNodeParameter('keyPattern', itemIndex) as string;
const getValues = this.getNodeParameter('getValues', itemIndex, true) as boolean;
const keys = await client.keys(keyPattern);
if (!getValues) {
returnItems.push({ json: { keys } });
continue;
}
for (const keyName of keys) {
item.json[keyName] = await getValue(client, keyName);
}
returnItems.push(item);
} else if (operation === 'llen') {
const redisList = this.getNodeParameter('list', itemIndex) as string;
const length = await client.lLen(redisList);
item.json = { [redisList]: length };
returnItems.push(item);
} else if (operation === 'set') {
const keySet = this.getNodeParameter('key', itemIndex) as string;
const value = this.getNodeParameter('value', itemIndex) as string;
const keyType = this.getNodeParameter('keyType', itemIndex) as string;
const valueIsJSON = this.getNodeParameter('valueIsJSON', itemIndex, true) as boolean;
const expire = this.getNodeParameter('expire', itemIndex, false) as boolean;
const ttl = this.getNodeParameter('ttl', itemIndex, -1) as number;
await setValue.call(this, client, keySet, value, expire, ttl, keyType, valueIsJSON);
returnItems.push(items[itemIndex]);
} else if (operation === 'incr') {
const keyIncr = this.getNodeParameter('key', itemIndex) as string;
const expire = this.getNodeParameter('expire', itemIndex, false) as boolean;
const ttl = this.getNodeParameter('ttl', itemIndex, -1) as number;
const incrementVal = await client.incr(keyIncr);
if (expire && ttl > 0) {
await client.expire(keyIncr, ttl);
}
returnItems.push({ json: { [keyIncr]: incrementVal } });
} else if (operation === 'publish') {
const channel = this.getNodeParameter('channel', itemIndex) as string;
const messageData = this.getNodeParameter('messageData', itemIndex) as string;
await client.publish(channel, messageData);
returnItems.push(items[itemIndex]);
} else if (operation === 'push') {
const redisList = this.getNodeParameter('list', itemIndex) as string;
const messageData = this.getNodeParameter('messageData', itemIndex) as string;
const tail = this.getNodeParameter('tail', itemIndex, false) as boolean;
await client[tail ? 'rPush' : 'lPush'](redisList, messageData);
returnItems.push(items[itemIndex]);
} else if (operation === 'pop') {
const redisList = this.getNodeParameter('list', itemIndex) as string;
const tail = this.getNodeParameter('tail', itemIndex, false) as boolean;
const propertyName = this.getNodeParameter(
'propertyName',
itemIndex,
'propertyName',
) as string;
const value = await client[tail ? 'rPop' : 'lPop'](redisList);
let outputValue;
try {
outputValue = value && JSON.parse(value);
} catch {
outputValue = value;
}
const options = this.getNodeParameter('options', itemIndex, {});
if (options.dotNotation === false) {
item.json[propertyName] = outputValue;
} else {
set(item.json, propertyName, outputValue);
}
returnItems.push(item);
}
} catch (error) {
if (this.continueOnFail()) {
returnItems.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw new NodeOperationError(this.getNode(), error, { itemIndex });
}
}
}
return [returnItems];
} finally {
// Ensure the Redis client is always closed to prevent leaked connections
try {
await client.quit();
} catch {
// If quit fails, forcefully disconnect
try {
await client.disconnect();
} catch {
// Ignore disconnect errors in cleanup
}
}
}
}
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.redisTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "Development", "Data & Storage"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/redis/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.redistrigger/"
}
]
}
}
@@ -0,0 +1,124 @@
import type {
ITriggerFunctions,
INodeType,
INodeTypeDescription,
ITriggerResponse,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import type { RedisCredential } from './types';
import { redisConnectionTest, setupRedisClient } from './utils';
interface Options {
jsonParseBody: boolean;
onlyMessage: boolean;
}
export class RedisTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Redis Trigger',
name: 'redisTrigger',
icon: 'file:redis.svg',
group: ['trigger'],
version: 1,
description: 'Subscribe to redis channel',
defaults: {
name: 'Redis Trigger',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'redis',
required: true,
testedBy: 'redisConnectionTest',
},
],
properties: [
{
displayName: 'Channels',
name: 'channels',
type: 'string',
default: '',
required: true,
description:
'Channels to subscribe to, multiple channels be defined with comma. Wildcard character(*) is supported.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'JSON Parse Body',
name: 'jsonParseBody',
type: 'boolean',
default: false,
description: 'Whether to try to parse the message to an object',
},
{
displayName: 'Only Message',
name: 'onlyMessage',
type: 'boolean',
default: false,
description: 'Whether to return only the message property',
},
],
},
],
};
methods = {
credentialTest: { redisConnectionTest },
};
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
const credentials = await this.getCredentials<RedisCredential>('redis');
const channels = (this.getNodeParameter('channels') as string).split(',');
const options = this.getNodeParameter('options') as Options;
if (!channels) {
throw new NodeOperationError(this.getNode(), 'Channels are mandatory!');
}
const client = setupRedisClient(credentials);
await client.connect();
await client.ping();
const onMessage = (message: string, channel: string) => {
if (options.jsonParseBody) {
try {
message = JSON.parse(message);
} catch (error) {}
}
const data = options.onlyMessage ? { message } : { channel, message };
this.emit([this.helpers.returnJsonArray(data)]);
};
const manualTriggerFunction = async () =>
await new Promise<void>(async (resolve) => {
await client.pSubscribe(channels, (message, channel) => {
onMessage(message, channel);
resolve();
});
});
if (this.getMode() === 'trigger') {
await client.pSubscribe(channels, onMessage);
}
async function closeFunction() {
await client.pUnsubscribe();
await client.quit();
}
return {
closeFunction,
manualTriggerFunction,
};
}
}
@@ -0,0 +1,599 @@
import { mock } from 'jest-mock-extended';
import type {
ICredentialsDecrypted,
ICredentialTestFunctions,
IExecuteFunctions,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
const mockClient = mock<RedisClient>();
const createClient = jest.fn().mockReturnValue(mockClient);
jest.mock('redis', () => ({ createClient }));
import { Redis } from '../Redis.node';
import type { RedisClient } from '../types';
import { redisConnectionTest, setupRedisClient } from '../utils';
describe('Redis Node', () => {
const node = new Redis();
beforeEach(() => {
jest.clearAllMocks();
createClient.mockReturnValue(mockClient);
});
afterEach(() => jest.resetAllMocks());
describe('setupRedisClient', () => {
it('should not configure TLS by default', () => {
setupRedisClient({
host: 'redis.domain',
port: 1234,
database: 0,
});
expect(createClient).toHaveBeenCalledWith({
database: 0,
socket: {
host: 'redis.domain',
port: 1234,
tls: false,
connectTimeout: 10000,
reconnectStrategy: expect.any(Function),
},
});
});
it('should configure TLS', () => {
setupRedisClient({
host: 'redis.domain',
port: 1234,
database: 0,
ssl: true,
});
expect(createClient).toHaveBeenCalledWith({
database: 0,
socket: {
host: 'redis.domain',
port: 1234,
tls: true,
connectTimeout: 10000,
reconnectStrategy: expect.any(Function),
},
});
});
it('should configure TLS with verification disabled for self-signed certificates', () => {
setupRedisClient({
host: 'redis.domain',
port: 1234,
database: 0,
ssl: true,
disableTlsVerification: true,
});
expect(createClient).toHaveBeenCalledWith({
database: 0,
socket: {
host: 'redis.domain',
port: 1234,
tls: true,
rejectUnauthorized: false,
connectTimeout: 10000,
reconnectStrategy: expect.any(Function),
},
});
});
it('should not set rejectUnauthorized when TLS verification is enabled', () => {
setupRedisClient({
host: 'redis.domain',
port: 1234,
database: 0,
ssl: true,
disableTlsVerification: false,
});
expect(createClient).toHaveBeenCalledWith({
database: 0,
socket: {
host: 'redis.domain',
port: 1234,
tls: true,
connectTimeout: 10000,
reconnectStrategy: expect.any(Function),
},
});
});
it('should not set rejectUnauthorized when SSL is disabled', () => {
setupRedisClient({
host: 'redis.domain',
port: 1234,
database: 0,
ssl: false,
disableTlsVerification: true,
});
expect(createClient).toHaveBeenCalledWith({
database: 0,
socket: {
host: 'redis.domain',
port: 1234,
tls: false,
connectTimeout: 10000,
reconnectStrategy: expect.any(Function),
},
});
});
it('should set user on auth', () => {
setupRedisClient({
host: 'redis.domain',
port: 1234,
database: 0,
user: 'test_user',
password: 'test_password',
});
expect(createClient).toHaveBeenCalledWith({
database: 0,
username: 'test_user',
password: 'test_password',
socket: {
host: 'redis.domain',
port: 1234,
tls: false,
connectTimeout: 10000,
reconnectStrategy: expect.any(Function),
},
});
});
it('should configure TLS with disabled verification and auth', () => {
setupRedisClient({
host: 'redis.domain',
port: 1234,
database: 0,
ssl: true,
disableTlsVerification: true,
user: 'test_user',
password: 'test_password',
});
expect(createClient).toHaveBeenCalledWith({
database: 0,
username: 'test_user',
password: 'test_password',
socket: {
host: 'redis.domain',
port: 1234,
tls: true,
rejectUnauthorized: false,
connectTimeout: 10000,
reconnectStrategy: expect.any(Function),
},
});
});
describe('reconnectStrategy behavior', () => {
it('should stop retrying after 10 retries', () => {
setupRedisClient({
host: 'redis.domain',
port: 1234,
database: 0,
});
const call = createClient.mock.calls[0][0];
const strategy = call.socket.reconnectStrategy;
const cause = { code: 'OTHER' } as NodeJS.ErrnoException;
expect(strategy(10, cause)).toBe(false);
expect(strategy(11, cause)).toBe(false);
});
it('should return a delay with jitter for valid retry attempts', () => {
setupRedisClient({
host: 'redis.domain',
port: 1234,
database: 0,
});
const call = createClient.mock.calls[0][0];
const strategy = call.socket.reconnectStrategy;
const cause = { code: 'OTHER' } as NodeJS.ErrnoException;
const retry = 3;
const result = strategy(retry, cause);
// delay = Math.pow(2, retries) * 1000;
const baseDelay = Math.pow(2, retry) * 1000;
// jitter added is between 0 and 999
expect(result).toBeGreaterThanOrEqual(baseDelay);
expect(result).toBeLessThanOrEqual(baseDelay + 999);
});
});
});
describe('redisConnectionTest', () => {
const thisArg = mock<ICredentialTestFunctions>({});
const credentials = mock<ICredentialsDecrypted>({
data: {
host: 'localhost',
port: 6379,
user: 'username',
password: 'password',
database: 0,
},
});
const redisOptions = {
socket: {
host: 'localhost',
port: 6379,
tls: false,
connectTimeout: 10000,
reconnectStrategy: false,
},
database: 0,
username: 'username',
password: 'password',
disableOfflineQueue: true,
enableOfflineQueue: false,
};
it('should return success when connection is established', async () => {
const result = await redisConnectionTest.call(thisArg, credentials);
expect(result).toEqual({
status: 'OK',
message: 'Connection successful!',
});
expect(createClient).toHaveBeenCalledWith(redisOptions);
expect(mockClient.connect).toHaveBeenCalled();
expect(mockClient.ping).toHaveBeenCalled();
});
it('should return error when connection fails', async () => {
mockClient.connect.mockRejectedValue(new Error('Connection failed'));
const result = await redisConnectionTest.call(thisArg, credentials);
expect(result).toEqual({
status: 'Error',
message: 'Connection failed',
});
expect(createClient).toHaveBeenCalledWith(redisOptions);
expect(mockClient.connect).toHaveBeenCalled();
expect(mockClient.ping).not.toHaveBeenCalled();
});
it('should return success when connection is established with disabled TLS verification', async () => {
const credentialsWithTls = mock<ICredentialsDecrypted>({
data: {
host: 'localhost',
port: 6379,
ssl: true,
disableTlsVerification: true,
user: 'username',
password: 'password',
database: 0,
},
});
const result = await redisConnectionTest.call(thisArg, credentialsWithTls);
expect(result).toEqual({
status: 'OK',
message: 'Connection successful!',
});
expect(createClient).toHaveBeenCalledWith({
socket: {
host: 'localhost',
port: 6379,
tls: true,
rejectUnauthorized: false,
connectTimeout: 10000,
reconnectStrategy: false,
},
database: 0,
username: 'username',
password: 'password',
disableOfflineQueue: true,
enableOfflineQueue: false,
});
expect(mockClient.connect).toHaveBeenCalled();
expect(mockClient.ping).toHaveBeenCalled();
});
});
describe('operations', () => {
const thisArg = mock<IExecuteFunctions>({});
beforeEach(() => {
setupRedisClient({
host: 'redis.domain',
port: 1234,
database: 0,
ssl: true,
});
const mockCredential = {
host: 'redis',
port: 1234,
database: 0,
password: 'random',
};
thisArg.getCredentials.calledWith('redis').mockResolvedValue(mockCredential);
});
afterEach(() => {
expect(createClient).toHaveBeenCalled();
expect(mockClient.connect).toHaveBeenCalled();
expect(mockClient.ping).toHaveBeenCalled();
expect(mockClient.quit).toHaveBeenCalled();
});
describe('info operation', () => {
it('should return info', async () => {
thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('info');
mockClient.info.mockResolvedValue(`
# Server
redis_version:6.2.14
redis_git_sha1:00000000
redis_git_dirty:0
redis_mode:standalone
arch_bits:64
tcp_port:6379
uptime_in_seconds:429905
uptime_in_days:4
# Clients
connected_clients:1
cluster_connections:0
max_clients:10000
# Memory
used_memory:876648
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
`);
const output = await node.execute.call(thisArg);
expect(mockClient.info).toHaveBeenCalled();
expect(output[0][0].json).toEqual({
redis_version: 6.2,
redis_git_sha1: 0,
redis_git_dirty: 0,
redis_mode: 'standalone',
arch_bits: 64,
tcp_port: 6379,
uptime_in_seconds: 429905,
uptime_in_days: 4,
connected_clients: 1,
cluster_connections: 0,
max_clients: 10000,
used_memory: 876648,
role: 'master',
connected_slaves: 0,
master_failover_state: 'no-failover',
});
});
it('should continue and return an error when continue on fail is enabled and an error is thrown', async () => {
thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('info');
thisArg.continueOnFail.mockReturnValue(true);
mockClient.info.mockRejectedValue(new Error('Redis error'));
const output = await node.execute.call(thisArg);
expect(mockClient.info).toHaveBeenCalled();
expect(output[0][0].json).toEqual({ error: 'Redis error' });
});
it('should throw an error when continue on fail is disabled and an error is thrown', async () => {
thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('info');
thisArg.continueOnFail.mockReturnValue(false);
mockClient.info.mockRejectedValue(new Error('Redis error'));
await expect(node.execute.call(thisArg)).rejects.toThrow(NodeOperationError);
expect(mockClient.info).toHaveBeenCalled();
expect(mockClient.quit).toHaveBeenCalled();
});
});
describe('delete operation', () => {
it('should delete', async () => {
thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]);
thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('delete');
thisArg.getNodeParameter.calledWith('key', 0).mockReturnValue('key1');
mockClient.del.calledWith('key1').mockResolvedValue(1);
const output = await node.execute.call(thisArg);
expect(mockClient.del).toHaveBeenCalledWith('key1');
expect(output[0][0].json).toEqual({ x: 1 });
});
it('should continue and return an error when continue on fail is enabled and an error is thrown', async () => {
thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]);
thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('delete');
thisArg.getNodeParameter.calledWith('key', 0).mockReturnValue('key1');
thisArg.continueOnFail.mockReturnValue(true);
mockClient.del.mockRejectedValue(new Error('Redis error'));
const output = await node.execute.call(thisArg);
expect(mockClient.del).toHaveBeenCalled();
expect(output[0][0].json).toEqual({ error: 'Redis error' });
});
it('should throw an error when continue on fail is disabled and an error is thrown', async () => {
thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]);
thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('delete');
thisArg.getNodeParameter.calledWith('key', 0).mockReturnValue('key1');
mockClient.del.mockRejectedValue(new Error('Redis error'));
await expect(node.execute.call(thisArg)).rejects.toThrow(NodeOperationError);
expect(mockClient.del).toHaveBeenCalled();
expect(mockClient.quit).toHaveBeenCalled();
});
});
describe('get operation', () => {
beforeEach(() => {
thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]);
thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('get');
thisArg.getNodeParameter.calledWith('options', 0).mockReturnValue({ dotNotation: true });
thisArg.getNodeParameter.calledWith('key', 0).mockReturnValue('key1');
thisArg.getNodeParameter.calledWith('propertyName', 0).mockReturnValue('x.y');
});
it('keyType = automatic', async () => {
thisArg.getNodeParameter.calledWith('keyType', 0).mockReturnValue('automatic');
mockClient.type.calledWith('key1').mockResolvedValue('string');
mockClient.get.calledWith('key1').mockResolvedValue('value');
const output = await node.execute.call(thisArg);
expect(mockClient.type).toHaveBeenCalledWith('key1');
expect(mockClient.get).toHaveBeenCalledWith('key1');
expect(output[0][0].json).toEqual({ x: { y: 'value' } });
});
it('keyType = hash', async () => {
thisArg.getNodeParameter.calledWith('keyType', 0).mockReturnValue('hash');
mockClient.hGetAll.calledWith('key1').mockResolvedValue({
field1: '1',
field2: '2',
});
const output = await node.execute.call(thisArg);
expect(mockClient.hGetAll).toHaveBeenCalledWith('key1');
expect(output[0][0].json).toEqual({
x: {
y: {
field1: '1',
field2: '2',
},
},
});
});
it('should continue and return an error when continue on fail is enabled and an error is thrown', async () => {
thisArg.getNodeParameter.calledWith('keyType', 0).mockReturnValue('automatic');
thisArg.continueOnFail.mockReturnValue(true);
mockClient.type.calledWith('key1').mockResolvedValue('string');
mockClient.get.mockRejectedValue(new Error('Redis error'));
const output = await node.execute.call(thisArg);
expect(mockClient.get).toHaveBeenCalled();
expect(output[0][0].json).toEqual({ error: 'Redis error' });
});
it('should throw an error when continue on fail is disabled and an error is thrown', async () => {
thisArg.getNodeParameter.calledWith('keyType', 0).mockReturnValue('automatic');
mockClient.type.calledWith('key1').mockResolvedValue('string');
mockClient.get.mockRejectedValue(new Error('Redis error'));
await expect(node.execute.call(thisArg)).rejects.toThrow(NodeOperationError);
expect(mockClient.get).toHaveBeenCalled();
expect(mockClient.quit).toHaveBeenCalled();
});
});
describe('keys operation', () => {
beforeEach(() => {
thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]);
thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('keys');
thisArg.getNodeParameter.calledWith('keyPattern', 0).mockReturnValue('key*');
mockClient.keys.calledWith('key*').mockResolvedValue(['key1', 'key2']);
});
it('getValues = false', async () => {
thisArg.getNodeParameter.calledWith('getValues', 0).mockReturnValue(false);
const output = await node.execute.call(thisArg);
expect(mockClient.keys).toHaveBeenCalledWith('key*');
expect(output[0][0].json).toEqual({ keys: ['key1', 'key2'] });
});
it('getValues = true', async () => {
thisArg.getNodeParameter.calledWith('getValues', 0).mockReturnValue(true);
mockClient.type.mockResolvedValue('string');
mockClient.get.calledWith('key1').mockResolvedValue('value1');
mockClient.get.calledWith('key2').mockResolvedValue('value2');
const output = await node.execute.call(thisArg);
expect(mockClient.keys).toHaveBeenCalledWith('key*');
expect(output[0][0].json).toEqual({ key1: 'value1', key2: 'value2' });
});
it('should continue and return an error when continue on fail is enabled and an error is thrown', async () => {
thisArg.continueOnFail.mockReturnValue(true);
thisArg.getNodeParameter.calledWith('getValues', 0).mockReturnValue(true);
mockClient.type.mockResolvedValue('string');
mockClient.get.mockRejectedValue(new Error('Redis error'));
const output = await node.execute.call(thisArg);
expect(mockClient.get).toHaveBeenCalled();
expect(output[0][0].json).toEqual({ error: 'Redis error' });
});
it('should throw an error when continue on fail is disabled and an error is thrown', async () => {
thisArg.getNodeParameter.calledWith('getValues', 0).mockReturnValue(true);
mockClient.type.mockResolvedValue('string');
mockClient.get.mockRejectedValue(new Error('Redis error'));
await expect(node.execute.call(thisArg)).rejects.toThrow(NodeOperationError);
expect(mockClient.get).toHaveBeenCalled();
expect(mockClient.quit).toHaveBeenCalled();
});
describe('llen operation', () => {
beforeEach(() => {
thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]);
thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('llen');
thisArg.getNodeParameter.calledWith('list', 0).mockReturnValue('bull:main:q');
});
it('should return the length of a list', async () => {
mockClient.lLen.calledWith('bull:main:q').mockResolvedValue(42);
const output = await node.execute.call(thisArg);
expect(mockClient.lLen).toHaveBeenCalledWith('bull:main:q');
expect(output[0][0].json).toEqual({ 'bull:main:q': 42 });
});
it('should continue and return an error when continue on fail is enabled and an error is thrown', async () => {
thisArg.continueOnFail.mockReturnValue(true);
mockClient.lLen.mockRejectedValue(new Error('Redis error'));
const output = await node.execute.call(thisArg);
expect(mockClient.lLen).toHaveBeenCalled();
expect(output[0][0].json).toEqual({ error: 'Redis error' });
});
it('should throw an error when continue on fail is disabled and an error is thrown', async () => {
mockClient.lLen.mockRejectedValue(new Error('Redis error'));
await expect(node.execute.call(thisArg)).rejects.toThrow(NodeOperationError);
expect(mockClient.lLen).toHaveBeenCalled();
expect(mockClient.quit).toHaveBeenCalled();
});
});
});
});
});
@@ -0,0 +1,120 @@
import { captor, mock } from 'jest-mock-extended';
import { returnJsonArray } from 'n8n-core';
import type { ICredentialDataDecryptedObject, ITriggerFunctions } from 'n8n-workflow';
import { RedisTrigger } from '../RedisTrigger.node';
import type { RedisClient } from '../types';
import { setupRedisClient } from '../utils';
jest.mock('../utils', () => {
const mockRedisClient = mock<RedisClient>();
return {
setupRedisClient: jest.fn().mockReturnValue(mockRedisClient),
};
});
describe('Redis Trigger Node', () => {
const channel = 'testing';
const credentials = mock<ICredentialDataDecryptedObject>();
const triggerFunctions = mock<ITriggerFunctions>({
helpers: { returnJsonArray },
});
beforeEach(() => {
jest.clearAllMocks();
triggerFunctions.getCredentials.calledWith('redis').mockResolvedValue(credentials);
triggerFunctions.getNodeParameter.calledWith('channels').mockReturnValue(channel);
});
it('should emit in manual mode', async () => {
triggerFunctions.getMode.mockReturnValue('manual');
triggerFunctions.getNodeParameter.calledWith('options').mockReturnValue({});
const response = await new RedisTrigger().trigger.call(triggerFunctions);
expect(response.manualTriggerFunction).toBeDefined();
expect(response.closeFunction).toBeDefined();
expect(triggerFunctions.getCredentials).toHaveBeenCalledTimes(1);
expect(triggerFunctions.getNodeParameter).toHaveBeenCalledTimes(2);
const mockRedisClient = setupRedisClient(mock());
expect(mockRedisClient.connect).toHaveBeenCalledTimes(1);
expect(mockRedisClient.ping).toHaveBeenCalledTimes(1);
// manually trigger the node, like Workflow.runNode does
const triggerPromise = response.manualTriggerFunction!();
const onMessageCaptor = captor<(message: string, channel: string) => unknown>();
expect(mockRedisClient.pSubscribe).toHaveBeenCalledWith([channel], onMessageCaptor);
expect(triggerFunctions.emit).not.toHaveBeenCalled();
// simulate a message
const onMessage = onMessageCaptor.value;
onMessage('{"testing": true}', channel);
expect(triggerFunctions.emit).toHaveBeenCalledWith([
[{ json: { message: '{"testing": true}', channel } }],
]);
// wait for the promise to resolve
await new Promise((resolve) => setImmediate(resolve));
await expect(triggerPromise).resolves.toEqual(undefined);
expect(mockRedisClient.quit).not.toHaveBeenCalled();
await response.closeFunction!();
expect(mockRedisClient.pUnsubscribe).toHaveBeenCalledTimes(1);
expect(mockRedisClient.quit).toHaveBeenCalledTimes(1);
});
it('should emit in trigger mode', async () => {
triggerFunctions.getMode.mockReturnValue('trigger');
triggerFunctions.getNodeParameter.calledWith('options').mockReturnValue({});
const response = await new RedisTrigger().trigger.call(triggerFunctions);
expect(response.manualTriggerFunction).toBeDefined();
expect(response.closeFunction).toBeDefined();
expect(triggerFunctions.getCredentials).toHaveBeenCalledTimes(1);
expect(triggerFunctions.getNodeParameter).toHaveBeenCalledTimes(2);
const mockRedisClient = setupRedisClient(mock());
expect(mockRedisClient.connect).toHaveBeenCalledTimes(1);
expect(mockRedisClient.ping).toHaveBeenCalledTimes(1);
const onMessageCaptor = captor<(message: string, channel: string) => unknown>();
expect(mockRedisClient.pSubscribe).toHaveBeenCalledWith([channel], onMessageCaptor);
expect(triggerFunctions.emit).not.toHaveBeenCalled();
// simulate a message
const onMessage = onMessageCaptor.value;
onMessage('{"testing": true}', channel);
expect(triggerFunctions.emit).toHaveBeenCalledWith([
[{ json: { message: '{"testing": true}', channel } }],
]);
expect(mockRedisClient.quit).not.toHaveBeenCalled();
await response.closeFunction!();
expect(mockRedisClient.pUnsubscribe).toHaveBeenCalledTimes(1);
expect(mockRedisClient.quit).toHaveBeenCalledTimes(1);
});
it('should parse JSON messages when configured', async () => {
triggerFunctions.getMode.mockReturnValue('trigger');
triggerFunctions.getNodeParameter.calledWith('options').mockReturnValue({
jsonParseBody: true,
});
await new RedisTrigger().trigger.call(triggerFunctions);
const mockRedisClient = setupRedisClient(mock());
const onMessageCaptor = captor<(message: string, channel: string) => unknown>();
expect(mockRedisClient.pSubscribe).toHaveBeenCalledWith([channel], onMessageCaptor);
// simulate a message
const onMessage = onMessageCaptor.value;
onMessage('{"testing": true}', channel);
expect(triggerFunctions.emit).toHaveBeenCalledWith([
[{ json: { message: { testing: true }, channel } }],
]);
});
});
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><g fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"><path fill="#A41E11" d="M57.656 43.99c-3.201 1.683-19.787 8.561-23.318 10.417s-5.494 1.838-8.283.494c-2.79-1.343-20.449-8.535-23.629-10.067C.834 44.066.002 43.422.002 42.811v-6.117s22.98-5.045 26.69-6.388 4.995-1.39 8.154-.225c3.16 1.165 22.035 4.603 25.154 5.756v6.032c0 .605-.72 1.283-2.35 2.124z"/><path fill="#D82C20" d="M57.656 37.872c-3.201 1.685-19.787 8.56-23.318 10.417s-5.494 1.838-8.283.494c-2.79-1.343-20.449-8.534-23.63-10.068s-3.243-2.588-.122-3.82l24.388-9.52c3.71-1.34 4.994-1.39 8.153-.225s19.643 7.78 22.747 8.951c3.103 1.17 3.24 2.086.037 3.786z"/><path fill="#A41E11" d="M57.656 34.015c-3.201 1.683-19.787 8.561-23.318 10.417s-5.494 1.838-8.283.495c-2.79-1.344-20.449-8.536-23.629-10.067C.834 34.092.002 33.447.002 32.836V26.72s22.98-5.045 26.69-6.387c3.711-1.343 4.995-1.39 8.154-.225 3.16 1.165 22.035 4.602 25.154 5.756v6.032c0 .605-.72 1.283-2.35 2.123z"/><path fill="#D82C20" d="M57.656 27.898c-3.201 1.685-19.787 8.561-23.318 10.417s-5.494 1.838-8.283.495c-2.79-1.344-20.449-8.534-23.63-10.067-3.18-1.534-3.243-2.588-.122-3.82l24.388-9.52c3.71-1.343 4.994-1.39 8.153-.225 3.16 1.166 19.644 7.785 22.765 8.935s3.24 2.085.038 3.785z"/><path fill="#A41E11" d="M57.656 23.671c-3.201 1.683-19.787 8.561-23.318 10.419s-5.494 1.838-8.283.495c-2.79-1.344-20.449-8.535-23.629-10.069-1.592-.765-2.424-1.411-2.424-2.02v-6.11s22.98-5.045 26.69-6.388 4.995-1.39 8.154-.225c3.16 1.165 22.035 4.591 25.154 5.745v6.032c0 .605-.72 1.283-2.35 2.123z"/><path fill="#D82C20" d="M57.656 17.553c-3.201 1.685-19.787 8.561-23.318 10.417s-5.494 1.838-8.283.495c-2.79-1.344-20.449-8.534-23.63-10.068s-3.243-2.587-.122-3.82l24.388-9.52c3.71-1.343 4.994-1.39 8.153-.226 3.16 1.165 19.643 7.785 22.765 8.936s3.24 2.085.038 3.785z"/><path fill="#FFF" d="m31.497 15.032-1.88-3.153-6.002-.545 4.48-1.63L26.75 7.2l4.192 1.653 3.955-1.305-1.07 2.586 4.032 1.524-5.198.546zm-10.014 6.275 13.903-2.153-4.2 6.211zm-11.17-5.167c0-1.61 3.314-2.906 7.431-2.906 4.118 0 7.432 1.296 7.432 2.906s-3.314 2.905-7.432 2.905c-4.117 0-7.431-1.295-7.431-2.905"/><path fill="#7A0C00" d="m52.233 15.714-8.224 3.276-.007-6.556z"/><path fill="#AD2115" d="m44.01 18.991-.89.353-8.217-3.276 9.094-3.63z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+13
View File
@@ -0,0 +1,13 @@
import type { createClient } from 'redis';
export type RedisClient = ReturnType<typeof createClient>;
export type RedisCredential = {
host: string;
port: number;
ssl?: boolean;
disableTlsVerification?: boolean;
database: number;
user?: string;
password?: string;
};
+241
View File
@@ -0,0 +1,241 @@
import type {
ICredentialTestFunctions,
ICredentialsDecrypted,
IDataObject,
IExecuteFunctions,
INodeCredentialTestResult,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { createClient } from 'redis';
import type { RedisCredential, RedisClient } from './types';
export function setupRedisClient(credentials: RedisCredential, isTest = false): RedisClient {
const socketConfig: any = {
host: credentials.host,
port: credentials.port,
tls: credentials.ssl === true,
connectTimeout: 10000,
reconnectStrategy: isTest
? false
: (retries: number) => {
// Retry for ~15min
if (retries >= 10) {
return false;
}
const jitter = Math.floor(Math.random() * 1000);
const delay = Math.pow(2, retries) * 1000;
return delay + jitter;
},
};
// If SSL is enabled and TLS verification should be disabled
if (credentials.ssl === true && credentials.disableTlsVerification === true) {
socketConfig.rejectUnauthorized = false;
}
const client = createClient({
socket: socketConfig,
database: credentials.database,
username: credentials.user ?? undefined,
password: credentials.password ?? undefined,
...(isTest && {
disableOfflineQueue: true,
enableOfflineQueue: false,
}),
});
client.on('error', () => {
// intentionally ignored,required for reconnectStrategy to function, error will be caught by try catch
// https://github.com/redis/node-redis/blob/a64134c55f550f2f102a0f512218bc11717a5e16/README.md?plain=1#L298
});
return client;
}
export async function redisConnectionTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> {
const credentials = credential.data as RedisCredential;
let client: RedisClient | undefined;
try {
client = setupRedisClient(credentials, true);
// Add error event handler to catch connection errors
const errorPromise = new Promise<never>((_, reject) => {
client!.on('error', (err) => {
reject(err);
});
});
// Create a timeout promise
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error('Connection timeout: Unable to connect to Redis server'));
}, 10000); // 10 seconds timeout
});
// Race between connecting and error/timeout
await Promise.race([client.connect(), errorPromise, timeoutPromise]);
await client.ping();
return {
status: 'OK',
message: 'Connection successful!',
};
} catch (error) {
// Handle specific error types for better user feedback
let errorMessage = error.message;
if (error.code === 'ECONNRESET') {
errorMessage =
'Connection reset: The Redis server rejected the connection. This often happens when trying to connect without SSL to an SSL-only server.';
} else if (error.code === 'ECONNREFUSED') {
errorMessage =
'Connection refused: Unable to connect to the Redis server. Please check the host and port.';
}
return {
status: 'Error',
message: errorMessage,
};
} finally {
// Ensure the Redis client is always closed to prevent leaked connections
if (client) {
try {
await client.quit();
} catch {
// If quit fails, forcefully disconnect
try {
await client.disconnect();
} catch {
// Ignore disconnect errors in cleanup
}
}
}
}
}
/** Parses the given value in a number if it is one else returns a string */
function getParsedValue(value: string): string | number {
if (value.match(/^[\d.]+$/) === null) {
// Is a string
return value;
} else {
// Is a number
return parseFloat(value);
}
}
/** Converts the Redis Info String into an object */
export function convertInfoToObject(stringData: string): IDataObject {
const returnData: IDataObject = {};
let key: string, value: string;
for (const line of stringData.split('\n')) {
if (['#', ''].includes(line.charAt(0))) {
continue;
}
[key, value] = line.split(':');
if (key === undefined || value === undefined) {
continue;
}
value = value.trim();
if (value.includes('=')) {
returnData[key] = {};
let key2: string, value2: string;
for (const keyValuePair of value.split(',')) {
[key2, value2] = keyValuePair.split('=');
(returnData[key] as IDataObject)[key2] = getParsedValue(value2);
}
} else {
returnData[key] = getParsedValue(value);
}
}
return returnData;
}
export async function getValue(client: RedisClient, keyName: string, type?: string) {
if (type === undefined || type === 'automatic') {
// Request the type first
type = await client.type(keyName);
}
if (type === 'string') {
return await client.get(keyName);
} else if (type === 'hash') {
return await client.hGetAll(keyName);
} else if (type === 'list') {
return await client.lRange(keyName, 0, -1);
} else if (type === 'sets') {
return await client.sMembers(keyName);
}
}
export async function setValue(
this: IExecuteFunctions,
client: RedisClient,
keyName: string,
value: string | number | object | string[] | number[],
expire: boolean,
ttl: number,
type?: string,
valueIsJSON?: boolean,
) {
if (type === undefined || type === 'automatic') {
// Request the type first
if (typeof value === 'string') {
type = 'string';
} else if (Array.isArray(value)) {
type = 'list';
} else if (typeof value === 'object') {
type = 'hash';
} else {
throw new NodeOperationError(
this.getNode(),
'Could not identify the type to set. Please set it manually!',
);
}
}
if (type === 'string') {
await client.set(keyName, value.toString());
} else if (type === 'hash') {
if (valueIsJSON) {
let values: unknown;
if (typeof value === 'string') {
try {
values = JSON.parse(value);
} catch {
// This is how we originally worked and prevents a breaking change
values = value;
}
} else {
values = value;
}
for (const key of Object.keys(values as object)) {
await client.hSet(keyName, key, (values as IDataObject)[key]!.toString());
}
} else {
const values = value.toString().split(' ');
await client.hSet(keyName, values);
}
} else if (type === 'list') {
for (let index = 0; index < (value as string[]).length; index++) {
await client.lSet(keyName, index, (value as IDataObject)[index]!.toString());
}
} else if (type === 'sets') {
//@ts-ignore
await client.sAdd(keyName, value);
}
if (expire) {
await client.expire(keyName, ttl);
}
return;
}