feat: Improvements to pairedItem

This commit is contained in:
Jan Oberhauser 2022-07-22 12:19:45 +02:00
parent 2f4f2cfb86
commit 1348349748
19 changed files with 161 additions and 90 deletions

View file

@ -8,6 +8,7 @@ import {
IDataObject,
IDeferredPromise,
IExecuteResponsePromiseData,
IPinData,
IRun,
IRunData,
IRunExecutionData,
@ -15,7 +16,6 @@ import {
ITelemetrySettings,
ITelemetryTrackProperties,
IWorkflowBase as IWorkflowBaseWorkflow,
PinData,
Workflow,
WorkflowExecuteMode,
} from 'n8n-workflow';
@ -689,7 +689,7 @@ export interface IWorkflowExecutionDataProcess {
executionMode: WorkflowExecuteMode;
executionData?: IRunExecutionData;
runData?: IRunData;
pinData?: PinData;
pinData?: IPinData;
retryOf?: number | string;
sessionId?: string;
startNodes?: string[];

View file

@ -70,11 +70,11 @@ import {
INodeType,
INodeTypeDescription,
INodeTypeNameVersion,
IPinData,
ITelemetrySettings,
IWorkflowBase,
LoggerProxy,
NodeHelpers,
PinData,
WebhookHttpMethod,
Workflow,
WorkflowExecuteMode,
@ -2836,7 +2836,7 @@ const TRIGGER_NODE_SUFFIXES = ['trigger', 'webhook'];
const isTrigger = (str: string) =>
TRIGGER_NODE_SUFFIXES.some((suffix) => str.toLowerCase().includes(suffix));
function findFirstPinnedTrigger(workflow: IWorkflowDb, pinData?: PinData) {
function findFirstPinnedTrigger(workflow: IWorkflowDb, pinData?: IPinData) {
if (!pinData) return;
const firstPinnedTriggerName = Object.keys(pinData).find(isTrigger);

View file

@ -230,6 +230,7 @@ export class WorkflowRunnerProcess {
nodeTypes,
staticData: this.data.workflowData.staticData,
settings: this.data.workflowData.settings,
pinData: this.data.pinData,
});
await checkPermissionsForExecution(this.workflow, userId);
const additionalData = await WorkflowExecuteAdditionalData.getBase(

View file

@ -2,7 +2,7 @@
/* eslint-disable import/no-cycle */
import { Length } from 'class-validator';
import { IConnections, IDataObject, INode, IWorkflowSettings, PinData } from 'n8n-workflow';
import { IConnections, IDataObject, INode, IPinData, IWorkflowSettings } from 'n8n-workflow';
import {
BeforeUpdate,
@ -122,7 +122,7 @@ export class WorkflowEntity implements IWorkflowDb {
nullable: true,
transformer: serializer,
})
pinData: PinData;
pinData: IPinData;
@BeforeUpdate()
setUpdateDate() {

View file

@ -6,9 +6,9 @@ import {
ICredentialNodeAccess,
INode,
INodeCredentialTestRequest,
IPinData,
IRunData,
IWorkflowSettings,
PinData,
} from 'n8n-workflow';
import { User } from './databases/entities/User';
@ -72,7 +72,7 @@ export declare namespace WorkflowRequest {
{
workflowData: IWorkflowDb;
runData: IRunData;
pinData: PinData;
pinData: IPinData;
startNodes?: string[];
destinationNode?: string;
}

View file

@ -4,7 +4,7 @@ import * as utils from './shared/utils';
import * as testDb from './shared/testDb';
import { WorkflowEntity } from '../../src/databases/entities/WorkflowEntity';
import type { Role } from '../../src/databases/entities/Role';
import { PinData } from 'n8n-workflow';
import { IPinData } from 'n8n-workflow';
jest.mock('../../src/telemetry');
@ -44,7 +44,7 @@ test('POST /workflows should store pin data for node in workflow', async () => {
expect(response.statusCode).toBe(200);
const { pinData } = response.body.data as { pinData: PinData };
const { pinData } = response.body.data as { pinData: IPinData };
expect(pinData).toMatchObject({ Spotify: [{ myKey: 'myValue' }] });
});
@ -59,7 +59,7 @@ test('POST /workflows should set pin data to null if no pin data', async () => {
expect(response.statusCode).toBe(200);
const { pinData } = response.body.data as { pinData: PinData };
const { pinData } = response.body.data as { pinData: IPinData };
expect(pinData).toBeNull();
});
@ -78,7 +78,7 @@ test('GET /workflows/:id should return pin data', async () => {
expect(workflowRetrievalResponse.statusCode).toBe(200);
const { pinData } = workflowRetrievalResponse.body.data as { pinData: PinData };
const { pinData } = workflowRetrievalResponse.body.data as { pinData: IPinData };
expect(pinData).toMatchObject({ Spotify: [{ myKey: 'myValue' }] });
});

View file

@ -19,6 +19,7 @@ import {
INode,
INodeConnections,
INodeExecutionData,
IPinData,
IRun,
IRunData,
IRunExecutionData,
@ -32,7 +33,6 @@ import {
LoggerProxy as Logger,
NodeApiError,
NodeOperationError,
PinData,
Workflow,
WorkflowExecuteMode,
WorkflowOperationError,
@ -88,7 +88,7 @@ export class WorkflowExecute {
workflow: Workflow,
startNode?: INode,
destinationNode?: string,
pinData?: PinData,
pinData?: IPinData,
): PCancelable<IRun> {
// Get the nodes to start workflow execution from
startNode = startNode || workflow.getStartNode(destinationNode);
@ -160,7 +160,7 @@ export class WorkflowExecute {
runData: IRunData,
startNodes: string[],
destinationNode: string,
pinData?: PinData,
pinData?: IPinData,
// @ts-ignore
): PCancelable<IRun> {
let incomingNodeConnections: INodeConnections | undefined;

View file

@ -14,6 +14,7 @@ import {
INodePropertyOptions,
INodeTypeDescription,
INodeTypeNameVersion,
IPinData,
IRunExecutionData,
IRun,
IRunData,
@ -21,15 +22,9 @@ import {
ITelemetrySettings,
IWorkflowSettings as IWorkflowSettingsWorkflow,
WorkflowExecuteMode,
PinData,
PublicInstalledPackage,
} from 'n8n-workflow';
import {
COMMUNITY_PACKAGE_MANAGE_ACTIONS,
} from './constants';
export * from 'n8n-design-system/src/types';
declare module 'jsplumb' {
@ -218,7 +213,7 @@ export interface IStartRunData {
startNodes?: string[];
destinationNode?: string;
runData?: IRunData;
pinData?: PinData;
pinData?: IPinData;
}
export interface IRunDataUi {
@ -253,7 +248,7 @@ export interface IWorkflowData {
connections: IConnections;
settings?: IWorkflowSettings;
tags?: string[];
pinData?: PinData;
pinData?: IPinData;
}
export interface IWorkflowDataUpdate {
@ -264,7 +259,7 @@ export interface IWorkflowDataUpdate {
settings?: IWorkflowSettings;
active?: boolean;
tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response
pinData?: PinData;
pinData?: IPinData;
}
export interface IWorkflowTemplate {
@ -287,7 +282,7 @@ export interface IWorkflowDb {
connections: IConnections;
settings?: IWorkflowSettings;
tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response
pinData?: PinData;
pinData?: IPinData;
}
// Identical to cli.Interfaces.ts

View file

@ -2,7 +2,7 @@
<div>
<div class="error-header">
<div class="error-message">{{ $locale.baseText('nodeErrorView.error') + ': ' + getErrorMessage() }}</div>
<div class="error-description" v-if="error.description">{{error.description}}</div>
<div class="error-description" v-if="error.description">{{getErrorDescription()}}</div>
</div>
<details>
<summary class="error-details__summary">
@ -139,6 +139,14 @@ export default mixins(
},
},
methods: {
getErrorDescription (): string {
if (!this.error.context || !this.error.context.descriptionTemplate) {
return this.error.description;
}
const parameterName = this.parameterDisplayName(this.error.context.parameter);
return this.error.context.descriptionTemplate.replace(/%%PARAMETER%%/g, parameterName);
},
getErrorMessage (): string {
if (!this.error.context || !this.error.context.messageTemplate) {
return this.error.message;

View file

@ -343,7 +343,6 @@ import {
INodeTypeDescription,
IRunData,
IRunExecutionData,
PinData,
} from 'n8n-workflow';
import {

View file

@ -21,10 +21,10 @@ import {
IContextObject,
IDataObject,
INodeExecutionData,
IPinData,
IRunData,
IRunExecutionData,
IWorkflowDataProxyAdditionalKeys,
PinData,
Workflow,
WorkflowDataProxy,
} from 'n8n-workflow';
@ -307,11 +307,11 @@ export default mixins(
* Get the node's output using pinData
*
* @param {string} nodeName The name of the node to get the data of
* @param {PinData[string]} pinData The node's pin data
* @param {IPinData[string]} pinData The node's pin data
* @param {string} filterText Filter text for parameters
* @param {boolean} [useShort=false] Use short notation $json vs. $node[NodeName].json
*/
getNodePinDataOutput(nodeName: string, pinData: PinData[string], filterText: string, useShort = false): IVariableSelectorOption[] | null {
getNodePinDataOutput(nodeName: string, pinData: IPinData[string], filterText: string, useShort = false): IVariableSelectorOption[] | null {
const outputData = pinData.map((data) => ({ json: data } as INodeExecutionData))[0];
return this.getNodeOutput(nodeName, outputData, filterText, useShort);

View file

@ -1,17 +1,17 @@
import Vue from 'vue';
import { INodeUi } from "@/Interface";
import {IDataObject, PinData} from "n8n-workflow";
import {stringSizeInBytes} from "@/components/helpers";
import {MAX_WORKFLOW_PINNED_DATA_SIZE, PIN_DATA_NODE_TYPES_DENYLIST} from "@/constants";
import { INodeUi } from '@/Interface';
import { IPinData } from 'n8n-workflow';
import { stringSizeInBytes } from '@/components/helpers';
import { MAX_WORKFLOW_PINNED_DATA_SIZE, PIN_DATA_NODE_TYPES_DENYLIST } from '@/constants';
interface PinDataContext {
interface IPinDataContext {
node: INodeUi;
$showError(error: Error, title: string): void;
}
export const pinData = (Vue as Vue.VueConstructor<Vue & PinDataContext>).extend({
export const pinData = (Vue as Vue.VueConstructor<Vue & IPinDataContext>).extend({
computed: {
pinData (): PinData[string] | undefined {
pinData (): IPinData[string] | undefined {
return this.node ? this.$store.getters['pinDataByNodeName'](this.node!.name) : undefined;
},
hasPinData (): boolean {

View file

@ -21,6 +21,7 @@ import {
INodeTypeData,
INodeTypeDescription,
INodeVersionedType,
IPinData,
IRunData,
IRunExecutionData,
IWorfklowIssues,
@ -30,7 +31,6 @@ import {
IExecuteData,
INodeConnection,
IWebhookDescription,
PinData,
} from 'n8n-workflow';
import {
@ -330,11 +330,16 @@ export const workflowHelpers = mixins(
const workflowName = this.$store.getters.workflowName;
if (copyData === true) {
return new Workflow({ id: workflowId, name: workflowName, nodes: JSON.parse(JSON.stringify(nodes)), connections: JSON.parse(JSON.stringify(connections)), active: false, nodeTypes, settings: this.$store.getters.workflowSettings});
} else {
return new Workflow({ id: workflowId, name: workflowName, nodes, connections, active: false, nodeTypes, settings: this.$store.getters.workflowSettings});
}
return new Workflow({
id: workflowId,
name: workflowName,
nodes: copyData ? JSON.parse(JSON.stringify(nodes)) : nodes,
connections: copyData? JSON.parse(JSON.stringify(connections)): connections,
active: false,
nodeTypes,
settings: this.$store.getters.workflowSettings,
pinData: this.$store.getters.pinData,
});
},
// Returns the currently loaded workflow as JSON.
@ -526,7 +531,7 @@ export const workflowHelpers = mixins(
}
parentNode.forEach((parentNodeName) => {
const pinData: PinData[string] = this.$store.getters['pinDataByNodeName'](parentNodeName);
const pinData: IPinData[string] = this.$store.getters['pinDataByNodeName'](parentNodeName);
if (pinData) {
runExecutionData = {

View file

@ -13,10 +13,10 @@ import {
INodeConnections,
INodeIssueData,
INodeTypeDescription,
IPinData,
IRunData,
ITaskData,
IWorkflowSettings,
PinData,
} from 'n8n-workflow';
import {
@ -213,7 +213,7 @@ export const store = new Vuex.Store({
},
// Pin data
pinData(state, payload: { node: INodeUi, data: PinData[string] }) {
pinData(state, payload: { node: INodeUi, data: IPinData[string] }) {
if (state.workflow.pinData) {
Vue.set(state.workflow.pinData, payload.node.name, payload.data);
}
@ -651,7 +651,7 @@ export const store = new Vuex.Store({
Vue.set(state.workflow, 'settings', workflowSettings);
},
setWorkflowPinData(state, pinData: PinData) {
setWorkflowPinData(state, pinData: IPinData) {
Vue.set(state.workflow, 'pinData', pinData);
dataPinningEventBus.$emit('pin-data', pinData);
@ -905,7 +905,7 @@ export const store = new Vuex.Store({
* Pin data
*/
pinData: (state): PinData | undefined => {
pinData: (state): IPinData | undefined => {
return state.workflow.pinData;
},
pinDataByNodeName: (state) => (nodeName: string) => {

View file

@ -184,18 +184,18 @@ import {
IDataObject,
INode,
INodeConnections,
INodeCredentialsDetails,
INodeIssues,
INodeTypeDescription,
INodeTypeNameVersion,
NodeHelpers,
Workflow,
IPinData,
IRun,
ITaskData,
INodeCredentialsDetails,
TelemetryHelpers,
ITelemetryTrackProperties,
IWorkflowBase,
PinData,
NodeHelpers,
TelemetryHelpers,
Workflow,
} from 'n8n-workflow';
import {
ICredentialsResponse,
@ -2963,7 +2963,7 @@ export default mixins(
await this.importWorkflowData(workflowData);
}
},
addPinDataConnections(pinData: PinData) {
addPinDataConnections(pinData: IPinData) {
Object.keys(pinData).forEach((nodeName) => {
// @ts-ignore
const connections = this.instance.getConnections({
@ -2978,7 +2978,7 @@ export default mixins(
});
});
},
removePinDataConnections(pinData: PinData) {
removePinDataConnections(pinData: IPinData) {
Object.keys(pinData).forEach((nodeName) => {
// @ts-ignore
const connections = this.instance.getConnections({

View file

@ -10,6 +10,7 @@ export class ExpressionError extends ExecutionBaseError {
options?: {
causeDetailed?: string;
description?: string;
descriptionTemplate?: string;
runIndex?: number;
itemIndex?: number;
messageTemplate?: string;
@ -23,6 +24,10 @@ export class ExpressionError extends ExecutionBaseError {
this.description = options.description;
}
if (options?.descriptionTemplate !== undefined) {
this.context.descriptionTemplate = options.descriptionTemplate;
}
if (options?.causeDetailed !== undefined) {
this.context.causeDetailed = options.causeDetailed;
}

View file

@ -839,10 +839,9 @@ export interface INode {
parameters: INodeParameters;
credentials?: INodeCredentials;
webhookId?: string;
pinData?: IDataObject;
}
export interface PinData {
export interface IPinData {
[nodeName: string]: IDataObject[];
}
@ -1328,7 +1327,7 @@ export interface IRunExecutionData {
resultData: {
error?: ExecutionError;
runData: IRunData;
pinData?: PinData;
pinData?: IPinData;
lastNodeExecuted?: string;
};
executionData?: {
@ -1402,7 +1401,7 @@ export interface IWorkflowBase {
connections: IConnections;
settings?: IWorkflowSettings;
staticData?: IDataObject;
pinData?: PinData;
pinData?: IPinData;
}
export interface IWorkflowCredentials {

View file

@ -28,6 +28,7 @@ import {
INodes,
INodeType,
INodeTypes,
IPinData,
IPollFunctions,
IRunExecutionData,
ITaskDataConnections,
@ -84,6 +85,8 @@ export class Workflow {
// ids of registred webhooks of nodes
staticData: IDataObject;
pinData?: IPinData;
// constructor(id: string | undefined, nodes: INode[], connections: IConnections, active: boolean, nodeTypes: INodeTypes, staticData?: IDataObject, settings?: IWorkflowSettings) {
constructor(parameters: {
id?: string;
@ -94,10 +97,12 @@ export class Workflow {
nodeTypes: INodeTypes;
staticData?: IDataObject;
settings?: IWorkflowSettings;
pinData?: IPinData;
}) {
this.id = parameters.id;
this.name = parameters.name;
this.nodeTypes = parameters.nodeTypes;
this.pinData = parameters.pinData;
// Save nodes in workflow as object to be able to get the
// nodes easily by its name.
@ -410,6 +415,17 @@ export class Workflow {
return null;
}
/**
* Returns the pinData of the node with the given name if it exists
*
* @param {string} nodeName Name of the node to return the pinData of
* @returns {(IDataObject[] | undefined)}
* @memberof Workflow
*/
getPinDataOfNode(nodeName: string): IDataObject[] | undefined {
return this.pinData ? this.pinData[nodeName] : undefined;
}
/**
* Renames nodes in expressions
*

View file

@ -532,11 +532,27 @@ export class WorkflowDataProxy {
const createExpressionError = (
message: string,
context?: {
messageTemplate?: string;
description?: string;
causeDetailed?: string;
description?: string;
descriptionTemplate?: string;
messageTemplate?: string;
},
nodeName?: string,
) => {
if (nodeName) {
const pinData = this.workflow.getPinDataOfNode(nodeName);
if (pinData) {
if (!context) {
context = {};
}
message = `${nodeName} must be unpinned to execute`;
context.description = `To fetch the data the expression needs, The node ${nodeName} needs to execute without being pinned. <a>Unpin it</a>`;
context.description = `To fetch the data for the expression, you must unpin the node '${nodeName}' and execute the workflow again.`;
context.descriptionTemplate = `To fetch the data for the expression under '%%PARAMETER%%', you must unpin the node '${nodeName}' and execute the workflow again.`;
}
}
return new ExpressionError(message, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
@ -560,6 +576,7 @@ export class WorkflowDataProxy {
};
}
let nodeBeforeLast: string | undefined;
while (sourceData !== null && destinationNodeName !== sourceData.previousNode) {
taskData =
that.runExecutionData!.resultData.runData[sourceData.previousNode][
@ -569,20 +586,29 @@ export class WorkflowDataProxy {
const previousNodeOutput = sourceData.previousNodeOutput || 0;
if (previousNodeOutput >= taskData.data!.main.length) {
// `Could not resolve as the defined node-output is not valid on node '${sourceData.previousNode}'.`
throw createExpressionError('Cant get data for expression', {
messageTemplate: 'Cant get data for expression under %%PARAMETER%%',
description: `Apologies, this is an internal error. See details for more information`,
causeDetailed: 'Referencing a non-existent output on a node, problem with source data',
});
throw createExpressionError(
'Cant get data for expression',
{
messageTemplate: 'Cant get data for expression under %%PARAMETER%%',
description: `Apologies, this is an internal error. See details for more information`,
causeDetailed:
'Referencing a non-existent output on a node, problem with source data',
},
nodeBeforeLast,
);
}
if (pairedItem.item >= taskData.data!.main[previousNodeOutput]!.length) {
// `Could not resolve as the defined item index is not valid on node '${sourceData.previousNode}'.
throw createExpressionError('Cant get data for expression', {
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Item points to an item which does not exist`,
causeDetailed: `The pairedItem data points to an item ${pairedItem.item} which does not exist on node ${sourceData.previousNode} (output node did probably supply a wrong one)`,
});
throw createExpressionError(
'Cant get data for expression',
{
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Item points to an item which does not exist`,
causeDetailed: `The pairedItem data points to an item ${pairedItem.item} which does not exist on node ${sourceData.previousNode} (output node did probably supply a wrong one)`,
},
nodeBeforeLast,
);
}
const itemPreviousNode: INodeExecutionData =
@ -590,11 +616,15 @@ export class WorkflowDataProxy {
if (itemPreviousNode.pairedItem === undefined) {
// `Could not resolve, as pairedItem data is missing on node '${sourceData.previousNode}'.`,
throw createExpressionError('Cant get data for expression', {
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `To fetch the data from other nodes that this expression needs, more information is needed from the node ${sourceData.previousNode}`,
causeDetailed: `Missing pairedItem data (node ${sourceData.previousNode} did probably not supply it)`,
});
throw createExpressionError(
'Cant get data for expression',
{
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `To fetch the data from other nodes that this expression needs, more information is needed from the node ${sourceData.previousNode}`,
causeDetailed: `Missing pairedItem data (node ${sourceData.previousNode} did probably not supply it)`,
},
sourceData.previousNode,
);
}
if (Array.isArray(itemPreviousNode.pairedItem)) {
@ -647,22 +677,31 @@ export class WorkflowDataProxy {
});
}
// `Could not resolve pairedItem as the defined node input '${itemInput}' does not exist on node '${sourceData.previousNode}'.`
throw createExpressionError('Cant get data for expression', {
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Item points to a node input which does not exist`,
causeDetailed: `The pairedItem data points to a node input ${itemInput} which does not exist on node ${sourceData.previousNode} (node did probably supply a wrong one)`,
});
throw createExpressionError(
'Cant get data for expression',
{
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Item points to a node input which does not exist`,
causeDetailed: `The pairedItem data points to a node input ${itemInput} which does not exist on node ${sourceData.previousNode} (node did probably supply a wrong one)`,
},
nodeBeforeLast,
);
}
nodeBeforeLast = sourceData.previousNode;
sourceData = taskData.source[pairedItem.input || 0] || null;
}
if (sourceData === null) {
// 'Could not resolve, proably no pairedItem exists.'
throw createExpressionError('Cant get data for expression', {
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Could not resolve, proably no pairedItem exists`,
});
throw createExpressionError(
'Cant get data for expression',
{
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Could not resolve, proably no pairedItem exists`,
},
nodeBeforeLast,
);
}
taskData =
@ -682,11 +721,15 @@ export class WorkflowDataProxy {
if (pairedItem.item >= taskData.data!.main[previousNodeOutput]!.length) {
// `Could not resolve pairedItem as the item with the index '${pairedItem.item}' does not exist on node '${sourceData.previousNode}'.`
throw createExpressionError('Cant get data for expression', {
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Item points to an item which does not exist`,
causeDetailed: `The pairedItem data points to an item ${pairedItem.item} which does not exist on node ${sourceData.previousNode} (output node did probably supply a wrong one)`,
});
throw createExpressionError(
'Cant get data for expression',
{
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Item points to an item which does not exist`,
causeDetailed: `The pairedItem data points to an item ${pairedItem.item} which does not exist on node ${sourceData.previousNode} (output node did probably supply a wrong one)`,
},
nodeBeforeLast,
);
}
return taskData.data!.main[previousNodeOutput]![pairedItem.item];