feat(Redis Vector Store Node): Redis vector store node implementation (#19428)

Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
Tihomir Krasimirov Mateev 2025-10-13 13:42:52 +03:00 committed by GitHub
parent 353b992668
commit f178a59702
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1031 additions and 123 deletions

View File

@ -0,0 +1,524 @@
import { mock } from 'jest-mock-extended';
import { NodeOperationError, type ILoadOptionsFunctions } from 'n8n-workflow';
// Mock external modules that are not needed for these unit tests
jest.mock('@langchain/redis', () => {
const state: any = { ctorArgs: undefined };
class RedisVectorStore {
static fromDocuments = jest.fn();
constructor(...args: any[]) {
state.ctorArgs = args;
}
}
return { RedisVectorStore, __state: state };
});
jest.mock('@utils/sharedFields', () => ({ metadataFilterField: {} }), { virtual: true });
jest.mock(
'@utils/helpers',
() => ({ getMetadataFiltersValues: jest.fn(), logAiEvent: jest.fn() }),
{ virtual: true },
);
jest.mock('@utils/N8nBinaryLoader', () => ({ N8nBinaryLoader: class {} }), { virtual: true });
jest.mock('@utils/N8nJsonLoader', () => ({ N8nJsonLoader: class {} }), { virtual: true });
jest.mock('@utils/logWrapper', () => ({ logWrapper: (fn: any) => fn }), { virtual: true });
// Mock the vector store node factory to avoid deep imports but preserve passed methods
jest.mock('../shared/createVectorStoreNode/createVectorStoreNode', () => ({
createVectorStoreNode: (config: any) =>
class BaseNode {
async getVectorStoreClient(...args: any[]) {
return config.getVectorStoreClient.apply(config, args);
}
async populateVectorStore(...args: any[]) {
return config.populateVectorStore.apply(config, args);
}
},
}));
jest.mock('redis', () => ({ createClient: jest.fn() }));
import { createClient } from 'redis';
import * as RedisNode from './VectorStoreRedis.node';
const MockCreateClient = createClient as jest.MockedFunction<typeof createClient>;
describe('VectorStoreRedis.node', () => {
const helpers = mock<ILoadOptionsFunctions['helpers']>();
const loadOptionsFunctions = mock<ILoadOptionsFunctions>({ helpers });
loadOptionsFunctions.logger = {
info: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
verbose: jest.fn(),
} as any;
const baseCredentials = {
host: 'localhost',
port: 6379,
ssl: false,
user: 'default',
password: 'pass',
database: 0,
} as any;
beforeEach(() => {
jest.resetAllMocks();
// Reset cached client
RedisNode.redisConfig.client = null as any;
RedisNode.redisConfig.connectionString = '';
});
describe('getRedisClient', () => {
it('creates and reuses client for same configuration', async () => {
const mockClient = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn().mockResolvedValue(undefined),
quit: jest.fn().mockResolvedValue(undefined),
} as any;
MockCreateClient.mockReturnValue(mockClient);
const context = {
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
} as any;
const client1 = await RedisNode.getRedisClient(context);
const client2 = await RedisNode.getRedisClient(context);
expect(MockCreateClient).toHaveBeenCalledTimes(1);
expect(mockClient.connect).toHaveBeenCalledTimes(1);
expect(mockClient.disconnect).not.toHaveBeenCalled();
expect(client1).toBe(mockClient);
expect(client2).toBe(mockClient);
});
it('disconnects previous client and creates a new one when configuration changes', async () => {
const mockClient1 = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn().mockResolvedValue(undefined),
quit: jest.fn().mockResolvedValue(undefined),
} as any;
const mockClient2 = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn().mockResolvedValue(undefined),
quit: jest.fn().mockResolvedValue(undefined),
} as any;
MockCreateClient.mockImplementationOnce(() => mockClient1).mockImplementationOnce(
() => mockClient2,
);
const context = {
getCredentials: jest
.fn()
.mockResolvedValueOnce(baseCredentials)
.mockResolvedValueOnce({ ...baseCredentials, port: 6380 }),
} as any;
const client1 = await RedisNode.getRedisClient(context);
const client2 = await RedisNode.getRedisClient(context);
expect(MockCreateClient).toHaveBeenCalledTimes(2);
expect(mockClient1.disconnect).toHaveBeenCalledTimes(1);
expect(mockClient2.connect).toHaveBeenCalledTimes(1);
expect(client1).toBe(mockClient1);
expect(client2).toBe(mockClient2);
});
});
describe('listIndexes', () => {
it('returns mapped indexes when FT._LIST succeeds', async () => {
const mockClient = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
quit: jest.fn(),
ft: { _list: jest.fn().mockResolvedValue(['Idx1', 'Idx2']) },
} as any;
MockCreateClient.mockReturnValue(mockClient);
(loadOptionsFunctions as any).getCredentials = jest.fn().mockResolvedValue(baseCredentials);
const results = await (RedisNode.listIndexes as any).call(loadOptionsFunctions as any);
expect(mockClient.ft._list).toHaveBeenCalled();
expect(results).toEqual({
results: [
{ name: 'Idx1', value: 'Idx1' },
{ name: 'Idx2', value: 'Idx2' },
],
});
});
it('returns empty results when FT._LIST fails', async () => {
const mockClient = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
quit: jest.fn(),
ft: { _list: jest.fn().mockRejectedValue(new Error('no module')) },
} as any;
MockCreateClient.mockReturnValue(mockClient);
const failureCredentials = { ...baseCredentials, port: 6380 };
(loadOptionsFunctions as any).getCredentials = jest
.fn()
.mockResolvedValue(failureCredentials);
const results = await (RedisNode.listIndexes as any).call(loadOptionsFunctions as any);
expect(results).toEqual({ results: [] });
});
it('returns empty results when FT._LIST returns unexpected data type', async () => {
const mockClient = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
quit: jest.fn(),
ft: { _list: jest.fn().mockResolvedValue({ unexpected: 'object' }) },
} as any;
MockCreateClient.mockReturnValue(mockClient);
(loadOptionsFunctions as any).getCredentials = jest.fn().mockResolvedValue(baseCredentials);
const results = await (RedisNode.listIndexes as any).call(loadOptionsFunctions as any);
expect(results).toEqual({ results: [] });
expect(loadOptionsFunctions.logger.warn).toHaveBeenCalledWith(
'FT._LIST returned unexpected data type',
);
});
});
describe('getVectorStoreClient', () => {
it('constructs ExtendedRedisVectorSearch with correct options and passes filter tokens', async () => {
const mockClient = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
quit: jest.fn(),
sendCommand: jest
.fn()
.mockImplementation(async ([cmd]) =>
cmd === 'FT.INFO' ? await Promise.resolve(undefined) : await Promise.resolve([]),
),
} as any;
// Adapt to new client.ft.info usage
mockClient.ft = { ...(mockClient.ft || {}), info: jest.fn().mockResolvedValue(undefined) };
(MockCreateClient as any).mockReturnValue(mockClient);
// Provide a base class method that ExtendedRedisVectorSearch will call via super
const RedisVectorStoreMod: any = jest.requireMock('@langchain/redis');
RedisVectorStoreMod.RedisVectorStore.prototype.similaritySearchVectorWithScore = jest
.fn()
.mockResolvedValue('ok');
const context: any = {
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
getNodeParameter: (name: string) => {
const map: Record<string, any> = {
redisIndex: 'myIndex',
'options.keyPrefix': 'doc',
'options.metadataKey': 'm',
'options.contentKey': 'c',
'options.vectorKey': 'v',
'options.metadataFilter': 'a,b',
};
return map[name];
},
getNode: () => ({ name: 'VectorStoreRedis' }),
logger: loadOptionsFunctions.logger,
} as any;
const embeddings: any = {};
const instance = new RedisNode.VectorStoreRedis();
const client = await (instance as any).getVectorStoreClient(
context,
undefined,
embeddings,
0,
);
// Ensure FT.INFO is called to validate index
expect(mockClient.ft.info).toHaveBeenCalledWith('myIndex');
// The base class constructor should have been called with embeddings and options
const state = RedisVectorStoreMod.__state;
expect(state.ctorArgs[0]).toBe(embeddings);
expect(state.ctorArgs[1]).toMatchObject({
redisClient: mockClient,
indexName: 'myIndex',
keyPrefix: 'doc',
metadataKey: 'm',
contentKey: 'c',
vectorKey: 'v',
});
// Call the overridden method and ensure behavior is as expected
const res = await client.similaritySearchVectorWithScore([1, 2], 3);
expect(res).toBe('ok');
// Validate filter tokens got captured on the instance
expect(client.defaultFilter).toEqual(['a', 'b']);
});
it('trims and removes empty metadata filter tokens', async () => {
const mockClient = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
quit: jest.fn(),
ft: { info: jest.fn().mockResolvedValue(undefined) },
} as any;
(MockCreateClient as any).mockReturnValue(mockClient);
const RedisVectorStoreMod: any = jest.requireMock('@langchain/redis');
RedisVectorStoreMod.RedisVectorStore.prototype.similaritySearchVectorWithScore = jest
.fn()
.mockResolvedValue('ok');
const context: any = {
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
getNodeParameter: (name: string) => {
const map: Record<string, any> = {
redisIndex: 'idx2',
'options.keyPrefix': '',
'options.metadataKey': '',
'options.contentKey': '',
'options.vectorKey': '',
'options.metadataFilter': 'tag1, tag2 , ,tag3',
};
return map[name];
},
getNode: () => ({ name: 'VectorStoreRedis' }),
logger: loadOptionsFunctions.logger,
} as any;
const node = new RedisNode.VectorStoreRedis();
const client = await (node as any).getVectorStoreClient(context, undefined, {}, 0);
// Ensure trimming/removal works
expect(client.defaultFilter).toEqual(['tag1', 'tag2', 'tag3']);
});
it('omits optional keys when empty/whitespace and handles empty filter as null', async () => {
const mockClient = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
quit: jest.fn(),
ft: { info: jest.fn().mockResolvedValue(undefined) },
} as any;
(MockCreateClient as any).mockReturnValue(mockClient);
const RedisVectorStoreMod: any = jest.requireMock('@langchain/redis');
RedisVectorStoreMod.RedisVectorStore.prototype.similaritySearchVectorWithScore = jest
.fn()
.mockResolvedValue('ok');
const context: any = {
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
getNodeParameter: (name: string) => {
const map: Record<string, any> = {
redisIndex: 'myIndex',
'options.keyPrefix': ' ',
'options.metadataKey': ' ',
'options.contentKey': '',
'options.vectorKey': ' \t',
'options.metadataFilter': '',
};
return map[name];
},
getNode: () => ({ name: 'VectorStoreRedis' }),
logger: loadOptionsFunctions.logger,
} as any;
const embeddings: any = {};
const node = new RedisNode.VectorStoreRedis();
const instance = await (node as any).getVectorStoreClient(context, undefined, embeddings, 0);
// Ensure FT.INFO is called to validate index
expect(mockClient.ft.info).toHaveBeenCalledWith('myIndex');
const opts = RedisVectorStoreMod.__state.ctorArgs[1];
expect(opts).toMatchObject({ redisClient: mockClient, indexName: 'myIndex' });
expect(opts).not.toHaveProperty('keyPrefix');
expect(opts).not.toHaveProperty('metadataKey');
expect(opts).not.toHaveProperty('contentKey');
expect(opts).not.toHaveProperty('vectorKey');
const res = await instance.similaritySearchVectorWithScore([0], 1);
expect(res).toBeDefined();
expect(instance.defaultFilter).toBeUndefined();
});
it('returns undefined filter when filter string contains only whitespace and commas', async () => {
const mockClient = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
quit: jest.fn(),
ft: { info: jest.fn().mockResolvedValue(undefined) },
} as any;
(MockCreateClient as any).mockReturnValue(mockClient);
const RedisVectorStoreMod: any = jest.requireMock('@langchain/redis');
RedisVectorStoreMod.RedisVectorStore.prototype.similaritySearchVectorWithScore = jest
.fn()
.mockResolvedValue('ok');
const context: any = {
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
getNodeParameter: (name: string) => {
const map: Record<string, any> = {
redisIndex: 'myIndex',
'options.keyPrefix': '',
'options.metadataKey': '',
'options.contentKey': '',
'options.vectorKey': '',
'options.metadataFilter': ' , , , ',
};
return map[name];
},
getNode: () => ({ name: 'VectorStoreRedis' }),
logger: loadOptionsFunctions.logger,
} as any;
const node = new RedisNode.VectorStoreRedis();
const instance = await (node as any).getVectorStoreClient(context, undefined, {}, 0);
// Filter with only whitespace and commas should result in undefined
expect(instance.defaultFilter).toBeUndefined();
});
it('throws NodeOperationError when index is missing', async () => {
const mockClient = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
quit: jest.fn(),
ft: { info: jest.fn().mockRejectedValue(new Error('no such index')) },
} as any;
(MockCreateClient as any).mockReturnValue(mockClient);
const context: any = {
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
getNodeParameter: (name: string) => (name === 'redisIndex' ? 'idx' : ''),
getNode: () => ({ name: 'VectorStoreRedis' }),
};
const node = new RedisNode.VectorStoreRedis();
await expect((node as any).getVectorStoreClient(context, undefined, {}, 0)).rejects.toEqual(
new NodeOperationError(context.getNode(), 'Index idx not found', {
itemIndex: 0,
description: 'Please check that the index exists in your Redis instance',
}),
);
});
});
describe('populateVectorStore', () => {
it('drops index and deletes the documents when overwrite is true; passes TTL and batch size', async () => {
const mockClient = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
quit: jest.fn(),
ft: { dropIndex: jest.fn().mockResolvedValue(undefined) },
} as any;
(MockCreateClient as any).mockReturnValue(mockClient);
const RedisVectorStoreMod: any = jest.requireMock('@langchain/redis');
RedisVectorStoreMod.RedisVectorStore.fromDocuments = jest.fn().mockResolvedValue(undefined);
const context: any = {
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
getNodeParameter: (name: string) => {
const map: Record<string, any> = {
redisIndex: 'myIndex',
'options.overwriteDocuments': true,
'options.keyPrefix': 'doc',
'options.metadataKey': 'm',
'options.contentKey': 'c',
'options.vectorKey': 'v',
'options.ttl': 60,
embeddingBatchSize: 123,
};
return map[name];
},
getNode: () => ({ name: 'VectorStoreRedis' }),
logger: loadOptionsFunctions.logger,
} as any;
const node = new RedisNode.VectorStoreRedis();
await (node as any).populateVectorStore(
context,
{},
[{ pageContent: 'hello', metadata: {} }],
0,
);
expect(mockClient.ft.dropIndex).toHaveBeenCalledWith('myIndex', { DD: true });
expect(RedisVectorStoreMod.RedisVectorStore.fromDocuments).toHaveBeenCalledWith(
[{ pageContent: 'hello', metadata: {} }],
{},
{
redisClient: mockClient,
indexName: 'myIndex',
keyPrefix: 'doc',
metadataKey: 'm',
contentKey: 'c',
vectorKey: 'v',
ttl: 60,
},
);
});
it('logs and throws NodeOperationError on failure', async () => {
const mockClient = {
on: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
disconnect: jest.fn(),
quit: jest.fn(),
sendCommand: jest.fn().mockResolvedValue(undefined),
} as any;
(MockCreateClient as any).mockReturnValue(mockClient);
const RedisVectorStoreMod: any = jest.requireMock('@langchain/redis');
RedisVectorStoreMod.RedisVectorStore.fromDocuments = jest
.fn()
.mockRejectedValue(new Error('fail'));
const context: any = {
getCredentials: jest.fn().mockResolvedValue(baseCredentials),
getNodeParameter: (name: string) => (name === 'redisIndex' ? 'idx' : ''),
getNode: () => ({ name: 'VectorStoreRedis' }),
logger: loadOptionsFunctions.logger,
} as any;
const node = new RedisNode.VectorStoreRedis();
await expect((node as any).populateVectorStore(context, {}, [], 0)).rejects.toEqual(
new NodeOperationError(context.getNode(), 'Error: fail', {
itemIndex: 0,
description: 'Please check your index/schema and parameters',
}),
);
expect(loadOptionsFunctions.logger.info).toHaveBeenCalledWith(
'Error while populating the store: fail',
);
});
});
});

View File

@ -0,0 +1,412 @@
import type { EmbeddingsInterface } from '@langchain/core/embeddings';
import { RedisVectorStore } from '@langchain/redis';
import type { RedisVectorStoreConfig } from '@langchain/redis/dist/vectorstores';
import {
type IExecuteFunctions,
type ILoadOptionsFunctions,
type INodeProperties,
type ISupplyDataFunctions,
NodeOperationError,
} from 'n8n-workflow';
import type { RedisClientOptions } from 'redis';
import { createClient } from 'redis';
import { createVectorStoreNode } from '../shared/createVectorStoreNode/createVectorStoreNode';
/**
* Constants for the name of the credentials and Node parameters.
*/
const REDIS_CREDENTIALS = 'redis';
const REDIS_INDEX_NAME = 'redisIndex';
const REDIS_KEY_PREFIX = 'keyPrefix';
const REDIS_OVERWRITE_DOCUMENTS = 'overwriteDocuments';
const REDIS_METADATA_KEY = 'metadataKey';
const REDIS_METADATA_FILTER = 'metadataFilter';
const REDIS_CONTENT_KEY = 'contentKey';
const REDIS_EMBEDDING_KEY = 'vectorKey';
const REDIS_TTL = 'ttl';
const redisIndexRLC: INodeProperties = {
displayName: 'Redis Index',
name: REDIS_INDEX_NAME,
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'redisIndexSearch',
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
},
],
};
const metadataFilterField: INodeProperties = {
displayName: 'Metadata Filter',
name: REDIS_METADATA_FILTER,
type: 'string',
description:
'The comma-separated list of words by which to apply additional full-text metadata filtering',
placeholder: 'Item1,Item2,Item3',
default: '',
};
const metadataKeyField: INodeProperties = {
displayName: 'Metadata Key',
name: REDIS_METADATA_KEY,
type: 'string',
description: 'The hash key to be used to store the metadata of the document',
placeholder: 'metadata',
default: '',
};
const contentKeyField: INodeProperties = {
displayName: 'Content Key',
name: REDIS_CONTENT_KEY,
type: 'string',
description: 'The hash key to be used to store the content of the document',
placeholder: 'content',
default: '',
};
const embeddingKeyField: INodeProperties = {
displayName: 'Embedding Key',
name: REDIS_EMBEDDING_KEY,
type: 'string',
description: 'The hash key to be used to store the embedding of the document',
placeholder: 'content_vector',
default: '',
};
const overwriteDocuments: INodeProperties = {
displayName: 'Overwrite Documents',
name: REDIS_OVERWRITE_DOCUMENTS,
type: 'boolean',
description: 'Whether existing documents and the index should be overwritten',
default: false,
};
const keyPrefixField: INodeProperties = {
displayName: 'Key Prefix',
name: REDIS_KEY_PREFIX,
type: 'string',
description: 'Prefix for Redis keys storing the documents',
placeholder: 'doc',
default: '',
};
const ttlField: INodeProperties = {
displayName: 'Time-To-Live',
name: REDIS_TTL,
description: 'Time-to-live for the documents in seconds',
placeholder: '0',
type: 'number',
default: '',
};
const sharedFields: INodeProperties[] = [redisIndexRLC];
const insertFields: INodeProperties[] = [
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
keyPrefixField,
overwriteDocuments,
metadataKeyField,
contentKeyField,
embeddingKeyField,
ttlField,
],
},
];
const retrieveFields: INodeProperties[] = [
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
metadataFilterField,
keyPrefixField,
metadataKeyField,
contentKeyField,
embeddingKeyField,
],
},
];
export const redisConfig = {
client: null as ReturnType<typeof createClient> | null,
connectionString: '',
};
/**
* Type used for cleaner, more intentional typing.
*/
type IFunctionsContext = IExecuteFunctions | ISupplyDataFunctions | ILoadOptionsFunctions;
/**
* Get the Redis client.
* @param context - The context.
* @returns the Redis client for the node.
*/
export async function getRedisClient(context: IFunctionsContext) {
const credentials = await context.getCredentials(REDIS_CREDENTIALS);
// Create client configuration object
const config: RedisClientOptions = {
socket: {
host: (credentials.host as string) || 'localhost',
port: (credentials.port as number) || 6379,
tls: credentials.ssl === true,
},
username: credentials.user as string,
password: credentials.password as string,
database: credentials.database as number,
clientInfoTag: 'n8n',
};
if (!redisConfig.client || redisConfig.connectionString !== JSON.stringify(config)) {
if (redisConfig.client) {
await redisConfig.client.disconnect();
}
redisConfig.connectionString = JSON.stringify(config);
redisConfig.client = createClient(config);
if (redisConfig.client) {
redisConfig.client.on('error', (error: Error) => {
context.logger.error(`[Redis client] ${error.message}`, { error });
});
await redisConfig.client.connect();
}
}
return redisConfig.client;
}
/**
* Type guard to check if a value is a string array.
* @param value - The value to check.
* @returns True if the value is a string array, false otherwise.
*/
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string');
}
/**
* Get the complete list of indexes from Redis.
* @returns The list of indexes.
*/
export async function listIndexes(this: ILoadOptionsFunctions) {
const client = await getRedisClient(this);
if (client === null) {
return { results: [] };
}
try {
// Get all indexes using FT._LIST command
const indexes = await client.ft._list();
// Validate that indexes is actually a string array
if (!isStringArray(indexes)) {
this.logger.warn('FT._LIST returned unexpected data type');
return { results: [] };
}
const results = indexes.map((index) => ({
name: index,
value: index,
}));
return { results };
} catch (error) {
this.logger.info('Failed to get Redis indexes: ' + error.message);
return { results: [] };
}
}
/**
* Get a parameter from the context.
* @param key - The key of the parameter.
* @param context - The context.
* @param itemIndex - The index.
* @returns The value.
*/
export function getParameter(key: string, context: IFunctionsContext, itemIndex: number): string {
return context.getNodeParameter(key, itemIndex, '', {
extractValue: true,
}) as string;
}
/**
* Get a parameter from the context as a number.
* @param key - The key of the parameter.
* @param context - The context.
* @param itemIndex - The index.
* @returns The value.
*/
export function getParameterAsNumber(
key: string,
context: IFunctionsContext,
itemIndex: number,
): number {
return context.getNodeParameter(key, itemIndex, '', {
extractValue: true,
}) as number;
}
/**
* Extended RedisVectorStore class to handle custom filtering.
*
* This wrapper is necessary because when used as a retriever, the similaritySearchVectorWithScore should
* use a processed filter
*/
class ExtendedRedisVectorSearch extends RedisVectorStore {
defaultFilter?: string[];
constructor(embeddings: EmbeddingsInterface, options: RedisVectorStoreConfig, filter?: string[]) {
super(embeddings, options);
this.defaultFilter = filter;
}
async similaritySearchVectorWithScore(query: number[], k: number) {
return await super.similaritySearchVectorWithScore(query, k, this.defaultFilter);
}
}
const getIndexName = getParameter.bind(null, REDIS_INDEX_NAME);
const getKeyPrefix = getParameter.bind(null, `options.${REDIS_KEY_PREFIX}`);
const getOverwrite = getParameter.bind(null, `options.${REDIS_OVERWRITE_DOCUMENTS}`);
const getContentKey = getParameter.bind(null, `options.${REDIS_CONTENT_KEY}`);
const getMetadataFilter = getParameter.bind(null, `options.${REDIS_METADATA_FILTER}`);
const getMetadataKey = getParameter.bind(null, `options.${REDIS_METADATA_KEY}`);
const getEmbeddingKey = getParameter.bind(null, `options.${REDIS_EMBEDDING_KEY}`);
const getTtl = getParameterAsNumber.bind(null, `options.${REDIS_TTL}`);
export class VectorStoreRedis extends createVectorStoreNode({
meta: {
displayName: 'Redis Vector Store',
name: 'vectorStoreRedis',
description: 'Work with your data in a Redis vector index',
icon: { light: 'file:redis.svg', dark: 'file:redis.dark.svg' },
docsUrl:
'https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.vectorstoreredis/',
credentials: [
{
name: REDIS_CREDENTIALS,
required: true,
},
],
operationModes: ['load', 'insert', 'retrieve', 'update', 'retrieve-as-tool'],
},
methods: { listSearch: { redisIndexSearch: listIndexes } },
retrieveFields,
loadFields: retrieveFields,
insertFields,
sharedFields,
async getVectorStoreClient(context, _filter, embeddings, itemIndex) {
const client = await getRedisClient(context);
const indexField = getIndexName(context, itemIndex).trim();
const keyPrefixField = getKeyPrefix(context, itemIndex).trim();
const metadataField = getMetadataKey(context, itemIndex).trim();
const contentField = getContentKey(context, itemIndex).trim();
const embeddingField = getEmbeddingKey(context, itemIndex).trim();
const filter = getMetadataFilter(context, itemIndex).trim();
if (client === null) {
throw new NodeOperationError(context.getNode(), 'Redis client not initialized', {
itemIndex,
description: 'Please check your Redis connection details',
});
}
// Check if index exists by trying to get info about it
try {
await client.ft.info(indexField);
} catch (error) {
throw new NodeOperationError(context.getNode(), `Index ${indexField} not found`, {
itemIndex,
description: 'Please check that the index exists in your Redis instance',
});
}
// Process filter: split by comma, trim, and remove empty strings
// If no valid filter terms exist, pass undefined instead of empty array
const filterTerms = filter
? filter
.split(',')
.map((s) => s.trim())
.filter((s) => s)
: [];
return new ExtendedRedisVectorSearch(
embeddings,
{
redisClient: client,
indexName: indexField,
...(keyPrefixField ? { keyPrefix: keyPrefixField } : {}),
...(metadataField ? { metadataKey: metadataField } : {}),
...(contentField ? { contentKey: contentField } : {}),
...(embeddingField ? { vectorKey: embeddingField } : {}),
},
filterTerms.length > 0 ? filterTerms : undefined,
);
},
async populateVectorStore(context, embeddings, documents, itemIndex) {
const client = await getRedisClient(context);
if (client === null) {
throw new NodeOperationError(context.getNode(), 'Redis client not initialized', {
itemIndex,
description: 'Please check your Redis connection details',
});
}
try {
const indexField = getIndexName(context, itemIndex).trim();
const overwrite = getOverwrite(context, itemIndex);
const keyPrefixField = getKeyPrefix(context, itemIndex).trim();
const metadataField = getMetadataKey(context, itemIndex).trim();
const contentField = getContentKey(context, itemIndex).trim();
const embeddingField = getEmbeddingKey(context, itemIndex).trim();
const ttl = getTtl(context, itemIndex);
if (overwrite) {
await client.ft.dropIndex(indexField, { DD: true });
}
await ExtendedRedisVectorSearch.fromDocuments(documents, embeddings, {
redisClient: client,
indexName: indexField,
...(keyPrefixField ? { keyPrefix: keyPrefixField } : {}),
...(metadataField ? { metadataKey: metadataField } : {}),
...(contentField ? { contentKey: contentField } : {}),
...(embeddingField ? { vectorKey: embeddingField } : {}),
...(ttl ? { ttl } : {}),
});
} catch (error) {
context.logger.info(`Error while populating the store: ${error.message}`);
throw new NodeOperationError(context.getNode(), `Error: ${error.message}`, {
itemIndex,
description: 'Please check your index/schema and parameters',
});
}
},
}) {}

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
]>
<svg version="1.1" id="Layer_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 156.0529938 144"
style="enable-background:new 0 0 156.0529938 144;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<metadata>
<sfw xmlns="&ns_sfw;">
<slices></slices>
<sliceSourceBounds bottomLeftOrigin="true" height="143.9999773" width="156.053" x="0" y="-143.9999773"></sliceSourceBounds>
</sfw>
</metadata>
<path class="st0" d="M147.6701355,79.4482651c-10.7946014,13.6011963-22.4527664,29.1454239-45.769104,29.1454239
c-20.826828,0-28.5858688-18.3700104-29.1312943-33.2931747c4.5630341,9.6490402,13.4846039,17.4628448,27.4041595,17.1012726
c26.7706146-0.8635712,45.1214371-25.0434799,45.1214371-47.0644608C145.2953339,18.998497,125.6491547,0,91.5382156,0
C67.1424179,0,36.9175339,9.2833567,17.0554695,23.9640141c-0.2158928,15.1124401,8.2038975,34.7586136,11.2263851,32.5996933
c17.2190762-12.3804779,30.8731899-20.3501434,44.1166077-24.3462524C52.7944527,54.0785789,5.7588773,104.8390808,0,113.7750931
c0.6476761,8.2038956,10.7946014,30.2248917,15.7601175,30.2248917c1.5112448,0,2.8065958-0.8635712,4.3178396-2.3748169
c14.1801414-15.9326324,25.7396431-30.2172928,36.0214996-43.985733
c1.443779,20.1794739,11.3667984,44.8493042,39.1089249,44.8493042c24.8275833,0,49.4392776-17.9190445,60.665657-58.2908478
C157.1693878,79.2323685,151.1244202,75.3463135,147.6701355,79.4482651z M119.3882904,46.848568
c0,12.7376289-12.5217438,18.9985008-23.9640198,18.9985008c-6.1162338,0-10.8146286-1.6060715-14.5303345-3.6929359
c6.8368607-10.3524857,13.6045761-20.9680901,20.8757477-32.3321495
C114.5905914,31.9920235,119.3882904,39.118515,119.3882904,46.848568z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
]>
<svg version="1.1" id="Layer_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 156.0529938 144"
style="enable-background:new 0 0 156.0529938 144;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FF4438;}
</style>
<metadata>
<sfw xmlns="&ns_sfw;">
<slices></slices>
<sliceSourceBounds bottomLeftOrigin="true" height="144" width="156.0530246" x="0" y="-144"></sliceSourceBounds>
</sfw>
</metadata>
<path class="st0" d="M147.670166,79.4482727c-10.7946014,13.6012039-22.452774,29.1454315-45.7691193,29.1454315
c-20.8268356,0-28.5858765-18.3700104-29.1313019-33.2931824c4.5630417,9.6490479,13.4846115,17.4628525,27.4041672,17.1012802
c26.7706146-0.8635712,45.1214371-25.0434799,45.1214371-47.0644722C145.2953491,18.9985008,125.6491776,0,91.5382309,0
C67.1424255,0,36.9175415,9.2833586,17.0554714,23.9640179c-0.2158909,15.1124439,8.2038994,34.7586212,11.226387,32.5997009
c17.21908-12.3804817,30.8731976-20.3501434,44.1166153-24.3462563C52.7944603,54.0785866,5.7588782,104.8391037,0,113.775116
C0.6476762,121.9790115,10.7946024,144,15.7601204,144c1.5112438,0,2.8065968-0.8635712,4.3178406-2.3748169
c14.1801453-15.9326324,25.7396469-30.2172928,36.0215034-43.985733
c1.443779,20.1794739,11.366806,44.8493042,39.1089325,44.8493042c24.8275833,0,49.4392776-17.9190369,60.6656723-58.2908554
C157.1694183,79.2323837,151.1244354,75.3463287,147.670166,79.4482727z M119.3883057,46.8485756
c0,12.7376328-12.5217361,18.9985008-23.9640198,18.9985008c-6.1162338,0-10.8146286-1.6060638-14.5303345-3.6929359
c6.8368607-10.3524857,13.6045761-20.9680939,20.8757477-32.3321533
C114.5906143,31.9920292,119.3883057,39.1185226,119.3883057,46.8485756z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -137,6 +137,7 @@
"dist/nodes/vector_store/VectorStorePinecone/VectorStorePinecone.node.js",
"dist/nodes/vector_store/VectorStorePineconeInsert/VectorStorePineconeInsert.node.js",
"dist/nodes/vector_store/VectorStorePineconeLoad/VectorStorePineconeLoad.node.js",
"dist/nodes/vector_store/VectorStoreRedis/VectorStoreRedis.node.js",
"dist/nodes/vector_store/VectorStoreQdrant/VectorStoreQdrant.node.js",
"dist/nodes/vector_store/VectorStoreSupabase/VectorStoreSupabase.node.js",
"dist/nodes/vector_store/VectorStoreSupabaseInsert/VectorStoreSupabaseInsert.node.js",
@ -227,7 +228,7 @@
"pdf-parse": "1.1.1",
"pg": "8.12.0",
"proxy-from-env": "^1.1.0",
"redis": "4.6.12",
"redis": "4.6.14",
"sanitize-html": "2.12.1",
"sqlite3": "5.1.7",
"temp": "0.9.4",

View File

@ -1105,7 +1105,7 @@ importers:
version: 0.3.4(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)
'@langchain/community':
specifier: 'catalog:'
version: 0.3.50(8ac6ecc2064042e5620199e694862b5d)
version: 0.3.50(90a6aa8f17697e0046031fe5da1489d1)
'@langchain/core':
specifier: 'catalog:'
version: 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))
@ -1168,7 +1168,7 @@ importers:
version: link:../json-schema-to-zod
'@n8n/typeorm':
specifier: 'catalog:'
version: 0.3.20-13(@sentry/node@9.42.1)(ioredis@5.3.2)(mysql2@3.15.0)(pg@8.12.0)(redis@4.6.12)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
version: 0.3.20-13(@sentry/node@9.42.1)(ioredis@5.3.2)(mysql2@3.15.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
'@n8n/typescript-config':
specifier: workspace:*
version: link:../typescript-config
@ -1260,8 +1260,8 @@ importers:
specifier: ^1.1.0
version: 1.1.0
redis:
specifier: 4.6.12
version: 4.6.12
specifier: 4.6.14
version: 4.6.14
sanitize-html:
specifier: 2.12.1
version: 2.12.1
@ -6356,10 +6356,6 @@ packages:
peerDependencies:
'@redis/client': ^1.0.0
'@redis/client@1.5.13':
resolution: {integrity: sha512-epkUM9D0Sdmt93/8Ozk43PNjLi36RZzG+d/T1Gdu5AI8jvghonTeLYV69WVWdilvFo+PYxbP0TZ0saMvr6nscQ==}
engines: {node: '>=14'}
'@redis/client@1.5.16':
resolution: {integrity: sha512-X1a3xQ5kEMvTib5fBrHKh6Y+pXbeKXqziYuxOUo1ojQNECg4M5Etd1qqyhMap+lFUOAh8S7UYevgJHOm4A+NOg==}
engines: {node: '>=14'}
@ -14535,9 +14531,6 @@ packages:
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
engines: {node: '>=4'}
redis@4.6.12:
resolution: {integrity: sha512-41Xuuko6P4uH4VPe5nE3BqXHB7a9lkFL0J29AlxKaIfD6eWO8VO/5PDF9ad2oS+mswMsfFxaM5DlE3tnXT+P8Q==}
redis@4.6.14:
resolution: {integrity: sha512-GrNg/e33HtsQwNXL7kJT+iNFPSwE1IPmd7wzV3j4f2z0EYxZfZE7FVTmUysgAtqQQtg5NXF5SNLR9OdO/UHOfw==}
@ -19713,14 +19706,14 @@ snapshots:
'@jest/test-result': 29.6.2
'@jest/transform': 29.6.2
'@jest/types': 29.6.1
'@types/node': 20.19.1
'@types/node': 20.19.11
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.8.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.5.0
jest-config: 29.6.2(@types/node@20.19.1)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2))
jest-config: 29.6.2(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2))
jest-haste-map: 29.6.2
jest-message-util: 29.6.2
jest-regex-util: 29.4.3
@ -19748,14 +19741,14 @@ snapshots:
'@jest/test-result': 29.6.2
'@jest/transform': 29.6.2
'@jest/types': 29.6.1
'@types/node': 20.19.1
'@types/node': 20.19.11
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.8.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.5.0
jest-config: 29.6.2(@types/node@20.19.1)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
jest-config: 29.6.2(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))
jest-haste-map: 29.6.2
jest-message-util: 29.6.2
jest-regex-util: 29.4.3
@ -19780,7 +19773,7 @@ snapshots:
dependencies:
'@jest/fake-timers': 29.6.2
'@jest/types': 29.6.1
'@types/node': 20.19.10
'@types/node': 20.19.11
jest-mock: 29.6.2
'@jest/expect-utils@29.6.2':
@ -19798,7 +19791,7 @@ snapshots:
dependencies:
'@jest/types': 29.6.1
'@sinonjs/fake-timers': 10.0.2
'@types/node': 20.19.10
'@types/node': 20.19.11
jest-message-util: 29.6.2
jest-mock: 29.6.2
jest-util: 29.6.2
@ -19894,7 +19887,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.4
'@types/istanbul-reports': 3.0.1
'@types/node': 20.19.10
'@types/node': 20.19.11
'@types/yargs': 17.0.19
chalk: 4.1.2
@ -19980,7 +19973,7 @@ snapshots:
- aws-crt
- encoding
'@langchain/community@0.3.50(8ac6ecc2064042e5620199e694862b5d)':
'@langchain/community@0.3.50(90a6aa8f17697e0046031fe5da1489d1)':
dependencies:
'@browserbasehq/stagehand': 1.9.0(@playwright/test@1.54.2)(bufferutil@4.0.9)(deepmerge@4.3.1)(dotenv@16.6.1)(encoding@0.1.13)(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))(utf-8-validate@5.0.10)(zod@3.25.67)
'@ibm-cloud/watsonx-ai': 1.1.2
@ -20040,7 +20033,7 @@ snapshots:
pdf-parse: 1.1.1
pg: 8.12.0
playwright: 1.54.2
redis: 4.6.12
redis: 4.6.14
weaviate-client: 3.6.2(encoding@0.1.13)
web-auth-library: 1.0.3
ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)
@ -20374,36 +20367,6 @@ snapshots:
esprima-next: 5.8.4
recast: 0.22.0
'@n8n/typeorm@0.3.20-13(@sentry/node@9.42.1)(ioredis@5.3.2)(mysql2@3.15.0)(pg@8.12.0)(redis@4.6.12)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))':
dependencies:
'@n8n/p-retry': 6.2.0-2
'@sqltools/formatter': 1.2.5
app-root-path: 3.1.0
async-mutex: 0.5.0
buffer: 6.0.3
chalk: 4.1.2
dayjs: 1.11.10
debug: 4.4.1(supports-color@8.1.1)
dotenv: 16.6.1
glob: 10.4.5
mkdirp: 2.1.3
reflect-metadata: 0.2.2
sha.js: 2.4.12
tarn: 3.0.2
tslib: 2.8.1
uuid: 9.0.1
yargs: 17.7.2
optionalDependencies:
'@sentry/node': 9.42.1
ioredis: 5.3.2
mysql2: 3.15.0
pg: 8.12.0
redis: 4.6.12
sqlite3: 5.1.7
ts-node: 10.9.2(@types/node@20.19.11)(typescript@5.9.2)
transitivePeerDependencies:
- supports-color
'@n8n/typeorm@0.3.20-13(@sentry/node@9.42.1)(ioredis@5.3.2)(mysql2@3.15.0)(pg@8.12.0)(redis@4.6.14)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2))':
dependencies:
'@n8n/p-retry': 6.2.0-2
@ -20981,54 +20944,28 @@ snapshots:
'@qdrant/openapi-typescript-fetch@1.2.6': {}
'@redis/bloom@1.2.0(@redis/client@1.5.13)':
dependencies:
'@redis/client': 1.5.13
'@redis/bloom@1.2.0(@redis/client@1.5.16)':
dependencies:
'@redis/client': 1.5.16
'@redis/client@1.5.13':
dependencies:
cluster-key-slot: 1.1.2
generic-pool: 3.9.0
yallist: 4.0.0
'@redis/client@1.5.16':
dependencies:
cluster-key-slot: 1.1.2
generic-pool: 3.9.0
yallist: 4.0.0
'@redis/graph@1.1.1(@redis/client@1.5.13)':
dependencies:
'@redis/client': 1.5.13
'@redis/graph@1.1.1(@redis/client@1.5.16)':
dependencies:
'@redis/client': 1.5.16
'@redis/json@1.0.6(@redis/client@1.5.13)':
dependencies:
'@redis/client': 1.5.13
'@redis/json@1.0.6(@redis/client@1.5.16)':
dependencies:
'@redis/client': 1.5.16
'@redis/search@1.1.6(@redis/client@1.5.13)':
dependencies:
'@redis/client': 1.5.13
'@redis/search@1.1.6(@redis/client@1.5.16)':
dependencies:
'@redis/client': 1.5.16
'@redis/time-series@1.0.5(@redis/client@1.5.13)':
dependencies:
'@redis/client': 1.5.13
'@redis/time-series@1.0.5(@redis/client@1.5.16)':
dependencies:
'@redis/client': 1.5.16
@ -22365,7 +22302,7 @@ snapshots:
'@types/jsdom@20.0.1':
dependencies:
'@types/node': 20.19.1
'@types/node': 20.19.11
'@types/tough-cookie': 4.0.2
parse5: 7.1.2
@ -22637,7 +22574,7 @@ snapshots:
'@types/through@0.0.30':
dependencies:
'@types/node': 20.19.10
'@types/node': 20.19.11
'@types/tough-cookie@4.0.2': {}
@ -27842,7 +27779,7 @@ snapshots:
- babel-plugin-macros
- supports-color
jest-config@29.6.2(@types/node@20.19.1)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)):
jest-config@29.6.2(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.17.57)(typescript@5.9.2)):
dependencies:
'@babel/core': 7.26.10
'@jest/test-sequencer': 29.6.2
@ -27867,43 +27804,12 @@ snapshots:
slash: 3.0.0
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 20.19.1
'@types/node': 20.19.11
ts-node: 10.9.2(@types/node@20.17.57)(typescript@5.9.2)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
jest-config@29.6.2(@types/node@20.19.1)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)):
dependencies:
'@babel/core': 7.26.10
'@jest/test-sequencer': 29.6.2
'@jest/types': 29.6.1
babel-jest: 29.6.2(@babel/core@7.26.10)
chalk: 4.1.2
ci-info: 3.8.0
deepmerge: 4.3.1
glob: 7.2.3
graceful-fs: 4.2.11
jest-circus: 29.6.2
jest-environment-node: 29.6.2
jest-get-type: 29.4.3
jest-regex-util: 29.4.3
jest-resolve: 29.6.2
jest-runner: 29.6.2
jest-util: 29.6.2
jest-validate: 29.6.2
micromatch: 4.0.8
parse-json: 5.2.0
pretty-format: 29.7.0
slash: 3.0.0
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 20.19.1
ts-node: 10.9.2(@types/node@20.19.11)(typescript@5.9.2)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
jest-config@29.6.2(@types/node@20.19.11)(ts-node@10.9.2(@types/node@20.19.11)(typescript@5.9.2)):
dependencies:
'@babel/core': 7.26.10
@ -28179,7 +28085,7 @@ snapshots:
jest-util@29.5.0:
dependencies:
'@jest/types': 29.6.1
'@types/node': 20.19.1
'@types/node': 20.19.11
chalk: 4.1.2
ci-info: 3.8.0
graceful-fs: 4.2.11
@ -28188,7 +28094,7 @@ snapshots:
jest-util@29.6.2:
dependencies:
'@jest/types': 29.6.1
'@types/node': 20.19.10
'@types/node': 20.19.11
chalk: 4.1.2
ci-info: 3.8.0
graceful-fs: 4.2.11
@ -31187,15 +31093,6 @@ snapshots:
dependencies:
redis-errors: 1.2.0
redis@4.6.12:
dependencies:
'@redis/bloom': 1.2.0(@redis/client@1.5.13)
'@redis/client': 1.5.13
'@redis/graph': 1.1.1(@redis/client@1.5.13)
'@redis/json': 1.0.6(@redis/client@1.5.13)
'@redis/search': 1.1.6(@redis/client@1.5.13)
'@redis/time-series': 1.0.5(@redis/client@1.5.13)
redis@4.6.14:
dependencies:
'@redis/bloom': 1.2.0(@redis/client@1.5.16)