2022-09-13 08:09:16 -07:00
/* eslint-disable @typescript-eslint/no-use-before-define */
2023-07-31 02:00:48 -07:00
2021-09-21 10:38:24 -07:00
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
2023-07-31 02:00:48 -07:00
2019-06-23 03:35:23 -07:00
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-for-in-array */
2023-07-31 02:00:48 -07:00
2020-02-15 17:07:01 -08:00
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
2022-02-05 13:55:43 -08:00
2024-09-18 00:19:33 -07:00
import {
MANUAL_CHAT_TRIGGER_LANGCHAIN_NODE_TYPE ,
NODES_WITH_RENAMABLE_CONTENT ,
STARTING_NODE_TYPES ,
} from './Constants' ;
import type { IDeferredPromise } from './DeferredPromise' ;
import { ApplicationError } from './errors/application.error' ;
import { Expression } from './Expression' ;
import { getGlobalState } from './GlobalState' ;
2023-01-27 05:56:56 -08:00
import type {
2019-06-23 03:35:23 -07:00
IConnections ,
2021-11-05 09:45:51 -07:00
IExecuteResponsePromiseData ,
2019-08-08 11:38:25 -07:00
IGetExecuteTriggerFunctions ,
2019-06-23 03:35:23 -07:00
INode ,
INodeExecuteFunctions ,
INodeExecutionData ,
INodeIssues ,
2019-12-31 12:19:37 -08:00
INodeParameters ,
2020-10-22 06:46:03 -07:00
INodes ,
2019-06-23 03:35:23 -07:00
INodeType ,
INodeTypes ,
2022-07-22 03:19:45 -07:00
IPinData ,
2019-12-31 12:19:37 -08:00
IPollFunctions ,
2019-06-23 03:35:23 -07:00
IRunExecutionData ,
ITaskDataConnections ,
ITriggerResponse ,
IWebhookData ,
2019-10-11 04:02:44 -07:00
IWebhookResponseData ,
2022-11-30 03:16:19 -08:00
IWorkflowIssues ,
2019-06-23 03:35:23 -07:00
IWorkflowExecuteAdditionalData ,
IWorkflowSettings ,
2019-12-31 12:19:37 -08:00
WebhookSetupMethodNames ,
2021-03-23 11:08:47 -07:00
WorkflowActivateMode ,
2019-12-31 12:19:37 -08:00
WorkflowExecuteMode ,
2022-05-30 03:16:44 -07:00
IConnection ,
IConnectedNode ,
2022-06-03 08:25:07 -07:00
IDataObject ,
IExecuteData ,
INodeConnection ,
2022-05-30 03:16:44 -07:00
IObservableObject ,
IRun ,
IRunNodeResponse ,
2022-09-21 06:44:45 -07:00
NodeParameterValueType ,
2023-12-21 05:21:09 -08:00
CloseFunction ,
2024-02-14 01:42:55 -08:00
INodeOutputConfiguration ,
2022-05-30 03:16:44 -07:00
} from './Interfaces' ;
2024-02-14 01:42:55 -08:00
import { Node , NodeConnectionType } from './Interfaces' ;
2022-09-23 07:14:28 -07:00
import * as NodeHelpers from './NodeHelpers' ;
import * as ObservableObject from './ObservableObject' ;
import { RoutingNode } from './RoutingNode' ;
2022-05-23 08:56:15 -07:00
function dedupe < T > ( arr : T [ ] ) : T [ ] {
return [ . . . new Set ( arr ) ] ;
}
2019-06-23 03:35:23 -07:00
2024-06-27 02:20:59 -07:00
export interface WorkflowParameters {
id? : string ;
name? : string ;
nodes : INode [ ] ;
connections : IConnections ;
active : boolean ;
nodeTypes : INodeTypes ;
staticData? : IDataObject ;
settings? : IWorkflowSettings ;
pinData? : IPinData ;
}
2019-06-23 03:35:23 -07:00
export class Workflow {
2023-09-25 09:04:52 -07:00
id : string ;
2021-08-29 11:58:11 -07:00
2020-02-15 17:07:01 -08:00
name : string | undefined ;
2021-08-29 11:58:11 -07:00
2019-06-23 03:35:23 -07:00
nodes : INodes = { } ;
2021-08-29 11:58:11 -07:00
2019-06-23 03:35:23 -07:00
connectionsBySourceNode : IConnections ;
2021-08-29 11:58:11 -07:00
2019-06-23 03:35:23 -07:00
connectionsByDestinationNode : IConnections ;
2021-08-29 11:58:11 -07:00
2019-06-23 03:35:23 -07:00
nodeTypes : INodeTypes ;
2021-08-29 11:58:11 -07:00
2020-09-12 03:16:07 -07:00
expression : Expression ;
2021-08-29 11:58:11 -07:00
2019-06-23 03:35:23 -07:00
active : boolean ;
2021-08-29 11:58:11 -07:00
2019-06-23 03:35:23 -07:00
settings : IWorkflowSettings ;
2024-07-16 11:42:48 -07:00
readonly timezone : string ;
2019-06-23 03:35:23 -07:00
// To save workflow specific static data like for example
2022-09-02 07:13:17 -07:00
// ids of registered webhooks of nodes
2019-06-23 03:35:23 -07:00
staticData : IDataObject ;
2024-02-01 09:05:23 -08:00
testStaticData : IDataObject | undefined ;
2022-07-22 03:19:45 -07:00
pinData? : IPinData ;
2024-06-27 02:20:59 -07:00
constructor ( parameters : WorkflowParameters ) {
2023-12-14 09:13:12 -08:00
this . id = parameters . id as string ; // @tech_debt Ensure this is not optional
2020-02-15 17:07:01 -08:00
this . name = parameters . name ;
this . nodeTypes = parameters . nodeTypes ;
2022-07-22 03:19:45 -07:00
this . pinData = parameters . pinData ;
2019-06-23 03:35:23 -07:00
// Save nodes in workflow as object to be able to get the
// nodes easily by its name.
// Also directly add the default values of the node type.
let nodeType : INodeType | undefined ;
2020-02-15 17:07:01 -08:00
for ( const node of parameters . nodes ) {
2019-06-23 03:35:23 -07:00
this . nodes [ node . name ] = node ;
2021-09-21 10:38:24 -07:00
nodeType = this . nodeTypes . getByNameAndVersion ( node . type , node . typeVersion ) ;
2019-06-23 03:35:23 -07:00
if ( nodeType === undefined ) {
// Go on to next node when its type is not known.
// For now do not error because that causes problems with
// expression resolution also then when the unknown node
// does not get used.
continue ;
2023-11-30 03:46:45 -08:00
// throw new ApplicationError(`Node with unknown node type`, {
// tags: { nodeType: node.type },
// extra: { node },
// });
2019-06-23 03:35:23 -07:00
}
// Add default values
const nodeParameters = NodeHelpers . getNodeParameters (
nodeType . description . properties ,
node . parameters ,
true ,
false ,
2022-04-28 10:04:09 -07:00
node ,
2019-06-23 03:35:23 -07:00
) ;
node . parameters = nodeParameters !== null ? nodeParameters : { } ;
}
2020-02-15 17:07:01 -08:00
this . connectionsBySourceNode = parameters . connections ;
2019-06-23 03:35:23 -07:00
2022-09-02 07:13:17 -07:00
// Save also the connections by the destination nodes
2020-02-15 17:07:01 -08:00
this . connectionsByDestinationNode = this . __getConnectionsByDestination ( parameters . connections ) ;
2019-06-23 03:35:23 -07:00
2020-02-15 17:07:01 -08:00
this . active = parameters . active || false ;
2019-06-23 03:35:23 -07:00
2020-02-15 17:07:01 -08:00
this . staticData = ObservableObject . create ( parameters . staticData || { } , undefined , {
ignoreEmptyOnFirstChild : true ,
} ) ;
2019-06-23 03:35:23 -07:00
2020-02-15 17:07:01 -08:00
this . settings = parameters . settings || { } ;
2024-07-16 11:42:48 -07:00
this . timezone = this . settings . timezone ? ? getGlobalState ( ) . defaultTimezone ;
2020-09-12 03:16:07 -07:00
this . expression = new Expression ( this ) ;
2019-06-23 03:35:23 -07:00
}
/ * *
* The default connections are by source node . This function rewrites them by destination nodes
* to easily find parent nodes .
*
* /
__getConnectionsByDestination ( connections : IConnections ) : IConnections {
const returnConnection : IConnections = { } ;
let connectionInfo ;
let maxIndex : number ;
for ( const sourceNode in connections ) {
if ( ! connections . hasOwnProperty ( sourceNode ) ) {
continue ;
}
2024-06-03 07:33:20 -07:00
for ( const type of Object . keys ( connections [ sourceNode ] ) as NodeConnectionType [ ] ) {
2019-06-23 03:35:23 -07:00
if ( ! connections [ sourceNode ] . hasOwnProperty ( type ) ) {
continue ;
}
for ( const inputIndex in connections [ sourceNode ] [ type ] ) {
if ( ! connections [ sourceNode ] [ type ] . hasOwnProperty ( inputIndex ) ) {
continue ;
}
2024-11-06 06:16:47 -08:00
for ( connectionInfo of connections [ sourceNode ] [ type ] [ inputIndex ] ? ? [ ] ) {
2019-06-23 03:35:23 -07:00
if ( ! returnConnection . hasOwnProperty ( connectionInfo . node ) ) {
returnConnection [ connectionInfo . node ] = { } ;
}
if ( ! returnConnection [ connectionInfo . node ] . hasOwnProperty ( connectionInfo . type ) ) {
returnConnection [ connectionInfo . node ] [ connectionInfo . type ] = [ ] ;
}
maxIndex = returnConnection [ connectionInfo . node ] [ connectionInfo . type ] . length - 1 ;
for ( let j = maxIndex ; j < connectionInfo . index ; j ++ ) {
returnConnection [ connectionInfo . node ] [ connectionInfo . type ] . push ( [ ] ) ;
}
returnConnection [ connectionInfo . node ] [ connectionInfo . type ] [ connectionInfo . index ] . push ( {
node : sourceNode ,
type ,
2020-10-22 06:46:03 -07:00
index : parseInt ( inputIndex , 10 ) ,
2019-06-23 03:35:23 -07:00
} ) ;
}
}
}
}
return returnConnection ;
}
/ * *
* A workflow can only be activated if it has a node which has either triggers
* or webhooks defined .
*
* @param { string [ ] } [ ignoreNodeTypes ] Node - types to ignore in the check
* /
checkIfWorkflowCanBeActivated ( ignoreNodeTypes? : string [ ] ) : boolean {
let node : INode ;
let nodeType : INodeType | undefined ;
for ( const nodeName of Object . keys ( this . nodes ) ) {
node = this . nodes [ nodeName ] ;
if ( node . disabled === true ) {
// Deactivated nodes can not trigger a run so ignore
continue ;
}
if ( ignoreNodeTypes !== undefined && ignoreNodeTypes . includes ( node . type ) ) {
continue ;
}
2021-09-21 10:38:24 -07:00
nodeType = this . nodeTypes . getByNameAndVersion ( node . type , node . typeVersion ) ;
2019-06-23 03:35:23 -07:00
if ( nodeType === undefined ) {
// Type is not known so check is not possible
continue ;
}
2019-12-31 12:19:37 -08:00
if (
nodeType . poll !== undefined ||
nodeType . trigger !== undefined ||
nodeType . webhook !== undefined
) {
2019-06-23 03:35:23 -07:00
// Is a trigger node. So workflow can be activated.
return true ;
}
}
return false ;
}
/ * *
* Checks if everything in the workflow is complete
* and ready to be executed . If it returns null everything
* is fine . If there are issues it returns the issues
* which have been found for the different nodes .
* TODO : Does currently not check for credential issues !
* /
2024-06-27 02:20:59 -07:00
checkReadyForExecution (
inputData : {
startNode? : string ;
destinationNode? : string ;
pinDataNodeNames? : string [ ] ;
} = { } ,
) : IWorkflowIssues | null {
2022-11-30 03:16:19 -08:00
const workflowIssues : IWorkflowIssues = { } ;
2019-06-23 03:35:23 -07:00
2021-10-06 10:02:31 -07:00
let checkNodes : string [ ] = [ ] ;
if ( inputData . destinationNode ) {
// If a destination node is given we have to check all the nodes
// leading up to it
checkNodes = this . getParentNodes ( inputData . destinationNode ) ;
checkNodes . push ( inputData . destinationNode ) ;
} else if ( inputData . startNode ) {
// If a start node is given we have to check all nodes which
// come after it
checkNodes = this . getChildNodes ( inputData . startNode ) ;
checkNodes . push ( inputData . startNode ) ;
}
for ( const nodeName of checkNodes ) {
2024-06-27 02:20:59 -07:00
let nodeIssues : INodeIssues | null = null ;
const node = this . nodes [ nodeName ] ;
2019-06-23 03:35:23 -07:00
if ( node . disabled === true ) {
continue ;
}
2024-06-27 02:20:59 -07:00
const nodeType = this . nodeTypes . getByNameAndVersion ( node . type , node . typeVersion ) ;
2019-06-23 03:35:23 -07:00
if ( nodeType === undefined ) {
// Node type is not known
nodeIssues = {
typeUnknown : true ,
} ;
} else {
2022-07-20 08:50:39 -07:00
nodeIssues = NodeHelpers . getNodeParametersIssues (
nodeType . description . properties ,
node ,
inputData . pinDataNodeNames ,
) ;
2019-06-23 03:35:23 -07:00
}
if ( nodeIssues !== null ) {
workflowIssues [ node . name ] = nodeIssues ;
}
}
if ( Object . keys ( workflowIssues ) . length === 0 ) {
return null ;
}
return workflowIssues ;
}
/ * *
* Returns the static data of the workflow .
* It gets saved with the workflow and will be the same for
* all workflow - executions .
*
* @param { string } type The type of data to return ( "global" | "node" )
* @param { INode } [ node ] If type is set to "node" then the node has to be provided
* /
getStaticData ( type : string , node? : INode ) : IDataObject {
let key : string ;
if ( type === 'global' ) {
key = 'global' ;
} else if ( type === 'node' ) {
if ( node === undefined ) {
2023-11-30 03:46:45 -08:00
throw new ApplicationError (
2022-12-29 03:20:43 -08:00
'The request data of context type "node" the node parameter has to be set!' ,
2019-06-23 03:35:23 -07:00
) ;
}
key = ` node: ${ node . name } ` ;
} else {
2023-11-30 03:46:45 -08:00
throw new ApplicationError ( 'Unknown context type. Only `global` and `node` are supported.' , {
extra : { contextType : type } ,
} ) ;
2019-06-23 03:35:23 -07:00
}
2024-02-01 09:05:23 -08:00
if ( this . testStaticData ? . [ key ] ) return this . testStaticData [ key ] as IDataObject ;
2019-06-23 03:35:23 -07:00
if ( this . staticData [ key ] === undefined ) {
// Create it as ObservableObject that we can easily check if the data changed
// to know if the workflow with its data has to be saved afterwards or not.
this . staticData [ key ] = ObservableObject . create ( { } , this . staticData as IObservableObject ) ;
}
return this . staticData [ key ] as IDataObject ;
}
2024-02-01 09:05:23 -08:00
setTestStaticData ( testStaticData : IDataObject ) {
this . testStaticData = testStaticData ;
}
2019-06-23 03:35:23 -07:00
/ * *
* Returns all the trigger nodes in the workflow .
*
* /
getTriggerNodes ( ) : INode [ ] {
2019-12-31 12:19:37 -08:00
return this . queryNodes ( ( nodeType : INodeType ) = > ! ! nodeType . trigger ) ;
}
/ * *
* Returns all the poll nodes in the workflow
*
* /
getPollNodes ( ) : INode [ ] {
return this . queryNodes ( ( nodeType : INodeType ) = > ! ! nodeType . poll ) ;
}
/ * *
* Returns all the nodes in the workflow for which the given
* checkFunction return true
*
* @param { ( nodeType : INodeType ) = > boolean } checkFunction
* /
queryNodes ( checkFunction : ( nodeType : INodeType ) = > boolean ) : INode [ ] {
2019-06-23 03:35:23 -07:00
const returnNodes : INode [ ] = [ ] ;
// Check if it has any of them
let node : INode ;
let nodeType : INodeType | undefined ;
for ( const nodeName of Object . keys ( this . nodes ) ) {
node = this . nodes [ nodeName ] ;
if ( node . disabled === true ) {
continue ;
}
2021-09-21 10:38:24 -07:00
nodeType = this . nodeTypes . getByNameAndVersion ( node . type , node . typeVersion ) ;
2019-06-23 03:35:23 -07:00
2019-12-31 12:19:37 -08:00
if ( nodeType !== undefined && checkFunction ( nodeType ) ) {
2019-06-23 03:35:23 -07:00
returnNodes . push ( node ) ;
}
}
return returnNodes ;
}
/ * *
* Returns the node with the given name if it exists else null
*
* @param { string } nodeName Name of the node to return
* /
getNode ( nodeName : string ) : INode | null {
if ( this . nodes . hasOwnProperty ( nodeName ) ) {
return this . nodes [ nodeName ] ;
}
return null ;
}
2022-07-22 03:19:45 -07:00
/ * *
* 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
* /
2024-06-27 01:49:53 -07:00
getPinDataOfNode ( nodeName : string ) : INodeExecutionData [ ] | undefined {
2022-07-22 03:19:45 -07:00
return this . pinData ? this . pinData [ nodeName ] : undefined ;
}
2023-05-02 00:37:49 -07:00
renameNodeInParameterValue (
2022-09-21 06:44:45 -07:00
parameterValue : NodeParameterValueType ,
2019-06-23 03:35:23 -07:00
currentName : string ,
newName : string ,
2023-05-02 00:37:49 -07:00
{ hasRenamableContent } = { hasRenamableContent : false } ,
2022-09-21 06:44:45 -07:00
) : NodeParameterValueType {
2019-06-23 03:35:23 -07:00
if ( typeof parameterValue !== 'object' ) {
// Reached the actual value
2023-05-02 00:37:49 -07:00
if (
typeof parameterValue === 'string' &&
( parameterValue . charAt ( 0 ) === '=' || hasRenamableContent )
) {
2019-06-23 03:35:23 -07:00
// Is expression so has to be rewritten
// To not run the "expensive" regex stuff when it is not needed
// make a simple check first if it really contains the the node-name
if ( parameterValue . includes ( currentName ) ) {
// Really contains node-name (even though we do not know yet if really as $node-expression)
2022-09-13 08:09:16 -07:00
const escapedOldName = backslashEscape ( currentName ) ; // for match
const escapedNewName = dollarEscape ( newName ) ; // for replacement
const setNewName = ( expression : string , oldPattern : string ) = >
expression . replace ( new RegExp ( oldPattern , 'g' ) , ` $ 1 ${ escapedNewName } $ 2 ` ) ;
if ( parameterValue . includes ( '$(' ) ) {
const oldPattern = String . raw ` ( \ $ \ (['"]) ${ escapedOldName } (['"] \ )) ` ;
parameterValue = setNewName ( parameterValue , oldPattern ) ;
}
if ( parameterValue . includes ( '$node[' ) ) {
const oldPattern = String . raw ` ( \ $ node \ [['"]) ${ escapedOldName } (['"] \ ]) ` ;
parameterValue = setNewName ( parameterValue , oldPattern ) ;
}
if ( parameterValue . includes ( '$node.' ) ) {
const oldPattern = String . raw ` ( \ $ node \ .) ${ escapedOldName } ( \ .?) ` ;
parameterValue = setNewName ( parameterValue , oldPattern ) ;
if ( hasDotNotationBannedChar ( newName ) ) {
const regex = new RegExp ( ` . ${ backslashEscape ( newName ) } ( | \\ .) ` , 'g' ) ;
parameterValue = parameterValue . replace ( regex , ` [" ${ escapedNewName } "] $ 1 ` ) ;
}
}
2019-06-23 03:35:23 -07:00
2022-09-13 08:09:16 -07:00
if ( parameterValue . includes ( '$items(' ) ) {
const oldPattern = String . raw ` ( \ $ items \ (['"]) ${ escapedOldName } (['"],|['"] \ )) ` ;
parameterValue = setNewName ( parameterValue , oldPattern ) ;
}
2019-06-23 03:35:23 -07:00
}
}
return parameterValue ;
}
if ( Array . isArray ( parameterValue ) ) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const returnArray : any [ ] = [ ] ;
for ( const currentValue of parameterValue ) {
feat(editor): Implement Resource Mapper component (#6207)
* :zap: scaffolding
* :zap: finished scaffolding
* :zap: renamed types
* :zap: updated subtitle
* :zap: renamed functions file, UI updates
* :zap: query parameters fixes, ui updates, refactoring
* :zap: fixes for credentials test, setup for error parsing
* :zap: rlc for schema and table, error handling tweaks
* :zap: delete operation, new options
* :zap: columns loader
* :zap: linter fixes
* :zap: where clauses setup
* :zap: logic for processing where clauses
* :zap: select operation
* :zap: refactoring
* :zap: data mode for insert and update, wip
* :zap: data mapping, insert update, skip on conflict option
* :zap: select columns with spaces fix
* :zap: update operation update, wip
* :zap: finished update operation
* :zap: upsert operation
* :zap: ui fixes
* Copy updates.
* Copy updates.
* :zap: option to convert empty strings to nulls, schema checks
* :zap: UI requested updates
* :zap: ssh setup WIP
* :zap: fixes, ssh WIP
* :zap: ssh fixes, credentials
* :zap: credentials testing update
* :zap: uncaught error fix
* :zap: clean up
* :zap: address in use fix
* :zap: improved error message
* :zap: tests setup
* :zap: unit tests wip
* :zap: config files clean up
* :zap: utils unit tests
* :zap: refactoring
* :zap: setup for testing operations, tests for deleteTable operation
* :zap: executeQuery and insert operations tests
* :zap: select, update, upsert operations tests
* :zap: runQueries tests setup
* :zap: hint to query
* Copy updates.
* :zap: ui fixes
* :zap: clean up
* :zap: error message update
* :zap: ui update
* Minor tweaks to query params decription.
* feat(Google Sheets Node): Implement Resource mapper in Google Sheets node (#5752)
* ✨ Added initial resource mapping support in google sheets node
* ✨ Wired mapping API endpoint with node-specific logic for fetching mapping fields
* ✨ Implementing mapping fields logic for google sheets
* ✨ Updating Google Sheets execute methods to support resource mapper fields
* 🚧 Added initial version of `ResourceLocator` component
* 👌 Added `update` mode to resource mapper modes
* 👌 Addressing PR feedback
* 👌 Removing leftover const reference
* 👕 Fixing lint errors
* :zap: singlton for conections
* :zap: credentials test fix, clean up
* feat(Postgres Node): Add resource mapper to new version of Postgres node (#5814)
* :zap: scaffolding
* :zap: finished scaffolding
* :zap: renamed types
* :zap: updated subtitle
* :zap: renamed functions file, UI updates
* :zap: query parameters fixes, ui updates, refactoring
* :zap: fixes for credentials test, setup for error parsing
* :zap: rlc for schema and table, error handling tweaks
* :zap: delete operation, new options
* :zap: columns loader
* :zap: linter fixes
* :zap: where clauses setup
* :zap: logic for processing where clauses
* :zap: select operation
* :zap: refactoring
* :zap: data mode for insert and update, wip
* :zap: data mapping, insert update, skip on conflict option
* :zap: select columns with spaces fix
* :zap: update operation update, wip
* :zap: finished update operation
* :zap: upsert operation
* :zap: ui fixes
* Copy updates.
* Copy updates.
* :zap: option to convert empty strings to nulls, schema checks
* :zap: UI requested updates
* :zap: ssh setup WIP
* :zap: fixes, ssh WIP
* :zap: ssh fixes, credentials
* :zap: credentials testing update
* :zap: uncaught error fix
* :zap: clean up
* :zap: address in use fix
* :zap: improved error message
* :zap: tests setup
* :zap: unit tests wip
* :zap: config files clean up
* :zap: utils unit tests
* :zap: refactoring
* :zap: setup for testing operations, tests for deleteTable operation
* :zap: executeQuery and insert operations tests
* :zap: select, update, upsert operations tests
* :zap: runQueries tests setup
* :zap: hint to query
* Copy updates.
* :zap: ui fixes
* :zap: clean up
* :zap: error message update
* :zap: ui update
* Minor tweaks to query params decription.
* ✨ Updated Postgres node to use resource mapper component
* ✨ Implemented postgres <-> resource mapper type mapping
* ✨ Updated Postgres node execution to use resource mapper fields in v3
* 🔥 Removing unused import
---------
Co-authored-by: Michael Kret <michael.k@radency.com>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
* feat(core): Resource editor componend P0 (#5970)
* ✨ Added inital value of mapping mode dropdown
* ✨ Finished mapping mode selector
* ✨ Finished implementing mapping mode selector
* ✨ Implemented 'Columns to match on' dropdown
* ✨ Implemented `loadOptionsDependOn` support in resource mapper
* ✨ Implemented initial version of mapping fields
* ✨ Implementing dependant fields watcher in new component setup
* ✨ Generating correct resource mapper field types. Added `supportAutoMap` to node specification and UI. Not showing fields with `display=false`. Pre-selecting matching columns if it's the only one
* ✨ Handling matching columns correctly in UI
* ✨ Saving and loading resourceMapper values in component
* ✨ Implemented proper data saving and loading
* ✨ ResourceMapper component refactor, fixing value save/load
* ✨ Refactoring MatchingColumnSelect component. Updating Sheets node to use single key match and Postgres to use multi key
* ✨ Updated Google Sheets node to work with the new UI
* ✨ Updating Postgres Node to work with new UI
* ✨ Additional loading indicator that shown if there is no mapping mode selector
* ✨ Removing hard-coded values, fixing matching columns ordering, refactoring
* ✨ Updating field names in nodes
* ✨ Fixing minor UI issues
* ✨ Implemented matching fields filter logic
* ✨ Moving loading label outside of fields list
* ✅ Added initial unit tests for resource mapper
* ✅ Finished default rendering test
* ✅ Test refactoring
* ✅ Finished unit tests
* 🔨 Updating the way i18n is used in resource mapper components
* ✔️ Fixing value to match on logic for postgres node
* ✨ Hiding mapping fields when auto-map mode is selected
* ✨ Syncing selected mapping mode between components
* ✨ Fixing dateTime input rendering and adding update check to Postgres node
* ✨ Properly handling database connections. Sending null for empty string values.
* 💄 Updated wording in the error message for non-existing rows
* ✨ Fixing issues with selected matching values
* ✔️ Updating unit tests after matching logic update
* ✨ Updating matching columns when new fields are loaded
* ✨ Defaulting to null for empty parameter values
* ✨ Allowing zero as valid value for number imputs
* ✨ Updated list of types that use datepicker as widger
* ✨ Using text inputs for time types
* ✨ Initial mapping field rework
* ✨ Added new component for mapping fields, moved bit of logic from root component to matching selector, fixing some lint errors
* ✨ Added tooltip for columns that cannot be deleted
* ✨ Saving deleted values in parameter value
* ✨ Implemented control to add/remove mapping fields
* ✨ Syncing field list with add field dropdown when changing dependent values
* ✨ Not showing removed fields in matching columns selector. Updating wording in matching columns selector description
* ✨ Implementing disabled states for add/remove all fields options
* ✨ Saving removed columns separately, updating copy
* ✨ Implemented resource mapper values validation
* ✨ Updated validation logic and error input styling
* ✨ Validating resource mapper fields when new nodes are added
* ✨ Using node field words in validation, refactoring resource mapper component
* ✨ Implemented schema syncing and add/remove all fields
* ✨ Implemented custom parameter actions
* ✨ Implemented loading indicator in parameter options
* 🔨 Removing unnecessary constants and vue props
* ✨ Handling default values properly
* ✨ Fixing validation logic
* 👕 Fixing lint errors
* ⚡ Fixing type issues
* ⚡ Not showing fields by default if `addAllFields` is set to `false`
* ✨ Implemented field type validation in resource mapper
* ✨ Updated casing in copy, removed all/remove all option from bottom menu
* ✨ Added auto mapping mode notice
* ✨ Added support for more types in validation
* ✨ Added support for enumerated values
* ✨ Fixing imports after merging
* ✨ Not showing removed fields in matching columns selector. Refactoring validation logic.
* 👕 Fixing imports
* ✔️ Updating unit tests
* ✅ Added resource mapper schema tests
* ⚡ Removing `match` from resource mapper field definition, fixing matching columns loading
* ⚡ Fixed schema merging
* :zap: update operation return data fix
* :zap: review
* 🐛 Added missing import
* 💄 Updating parameter actions icon based on the ui review
* 💄 Updating word capitalisation in tooltips
* 💄 Added empty state to mapping fields list
* 💄 Removing asterisk from fields, updating tooltips for matching fields
* ⚡ Preventing matching fields from being removed by 'Remove All option'
* ⚡ Not showing hidden fields in the `Add field` dropdown
* ⚡ Added support for custom matching columns labels
* :zap: query optimization
* :zap: fix
* ⚡ Optimizing Postgres node enumeration logic
* ⚡ Added empty state for matching columns
* ⚡ Only fully loading fields if there is no schema fetched
* ⚡ Hiding mapping fields if there is no matching columns available in the schema
* ✔️ Fixing minor issues
* ✨ Implemented runtime type validation
* 🔨 Refactoring validation logic
* ✨ Implemented required check, added more custom messages
* ✨ Skipping boolean type in required check
* Type check improvements
* ✨ Only reloading fields if dependent values actually change
* ✨ Adding item index to validation error title
* ✨ Updating Postgres fetching logic, using resource mapper mode to determine if a field can be deleted
* ✨ Resetting field values when adding them via the addAll option
* ⚡ Using minor version (2.2) for new Postgres node
* ⚡ Implemented proper date validation and type casting
* 👕 Consolidating typing
* ✅ Added unit tests for type validations
* 👌 Addressing front-end review comments
* ⚡ More refactoring to address review changes
* ⚡ Updating leftover props
* ⚡ Added fallback for ISO dates with invalid timezones
* Added timestamp to datetime test cases
* ⚡ Reseting matching columns if operation changes
* ⚡ Not forcing auto-increment fields to be filled in in Postgres node. Handling null values
* 💄 Added a custom message for invalid dates
* ⚡ Better handling of JSON values
* ⚡ Updating codemirror readonly stauts based on component property, handling objects in json validation
* Deleting leftover console.log
* ⚡ Better time validation
* ⚡ Fixing build error after merging
* 👕 Fixing lint error
* ⚡ Updating node configuration values
* ⚡ Handling postgres arrays better
* ⚡ Handling SQL array syntax
* ⚡ Updating time validation rules to include timezone
* ⚡ Sending expressions that resolve to `null` or `undefined` by the resource mapper to delete cell content in Google Sheets
* ⚡ Allowing removed fields to be selected for match
* ⚡ Updated the query for fetching unique columns and primary keys
* ⚡ Optimizing the unique query
* ⚡ Setting timezone to all parsed dates
* ⚡ Addressing PR review feedback
* ⚡ Configuring Sheets node for production, minor vue component update
* New cases added to the TypeValidation test.
* ✅ Tweaking validation rules for arrays/objects and updating test cases
---------
Co-authored-by: Michael Kret <michael.k@radency.com>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
2023-05-31 02:56:09 -07:00
returnArray . push (
this . renameNodeInParameterValue (
currentValue as NodeParameterValueType ,
currentName ,
newName ,
) ,
) ;
2019-06-23 03:35:23 -07:00
}
return returnArray ;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const returnData : any = { } ;
2020-10-26 01:26:07 -07:00
for ( const parameterName of Object . keys ( parameterValue || { } ) ) {
2023-05-02 00:37:49 -07:00
returnData [ parameterName ] = this . renameNodeInParameterValue (
2022-09-21 06:44:45 -07:00
parameterValue ! [ parameterName as keyof typeof parameterValue ] ,
2020-10-26 01:26:07 -07:00
currentName ,
newName ,
2023-05-02 00:37:49 -07:00
{ hasRenamableContent } ,
2020-10-26 01:26:07 -07:00
) ;
2019-06-23 03:35:23 -07:00
}
return returnData ;
}
/ * *
* Rename a node in the workflow
*
* @param { string } currentName The current name of the node
* @param { string } newName The new name
* /
renameNode ( currentName : string , newName : string ) {
// Rename the node itself
if ( this . nodes [ currentName ] !== undefined ) {
this . nodes [ newName ] = this . nodes [ currentName ] ;
this . nodes [ newName ] . name = newName ;
delete this . nodes [ currentName ] ;
}
// Update the expressions which reference the node
// with its old name
for ( const node of Object . values ( this . nodes ) ) {
2023-05-02 00:37:49 -07:00
node . parameters = this . renameNodeInParameterValue (
2019-06-23 03:35:23 -07:00
node . parameters ,
currentName ,
newName ,
) as INodeParameters ;
2023-05-02 00:37:49 -07:00
if ( NODES_WITH_RENAMABLE_CONTENT . has ( node . type ) ) {
node . parameters . jsCode = this . renameNodeInParameterValue (
node . parameters . jsCode ,
currentName ,
newName ,
{ hasRenamableContent : true } ,
) ;
}
2019-06-23 03:35:23 -07:00
}
// Change all source connections
if ( this . connectionsBySourceNode . hasOwnProperty ( currentName ) ) {
this . connectionsBySourceNode [ newName ] = this . connectionsBySourceNode [ currentName ] ;
delete this . connectionsBySourceNode [ currentName ] ;
}
// Change all destination connections
let sourceNode : string ;
let type : string ;
let sourceIndex : string ;
let connectionIndex : string ;
let connectionData : IConnection ;
for ( sourceNode of Object . keys ( this . connectionsBySourceNode ) ) {
for ( type of Object . keys ( this . connectionsBySourceNode [ sourceNode ] ) ) {
for ( sourceIndex of Object . keys ( this . connectionsBySourceNode [ sourceNode ] [ type ] ) ) {
for ( connectionIndex of Object . keys (
this . connectionsBySourceNode [ sourceNode ] [ type ] [ parseInt ( sourceIndex , 10 ) ] ,
) ) {
connectionData =
this . connectionsBySourceNode [ sourceNode ] [ type ] [ parseInt ( sourceIndex , 10 ) ] [
parseInt ( connectionIndex , 10 )
] ;
if ( connectionData . node === currentName ) {
connectionData . node = newName ;
}
}
}
}
}
2022-09-02 07:13:17 -07:00
// Use the updated connections to create updated connections by destination nodes
2019-06-23 03:35:23 -07:00
this . connectionsByDestinationNode = this . __getConnectionsByDestination (
this . connectionsBySourceNode ,
) ;
}
/ * *
* Finds the highest parent nodes of the node with the given name
*
2024-08-29 06:55:53 -07:00
* @param { NodeConnectionType } [ type = 'main' ]
2019-06-23 03:35:23 -07:00
* /
getHighestNode (
nodeName : string ,
2024-08-29 06:55:53 -07:00
type : NodeConnectionType = NodeConnectionType . Main ,
2019-06-23 03:35:23 -07:00
nodeConnectionIndex? : number ,
checkedNodes? : string [ ] ,
) : string [ ] {
const currentHighest : string [ ] = [ ] ;
if ( this . nodes [ nodeName ] . disabled === false ) {
// If the current node is not disabled itself is the highest
currentHighest . push ( nodeName ) ;
}
if ( ! this . connectionsByDestinationNode . hasOwnProperty ( nodeName ) ) {
// Node does not have incoming connections
return currentHighest ;
}
if ( ! this . connectionsByDestinationNode [ nodeName ] . hasOwnProperty ( type ) ) {
// Node does not have incoming connections of given type
return currentHighest ;
}
2019-06-27 07:46:26 -07:00
checkedNodes = checkedNodes || [ ] ;
2019-06-23 03:35:23 -07:00
if ( checkedNodes . includes ( nodeName ) ) {
// Node got checked already before
2019-08-02 02:58:57 -07:00
return currentHighest ;
2019-06-23 03:35:23 -07:00
}
checkedNodes . push ( nodeName ) ;
const returnNodes : string [ ] = [ ] ;
let addNodes : string [ ] ;
let connectionsByIndex : IConnection [ ] ;
for (
let connectionIndex = 0 ;
connectionIndex < this . connectionsByDestinationNode [ nodeName ] [ type ] . length ;
connectionIndex ++
) {
if ( nodeConnectionIndex !== undefined && nodeConnectionIndex !== connectionIndex ) {
// If a connection-index is given ignore all other ones
continue ;
}
connectionsByIndex = this . connectionsByDestinationNode [ nodeName ] [ type ] [ connectionIndex ] ;
2020-02-15 17:07:01 -08:00
// eslint-disable-next-line @typescript-eslint/no-loop-func
2019-06-23 03:35:23 -07:00
connectionsByIndex . forEach ( ( connection ) = > {
2024-03-26 06:22:57 -07:00
if ( checkedNodes . includes ( connection . node ) ) {
2019-06-23 03:35:23 -07:00
// Node got checked already before
return ;
}
addNodes = this . getHighestNode ( connection . node , type , undefined , checkedNodes ) ;
if ( addNodes . length === 0 ) {
// The checked node does not have any further parents so add it
// if it is not disabled
if ( this . nodes [ connection . node ] . disabled !== true ) {
addNodes = [ connection . node ] ;
}
}
addNodes . forEach ( ( name ) = > {
2019-08-02 02:58:57 -07:00
// Only add if node is not on the list already anyway
if ( returnNodes . indexOf ( name ) === - 1 ) {
2019-06-23 03:35:23 -07:00
returnNodes . push ( name ) ;
}
} ) ;
} ) ;
}
return returnNodes ;
}
/ * *
2019-07-17 09:44:05 -07:00
* Returns all the after the given one
2019-06-23 03:35:23 -07:00
*
* @param { string } [ type = 'main' ]
* @param { * } [ depth = - 1 ]
* /
2023-10-02 08:33:43 -07:00
getChildNodes (
nodeName : string ,
2024-08-29 06:55:53 -07:00
type : NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionType . Main ,
2023-10-02 08:33:43 -07:00
depth = - 1 ,
) : string [ ] {
2019-07-17 09:44:05 -07:00
return this . getConnectedNodes ( this . connectionsBySourceNode , nodeName , type , depth ) ;
}
/ * *
* Returns all the nodes before the given one
*
2024-08-29 06:55:53 -07:00
* @param { NodeConnectionType } [ type = 'main' ]
2019-07-17 09:44:05 -07:00
* @param { * } [ depth = - 1 ]
* /
2023-10-02 08:33:43 -07:00
getParentNodes (
nodeName : string ,
2024-08-29 06:55:53 -07:00
type : NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionType . Main ,
2023-10-02 08:33:43 -07:00
depth = - 1 ,
) : string [ ] {
2019-07-17 09:44:05 -07:00
return this . getConnectedNodes ( this . connectionsByDestinationNode , nodeName , type , depth ) ;
}
/ * *
* Gets all the nodes which are connected nodes starting from
* the given one
*
2024-08-29 06:55:53 -07:00
* @param { NodeConnectionType } [ type = 'main' ]
2019-07-17 09:44:05 -07:00
* @param { * } [ depth = - 1 ]
* /
getConnectedNodes (
connections : IConnections ,
nodeName : string ,
2024-08-29 06:55:53 -07:00
connectionType : NodeConnectionType | 'ALL' | 'ALL_NON_MAIN' = NodeConnectionType . Main ,
2019-07-17 09:44:05 -07:00
depth = - 1 ,
2023-10-02 08:33:43 -07:00
checkedNodesIncoming? : string [ ] ,
2019-07-17 09:44:05 -07:00
) : string [ ] {
2019-06-23 03:35:23 -07:00
depth = depth === - 1 ? - 1 : depth ;
const newDepth = depth === - 1 ? depth : depth - 1 ;
if ( depth === 0 ) {
// Reached max depth
return [ ] ;
}
2019-07-17 09:44:05 -07:00
if ( ! connections . hasOwnProperty ( nodeName ) ) {
2019-06-23 03:35:23 -07:00
// Node does not have incoming connections
return [ ] ;
}
2024-08-29 06:55:53 -07:00
let types : NodeConnectionType [ ] ;
2023-10-02 08:33:43 -07:00
if ( connectionType === 'ALL' ) {
2024-08-29 06:55:53 -07:00
types = Object . keys ( connections [ nodeName ] ) as NodeConnectionType [ ] ;
2023-10-02 08:33:43 -07:00
} else if ( connectionType === 'ALL_NON_MAIN' ) {
types = Object . keys ( connections [ nodeName ] ) . filter (
( type ) = > type !== 'main' ,
2024-08-29 06:55:53 -07:00
) as NodeConnectionType [ ] ;
2023-10-02 08:33:43 -07:00
} else {
types = [ connectionType ] ;
2019-06-23 03:35:23 -07:00
}
let addNodes : string [ ] ;
let nodeIndex : number ;
let i : number ;
let parentNodeName : string ;
2023-10-02 08:33:43 -07:00
const returnNodes : string [ ] = [ ] ;
2019-06-23 03:35:23 -07:00
2023-10-02 08:33:43 -07:00
types . forEach ( ( type ) = > {
if ( ! connections [ nodeName ] . hasOwnProperty ( type ) ) {
// Node does not have incoming connections of given type
return ;
}
2019-06-23 03:35:23 -07:00
2023-10-02 08:33:43 -07:00
const checkedNodes = checkedNodesIncoming ? [ . . . checkedNodesIncoming ] : [ ] ;
if ( checkedNodes . includes ( nodeName ) ) {
// Node got checked already before
return ;
}
2019-06-23 03:35:23 -07:00
2023-10-02 08:33:43 -07:00
checkedNodes . push ( nodeName ) ;
connections [ nodeName ] [ type ] . forEach ( ( connectionsByIndex ) = > {
connectionsByIndex . forEach ( ( connection ) = > {
if ( checkedNodes . includes ( connection . node ) ) {
// Node got checked already before
return ;
2019-06-23 03:35:23 -07:00
}
2023-10-02 08:33:43 -07:00
returnNodes . unshift ( connection . node ) ;
addNodes = this . getConnectedNodes (
connections ,
connection . node ,
connectionType ,
newDepth ,
checkedNodes ,
) ;
for ( i = addNodes . length ; i -- ; i > 0 ) {
// Because nodes can have multiple parents it is possible that
// parts of the tree is parent of both and to not add nodes
// twice check first if they already got added before.
parentNodeName = addNodes [ i ] ;
nodeIndex = returnNodes . indexOf ( parentNodeName ) ;
if ( nodeIndex !== - 1 ) {
// Node got found before so remove it from current location
// that node-order stays correct
returnNodes . splice ( nodeIndex , 1 ) ;
}
returnNodes . unshift ( parentNodeName ) ;
}
} ) ;
2019-06-23 03:35:23 -07:00
} ) ;
} ) ;
return returnNodes ;
}
2022-05-23 08:56:15 -07:00
/ * *
* Returns all the nodes before the given one
*
* @param { * } [ maxDepth = - 1 ]
* /
getParentNodesByDepth ( nodeName : string , maxDepth = - 1 ) : IConnectedNode [ ] {
return this . searchNodesBFS ( this . connectionsByDestinationNode , nodeName , maxDepth ) ;
}
/ * *
* Gets all the nodes which are connected nodes starting from
* the given one
* Uses BFS traversal
*
* @param { * } [ maxDepth = - 1 ]
* /
searchNodesBFS ( connections : IConnections , sourceNode : string , maxDepth = - 1 ) : IConnectedNode [ ] {
const returnConns : IConnectedNode [ ] = [ ] ;
2024-08-29 06:55:53 -07:00
const type : NodeConnectionType = NodeConnectionType . Main ;
2022-05-23 08:56:15 -07:00
let queue : IConnectedNode [ ] = [ ] ;
queue . push ( {
name : sourceNode ,
depth : 0 ,
indicies : [ ] ,
} ) ;
const visited : { [ key : string ] : IConnectedNode } = { } ;
let depth = 0 ;
while ( queue . length > 0 ) {
if ( maxDepth !== - 1 && depth > maxDepth ) {
break ;
}
depth ++ ;
const toAdd = [ . . . queue ] ;
queue = [ ] ;
// eslint-disable-next-line @typescript-eslint/no-loop-func
toAdd . forEach ( ( curr ) = > {
if ( visited [ curr . name ] ) {
visited [ curr . name ] . indicies = dedupe ( visited [ curr . name ] . indicies . concat ( curr . indicies ) ) ;
return ;
}
visited [ curr . name ] = curr ;
if ( curr . name !== sourceNode ) {
returnConns . push ( curr ) ;
}
if (
! connections . hasOwnProperty ( curr . name ) ||
! connections [ curr . name ] . hasOwnProperty ( type )
) {
return ;
}
connections [ curr . name ] [ type ] . forEach ( ( connectionsByIndex ) = > {
connectionsByIndex . forEach ( ( connection ) = > {
queue . push ( {
name : connection.node ,
indicies : [ connection . index ] ,
depth ,
} ) ;
} ) ;
} ) ;
} ) ;
}
return returnConns ;
}
2024-02-14 01:42:55 -08:00
getParentMainInputNode ( node : INode ) : INode {
if ( node ) {
const nodeType = this . nodeTypes . getByNameAndVersion ( node . type , node . typeVersion ) ;
const outputs = NodeHelpers . getNodeOutputs ( this , node , nodeType . description ) ;
if (
! ! outputs . find (
( output ) = >
( ( output as INodeOutputConfiguration ) ? . type ? ? output ) !== NodeConnectionType . Main ,
)
) {
// Get the first node which is connected to a non-main output
const nonMainNodesConnected = outputs ? . reduce ( ( acc , outputName ) = > {
const parentNodes = this . getChildNodes (
node . name ,
( outputName as INodeOutputConfiguration ) ? . type ? ? outputName ,
) ;
if ( parentNodes . length > 0 ) {
acc . push ( . . . parentNodes ) ;
}
return acc ;
} , [ ] as string [ ] ) ;
if ( nonMainNodesConnected . length ) {
const returnNode = this . getNode ( nonMainNodesConnected [ 0 ] ) ;
if ( returnNode === null ) {
// This should theoretically never happen as the node is connected
// but who knows and it makes TS happy
throw new ApplicationError ( ` Node " ${ nonMainNodesConnected [ 0 ] } " not found ` ) ;
}
// The chain of non-main nodes is potentially not finished yet so
// keep on going
return this . getParentMainInputNode ( returnNode ) ;
}
}
}
return node ;
}
2019-06-27 07:46:26 -07:00
/ * *
2022-06-03 08:25:07 -07:00
* Returns via which output of the parent - node and index the current node
* they are connected
2019-06-27 07:46:26 -07:00
*
* @param { string } nodeName The node to check how it is connected with parent node
* @param { string } parentNodeName The parent node to get the output index of
* @param { string } [ type = 'main' ]
* @param { * } [ depth = - 1 ]
* /
2022-06-03 08:25:07 -07:00
getNodeConnectionIndexes (
2019-06-27 07:46:26 -07:00
nodeName : string ,
parentNodeName : string ,
2024-08-29 06:55:53 -07:00
type : NodeConnectionType = NodeConnectionType . Main ,
2019-06-27 07:46:26 -07:00
depth = - 1 ,
checkedNodes? : string [ ] ,
2022-06-03 08:25:07 -07:00
) : INodeConnection | undefined {
2021-08-19 09:29:20 -07:00
const node = this . getNode ( parentNodeName ) ;
if ( node === null ) {
return undefined ;
}
2019-06-27 07:46:26 -07:00
depth = depth === - 1 ? - 1 : depth ;
const newDepth = depth === - 1 ? depth : depth - 1 ;
if ( depth === 0 ) {
// Reached max depth
return undefined ;
}
if ( ! this . connectionsByDestinationNode . hasOwnProperty ( nodeName ) ) {
// Node does not have incoming connections
return undefined ;
}
if ( ! this . connectionsByDestinationNode [ nodeName ] . hasOwnProperty ( type ) ) {
// Node does not have incoming connections of given type
return undefined ;
}
checkedNodes = checkedNodes || [ ] ;
if ( checkedNodes . includes ( nodeName ) ) {
// Node got checked already before
return undefined ;
}
checkedNodes . push ( nodeName ) ;
2022-06-03 08:25:07 -07:00
let outputIndex : INodeConnection | undefined ;
2019-06-27 07:46:26 -07:00
for ( const connectionsByIndex of this . connectionsByDestinationNode [ nodeName ] [ type ] ) {
2022-06-03 08:25:07 -07:00
for (
let destinationIndex = 0 ;
destinationIndex < connectionsByIndex . length ;
destinationIndex ++
) {
const connection = connectionsByIndex [ destinationIndex ] ;
2019-06-27 07:46:26 -07:00
if ( parentNodeName === connection . node ) {
2022-06-03 08:25:07 -07:00
return {
sourceIndex : connection.index ,
destinationIndex ,
} ;
2019-06-27 07:46:26 -07:00
}
if ( checkedNodes . includes ( connection . node ) ) {
2021-08-19 09:29:20 -07:00
// Node got checked already before so continue with the next one
continue ;
2019-06-27 07:46:26 -07:00
}
2022-06-03 08:25:07 -07:00
outputIndex = this . getNodeConnectionIndexes (
2019-06-27 07:46:26 -07:00
connection . node ,
parentNodeName ,
type ,
newDepth ,
checkedNodes ,
) ;
if ( outputIndex !== undefined ) {
return outputIndex ;
}
}
}
return undefined ;
}
2020-03-16 01:58:49 -07:00
/ * *
* Returns from which of the given nodes the workflow should get started from
*
* @param { string [ ] } nodeNames The potential start nodes
* /
__getStartNode ( nodeNames : string [ ] ) : INode | undefined {
// Check if there are any trigger or poll nodes and then return the first one
let node : INode ;
let nodeType : INodeType ;
for ( const nodeName of nodeNames ) {
node = this . nodes [ nodeName ] ;
2021-09-21 10:38:24 -07:00
2022-10-24 06:03:41 -07:00
if ( nodeNames . length === 1 && ! node . disabled ) {
return node ;
}
2022-11-30 03:16:19 -08:00
nodeType = this . nodeTypes . getByNameAndVersion ( node . type , node . typeVersion ) ;
2020-03-16 01:58:49 -07:00
2023-11-29 03:13:55 -08:00
// TODO: Identify later differently
2024-03-07 02:46:07 -08:00
if ( nodeType . description . name === MANUAL_CHAT_TRIGGER_LANGCHAIN_NODE_TYPE ) {
2023-11-29 03:13:55 -08:00
continue ;
}
2022-07-20 07:24:03 -07:00
if ( nodeType && ( nodeType . trigger !== undefined || nodeType . poll !== undefined ) ) {
2020-08-03 01:15:19 -07:00
if ( node . disabled === true ) {
continue ;
}
2020-03-16 01:58:49 -07:00
return node ;
}
}
feat(editor, core, cli): implement new workflow experience (#4358)
* feat(ExecuteWorkflowTrigger node): Implement ExecuteWorkflowTrigger node (#4108)
* feat(ExecuteWorkflowTrigger node): Implement ExecuteWorkflowTrigger node
* feat(editor): Do not show duplicate button if canvas contains `maxNodes` amount of nodes
* feat(ManualTrigger node): Implement ManualTrigger node (#4110)
* feat(ManualTrigger node): Implement ManualTrigger node
* :memo: Remove generics doc items from ManualTrigger node
* feat(editor-ui): Trigger tab redesign (#4150)
* :construction: Begin with TriggerPanel implementation, add Other Trigger Nodes subcategory
* :construction: Extracted categorized categories/subcategory/nodes rendering into its own component — CategorizedItems, removed SubcategoryPanel, added translations
* :sparkles: Implement MainPanel background scrim
* :recycle: Move `categoriesWithNodes`, 'visibleNodeTypes` and 'categorizedItems` to store, implemented dynamic categories count based on `selectedType`
* :bug: Fix SlideTransition for all the NodeCreato panels
* :lipstick: Fix cursos for CategoryItem and NodeItem
* :bug: Make sure ALL_NODE_FILTER is always set when MainPanel is mounted
* :art: Address PR comments
* label: Use Array type for CategorizedItems props
* :label: Add proper types for Vue props
* 🎨 Use standard component registration for CategorizedItems inside TriggerHelperPanel
* 🎨 Use kebab case for main-panel and icon component
* :label: Improve types
* feat(editor-ui): Redesign search input inside node creator panel (#4204)
* :construction: Begin with TriggerPanel implementation, add Other Trigger Nodes subcategory
* :construction: Extracted categorized categories/subcategory/nodes rendering into its own component — CategorizedItems, removed SubcategoryPanel, added translations
* :sparkles: Implement MainPanel background scrim
* :recycle: Move `categoriesWithNodes`, 'visibleNodeTypes` and 'categorizedItems` to store, implemented dynamic categories count based on `selectedType`
* :bug: Fix SlideTransition for all the NodeCreato panels
* :lipstick: Fix cursos for CategoryItem and NodeItem
* :bug: Make sure ALL_NODE_FILTER is always set when MainPanel is mounted
* :art: Address PR comments
* label: Use Array type for CategorizedItems props
* :label: Add proper types for Vue props
* 🎨 Use standard component registration for CategorizedItems inside TriggerHelperPanel
* :sparkles: Redesign search input and unify usage of categorized items
* :label: Use lowercase "Boolean" as `isSearchVisible` computed return type
* :fire: Remove useless emit
* :sparkles: Implement no result view based on subcategory, minor fixes
* :art: Remove unused properties
* feat(node-email): Change EmailReadImap display name and name (#4239)
* feat(editor-ui): Implement "Choose a Triger" action and related behaviour (#4226)
* :sparkles: Implement "Choose a Triger" action and related behaviour
* :mute: Lint fix
* :recycle: Remove PlaceholderTrigger node, add a button instead
* :art: Merge onMouseEnter and onMouseLeave to a single function
* :bulb: Add comment
* :fire: Remove PlaceholderNode registration
* :art: Rename TriggerPlaceholderButton to CanvasAddButton
* :sparkles: Add method to unregister custom action and rework CanvasAddButton centering logic
* :art: Run `setRecenteredCanvasAddButtonPosition` on `CanvasAddButton` mount
* fix(editor): Fix selecting of node from node-creator panel by clicking
* :twisted_rightwards_arrows: Merge fixes
* fix(editor): Show execute workflow trigger instead of workflow trigger in the trigger helper panel
* feat(editor): Fix node creator panel slide transition (#4261)
* fix(editor): Fix node creator panel slide-in/slide-out transitions
* :art: Fix naming
* :art: Use kebab-case for transition component name
* feat(editor): Disable execution and show notice when user tries to run workflow without enabled triggers
* fix(editor): Address first batch of new WF experience review (#4279)
* fix(editor): Fix first batch of review items
* bug(editor): Fix nodeview canvas add button centering
* :mute: Fix linter errors
* bug(ManualTrigger Node): Fix manual trigger node execution
* fix(editor): Do not show canvas add button in execution or demo mode and prevent clicking if creator is open
* fix(editor): do not show pin data tooltip for manual trigger node
* fix(editor): do not use nodeViewOffset on zoomToFit
* :lipstick: Add margin for last node creator item and set font-weight to 700 for category title
* :sparkles: Position welcome note next to the added trigger node
* :bug: Remve always true welcome note
* feat(editor): Minor UI and UX tweaks (#4328)
* :lipstick: Make top viewport buttons less prominent
* :sparkles: Allow user to switch to all tabs if it contains filter results, move nodecreator state props to its own module
* :mute: Fix linting errors
* :mute: Fix linting errors
* :mute: Fix linting errors
* chore(build): Ping Turbo version to 1.5.5
* :lipstick: Minor traigger panel and node view style changes
* :speech_balloon: Update display name of execute workflow trigger
* feat(core, editor): Update subworkflow execution logic (#4269)
* :sparkles: Implement `findWorkflowStart`
* :zap: Extend `WorkflowOperationError`
* :zap: Add `WorkflowOperationError` to toast
* :blue_book: Extend interface
* :sparkles: Add `subworkflowExecutionError` to store
* :sparkles: Create `SubworkflowOperationError`
* :zap: Render subworkflow error as node error
* :truck: Move subworkflow start validation to `cli`
* :zap: Reset subworkflow execution error state
* :fire: Remove unused import
* :zap: Adjust CLI commands
* :fire: Remove unneeded check
* :fire: Remove stray log
* :zap: Simplify syntax
* :zap: Sort in case both Start and EWT present
* :recycle: Address Omar's feedback
* :fire: Remove unneeded lint exception
* :pencil2: Fix copy
* :shirt: Fix lint
* fix: moved find start node function to catchable place
Co-authored-by: Omar Ajoue <krynble@gmail.com>
* :lipstick: Change ExecuteWorkflow node to primary
* :sparkles: Allow user to navigate to all tab if it contains search results
* :bug: Fixed canvas control button while in demo, disable workflow activation for non-activavle nodes and revert zoomToFit bottom offset
* :fix: Do not chow request text if there's results
* :speech_balloon: Update noResults text
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
2022-10-18 05:23:22 -07:00
const sortedNodeNames = Object . values ( this . nodes )
2024-03-07 02:46:07 -08:00
. sort ( ( a , b ) = > STARTING_NODE_TYPES . indexOf ( a . type ) - STARTING_NODE_TYPES . indexOf ( b . type ) )
feat(editor, core, cli): implement new workflow experience (#4358)
* feat(ExecuteWorkflowTrigger node): Implement ExecuteWorkflowTrigger node (#4108)
* feat(ExecuteWorkflowTrigger node): Implement ExecuteWorkflowTrigger node
* feat(editor): Do not show duplicate button if canvas contains `maxNodes` amount of nodes
* feat(ManualTrigger node): Implement ManualTrigger node (#4110)
* feat(ManualTrigger node): Implement ManualTrigger node
* :memo: Remove generics doc items from ManualTrigger node
* feat(editor-ui): Trigger tab redesign (#4150)
* :construction: Begin with TriggerPanel implementation, add Other Trigger Nodes subcategory
* :construction: Extracted categorized categories/subcategory/nodes rendering into its own component — CategorizedItems, removed SubcategoryPanel, added translations
* :sparkles: Implement MainPanel background scrim
* :recycle: Move `categoriesWithNodes`, 'visibleNodeTypes` and 'categorizedItems` to store, implemented dynamic categories count based on `selectedType`
* :bug: Fix SlideTransition for all the NodeCreato panels
* :lipstick: Fix cursos for CategoryItem and NodeItem
* :bug: Make sure ALL_NODE_FILTER is always set when MainPanel is mounted
* :art: Address PR comments
* label: Use Array type for CategorizedItems props
* :label: Add proper types for Vue props
* 🎨 Use standard component registration for CategorizedItems inside TriggerHelperPanel
* 🎨 Use kebab case for main-panel and icon component
* :label: Improve types
* feat(editor-ui): Redesign search input inside node creator panel (#4204)
* :construction: Begin with TriggerPanel implementation, add Other Trigger Nodes subcategory
* :construction: Extracted categorized categories/subcategory/nodes rendering into its own component — CategorizedItems, removed SubcategoryPanel, added translations
* :sparkles: Implement MainPanel background scrim
* :recycle: Move `categoriesWithNodes`, 'visibleNodeTypes` and 'categorizedItems` to store, implemented dynamic categories count based on `selectedType`
* :bug: Fix SlideTransition for all the NodeCreato panels
* :lipstick: Fix cursos for CategoryItem and NodeItem
* :bug: Make sure ALL_NODE_FILTER is always set when MainPanel is mounted
* :art: Address PR comments
* label: Use Array type for CategorizedItems props
* :label: Add proper types for Vue props
* 🎨 Use standard component registration for CategorizedItems inside TriggerHelperPanel
* :sparkles: Redesign search input and unify usage of categorized items
* :label: Use lowercase "Boolean" as `isSearchVisible` computed return type
* :fire: Remove useless emit
* :sparkles: Implement no result view based on subcategory, minor fixes
* :art: Remove unused properties
* feat(node-email): Change EmailReadImap display name and name (#4239)
* feat(editor-ui): Implement "Choose a Triger" action and related behaviour (#4226)
* :sparkles: Implement "Choose a Triger" action and related behaviour
* :mute: Lint fix
* :recycle: Remove PlaceholderTrigger node, add a button instead
* :art: Merge onMouseEnter and onMouseLeave to a single function
* :bulb: Add comment
* :fire: Remove PlaceholderNode registration
* :art: Rename TriggerPlaceholderButton to CanvasAddButton
* :sparkles: Add method to unregister custom action and rework CanvasAddButton centering logic
* :art: Run `setRecenteredCanvasAddButtonPosition` on `CanvasAddButton` mount
* fix(editor): Fix selecting of node from node-creator panel by clicking
* :twisted_rightwards_arrows: Merge fixes
* fix(editor): Show execute workflow trigger instead of workflow trigger in the trigger helper panel
* feat(editor): Fix node creator panel slide transition (#4261)
* fix(editor): Fix node creator panel slide-in/slide-out transitions
* :art: Fix naming
* :art: Use kebab-case for transition component name
* feat(editor): Disable execution and show notice when user tries to run workflow without enabled triggers
* fix(editor): Address first batch of new WF experience review (#4279)
* fix(editor): Fix first batch of review items
* bug(editor): Fix nodeview canvas add button centering
* :mute: Fix linter errors
* bug(ManualTrigger Node): Fix manual trigger node execution
* fix(editor): Do not show canvas add button in execution or demo mode and prevent clicking if creator is open
* fix(editor): do not show pin data tooltip for manual trigger node
* fix(editor): do not use nodeViewOffset on zoomToFit
* :lipstick: Add margin for last node creator item and set font-weight to 700 for category title
* :sparkles: Position welcome note next to the added trigger node
* :bug: Remve always true welcome note
* feat(editor): Minor UI and UX tweaks (#4328)
* :lipstick: Make top viewport buttons less prominent
* :sparkles: Allow user to switch to all tabs if it contains filter results, move nodecreator state props to its own module
* :mute: Fix linting errors
* :mute: Fix linting errors
* :mute: Fix linting errors
* chore(build): Ping Turbo version to 1.5.5
* :lipstick: Minor traigger panel and node view style changes
* :speech_balloon: Update display name of execute workflow trigger
* feat(core, editor): Update subworkflow execution logic (#4269)
* :sparkles: Implement `findWorkflowStart`
* :zap: Extend `WorkflowOperationError`
* :zap: Add `WorkflowOperationError` to toast
* :blue_book: Extend interface
* :sparkles: Add `subworkflowExecutionError` to store
* :sparkles: Create `SubworkflowOperationError`
* :zap: Render subworkflow error as node error
* :truck: Move subworkflow start validation to `cli`
* :zap: Reset subworkflow execution error state
* :fire: Remove unused import
* :zap: Adjust CLI commands
* :fire: Remove unneeded check
* :fire: Remove stray log
* :zap: Simplify syntax
* :zap: Sort in case both Start and EWT present
* :recycle: Address Omar's feedback
* :fire: Remove unneeded lint exception
* :pencil2: Fix copy
* :shirt: Fix lint
* fix: moved find start node function to catchable place
Co-authored-by: Omar Ajoue <krynble@gmail.com>
* :lipstick: Change ExecuteWorkflow node to primary
* :sparkles: Allow user to navigate to all tab if it contains search results
* :bug: Fixed canvas control button while in demo, disable workflow activation for non-activavle nodes and revert zoomToFit bottom offset
* :fix: Do not chow request text if there's results
* :speech_balloon: Update noResults text
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
2022-10-18 05:23:22 -07:00
. map ( ( n ) = > n . name ) ;
for ( const nodeName of sortedNodeNames ) {
2020-03-16 01:58:49 -07:00
node = this . nodes [ nodeName ] ;
2024-03-07 02:46:07 -08:00
if ( STARTING_NODE_TYPES . includes ( node . type ) ) {
feat(editor, core, cli): implement new workflow experience (#4358)
* feat(ExecuteWorkflowTrigger node): Implement ExecuteWorkflowTrigger node (#4108)
* feat(ExecuteWorkflowTrigger node): Implement ExecuteWorkflowTrigger node
* feat(editor): Do not show duplicate button if canvas contains `maxNodes` amount of nodes
* feat(ManualTrigger node): Implement ManualTrigger node (#4110)
* feat(ManualTrigger node): Implement ManualTrigger node
* :memo: Remove generics doc items from ManualTrigger node
* feat(editor-ui): Trigger tab redesign (#4150)
* :construction: Begin with TriggerPanel implementation, add Other Trigger Nodes subcategory
* :construction: Extracted categorized categories/subcategory/nodes rendering into its own component — CategorizedItems, removed SubcategoryPanel, added translations
* :sparkles: Implement MainPanel background scrim
* :recycle: Move `categoriesWithNodes`, 'visibleNodeTypes` and 'categorizedItems` to store, implemented dynamic categories count based on `selectedType`
* :bug: Fix SlideTransition for all the NodeCreato panels
* :lipstick: Fix cursos for CategoryItem and NodeItem
* :bug: Make sure ALL_NODE_FILTER is always set when MainPanel is mounted
* :art: Address PR comments
* label: Use Array type for CategorizedItems props
* :label: Add proper types for Vue props
* 🎨 Use standard component registration for CategorizedItems inside TriggerHelperPanel
* 🎨 Use kebab case for main-panel and icon component
* :label: Improve types
* feat(editor-ui): Redesign search input inside node creator panel (#4204)
* :construction: Begin with TriggerPanel implementation, add Other Trigger Nodes subcategory
* :construction: Extracted categorized categories/subcategory/nodes rendering into its own component — CategorizedItems, removed SubcategoryPanel, added translations
* :sparkles: Implement MainPanel background scrim
* :recycle: Move `categoriesWithNodes`, 'visibleNodeTypes` and 'categorizedItems` to store, implemented dynamic categories count based on `selectedType`
* :bug: Fix SlideTransition for all the NodeCreato panels
* :lipstick: Fix cursos for CategoryItem and NodeItem
* :bug: Make sure ALL_NODE_FILTER is always set when MainPanel is mounted
* :art: Address PR comments
* label: Use Array type for CategorizedItems props
* :label: Add proper types for Vue props
* 🎨 Use standard component registration for CategorizedItems inside TriggerHelperPanel
* :sparkles: Redesign search input and unify usage of categorized items
* :label: Use lowercase "Boolean" as `isSearchVisible` computed return type
* :fire: Remove useless emit
* :sparkles: Implement no result view based on subcategory, minor fixes
* :art: Remove unused properties
* feat(node-email): Change EmailReadImap display name and name (#4239)
* feat(editor-ui): Implement "Choose a Triger" action and related behaviour (#4226)
* :sparkles: Implement "Choose a Triger" action and related behaviour
* :mute: Lint fix
* :recycle: Remove PlaceholderTrigger node, add a button instead
* :art: Merge onMouseEnter and onMouseLeave to a single function
* :bulb: Add comment
* :fire: Remove PlaceholderNode registration
* :art: Rename TriggerPlaceholderButton to CanvasAddButton
* :sparkles: Add method to unregister custom action and rework CanvasAddButton centering logic
* :art: Run `setRecenteredCanvasAddButtonPosition` on `CanvasAddButton` mount
* fix(editor): Fix selecting of node from node-creator panel by clicking
* :twisted_rightwards_arrows: Merge fixes
* fix(editor): Show execute workflow trigger instead of workflow trigger in the trigger helper panel
* feat(editor): Fix node creator panel slide transition (#4261)
* fix(editor): Fix node creator panel slide-in/slide-out transitions
* :art: Fix naming
* :art: Use kebab-case for transition component name
* feat(editor): Disable execution and show notice when user tries to run workflow without enabled triggers
* fix(editor): Address first batch of new WF experience review (#4279)
* fix(editor): Fix first batch of review items
* bug(editor): Fix nodeview canvas add button centering
* :mute: Fix linter errors
* bug(ManualTrigger Node): Fix manual trigger node execution
* fix(editor): Do not show canvas add button in execution or demo mode and prevent clicking if creator is open
* fix(editor): do not show pin data tooltip for manual trigger node
* fix(editor): do not use nodeViewOffset on zoomToFit
* :lipstick: Add margin for last node creator item and set font-weight to 700 for category title
* :sparkles: Position welcome note next to the added trigger node
* :bug: Remve always true welcome note
* feat(editor): Minor UI and UX tweaks (#4328)
* :lipstick: Make top viewport buttons less prominent
* :sparkles: Allow user to switch to all tabs if it contains filter results, move nodecreator state props to its own module
* :mute: Fix linting errors
* :mute: Fix linting errors
* :mute: Fix linting errors
* chore(build): Ping Turbo version to 1.5.5
* :lipstick: Minor traigger panel and node view style changes
* :speech_balloon: Update display name of execute workflow trigger
* feat(core, editor): Update subworkflow execution logic (#4269)
* :sparkles: Implement `findWorkflowStart`
* :zap: Extend `WorkflowOperationError`
* :zap: Add `WorkflowOperationError` to toast
* :blue_book: Extend interface
* :sparkles: Add `subworkflowExecutionError` to store
* :sparkles: Create `SubworkflowOperationError`
* :zap: Render subworkflow error as node error
* :truck: Move subworkflow start validation to `cli`
* :zap: Reset subworkflow execution error state
* :fire: Remove unused import
* :zap: Adjust CLI commands
* :fire: Remove unneeded check
* :fire: Remove stray log
* :zap: Simplify syntax
* :zap: Sort in case both Start and EWT present
* :recycle: Address Omar's feedback
* :fire: Remove unneeded lint exception
* :pencil2: Fix copy
* :shirt: Fix lint
* fix: moved find start node function to catchable place
Co-authored-by: Omar Ajoue <krynble@gmail.com>
* :lipstick: Change ExecuteWorkflow node to primary
* :sparkles: Allow user to navigate to all tab if it contains search results
* :bug: Fixed canvas control button while in demo, disable workflow activation for non-activavle nodes and revert zoomToFit bottom offset
* :fix: Do not chow request text if there's results
* :speech_balloon: Update noResults text
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
2022-10-18 05:23:22 -07:00
if ( node . disabled === true ) {
continue ;
}
2020-03-16 01:58:49 -07:00
return node ;
}
}
return undefined ;
}
2019-06-23 03:35:23 -07:00
/ * *
2022-09-02 07:13:17 -07:00
* Returns the start node to start the workflow from
2019-06-23 03:35:23 -07:00
*
* /
2019-08-02 03:26:51 -07:00
getStartNode ( destinationNode? : string ) : INode | undefined {
2019-06-23 03:35:23 -07:00
if ( destinationNode ) {
// Find the highest parent nodes of the given one
const nodeNames = this . getHighestNode ( destinationNode ) ;
2019-08-02 03:26:51 -07:00
2019-06-23 03:35:23 -07:00
if ( nodeNames . length === 0 ) {
// If no parent nodes have been found then only the destination-node
// is in the tree so add that one
nodeNames . push ( destinationNode ) ;
}
2019-08-02 03:26:51 -07:00
// Check which node to return as start node
2020-03-16 01:58:49 -07:00
const node = this . __getStartNode ( nodeNames ) ;
if ( node !== undefined ) {
return node ;
2019-08-02 03:26:51 -07:00
}
// If none of the above did find anything simply return the
// first parent node in the list
return this . nodes [ nodeNames [ 0 ] ] ;
2019-06-23 03:35:23 -07:00
}
2020-03-16 01:58:49 -07:00
return this . __getStartNode ( Object . keys ( this . nodes ) ) ;
2019-06-23 03:35:23 -07:00
}
2023-08-17 08:18:14 -07:00
async createWebhookIfNotExists (
webhookData : IWebhookData ,
nodeExecuteFunctions : INodeExecuteFunctions ,
mode : WorkflowExecuteMode ,
activation : WorkflowActivateMode ,
) : Promise < void > {
const webhookExists = await this . runWebhookMethod (
'checkExists' ,
webhookData ,
nodeExecuteFunctions ,
mode ,
activation ,
) ;
if ( ! webhookExists ) {
// If webhook does not exist yet create it
2023-12-19 08:32:02 -08:00
await this . runWebhookMethod ( 'create' , webhookData , nodeExecuteFunctions , mode , activation ) ;
2023-08-17 08:18:14 -07:00
}
}
async deleteWebhook (
webhookData : IWebhookData ,
nodeExecuteFunctions : INodeExecuteFunctions ,
mode : WorkflowExecuteMode ,
activation : WorkflowActivateMode ,
) {
2023-12-19 08:32:02 -08:00
await this . runWebhookMethod ( 'delete' , webhookData , nodeExecuteFunctions , mode , activation ) ;
2023-08-17 08:18:14 -07:00
}
private async runWebhookMethod (
2021-03-23 11:08:47 -07:00
method : WebhookSetupMethodNames ,
webhookData : IWebhookData ,
nodeExecuteFunctions : INodeExecuteFunctions ,
mode : WorkflowExecuteMode ,
activation : WorkflowActivateMode ,
) : Promise < boolean | undefined > {
2023-11-17 08:10:26 -08:00
const node = this . getNode ( webhookData . node ) ;
if ( ! node ) return ;
2022-11-30 03:16:19 -08:00
const nodeType = this . nodeTypes . getByNameAndVersion ( node . type , node . typeVersion ) ;
2019-06-23 03:35:23 -07:00
2023-03-17 04:25:31 -07:00
const webhookFn = nodeType . webhookMethods ? . [ webhookData . webhookDescription . name ] ? . [ method ] ;
if ( webhookFn === undefined ) return ;
2019-06-23 03:35:23 -07:00
2021-03-23 11:08:47 -07:00
const thisArgs = nodeExecuteFunctions . getExecuteHookFunctions (
this ,
node ,
webhookData . workflowExecuteAdditionalData ,
mode ,
activation ,
webhookData ,
) ;
2023-03-17 04:25:31 -07:00
2024-01-17 07:08:50 -08:00
return await webhookFn . call ( thisArgs ) ;
2019-06-23 03:35:23 -07:00
}
/ * *
* Runs the given trigger node so that it can trigger the workflow
* when the node has data .
*
* /
2021-03-23 11:08:47 -07:00
async runTrigger (
node : INode ,
getTriggerFunctions : IGetExecuteTriggerFunctions ,
additionalData : IWorkflowExecuteAdditionalData ,
mode : WorkflowExecuteMode ,
activation : WorkflowActivateMode ,
) : Promise < ITriggerResponse | undefined > {
const triggerFunctions = getTriggerFunctions ( this , node , additionalData , mode , activation ) ;
2019-06-23 03:35:23 -07:00
2021-09-21 10:38:24 -07:00
const nodeType = this . nodeTypes . getByNameAndVersion ( node . type , node . typeVersion ) ;
2019-06-23 03:35:23 -07:00
if ( nodeType === undefined ) {
2023-11-30 03:46:45 -08:00
throw new ApplicationError ( 'Node with unknown node type' , {
extra : { nodeName : node.name } ,
tags : { nodeType : node.type } ,
} ) ;
2019-06-23 03:35:23 -07:00
}
if ( ! nodeType . trigger ) {
2023-11-30 03:46:45 -08:00
throw new ApplicationError ( 'Node type does not have a trigger function defined' , {
extra : { nodeName : node.name } ,
tags : { nodeType : node.type } ,
} ) ;
2019-06-23 03:35:23 -07:00
}
if ( mode === 'manual' ) {
// In manual mode we do not just start the trigger function we also
// want to be able to get informed as soon as the first data got emitted
2019-08-28 08:16:09 -07:00
const triggerResponse = await nodeType . trigger . call ( triggerFunctions ) ;
2019-06-23 03:35:23 -07:00
// Add the manual trigger response which resolves when the first time data got emitted
2022-04-02 08:33:31 -07:00
triggerResponse ! . manualTriggerResponse = new Promise ( ( resolve , reject ) = > {
2021-11-05 09:45:51 -07:00
triggerFunctions . emit = (
( resolveEmit ) = >
(
data : INodeExecutionData [ ] [ ] ,
responsePromise? : IDeferredPromise < IExecuteResponsePromiseData > ,
2022-05-30 03:16:44 -07:00
donePromise? : IDeferredPromise < IRun > ,
2021-11-05 09:45:51 -07:00
) = > {
additionalData . hooks ! . hookFunctions . sendResponse = [
async ( response : IExecuteResponsePromiseData ) : Promise < void > = > {
if ( responsePromise ) {
responsePromise . resolve ( response ) ;
}
} ,
] ;
2022-05-30 03:16:44 -07:00
if ( donePromise ) {
additionalData . hooks ! . hookFunctions . workflowExecuteAfter ? . unshift (
async ( runData : IRun ) : Promise < void > = > {
return donePromise . resolve ( runData ) ;
} ,
) ;
}
2021-11-05 09:45:51 -07:00
resolveEmit ( data ) ;
}
) ( resolve ) ;
2022-04-02 08:33:31 -07:00
triggerFunctions . emitError = (
( rejectEmit ) = >
( error : Error , responsePromise? : IDeferredPromise < IExecuteResponsePromiseData > ) = > {
additionalData . hooks ! . hookFunctions . sendResponse = [
async ( ) : Promise < void > = > {
if ( responsePromise ) {
responsePromise . reject ( error ) ;
}
} ,
] ;
rejectEmit ( error ) ;
}
) ( reject ) ;
2019-06-23 03:35:23 -07:00
} ) ;
2019-08-28 08:16:09 -07:00
return triggerResponse ;
2019-06-23 03:35:23 -07:00
}
// In all other modes simply start the trigger
2024-01-17 07:08:50 -08:00
return await nodeType . trigger . call ( triggerFunctions ) ;
2019-06-23 03:35:23 -07:00
}
2019-12-31 12:19:37 -08:00
/ * *
* Runs the given trigger node so that it can trigger the workflow
* when the node has data .
*
* /
2021-09-21 10:38:24 -07:00
2019-12-31 12:19:37 -08:00
async runPoll (
node : INode ,
pollFunctions : IPollFunctions ,
) : Promise < INodeExecutionData [ ] [ ] | null > {
2021-09-21 10:38:24 -07:00
const nodeType = this . nodeTypes . getByNameAndVersion ( node . type , node . typeVersion ) ;
2019-12-31 12:19:37 -08:00
if ( nodeType === undefined ) {
2023-11-30 03:46:45 -08:00
throw new ApplicationError ( 'Node with unknown node type' , {
extra : { nodeName : node.name } ,
tags : { nodeType : node.type } ,
} ) ;
2019-12-31 12:19:37 -08:00
}
if ( ! nodeType . poll ) {
2023-11-30 03:46:45 -08:00
throw new ApplicationError ( 'Node type does not have a poll function defined' , {
extra : { nodeName : node.name } ,
tags : { nodeType : node.type } ,
} ) ;
2019-12-31 12:19:37 -08:00
}
2024-01-17 07:08:50 -08:00
return await nodeType . poll . call ( pollFunctions ) ;
2019-12-31 12:19:37 -08:00
}
2019-06-23 03:35:23 -07:00
/ * *
* Executes the webhook data to see what it should return and if the
* workflow should be started or not
*
* /
2019-10-11 04:02:44 -07:00
async runWebhook (
webhookData : IWebhookData ,
node : INode ,
additionalData : IWorkflowExecuteAdditionalData ,
nodeExecuteFunctions : INodeExecuteFunctions ,
mode : WorkflowExecuteMode ,
2024-08-01 01:21:58 -07:00
runExecutionData : IRunExecutionData | null ,
2019-10-11 04:02:44 -07:00
) : Promise < IWebhookResponseData > {
2021-09-21 10:38:24 -07:00
const nodeType = this . nodeTypes . getByNameAndVersion ( node . type , node . typeVersion ) ;
2019-06-23 03:35:23 -07:00
if ( nodeType === undefined ) {
2023-11-30 03:46:45 -08:00
throw new ApplicationError ( 'Unknown node type of webhook node' , {
extra : { nodeName : node.name } ,
} ) ;
2019-06-23 03:35:23 -07:00
} else if ( nodeType . webhook === undefined ) {
2023-11-30 03:46:45 -08:00
throw new ApplicationError ( 'Node does not have any webhooks defined' , {
extra : { nodeName : node.name } ,
} ) ;
2019-06-23 03:35:23 -07:00
}
2024-01-09 03:11:39 -08:00
const closeFunctions : CloseFunction [ ] = [ ] ;
2023-07-04 07:17:50 -07:00
const context = nodeExecuteFunctions . getExecuteWebhookFunctions (
2019-07-12 02:33:18 -07:00
this ,
node ,
additionalData ,
mode ,
webhookData ,
2024-01-09 03:11:39 -08:00
closeFunctions ,
2024-08-01 01:21:58 -07:00
runExecutionData ,
2019-07-12 02:33:18 -07:00
) ;
2024-01-17 07:08:50 -08:00
return nodeType instanceof Node
? await nodeType . webhook ( context )
: await nodeType . webhook . call ( context ) ;
2019-06-23 03:35:23 -07:00
}
/ * *
* Executes the given node .
*
* /
2024-04-10 05:02:02 -07:00
// eslint-disable-next-line complexity
2020-07-02 06:07:55 -07:00
async runNode (
2022-06-03 08:25:07 -07:00
executionData : IExecuteData ,
2020-07-02 06:07:55 -07:00
runExecutionData : IRunExecutionData ,
runIndex : number ,
additionalData : IWorkflowExecuteAdditionalData ,
nodeExecuteFunctions : INodeExecuteFunctions ,
mode : WorkflowExecuteMode ,
2023-11-24 09:17:06 -08:00
abortSignal? : AbortSignal ,
2022-05-30 03:16:44 -07:00
) : Promise < IRunNodeResponse > {
2022-06-03 08:25:07 -07:00
const { node } = executionData ;
let inputData = executionData . data ;
2019-06-23 03:35:23 -07:00
if ( node . disabled === true ) {
// If node is disabled simply pass the data through
// return NodeRunHelpers.
if ( inputData . hasOwnProperty ( 'main' ) && inputData . main . length > 0 ) {
// If the node is disabled simply return the data from the first main input
if ( inputData . main [ 0 ] === null ) {
2022-05-30 03:16:44 -07:00
return { data : undefined } ;
2019-06-23 03:35:23 -07:00
}
2022-05-30 03:16:44 -07:00
return { data : [ inputData . main [ 0 ] ] } ;
2019-06-23 03:35:23 -07:00
}
2022-05-30 03:16:44 -07:00
return { data : undefined } ;
2019-06-23 03:35:23 -07:00
}
2021-09-21 10:38:24 -07:00
const nodeType = this . nodeTypes . getByNameAndVersion ( node . type , node . typeVersion ) ;
2019-06-23 03:35:23 -07:00
if ( nodeType === undefined ) {
2023-11-30 03:46:45 -08:00
throw new ApplicationError ( 'Node type is unknown so cannot run it' , {
tags : { nodeType : node.type } ,
} ) ;
2019-06-23 03:35:23 -07:00
}
let connectionInputData : INodeExecutionData [ ] = [ ] ;
2023-06-23 01:50:08 -07:00
if ( nodeType . execute || ( ! nodeType . poll && ! nodeType . trigger && ! nodeType . webhook ) ) {
// Only stop if first input is empty for execute runs. For all others run anyways
2020-12-02 15:23:49 -08:00
// because then it is a trigger node. As they only pass data through and so the input-data
// becomes output-data it has to be possible.
2019-06-23 03:35:23 -07:00
2023-06-23 03:07:52 -07:00
if ( inputData . main ? . length > 0 ) {
2023-06-23 01:50:08 -07:00
// We always use the data of main input and the first input for execute
2020-12-02 15:23:49 -08:00
connectionInputData = inputData . main [ 0 ] as INodeExecutionData [ ] ;
}
2023-07-05 09:47:34 -07:00
const forceInputNodeExecution = this . settings . executionOrder !== 'v1' ;
if ( ! forceInputNodeExecution ) {
// If the nodes do not get force executed data of some inputs may be missing
// for that reason do we use the data of the first one that contains any
for ( const mainData of inputData . main ) {
if ( mainData ? . length ) {
connectionInputData = mainData ;
break ;
2023-06-23 03:07:52 -07:00
}
}
}
2020-12-02 15:23:49 -08:00
if ( connectionInputData . length === 0 ) {
// No data for node so return
2022-05-30 03:16:44 -07:00
return { data : undefined } ;
2020-12-02 15:23:49 -08:00
}
2019-06-23 03:35:23 -07:00
}
2019-11-23 12:57:50 -08:00
if (
runExecutionData . resultData . lastNodeExecuted === node . name &&
runExecutionData . resultData . error !== undefined
) {
// The node did already fail. So throw an error here that it displays and logs it correctly.
// Does get used by webhook and trigger nodes in case they throw an error that it is possible
// to log the error and display in Editor-UI.
2024-03-28 01:46:39 -07:00
if (
runExecutionData . resultData . error . name === 'NodeOperationError' ||
runExecutionData . resultData . error . name === 'NodeApiError'
) {
throw runExecutionData . resultData . error ;
}
2021-04-16 09:33:36 -07:00
2019-11-23 12:57:50 -08:00
const error = new Error ( runExecutionData . resultData . error . message ) ;
error . stack = runExecutionData . resultData . error . stack ;
throw error ;
}
2020-08-08 11:31:04 -07:00
if ( node . executeOnce === true ) {
2020-12-02 15:23:49 -08:00
// If node should be executed only once so use only the first input item
2020-08-08 11:31:04 -07:00
const newInputData : ITaskDataConnections = { } ;
for ( const inputName of Object . keys ( inputData ) ) {
newInputData [ inputName ] = inputData [ inputName ] . map ( ( input ) = > {
2020-02-15 17:07:01 -08:00
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
2020-08-08 11:31:04 -07:00
return input && input . slice ( 0 , 1 ) ;
} ) ;
}
inputData = newInputData ;
}
2023-06-23 01:50:08 -07:00
if ( nodeType . execute ) {
2023-12-21 05:21:09 -08:00
const closeFunctions : CloseFunction [ ] = [ ] ;
2023-07-04 07:17:50 -07:00
const context = nodeExecuteFunctions . getExecuteFunctions (
2019-06-23 03:35:23 -07:00
this ,
runExecutionData ,
runIndex ,
connectionInputData ,
inputData ,
node ,
additionalData ,
2022-06-03 08:25:07 -07:00
executionData ,
2019-06-23 03:35:23 -07:00
mode ,
2023-12-21 05:21:09 -08:00
closeFunctions ,
2023-11-24 09:17:06 -08:00
abortSignal ,
2019-06-23 03:35:23 -07:00
) ;
2023-07-04 07:17:50 -07:00
const data =
nodeType instanceof Node
? await nodeType . execute ( context )
: await nodeType . execute . call ( context ) ;
2023-12-21 05:21:09 -08:00
const closeFunctionsResults = await Promise . allSettled (
2024-01-17 07:08:50 -08:00
closeFunctions . map ( async ( fn ) = > await fn ( ) ) ,
2023-12-21 05:21:09 -08:00
) ;
const closingErrors = closeFunctionsResults
. filter ( ( result ) : result is PromiseRejectedResult = > result . status === 'rejected' )
. map ( ( result ) = > result . reason ) ;
if ( closingErrors . length > 0 ) {
if ( closingErrors [ 0 ] instanceof Error ) throw closingErrors [ 0 ] ;
throw new ApplicationError ( "Error on execution node's close function(s)" , {
extra : { nodeName : node.name } ,
tags : { nodeType : node.type } ,
cause : closingErrors ,
} ) ;
}
2023-07-04 07:17:50 -07:00
return { data } ;
2019-12-31 12:19:37 -08:00
} else if ( nodeType . poll ) {
2020-11-24 10:53:48 -08:00
if ( mode === 'manual' ) {
// In manual mode run the poll function
2021-03-23 11:08:47 -07:00
const thisArgs = nodeExecuteFunctions . getExecutePollFunctions (
this ,
node ,
additionalData ,
mode ,
'manual' ,
) ;
2022-05-30 03:16:44 -07:00
return { data : await nodeType . poll . call ( thisArgs ) } ;
2020-11-24 10:53:48 -08:00
}
// In any other mode pass data through as it already contains the result of the poll
2022-05-30 03:16:44 -07:00
return { data : inputData.main as INodeExecutionData [ ] [ ] } ;
2019-06-23 03:35:23 -07:00
} else if ( nodeType . trigger ) {
if ( mode === 'manual' ) {
// In manual mode start the trigger
2021-03-23 11:08:47 -07:00
const triggerResponse = await this . runTrigger (
node ,
nodeExecuteFunctions . getExecuteTriggerFunctions ,
additionalData ,
mode ,
'manual' ,
) ;
2019-06-23 03:35:23 -07:00
if ( triggerResponse === undefined ) {
2022-05-30 03:16:44 -07:00
return { data : null } ;
2019-06-23 03:35:23 -07:00
}
2022-05-30 03:16:44 -07:00
let closeFunction ;
2019-06-23 03:35:23 -07:00
if ( triggerResponse . closeFunction ) {
2022-05-30 03:16:44 -07:00
// In manual mode we return the trigger closeFunction. That allows it to be called directly
// but we do not have to wait for it to finish. That is important for things like queue-nodes.
// There the full close will may be delayed till a message gets acknowledged after the execution.
// If we would not be able to wait for it to close would it cause problems with "own" mode as the
// process would be killed directly after it and so the acknowledge would not have been finished yet.
closeFunction = triggerResponse . closeFunction ;
2024-07-10 03:37:35 -07:00
// Manual testing of Trigger nodes creates an execution. If the execution is cancelled, `closeFunction` should be called to cleanup any open connections/consumers
abortSignal ? . addEventListener ( 'abort' , closeFunction ) ;
2019-06-23 03:35:23 -07:00
}
2024-07-10 03:37:35 -07:00
if ( triggerResponse . manualTriggerFunction !== undefined ) {
// If a manual trigger function is defined call it and wait till it did run
await triggerResponse . manualTriggerFunction ( ) ;
}
const response = await triggerResponse . manualTriggerResponse ! ;
2019-06-23 03:35:23 -07:00
if ( response . length === 0 ) {
2022-05-30 03:16:44 -07:00
return { data : null , closeFunction } ;
2019-06-23 03:35:23 -07:00
}
2022-05-30 03:16:44 -07:00
return { data : response , closeFunction } ;
2019-06-23 03:35:23 -07:00
}
// For trigger nodes in any mode except "manual" do we simply pass the data through
2022-05-30 03:16:44 -07:00
return { data : inputData.main as INodeExecutionData [ ] [ ] } ;
2019-06-23 03:35:23 -07:00
} else if ( nodeType . webhook ) {
// For webhook nodes always simply pass the data through
2022-05-30 03:16:44 -07:00
return { data : inputData.main as INodeExecutionData [ ] [ ] } ;
2022-02-05 13:55:43 -08:00
} else {
// For nodes which have routing information on properties
const routingNode = new RoutingNode (
this ,
node ,
connectionInputData ,
runExecutionData ? ? null ,
additionalData ,
mode ,
) ;
2022-05-30 03:16:44 -07:00
return {
2022-06-03 08:25:07 -07:00
data : await routingNode . runNode (
inputData ,
runIndex ,
nodeType ,
executionData ,
nodeExecuteFunctions ,
2023-11-24 09:17:06 -08:00
undefined ,
abortSignal ,
2022-06-03 08:25:07 -07:00
) ,
2022-05-30 03:16:44 -07:00
} ;
2019-06-23 03:35:23 -07:00
}
}
}
2022-09-13 08:09:16 -07:00
function hasDotNotationBannedChar ( nodeName : string ) {
const DOT_NOTATION_BANNED_CHARS = /^(\d)|[\\ `!@#$%^&*()_+\-=[\]{};':"\\|,.<>?~]/g ;
return DOT_NOTATION_BANNED_CHARS . test ( nodeName ) ;
}
function backslashEscape ( nodeName : string ) {
const BACKSLASH_ESCAPABLE_CHARS = /[.*+?^${}()|[\]\\]/g ;
return nodeName . replace ( BACKSLASH_ESCAPABLE_CHARS , ( char ) = > ` \\ ${ char } ` ) ;
}
function dollarEscape ( nodeName : string ) {
return nodeName . replace ( new RegExp ( '\\$' , 'g' ) , '$$$$' ) ;
}