/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import get from 'lodash/get'; import merge from 'lodash/merge'; import set from 'lodash/set'; import url from 'node:url'; import { NodeApiError } from './errors/node-api.error'; import { NodeOperationError } from './errors/node-operation.error'; import type { ICredentialDataDecryptedObject, ICredentialsDecrypted, IHttpRequestOptions, IN8nHttpFullResponse, INode, INodeExecuteFunctions, INodeExecutionData, INodeParameters, INodePropertyOptions, INodeType, DeclarativeRestApiSettings, IRunExecutionData, ITaskDataConnections, IWorkflowDataProxyAdditionalKeys, IWorkflowExecuteAdditionalData, NodeParameterValue, WorkflowExecuteMode, IDataObject, IExecuteData, IExecuteSingleFunctions, IN8nRequestOperations, INodeProperties, INodePropertyCollection, NodeParameterValueType, PostReceiveAction, JsonObject, CloseFunction, } from './Interfaces'; import * as NodeHelpers from './NodeHelpers'; import { sleep } from './utils'; import type { Workflow } from './Workflow'; export class RoutingNode { additionalData: IWorkflowExecuteAdditionalData; connectionInputData: INodeExecutionData[]; node: INode; mode: WorkflowExecuteMode; runExecutionData: IRunExecutionData; workflow: Workflow; constructor( workflow: Workflow, node: INode, connectionInputData: INodeExecutionData[], runExecutionData: IRunExecutionData, additionalData: IWorkflowExecuteAdditionalData, mode: WorkflowExecuteMode, ) { this.additionalData = additionalData; this.connectionInputData = connectionInputData; this.runExecutionData = runExecutionData; this.mode = mode; this.node = node; this.workflow = workflow; } // eslint-disable-next-line complexity async runNode( inputData: ITaskDataConnections, runIndex: number, nodeType: INodeType, executeData: IExecuteData, nodeExecuteFunctions: INodeExecuteFunctions, credentialsDecrypted?: ICredentialsDecrypted, abortSignal?: AbortSignal, ): Promise { const items = inputData.main[0] as INodeExecutionData[]; const returnData: INodeExecutionData[] = []; let credentialType: string | undefined; if (nodeType.description.credentials?.length) { credentialType = nodeType.description.credentials[0].name; } const closeFunctions: CloseFunction[] = []; const executeFunctions = nodeExecuteFunctions.getExecuteFunctions( this.workflow, this.runExecutionData, runIndex, this.connectionInputData, inputData, this.node, this.additionalData, executeData, this.mode, closeFunctions, abortSignal, ); let credentials: ICredentialDataDecryptedObject | undefined; if (credentialsDecrypted) { credentials = credentialsDecrypted.data; } else if (credentialType) { try { credentials = (await executeFunctions.getCredentials(credentialType)) || {}; } catch (error) { if ( nodeType.description.credentials?.length && nodeType.description.credentials[0].required ) { // Only throw error if credential is mandatory throw error; } else { // Do not request cred type since it doesn't exist credentialType = undefined; } } } const { batching } = executeFunctions.getNodeParameter('requestOptions', 0, {}) as { batching: { batch: { batchSize: number; batchInterval: number } }; }; const batchSize = batching?.batch?.batchSize > 0 ? batching?.batch?.batchSize : 1; const batchInterval = batching?.batch.batchInterval; const requestPromises = []; const itemContext: Array<{ thisArgs: IExecuteSingleFunctions; requestData: DeclarativeRestApiSettings.ResultOptions; }> = []; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { if (itemIndex > 0 && batchSize >= 0 && batchInterval > 0) { if (itemIndex % batchSize === 0) { await sleep(batchInterval); } } itemContext.push({ thisArgs: nodeExecuteFunctions.getExecuteSingleFunctions( this.workflow, this.runExecutionData, runIndex, this.connectionInputData, inputData, this.node, itemIndex, this.additionalData, executeData, this.mode, abortSignal, ), requestData: { options: { qs: {}, body: {}, headers: {}, }, preSend: [], postReceive: [], requestOperations: {}, } as DeclarativeRestApiSettings.ResultOptions, }); const { proxy, timeout, allowUnauthorizedCerts } = itemContext[ itemIndex ].thisArgs.getNodeParameter('requestOptions', 0, {}) as { proxy: string; timeout: number; allowUnauthorizedCerts: boolean; }; if (nodeType.description.requestOperations) { itemContext[itemIndex].requestData.requestOperations = { ...nodeType.description.requestOperations, }; } if (nodeType.description.requestDefaults) { for (const key of Object.keys(nodeType.description.requestDefaults)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any let value = (nodeType.description.requestDefaults as Record)[key]; // If the value is an expression resolve it value = this.getParameterValue( value, itemIndex, runIndex, executeData, { $credentials: credentials, $version: this.node.typeVersion }, false, ) as string; // eslint-disable-next-line @typescript-eslint/no-explicit-any (itemContext[itemIndex].requestData.options as Record)[key] = value; } } for (const property of nodeType.description.properties) { let value = get(this.node.parameters, property.name, []) as string | NodeParameterValue; // If the value is an expression resolve it value = this.getParameterValue( value, itemIndex, runIndex, executeData, { $credentials: credentials, $version: this.node.typeVersion }, false, ) as string | NodeParameterValue; const tempOptions = this.getRequestOptionsFromParameters( itemContext[itemIndex].thisArgs, property, itemIndex, runIndex, '', { $credentials: credentials, $value: value, $version: this.node.typeVersion }, ); this.mergeOptions(itemContext[itemIndex].requestData, tempOptions); } if (proxy) { const proxyParsed = url.parse(proxy); const proxyProperties = ['host', 'port']; for (const property of proxyProperties) { if ( !(property in proxyParsed) || proxyParsed[property as keyof typeof proxyParsed] === null ) { throw new NodeOperationError(this.node, 'The proxy is not value', { runIndex, itemIndex, description: `The proxy URL does not contain a valid value for "${property}"`, }); } } itemContext[itemIndex].requestData.options.proxy = { host: proxyParsed.hostname as string, port: parseInt(proxyParsed.port!), protocol: proxyParsed.protocol?.replace(/:$/, '') || undefined, }; if (proxyParsed.auth) { const [username, password] = proxyParsed.auth.split(':'); itemContext[itemIndex].requestData.options.proxy!.auth = { username, password, }; } } if (allowUnauthorizedCerts) { itemContext[itemIndex].requestData.options.skipSslCertificateValidation = allowUnauthorizedCerts; } if (timeout) { itemContext[itemIndex].requestData.options.timeout = timeout; } else { // set default timeout to 5 minutes itemContext[itemIndex].requestData.options.timeout = 300_000; } requestPromises.push( this.makeRoutingRequest( itemContext[itemIndex].requestData, itemContext[itemIndex].thisArgs, itemIndex, runIndex, credentialType, itemContext[itemIndex].requestData.requestOperations, credentialsDecrypted, ), ); } const promisesResponses = await Promise.allSettled(requestPromises); let responseData: any; for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { responseData = promisesResponses.shift(); if (responseData!.status !== 'fulfilled') { if (responseData.reason.statusCode === 429) { responseData.reason.message = "Try spacing your requests out using the batching settings under 'Options'"; } const error = responseData.reason; if (itemContext[itemIndex].thisArgs?.continueOnFail()) { returnData.push({ json: {}, error: error as NodeApiError }); continue; } if (error instanceof NodeApiError) { set(error, 'context.itemIndex', itemIndex); set(error, 'context.runIndex', runIndex); throw error; } throw new NodeApiError(this.node, error as JsonObject, { runIndex, itemIndex, message: error?.message, description: error?.description, httpCode: error.isAxiosError && error.response ? String(error.response?.status) : 'none', }); } if (itemContext[itemIndex].requestData.maxResults) { // Remove not needed items in case APIs return to many responseData.value.splice(itemContext[itemIndex].requestData.maxResults as number); } returnData.push(...responseData.value); } return [returnData]; } mergeOptions( destinationOptions: DeclarativeRestApiSettings.ResultOptions, sourceOptions?: DeclarativeRestApiSettings.ResultOptions, ): void { if (sourceOptions) { destinationOptions.paginate = destinationOptions.paginate ?? sourceOptions.paginate; destinationOptions.maxResults = sourceOptions.maxResults ? sourceOptions.maxResults : destinationOptions.maxResults; merge(destinationOptions.options, sourceOptions.options); destinationOptions.preSend.push(...sourceOptions.preSend); destinationOptions.postReceive.push(...sourceOptions.postReceive); if (sourceOptions.requestOperations && destinationOptions.requestOperations) { destinationOptions.requestOperations = Object.assign( destinationOptions.requestOperations, sourceOptions.requestOperations, ); } } } async runPostReceiveAction( executeSingleFunctions: IExecuteSingleFunctions, action: PostReceiveAction, inputData: INodeExecutionData[], responseData: IN8nHttpFullResponse, parameterValue: string | IDataObject | undefined, itemIndex: number, runIndex: number, ): Promise { if (typeof action === 'function') { return await action.call(executeSingleFunctions, inputData, responseData); } if (action.type === 'rootProperty') { try { return inputData.flatMap((item) => { let itemContent = get(item.json, action.properties.property); if (!Array.isArray(itemContent)) { itemContent = [itemContent]; } return (itemContent as IDataObject[]).map((json) => { return { json, }; }); }); } catch (error) { throw new NodeOperationError(this.node, error as Error, { runIndex, itemIndex, description: `The rootProperty "${action.properties.property}" could not be found on item.`, }); } } if (action.type === 'filter') { const passValue = action.properties.pass; inputData = inputData.filter((item) => { // If the value is an expression resolve it return this.getParameterValue( passValue, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), { $response: responseData, $responseItem: item.json, $value: parameterValue, $version: this.node.typeVersion, }, false, ) as boolean; }); return inputData; } if (action.type === 'limit') { const maxResults = this.getParameterValue( action.properties.maxResults, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), { $response: responseData, $value: parameterValue, $version: this.node.typeVersion }, false, ) as string; return inputData.slice(0, parseInt(maxResults, 10)); } if (action.type === 'set') { const { value } = action.properties; // If the value is an expression resolve it return [ { json: this.getParameterValue( value, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), { $response: responseData, $value: parameterValue, $version: this.node.typeVersion }, false, ) as IDataObject, }, ]; } if (action.type === 'sort') { // Sort the returned options const sortKey = action.properties.key; inputData.sort((a, b) => { const aSortValue = a.json[sortKey]?.toString().toLowerCase() ?? ''; const bSortValue = b.json[sortKey]?.toString().toLowerCase() ?? ''; if (aSortValue < bSortValue) { return -1; } if (aSortValue > bSortValue) { return 1; } return 0; }); return inputData; } if (action.type === 'setKeyValue') { const returnData: INodeExecutionData[] = []; inputData.forEach((item) => { const returnItem: IDataObject = {}; for (const key of Object.keys(action.properties)) { let propertyValue = ( action.properties as Record< string, // eslint-disable-next-line @typescript-eslint/no-explicit-any any > )[key]; // If the value is an expression resolve it propertyValue = this.getParameterValue( propertyValue, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), { $response: responseData, $responseItem: item.json, $value: parameterValue, $version: this.node.typeVersion, }, false, ) as string; // eslint-disable-next-line @typescript-eslint/no-explicit-any (returnItem as Record)[key] = propertyValue; } returnData.push({ json: returnItem }); }); return returnData; } if (action.type === 'binaryData') { const body = (responseData.body = Buffer.from(responseData.body as string)); let { destinationProperty } = action.properties; destinationProperty = this.getParameterValue( destinationProperty, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), { $response: responseData, $value: parameterValue, $version: this.node.typeVersion }, false, ) as string; const binaryData = await executeSingleFunctions.helpers.prepareBinaryData(body); return inputData.map((item) => { if (typeof item.json === 'string') { // By default is probably the binary data as string set, in this case remove it item.json = {}; } item.binary = { [destinationProperty]: binaryData, }; return item; }); } return []; } async postProcessResponseData( executeSingleFunctions: IExecuteSingleFunctions, responseData: IN8nHttpFullResponse, requestData: DeclarativeRestApiSettings.ResultOptions, itemIndex: number, runIndex: number, ): Promise { let returnData: INodeExecutionData[] = [ { json: responseData.body as IDataObject, }, ]; if (requestData.postReceive.length) { // If postReceive functionality got defined execute all of them for (const postReceiveMethod of requestData.postReceive) { for (const action of postReceiveMethod.actions) { returnData = await this.runPostReceiveAction( executeSingleFunctions, action, returnData, responseData, postReceiveMethod.data.parameterValue, itemIndex, runIndex, ); } } } else { // No postReceive functionality got defined so simply add data as it is if (Array.isArray(responseData.body)) { returnData = responseData.body.map((json) => { return { json, } as INodeExecutionData; }); } else { returnData[0].json = responseData.body as IDataObject; } } return returnData; } async rawRoutingRequest( executeSingleFunctions: IExecuteSingleFunctions, requestData: DeclarativeRestApiSettings.ResultOptions, credentialType?: string, credentialsDecrypted?: ICredentialsDecrypted, ): Promise { let responseData: IN8nHttpFullResponse; requestData.options.returnFullResponse = true; if (credentialType) { responseData = (await executeSingleFunctions.helpers.httpRequestWithAuthentication.call( executeSingleFunctions, credentialType, requestData.options as IHttpRequestOptions, { credentialsDecrypted }, )) as IN8nHttpFullResponse; } else { responseData = (await executeSingleFunctions.helpers.httpRequest( requestData.options as IHttpRequestOptions, )) as IN8nHttpFullResponse; } return responseData; } async makeRoutingRequest( requestData: DeclarativeRestApiSettings.ResultOptions, executeSingleFunctions: IExecuteSingleFunctions, itemIndex: number, runIndex: number, credentialType?: string, requestOperations?: IN8nRequestOperations, credentialsDecrypted?: ICredentialsDecrypted, ): Promise { let responseData: INodeExecutionData[]; for (const preSendMethod of requestData.preSend) { requestData.options = await preSendMethod.call( executeSingleFunctions, requestData.options as IHttpRequestOptions, ); } const executePaginationFunctions = { ...executeSingleFunctions, makeRoutingRequest: async (requestOptions: DeclarativeRestApiSettings.ResultOptions) => { return await this.rawRoutingRequest( executeSingleFunctions, requestOptions, credentialType, credentialsDecrypted, ).then( async (data) => await this.postProcessResponseData( executeSingleFunctions, data, requestData, itemIndex, runIndex, ), ); }, }; if (requestData.paginate && requestOperations?.pagination) { // Has pagination if (typeof requestOperations.pagination === 'function') { // Pagination via function responseData = await requestOperations.pagination.call( executePaginationFunctions, requestData, ); } else { // Pagination via JSON properties responseData = []; if (!requestData.options.qs) { requestData.options.qs = {}; } // Different predefined pagination types if (requestOperations.pagination.type === 'generic') { let tempResponseData: IN8nHttpFullResponse; let tempResponseItems: INodeExecutionData[]; let makeAdditionalRequest: boolean; let paginateRequestData: IHttpRequestOptions; const additionalKeys = { $request: requestData.options, $response: {} as IN8nHttpFullResponse, $version: this.node.typeVersion, }; do { additionalKeys.$request = requestData.options; paginateRequestData = this.getParameterValue( requestOperations.pagination.properties.request as unknown as NodeParameterValueType, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), additionalKeys, false, ) as object as IHttpRequestOptions; // Make the HTTP request tempResponseData = await this.rawRoutingRequest( executeSingleFunctions, { ...requestData, options: { ...requestData.options, ...paginateRequestData } }, credentialType, credentialsDecrypted, ); additionalKeys.$response = tempResponseData; tempResponseItems = await this.postProcessResponseData( executeSingleFunctions, tempResponseData, requestData, itemIndex, runIndex, ); responseData.push(...tempResponseItems); makeAdditionalRequest = this.getParameterValue( requestOperations.pagination.properties.continue, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), additionalKeys, false, ) as boolean; } while (makeAdditionalRequest); } else if (requestOperations.pagination.type === 'offset') { const { properties } = requestOperations.pagination; const optionsType = properties.type === 'body' ? 'body' : 'qs'; if (properties.type === 'body' && !requestData.options.body) { requestData.options.body = {}; } (requestData.options[optionsType] as IDataObject)[properties.limitParameter] = properties.pageSize; (requestData.options[optionsType] as IDataObject)[properties.offsetParameter] = 0; let tempResponseData: INodeExecutionData[]; do { if (requestData?.maxResults) { // Only request as many results as needed const resultsMissing = (requestData?.maxResults as number) - responseData.length; if (resultsMissing < 1) { break; } (requestData.options[optionsType] as IDataObject)[properties.limitParameter] = Math.min(properties.pageSize, resultsMissing); } tempResponseData = await this.rawRoutingRequest( executeSingleFunctions, requestData, credentialType, credentialsDecrypted, ).then( async (data) => await this.postProcessResponseData( executeSingleFunctions, data, requestData, itemIndex, runIndex, ), ); (requestData.options[optionsType] as IDataObject)[properties.offsetParameter] = ((requestData.options[optionsType] as IDataObject)[ properties.offsetParameter ] as number) + properties.pageSize; if (properties.rootProperty) { const tempResponseValue = get(tempResponseData[0].json, properties.rootProperty) as | IDataObject[] | undefined; if (tempResponseValue === undefined) { throw new NodeOperationError( this.node, `The rootProperty "${properties.rootProperty}" could not be found on item.`, { runIndex, itemIndex }, ); } tempResponseData = tempResponseValue.map((item) => { return { json: item, }; }); } responseData.push(...tempResponseData); } while (tempResponseData.length && tempResponseData.length === properties.pageSize); } } } else { // No pagination responseData = await this.rawRoutingRequest( executeSingleFunctions, requestData, credentialType, credentialsDecrypted, ).then( async (data) => await this.postProcessResponseData( executeSingleFunctions, data, requestData, itemIndex, runIndex, ), ); } return responseData; } getParameterValue( parameterValue: NodeParameterValueType, itemIndex: number, runIndex: number, executeData: IExecuteData, additionalKeys?: IWorkflowDataProxyAdditionalKeys, returnObjectAsString = false, ): NodeParameterValueType { if ( typeof parameterValue === 'object' || (typeof parameterValue === 'string' && parameterValue.charAt(0) === '=') ) { return this.workflow.expression.getParameterValue( parameterValue, this.runExecutionData ?? null, runIndex, itemIndex, this.node.name, this.connectionInputData, this.mode, additionalKeys ?? {}, executeData, returnObjectAsString, ); } return parameterValue; } // eslint-disable-next-line complexity getRequestOptionsFromParameters( executeSingleFunctions: IExecuteSingleFunctions, nodeProperties: INodeProperties | INodePropertyOptions, itemIndex: number, runIndex: number, path: string, additionalKeys?: IWorkflowDataProxyAdditionalKeys, ): DeclarativeRestApiSettings.ResultOptions | undefined { const returnData: DeclarativeRestApiSettings.ResultOptions = { options: { qs: {}, body: {}, headers: {}, }, preSend: [], postReceive: [], requestOperations: {}, }; let basePath = path ? `${path}.` : ''; if ( !NodeHelpers.displayParameter( this.node.parameters, nodeProperties, this.node, this.node.parameters, ) ) { return undefined; } if (nodeProperties.routing) { let parameterValue: string | undefined; if (basePath + nodeProperties.name && 'type' in nodeProperties) { // Extract value if it has extractValue defined or if it's a // resourceLocator component. Resource locators are likely to have extractors // and we can't know if the mode has one unless we dig all the way in. const shouldExtractValue = nodeProperties.extractValue !== undefined || nodeProperties.type === 'resourceLocator'; parameterValue = executeSingleFunctions.getNodeParameter( basePath + nodeProperties.name, undefined, { extractValue: shouldExtractValue }, ) as string; } if (nodeProperties.routing.operations) { returnData.requestOperations = { ...nodeProperties.routing.operations }; } if (nodeProperties.routing.request) { for (const key of Object.keys(nodeProperties.routing.request)) { // eslint-disable-next-line @typescript-eslint/no-explicit-any let propertyValue = (nodeProperties.routing.request as Record)[key]; // If the value is an expression resolve it propertyValue = this.getParameterValue( propertyValue, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), { ...additionalKeys, $value: parameterValue }, false, ) as string; // eslint-disable-next-line @typescript-eslint/no-explicit-any (returnData.options as Record)[key] = propertyValue; } } if (nodeProperties.routing.send) { let propertyName = nodeProperties.routing.send.property; if (propertyName !== undefined) { // If the propertyName is an expression resolve it propertyName = this.getParameterValue( propertyName, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), additionalKeys, true, ) as string; let value = parameterValue; if (nodeProperties.routing.send.value) { const valueString = nodeProperties.routing.send.value; // Special value got set // If the valueString is an expression resolve it value = this.getParameterValue( valueString, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), { ...additionalKeys, $value: value }, false, ) as string; } if (nodeProperties.routing.send.type === 'body') { // Send in "body" if (nodeProperties.routing.send.propertyInDotNotation === false) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (returnData.options.body as Record)[propertyName] = value; } else { set(returnData.options.body as object, propertyName, value); } } else { // Send in "query" if (nodeProperties.routing.send.propertyInDotNotation === false) { returnData.options.qs![propertyName] = value; } else { set(returnData.options.qs as object, propertyName, value); } } } if (nodeProperties.routing.send.paginate !== undefined) { let paginateValue = nodeProperties.routing.send.paginate; if (typeof paginateValue === 'string' && paginateValue.charAt(0) === '=') { // If the propertyName is an expression resolve it paginateValue = this.getParameterValue( paginateValue, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), { ...additionalKeys, $value: parameterValue }, true, ) as string; } returnData.paginate = !!paginateValue; } if (nodeProperties.routing.send.preSend) { returnData.preSend.push(...nodeProperties.routing.send.preSend); } } if (nodeProperties.routing.output) { if (nodeProperties.routing.output.maxResults !== undefined) { let maxResultsValue = nodeProperties.routing.output.maxResults; if (typeof maxResultsValue === 'string' && maxResultsValue.charAt(0) === '=') { // If the propertyName is an expression resolve it maxResultsValue = this.getParameterValue( maxResultsValue, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), { ...additionalKeys, $value: parameterValue }, true, ) as string; } returnData.maxResults = maxResultsValue; } if (nodeProperties.routing.output.postReceive) { const postReceiveActions = nodeProperties.routing.output.postReceive.filter((action) => { if (typeof action === 'function') { return true; } if (typeof action.enabled === 'string' && action.enabled.charAt(0) === '=') { // If the propertyName is an expression resolve it return this.getParameterValue( action.enabled, itemIndex, runIndex, executeSingleFunctions.getExecuteData(), { ...additionalKeys, $value: parameterValue }, true, ) as boolean; } return action.enabled !== false; }); if (postReceiveActions.length) { returnData.postReceive.push({ data: { parameterValue, }, actions: postReceiveActions, }); } } } } // Check if there are any child properties if (!Object.prototype.hasOwnProperty.call(nodeProperties, 'options')) { // There are none so nothing else to check return returnData; } // Everything after this point can only be of type INodeProperties nodeProperties = nodeProperties as INodeProperties; // Check the child parameters let value; if (nodeProperties.type === 'options') { const optionValue = NodeHelpers.getParameterValueByPath( this.node.parameters, nodeProperties.name, basePath.slice(0, -1), ); // Find the selected option const selectedOption = (nodeProperties.options as INodePropertyOptions[]).filter( (option) => option.value === optionValue, ); if (selectedOption.length) { // Check only if option is set and if of type INodeProperties const tempOptions = this.getRequestOptionsFromParameters( executeSingleFunctions, selectedOption[0], itemIndex, runIndex, `${basePath}${nodeProperties.name}`, { $value: optionValue, $version: this.node.typeVersion }, ); this.mergeOptions(returnData, tempOptions); } } else if (nodeProperties.type === 'collection') { value = NodeHelpers.getParameterValueByPath( this.node.parameters, nodeProperties.name, basePath.slice(0, -1), ); for (const propertyOption of nodeProperties.options as INodeProperties[]) { if ( Object.keys(value as IDataObject).includes(propertyOption.name) && propertyOption.type !== undefined ) { // Check only if option is set and if of type INodeProperties const tempOptions = this.getRequestOptionsFromParameters( executeSingleFunctions, propertyOption, itemIndex, runIndex, `${basePath}${nodeProperties.name}`, { $version: this.node.typeVersion }, ); this.mergeOptions(returnData, tempOptions); } } } else if (nodeProperties.type === 'fixedCollection') { basePath = `${basePath}${nodeProperties.name}.`; for (const propertyOptions of nodeProperties.options as INodePropertyCollection[]) { // Check if the option got set and if not skip it value = NodeHelpers.getParameterValueByPath( this.node.parameters, propertyOptions.name, basePath.slice(0, -1), ); if (value === undefined) { continue; } // Make sure that it is always an array to be able to use the same code for multi and single if (!Array.isArray(value)) { value = [value]; } // Resolve expressions value = this.getParameterValue( value as INodeParameters[], itemIndex, runIndex, executeSingleFunctions.getExecuteData(), { ...additionalKeys }, false, ) as INodeParameters[]; const loopBasePath = `${basePath}${propertyOptions.name}`; for (let i = 0; i < value.length; i++) { for (const option of propertyOptions.values) { const tempOptions = this.getRequestOptionsFromParameters( executeSingleFunctions, option, itemIndex, runIndex, nodeProperties.typeOptions?.multipleValues ? `${loopBasePath}[${i}]` : loopBasePath, { ...(additionalKeys || {}), $index: i, $parent: value[i] }, ); this.mergeOptions(returnData, tempOptions); } } } } return returnData; } }