From 2514301abdd64b86ed4f9f1cf6048b7e6c2506b8 Mon Sep 17 00:00:00 2001 From: ongdisheng <63136897+ongdisheng@users.noreply.github.com> Date: Wed, 10 Sep 2025 21:03:57 +0800 Subject: [PATCH] fix(Set Node): Handle special replacement patterns in JSON expressions (#18162) --- .../nodes/Set/test/v2/utils.test.ts | 93 ++++++++++++++++++- .../nodes-base/nodes/Set/v2/helpers/utils.ts | 5 +- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/packages/nodes-base/nodes/Set/test/v2/utils.test.ts b/packages/nodes-base/nodes/Set/test/v2/utils.test.ts index 43566c634c..0d824d1292 100644 --- a/packages/nodes-base/nodes/Set/test/v2/utils.test.ts +++ b/packages/nodes-base/nodes/Set/test/v2/utils.test.ts @@ -3,7 +3,12 @@ import { constructExecutionMetaData } from 'n8n-core'; import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow'; import type { SetNodeOptions } from '../../v2/helpers/interfaces'; -import { composeReturnItem, parseJsonParameter, validateEntry } from '../../v2/helpers/utils'; +import { + composeReturnItem, + parseJsonParameter, + validateEntry, + resolveRawData, +} from '../../v2/helpers/utils'; export const node: INode = { id: '11', @@ -342,3 +347,89 @@ describe('test Set2, validateEntry', () => { }); }); }); + +describe('test Set2, resolveRawData', () => { + const createMockExecuteFunctionForResolve = (returnValue: any) => { + return { + evaluateExpression: jest.fn().mockReturnValue(returnValue), + } as unknown as IExecuteFunctions; + }; + + it('should handle strings with special replacement patterns like $&', () => { + const mockFunction = createMockExecuteFunctionForResolve('hello world $&'); + const input = '{{ "hello world $&" }}'; + const result = resolveRawData.call(mockFunction, input, 0); + + // The result should contain the literal $& and not have it replaced + expect(result).toBe('hello world $&'); + expect(mockFunction.evaluateExpression).toHaveBeenCalledWith('{{ "hello world $&" }}', 0); + }); + + it('should handle strings with $` pattern', () => { + const mockFunction = createMockExecuteFunctionForResolve('hello world $`'); + const input = '{{ "hello world $`" }}'; + const result = resolveRawData.call(mockFunction, input, 0); + + expect(result).toBe('hello world $`'); + }); + + it("should handle strings with $' pattern", () => { + const mockFunction = createMockExecuteFunctionForResolve("hello world $'"); + const input = '{{ "hello world $\'" }}'; + const result = resolveRawData.call(mockFunction, input, 0); + + expect(result).toBe("hello world $'"); + }); + + it('should handle strings with $n pattern', () => { + const mockFunction = createMockExecuteFunctionForResolve('hello world $1 $2'); + const input = '{{ "hello world $1 $2" }}'; + const result = resolveRawData.call(mockFunction, input, 0); + + expect(result).toBe('hello world $1 $2'); + }); + + it('should handle JSON objects with special patterns', () => { + const mockFunction = createMockExecuteFunctionForResolve({ message: 'hello world $&' }); + const input = '{{ {"message": "hello world $&"} }}'; + const result = resolveRawData.call(mockFunction, input, 0); + + expect(result).toBe('{"message":"hello world $&"}'); + }); + + it('should handle multiple expressions with special patterns', () => { + const mockFunction = { + evaluateExpression: jest.fn().mockReturnValueOnce('hello $&').mockReturnValueOnce('world $`'), + } as unknown as IExecuteFunctions; + + const input = 'start {{ expr1 }} middle {{ expr2 }} end'; + const result = resolveRawData.call(mockFunction, input, 0); + + expect(result).toBe('start hello $& middle world $` end'); + }); + + it('should handle regular strings without special patterns', () => { + const mockFunction = createMockExecuteFunctionForResolve('hello world'); + const input = '{{ "hello world" }}'; + const result = resolveRawData.call(mockFunction, input, 0); + + expect(result).toBe('hello world'); + }); + + it('should handle objects and stringify them', () => { + const mockFunction = createMockExecuteFunctionForResolve({ message: 'hello world', count: 42 }); + const input = '{{ someExpression }}'; + const result = resolveRawData.call(mockFunction, input, 0); + + expect(result).toBe('{"message":"hello world","count":42}'); + }); + + it('should return raw data when no resolvables are found', () => { + const mockFunction = createMockExecuteFunctionForResolve('unused'); + const input = 'hello world without expressions'; + const result = resolveRawData.call(mockFunction, input, 0); + + expect(result).toBe('hello world without expressions'); + expect(mockFunction.evaluateExpression).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/nodes-base/nodes/Set/v2/helpers/utils.ts b/packages/nodes-base/nodes/Set/v2/helpers/utils.ts index ddfe933159..503323851d 100644 --- a/packages/nodes-base/nodes/Set/v2/helpers/utils.ts +++ b/packages/nodes-base/nodes/Set/v2/helpers/utils.ts @@ -232,10 +232,11 @@ export function resolveRawData( for (const resolvable of resolvables) { const resolvedValue = this.evaluateExpression(`${resolvable}`, i); + // Use a function replacer to avoid issues with special replacement patterns like $& if (typeof resolvedValue === 'object' && resolvedValue !== null) { - returnData = returnData.replace(resolvable, JSON.stringify(resolvedValue)); + returnData = returnData.replace(resolvable, () => JSON.stringify(resolvedValue)); } else { - returnData = returnData.replace(resolvable, resolvedValue as string); + returnData = returnData.replace(resolvable, () => String(resolvedValue)); } } }