mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
fix(editor): Use pinned data to resolve expressions in unexecuted nodes (#9693)
Co-authored-by: Milorad Filipovic <milorad@n8n.io> Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
parent
e995309789
commit
6cb3072a5d
135
cypress/e2e/2111-ado-support-pinned-data-in-expressions.cy.ts
Normal file
135
cypress/e2e/2111-ado-support-pinned-data-in-expressions.cy.ts
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
import { WorkflowPage, NDV } from '../pages';
|
||||||
|
|
||||||
|
const workflowPage = new WorkflowPage();
|
||||||
|
const ndv = new NDV();
|
||||||
|
|
||||||
|
describe('ADO-2111 expressions should support pinned data', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
workflowPage.actions.visit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports pinned data in expressions unexecuted and executed parent nodes', () => {
|
||||||
|
cy.createFixtureWorkflow('Test_workflow_pinned_data_in_expressions.json', 'Expressions');
|
||||||
|
|
||||||
|
// test previous node unexecuted
|
||||||
|
workflowPage.actions.openNode('NotPinnedWithExpressions');
|
||||||
|
ndv.getters
|
||||||
|
.parameterExpressionPreview('value')
|
||||||
|
.eq(0)
|
||||||
|
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
|
||||||
|
ndv.getters
|
||||||
|
.parameterExpressionPreview('value')
|
||||||
|
.eq(1)
|
||||||
|
.should('contain.text', '0,0\nJoe\n\nJoe\n\nJoe\n\nJoe\nJoe');
|
||||||
|
|
||||||
|
// test can resolve correctly based on item
|
||||||
|
ndv.actions.switchInputMode('Table');
|
||||||
|
|
||||||
|
ndv.getters.inputTableRow(2).realHover();
|
||||||
|
cy.wait(50);
|
||||||
|
ndv.getters
|
||||||
|
.parameterExpressionPreview('value')
|
||||||
|
.eq(0)
|
||||||
|
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
|
||||||
|
ndv.getters
|
||||||
|
.parameterExpressionPreview('value')
|
||||||
|
.eq(1)
|
||||||
|
.should('contain.text', '0,1\nJoan\n\nJoan\n\nJoan\n\nJoan\nJoan');
|
||||||
|
|
||||||
|
// test previous node executed
|
||||||
|
ndv.actions.execute();
|
||||||
|
ndv.getters.inputTableRow(1).realHover();
|
||||||
|
cy.wait(50);
|
||||||
|
|
||||||
|
ndv.getters
|
||||||
|
.parameterExpressionPreview('value')
|
||||||
|
.eq(0)
|
||||||
|
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
|
||||||
|
|
||||||
|
ndv.getters
|
||||||
|
.parameterExpressionPreview('value')
|
||||||
|
.eq(1)
|
||||||
|
.should('contain.text', '0,0\nJoe\n\nJoe\n\nJoe\n\nJoe\nJoe');
|
||||||
|
|
||||||
|
ndv.getters.inputTableRow(2).realHover();
|
||||||
|
cy.wait(50);
|
||||||
|
ndv.getters
|
||||||
|
.parameterExpressionPreview('value')
|
||||||
|
.eq(0)
|
||||||
|
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
|
||||||
|
ndv.getters
|
||||||
|
.parameterExpressionPreview('value')
|
||||||
|
.eq(1)
|
||||||
|
.should('contain.text', '0,1\nJoan\n\nJoan\n\nJoan\n\nJoan\nJoan');
|
||||||
|
|
||||||
|
// check it resolved correctly on the backend
|
||||||
|
ndv.getters
|
||||||
|
.outputTbodyCell(1, 0)
|
||||||
|
.should('contain.text', 'Joe\\nJoe\\nJoan\\nJoan\\nJoe\\nJoan\\n\\nJoe\\nJoan\\n\\nJoe');
|
||||||
|
|
||||||
|
ndv.getters
|
||||||
|
.outputTbodyCell(2, 0)
|
||||||
|
.should('contain.text', 'Joe\\nJoe\\nJoan\\nJoan\\nJoe\\nJoan\\n\\nJoe\\nJoan\\n\\nJoe');
|
||||||
|
|
||||||
|
ndv.getters
|
||||||
|
.outputTbodyCell(1, 1)
|
||||||
|
.should('contain.text', '0,0\\nJoe\\n\\nJoe\\n\\nJoe\\n\\nJoe\\nJoe');
|
||||||
|
|
||||||
|
ndv.getters
|
||||||
|
.outputTbodyCell(2, 1)
|
||||||
|
.should('contain.text', '0,1\\nJoan\\n\\nJoan\\n\\nJoan\\n\\nJoan\\nJoan');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets expressions after node is unpinned', () => {
|
||||||
|
cy.createFixtureWorkflow('Test_workflow_pinned_data_in_expressions.json', 'Expressions');
|
||||||
|
|
||||||
|
// test previous node unexecuted
|
||||||
|
workflowPage.actions.openNode('NotPinnedWithExpressions');
|
||||||
|
ndv.getters
|
||||||
|
.parameterExpressionPreview('value')
|
||||||
|
.eq(0)
|
||||||
|
.should('include.text', 'Joe\nJoe\nJoan\nJoan\nJoe\nJoan\n\nJoe\nJoan\n\nJoe');
|
||||||
|
ndv.getters
|
||||||
|
.parameterExpressionPreview('value')
|
||||||
|
.eq(1)
|
||||||
|
.should('contain.text', '0,0\nJoe\n\nJoe\n\nJoe\n\nJoe\nJoe');
|
||||||
|
|
||||||
|
ndv.actions.close();
|
||||||
|
|
||||||
|
// unpin pinned node
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('PinnedSet')
|
||||||
|
.eq(0)
|
||||||
|
.find('.node-pin-data-icon')
|
||||||
|
.should('exist');
|
||||||
|
workflowPage.getters.canvasNodeByName('PinnedSet').eq(0).click();
|
||||||
|
workflowPage.actions.hitPinNodeShortcut();
|
||||||
|
workflowPage.getters
|
||||||
|
.canvasNodeByName('PinnedSet')
|
||||||
|
.eq(0)
|
||||||
|
.find('.node-pin-data-icon')
|
||||||
|
.should('not.exist');
|
||||||
|
|
||||||
|
workflowPage.actions.openNode('NotPinnedWithExpressions');
|
||||||
|
ndv.getters.nodeParameters().find('parameter-expression-preview-value').should('not.exist');
|
||||||
|
|
||||||
|
ndv.getters.parameterInput('value').eq(0).click();
|
||||||
|
ndv.getters
|
||||||
|
.inlineExpressionEditorOutput()
|
||||||
|
.should(
|
||||||
|
'have.text',
|
||||||
|
'[Execute node ‘PinnedSet’ for preview][Execute node ‘PinnedSet’ for preview][Execute node ‘PinnedSet’ for preview][Execute node ‘PinnedSet’ for preview][Execute node ‘PinnedSet’ for preview][Execute node ‘PinnedSet’ for preview][Execute previous nodes for preview][Execute previous nodes for preview][undefined]',
|
||||||
|
);
|
||||||
|
|
||||||
|
// close open expression
|
||||||
|
ndv.getters.inputLabel().eq(0).click();
|
||||||
|
|
||||||
|
ndv.getters.parameterInput('value').eq(1).click();
|
||||||
|
ndv.getters
|
||||||
|
.inlineExpressionEditorOutput()
|
||||||
|
.should(
|
||||||
|
'have.text',
|
||||||
|
'0,0[Execute node ‘PinnedSet’ for preview][Execute node ‘PinnedSet’ for preview][Execute previous nodes for preview][Execute previous nodes for preview][Execute previous nodes for preview]',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
112
cypress/fixtures/Test_workflow_pinned_data_in_expressions.json
Normal file
112
cypress/fixtures/Test_workflow_pinned_data_in_expressions.json
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "5bd32b91ed2a88e542012920460f736c3687a32fbb953718f6952d182231c0ff"
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "a482f1fd-4815-4da4-a733-7beafb43c500",
|
||||||
|
"name": "static",
|
||||||
|
"value": "={{ $('PinnedSet').first().json.firstName }}\n{{ $('PinnedSet').itemMatching(0).json.firstName }}\n{{ $('PinnedSet').itemMatching(1).json.firstName }}\n{{ $('PinnedSet').last().json.firstName }}\n{{ $('PinnedSet').all()[0].json.firstName }}\n{{ $('PinnedSet').all()[1].json.firstName }}\n\n{{ $input.first().json.firstName }}\n{{ $input.last().json.firstName }}\n\n{{ $items()[0].json.firstName }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2c973f2a-7ca0-41bc-903c-7174bee251b0",
|
||||||
|
"name": "variable",
|
||||||
|
"value": "={{ $runIndex }},{{ $itemIndex }}\n{{ $node['PinnedSet'].json.firstName }}\n\n{{ $('PinnedSet').item.json.firstName }}\n\n{{ $input.item.json.firstName }}\n\n{{ $json.firstName }}\n{{ $data.firstName }}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "ac55ee16-4598-48bf-ace3-a48fed1d4ff3",
|
||||||
|
"name": "NotPinnedWithExpressions",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.3,
|
||||||
|
"position": [
|
||||||
|
1600,
|
||||||
|
640
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "3058c300-b377-41b7-9c90-a01372f9b581",
|
||||||
|
"name": "firstName",
|
||||||
|
"value": "Joe",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bb871662-c23c-4234-ac0c-b78c279bbf34",
|
||||||
|
"name": "lastName",
|
||||||
|
"value": "Smith",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "300a3888-cc2f-4e61-8578-b0adbcf33450",
|
||||||
|
"name": "PinnedSet",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.3,
|
||||||
|
"position": [
|
||||||
|
1340,
|
||||||
|
640
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "426ff39a-3408-48b4-899f-60db732675f8",
|
||||||
|
"name": "Start",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"position": [
|
||||||
|
1100,
|
||||||
|
640
|
||||||
|
],
|
||||||
|
"typeVersion": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"PinnedSet": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "NotPinnedWithExpressions",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Start": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "PinnedSet",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pinData": {
|
||||||
|
"PinnedSet": [
|
||||||
|
{
|
||||||
|
"firstName": "Joe",
|
||||||
|
"lastName": "Smith"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"firstName": "Joan",
|
||||||
|
"lastName": "Summers"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ export class NDV extends BasePage {
|
||||||
nodeOutputHint: () => cy.getByTestId('ndv-output-run-node-hint'),
|
nodeOutputHint: () => cy.getByTestId('ndv-output-run-node-hint'),
|
||||||
savePinnedDataButton: () =>
|
savePinnedDataButton: () =>
|
||||||
this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'),
|
this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'),
|
||||||
|
inputLabel: () => cy.getByTestId('input-label'),
|
||||||
outputTableRows: () => this.getters.outputDataContainer().find('table tr'),
|
outputTableRows: () => this.getters.outputDataContainer().find('table tr'),
|
||||||
outputTableHeaders: () => this.getters.outputDataContainer().find('table thead th'),
|
outputTableHeaders: () => this.getters.outputDataContainer().find('table thead th'),
|
||||||
outputTableHeaderByText: (text: string) => this.getters.outputTableHeaders().contains(text),
|
outputTableHeaderByText: (text: string) => this.getters.outputTableHeaders().contains(text),
|
||||||
|
|
|
@ -218,7 +218,6 @@ export function resolveParameter<T = IDataObject>(
|
||||||
ExpressionEvaluatorProxy.setEvaluator(
|
ExpressionEvaluatorProxy.setEvaluator(
|
||||||
useSettingsStore().settings.expressions?.evaluator ?? 'tmpl',
|
useSettingsStore().settings.expressions?.evaluator ?? 'tmpl',
|
||||||
);
|
);
|
||||||
|
|
||||||
return workflow.expression.getParameterValue(
|
return workflow.expression.getParameterValue(
|
||||||
parameter,
|
parameter,
|
||||||
runExecutionData,
|
runExecutionData,
|
||||||
|
@ -342,39 +341,6 @@ function connectionInputData(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
|
||||||
|
|
||||||
if (workflowsStore.shouldReplaceInputDataWithPinData) {
|
|
||||||
const parentPinData = parentNode.reduce<INodeExecutionData[]>((acc, parentNodeName, index) => {
|
|
||||||
const pinData = workflowsStore.pinDataByNodeName(parentNodeName);
|
|
||||||
|
|
||||||
if (pinData) {
|
|
||||||
acc.push({
|
|
||||||
json: pinData[0],
|
|
||||||
pairedItem: {
|
|
||||||
item: index,
|
|
||||||
input: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (parentPinData.length > 0) {
|
|
||||||
if (connectionInputData && connectionInputData.length > 0) {
|
|
||||||
parentPinData.forEach((parentPinDataEntry) => {
|
|
||||||
connectionInputData![0].json = {
|
|
||||||
...connectionInputData![0].json,
|
|
||||||
...parentPinDataEntry.json,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
connectionInputData = parentPinData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return connectionInputData;
|
return connectionInputData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -362,7 +362,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
function getCurrentWorkflow(copyData?: boolean): Workflow {
|
function getCurrentWorkflow(copyData?: boolean): Workflow {
|
||||||
const nodes = getNodes();
|
const nodes = getNodes();
|
||||||
const connections = allConnections.value;
|
const connections = allConnections.value;
|
||||||
const cacheKey = JSON.stringify({ nodes, connections });
|
const cacheKey = JSON.stringify({ nodes, connections, pinData: pinnedWorkflowData.value });
|
||||||
if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) {
|
if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) {
|
||||||
return cachedWorkflow;
|
return cachedWorkflow;
|
||||||
}
|
}
|
||||||
|
|
|
@ -415,7 +415,7 @@ export class Workflow {
|
||||||
*
|
*
|
||||||
* @param {string} nodeName Name of the node to return the pinData of
|
* @param {string} nodeName Name of the node to return the pinData of
|
||||||
*/
|
*/
|
||||||
getPinDataOfNode(nodeName: string): IDataObject[] | undefined {
|
getPinDataOfNode(nodeName: string): INodeExecutionData[] | undefined {
|
||||||
return this.pinData ? this.pinData[nodeName] : undefined;
|
return this.pinData ? this.pinData[nodeName] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { deepCopy } from './utils';
|
||||||
import { getGlobalState } from './GlobalState';
|
import { getGlobalState } from './GlobalState';
|
||||||
import { ApplicationError } from './errors/application.error';
|
import { ApplicationError } from './errors/application.error';
|
||||||
import { SCRIPTING_NODE_TYPES } from './Constants';
|
import { SCRIPTING_NODE_TYPES } from './Constants';
|
||||||
|
import { getPinDataIfManualExecution } from './WorkflowDataProxyHelpers';
|
||||||
|
|
||||||
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
|
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
|
@ -241,6 +242,29 @@ export class WorkflowDataProxy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getNodeExecutionOrPinnedData({
|
||||||
|
nodeName,
|
||||||
|
branchIndex,
|
||||||
|
runIndex,
|
||||||
|
shortSyntax = false,
|
||||||
|
}: {
|
||||||
|
nodeName: string;
|
||||||
|
branchIndex?: number;
|
||||||
|
runIndex?: number;
|
||||||
|
shortSyntax?: boolean;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
return this.getNodeExecutionData(nodeName, shortSyntax, branchIndex, runIndex);
|
||||||
|
} catch (e) {
|
||||||
|
const pinData = getPinDataIfManualExecution(this.workflow, nodeName, this.mode);
|
||||||
|
if (pinData) {
|
||||||
|
return pinData;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the node ExecutionData
|
* Returns the node ExecutionData
|
||||||
*
|
*
|
||||||
|
@ -283,7 +307,7 @@ export class WorkflowDataProxy {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName) &&
|
!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName) &&
|
||||||
!that.workflow.getPinDataOfNode(nodeName)
|
!getPinDataIfManualExecution(that.workflow, nodeName, that.mode)
|
||||||
) {
|
) {
|
||||||
throw new ExpressionError('Referenced node is unexecuted', {
|
throw new ExpressionError('Referenced node is unexecuted', {
|
||||||
runIndex: that.runIndex,
|
runIndex: that.runIndex,
|
||||||
|
@ -383,7 +407,10 @@ export class WorkflowDataProxy {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['binary', 'data', 'json'].includes(name)) {
|
if (['binary', 'data', 'json'].includes(name)) {
|
||||||
const executionData = that.getNodeExecutionData(nodeName, shortSyntax, undefined);
|
const executionData = that.getNodeExecutionOrPinnedData({
|
||||||
|
nodeName,
|
||||||
|
shortSyntax,
|
||||||
|
});
|
||||||
|
|
||||||
if (executionData.length === 0) {
|
if (executionData.length === 0) {
|
||||||
if (that.workflow.getParentNodes(nodeName).length === 0) {
|
if (that.workflow.getParentNodes(nodeName).length === 0) {
|
||||||
|
@ -619,11 +646,6 @@ export class WorkflowDataProxy {
|
||||||
getDataProxy(): IWorkflowDataProxyData {
|
getDataProxy(): IWorkflowDataProxyData {
|
||||||
const that = this;
|
const that = this;
|
||||||
|
|
||||||
const getNodeOutput = (nodeName: string, branchIndex: number, runIndex?: number) => {
|
|
||||||
runIndex = runIndex === undefined ? -1 : runIndex;
|
|
||||||
return that.getNodeExecutionData(nodeName, false, branchIndex, runIndex);
|
|
||||||
};
|
|
||||||
|
|
||||||
// replacing proxies with the actual data.
|
// replacing proxies with the actual data.
|
||||||
const jmespathWrapper = (data: IDataObject | IDataObject[], query: string) => {
|
const jmespathWrapper = (data: IDataObject | IDataObject[], query: string) => {
|
||||||
if (typeof data !== 'object' || typeof query !== 'string') {
|
if (typeof data !== 'object' || typeof query !== 'string') {
|
||||||
|
@ -662,7 +684,7 @@ export class WorkflowDataProxy {
|
||||||
|
|
||||||
if (context?.nodeCause) {
|
if (context?.nodeCause) {
|
||||||
const nodeName = context.nodeCause;
|
const nodeName = context.nodeCause;
|
||||||
const pinData = this.workflow.getPinDataOfNode(nodeName);
|
const pinData = getPinDataIfManualExecution(that.workflow, nodeName, that.mode);
|
||||||
|
|
||||||
if (pinData) {
|
if (pinData) {
|
||||||
if (!context) {
|
if (!context) {
|
||||||
|
@ -776,7 +798,8 @@ export class WorkflowDataProxy {
|
||||||
|
|
||||||
const previousNodeOutputData =
|
const previousNodeOutputData =
|
||||||
taskData?.data?.main?.[previousNodeOutput] ??
|
taskData?.data?.main?.[previousNodeOutput] ??
|
||||||
(that.workflow.getPinDataOfNode(sourceData.previousNode) as INodeExecutionData[]);
|
getPinDataIfManualExecution(that.workflow, sourceData.previousNode, that.mode) ??
|
||||||
|
[];
|
||||||
const source = taskData?.source ?? [];
|
const source = taskData?.source ?? [];
|
||||||
|
|
||||||
if (pairedItem.item >= previousNodeOutputData.length) {
|
if (pairedItem.item >= previousNodeOutputData.length) {
|
||||||
|
@ -897,10 +920,22 @@ export class WorkflowDataProxy {
|
||||||
}
|
}
|
||||||
|
|
||||||
taskData =
|
taskData =
|
||||||
that.runExecutionData!.resultData.runData[sourceData.previousNode][
|
that.runExecutionData!.resultData.runData[sourceData.previousNode]?.[
|
||||||
sourceData?.previousNodeRun || 0
|
sourceData?.previousNodeRun || 0
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (!taskData) {
|
||||||
|
const pinData = getPinDataIfManualExecution(
|
||||||
|
that.workflow,
|
||||||
|
sourceData.previousNode,
|
||||||
|
that.mode,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pinData) {
|
||||||
|
taskData = { data: { main: [pinData] }, startTime: 0, executionTime: 0, source: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const previousNodeOutput = sourceData.previousNodeOutput || 0;
|
const previousNodeOutput = sourceData.previousNodeOutput || 0;
|
||||||
if (previousNodeOutput >= taskData.data!.main.length) {
|
if (previousNodeOutput >= taskData.data!.main.length) {
|
||||||
throw createExpressionError('Can’t get data for expression', {
|
throw createExpressionError('Can’t get data for expression', {
|
||||||
|
@ -944,7 +979,7 @@ export class WorkflowDataProxy {
|
||||||
const ensureNodeExecutionData = () => {
|
const ensureNodeExecutionData = () => {
|
||||||
if (
|
if (
|
||||||
!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) &&
|
!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) &&
|
||||||
!that.workflow.getPinDataOfNode(nodeName)
|
!getPinDataIfManualExecution(that.workflow, nodeName, that.mode)
|
||||||
) {
|
) {
|
||||||
throw createExpressionError('Referenced node is unexecuted', {
|
throw createExpressionError('Referenced node is unexecuted', {
|
||||||
runIndex: that.runIndex,
|
runIndex: that.runIndex,
|
||||||
|
@ -1009,8 +1044,20 @@ export class WorkflowDataProxy {
|
||||||
itemIndex = that.itemIndex;
|
itemIndex = that.itemIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!that.connectionInputData.length) {
|
||||||
|
const pinnedData = getPinDataIfManualExecution(
|
||||||
|
that.workflow,
|
||||||
|
nodeName,
|
||||||
|
that.mode,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pinnedData) {
|
||||||
|
return pinnedData[itemIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const executionData = that.connectionInputData;
|
const executionData = that.connectionInputData;
|
||||||
const input = executionData[itemIndex];
|
const input = executionData?.[itemIndex];
|
||||||
if (!input) {
|
if (!input) {
|
||||||
throw createExpressionError('Can’t get data for expression', {
|
throw createExpressionError('Can’t get data for expression', {
|
||||||
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
|
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
|
||||||
|
@ -1061,6 +1108,7 @@ export class WorkflowDataProxy {
|
||||||
}
|
}
|
||||||
return pairedItemMethod;
|
return pairedItemMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (property === 'first') {
|
if (property === 'first') {
|
||||||
ensureNodeExecutionData();
|
ensureNodeExecutionData();
|
||||||
return (branchIndex?: number, runIndex?: number) => {
|
return (branchIndex?: number, runIndex?: number) => {
|
||||||
|
@ -1070,7 +1118,11 @@ export class WorkflowDataProxy {
|
||||||
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
|
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
|
||||||
?.sourceIndex ??
|
?.sourceIndex ??
|
||||||
0;
|
0;
|
||||||
const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
|
const executionData = that.getNodeExecutionOrPinnedData({
|
||||||
|
nodeName,
|
||||||
|
branchIndex,
|
||||||
|
runIndex,
|
||||||
|
});
|
||||||
if (executionData[0]) return executionData[0];
|
if (executionData[0]) return executionData[0];
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
@ -1084,7 +1136,11 @@ export class WorkflowDataProxy {
|
||||||
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
|
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
|
||||||
?.sourceIndex ??
|
?.sourceIndex ??
|
||||||
0;
|
0;
|
||||||
const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
|
const executionData = that.getNodeExecutionOrPinnedData({
|
||||||
|
nodeName,
|
||||||
|
branchIndex,
|
||||||
|
runIndex,
|
||||||
|
});
|
||||||
if (!executionData.length) return undefined;
|
if (!executionData.length) return undefined;
|
||||||
if (executionData[executionData.length - 1]) {
|
if (executionData[executionData.length - 1]) {
|
||||||
return executionData[executionData.length - 1];
|
return executionData[executionData.length - 1];
|
||||||
|
@ -1101,7 +1157,7 @@ export class WorkflowDataProxy {
|
||||||
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
|
that.workflow.getNodeConnectionIndexes(that.activeNodeName, nodeName)
|
||||||
?.sourceIndex ??
|
?.sourceIndex ??
|
||||||
0;
|
0;
|
||||||
return getNodeOutput(nodeName, branchIndex, runIndex);
|
return that.getNodeExecutionOrPinnedData({ nodeName, branchIndex, runIndex });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (property === 'context') {
|
if (property === 'context') {
|
||||||
|
|
12
packages/workflow/src/WorkflowDataProxyHelpers.ts
Normal file
12
packages/workflow/src/WorkflowDataProxyHelpers.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import type { INodeExecutionData, Workflow, WorkflowExecuteMode } from '.';
|
||||||
|
|
||||||
|
export function getPinDataIfManualExecution(
|
||||||
|
workflow: Workflow,
|
||||||
|
nodeName: string,
|
||||||
|
mode: WorkflowExecuteMode,
|
||||||
|
): INodeExecutionData[] | undefined {
|
||||||
|
if (mode !== 'manual') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return workflow.getPinDataOfNode(nodeName);
|
||||||
|
}
|
|
@ -571,6 +571,37 @@ const setNode: LoadedClass<INodeType> = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const manualTriggerNode: LoadedClass<INodeType> = {
|
||||||
|
sourcePath: '',
|
||||||
|
type: {
|
||||||
|
description: {
|
||||||
|
displayName: 'Manual Trigger',
|
||||||
|
name: 'n8n-nodes-base.manualTrigger',
|
||||||
|
icon: 'fa:mouse-pointer',
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Runs the flow on clicking a button in n8n',
|
||||||
|
eventTriggerDescription: '',
|
||||||
|
maxNodes: 1,
|
||||||
|
defaults: {
|
||||||
|
name: 'When clicking ‘Test workflow’',
|
||||||
|
color: '#909298',
|
||||||
|
},
|
||||||
|
inputs: [],
|
||||||
|
outputs: ['main'],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName:
|
||||||
|
'This node is where the workflow execution starts (when you click the ‘test’ button on the canvas).<br><br> <a data-action="showNodeCreator">Explore other ways to trigger your workflow</a> (e.g on a schedule, or a webhook)',
|
||||||
|
name: 'notice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export class NodeTypes implements INodeTypes {
|
export class NodeTypes implements INodeTypes {
|
||||||
nodeTypes: INodeTypeData = {
|
nodeTypes: INodeTypeData = {
|
||||||
'n8n-nodes-base.stickyNote': stickyNode,
|
'n8n-nodes-base.stickyNote': stickyNode,
|
||||||
|
@ -628,6 +659,7 @@ export class NodeTypes implements INodeTypes {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
'n8n-nodes-base.manualTrigger': manualTriggerNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
getByName(nodeType: string): INodeType | IVersionedNodeType {
|
getByName(nodeType: string): INodeType | IVersionedNodeType {
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import type { IExecuteData, INode, IRun, IWorkflowBase } from '@/Interfaces';
|
import type {
|
||||||
|
IExecuteData,
|
||||||
|
INode,
|
||||||
|
IPinData,
|
||||||
|
IRun,
|
||||||
|
IWorkflowBase,
|
||||||
|
WorkflowExecuteMode,
|
||||||
|
} from '@/Interfaces';
|
||||||
import { Workflow } from '@/Workflow';
|
import { Workflow } from '@/Workflow';
|
||||||
import { WorkflowDataProxy } from '@/WorkflowDataProxy';
|
import { WorkflowDataProxy } from '@/WorkflowDataProxy';
|
||||||
import { ExpressionError } from '@/errors/expression.error';
|
import { ExpressionError } from '@/errors/expression.error';
|
||||||
|
@ -13,7 +20,12 @@ const loadFixture = (fixture: string) => {
|
||||||
return { workflow, run };
|
return { workflow, run };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProxyFromFixture = (workflow: IWorkflowBase, run: IRun | null, activeNode: string) => {
|
const getProxyFromFixture = (
|
||||||
|
workflow: IWorkflowBase,
|
||||||
|
run: IRun | null,
|
||||||
|
activeNode: string,
|
||||||
|
mode?: WorkflowExecuteMode,
|
||||||
|
) => {
|
||||||
const taskData = run?.data.resultData.runData[activeNode]?.[0];
|
const taskData = run?.data.resultData.runData[activeNode]?.[0];
|
||||||
const lastNodeConnectionInputData = taskData?.data?.main[0];
|
const lastNodeConnectionInputData = taskData?.data?.main[0];
|
||||||
|
|
||||||
|
@ -29,6 +41,16 @@ const getProxyFromFixture = (workflow: IWorkflowBase, run: IRun | null, activeNo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pinData: IPinData = {};
|
||||||
|
if (workflow.pinData) {
|
||||||
|
// json key is stored as part of workflow
|
||||||
|
// but dropped when copy/pasting
|
||||||
|
// so adding here to keep updating tests simple
|
||||||
|
for (let nodeName in workflow.pinData) {
|
||||||
|
pinData[nodeName] = workflow.pinData[nodeName].map((item) => ({ json: item }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dataProxy = new WorkflowDataProxy(
|
const dataProxy = new WorkflowDataProxy(
|
||||||
new Workflow({
|
new Workflow({
|
||||||
id: '123',
|
id: '123',
|
||||||
|
@ -37,6 +59,7 @@ const getProxyFromFixture = (workflow: IWorkflowBase, run: IRun | null, activeNo
|
||||||
connections: workflow.connections,
|
connections: workflow.connections,
|
||||||
active: false,
|
active: false,
|
||||||
nodeTypes: Helpers.NodeTypes(),
|
nodeTypes: Helpers.NodeTypes(),
|
||||||
|
pinData,
|
||||||
}),
|
}),
|
||||||
run?.data ?? null,
|
run?.data ?? null,
|
||||||
0,
|
0,
|
||||||
|
@ -44,7 +67,7 @@ const getProxyFromFixture = (workflow: IWorkflowBase, run: IRun | null, activeNo
|
||||||
activeNode,
|
activeNode,
|
||||||
lastNodeConnectionInputData ?? [],
|
lastNodeConnectionInputData ?? [],
|
||||||
{},
|
{},
|
||||||
'manual',
|
mode ?? 'integrated',
|
||||||
{},
|
{},
|
||||||
executeData,
|
executeData,
|
||||||
);
|
);
|
||||||
|
@ -323,4 +346,61 @@ describe('WorkflowDataProxy', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Pinned data with manual execution', () => {
|
||||||
|
const fixture = loadFixture('pindata');
|
||||||
|
const proxy = getProxyFromFixture(fixture.workflow, null, 'NotPinnedSet1', 'manual');
|
||||||
|
|
||||||
|
test('$(PinnedSet).item.json', () => {
|
||||||
|
expect(proxy.$('PinnedSet').item.json).toEqual({ firstName: 'Joe', lastName: 'Smith' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$(PinnedSet).item.json.firstName', () => {
|
||||||
|
expect(proxy.$('PinnedSet').item.json.firstName).toBe('Joe');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$(PinnedSet).pairedItem().json.firstName', () => {
|
||||||
|
expect(proxy.$('PinnedSet').pairedItem().json.firstName).toBe('Joe');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$(PinnedSet).first().json.firstName', () => {
|
||||||
|
expect(proxy.$('PinnedSet').first().json.firstName).toBe('Joe');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$(PinnedSet).first().json.firstName', () => {
|
||||||
|
expect(proxy.$('PinnedSet').first().json.firstName).toBe('Joe');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$(PinnedSet).last().json.firstName', () => {
|
||||||
|
expect(proxy.$('PinnedSet').last().json.firstName).toBe('Joan');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$(PinnedSet).all()[0].json.firstName', () => {
|
||||||
|
expect(proxy.$('PinnedSet').all()[0].json.firstName).toBe('Joe');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$(PinnedSet).all()[1].json.firstName', () => {
|
||||||
|
expect(proxy.$('PinnedSet').all()[1].json.firstName).toBe('Joan');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$(PinnedSet).all()[2]', () => {
|
||||||
|
expect(proxy.$('PinnedSet').all()[2]).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$(PinnedSet).itemMatching(0).json.firstName', () => {
|
||||||
|
expect(proxy.$('PinnedSet').itemMatching(0).json.firstName).toBe('Joe');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$(PinnedSet).itemMatching(1).json.firstName', () => {
|
||||||
|
expect(proxy.$('PinnedSet').itemMatching(1).json.firstName).toBe('Joan');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$(PinnedSet).itemMatching(2)', () => {
|
||||||
|
expect(proxy.$('PinnedSet').itemMatching(2)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$node[PinnedSet].json.firstName', () => {
|
||||||
|
expect(proxy.$node.PinnedSet.json.firstName).toBe('Joe');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
7
packages/workflow/test/fixtures/WorkflowDataProxy/pindata_run.json
vendored
Normal file
7
packages/workflow/test/fixtures/WorkflowDataProxy/pindata_run.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"resultData": {
|
||||||
|
"runData": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
106
packages/workflow/test/fixtures/WorkflowDataProxy/pindata_workflow.json
vendored
Normal file
106
packages/workflow/test/fixtures/WorkflowDataProxy/pindata_workflow.json
vendored
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0"
|
||||||
|
},
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "3058c300-b377-41b7-9c90-a01372f9b581",
|
||||||
|
"name": "firstName",
|
||||||
|
"value": "Joe",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bb871662-c23c-4234-ac0c-b78c279bbf34",
|
||||||
|
"name": "lastName",
|
||||||
|
"value": "Smith",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "baee2bf4-5083-4cbe-8e51-4eddcf859ef5",
|
||||||
|
"name": "PinnedSet",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.3,
|
||||||
|
"position": [
|
||||||
|
1120,
|
||||||
|
380
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "a482f1fd-4815-4da4-a733-7beafb43c500",
|
||||||
|
"name": "test",
|
||||||
|
"value": "={{ $('PinnedSet').all().json }}\n{{ $('PinnedSet').item.json.firstName }}\n{{ $('PinnedSet').first().json.firstName }}\n{{ $('PinnedSet').itemMatching(0).json.firstName }}\n{{ $('PinnedSet').itemMatching(1).json.firstName }}\n{{ $('PinnedSet').last().json.firstName }}\n{{ $('PinnedSet').all()[0].json.firstName }}\n{{ $('PinnedSet').all()[1].json.firstName }}\n\n{{ $input.first().json.firstName }}\n{{ $input.last().json.firstName }}\n{{ $input.item.json.firstName }}\n\n{{ $json.firstName }}\n{{ $data.firstName }}\n\n{{ $items()[0].json.firstName }}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "2a543169-e2c1-4764-ac63-09534310b2b9",
|
||||||
|
"name": "NotPinnedSet1",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.3,
|
||||||
|
"position": [
|
||||||
|
1360,
|
||||||
|
380
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "f36672e5-8c87-480e-a5b8-de9da6b63192",
|
||||||
|
"name": "Start",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"position": [
|
||||||
|
920,
|
||||||
|
380
|
||||||
|
],
|
||||||
|
"typeVersion": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"PinnedSet": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "NotPinnedSet1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Start": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "PinnedSet",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pinData": {
|
||||||
|
"PinnedSet": [
|
||||||
|
{
|
||||||
|
"firstName": "Joe",
|
||||||
|
"lastName": "Smith"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"firstName": "Joan",
|
||||||
|
"lastName": "Summers"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue