feat(Redis Node): Update node-redis (no-changelog) (#8269)

Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-01-17 10:15:20 +01:00 committed by GitHub
parent 3734c89cf6
commit ab52aaf7e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 517 additions and 426 deletions

View file

@ -148,7 +148,7 @@
"openai": "4.20.0", "openai": "4.20.0",
"pdf-parse": "1.1.1", "pdf-parse": "1.1.1",
"pg": "8.11.3", "pg": "8.11.3",
"redis": "4.6.11", "redis": "4.6.12",
"sqlite3": "5.1.6", "sqlite3": "5.1.6",
"temp": "0.9.4", "temp": "0.9.4",
"typeorm": "0.3.17", "typeorm": "0.3.17",

View file

@ -1,19 +1,19 @@
import util from 'util';
import type { import type {
IExecuteFunctions, IExecuteFunctions,
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeCredentialTestResult,
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import set from 'lodash/set'; import set from 'lodash/set';
import redis from 'redis';
import {
setupRedisClient,
redisConnectionTest,
convertInfoToObject,
getValue,
setValue,
} from './utils';
export class Redis implements INodeType { export class Redis implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -100,6 +100,23 @@ export class Redis implements INodeType {
default: 'info', 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 // get
// ---------------------------------- // ----------------------------------
@ -117,19 +134,6 @@ export class Redis implements INodeType {
description: description:
'Name of the property to write received data to. Supports dot-notation. Example: "data.person[0].name".', '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: ['delete'],
},
},
default: '',
required: true,
description: 'Name of the key to delete from Redis',
},
{ {
displayName: 'Key', displayName: 'Key',
name: 'key', name: 'key',
@ -498,345 +502,135 @@ export class Redis implements INodeType {
}; };
methods = { methods = {
credentialTest: { credentialTest: { redisConnectionTest },
async redisConnectionTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> {
const credentials = credential.data as ICredentialDataDecryptedObject;
const redisOptions: redis.ClientOpts = {
host: credentials.host as string,
port: credentials.port as number,
db: credentials.database as number,
};
if (credentials.password) {
redisOptions.password = credentials.password as string;
}
try {
const client = redis.createClient(redisOptions);
await new Promise((resolve, reject): any => {
client.on('connect', async () => {
client.ping('ping', (error, pong) => {
if (error) reject(error);
resolve(pong);
client.quit();
});
});
client.on('error', async (err) => {
client.quit();
reject(err);
});
});
} catch (error) {
return {
status: 'Error',
message: error.message,
};
}
return {
status: 'OK',
message: 'Connection successful!',
};
},
},
}; };
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions) {
// Parses the given value in a number if it is one else returns a string // TODO: For array and object fields it should not have a "value" field it should
function getParsedValue(value: string): string | number { // have a parameter field for a path. Because it is not possible to set
if (value.match(/^[\d\.]+$/) === null) { // array, object via parameter directly (should maybe be possible?!?!)
// Is a string // Should maybe have a parameter which is JSON.
return value; const credentials = await this.getCredentials('redis');
} else {
// Is a number
return parseFloat(value);
}
}
// Converts the Redis Info String into an object const client = setupRedisClient(credentials);
function convertInfoToObject(stringData: string): IDataObject { await client.connect();
const returnData: IDataObject = {}; await client.ping();
let key: string, value: string; const operation = this.getNodeParameter('operation', 0);
for (const line of stringData.split('\n')) { const returnItems: INodeExecutionData[] = [];
if (['#', ''].includes(line.charAt(0))) {
continue;
}
[key, value] = line.split(':');
if (key === undefined || value === undefined) {
continue;
}
value = value.trim();
if (value.includes('=')) { try {
returnData[key] = {}; if (operation === 'info') {
let key2: string, value2: string; const result = await client.info();
for (const keyValuePair of value.split(',')) { returnItems.push({ json: convertInfoToObject(result) });
[key2, value2] = keyValuePair.split('='); } else if (
(returnData[key] as IDataObject)[key2] = getParsedValue(value2); ['delete', 'get', 'keys', 'set', 'incr', 'publish', 'push', 'pop'].includes(operation)
} ) {
} else { const items = this.getInputData();
returnData[key] = getParsedValue(value);
}
}
return returnData; let item: INodeExecutionData;
} for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
item = { json: {} };
async function getValue(client: redis.RedisClient, keyName: string, type?: string) { if (operation === 'delete') {
if (type === undefined || type === 'automatic') { const keyDelete = this.getNodeParameter('key', itemIndex) as string;
// Request the type first
const clientType = util.promisify(client.type).bind(client);
type = await clientType(keyName);
}
if (type === 'string') { await client.del(keyDelete);
const clientGet = util.promisify(client.get).bind(client); returnItems.push(items[itemIndex]);
return clientGet(keyName); } else if (operation === 'get') {
} else if (type === 'hash') { const propertyName = this.getNodeParameter('propertyName', itemIndex) as string;
const clientHGetAll = util.promisify(client.hgetall).bind(client); const keyGet = this.getNodeParameter('key', itemIndex) as string;
return clientHGetAll(keyName); const keyType = this.getNodeParameter('keyType', itemIndex) as string;
} else if (type === 'list') {
const clientLRange = util.promisify(client.lrange).bind(client);
return clientLRange(keyName, 0, -1);
} else if (type === 'sets') {
const clientSMembers = util.promisify(client.smembers).bind(client);
return clientSMembers(keyName);
}
}
const setValue = async ( const value = (await getValue(client, keyGet, keyType)) ?? null;
client: redis.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') { const options = this.getNodeParameter('options', itemIndex, {});
const clientSet = util.promisify(client.set).bind(client);
await clientSet(keyName, value.toString()); if (options.dotNotation === false) {
} else if (type === 'hash') { item.json[propertyName] = value;
const clientHset = util.promisify(client.hset).bind(client); } else {
if (valueIsJSON) { set(item.json, propertyName, value);
let values: unknown; }
if (typeof value === 'string') {
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 === '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 { try {
values = JSON.parse(value); outputValue = value && JSON.parse(value);
} catch { } catch {
// This is how we originally worked and prevents a breaking change outputValue = value;
values = value;
} }
} else { const options = this.getNodeParameter('options', itemIndex, {});
values = value; if (options.dotNotation === false) {
} item.json[propertyName] = outputValue;
for (const key of Object.keys(values as object)) { } else {
// @ts-ignore set(item.json, propertyName, outputValue);
await clientHset(keyName, key, (values as IDataObject)[key]!.toString());
}
} else {
const values = value.toString().split(' ');
//@ts-ignore
await clientHset(keyName, values);
}
} else if (type === 'list') {
const clientLset = util.promisify(client.lset).bind(client);
for (let index = 0; index < (value as string[]).length; index++) {
await clientLset(keyName, index, (value as IDataObject)[index]!.toString());
}
} else if (type === 'sets') {
const clientSadd = util.promisify(client.sadd).bind(client);
//@ts-ignore
await clientSadd(keyName, value);
}
if (expire) {
const clientExpire = util.promisify(client.expire).bind(client);
await clientExpire(keyName, ttl);
}
return;
};
return new Promise(async (resolve, reject) => {
// 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('redis');
const redisOptions: redis.ClientOpts = {
host: credentials.host as string,
port: credentials.port as number,
db: credentials.database as number,
};
if (credentials.password) {
redisOptions.password = credentials.password as string;
}
const client = redis.createClient(redisOptions);
const operation = this.getNodeParameter('operation', 0);
client.on('error', (err: Error) => {
client.quit();
reject(err);
});
client.on('ready', async (_err: Error | null) => {
client.select(credentials.database as number);
try {
if (operation === 'info') {
const clientInfo = util.promisify(client.info).bind(client);
const result = await clientInfo();
resolve([[{ json: convertInfoToObject(result as string) }]]);
client.quit();
} else if (
['delete', 'get', 'keys', 'set', 'incr', 'publish', 'push', 'pop'].includes(operation)
) {
const items = this.getInputData();
const returnItems: INodeExecutionData[] = [];
let item: INodeExecutionData;
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
item = { json: {} };
if (operation === 'delete') {
const keyDelete = this.getNodeParameter('key', itemIndex) as string;
const clientDel = util.promisify(client.del).bind(client);
// @ts-ignore
await clientDel(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 clientKeys = util.promisify(client.keys).bind(client);
const keys = await clientKeys(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 === '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(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 clientIncr = util.promisify(client.incr).bind(client);
// @ts-ignore
const incrementVal = await clientIncr(keyIncr);
if (expire && ttl > 0) {
const clientExpire = util.promisify(client.expire).bind(client);
await clientExpire(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;
const clientPublish = util.promisify(client.publish).bind(client);
await clientPublish(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;
const action = tail ? client.RPUSH : client.LPUSH;
const clientPush = util.promisify(action).bind(client);
// @ts-ignore: typescript not understanding generic function signatures
await clientPush(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 action = tail ? client.rpop : client.lpop;
const clientPop = util.promisify(action).bind(client);
const value = await clientPop(redisList);
let outputValue;
try {
outputValue = 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);
}
} }
returnItems.push(item);
client.quit();
resolve([returnItems]);
} }
} catch (error) {
reject(error);
} }
}); }
}); } catch (error) {
throw error;
} finally {
await client.quit();
}
return [returnItems];
} }
} }

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-loop-func */
import type { import type {
ITriggerFunctions, ITriggerFunctions,
IDataObject, IDataObject,
@ -7,7 +8,7 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow';
import redis from 'redis'; import { redisConnectionTest, setupRedisClient } from './utils';
export class RedisTrigger implements INodeType { export class RedisTrigger implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -26,6 +27,7 @@ export class RedisTrigger implements INodeType {
{ {
name: 'redis', name: 'redis',
required: true, required: true,
testedBy: 'redisConnectionTest',
}, },
], ],
properties: [ properties: [
@ -64,36 +66,29 @@ export class RedisTrigger implements INodeType {
], ],
}; };
methods = {
credentialTest: { redisConnectionTest },
};
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> { async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
const credentials = await this.getCredentials('redis'); const credentials = await this.getCredentials('redis');
const redisOptions: redis.ClientOpts = {
host: credentials.host as string,
port: credentials.port as number,
db: credentials.database as number,
};
if (credentials.password) {
redisOptions.password = credentials.password as string;
}
const channels = (this.getNodeParameter('channels') as string).split(','); const channels = (this.getNodeParameter('channels') as string).split(',');
const options = this.getNodeParameter('options') as IDataObject; const options = this.getNodeParameter('options') as IDataObject;
if (!channels) { if (!channels) {
throw new NodeOperationError(this.getNode(), 'Channels are mandatory!'); throw new NodeOperationError(this.getNode(), 'Channels are mandatory!');
} }
const client = redis.createClient(redisOptions); const client = setupRedisClient(credentials);
const manualTriggerFunction = async () => { const manualTriggerFunction = async () => {
await new Promise((resolve, reject) => { await client.connect();
client.on('connect', () => { await client.ping();
for (const channel of channels) {
client.psubscribe(channel); try {
} for (const channel of channels) {
client.on('pmessage', (pattern: string, channel: string, message: string) => { await client.pSubscribe(channel, (message) => {
if (options.jsonParseBody) { if (options.jsonParseBody) {
try { try {
message = JSON.parse(message); message = JSON.parse(message);
@ -102,19 +97,15 @@ export class RedisTrigger implements INodeType {
if (options.onlyMessage) { if (options.onlyMessage) {
this.emit([this.helpers.returnJsonArray({ message })]); this.emit([this.helpers.returnJsonArray({ message })]);
resolve(true);
return; return;
} }
this.emit([this.helpers.returnJsonArray({ channel, message })]); this.emit([this.helpers.returnJsonArray({ channel, message })]);
resolve(true);
}); });
}); }
} catch (error) {
client.on('error', (error) => { throw new NodeOperationError(this.getNode(), error);
reject(error); }
});
});
}; };
if (this.getMode() === 'trigger') { if (this.getMode() === 'trigger') {
@ -122,7 +113,7 @@ export class RedisTrigger implements INodeType {
} }
async function closeFunction() { async function closeFunction() {
client.quit(); await client.quit();
} }
return { return {

View file

@ -0,0 +1,158 @@
import { mock } from 'jest-mock-extended';
import type { RedisClientType } from '@redis/client';
import type { IExecuteFunctions } from 'n8n-workflow';
import { Redis } from '../Redis.node';
const mockClient = mock<RedisClientType>();
jest.mock('redis', () => ({
createClient: () => mockClient,
}));
describe('Redis Node', () => {
const mockCredential = {
host: 'redis',
port: 1234,
database: 0,
password: 'random',
};
const node = new Redis();
const thisArg = mock<IExecuteFunctions>({});
thisArg.getCredentials.calledWith('redis').mockResolvedValue(mockCredential);
beforeEach(() => jest.clearAllMocks());
afterEach(() => {
expect(mockClient.connect).toHaveBeenCalled();
expect(mockClient.ping).toHaveBeenCalled();
expect(mockClient.quit).toHaveBeenCalled();
});
it('info operation', 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('delete operation', 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 });
});
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',
},
},
});
});
});
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' });
});
});
});

View file

@ -0,0 +1,168 @@
import type {
ICredentialDataDecryptedObject,
ICredentialTestFunctions,
ICredentialsDecrypted,
IDataObject,
IExecuteFunctions,
INodeCredentialTestResult,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { createClient } from 'redis';
export type RedisClientType = ReturnType<typeof createClient>;
export function setupRedisClient(credentials: ICredentialDataDecryptedObject): RedisClientType {
const redisOptions = {
socket: {
host: credentials.host as string,
port: credentials.port as number,
},
database: credentials.database as number,
password: (credentials.password as string) || undefined,
};
return createClient(redisOptions);
}
export async function redisConnectionTest(
this: ICredentialTestFunctions,
credential: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> {
const credentials = credential.data as ICredentialDataDecryptedObject;
try {
const client = setupRedisClient(credentials);
await client.connect();
await client.ping();
return {
status: 'OK',
message: 'Connection successful!',
};
} catch (error) {
return {
status: 'Error',
message: error.message,
};
}
}
/** 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: RedisClientType, keyName: string, type?: string) {
if (type === undefined || type === 'automatic') {
// Request the type first
type = await client.type(keyName);
}
if (type === 'string') {
return client.get(keyName);
} else if (type === 'hash') {
return client.hGetAll(keyName);
} else if (type === 'list') {
return client.lRange(keyName, 0, -1);
} else if (type === 'sets') {
return client.sMembers(keyName);
}
}
export async function setValue(
this: IExecuteFunctions,
client: RedisClientType,
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;
}

View file

@ -816,7 +816,6 @@
"@types/node-ssh": "^7.0.1", "@types/node-ssh": "^7.0.1",
"@types/nodemailer": "^6.4.0", "@types/nodemailer": "^6.4.0",
"@types/promise-ftp": "^1.3.4", "@types/promise-ftp": "^1.3.4",
"@types/redis": "^2.8.11",
"@types/request-promise-native": "~1.0.15", "@types/request-promise-native": "~1.0.15",
"@types/rfc2047": "^2.0.1", "@types/rfc2047": "^2.0.1",
"@types/showdown": "^1.9.4", "@types/showdown": "^1.9.4",
@ -877,7 +876,7 @@
"pretty-bytes": "5.6.0", "pretty-bytes": "5.6.0",
"promise-ftp": "1.3.5", "promise-ftp": "1.3.5",
"pyodide": "0.23.4", "pyodide": "0.23.4",
"redis": "3.1.2", "redis": "4.6.12",
"rfc2047": "4.0.1", "rfc2047": "4.0.1",
"rhea": "1.0.24", "rhea": "1.0.24",
"rss-parser": "3.12.0", "rss-parser": "3.12.0",

View file

@ -231,7 +231,7 @@ importers:
version: 1.2.0 version: 1.2.0
langchain: langchain:
specifier: 0.0.198 specifier: 0.0.198
version: 0.0.198(@aws-sdk/client-bedrock-runtime@3.454.0)(@aws-sdk/credential-provider-node@3.451.0)(@getzep/zep-js@0.9.0)(@google-ai/generativelanguage@0.2.1)(@huggingface/inference@2.6.4)(@pinecone-database/pinecone@1.1.2)(@qdrant/js-client-rest@1.7.0)(@supabase/supabase-js@2.38.5)(@xata.io/client@0.25.3)(axios@1.6.2)(cohere-ai@6.2.2)(d3-dsv@2.0.0)(epub2@3.0.1)(html-to-text@9.0.5)(lodash@4.17.21)(mammoth@1.6.0)(pdf-parse@1.1.1)(pg@8.11.3)(redis@4.6.11)(typeorm@0.3.17) version: 0.0.198(@aws-sdk/client-bedrock-runtime@3.454.0)(@aws-sdk/credential-provider-node@3.451.0)(@getzep/zep-js@0.9.0)(@google-ai/generativelanguage@0.2.1)(@huggingface/inference@2.6.4)(@pinecone-database/pinecone@1.1.2)(@qdrant/js-client-rest@1.7.0)(@supabase/supabase-js@2.38.5)(@xata.io/client@0.25.3)(axios@1.6.2)(cohere-ai@6.2.2)(d3-dsv@2.0.0)(epub2@3.0.1)(html-to-text@9.0.5)(lodash@4.17.21)(mammoth@1.6.0)(pdf-parse@1.1.1)(pg@8.11.3)(redis@4.6.12)(typeorm@0.3.17)
lodash: lodash:
specifier: 4.17.21 specifier: 4.17.21
version: 4.17.21 version: 4.17.21
@ -257,8 +257,8 @@ importers:
specifier: 8.11.3 specifier: 8.11.3
version: 8.11.3 version: 8.11.3
redis: redis:
specifier: 4.6.11 specifier: 4.6.12
version: 4.6.11 version: 4.6.12
sqlite3: sqlite3:
specifier: 5.1.6 specifier: 5.1.6
version: 5.1.6 version: 5.1.6
@ -267,7 +267,7 @@ importers:
version: 0.9.4 version: 0.9.4
typeorm: typeorm:
specifier: 0.3.17 specifier: 0.3.17
version: 0.3.17(mssql@9.1.1)(pg@8.11.3)(redis@4.6.11)(sqlite3@5.1.6) version: 0.3.17(mssql@9.1.1)(pg@8.11.3)(redis@4.6.12)(sqlite3@5.1.6)
zod: zod:
specifier: 3.22.4 specifier: 3.22.4
version: 3.22.4 version: 3.22.4
@ -1363,8 +1363,8 @@ importers:
specifier: 0.23.4 specifier: 0.23.4
version: 0.23.4(patch_hash=kzcwsjcayy5m6iezu7r4tdimjq) version: 0.23.4(patch_hash=kzcwsjcayy5m6iezu7r4tdimjq)
redis: redis:
specifier: 3.1.2 specifier: 4.6.12
version: 3.1.2 version: 4.6.12
rfc2047: rfc2047:
specifier: 4.0.1 specifier: 4.0.1
version: 4.0.1 version: 4.0.1
@ -1465,9 +1465,6 @@ importers:
'@types/promise-ftp': '@types/promise-ftp':
specifier: ^1.3.4 specifier: ^1.3.4
version: 1.3.4 version: 1.3.4
'@types/redis':
specifier: ^2.8.11
version: 2.8.32
'@types/request-promise-native': '@types/request-promise-native':
specifier: ~1.0.15 specifier: ~1.0.15
version: 1.0.18 version: 1.0.18
@ -7158,16 +7155,16 @@ packages:
'@babel/runtime': 7.22.6 '@babel/runtime': 7.22.6
dev: true dev: true
/@redis/bloom@1.2.0(@redis/client@1.5.12): /@redis/bloom@1.2.0(@redis/client@1.5.13):
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
peerDependencies: peerDependencies:
'@redis/client': ^1.0.0 '@redis/client': ^1.0.0
dependencies: dependencies:
'@redis/client': 1.5.12 '@redis/client': 1.5.13
dev: false dev: false
/@redis/client@1.5.12: /@redis/client@1.5.13:
resolution: {integrity: sha512-/ZjE18HRzMd80eXIIUIPcH81UoZpwulbo8FmbElrjPqH0QC0SeIKu1BOU49bO5trM5g895kAjhvalt5h77q+4A==} resolution: {integrity: sha512-epkUM9D0Sdmt93/8Ozk43PNjLi36RZzG+d/T1Gdu5AI8jvghonTeLYV69WVWdilvFo+PYxbP0TZ0saMvr6nscQ==}
engines: {node: '>=14'} engines: {node: '>=14'}
dependencies: dependencies:
cluster-key-slot: 1.1.2 cluster-key-slot: 1.1.2
@ -7175,36 +7172,36 @@ packages:
yallist: 4.0.0 yallist: 4.0.0
dev: false dev: false
/@redis/graph@1.1.1(@redis/client@1.5.12): /@redis/graph@1.1.1(@redis/client@1.5.13):
resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==}
peerDependencies: peerDependencies:
'@redis/client': ^1.0.0 '@redis/client': ^1.0.0
dependencies: dependencies:
'@redis/client': 1.5.12 '@redis/client': 1.5.13
dev: false dev: false
/@redis/json@1.0.6(@redis/client@1.5.12): /@redis/json@1.0.6(@redis/client@1.5.13):
resolution: {integrity: sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==} resolution: {integrity: sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==}
peerDependencies: peerDependencies:
'@redis/client': ^1.0.0 '@redis/client': ^1.0.0
dependencies: dependencies:
'@redis/client': 1.5.12 '@redis/client': 1.5.13
dev: false dev: false
/@redis/search@1.1.6(@redis/client@1.5.12): /@redis/search@1.1.6(@redis/client@1.5.13):
resolution: {integrity: sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==} resolution: {integrity: sha512-mZXCxbTYKBQ3M2lZnEddwEAks0Kc7nauire8q20oA0oA/LoA+E/b5Y5KZn232ztPb1FkIGqo12vh3Lf+Vw5iTw==}
peerDependencies: peerDependencies:
'@redis/client': ^1.0.0 '@redis/client': ^1.0.0
dependencies: dependencies:
'@redis/client': 1.5.12 '@redis/client': 1.5.13
dev: false dev: false
/@redis/time-series@1.0.5(@redis/client@1.5.12): /@redis/time-series@1.0.5(@redis/client@1.5.13):
resolution: {integrity: sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==} resolution: {integrity: sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==}
peerDependencies: peerDependencies:
'@redis/client': ^1.0.0 '@redis/client': ^1.0.0
dependencies: dependencies:
'@redis/client': 1.5.12 '@redis/client': 1.5.13
dev: false dev: false
/@rollup/plugin-alias@5.1.0(rollup@3.29.4): /@rollup/plugin-alias@5.1.0(rollup@3.29.4):
@ -10598,12 +10595,6 @@ packages:
csstype: 3.1.1 csstype: 3.1.1
dev: true dev: true
/@types/redis@2.8.32:
resolution: {integrity: sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==}
dependencies:
'@types/node': 18.16.16
dev: true
/@types/replacestream@4.0.1: /@types/replacestream@4.0.1:
resolution: {integrity: sha512-3ecTmnzB90sgarVpIszCF1cX2cnxwqDovWb31jGrKfxAL0Knui1H7Reaz/zlT9zaE3u0un7L5cNy9fQPy0d2sg==} resolution: {integrity: sha512-3ecTmnzB90sgarVpIszCF1cX2cnxwqDovWb31jGrKfxAL0Knui1H7Reaz/zlT9zaE3u0un7L5cNy9fQPy0d2sg==}
dev: true dev: true
@ -18941,7 +18932,7 @@ packages:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
dev: false dev: false
/langchain@0.0.198(@aws-sdk/client-bedrock-runtime@3.454.0)(@aws-sdk/credential-provider-node@3.451.0)(@getzep/zep-js@0.9.0)(@google-ai/generativelanguage@0.2.1)(@huggingface/inference@2.6.4)(@pinecone-database/pinecone@1.1.2)(@qdrant/js-client-rest@1.7.0)(@supabase/supabase-js@2.38.5)(@xata.io/client@0.25.3)(axios@1.6.2)(cohere-ai@6.2.2)(d3-dsv@2.0.0)(epub2@3.0.1)(html-to-text@9.0.5)(lodash@4.17.21)(mammoth@1.6.0)(pdf-parse@1.1.1)(pg@8.11.3)(redis@4.6.11)(typeorm@0.3.17): /langchain@0.0.198(@aws-sdk/client-bedrock-runtime@3.454.0)(@aws-sdk/credential-provider-node@3.451.0)(@getzep/zep-js@0.9.0)(@google-ai/generativelanguage@0.2.1)(@huggingface/inference@2.6.4)(@pinecone-database/pinecone@1.1.2)(@qdrant/js-client-rest@1.7.0)(@supabase/supabase-js@2.38.5)(@xata.io/client@0.25.3)(axios@1.6.2)(cohere-ai@6.2.2)(d3-dsv@2.0.0)(epub2@3.0.1)(html-to-text@9.0.5)(lodash@4.17.21)(mammoth@1.6.0)(pdf-parse@1.1.1)(pg@8.11.3)(redis@4.6.12)(typeorm@0.3.17):
resolution: {integrity: sha512-YC0O1g8r61InCWyF5NmiQjdghdq6LKcgMrDZtqLbgDxAe4RoSldonm+5oNXS3yjCISG0j3s5Cty+yB7klqvUpg==} resolution: {integrity: sha512-YC0O1g8r61InCWyF5NmiQjdghdq6LKcgMrDZtqLbgDxAe4RoSldonm+5oNXS3yjCISG0j3s5Cty+yB7klqvUpg==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
@ -19279,8 +19270,8 @@ packages:
p-retry: 4.6.2 p-retry: 4.6.2
pdf-parse: 1.1.1 pdf-parse: 1.1.1
pg: 8.11.3 pg: 8.11.3
redis: 4.6.11 redis: 4.6.12
typeorm: 0.3.17(mssql@9.1.1)(pg@8.11.3)(redis@4.6.11)(sqlite3@5.1.6) typeorm: 0.3.17(mssql@9.1.1)(pg@8.11.3)(redis@4.6.12)(sqlite3@5.1.6)
uuid: 9.0.0 uuid: 9.0.0
yaml: 2.3.4 yaml: 2.3.4
zod: 3.22.4 zod: 3.22.4
@ -23233,25 +23224,15 @@ packages:
dependencies: dependencies:
redis-errors: 1.2.0 redis-errors: 1.2.0
/redis@3.1.2: /redis@4.6.12:
resolution: {integrity: sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==} resolution: {integrity: sha512-41Xuuko6P4uH4VPe5nE3BqXHB7a9lkFL0J29AlxKaIfD6eWO8VO/5PDF9ad2oS+mswMsfFxaM5DlE3tnXT+P8Q==}
engines: {node: '>=10'}
dependencies: dependencies:
denque: 1.5.1 '@redis/bloom': 1.2.0(@redis/client@1.5.13)
redis-commands: 1.7.0 '@redis/client': 1.5.13
redis-errors: 1.2.0 '@redis/graph': 1.1.1(@redis/client@1.5.13)
redis-parser: 3.0.0 '@redis/json': 1.0.6(@redis/client@1.5.13)
dev: false '@redis/search': 1.1.6(@redis/client@1.5.13)
'@redis/time-series': 1.0.5(@redis/client@1.5.13)
/redis@4.6.11:
resolution: {integrity: sha512-kg1Lt4NZLYkAjPOj/WcyIGWfZfnyfKo1Wg9YKVSlzhFwxpFIl3LYI8BWy1Ab963LLDsTz2+OwdsesHKljB3WMQ==}
dependencies:
'@redis/bloom': 1.2.0(@redis/client@1.5.12)
'@redis/client': 1.5.12
'@redis/graph': 1.1.1(@redis/client@1.5.12)
'@redis/json': 1.0.6(@redis/client@1.5.12)
'@redis/search': 1.1.6(@redis/client@1.5.12)
'@redis/time-series': 1.0.5(@redis/client@1.5.12)
dev: false dev: false
/reflect-metadata@0.1.13: /reflect-metadata@0.1.13:
@ -25740,7 +25721,7 @@ packages:
- supports-color - supports-color
dev: false dev: false
/typeorm@0.3.17(mssql@9.1.1)(pg@8.11.3)(redis@4.6.11)(sqlite3@5.1.6): /typeorm@0.3.17(mssql@9.1.1)(pg@8.11.3)(redis@4.6.12)(sqlite3@5.1.6):
resolution: {integrity: sha512-UDjUEwIQalO9tWw9O2A4GU+sT3oyoUXheHJy4ft+RFdnRdQctdQ34L9SqE2p7LdwzafHx1maxT+bqXON+Qnmig==} resolution: {integrity: sha512-UDjUEwIQalO9tWw9O2A4GU+sT3oyoUXheHJy4ft+RFdnRdQctdQ34L9SqE2p7LdwzafHx1maxT+bqXON+Qnmig==}
engines: {node: '>= 12.9.0'} engines: {node: '>= 12.9.0'}
hasBin: true hasBin: true
@ -25810,7 +25791,7 @@ packages:
mkdirp: 2.1.3 mkdirp: 2.1.3
mssql: 9.1.1 mssql: 9.1.1
pg: 8.11.3 pg: 8.11.3
redis: 4.6.11 redis: 4.6.12
reflect-metadata: 0.1.13 reflect-metadata: 0.1.13
sha.js: 2.4.11 sha.js: 2.4.11
sqlite3: 5.1.6 sqlite3: 5.1.6