mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-01 01:07:04 +02:00
fix(core): Iterate over all main output branches when extracting response (#19963)
Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
parent
977d37f658
commit
0b7db24070
|
|
@ -146,6 +146,51 @@ describe('getMessage', () => {
|
|||
const result = getMessage(execution);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return message from the second output branch when first is empty', () => {
|
||||
const execution = createMockExecution({}, undefined, [
|
||||
{
|
||||
data: {
|
||||
main: [
|
||||
[], // First output branch is empty
|
||||
[
|
||||
{
|
||||
json: { test: 'data' },
|
||||
sendMessage: 'Message from second branch',
|
||||
},
|
||||
], // Second output branch has the message
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const result = getMessage(execution);
|
||||
expect(result).toBe('Message from second branch');
|
||||
});
|
||||
|
||||
it('should prioritize message from the first branch when multiple branches have messages', () => {
|
||||
const execution = createMockExecution({}, undefined, [
|
||||
{
|
||||
data: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
json: { test: 'data1' },
|
||||
sendMessage: 'Message from first branch',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
json: { test: 'data2' },
|
||||
sendMessage: 'Message from second branch',
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const result = getMessage(execution);
|
||||
expect(result).toBe('Message from first branch');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLastNodeExecuted', () => {
|
||||
|
|
|
|||
|
|
@ -10,9 +10,18 @@ export function getMessage(execution: IExecutionResponse) {
|
|||
if (typeof lastNodeExecuted !== 'string') return undefined;
|
||||
|
||||
const runIndex = execution.data.resultData.runData[lastNodeExecuted].length - 1;
|
||||
const nodeExecutionData =
|
||||
execution.data.resultData.runData[lastNodeExecuted][runIndex]?.data?.main?.[0];
|
||||
return nodeExecutionData?.[0] ? nodeExecutionData[0].sendMessage : undefined;
|
||||
const mainOutputs = execution.data.resultData.runData[lastNodeExecuted][runIndex]?.data?.main;
|
||||
|
||||
// Check all main output branches for a message
|
||||
if (mainOutputs && Array.isArray(mainOutputs)) {
|
||||
for (const branch of mainOutputs) {
|
||||
if (branch && Array.isArray(branch) && branch.length > 0 && branch[0].sendMessage) {
|
||||
return branch[0].sendMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -116,6 +116,47 @@ describe('extractWebhookLastNodeResponse', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return data from second branch when first is empty', async () => {
|
||||
const jsonData = { foo: 'bar', fromSecondBranch: true };
|
||||
lastNodeTaskData.data = {
|
||||
main: [
|
||||
[], // First branch is empty
|
||||
[{ json: jsonData }], // Second branch has data
|
||||
],
|
||||
};
|
||||
|
||||
const result = await extractWebhookLastNodeResponse(
|
||||
context,
|
||||
'firstEntryJson',
|
||||
lastNodeTaskData,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
result: {
|
||||
type: 'static',
|
||||
body: jsonData,
|
||||
contentType: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when all branches are empty', async () => {
|
||||
lastNodeTaskData.data = {
|
||||
main: [[], [], []],
|
||||
};
|
||||
|
||||
const result = await extractWebhookLastNodeResponse(
|
||||
context,
|
||||
'firstEntryJson',
|
||||
lastNodeTaskData,
|
||||
);
|
||||
|
||||
assert(!result.ok);
|
||||
expect(result.error).toBeInstanceOf(OperationalError);
|
||||
expect(result.error.message).toBe('No item to return was found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('responseDataType: firstEntryBinary', () => {
|
||||
|
|
@ -288,6 +329,56 @@ describe('extractWebhookLastNodeResponse', () => {
|
|||
"The binary property 'nonExistentProperty' which should be returned does not exist",
|
||||
);
|
||||
});
|
||||
|
||||
it('should return binary data from second branch when first is empty', async () => {
|
||||
const binaryData: IBinaryData = {
|
||||
data: Buffer.from('binary from second branch').toString(BINARY_ENCODING),
|
||||
mimeType: 'text/plain',
|
||||
};
|
||||
const nodeExecutionData: INodeExecutionData = {
|
||||
json: {},
|
||||
binary: { data: binaryData },
|
||||
};
|
||||
lastNodeTaskData.data = {
|
||||
main: [
|
||||
[], // First branch is empty
|
||||
[nodeExecutionData], // Second branch has binary data
|
||||
],
|
||||
};
|
||||
|
||||
context.evaluateSimpleWebhookDescriptionExpression.mockReturnValue('data');
|
||||
|
||||
const result = await extractWebhookLastNodeResponse(
|
||||
context,
|
||||
'firstEntryBinary',
|
||||
lastNodeTaskData,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
result: {
|
||||
type: 'static',
|
||||
body: Buffer.from('binary from second branch'),
|
||||
contentType: 'text/plain',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when all branches are empty for binary', async () => {
|
||||
lastNodeTaskData.data = {
|
||||
main: [[], [], []],
|
||||
};
|
||||
|
||||
const result = await extractWebhookLastNodeResponse(
|
||||
context,
|
||||
'firstEntryBinary',
|
||||
lastNodeTaskData,
|
||||
);
|
||||
|
||||
assert(!result.ok);
|
||||
expect(result.error).toBeInstanceOf(OperationalError);
|
||||
expect(result.error.message).toBe('No item was found to return');
|
||||
});
|
||||
});
|
||||
|
||||
describe('responseDataType: noData', () => {
|
||||
|
|
@ -342,5 +433,68 @@ describe('extractWebhookLastNodeResponse', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return all entries from second branch when first is empty', async () => {
|
||||
const jsonData1 = { item: 1, fromSecondBranch: true };
|
||||
const jsonData2 = { item: 2, fromSecondBranch: true };
|
||||
const jsonData3 = { item: 3, fromSecondBranch: true };
|
||||
lastNodeTaskData.data = {
|
||||
main: [
|
||||
[], // First branch is empty
|
||||
[{ json: jsonData1 }, { json: jsonData2 }, { json: jsonData3 }], // Second branch has data
|
||||
],
|
||||
};
|
||||
|
||||
const result = await extractWebhookLastNodeResponse(context, 'allEntries', lastNodeTaskData);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
result: {
|
||||
type: 'static',
|
||||
body: [jsonData1, jsonData2, jsonData3],
|
||||
contentType: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return entries from first non-empty branch only', async () => {
|
||||
const branch2Data = { item: 'from-second' };
|
||||
const branch3Data = { item: 'from-third' };
|
||||
lastNodeTaskData.data = {
|
||||
main: [
|
||||
[], // First branch is empty
|
||||
[{ json: branch2Data }], // Second branch has data - this should be used
|
||||
[{ json: branch3Data }], // Third branch also has data - should be ignored
|
||||
],
|
||||
};
|
||||
|
||||
const result = await extractWebhookLastNodeResponse(context, 'allEntries', lastNodeTaskData);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
result: {
|
||||
type: 'static',
|
||||
body: [branch2Data], // Only data from second branch
|
||||
contentType: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array when all branches are empty', async () => {
|
||||
lastNodeTaskData.data = {
|
||||
main: [[], [], []],
|
||||
};
|
||||
|
||||
const result = await extractWebhookLastNodeResponse(context, 'allEntries', lastNodeTaskData);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
result: {
|
||||
type: 'static',
|
||||
body: [],
|
||||
contentType: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,17 +50,29 @@ export async function extractWebhookLastNodeResponse(
|
|||
}
|
||||
|
||||
/**
|
||||
* Extracts the JSON data of the first item of the last node
|
||||
* Extracts the JSON data of the first item of the first non-empty branch of the last node
|
||||
*/
|
||||
function extractFirstEntryJsonFromTaskData(
|
||||
context: WebhookExecutionContext,
|
||||
lastNodeTaskData: ITaskData,
|
||||
): Result<StaticResponse, OperationalError> {
|
||||
if (lastNodeTaskData.data!.main[0]![0] === undefined) {
|
||||
const mainOutputs = lastNodeTaskData.data?.main;
|
||||
let firstItem: INodeExecutionData | undefined;
|
||||
|
||||
if (mainOutputs && Array.isArray(mainOutputs)) {
|
||||
for (const branch of mainOutputs) {
|
||||
if (branch && Array.isArray(branch) && branch.length > 0) {
|
||||
firstItem = branch[0];
|
||||
break; // Stop after processing the first non-empty branch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstItem === undefined) {
|
||||
return createResultError(new OperationalError('No item to return was found'));
|
||||
}
|
||||
|
||||
let lastNodeFirstJsonItem: unknown = lastNodeTaskData.data!.main[0]![0].json;
|
||||
let lastNodeFirstJsonItem: unknown = firstItem.json;
|
||||
|
||||
const responsePropertyName =
|
||||
context.evaluateSimpleWebhookDescriptionExpression<string>('responsePropertyName');
|
||||
|
|
@ -82,14 +94,24 @@ function extractFirstEntryJsonFromTaskData(
|
|||
}
|
||||
|
||||
/**
|
||||
* Extracts the binary data of the first item of the last node
|
||||
* Extracts the binary data of the first item of the first non-empty branch of the last node
|
||||
*/
|
||||
async function extractFirstEntryBinaryFromTaskData(
|
||||
context: WebhookExecutionContext,
|
||||
lastNodeTaskData: ITaskData,
|
||||
): Promise<Result<StaticResponse | StreamResponse, OperationalError>> {
|
||||
// Return the binary data of the first entry
|
||||
const lastNodeFirstJsonItem: INodeExecutionData = lastNodeTaskData.data!.main[0]![0];
|
||||
const mainOutputs = lastNodeTaskData.data?.main;
|
||||
let lastNodeFirstJsonItem: INodeExecutionData | undefined;
|
||||
|
||||
if (mainOutputs && Array.isArray(mainOutputs)) {
|
||||
for (const branch of mainOutputs) {
|
||||
if (branch && Array.isArray(branch) && branch.length > 0) {
|
||||
// Found a non-empty branch, take the first item from it
|
||||
lastNodeFirstJsonItem = branch[0];
|
||||
break; // Stop after processing the first non-empty branch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastNodeFirstJsonItem === undefined) {
|
||||
return createResultError(new OperationalError('No item was found to return'));
|
||||
|
|
@ -139,14 +161,24 @@ async function extractFirstEntryBinaryFromTaskData(
|
|||
}
|
||||
|
||||
/**
|
||||
* Extracts the JSON data of all the items of the last node
|
||||
* Extracts the JSON data of all the items from the first non-empty branch of the last node
|
||||
*/
|
||||
function extractAllEntriesJsonFromTaskData(
|
||||
lastNodeTaskData: ITaskData,
|
||||
): Result<StaticResponse, OperationalError> {
|
||||
const data: unknown[] = [];
|
||||
for (const entry of lastNodeTaskData.data!.main[0]!) {
|
||||
data.push(entry.json);
|
||||
const mainOutputs = lastNodeTaskData.data?.main;
|
||||
|
||||
if (mainOutputs && Array.isArray(mainOutputs)) {
|
||||
for (const branch of mainOutputs) {
|
||||
if (branch && Array.isArray(branch) && branch.length > 0) {
|
||||
// Found a non-empty branch, extract all items from it
|
||||
for (const entry of branch) {
|
||||
data.push(entry.json);
|
||||
}
|
||||
break; // Stop after processing the first non-empty branch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createResultOk({
|
||||
|
|
|
|||
|
|
@ -1275,6 +1275,123 @@ describe('extractBotResponse', () => {
|
|||
const result = extractBotResponse(resultData, executionId);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should extract response from second output branch when first is empty', () => {
|
||||
const resultData: IRunExecutionData['resultData'] = {
|
||||
lastNodeExecuted: 'nodeA',
|
||||
runData: {
|
||||
nodeA: [
|
||||
{
|
||||
executionTime: 1,
|
||||
startTime: 1,
|
||||
executionIndex: 1,
|
||||
source: [],
|
||||
data: {
|
||||
main: [
|
||||
[], // First output branch is empty
|
||||
[{ json: { message: 'Response from second branch' } }], // Second branch has response
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const executionId = 'test-exec-id';
|
||||
const result = extractBotResponse(resultData, executionId);
|
||||
expect(result).toEqual({
|
||||
text: 'Response from second branch',
|
||||
sender: 'bot',
|
||||
id: executionId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract response from second branch when first has empty json', () => {
|
||||
const resultData: IRunExecutionData['resultData'] = {
|
||||
lastNodeExecuted: 'nodeA',
|
||||
runData: {
|
||||
nodeA: [
|
||||
{
|
||||
executionTime: 1,
|
||||
startTime: 1,
|
||||
executionIndex: 1,
|
||||
source: [],
|
||||
data: {
|
||||
main: [
|
||||
[{ json: {} }], // First branch has empty json object
|
||||
[{ json: { text: 'Response from second branch' } }], // Second branch has response
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const executionId = 'test-exec-id';
|
||||
const result = extractBotResponse(resultData, executionId);
|
||||
expect(result).toEqual({
|
||||
text: 'Response from second branch',
|
||||
sender: 'bot',
|
||||
id: executionId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract response from first available branch when multiple exist', () => {
|
||||
const resultData: IRunExecutionData['resultData'] = {
|
||||
lastNodeExecuted: 'nodeA',
|
||||
runData: {
|
||||
nodeA: [
|
||||
{
|
||||
executionTime: 1,
|
||||
startTime: 1,
|
||||
executionIndex: 1,
|
||||
source: [],
|
||||
data: {
|
||||
main: [
|
||||
[], // First branch empty
|
||||
[{ json: {} }], // Second branch has empty object
|
||||
[{ json: { output: 'Response from third branch' } }], // Third branch has response
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const executionId = 'test-exec-id';
|
||||
const result = extractBotResponse(resultData, executionId);
|
||||
expect(result).toEqual({
|
||||
text: 'Response from third branch',
|
||||
sender: 'bot',
|
||||
id: executionId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use response from first branch when multiple branches have valid text', () => {
|
||||
const resultData: IRunExecutionData['resultData'] = {
|
||||
lastNodeExecuted: 'nodeA',
|
||||
runData: {
|
||||
nodeA: [
|
||||
{
|
||||
executionTime: 1,
|
||||
startTime: 1,
|
||||
executionIndex: 1,
|
||||
source: [],
|
||||
data: {
|
||||
main: [
|
||||
[{ json: { text: 'First branch response' } }],
|
||||
[{ json: { text: 'Second branch response' } }],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const executionId = 'test-exec-id';
|
||||
const result = extractBotResponse(resultData, executionId);
|
||||
expect(result).toEqual({
|
||||
text: 'First branch response',
|
||||
sender: 'bot',
|
||||
id: executionId,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(mergeStartData, () => {
|
||||
|
|
|
|||
|
|
@ -502,8 +502,24 @@ export function extractBotResponse(
|
|||
if (get(nodeResponseData, 'error')) {
|
||||
responseMessage = '[ERROR: ' + get(nodeResponseData, 'error.message') + ']';
|
||||
} else {
|
||||
const responseData = get(nodeResponseData, 'data.main[0][0].json');
|
||||
const text = extractResponseText(responseData) ?? emptyText;
|
||||
// Check all output branches for response data, not just the first one
|
||||
const mainOutputs = get(nodeResponseData, 'data.main');
|
||||
let text: string | undefined;
|
||||
|
||||
if (mainOutputs && Array.isArray(mainOutputs)) {
|
||||
for (const branch of mainOutputs) {
|
||||
if (branch?.[0]?.json) {
|
||||
const responseData = branch[0].json;
|
||||
text = extractResponseText(responseData);
|
||||
if (text) {
|
||||
break; // Found a valid response, stop searching
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to emptyText if no valid response found
|
||||
text = text ?? emptyText;
|
||||
|
||||
if (!text) {
|
||||
return undefined;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user