mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat: No expression error when node hasn’t executed (#8448)
Co-authored-by: Giulio Andreini <andreini@netseven.it> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
parent
737170893d
commit
f9a99ec029
|
@ -1,72 +1,136 @@
|
|||
import { NDV } from '../pages/ndv';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
const ndv = new NDV();
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
|
||||
describe('Inline expression editor', () => {
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.visit();
|
||||
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openInlineExpressionEditor();
|
||||
|
||||
WorkflowPage.actions.addInitialNodeToCanvas('Schedule');
|
||||
cy.on('uncaught:exception', (err) => err.name !== 'ExpressionError');
|
||||
});
|
||||
|
||||
it('should resolve primitive resolvables', () => {
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('1 + 2');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^3$/);
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
describe('Static data', () => {
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openInlineExpressionEditor();
|
||||
});
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('"ab"');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{rightArrow}+');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('"cd"');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^abcd$/);
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
it('should resolve primitive resolvables', () => {
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('1 + 2');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^3$/);
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('true && false');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^false$/);
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('"ab"');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{rightArrow}+');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('"cd"');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^abcd$/);
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('true && false');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^false$/);
|
||||
});
|
||||
|
||||
it('should resolve object resolvables', () => {
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.type('{ a: 1 }', { parseSpecialCharSequences: false });
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Object: \{"a": 1\}\]$/);
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.type('{ a: 1 }.a', { parseSpecialCharSequences: false });
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^1$/);
|
||||
});
|
||||
|
||||
it('should resolve array resolvables', () => {
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Array: \[1,2,3\]\]$/);
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('[0]');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^1$/);
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve object resolvables', () => {
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.type('{ a: 1 }', { parseSpecialCharSequences: false });
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Object: \{"a": 1\}\]$/);
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
describe('Dynamic data', () => {
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.openNode('Schedule Trigger');
|
||||
ndv.actions.setPinnedData([{ myStr: 'Monday' }]);
|
||||
ndv.actions.close();
|
||||
WorkflowPage.actions.addNodeToCanvas('No Operation');
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openInlineExpressionEditor();
|
||||
});
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.type('{ a: 1 }.a', { parseSpecialCharSequences: false });
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^1$/);
|
||||
});
|
||||
it('should resolve $parameter[]', () => {
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
// Resolving $parameter is slow, especially on CI runner
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'getAll');
|
||||
});
|
||||
|
||||
it('should resolve array resolvables', () => {
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Array: \[1,2,3\]\]$/);
|
||||
it('should resolve input: $json,$input,$(nodeName)', () => {
|
||||
// Previous nodes have not run, input is empty
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorOutput()
|
||||
.should('have.text', '[Execute previous nodes for preview]');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorOutput()
|
||||
.should('have.text', '[Execute previous nodes for preview]');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.type("$('Schedule Trigger').item.json.myStr");
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorOutput()
|
||||
.should('have.text', '[Execute previous nodes for preview]');
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
// Run workflow
|
||||
ndv.actions.close();
|
||||
WorkflowPage.actions.executeNode('No Operation');
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openInlineExpressionEditor();
|
||||
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('[0]');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^1$/);
|
||||
});
|
||||
|
||||
it('should resolve $parameter[]', () => {
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
// Resolving $parameter is slow, especially on CI runner
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'getAll');
|
||||
// Previous nodes have run, input can be resolved
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr');
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday');
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().clear();
|
||||
WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
|
||||
WorkflowPage.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.type("$('Schedule Trigger').item.json.myStr");
|
||||
WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -181,11 +181,6 @@ describe('Data mapping', () => {
|
|||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
.should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`);
|
||||
ndv.getters
|
||||
.parameterExpressionPreview('value')
|
||||
.invoke('text')
|
||||
.invoke('replace', /\u00a0/g, ' ')
|
||||
.should('equal', '[ERROR: no data, execute "Schedule Trigger" node first]');
|
||||
|
||||
ndv.actions.switchInputMode('Table');
|
||||
ndv.actions.mapDataFromHeader(1, 'value');
|
||||
|
@ -195,7 +190,6 @@ describe('Data mapping', () => {
|
|||
'have.text',
|
||||
`{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }} {{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input }}`,
|
||||
);
|
||||
ndv.actions.validateExpressionPreview('value', ' ');
|
||||
|
||||
ndv.actions.selectInputNode('Set');
|
||||
|
||||
|
|
|
@ -1,62 +1,117 @@
|
|||
import { NDV } from '../pages/ndv';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
const ndv = new NDV();
|
||||
|
||||
describe('Expression editor modal', () => {
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.visit();
|
||||
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openExpressionEditorModal();
|
||||
|
||||
WorkflowPage.actions.addInitialNodeToCanvas('Schedule');
|
||||
cy.on('uncaught:exception', (err) => err.name !== 'ExpressionError');
|
||||
});
|
||||
|
||||
it('should resolve primitive resolvables', () => {
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ 1 + 2');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^3$/);
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
describe('Static data', () => {
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openExpressionEditorModal();
|
||||
});
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ "ab" + "cd"');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^abcd$/);
|
||||
it('should resolve primitive resolvables', () => {
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ 1 + 2');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^3$/);
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ "ab" + "cd"');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^abcd$/);
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ true && false');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^false$/);
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ true && false');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^false$/);
|
||||
});
|
||||
|
||||
it('should resolve object resolvables', () => {
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters
|
||||
.expressionModalInput()
|
||||
.type('{{ { a : 1 }', { parseSpecialCharSequences: false });
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^\[Object: \{"a": 1\}\]$/);
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
|
||||
WorkflowPage.getters
|
||||
.expressionModalInput()
|
||||
.type('{{ { a : 1 }.a', { parseSpecialCharSequences: false });
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^1$/);
|
||||
});
|
||||
|
||||
it('should resolve array resolvables', () => {
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3]');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^\[Array: \[1,2,3\]\]$/);
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3][0]');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^1$/);
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve object resolvables', () => {
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters
|
||||
.expressionModalInput()
|
||||
.type('{{ { a : 1 }', { parseSpecialCharSequences: false });
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^\[Object: \{"a": 1\}\]$/);
|
||||
describe('Dynamic data', () => {
|
||||
beforeEach(() => {
|
||||
WorkflowPage.actions.openNode('Schedule Trigger');
|
||||
ndv.actions.setPinnedData([{ myStr: 'Monday' }]);
|
||||
ndv.actions.close();
|
||||
WorkflowPage.actions.addNodeToCanvas('No Operation');
|
||||
WorkflowPage.actions.addNodeToCanvas('Hacker News');
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openExpressionEditorModal();
|
||||
});
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
it('should resolve $parameter[]', () => {
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]');
|
||||
WorkflowPage.getters.expressionModalOutput().should('have.text', 'getAll');
|
||||
});
|
||||
|
||||
WorkflowPage.getters
|
||||
.expressionModalInput()
|
||||
.type('{{ { a : 1 }.a', { parseSpecialCharSequences: false });
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^1$/);
|
||||
});
|
||||
it('should resolve input: $json,$input,$(nodeName)', () => {
|
||||
// Previous nodes have not run, input is empty
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ $json.myStr');
|
||||
WorkflowPage.getters
|
||||
.expressionModalOutput()
|
||||
.should('have.text', '[Execute previous nodes for preview]');
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ $input.item.json.myStr');
|
||||
WorkflowPage.getters
|
||||
.expressionModalOutput()
|
||||
.should('have.text', '[Execute previous nodes for preview]');
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type("{{ $('Schedule Trigger').item.json.myStr");
|
||||
WorkflowPage.getters
|
||||
.expressionModalOutput()
|
||||
.should('have.text', '[Execute previous nodes for preview]');
|
||||
|
||||
it('should resolve array resolvables', () => {
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3]');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^\[Array: \[1,2,3\]\]$/);
|
||||
// Run workflow
|
||||
cy.get('body').type('{esc}');
|
||||
ndv.actions.close();
|
||||
WorkflowPage.actions.executeNode('No Operation');
|
||||
WorkflowPage.actions.openNode('Hacker News');
|
||||
WorkflowPage.actions.openExpressionEditorModal();
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3][0]');
|
||||
WorkflowPage.getters.expressionModalOutput().contains(/^1$/);
|
||||
});
|
||||
|
||||
it('should resolve $parameter[]', () => {
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]');
|
||||
WorkflowPage.getters.expressionModalOutput().should('have.text', 'getAll');
|
||||
// Previous nodes have run, input can be resolved
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ $json.myStr');
|
||||
WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday');
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type('{{ $input.item.json.myStr');
|
||||
WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday');
|
||||
WorkflowPage.getters.expressionModalInput().clear();
|
||||
WorkflowPage.getters.expressionModalInput().type("{{ $('Schedule Trigger').item.json.myStr");
|
||||
WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -74,6 +74,8 @@
|
|||
--color-valid-resolvable-background: var(--prim-color-alt-a-alpha-025);
|
||||
--color-invalid-resolvable-foreground: var(--prim-color-alt-c-tint-250);
|
||||
--color-invalid-resolvable-background: var(--prim-color-alt-c-alpha-02);
|
||||
--color-pending-resolvable-foreground: var(--color-text-base);
|
||||
--color-pending-resolvable-background: var(--prim-gray-70-alpha-01);
|
||||
--color-expression-editor-background: var(--prim-gray-800);
|
||||
--color-expression-syntax-example: var(--prim-gray-670);
|
||||
|
||||
|
|
|
@ -107,6 +107,8 @@
|
|||
--color-valid-resolvable-background: var(--prim-color-alt-a-tint-500);
|
||||
--color-invalid-resolvable-foreground: var(--prim-color-alt-c);
|
||||
--color-invalid-resolvable-background: var(--prim-color-alt-c-tint-450);
|
||||
--color-pending-resolvable-foreground: var(--color-text-base);
|
||||
--color-pending-resolvable-background: var(--prim-gray-40);
|
||||
--color-expression-editor-background: var(--prim-gray-0);
|
||||
--color-expression-syntax-example: var(--prim-gray-40);
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
<InlineExpressionEditorOutput
|
||||
:segments="segments"
|
||||
:is-read-only="isReadOnly"
|
||||
:no-input-data="noInputData"
|
||||
:visible="isFocused"
|
||||
:hovering-item-number="hoveringItemNumber"
|
||||
/>
|
||||
|
@ -118,6 +119,9 @@ export default defineComponent({
|
|||
isDragging(): boolean {
|
||||
return this.ndvStore.isDraggableDragging;
|
||||
},
|
||||
noInputData(): boolean {
|
||||
return !this.ndvStore.hasInputData;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
focus() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div :class="visible ? $style.dropdown : $style.hidden">
|
||||
<n8n-text size="small" compact :class="$style.header">
|
||||
<n8n-text v-if="!noInputData" size="small" compact :class="$style.header">
|
||||
{{ i18n.baseText('parameterInput.resultForItem') }} {{ hoveringItemNumber }}
|
||||
</n8n-text>
|
||||
<n8n-text :class="$style.body">
|
||||
|
@ -57,6 +57,10 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
noInputData: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hoveringItemNumber: {
|
||||
type: Number,
|
||||
required: true,
|
||||
|
@ -169,6 +173,10 @@ export default defineComponent({
|
|||
padding-top: 0;
|
||||
padding-left: var(--spacing-2xs);
|
||||
color: var(--color-text-dark);
|
||||
|
||||
&:first-child {
|
||||
padding-top: var(--spacing-2xs);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
|
|
@ -110,6 +110,7 @@
|
|||
</template>
|
||||
<NodeExecuteButton
|
||||
type="secondary"
|
||||
hide-icon
|
||||
:transparent="true"
|
||||
:node-name="isActiveNodeConfig ? rootNode : currentNodeName"
|
||||
:label="$locale.baseText('ndv.input.noOutputData.executePrevious')"
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
:label="buttonLabel"
|
||||
:type="type"
|
||||
:size="size"
|
||||
:icon="!isListeningForEvents && 'flask'"
|
||||
:icon="!isListeningForEvents && !hideIcon && 'flask'"
|
||||
:transparent-background="transparent"
|
||||
:title="!isTriggerNode ? $locale.baseText('ndv.execute.testNode.description') : ''"
|
||||
@click="onClick"
|
||||
|
@ -72,6 +72,9 @@ export default defineComponent({
|
|||
telemetrySource: {
|
||||
type: String,
|
||||
},
|
||||
hideIcon: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
setup(props, ctx) {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
|
|
|
@ -67,11 +67,11 @@ import type {
|
|||
} from 'n8n-workflow';
|
||||
import { isResourceLocatorValue } from 'n8n-workflow';
|
||||
|
||||
import { get } from 'lodash-es';
|
||||
import type { EventBus } from 'n8n-design-system/utils';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { getExpressionErrorMessage, getResolvableState } from '@/utils/expressions';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ParameterInputWrapper',
|
||||
|
@ -230,9 +230,12 @@ export default defineComponent({
|
|||
const evaluated = this.evaluatedExpression;
|
||||
|
||||
if (!evaluated.ok) {
|
||||
return `[${this.$locale.baseText('parameterInput.error')}: ${get(
|
||||
evaluated.error,
|
||||
'message',
|
||||
if (getResolvableState(evaluated.error) !== 'invalid') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `[${this.$locale.baseText('parameterInput.error')}: ${getExpressionErrorMessage(
|
||||
evaluated.error as Error,
|
||||
)}]`;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { ensureSyntaxTree } from '@codemirror/language';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import { Expression, ExpressionExtensions } from 'n8n-workflow';
|
||||
import { ensureSyntaxTree } from '@codemirror/language';
|
||||
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { EXPRESSION_EDITOR_PARSER_TIMEOUT } from '@/constants';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import type { TargetItem } from '@/Interface';
|
||||
import type { Html, Plaintext, RawSegment, Resolvable, Segment } from '@/types/expressions';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { Html, Plaintext, RawSegment, Resolvable, Segment } from '@/types/expressions';
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { getExpressionErrorMessage, getResolvableState } from '@/utils/expressions';
|
||||
|
||||
export const expressionManager = defineComponent({
|
||||
props: {
|
||||
|
@ -32,18 +34,8 @@ export const expressionManager = defineComponent({
|
|||
editorState: undefined,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
targetItem() {
|
||||
setTimeout(() => {
|
||||
this.$emit('change', {
|
||||
value: this.unresolvedExpression,
|
||||
segments: this.displayableSegments,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNDVStore),
|
||||
...mapStores(useNDVStore, useWorkflowsStore),
|
||||
|
||||
unresolvedExpression(): string {
|
||||
return this.segments.reduce((acc, segment) => {
|
||||
|
@ -116,7 +108,7 @@ export const expressionManager = defineComponent({
|
|||
const { from, to, text, token } = segment;
|
||||
|
||||
if (token === 'Resolvable') {
|
||||
const { resolved, error, fullError } = this.resolve(text, this.hoveringItem);
|
||||
const { resolved, fullError } = this.resolve(text, this.hoveringItem);
|
||||
|
||||
acc.push({
|
||||
kind: 'resolvable',
|
||||
|
@ -127,8 +119,8 @@ export const expressionManager = defineComponent({
|
|||
// For some reason, expressions that resolve to a number 0 are breaking preview in the SQL editor
|
||||
// This fixes that but as as TODO we should figure out why this is happening
|
||||
resolved: String(resolved),
|
||||
error,
|
||||
fullError,
|
||||
state: getResolvableState(fullError),
|
||||
error: fullError,
|
||||
});
|
||||
|
||||
return acc;
|
||||
|
@ -188,6 +180,16 @@ export const expressionManager = defineComponent({
|
|||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
targetItem() {
|
||||
setTimeout(() => {
|
||||
this.$emit('change', {
|
||||
value: this.unresolvedExpression,
|
||||
segments: this.displayableSegments,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isEmptyExpression(resolvable: string) {
|
||||
return /\{\{\s*\}\}/.test(resolvable);
|
||||
|
@ -220,7 +222,7 @@ export const expressionManager = defineComponent({
|
|||
result.resolved = workflowHelpers.resolveExpression('=' + resolvable, undefined, opts);
|
||||
}
|
||||
} catch (error) {
|
||||
result.resolved = `[${error.message}]`;
|
||||
result.resolved = `[${getExpressionErrorMessage(error)}]`;
|
||||
result.error = true;
|
||||
result.fullError = error;
|
||||
}
|
||||
|
|
|
@ -378,7 +378,9 @@ export const pushConnection = defineComponent({
|
|||
|
||||
if (
|
||||
error.context.nodeCause &&
|
||||
['no pairing info', 'invalid pairing info'].includes(error.context.type as string)
|
||||
['paired_item_no_info', 'paired_item_invalid_info'].includes(
|
||||
error.context.type as string,
|
||||
)
|
||||
) {
|
||||
const node = workflow.getNode(error.context.nodeCause as string);
|
||||
|
||||
|
|
|
@ -4,11 +4,17 @@ import { StateField, StateEffect } from '@codemirror/state';
|
|||
import { tags } from '@lezer/highlight';
|
||||
import { syntaxHighlighting, HighlightStyle } from '@codemirror/language';
|
||||
|
||||
import type { ColoringStateEffect, Plaintext, Resolvable } from '@/types/expressions';
|
||||
import type {
|
||||
ColoringStateEffect,
|
||||
Plaintext,
|
||||
Resolvable,
|
||||
ResolvableState,
|
||||
} from '@/types/expressions';
|
||||
|
||||
const cssClasses = {
|
||||
validResolvable: 'cm-valid-resolvable',
|
||||
invalidResolvable: 'cm-invalid-resolvable',
|
||||
pendingResolvable: 'cm-pending-resolvable',
|
||||
brokenResolvable: 'cm-broken-resolvable',
|
||||
plaintext: 'cm-plaintext',
|
||||
};
|
||||
|
@ -22,20 +28,25 @@ const resolvablesTheme = EditorView.theme({
|
|||
color: 'var(--color-invalid-resolvable-foreground)',
|
||||
backgroundColor: 'var(--color-invalid-resolvable-background)',
|
||||
},
|
||||
['.' + cssClasses.pendingResolvable]: {
|
||||
color: 'var(--color-pending-resolvable-foreground)',
|
||||
backgroundColor: 'var(--color-pending-resolvable-background)',
|
||||
},
|
||||
});
|
||||
|
||||
const marks = {
|
||||
const resolvableStateToDecoration: Record<ResolvableState, Decoration> = {
|
||||
valid: Decoration.mark({ class: cssClasses.validResolvable }),
|
||||
invalid: Decoration.mark({ class: cssClasses.invalidResolvable }),
|
||||
pending: Decoration.mark({ class: cssClasses.pendingResolvable }),
|
||||
};
|
||||
|
||||
const coloringStateEffects = {
|
||||
addColorEffect: StateEffect.define<ColoringStateEffect.Value>({
|
||||
map: ({ from, to, kind, error }, change) => ({
|
||||
map: ({ from, to, kind, state }, change) => ({
|
||||
from: change.mapPos(from),
|
||||
to: change.mapPos(to),
|
||||
kind,
|
||||
error,
|
||||
state,
|
||||
}),
|
||||
}),
|
||||
removeColorEffect: StateEffect.define<ColoringStateEffect.Value>({
|
||||
|
@ -66,7 +77,7 @@ const coloringStateField = StateField.define<DecorationSet>({
|
|||
filter: (from, to) => txEffect.value.from !== from && txEffect.value.to !== to,
|
||||
});
|
||||
|
||||
const decoration = txEffect.value.error ? marks.invalid : marks.valid;
|
||||
const decoration = resolvableStateToDecoration[txEffect.value.state ?? 'pending'];
|
||||
|
||||
if (txEffect.value.from === 0 && txEffect.value.to === 0) continue;
|
||||
|
||||
|
@ -81,8 +92,8 @@ const coloringStateField = StateField.define<DecorationSet>({
|
|||
});
|
||||
|
||||
function addColor(view: EditorView, segments: Resolvable[]) {
|
||||
const effects: Array<StateEffect<unknown>> = segments.map(({ from, to, kind, error }) =>
|
||||
coloringStateEffects.addColorEffect.of({ from, to, kind, error }),
|
||||
const effects: Array<StateEffect<unknown>> = segments.map(({ from, to, kind, state }) =>
|
||||
coloringStateEffects.addColorEffect.of({ from, to, kind, state }),
|
||||
);
|
||||
|
||||
if (effects.length === 0) return;
|
||||
|
|
|
@ -662,6 +662,12 @@
|
|||
"expressionModalInput.empty": "[empty]",
|
||||
"expressionModalInput.undefined": "[undefined]",
|
||||
"expressionModalInput.null": "null",
|
||||
"expressionModalInput.noExecutionData": "Execute previous nodes for preview",
|
||||
"expressionModalInput.noNodeExecutionData": "Execute node ‘{node}’ for preview",
|
||||
"expressionModalInput.noInputConnection": "No input connected",
|
||||
"expressionModalInput.pairedItemConnectionError": "No path back to node",
|
||||
"expressionModalInput.pairedItemInvalidPinnedError": "Unpin node ‘{node}’ and execute",
|
||||
"expressionModalInput.pairedItemError": "Can’t determine which item to use",
|
||||
"fakeDoor.settings.environments.name": "Environments",
|
||||
"fakeDoor.settings.sso.name": "Single Sign-On",
|
||||
"fakeDoor.settings.sso.actionBox.title": "We’re working on this (as a paid feature)",
|
||||
|
@ -780,7 +786,7 @@
|
|||
"ndv.input.noOutputDataInBranch": "No input data in this branch",
|
||||
"ndv.input.noOutputDataInNode": "Node did not output any data. n8n stops executing the workflow when a node has no output data.",
|
||||
"ndv.input.noOutputData": "No data",
|
||||
"ndv.input.noOutputData.executePrevious": "Test previous nodes",
|
||||
"ndv.input.noOutputData.executePrevious": "Execute previous nodes",
|
||||
"ndv.input.noOutputData.title": "No input data yet",
|
||||
"ndv.input.noOutputData.hint": "(From the earliest node that has no output data yet)",
|
||||
"ndv.input.executingPrevious": "Executing previous nodes...",
|
||||
|
|
|
@ -75,6 +75,10 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
|||
return executionData.data?.resultData?.runData?.[inputNodeName]?.[inputRunIndex]?.data
|
||||
?.main?.[inputBranchIndex];
|
||||
},
|
||||
hasInputData(): boolean {
|
||||
const data = this.ndvInputData;
|
||||
return data && data.length > 0;
|
||||
},
|
||||
getPanelDisplayMode() {
|
||||
return (panel: NodePanelType) => this[panel].displayMode;
|
||||
},
|
||||
|
|
|
@ -8,12 +8,14 @@ export type Plaintext = { kind: 'plaintext'; plaintext: string } & Range;
|
|||
|
||||
export type Html = Plaintext; // for n8n parser, functionally identical to plaintext
|
||||
|
||||
export type ResolvableState = 'valid' | 'invalid' | 'pending';
|
||||
|
||||
export type Resolvable = {
|
||||
kind: 'resolvable';
|
||||
resolvable: string;
|
||||
resolved: unknown;
|
||||
error: boolean;
|
||||
fullError: Error | null;
|
||||
state: ResolvableState;
|
||||
error: Error | null;
|
||||
} & Range;
|
||||
|
||||
export type Resolved = Resolvable;
|
||||
|
@ -21,6 +23,6 @@ export type Resolved = Resolvable;
|
|||
export namespace ColoringStateEffect {
|
||||
export type Value = {
|
||||
kind?: 'plaintext' | 'resolvable';
|
||||
error?: boolean;
|
||||
state?: ResolvableState;
|
||||
} & Range;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { ExpressionParser } from 'n8n-workflow';
|
||||
import type { ResolvableState } from '@/types/expressions';
|
||||
import { ExpressionError, ExpressionParser } from 'n8n-workflow';
|
||||
import { i18n } from '@/plugins/i18n';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
|
||||
export const isExpression = (expr: unknown) => {
|
||||
if (typeof expr !== 'string') return false;
|
||||
|
@ -13,3 +16,88 @@ export const isTestableExpression = (expr: string) => {
|
|||
return /\$secrets(\.[a-zA-Z0-9_]+)+$/.test(c.text.trim());
|
||||
});
|
||||
};
|
||||
|
||||
export const isNoExecDataExpressionError = (error: unknown): error is ExpressionError => {
|
||||
return error instanceof ExpressionError && error.context.type === 'no_execution_data';
|
||||
};
|
||||
|
||||
export const isNoNodeExecDataExpressionError = (error: unknown): error is ExpressionError => {
|
||||
return error instanceof ExpressionError && error.context.type === 'no_node_execution_data';
|
||||
};
|
||||
|
||||
export const isPairedItemIntermediateNodesError = (error: unknown): error is ExpressionError => {
|
||||
return (
|
||||
error instanceof ExpressionError && error.context.type === 'paired_item_intermediate_nodes'
|
||||
);
|
||||
};
|
||||
|
||||
export const isPairedItemNoConnectionError = (error: unknown): error is ExpressionError => {
|
||||
return error instanceof ExpressionError && error.context.type === 'paired_item_no_connection';
|
||||
};
|
||||
|
||||
export const isInvalidPairedItemError = (error: unknown): error is ExpressionError => {
|
||||
return error instanceof ExpressionError && error.context.type === 'paired_item_invalid_info';
|
||||
};
|
||||
|
||||
export const isNoPairedItemError = (error: unknown): error is ExpressionError => {
|
||||
return error instanceof ExpressionError && error.context.type === 'paired_item_no_info';
|
||||
};
|
||||
|
||||
export const isNoInputConnectionError = (error: unknown): error is ExpressionError => {
|
||||
return error instanceof ExpressionError && error.context.type === 'no_input_connection';
|
||||
};
|
||||
|
||||
export const isAnyPairedItemError = (error: unknown): error is ExpressionError => {
|
||||
return error instanceof ExpressionError && error.context.functionality === 'pairedItem';
|
||||
};
|
||||
|
||||
export const getResolvableState = (error: unknown): ResolvableState => {
|
||||
if (!error) return 'valid';
|
||||
|
||||
if (
|
||||
isNoExecDataExpressionError(error) ||
|
||||
isNoNodeExecDataExpressionError(error) ||
|
||||
isPairedItemIntermediateNodesError(error)
|
||||
) {
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
return 'invalid';
|
||||
};
|
||||
|
||||
export const getExpressionErrorMessage = (error: Error): string => {
|
||||
if (isNoExecDataExpressionError(error) || isPairedItemIntermediateNodesError(error)) {
|
||||
return i18n.baseText('expressionModalInput.noExecutionData');
|
||||
}
|
||||
|
||||
if (isNoNodeExecDataExpressionError(error)) {
|
||||
const nodeCause = error.context.nodeCause as string;
|
||||
return i18n.baseText('expressionModalInput.noNodeExecutionData', {
|
||||
interpolate: { node: nodeCause },
|
||||
});
|
||||
}
|
||||
if (isNoInputConnectionError(error)) {
|
||||
return i18n.baseText('expressionModalInput.noInputConnection');
|
||||
}
|
||||
|
||||
if (isPairedItemNoConnectionError(error)) {
|
||||
return i18n.baseText('expressionModalInput.pairedItemConnectionError');
|
||||
}
|
||||
|
||||
if (isInvalidPairedItemError(error) || isNoPairedItemError(error)) {
|
||||
const nodeCause = error.context.nodeCause as string;
|
||||
const isPinned = !!useWorkflowsStore().pinDataByNodeName(nodeCause);
|
||||
|
||||
if (isPinned) {
|
||||
return i18n.baseText('expressionModalInput.pairedItemInvalidPinnedError', {
|
||||
interpolate: { node: nodeCause },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isAnyPairedItemError(error)) {
|
||||
return i18n.baseText('expressionModalInput.pairedItemError');
|
||||
}
|
||||
|
||||
return error.message;
|
||||
};
|
||||
|
|
|
@ -2584,6 +2584,6 @@ export type BannerName =
|
|||
| 'NON_PRODUCTION_LICENSE'
|
||||
| 'EMAIL_CONFIRMATION';
|
||||
|
||||
export type Functionality = 'regular' | 'configuration-node';
|
||||
export type Functionality = 'regular' | 'configuration-node' | 'pairedItem';
|
||||
|
||||
export type Result<T, E> = { ok: true; result: T } | { ok: false; error: E };
|
||||
|
|
|
@ -22,7 +22,7 @@ import type {
|
|||
ProxyInput,
|
||||
} from './Interfaces';
|
||||
import * as NodeHelpers from './NodeHelpers';
|
||||
import { ExpressionError } from './errors/expression.error';
|
||||
import { ExpressionError, type ExpressionErrorOptions } from './errors/expression.error';
|
||||
import type { Workflow } from './Workflow';
|
||||
import { augmentArray, augmentObject } from './AugmentObject';
|
||||
import { deepCopy } from './utils';
|
||||
|
@ -104,6 +104,7 @@ export class WorkflowDataProxy {
|
|||
{
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_execution_data',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -274,19 +275,25 @@ export class WorkflowDataProxy {
|
|||
);
|
||||
}
|
||||
|
||||
if (!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
|
||||
if (that.workflow.getNode(nodeName)) {
|
||||
throw new ExpressionError(`no data, execute "${nodeName}" node first`, {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
});
|
||||
}
|
||||
if (!that.workflow.getNode(nodeName)) {
|
||||
throw new ExpressionError(`"${nodeName}" node doesn't exist`, {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName) &&
|
||||
!that.workflow.getPinDataOfNode(nodeName)
|
||||
) {
|
||||
throw new ExpressionError(`no data, execute "${nodeName}" node first`, {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_node_execution_data',
|
||||
nodeCause: nodeName,
|
||||
});
|
||||
}
|
||||
|
||||
runIndex = runIndex === undefined ? that.defaultReturnRunIndex : runIndex;
|
||||
runIndex =
|
||||
runIndex === -1 ? that.runExecutionData.resultData.runData[nodeName].length - 1 : runIndex;
|
||||
|
@ -372,6 +379,28 @@ export class WorkflowDataProxy {
|
|||
|
||||
if (['binary', 'data', 'json'].includes(name)) {
|
||||
const executionData = that.getNodeExecutionData(nodeName, shortSyntax, undefined);
|
||||
|
||||
if (executionData.length === 0) {
|
||||
if (that.workflow.getParentNodes(nodeName).length === 0) {
|
||||
throw new ExpressionError('No execution data available', {
|
||||
messageTemplate:
|
||||
'No execution data available to expression under ‘%%PARAMETER%%’',
|
||||
description:
|
||||
'This node has no input data. Please make sure this node is connected to another node.',
|
||||
nodeCause: nodeName,
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_input_connection',
|
||||
});
|
||||
}
|
||||
|
||||
throw new ExpressionError('No execution data available', {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_execution_data',
|
||||
});
|
||||
}
|
||||
|
||||
if (executionData.length <= that.itemIndex) {
|
||||
throw new ExpressionError(`No data found for item-index: "${that.itemIndex}"`, {
|
||||
runIndex: that.runIndex,
|
||||
|
@ -615,22 +644,13 @@ export class WorkflowDataProxy {
|
|||
|
||||
const createExpressionError = (
|
||||
message: string,
|
||||
context?: {
|
||||
causeDetailed?: string;
|
||||
description?: string;
|
||||
descriptionTemplate?: string;
|
||||
functionality?: 'pairedItem';
|
||||
context?: ExpressionErrorOptions & {
|
||||
moreInfoLink?: boolean;
|
||||
functionOverrides?: {
|
||||
// Custom data to display for Function-Nodes
|
||||
message?: string;
|
||||
description?: string;
|
||||
};
|
||||
itemIndex?: number;
|
||||
messageTemplate?: string;
|
||||
moreInfoLink?: boolean;
|
||||
nodeCause?: string;
|
||||
runIndex?: number;
|
||||
type?: string;
|
||||
},
|
||||
) => {
|
||||
if (isScriptingNode(that.activeNodeName, that.workflow) && context?.functionOverrides) {
|
||||
|
@ -678,7 +698,7 @@ export class WorkflowDataProxy {
|
|||
incomingSourceData: ISourceData | null,
|
||||
pairedItem: IPairedItemData,
|
||||
): INodeExecutionData | null => {
|
||||
let taskData: ITaskData;
|
||||
let taskData: ITaskData | undefined;
|
||||
|
||||
let sourceData: ISourceData | null = incomingSourceData;
|
||||
|
||||
|
@ -697,13 +717,12 @@ export class WorkflowDataProxy {
|
|||
let nodeBeforeLast: string | undefined;
|
||||
|
||||
while (sourceData !== null && destinationNodeName !== sourceData.previousNode) {
|
||||
taskData =
|
||||
that.runExecutionData!.resultData.runData[sourceData.previousNode][
|
||||
sourceData?.previousNodeRun || 0
|
||||
];
|
||||
|
||||
const runIndex = sourceData?.previousNodeRun || 0;
|
||||
const previousNodeOutput = sourceData.previousNodeOutput || 0;
|
||||
if (previousNodeOutput >= taskData.data!.main.length) {
|
||||
taskData =
|
||||
that.runExecutionData?.resultData?.runData?.[sourceData.previousNode]?.[runIndex];
|
||||
|
||||
if (taskData?.data?.main && previousNodeOutput >= taskData.data.main.length) {
|
||||
throw createExpressionError('Can’t get data for expression', {
|
||||
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
|
||||
functionOverrides: {
|
||||
|
@ -716,7 +735,12 @@ export class WorkflowDataProxy {
|
|||
});
|
||||
}
|
||||
|
||||
if (pairedItem.item >= taskData.data!.main[previousNodeOutput]!.length) {
|
||||
const previousNodeOutputData =
|
||||
taskData?.data?.main?.[previousNodeOutput] ??
|
||||
(that.workflow.getPinDataOfNode(sourceData.previousNode) as INodeExecutionData[]);
|
||||
const source = taskData?.source ?? [];
|
||||
|
||||
if (pairedItem.item >= previousNodeOutputData.length) {
|
||||
throw createExpressionError('Can’t get data for expression', {
|
||||
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
|
||||
functionality: 'pairedItem',
|
||||
|
@ -733,13 +757,12 @@ export class WorkflowDataProxy {
|
|||
}points to an input item on node ‘<strong>${
|
||||
sourceData.previousNode
|
||||
}</strong>‘ that doesn’t exist.`,
|
||||
type: 'invalid pairing info',
|
||||
type: 'paired_item_invalid_info',
|
||||
moreInfoLink: true,
|
||||
});
|
||||
}
|
||||
|
||||
const itemPreviousNode: INodeExecutionData =
|
||||
taskData.data!.main[previousNodeOutput]![pairedItem.item];
|
||||
const itemPreviousNode: INodeExecutionData = previousNodeOutputData[pairedItem.item];
|
||||
|
||||
if (itemPreviousNode.pairedItem === undefined) {
|
||||
throw createExpressionError('Can’t get data for expression', {
|
||||
|
@ -751,7 +774,7 @@ export class WorkflowDataProxy {
|
|||
nodeCause: sourceData.previousNode,
|
||||
description: `To fetch the data from other nodes that this expression needs, more information is needed from the node ‘<strong>${sourceData.previousNode}</strong>’`,
|
||||
causeDetailed: `Missing pairedItem data (node ‘${sourceData.previousNode}’ probably didn’t supply it)`,
|
||||
type: 'no pairing info',
|
||||
type: 'paired_item_no_info',
|
||||
moreInfoLink: true,
|
||||
});
|
||||
}
|
||||
|
@ -763,13 +786,13 @@ export class WorkflowDataProxy {
|
|||
.map((item) => {
|
||||
try {
|
||||
const itemInput = item.input || 0;
|
||||
if (itemInput >= taskData.source.length) {
|
||||
if (itemInput >= source.length) {
|
||||
// `Could not resolve pairedItem as the defined node input '${itemInput}' does not exist on node '${sourceData!.previousNode}'.`
|
||||
// Actual error does not matter as it gets caught below and `null` will be returned
|
||||
throw new ApplicationError('Not found');
|
||||
}
|
||||
|
||||
return getPairedItem(destinationNodeName, taskData.source[itemInput], item);
|
||||
return getPairedItem(destinationNodeName, source[itemInput], item);
|
||||
} catch (error) {
|
||||
// Means pairedItem could not be found
|
||||
return null;
|
||||
|
@ -793,7 +816,7 @@ export class WorkflowDataProxy {
|
|||
message: 'Invalid code',
|
||||
},
|
||||
description: `The expression uses data in the node ‘<strong>${destinationNodeName}</strong>’ but there is more than one matching item in that node`,
|
||||
type: 'multiple matches',
|
||||
type: 'paired_item_multiple_matches',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -812,8 +835,8 @@ export class WorkflowDataProxy {
|
|||
}
|
||||
|
||||
const itemInput = pairedItem.input || 0;
|
||||
if (itemInput >= taskData.source.length) {
|
||||
if (taskData.source.length === 0) {
|
||||
if (itemInput >= source.length) {
|
||||
if (source.length === 0) {
|
||||
// A trigger node got reached, so looks like that that item can not be resolved
|
||||
throw createExpressionError('Invalid expression', {
|
||||
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
||||
|
@ -823,7 +846,7 @@ export class WorkflowDataProxy {
|
|||
message: 'Invalid code',
|
||||
},
|
||||
description: `The expression uses data in the node ‘<strong>${destinationNodeName}</strong>’ but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
|
||||
type: 'no connection',
|
||||
type: 'paired_item_no_connection',
|
||||
moreInfoLink: true,
|
||||
});
|
||||
}
|
||||
|
@ -841,12 +864,12 @@ export class WorkflowDataProxy {
|
|||
? `of run ${(sourceData.previousNodeRun || 0).toString()} `
|
||||
: ''
|
||||
}points to a branch that doesn’t exist.`,
|
||||
type: 'invalid pairing info',
|
||||
type: 'paired_item_invalid_info',
|
||||
});
|
||||
}
|
||||
|
||||
nodeBeforeLast = sourceData.previousNode;
|
||||
sourceData = taskData.source[pairedItem.input || 0] || null;
|
||||
sourceData = source[pairedItem.input || 0] || null;
|
||||
|
||||
if (pairedItem.sourceOverwrite) {
|
||||
sourceData = pairedItem.sourceOverwrite;
|
||||
|
@ -862,7 +885,7 @@ export class WorkflowDataProxy {
|
|||
},
|
||||
nodeCause: nodeBeforeLast,
|
||||
description: 'Could not resolve, probably no pairedItem exists',
|
||||
type: 'no pairing info',
|
||||
type: 'paired_item_no_info',
|
||||
moreInfoLink: true,
|
||||
});
|
||||
}
|
||||
|
@ -882,7 +905,7 @@ export class WorkflowDataProxy {
|
|||
},
|
||||
description: 'Item points to a node output which does not exist',
|
||||
causeDetailed: `The sourceData points to a node output ‘${previousNodeOutput}‘ which does not exist on node ‘${sourceData.previousNode}‘ (output node did probably supply a wrong one)`,
|
||||
type: 'invalid pairing info',
|
||||
type: 'paired_item_invalid_info',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -903,7 +926,7 @@ export class WorkflowDataProxy {
|
|||
}points to an input item on node ‘<strong>${
|
||||
sourceData.previousNode
|
||||
}</strong>‘ that doesn’t exist.`,
|
||||
type: 'invalid pairing info',
|
||||
type: 'paired_item_invalid_info',
|
||||
moreInfoLink: true,
|
||||
});
|
||||
}
|
||||
|
@ -922,6 +945,18 @@ export class WorkflowDataProxy {
|
|||
throw createExpressionError(`"${nodeName}" node doesn't exist`);
|
||||
}
|
||||
|
||||
const ensureNodeExecutionData = () => {
|
||||
if (
|
||||
!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) &&
|
||||
!that.workflow.getPinDataOfNode(nodeName)
|
||||
) {
|
||||
throw createExpressionError(`no data, execute "${nodeName}" node first`, {
|
||||
type: 'no_node_execution_data',
|
||||
nodeCause: nodeName,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
|
@ -942,13 +977,38 @@ export class WorkflowDataProxy {
|
|||
get(target, property, receiver) {
|
||||
if (property === 'isProxy') return true;
|
||||
|
||||
if (!that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName)) {
|
||||
if (property === 'isExecuted') return false;
|
||||
throw createExpressionError(`no data, execute "${nodeName}" node first`);
|
||||
if (property === 'isExecuted') {
|
||||
return (
|
||||
that?.runExecutionData?.resultData?.runData.hasOwnProperty(nodeName) ?? false
|
||||
);
|
||||
}
|
||||
if (property === 'isExecuted') return true;
|
||||
|
||||
if (['pairedItem', 'itemMatching', 'item'].includes(property as string)) {
|
||||
// Before resolving the pairedItem make sure that the requested node comes in the
|
||||
// graph before the current one
|
||||
const activeNode = that.workflow.getNode(that.activeNodeName);
|
||||
let contextNode = that.contextNodeName;
|
||||
if (activeNode) {
|
||||
const parentMainInputNode = that.workflow.getParentMainInputNode(activeNode);
|
||||
contextNode = parentMainInputNode.name ?? contextNode;
|
||||
}
|
||||
const parentNodes = that.workflow.getParentNodes(contextNode);
|
||||
if (!parentNodes.includes(nodeName)) {
|
||||
throw createExpressionError('Invalid expression', {
|
||||
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
||||
functionality: 'pairedItem',
|
||||
functionOverrides: {
|
||||
description: `The code uses data in the node <strong>‘${nodeName}’</strong> but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
|
||||
message: `No path back to node ‘${nodeName}’`,
|
||||
},
|
||||
description: `The expression uses data in the node <strong>‘${nodeName}’</strong> but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
|
||||
nodeCause: nodeName,
|
||||
type: 'paired_item_no_connection',
|
||||
});
|
||||
}
|
||||
|
||||
ensureNodeExecutionData();
|
||||
|
||||
const pairedItemMethod = (itemIndex?: number) => {
|
||||
if (itemIndex === undefined) {
|
||||
if (property === 'itemMatching') {
|
||||
|
@ -960,11 +1020,26 @@ export class WorkflowDataProxy {
|
|||
}
|
||||
|
||||
const executionData = that.connectionInputData;
|
||||
const input = executionData[itemIndex];
|
||||
if (!input) {
|
||||
throw createExpressionError('Can’t get data for expression', {
|
||||
messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
|
||||
functionality: 'pairedItem',
|
||||
functionOverrides: {
|
||||
description: `Some intermediate nodes between ‘<strong>${nodeName}</strong>‘ and ‘<strong>${that.activeNodeName}</strong>‘ have not executed yet.`,
|
||||
message: 'Can’t get data',
|
||||
},
|
||||
description: `Some intermediate nodes between ‘<strong>${nodeName}</strong>‘ and ‘<strong>${that.activeNodeName}</strong>‘ have not executed yet.`,
|
||||
causeDetailed: `pairedItem can\'t be found when intermediate nodes between ‘<strong>${nodeName}</strong>‘ and ‘<strong>${that.activeNodeName}</strong> have not executed yet.`,
|
||||
itemIndex,
|
||||
type: 'paired_item_intermediate_nodes',
|
||||
});
|
||||
}
|
||||
|
||||
// As we operate on the incoming item we can be sure that pairedItem is not an
|
||||
// array. After all can it only come from exactly one previous node via a certain
|
||||
// input. For that reason do we not have to consider the array case.
|
||||
const pairedItem = executionData[itemIndex].pairedItem as IPairedItemData;
|
||||
const pairedItem = input.pairedItem as IPairedItemData;
|
||||
|
||||
if (pairedItem === undefined) {
|
||||
throw createExpressionError('Can’t get data for expression', {
|
||||
|
@ -994,28 +1069,6 @@ export class WorkflowDataProxy {
|
|||
});
|
||||
}
|
||||
|
||||
// Before resolving the pairedItem make sure that the requested node comes in the
|
||||
// graph before the current one
|
||||
const activeNode = that.workflow.getNode(that.activeNodeName);
|
||||
let contextNode = that.contextNodeName;
|
||||
if (activeNode) {
|
||||
const parentMainInputNode = that.workflow.getParentMainInputNode(activeNode);
|
||||
contextNode = parentMainInputNode.name ?? contextNode;
|
||||
}
|
||||
const parentNodes = that.workflow.getParentNodes(contextNode);
|
||||
if (!parentNodes.includes(nodeName)) {
|
||||
throw createExpressionError('Invalid expression', {
|
||||
messageTemplate: 'Invalid expression under ‘%%PARAMETER%%’',
|
||||
functionality: 'pairedItem',
|
||||
functionOverrides: {
|
||||
description: `The code uses data in the node <strong>‘${nodeName}’</strong> but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
|
||||
message: `No path back to node ‘${nodeName}’`,
|
||||
},
|
||||
description: `The expression uses data in the node <strong>‘${nodeName}’</strong> but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
|
||||
itemIndex,
|
||||
});
|
||||
}
|
||||
|
||||
const sourceData: ISourceData | null =
|
||||
that.executeData.source.main[pairedItem.input || 0] ??
|
||||
that.executeData.source.main[0];
|
||||
|
@ -1029,6 +1082,7 @@ export class WorkflowDataProxy {
|
|||
return pairedItemMethod;
|
||||
}
|
||||
if (property === 'first') {
|
||||
ensureNodeExecutionData();
|
||||
return (branchIndex?: number, runIndex?: number) => {
|
||||
const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
|
||||
if (executionData[0]) return executionData[0];
|
||||
|
@ -1036,6 +1090,7 @@ export class WorkflowDataProxy {
|
|||
};
|
||||
}
|
||||
if (property === 'last') {
|
||||
ensureNodeExecutionData();
|
||||
return (branchIndex?: number, runIndex?: number) => {
|
||||
const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
|
||||
if (!executionData.length) return undefined;
|
||||
|
@ -1046,6 +1101,7 @@ export class WorkflowDataProxy {
|
|||
};
|
||||
}
|
||||
if (property === 'all') {
|
||||
ensureNodeExecutionData();
|
||||
return (branchIndex?: number, runIndex?: number) =>
|
||||
getNodeOutput(nodeName, branchIndex, runIndex);
|
||||
}
|
||||
|
@ -1075,6 +1131,14 @@ export class WorkflowDataProxy {
|
|||
get(target, property, receiver) {
|
||||
if (property === 'isProxy') return true;
|
||||
|
||||
if (that.connectionInputData.length === 0) {
|
||||
throw createExpressionError('No execution data available', {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
type: 'no_execution_data',
|
||||
});
|
||||
}
|
||||
|
||||
if (property === 'item') {
|
||||
return that.connectionInputData[that.itemIndex];
|
||||
}
|
||||
|
|
|
@ -1,26 +1,34 @@
|
|||
import type { IDataObject } from '../Interfaces';
|
||||
import { ExecutionBaseError } from './abstract/execution-base.error';
|
||||
|
||||
export interface ExpressionErrorOptions {
|
||||
cause?: Error;
|
||||
causeDetailed?: string;
|
||||
description?: string;
|
||||
descriptionTemplate?: string;
|
||||
functionality?: 'pairedItem';
|
||||
itemIndex?: number;
|
||||
messageTemplate?: string;
|
||||
nodeCause?: string;
|
||||
parameter?: string;
|
||||
runIndex?: number;
|
||||
type?:
|
||||
| 'no_execution_data'
|
||||
| 'no_node_execution_data'
|
||||
| 'no_input_connection'
|
||||
| 'internal'
|
||||
| 'paired_item_invalid_info'
|
||||
| 'paired_item_no_info'
|
||||
| 'paired_item_multiple_matches'
|
||||
| 'paired_item_no_connection'
|
||||
| 'paired_item_intermediate_nodes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for instantiating an expression error
|
||||
*/
|
||||
export class ExpressionError extends ExecutionBaseError {
|
||||
constructor(
|
||||
message: string,
|
||||
options?: {
|
||||
cause?: Error;
|
||||
causeDetailed?: string;
|
||||
description?: string;
|
||||
descriptionTemplate?: string;
|
||||
functionality?: 'pairedItem';
|
||||
itemIndex?: number;
|
||||
messageTemplate?: string;
|
||||
nodeCause?: string;
|
||||
parameter?: string;
|
||||
runIndex?: number;
|
||||
type?: string;
|
||||
},
|
||||
) {
|
||||
constructor(message: string, options?: ExpressionErrorOptions) {
|
||||
super(message, { cause: options?.cause });
|
||||
|
||||
if (options?.description !== undefined) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { INodeExecutionData } from '@/Interfaces';
|
|||
import { extendSyntax } from '@/Extensions/ExpressionExtension';
|
||||
import { ExpressionError } from '@/errors/expression.error';
|
||||
import { setDifferEnabled, setEvaluator } from '@/ExpressionEvaluatorProxy';
|
||||
import { workflow } from './ExpressionExtensions/Helpers';
|
||||
|
||||
setDifferEnabled(true);
|
||||
|
||||
|
@ -172,24 +173,6 @@ for (const evaluator of ['tmpl', 'tournament'] as const) {
|
|||
});
|
||||
|
||||
describe('Test all expression value fixtures', () => {
|
||||
const nodeTypes = Helpers.NodeTypes();
|
||||
const workflow = new Workflow({
|
||||
id: '1',
|
||||
nodes: [
|
||||
{
|
||||
name: 'node',
|
||||
typeVersion: 1,
|
||||
type: 'test.set',
|
||||
id: 'uuid-1234',
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
|
||||
const expression = workflow.expression;
|
||||
|
||||
const evaluate = (value: string, data: INodeExecutionData[]) => {
|
||||
|
@ -202,12 +185,18 @@ for (const evaluator of ['tmpl', 'tournament'] as const) {
|
|||
continue;
|
||||
}
|
||||
test(t.expression, () => {
|
||||
for (const test of t.tests.filter(
|
||||
const evaluationTests = t.tests.filter(
|
||||
(test) => test.type === 'evaluation',
|
||||
) as ExpressionTestEvaluation[]) {
|
||||
expect(
|
||||
evaluate(t.expression, test.input.map((d) => ({ json: d })) as any),
|
||||
).toStrictEqual(test.output);
|
||||
) as ExpressionTestEvaluation[];
|
||||
|
||||
for (const test of evaluationTests) {
|
||||
const input = test.input.map((d) => ({ json: d })) as any;
|
||||
|
||||
if ('error' in test) {
|
||||
expect(() => evaluate(t.expression, input)).toThrowError(test.error);
|
||||
} else {
|
||||
expect(evaluate(t.expression, input)).toStrictEqual(test.output);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
import type { GenericValue, IDataObject } from '@/Interfaces';
|
||||
import { ExpressionError } from '@/errors/expression.error';
|
||||
|
||||
export interface ExpressionTestBase {
|
||||
type: string;
|
||||
interface ExpressionTestBase {
|
||||
type: 'evaluation' | 'transform';
|
||||
}
|
||||
|
||||
export interface ExpressionTestEvaluation extends ExpressionTestBase {
|
||||
interface ExpressionTestSuccess extends ExpressionTestBase {
|
||||
type: 'evaluation';
|
||||
input: Array<IDataObject | GenericValue>;
|
||||
output: IDataObject | GenericValue;
|
||||
}
|
||||
|
||||
interface ExpressionTestFailure extends ExpressionTestBase {
|
||||
type: 'evaluation';
|
||||
input: Array<IDataObject | GenericValue>;
|
||||
error: ExpressionError;
|
||||
}
|
||||
|
||||
export interface ExpressionTestTransform extends ExpressionTestBase {
|
||||
type: 'transform';
|
||||
// If we don't specify a result we expect it to be the same as the input
|
||||
|
@ -17,6 +24,7 @@ export interface ExpressionTestTransform extends ExpressionTestBase {
|
|||
forceTransform?: boolean;
|
||||
}
|
||||
|
||||
export type ExpressionTestEvaluation = ExpressionTestSuccess | ExpressionTestFailure;
|
||||
export type ExpressionTests = ExpressionTestEvaluation | ExpressionTestTransform;
|
||||
|
||||
export interface ExpressionTestFixture {
|
||||
|
@ -265,7 +273,11 @@ export const baseFixtures: ExpressionTestFixture[] = [
|
|||
{
|
||||
type: 'evaluation',
|
||||
input: [],
|
||||
output: undefined,
|
||||
error: new ExpressionError('No execution data available', {
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
type: 'no_execution_data',
|
||||
}),
|
||||
},
|
||||
{ type: 'transform' },
|
||||
{ type: 'transform', forceTransform: true },
|
||||
|
|
|
@ -38,6 +38,8 @@ import { deepCopy } from '@/utils';
|
|||
import { getGlobalState } from '@/GlobalState';
|
||||
import { ApplicationError } from '@/errors/application.error';
|
||||
import { NodeTypes as NodeTypesClass } from './NodeTypes';
|
||||
import { readFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export interface INodeTypesObject {
|
||||
[key: string]: INodeType;
|
||||
|
@ -558,3 +560,7 @@ export function WorkflowExecuteAdditionalData(): IWorkflowExecuteAdditionalData
|
|||
userId: '123',
|
||||
};
|
||||
}
|
||||
|
||||
const BASE_DIR = path.resolve(__dirname, '..');
|
||||
export const readJsonFileSync = <T>(filePath: string) =>
|
||||
JSON.parse(readFileSync(path.join(BASE_DIR, filePath), 'utf-8')) as T;
|
||||
|
|
|
@ -536,42 +536,45 @@ const googleSheetsNode: LoadedClass<IVersionedNodeType> = {
|
|||
},
|
||||
};
|
||||
|
||||
const setNode: LoadedClass<INodeType> = {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets a value',
|
||||
defaults: {
|
||||
name: 'Set',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Value1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: 'default-value1',
|
||||
},
|
||||
{
|
||||
displayName: 'Value2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
default: 'default-value2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export class NodeTypes implements INodeTypes {
|
||||
nodeTypes: INodeTypeData = {
|
||||
'n8n-nodes-base.stickyNote': stickyNode,
|
||||
'n8n-nodes-base.set': setNode,
|
||||
'test.googleSheets': googleSheetsNode,
|
||||
'test.set': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: {
|
||||
displayName: 'Set',
|
||||
name: 'set',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
description: 'Sets a value',
|
||||
defaults: {
|
||||
name: 'Set',
|
||||
color: '#0000FF',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Value1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: 'default-value1',
|
||||
},
|
||||
{
|
||||
displayName: 'Value2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
default: 'default-value2',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'test.set': setNode,
|
||||
'test.setMulti': {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
|
|
|
@ -1,344 +1,144 @@
|
|||
import type { IConnections, IExecuteData, INode, IRunExecutionData } from '@/Interfaces';
|
||||
import type { IExecuteData, INode, IRun, IWorkflowBase } from '@/Interfaces';
|
||||
import { Workflow } from '@/Workflow';
|
||||
import { WorkflowDataProxy } from '@/WorkflowDataProxy';
|
||||
import * as Helpers from './Helpers';
|
||||
import { ExpressionError } from '@/errors/expression.error';
|
||||
import * as Helpers from './Helpers';
|
||||
|
||||
describe('WorkflowDataProxy', () => {
|
||||
describe('test data proxy', () => {
|
||||
const nodes: INode[] = [
|
||||
{
|
||||
name: 'Start',
|
||||
type: 'test.set',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-1',
|
||||
position: [100, 200],
|
||||
},
|
||||
{
|
||||
name: 'Function',
|
||||
type: 'test.set',
|
||||
parameters: {
|
||||
functionCode:
|
||||
'// Code here will run only once, no matter how many input items there are.\n// More info and help: https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.function/\nconst { DateTime, Duration, Interval } = require("luxon");\n\nconst data = [\n {\n "length": 105\n },\n {\n "length": 160\n },\n {\n "length": 121\n },\n {\n "length": 275\n },\n {\n "length": 950\n },\n];\n\nreturn data.map(fact => ({json: fact}));',
|
||||
},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-2',
|
||||
position: [280, 200],
|
||||
},
|
||||
{
|
||||
name: 'Rename',
|
||||
type: 'test.set',
|
||||
parameters: {
|
||||
value1: 'data',
|
||||
value2: 'initialName',
|
||||
},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-3',
|
||||
position: [460, 200],
|
||||
},
|
||||
{
|
||||
name: 'Set',
|
||||
type: 'test.set',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-4',
|
||||
position: [640, 200],
|
||||
},
|
||||
{
|
||||
name: 'End',
|
||||
type: 'test.set',
|
||||
parameters: {},
|
||||
typeVersion: 1,
|
||||
id: 'uuid-5',
|
||||
position: [640, 200],
|
||||
},
|
||||
];
|
||||
const loadFixture = (fixture: string) => {
|
||||
const workflow = Helpers.readJsonFileSync<IWorkflowBase>(
|
||||
`test/fixtures/WorkflowDataProxy/${fixture}_workflow.json`,
|
||||
);
|
||||
const run = Helpers.readJsonFileSync<IRun>(`test/fixtures/WorkflowDataProxy/${fixture}_run.json`);
|
||||
|
||||
const connections: IConnections = {
|
||||
Start: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Function',
|
||||
type: 'main',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
Function: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Rename',
|
||||
type: 'main',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
Rename: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'End',
|
||||
type: 'main',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
return { workflow, run };
|
||||
};
|
||||
|
||||
const getProxyFromFixture = (workflow: IWorkflowBase, run: IRun | null, activeNode: string) => {
|
||||
const taskData = run?.data.resultData.runData[activeNode]?.[0];
|
||||
const lastNodeConnectionInputData = taskData?.data?.main[0];
|
||||
|
||||
let executeData: IExecuteData | undefined;
|
||||
|
||||
if (taskData) {
|
||||
executeData = {
|
||||
data: taskData.data!,
|
||||
node: workflow.nodes.find((node) => node.name === activeNode) as INode,
|
||||
source: {
|
||||
main: taskData.source,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
resultData: {
|
||||
runData: {
|
||||
Start: [
|
||||
{
|
||||
startTime: 1,
|
||||
executionTime: 1,
|
||||
data: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
json: {},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
Function: [
|
||||
{
|
||||
startTime: 1,
|
||||
executionTime: 1,
|
||||
data: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
json: { initialName: 105 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { initialName: 160 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { initialName: 121 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { initialName: 275 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { initialName: 950 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
source: [
|
||||
{
|
||||
previousNode: 'Start',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
Rename: [
|
||||
{
|
||||
startTime: 1,
|
||||
executionTime: 1,
|
||||
data: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
json: { data: 105 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { data: 160 },
|
||||
pairedItem: { item: 1 },
|
||||
},
|
||||
{
|
||||
json: { data: 121 },
|
||||
pairedItem: { item: 2 },
|
||||
},
|
||||
{
|
||||
json: { data: 275 },
|
||||
pairedItem: { item: 3 },
|
||||
},
|
||||
{
|
||||
json: { data: 950 },
|
||||
pairedItem: { item: 4 },
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
source: [
|
||||
{
|
||||
previousNode: 'Function',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
End: [
|
||||
{
|
||||
startTime: 1,
|
||||
executionTime: 1,
|
||||
data: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
json: { data: 105 },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
{
|
||||
json: { data: 160 },
|
||||
pairedItem: { item: 1 },
|
||||
},
|
||||
{
|
||||
json: { data: 121 },
|
||||
pairedItem: { item: 2 },
|
||||
},
|
||||
{
|
||||
json: { data: 275 },
|
||||
pairedItem: { item: 3 },
|
||||
},
|
||||
{
|
||||
json: { data: 950 },
|
||||
pairedItem: { item: 4 },
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
source: [
|
||||
{
|
||||
previousNode: 'Rename',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const nodeTypes = Helpers.NodeTypes();
|
||||
const workflow = new Workflow({
|
||||
const dataProxy = new WorkflowDataProxy(
|
||||
new Workflow({
|
||||
id: '123',
|
||||
name: 'test workflow',
|
||||
nodes,
|
||||
connections,
|
||||
nodes: workflow.nodes,
|
||||
connections: workflow.connections,
|
||||
active: false,
|
||||
nodeTypes,
|
||||
});
|
||||
const nameLastNode = 'End';
|
||||
nodeTypes: Helpers.NodeTypes(),
|
||||
}),
|
||||
run?.data ?? null,
|
||||
0,
|
||||
0,
|
||||
activeNode,
|
||||
lastNodeConnectionInputData ?? [],
|
||||
{},
|
||||
'manual',
|
||||
{},
|
||||
executeData,
|
||||
);
|
||||
|
||||
const lastNodeConnectionInputData =
|
||||
runExecutionData.resultData.runData[nameLastNode][0].data!.main[0];
|
||||
return dataProxy.getDataProxy();
|
||||
};
|
||||
|
||||
const executeData: IExecuteData = {
|
||||
data: runExecutionData.resultData.runData[nameLastNode][0].data!,
|
||||
node: nodes.find((node) => node.name === nameLastNode) as INode,
|
||||
source: {
|
||||
main: runExecutionData.resultData.runData[nameLastNode][0].source,
|
||||
},
|
||||
};
|
||||
describe('WorkflowDataProxy', () => {
|
||||
describe('Base', () => {
|
||||
const fixture = loadFixture('base');
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'End');
|
||||
|
||||
const dataProxy = new WorkflowDataProxy(
|
||||
workflow,
|
||||
runExecutionData,
|
||||
0,
|
||||
0,
|
||||
nameLastNode,
|
||||
lastNodeConnectionInputData ?? [],
|
||||
{},
|
||||
'manual',
|
||||
{},
|
||||
executeData,
|
||||
);
|
||||
const proxy = dataProxy.getDataProxy();
|
||||
|
||||
test('test $("NodeName").all()', () => {
|
||||
test('$("NodeName").all()', () => {
|
||||
expect(proxy.$('Rename').all()[1].json.data).toEqual(160);
|
||||
});
|
||||
test('test $("NodeName").all() length', () => {
|
||||
test('$("NodeName").all() length', () => {
|
||||
expect(proxy.$('Rename').all().length).toEqual(5);
|
||||
});
|
||||
test('test $("NodeName").item', () => {
|
||||
test('$("NodeName").item', () => {
|
||||
expect(proxy.$('Rename').item).toEqual({ json: { data: 105 }, pairedItem: { item: 0 } });
|
||||
});
|
||||
test('test $("NodeNameEarlier").item', () => {
|
||||
test('$("NodeNameEarlier").item', () => {
|
||||
expect(proxy.$('Function').item).toEqual({
|
||||
json: { initialName: 105 },
|
||||
pairedItem: { item: 0 },
|
||||
});
|
||||
});
|
||||
test('test $("NodeName").itemMatching(2)', () => {
|
||||
test('$("NodeName").itemMatching(2)', () => {
|
||||
expect(proxy.$('Rename').itemMatching(2).json.data).toEqual(121);
|
||||
});
|
||||
test('test $("NodeName").first()', () => {
|
||||
test('$("NodeName").first()', () => {
|
||||
expect(proxy.$('Rename').first().json.data).toEqual(105);
|
||||
});
|
||||
test('test $("NodeName").last()', () => {
|
||||
test('$("NodeName").last()', () => {
|
||||
expect(proxy.$('Rename').last().json.data).toEqual(950);
|
||||
});
|
||||
|
||||
test('test $("NodeName").params', () => {
|
||||
test('$("NodeName").params', () => {
|
||||
expect(proxy.$('Rename').params).toEqual({ value1: 'data', value2: 'initialName' });
|
||||
});
|
||||
|
||||
test('$("NodeName")', () => {
|
||||
test('$("NodeName") not in workflow should throw', () => {
|
||||
expect(() => proxy.$('doNotExist')).toThrowError(ExpressionError);
|
||||
});
|
||||
|
||||
test('test $("NodeName").isExecuted', () => {
|
||||
test('$("NodeName").item on Node that has not executed', () => {
|
||||
expect(() => proxy.$('Set').item).toThrowError(ExpressionError);
|
||||
});
|
||||
|
||||
test('$("NodeName").isExecuted', () => {
|
||||
expect(proxy.$('Function').isExecuted).toEqual(true);
|
||||
expect(proxy.$('Set').isExecuted).toEqual(false);
|
||||
});
|
||||
|
||||
test('test $input.all()', () => {
|
||||
test('$input.all()', () => {
|
||||
expect(proxy.$input.all()[1].json.data).toEqual(160);
|
||||
});
|
||||
test('test $input.all() length', () => {
|
||||
test('$input.all() length', () => {
|
||||
expect(proxy.$input.all().length).toEqual(5);
|
||||
});
|
||||
test('test $input.first()', () => {
|
||||
expect(proxy.$input.first().json.data).toEqual(105);
|
||||
test('$input.first()', () => {
|
||||
expect(proxy.$input.first()?.json?.data).toEqual(105);
|
||||
});
|
||||
test('test $input.last()', () => {
|
||||
expect(proxy.$input.last().json.data).toEqual(950);
|
||||
test('$input.last()', () => {
|
||||
expect(proxy.$input.last()?.json?.data).toEqual(950);
|
||||
});
|
||||
test('test $input.item', () => {
|
||||
expect(proxy.$input.item.json.data).toEqual(105);
|
||||
test('$input.item', () => {
|
||||
expect(proxy.$input.item?.json?.data).toEqual(105);
|
||||
});
|
||||
test('test $thisItem', () => {
|
||||
test('$thisItem', () => {
|
||||
expect(proxy.$thisItem.json.data).toEqual(105);
|
||||
});
|
||||
|
||||
test('test $binary', () => {
|
||||
test('$binary', () => {
|
||||
expect(proxy.$binary).toEqual({});
|
||||
});
|
||||
|
||||
test('test $json', () => {
|
||||
test('$json', () => {
|
||||
expect(proxy.$json).toEqual({ data: 105 });
|
||||
});
|
||||
|
||||
test('test $itemIndex', () => {
|
||||
test('$itemIndex', () => {
|
||||
expect(proxy.$itemIndex).toEqual(0);
|
||||
});
|
||||
|
||||
test('test $prevNode', () => {
|
||||
test('$prevNode', () => {
|
||||
expect(proxy.$prevNode).toEqual({ name: 'Rename', outputIndex: 0, runIndex: 0 });
|
||||
});
|
||||
|
||||
test('test $runIndex', () => {
|
||||
test('$runIndex', () => {
|
||||
expect(proxy.$runIndex).toEqual(0);
|
||||
});
|
||||
|
||||
test('test $workflow', () => {
|
||||
test('$workflow', () => {
|
||||
expect(proxy.$workflow).toEqual({
|
||||
active: false,
|
||||
id: '123',
|
||||
|
@ -346,4 +146,141 @@ describe('WorkflowDataProxy', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Errors', () => {
|
||||
const fixture = loadFixture('errors');
|
||||
|
||||
test('$("NodeName").item, Node does not exist', (done) => {
|
||||
const proxy = getProxyFromFixture(
|
||||
fixture.workflow,
|
||||
fixture.run,
|
||||
'Reference non-existent node',
|
||||
);
|
||||
try {
|
||||
proxy.$('does not exist').item;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('"does not exist" node doesn\'t exist');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$("NodeName").item, node has no connection to referenced node', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'NoPathBack');
|
||||
try {
|
||||
proxy.$('Customer Datastore (n8n training)').item;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('Invalid expression');
|
||||
expect(exprError.context.type).toEqual('paired_item_no_connection');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$("NodeName").first(), node has no connection to referenced node', (done) => {
|
||||
const proxy = getProxyFromFixture(
|
||||
fixture.workflow,
|
||||
fixture.run,
|
||||
'Reference impossible with .first()',
|
||||
);
|
||||
try {
|
||||
proxy.$('Impossible').first().json.name;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('no data, execute "Impossible" node first');
|
||||
expect(exprError.context.type).toEqual('no_node_execution_data');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$json, Node has no connections', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'NoInputConnection');
|
||||
try {
|
||||
proxy.$json.email;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('No execution data available');
|
||||
expect(exprError.context.type).toEqual('no_input_connection');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$("NodeName").item, Node has not run', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Impossible');
|
||||
try {
|
||||
proxy.$('Impossible if').item;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('no data, execute "Impossible if" node first');
|
||||
expect(exprError.context.type).toEqual('no_node_execution_data');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$json, Node has not run', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'Impossible');
|
||||
try {
|
||||
proxy.$json.email;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('No execution data available');
|
||||
expect(exprError.context.type).toEqual('no_execution_data');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$("NodeName").item, paired item error: more than 1 matching item', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'PairedItemMultipleMatches');
|
||||
try {
|
||||
proxy.$('Edit Fields').item;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('Invalid expression');
|
||||
expect(exprError.context.type).toEqual('paired_item_multiple_matches');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$("NodeName").item, paired item error: missing paired item', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'PairedItemInfoMissing');
|
||||
try {
|
||||
proxy.$('Edit Fields').item;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('Can’t get data for expression');
|
||||
expect(exprError.context.type).toEqual('paired_item_no_info');
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
test('$("NodeName").item, paired item error: invalid paired item', (done) => {
|
||||
const proxy = getProxyFromFixture(fixture.workflow, fixture.run, 'IncorrectPairedItem');
|
||||
try {
|
||||
proxy.$('Edit Fields').item;
|
||||
done('should throw');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ExpressionError);
|
||||
const exprError = error as ExpressionError;
|
||||
expect(exprError.message).toEqual('Can’t get data for expression');
|
||||
expect(exprError.context.type).toEqual('paired_item_invalid_info');
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
140
packages/workflow/test/fixtures/WorkflowDataProxy/base_run.json
vendored
Normal file
140
packages/workflow/test/fixtures/WorkflowDataProxy/base_run.json
vendored
Normal file
|
@ -0,0 +1,140 @@
|
|||
{
|
||||
"data": {
|
||||
"startData": {},
|
||||
"resultData": {
|
||||
"runData": {
|
||||
"Start": [
|
||||
{
|
||||
"startTime": 1,
|
||||
"executionTime": 1,
|
||||
"data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"json": {}
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"Function": [
|
||||
{
|
||||
"startTime": 1,
|
||||
"executionTime": 1,
|
||||
"data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"json": { "initialName": 105 },
|
||||
"pairedItem": { "item": 0 }
|
||||
},
|
||||
{
|
||||
"json": { "initialName": 160 },
|
||||
"pairedItem": { "item": 0 }
|
||||
},
|
||||
{
|
||||
"json": { "initialName": 121 },
|
||||
"pairedItem": { "item": 0 }
|
||||
},
|
||||
{
|
||||
"json": { "initialName": 275 },
|
||||
"pairedItem": { "item": 0 }
|
||||
},
|
||||
{
|
||||
"json": { "initialName": 950 },
|
||||
"pairedItem": { "item": 0 }
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"source": [
|
||||
{
|
||||
"previousNode": "Start"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Rename": [
|
||||
{
|
||||
"startTime": 1,
|
||||
"executionTime": 1,
|
||||
"data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"json": { "data": 105 },
|
||||
"pairedItem": { "item": 0 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 160 },
|
||||
"pairedItem": { "item": 1 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 121 },
|
||||
"pairedItem": { "item": 2 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 275 },
|
||||
"pairedItem": { "item": 3 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 950 },
|
||||
"pairedItem": { "item": 4 }
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"source": [
|
||||
{
|
||||
"previousNode": "Function"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"End": [
|
||||
{
|
||||
"startTime": 1,
|
||||
"executionTime": 1,
|
||||
"data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"json": { "data": 105 },
|
||||
"pairedItem": { "item": 0 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 160 },
|
||||
"pairedItem": { "item": 1 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 121 },
|
||||
"pairedItem": { "item": 2 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 275 },
|
||||
"pairedItem": { "item": 3 }
|
||||
},
|
||||
{
|
||||
"json": { "data": 950 },
|
||||
"pairedItem": { "item": 4 }
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"source": [
|
||||
{
|
||||
"previousNode": "Rename"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"mode": "manual",
|
||||
"startedAt": "2024-02-08T15:45:18.848Z",
|
||||
"stoppedAt": "2024-02-08T15:45:18.862Z",
|
||||
"status": "running"
|
||||
}
|
86
packages/workflow/test/fixtures/WorkflowDataProxy/base_workflow.json
vendored
Normal file
86
packages/workflow/test/fixtures/WorkflowDataProxy/base_workflow.json
vendored
Normal file
|
@ -0,0 +1,86 @@
|
|||
{
|
||||
"name": "",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Start",
|
||||
"type": "test.set",
|
||||
"parameters": {},
|
||||
"typeVersion": 1,
|
||||
"id": "uuid-1",
|
||||
"position": [100, 200]
|
||||
},
|
||||
{
|
||||
"name": "Function",
|
||||
"type": "test.set",
|
||||
"parameters": {
|
||||
"functionCode": "// Code here will run only once, no matter how many input items there are.\n// More info and help: https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.function/\nconst { DateTime, Duration, Interval } = require(\"luxon\");\n\nconst data = [\n {\n \"length\": 105\n },\n {\n \"length\": 160\n },\n {\n \"length\": 121\n },\n {\n \"length\": 275\n },\n {\n \"length\": 950\n },\n];\n\nreturn data.map(fact => ({json: fact}));"
|
||||
},
|
||||
"typeVersion": 1,
|
||||
"id": "uuid-2",
|
||||
"position": [280, 200]
|
||||
},
|
||||
{
|
||||
"name": "Rename",
|
||||
"type": "test.set",
|
||||
"parameters": {
|
||||
"value1": "data",
|
||||
"value2": "initialName"
|
||||
},
|
||||
"typeVersion": 1,
|
||||
"id": "uuid-3",
|
||||
"position": [460, 200]
|
||||
},
|
||||
{
|
||||
"name": "Set",
|
||||
"type": "test.set",
|
||||
"parameters": {},
|
||||
"typeVersion": 1,
|
||||
"id": "uuid-4",
|
||||
"position": [640, 200]
|
||||
},
|
||||
{
|
||||
"name": "End",
|
||||
"type": "test.set",
|
||||
"parameters": {},
|
||||
"typeVersion": 1,
|
||||
"id": "uuid-5",
|
||||
"position": [640, 200]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Start": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Function",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Function": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Rename",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Rename": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "End",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
1017
packages/workflow/test/fixtures/WorkflowDataProxy/errors_run.json
vendored
Normal file
1017
packages/workflow/test/fixtures/WorkflowDataProxy/errors_run.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
747
packages/workflow/test/fixtures/WorkflowDataProxy/errors_workflow.json
vendored
Normal file
747
packages/workflow/test/fixtures/WorkflowDataProxy/errors_workflow.json
vendored
Normal file
|
@ -0,0 +1,747 @@
|
|||
{
|
||||
"name": "WorkflowDataProxy errors",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "b5122d27-4bb5-4100-a69b-03b1dcac76c7",
|
||||
"name": "When clicking \"Test workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [740, 1680]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "getAllPeople"
|
||||
},
|
||||
"id": "bf471582-900d-47af-848c-2d4218798775",
|
||||
"name": "Customer Datastore (n8n training)",
|
||||
"type": "n8n-nodes-base.n8nTrainingCustomerDatastore",
|
||||
"typeVersion": 1,
|
||||
"position": [1180, 1680]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "name",
|
||||
"stringValue": "={{ $json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "1de94b04-c87b-4ef1-b5d7-5078f9e33220",
|
||||
"name": "Edit Fields",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1400, 1680]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "These expression should always be red — there is no way of getting the input data even if you execute. Text should be:",
|
||||
"height": 349.2762683040461,
|
||||
"width": 339
|
||||
},
|
||||
"id": "c277f7c6-8a7a-41e9-9484-78e90bd205bf",
|
||||
"name": "Sticky Note",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 1040]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToAggregate": {
|
||||
"fieldToAggregate": [
|
||||
{
|
||||
"fieldToAggregate": "name"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "f6606ff5-4d66-4efb-8dad-de7662f20867",
|
||||
"name": "Aggregate",
|
||||
"type": "n8n-nodes-base.aggregate",
|
||||
"typeVersion": 1,
|
||||
"position": [1820, 860]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "This error should be\n\n[Can't determine which item to use]",
|
||||
"height": 255,
|
||||
"width": 177
|
||||
},
|
||||
"id": "71fbae4a-f5b3-4db1-9684-83c4d2037099",
|
||||
"name": "Sticky Note1",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 760]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "[No path back to node]",
|
||||
"height": 209,
|
||||
"width": 150
|
||||
},
|
||||
"id": "24e878cb-a681-4c00-bec1-83188aa20eb7",
|
||||
"name": "Sticky Note2",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1020, 1132]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "[No input connected]",
|
||||
"height": 201,
|
||||
"width": 150
|
||||
},
|
||||
"id": "4bd26f55-87b5-4ad1-b3f1-ae2786941114",
|
||||
"name": "Sticky Note3",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1200, 1132]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nreturn [\n {\n \"field\": \"the same\"\n }\n];"
|
||||
},
|
||||
"id": "6538818e-c5b3-422b-920c-d5d52533578b",
|
||||
"name": "Break pairedItem chain",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1820, 1120]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "This error should be\n\n[Can't determine which item to use]",
|
||||
"height": 255,
|
||||
"width": 177
|
||||
},
|
||||
"id": "42641e54-60e1-46d7-bcb4-b55a83f89f6b",
|
||||
"name": "Sticky Note4",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 1020]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nreturn [\n {\n \"json\": {\n \"field\": \"the same\"\n },\n \"pairedItem\": 99\n }\n];"
|
||||
},
|
||||
"id": "05583883-ab4a-42c2-9edb-8e8cf3c9d074",
|
||||
"name": "Incorrect pairedItem info",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1820, 1680]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "This error should be\n\n[Can't determine which item to use]",
|
||||
"height": 255,
|
||||
"width": 177
|
||||
},
|
||||
"id": "aea58e9e-5a00-4a86-a0bc-b077a07cd1f4",
|
||||
"name": "Sticky Note5",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 1580]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "If the pinned node is executed, make grey and use text:\n[For preview, unpin node ‘<node_name>’ and execute]",
|
||||
"height": 255,
|
||||
"width": 237.63786881219818
|
||||
},
|
||||
"id": "3fdf6bdc-8065-421b-9ecf-6453946356a4",
|
||||
"name": "Sticky Note6",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 1840]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nreturn [\n {\n \"json\": {\n \"field\": \"the same\"\n },\n \"pairedItem\": [1, 2, 3, 4]\n }\n];"
|
||||
},
|
||||
"id": "f8de7b0a-79c1-4b7a-a183-feb94f2f8625",
|
||||
"name": "Multiple matching items",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1820, 2200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "This error should be\n\n[Can't determine which item to use]",
|
||||
"height": 255,
|
||||
"width": 177
|
||||
},
|
||||
"id": "601c050a-7909-4708-be8d-4de248b68392",
|
||||
"name": "Sticky Note7",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 2100]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "This should be grey, with text\n\n[For preview, unpin node ‘<node_name>’ and execute]",
|
||||
"height": 291.70186796527776,
|
||||
"width": 177
|
||||
},
|
||||
"id": "dfdcfaf4-a76b-4307-97a6-3fd7772e9fa8",
|
||||
"name": "Sticky Note8",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 2360]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nreturn [\n {\n \"json\": {\n \"field\": \"the same\"\n },\n \"pairedItem\": [1, 2, 3, 4]\n }\n];"
|
||||
},
|
||||
"id": "8f2a9642-68e7-4dc6-a6c2-2018919327a3",
|
||||
"name": "Multiple matching items, pinned",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1820, 2500]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "If the pinned node isn't executed (e.g. if you execute one of the other code nodes in the same column), the expression is green!",
|
||||
"height": 128.93706220621976,
|
||||
"width": 177
|
||||
},
|
||||
"id": "65cf9b4c-a96d-46f5-b9bb-f6d88d1fbc44",
|
||||
"name": "Sticky Note9",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2220, 1940]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nreturn [\n {\n \"json\": {\n \"field\": \"the same\"\n },\n \"pairedItem\": 99\n }\n];"
|
||||
},
|
||||
"id": "0bdfe0d2-7de2-472d-bc0a-2d0eff0e08c7",
|
||||
"name": "Incorrect pairedItem info, pinned1",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1820, 1940]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "\nreturn [\n {\n \"field\": \"the same\"\n }\n];"
|
||||
},
|
||||
"id": "b080a98e-d983-414a-b925-bdfc7ab2c3b6",
|
||||
"name": "Break pairedItem chain, pinned",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1820, 1420]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "This should be grey, with text\n\n[For preview, unpin node ‘<node_name>’ and execute]",
|
||||
"height": 291.70186796527776,
|
||||
"width": 177
|
||||
},
|
||||
"id": "ce083193-1944-4c6c-925d-9e23c5194d98",
|
||||
"name": "Sticky Note11",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2000, 1280]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "We should also change the output pane error on execution in this case.\n\nERROR: No path back to '<node_name>' node\nDescription: Please make sure it is connected to this node (there can be other nodes in between)",
|
||||
"height": 209,
|
||||
"width": 301.59467203049536
|
||||
},
|
||||
"id": "755e07f0-3f18-4b08-ad30-79221a76507a",
|
||||
"name": "Sticky Note10",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1080, 1360]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "1fff886f-3d13-4fbf-b0fb-7e2f845937c0",
|
||||
"leftValue": "={{ false }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "56dd65f0-d67a-42ce-a876-77434f621dc3",
|
||||
"name": "Impossible if",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [1000, 2000]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "test",
|
||||
"stringValue": "xzy"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "11eadfc8-d14d-407c-b6d5-6e59b2e427a1",
|
||||
"name": "Impossible",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1180, 1980]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "Should be an error when using .item:\n\n[No path back to node]",
|
||||
"height": 237.7232010163043,
|
||||
"width": 150
|
||||
},
|
||||
"id": "c3a3fdc2-66fa-4562-a359-45bdece2f625",
|
||||
"name": "Sticky Note12",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1400, 1880]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "name",
|
||||
"stringValue": "={{ $('Impossible').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "4cbbee96-dd4c-4625-95b9-c68faef3e9a8",
|
||||
"name": "Reference impossible with .item",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1420, 2000]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"name": "name",
|
||||
"stringValue": "={{ $('Impossible').first().json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "6d47bd08-810a-4ade-be57-635adc1df47f",
|
||||
"name": "Reference impossible with .first()",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1420, 2320]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "When using .first(), .last() or .all() and the node isn't executed, show grey warning:\n\n[Execute ‘<node_name>’ for preview]",
|
||||
"height": 330.27573762439613,
|
||||
"width": 229.78666948973432
|
||||
},
|
||||
"id": "1fcf2562-0789-41ad-8c92-44bcdd5d44e6",
|
||||
"name": "Sticky Note13",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1400, 2180]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('non existent') }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "327d7f7b-61a5-4d60-9542-d61f84e7c83a",
|
||||
"name": "Reference non-existent node",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1000, 2320]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Customer Datastore (n8n training)').item.json.email }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "38e3a736-4e13-4c23-af16-e50e605c4fb5",
|
||||
"name": "NoPathBack",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1040, 1184]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $json.email }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "2a7eaf81-6d64-488d-baf6-cc2f962908af",
|
||||
"name": "NoInputConnection",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [1220, 1180]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "166ee813-1db8-43a6-ace4-990c41dfeaea",
|
||||
"name": "PairedItemInfoMissing",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 1120]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "a2dca54c-03ef-4a16-bf29-71eb0012cf0b",
|
||||
"name": "PairedItemInfoMissingPinned",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 1420]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "0a1f566b-8dcf-4e28-81c4-faeadcdc02fb",
|
||||
"name": "IncorrectPairedItem",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 1680]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.to }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "4d76b75f-5896-48ba-bb2f-8a2574ec1b8b",
|
||||
"name": "IncorrectPairedItemPinned",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 1940]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "c4636b5c-c13a-441b-a59c-23962b2757b3",
|
||||
"name": "PairedItemMultipleMatches2",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 2200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "6d687cf8-5309-4d44-aab3-aa023a42fa27",
|
||||
"name": "PairedItemMultipleMatches",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 860]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fields": {
|
||||
"values": [
|
||||
{
|
||||
"stringValue": "={{ $('Edit Fields').item.json.name }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "d87a7aa4-b4c7-4fad-897d-a7ce0657bef3",
|
||||
"name": "IncorrectPairedItemPinned2",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"position": [2040, 2500]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Multiple matching items, pinned": [
|
||||
{
|
||||
"json": {
|
||||
"field": "the same"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Incorrect pairedItem info, pinned1": [
|
||||
{
|
||||
"json": {
|
||||
"field": "the same"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Break pairedItem chain, pinned": [
|
||||
{
|
||||
"json": {
|
||||
"field": "the same"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Test workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Customer Datastore (n8n training)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Impossible if",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Reference non-existent node",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Customer Datastore (n8n training)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Reference impossible with .item",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Reference impossible with .first()",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Edit Fields": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Aggregate",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Break pairedItem chain",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Incorrect pairedItem info",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Multiple matching items",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Incorrect pairedItem info, pinned1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Multiple matching items, pinned",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Break pairedItem chain, pinned",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Aggregate": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PairedItemMultipleMatches",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Break pairedItem chain": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PairedItemInfoMissing",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Incorrect pairedItem info": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "IncorrectPairedItem",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Multiple matching items": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PairedItemMultipleMatches2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Multiple matching items, pinned": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "IncorrectPairedItemPinned2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Incorrect pairedItem info, pinned1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "IncorrectPairedItemPinned",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Break pairedItem chain, pinned": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "PairedItemInfoMissingPinned",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Impossible if": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Impossible",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "f6276c80-c1d1-485b-9d07-894868bcd701",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "2e88d456a76a9edc44cbcda082bb44ddef9555356ef691b0c6a45099d5095a45"
|
||||
},
|
||||
"id": "BmXv9neCtTggKXuG",
|
||||
"tags": []
|
||||
}
|
Loading…
Reference in a new issue