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
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:
@@ -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 |
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user