import { ERROR_TRIGGER_NODE_TYPE , PLACEHOLDER_FILLED_AT_EXECUTION_TIME , PLACEHOLDER_EMPTY_WORKFLOW_ID , START_NODE_TYPE , WEBHOOK_NODE_TYPE , } from '@/constants' ; import { IConnections , IDataObject , INode , INodeExecutionData , INodeIssues , INodeParameters , NodeParameterValue , INodeCredentials , INodeType , INodeTypes , INodeTypeData , INodeTypeDescription , INodeVersionedType , IRunData , IRunExecutionData , IWorfklowIssues , IWorkflowDataProxyAdditionalKeys , TelemetryHelpers , Workflow , NodeHelpers , } from 'n8n-workflow' ; import { IExecutionResponse , INodeTypesMaxCount , INodeUi , IWorkflowData , IWorkflowDb , IWorkflowDataUpdate , XYPosition , ITag , IUpdateInformation , } from '../../Interface' ; import { externalHooks } from '@/components/mixins/externalHooks' ; import { restApi } from '@/components/mixins/restApi' ; import { nodeHelpers } from '@/components/mixins/nodeHelpers' ; import { showMessage } from '@/components/mixins/showMessage' ; import { isEqual } from 'lodash' ; import mixins from 'vue-typed-mixins' ; import { v4 as uuidv4 } from 'uuid' ; export const workflowHelpers = mixins ( externalHooks , nodeHelpers , restApi , showMessage ) . extend ( { methods : { connectionInputData ( parentNode , inputName , runIndex , inputIndex ) { let connectionInputData = null ; if ( parentNode . length ) { const parentNodeName = parentNode [ 0 ] ; const workflowRunData = this . $store . getters . getWorkflowRunData as IRunData | null ; if ( workflowRunData === null ) { return null ; } if ( ! workflowRunData [ parentNodeName ] || workflowRunData [ parentNodeName ] . length <= runIndex || ! workflowRunData [ parentNodeName ] [ runIndex ] . hasOwnProperty ( 'data' ) || workflowRunData [ parentNodeName ] [ runIndex ] . data === undefined || ! workflowRunData [ parentNodeName ] [ runIndex ] . data ! . hasOwnProperty ( inputName ) || workflowRunData [ parentNodeName ] [ runIndex ] . data ! [ inputName ] . length <= inputIndex ) { connectionInputData = [ ] ; } else { connectionInputData = workflowRunData [ parentNodeName ] [ runIndex ] . data ! [ inputName ] [ inputIndex ] ; } } return connectionInputData ; } , getNodes ( ) { const nodes = this . $store . getters . allNodes ; const returnNodes = [ ] ; for ( const node of nodes ) { returnNodes . push ( Object . assign ( { } , node ) ) ; } return returnNodes ; } , getNodeTypesMaxCount ( ) { const nodes = this . $store . getters . allNodes ; const returnData = { } ; const nodeTypes = this . $store . getters . allNodeTypes ; for ( const nodeType of nodeTypes ) { if ( nodeType . maxNodes !== undefined ) { returnData [ nodeType . name ] = { exist : 0 , max : nodeType . maxNodes , nodeNames : [ ] , } ; } } for ( const node of nodes ) { if ( returnData [ node . type ] !== undefined ) { returnData [ node . type ] . exist += 1 ; returnData [ node . type ] . nodeNames . push ( node . name ) ; } } return returnData ; } , getNodeTypeCount ( nodeType ) { const nodes = this . $store . getters . allNodes ; let count = 0 ; for ( const node of nodes ) { if ( node . type === nodeType ) { count ++ ; } } return count ; } , checkReadyForExecution ( workflow , lastNodeName ? ) { let node ; let nodeType ; let nodeIssues = null ; const workflowIssues = { } ; let checkNodes = Object . keys ( workflow . nodes ) ; if ( lastNodeName ) { checkNodes = workflow . getParentNodes ( lastNodeName ) ; checkNodes . push ( lastNodeName ) ; } else { let checkWebhook = [ ] ; for ( const nodeName of Object . keys ( workflow . nodes ) ) { if ( workflow . nodes [ nodeName ] . disabled !== true && workflow . nodes [ nodeName ] . type === WEBHOOK_NODE_TYPE ) { checkWebhook = [ nodeName , ... checkWebhook , ... workflow . getChildNodes ( nodeName ) ] ; } } if ( checkWebhook . length ) { checkNodes = checkWebhook ; } else { const startNode = workflow . getStartNode ( ) ; if ( startNode !== undefined ) { checkNodes = workflow . getChildNodes ( startNode . name ) ; checkNodes . push ( startNode . name ) ; } } } for ( const nodeName of checkNodes ) { nodeIssues = null ; node = workflow . nodes [ nodeName ] ; if ( node . disabled === true ) { continue ; } nodeType = workflow . nodeTypes . getByNameAndVersion ( node . type , node . typeVersion ) ; if ( nodeType === undefined ) { nodeIssues = { typeUnknown : true , } ; } else { nodeIssues = this . getNodeIssues ( nodeType . description , node , [ 'execution' ] ) ; } if ( nodeIssues !== null ) { workflowIssues [ node . name ] = nodeIssues ; } } if ( Object . keys ( workflowIssues ) . length === 0 ) { return null ; } return workflowIssues ; } , getWorkflow ( nodes ? , connections ? , copyData ? ) { nodes = nodes || this . getNodes ( ) ; connections = connections || ( this . $store . getters . allConnections as IConnections ) ; const nodeTypes = { nodeTypes : { } , init : async ( nodeTypes ? ) : Promise < void > => { } , getAll : ( ) : Array < INodeType | INodeVersionedType > => { return [ ] ; } , getByName : ( nodeType ) : INodeType | INodeVersionedType | undefined => { const nodeTypeDescription = this . $store . getters . nodeType ( nodeType ) as INodeTypeDescription | null ; if ( nodeTypeDescription === null ) { return undefined ; } return { description : nodeTypeDescription , } ; } , getByNameAndVersion : ( nodeType , version ? ) : INodeType | undefined => { const nodeTypeDescription = this . $store . getters . nodeType ( nodeType , version ) as INodeTypeDescription | null ; if ( nodeTypeDescription === null ) { return undefined ; } return { description : nodeTypeDescription , trigger : ! [ ERROR_TRIGGER_NODE_TYPE , START_NODE_TYPE ] . includes ( nodeType ) && nodeTypeDescription . inputs . length === 0 && ! nodeTypeDescription . webhooks || undefined , } ; } , } ; let workflowId = this . $store . getters . workflowId ; if ( workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID ) { workflowId = undefined ; } 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 } ) ; } else { return new Workflow ( { id : workflowId , name : workflowName , nodes , connections , active : false , nodeTypes } ) ; } } , getWorkflowDataToSave ( ) { const workflowNodes = this . $store . getters . allNodes ; const workflowConnections = this . $store . getters . allConnections ; let nodeData ; const nodes = [ ] ; for ( let nodeIndex = 0 ; nodeIndex < workflowNodes . length ; nodeIndex ++ ) { try { nodeData = this . getNodeDataToSave ( workflowNodes [ nodeIndex ] ) ; } catch ( e ) { return Promise . reject ( e ) ; } nodes . push ( nodeData ) ; } const data = { name : this . $store . getters . workflowName , nodes , connections : workflowConnections , active : this . $store . getters . isActive , settings : this . $store . getters . workflowSettings , tags : this . $store . getters . workflowTags , } ; const workflowId = this . $store . getters . workflowId ; if ( workflowId !== PLACEHOLDER_EMPTY_WORKFLOW_ID ) { data . id = workflowId ; } return Promise . resolve ( data ) ; } , getNodeDataToSave ( node ) { const skipKeys = [ 'color' , 'continueOnFail' , 'credentials' , 'disabled' , 'issues' , 'notes' , 'parameters' , 'status' , ] ; const nodeData = { parameters : { } , } ; for ( const key in node ) { if ( key . charAt ( 0 ) !== '_' && skipKeys . indexOf ( key ) === - 1 ) { nodeData [ key ] = node [ key ] ; } } const nodeType = this . $store . getters . nodeType ( node . type , node . typeVersion ) as INodeTypeDescription | null ; if ( nodeType !== null ) { const nodeParameters = NodeHelpers . getNodeParameters ( nodeType . properties , node . parameters , false , false ) ; nodeData . parameters = nodeParameters !== null ? nodeParameters : { } ; if ( node . credentials !== undefined && nodeType . credentials !== undefined ) { const saveCredenetials = { } ; for ( const nodeCredentialTypeName of Object . keys ( node . credentials ) ) { const credentialTypeDescription = nodeType . credentials . find ( ( credentialTypeDescription ) => credentialTypeDescription . name === nodeCredentialTypeName ) ; if ( credentialTypeDescription === undefined ) { continue ; } if ( this . displayParameter ( node . parameters , credentialTypeDescription , '' ) === false ) { continue ; } saveCredenetials [ nodeCredentialTypeName ] = node . credentials [ nodeCredentialTypeName ] ; } if ( Object . keys ( saveCredenetials ) . length !== 0 ) { nodeData . credentials = saveCredenetials ; } } } else { nodeData . credentials = node . credentials ; nodeData . parameters = node . parameters ; if ( nodeData . color !== undefined ) { nodeData . color = node . color ; } } if ( node . disabled === true ) { nodeData . disabled = true ; } if ( node . continueOnFail === true ) { nodeData . continueOnFail = true ; } if ( ! [ undefined , '' ] . includes ( node . notes ) ) { nodeData . notes = node . notes ; } return nodeData ; } , resolveParameter ( parameter ) { const itemIndex = 0 ; const runIndex = 0 ; const inputName = 'main' ; const activeNode = this . $store . getters . activeNode ; const workflow = this . getWorkflow ( ) ; const parentNode = workflow . getParentNodes ( activeNode . name , inputName , 1 ) ; const executionData = this . $store . getters . getWorkflowExecution as IExecutionResponse | null ; const inputIndex = workflow . getNodeConnectionOutputIndex ( activeNode ! . name , parentNode [ 0 ] ) || 0 ; let connectionInputData = this . connectionInputData ( parentNode , inputName , runIndex , inputIndex ) ; let runExecutionData ; if ( executionData === null ) { runExecutionData = { resultData : { runData : { } , } , } ; } else { runExecutionData = executionData . data ; } if ( connectionInputData === null ) { connectionInputData = [ ] ; } const additionalKeys = { $executionId : PLACEHOLDER_FILLED_AT_EXECUTION_TIME , $resumeWebhookUrl : PLACEHOLDER_FILLED_AT_EXECUTION_TIME , } ; return workflow . expression . getParameterValue ( parameter , runExecutionData , runIndex , itemIndex , activeNode . name , connectionInputData , 'manual' , additionalKeys , false ) as IDataObject ; } , resolveExpression ( expression , siblingParameters = { } ) { const parameters = { '__xxxxxxx__' : expression , ... siblingParameters , } ; const returnData = this . resolveParameter ( parameters ) as IDataObject ; if ( typeof returnData [ '__xxxxxxx__' ] === 'object' ) { const workflow = this . getWorkflow ( ) ; return workflow . expression . convertObjectValueToString ( returnData [ '__xxxxxxx__' ] as object ) ; } return returnData [ '__xxxxxxx__' ] ; } , async saveCurrentWorkflow ( { name , tags } = { } ) { const currentWorkflow = this . $route . params . name ; if ( ! currentWorkflow ) { return this . saveAsNewWorkflow ( { name , tags } ) ; } try { this . $store . commit ( 'addActiveAction' , 'workflowSaving' ) ; const workflowDataRequest = await this . getWorkflowDataToSave ( ) ; if ( name ) { workflowDataRequest . name = name . trim ( ) ; } if ( tags ) { workflowDataRequest . tags = tags ; } const workflowData = await this . restApi ( ) . updateWorkflow ( currentWorkflow , workflowDataRequest ) ; if ( name ) { this . $store . commit ( 'setWorkflowName' , { newName : workflowData . name } ) ; } if ( tags ) { const createdTags = ( workflowData . tags || [ ] ) as ITag [ ] ; const tagIds = createdTags . map ( ( tag ) : string => tag . id ) ; this . $store . commit ( 'setWorkflowTagIds' , tagIds ) ; } this . $store . commit ( 'setStateDirty' , false ) ; this . $store . commit ( 'removeActiveAction' , 'workflowSaving' ) ; this . $externalHooks ( ) . run ( 'workflow.afterUpdate' , { workflowData } ) ; return true ; } catch ( e ) { this . $store . commit ( 'removeActiveAction' , 'workflowSaving' ) ; this . $showMessage ( { title : this . $locale . baseText ( 'workflowHelpers.showMessage.title' ) , message : this . $locale . baseText ( 'workflowHelpers.showMessage.message' ) + `"${ e . message }"` , type : 'error' , } ) ; return false ; } } , async saveAsNewWorkflow ( { name , tags , resetWebhookUrls , openInNewWindow } = { } ) { try { this . $store . commit ( 'addActiveAction' , 'workflowSaving' ) ; const workflowDataRequest = await this . getWorkflowDataToSave ( ) ; workflowDataRequest . active = false ; const changedNodes = { } as IDataObject ; if ( resetWebhookUrls ) { workflowDataRequest . nodes = workflowDataRequest . nodes ! . map ( node => { if ( node . webhookId ) { node . webhookId = uuidv4 ( ) ; changedNodes [ node . name ] = node . webhookId ; } return node ; } ) ; } if ( name ) { workflowDataRequest . name = name . trim ( ) ; } if ( tags ) { workflowDataRequest . tags = tags ; } const workflowData = await this . restApi ( ) . createNewWorkflow ( workflowDataRequest ) ; if ( openInNewWindow ) { const routeData = this . $router . resolve ( { name : 'NodeViewExisting' , params : { name : workflowData . id } } ) ; window . open ( routeData . href , '_blank' ) ; this . $store . commit ( 'removeActiveAction' , 'workflowSaving' ) ; return true ; } this . $store . commit ( 'setActive' , workflowData . active || false ) ; this . $store . commit ( 'setWorkflowId' , workflowData . id ) ; this . $store . commit ( 'setWorkflowName' , { newName : workflowData . name , setStateDirty : false } ) ; this . $store . commit ( 'setWorkflowSettings' , workflowData . settings || { } ) ; this . $store . commit ( 'setStateDirty' , false ) ; Object . keys ( changedNodes ) . forEach ( ( nodeName ) => { const changes = { key : 'webhookId' , value : changedNodes [ nodeName ] , name : nodeName , } as IUpdateInformation ; this . $store . commit ( 'setNodeValue' , changes ) ; } ) ; const createdTags = ( workflowData . tags || [ ] ) as ITag [ ] ; const tagIds = createdTags . map ( ( tag ) : string => tag . id ) ; this . $store . commit ( 'setWorkflowTagIds' , tagIds ) ; this . $router . push ( { name : 'NodeViewExisting' , params : { name : workflowData . id as string , action : 'workflowSave' } , } ) ; this . $store . commit ( 'removeActiveAction' , 'workflowSaving' ) ; this . $store . commit ( 'setStateDirty' , false ) ; this . $externalHooks ( ) . run ( 'workflow.afterUpdate' , { workflowData } ) ; return true ; } catch ( e ) { this . $store . commit ( 'removeActiveAction' , 'workflowSaving' ) ; this . $showMessage ( { title : this . $locale . baseText ( 'workflowHelpers.showMessage.title' ) , message : this . $locale . baseText ( 'workflowHelpers.showMessage.message' ) + `"${ e . message }"` , type : 'error' , } ) ; return false ; } } , updateNodePositions ( workflowData , position ) { if ( workflowData . nodes === undefined ) { return ; } const minPosition = [ 99999999 , 99999999 ] ; for ( const node of workflowData . nodes ) { if ( node . position [ 1 ] < minPosition [ 1 ] ) { minPosition [ 0 ] = node . position [ 0 ] ; minPosition [ 1 ] = node . position [ 1 ] ; } else if ( node . position [ 1 ] === minPosition [ 1 ] ) { if ( node . position [ 0 ] < minPosition [ 0 ] ) { minPosition [ 0 ] = node . position [ 0 ] ; minPosition [ 1 ] = node . position [ 1 ] ; } } } const offsetPosition = [ position [ 0 ] - minPosition [ 0 ] , position [ 1 ] - minPosition [ 1 ] ] ; for ( const node of workflowData . nodes ) { node . position [ 0 ] += offsetPosition [ 0 ] ; node . position [ 1 ] += offsetPosition [ 1 ] ; } } , async dataHasChanged ( id ) { const currentData = await this . getWorkflowDataToSave ( ) ; const data = await this . restApi ( ) . getWorkflow ( id ) ; if ( data !== undefined ) { const x = { nodes : data . nodes , connections : data . connections , settings : data . settings , name : data . name , } ; const y = { nodes : currentData . nodes , connections : currentData . connections , settings : currentData . settings , name : currentData . name , } ; return ! isEqual ( x , y ) ; } return true ; } , } , } ) ;