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
7 changed files with 517 additions and 426 deletions

View File

@@ -1,19 +1,19 @@
import util from 'util';
import type {
IExecuteFunctions,
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
INodeCredentialTestResult,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import set from 'lodash/set';
import redis from 'redis';
import {
setupRedisClient,
redisConnectionTest,
convertInfoToObject,
getValue,
setValue,
} from './utils';
export class Redis implements INodeType {
description: INodeTypeDescription = {
@@ -100,6 +100,23 @@ export class Redis implements INodeType {
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
// ----------------------------------
@@ -117,19 +134,6 @@ export class Redis implements INodeType {
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: ['delete'],
},
},
default: '',
required: true,
description: 'Name of the key to delete from Redis',
},
{
displayName: 'Key',
name: 'key',
@@ -498,345 +502,135 @@ export class Redis implements INodeType {
};
methods = {
credentialTest: {
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!',
};
},
},
credentialTest: { redisConnectionTest },
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
// 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);
}
}
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('redis');
// Converts the Redis Info String into an object
function convertInfoToObject(stringData: string): IDataObject {
const returnData: IDataObject = {};
const client = setupRedisClient(credentials);
await client.connect();
await client.ping();
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();
const operation = this.getNodeParameter('operation', 0);
const returnItems: INodeExecutionData[] = [];
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);
}
}
try {
if (operation === 'info') {
const result = await client.info();
returnItems.push({ json: convertInfoToObject(result) });
} else if (
['delete', 'get', 'keys', 'set', 'incr', 'publish', 'push', 'pop'].includes(operation)
) {
const items = this.getInputData();
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 (type === undefined || type === 'automatic') {
// Request the type first
const clientType = util.promisify(client.type).bind(client);
type = await clientType(keyName);
}
if (operation === 'delete') {
const keyDelete = this.getNodeParameter('key', itemIndex) as string;
if (type === 'string') {
const clientGet = util.promisify(client.get).bind(client);
return clientGet(keyName);
} else if (type === 'hash') {
const clientHGetAll = util.promisify(client.hgetall).bind(client);
return clientHGetAll(keyName);
} 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);
}
}
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 setValue = async (
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!',
);
}
}
const value = (await getValue(client, keyGet, keyType)) ?? null;
if (type === 'string') {
const clientSet = util.promisify(client.set).bind(client);
await clientSet(keyName, value.toString());
} else if (type === 'hash') {
const clientHset = util.promisify(client.hset).bind(client);
if (valueIsJSON) {
let values: unknown;
if (typeof value === 'string') {
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 === '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 {
values = JSON.parse(value);
outputValue = value && JSON.parse(value);
} catch {
// This is how we originally worked and prevents a breaking change
values = value;
outputValue = value;
}
} else {
values = value;
}
for (const key of Object.keys(values as object)) {
// @ts-ignore
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);
}
const options = this.getNodeParameter('options', itemIndex, {});
if (options.dotNotation === false) {
item.json[propertyName] = outputValue;
} else {
set(item.json, propertyName, outputValue);
}
client.quit();
resolve([returnItems]);
returnItems.push(item);
}
} catch (error) {
reject(error);
}
});
});
}
} catch (error) {
throw error;
} finally {
await client.quit();
}
return [returnItems];
}
}