mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-02 08:27:29 -08:00
c85faff4f1
* ✨ Make it possible to dynamically load node packages * ⚡ Fix comment * ✨ Make possible to dynamically install nodes from npm * Created migration for sqlite regarding community nodes * Saving to db whenever a package with nodes is installed * Created endpoint to fetch installed packages * WIP - uninstall package with nodes * Fix lint issues * Updating nodes via API * Lint and improvement fixes * Created community node helpers and removed packages taht do not contain nodes * Check for package updates when fetching installed packages * Blocked access to non-owner and preventing incorrect install of packages * Added auto healing process * Unit tests for helpers * Finishing tests for helpers * Improved unit tests, refactored more helpers and created integration tests for GET * Implemented detection of missing packages on init and added warning to frontend settings * Add check for banned packages and fix broken tests * Create migrations for other db systems * Updated with latest changes from master * Fixed conflict errors * Improved unit tests, refactored more helpers and created integration tests for GET * Implemented detection of missing packages on init and added warning to frontend settings * 🔥 Removing access check for the Settings sidebar item * ✨ Added inital community nodes settings screen * ⚡Added executionMode flag to settings * ✨ Implemented N8N-callout component * 💄Updating Callout component template propery names * 💄 Updating Callout component styling. * 💄Updating Callout component sizing and colors. * ✔️ Updating Callout component test snapshots after styling changes * ✨ Updating the `ActionBox` component so it supports callouts and conditional button rendering * 💄 Removing duplicate callout theme validation in the `ActionBox` component. Adding a selection control for it in the storybook. * ✨ Added warning message if instance is in the queue mode. Updated colors based on the new design. * ⚡ Added a custom permission support to router * 🔨 Implemented UM detection as a custom permission. * 👌Updating route permission logic. * ✨ Implemented installed community packages list in the settings view * 👌 Updating settings routes rules and community nodes setting view. * Allow installation of packages that failed to load * 👌 Updating `ActionBox`, `CommuntyPackageCard` components and settings loading logic. * 👌 Fixing community nodes loading state and sidebar icon spacing. * ✨ Implemented loading skeletons for community package cards * 👌 Handling errrors while loading installed package list. Updating spacing. * 👌 Updating community nodes error messages. * Added disable flag * 🐛 Fixing a community nodes update detection bug when there are missing packages. (#3497) * ✨ Added front-end support for community nodes feature flag * ✨ Implemented community package installation modal dialog * 💄 Community nodes installation modal updates: Moved links to constants and used them in translations, disabling inputs in loading state. * ✨ Implemented community packages install flow * Standardize error codes (#3501) * Standardize error: 400 for request issues such as invalid package name and 500 for installation problems * Fix http status code for when package is not found * ✨ Implemented community package installation modal dialog * 💄 Community nodes installation modal updates: Moved links to constants and used them in translations, disabling inputs in loading state. * ✨ Implemented community packages install flow * ✨ Updated error handling based on the response codes * ✨ Implemented community package installation modal dialog * ✨ Implemented community package uninstall flow. * ✨ Finished update confirm modal UI * 💄 Replaced community nodes tooltip image with the one exported from figma. * ✨ Implemented community package update process * ✨ Updating community nodes list after successful package update * 🔒 Updating public API setting route to use new access rules. Updating express app definition in community nodes tests * ✨ Implemented community package installation modal dialog * 💄 Community nodes installation modal updates: Moved links to constants and used them in translations, disabling inputs in loading state. * ✨ Implemented community packages install flow * ✨ Updated error handling based on the response codes * Change output for installation request * Improve payload for update requests * 👌 Updating community nodes install modal UI * 👌 Updating community nodes confirm modal logic * 👌 Refactoring community nodes confirm modal dialog * 👌 Separating community nodes components loading states * 💄 Updating community nodes install modal spacing. * Fix behavior for installing already installed packages * 💡 Commenting community nodes install process * 🔥 Removing leftover commits of deleted Vue mutations * ✨ Updated node list to identify community nodes and handle node name clash * ✨ Implemented missing community node dialog. * 💄 Updating n8n-tabs component to support tooltips * ✨ Updating node details with community node details. * 🔨 Using back-end response when updating community packages * 👌 Updating tabs component and refactoring community nodes store mutations * 👌 Adding community node flag to node type descriptions and using it to identify community nodes * 👌 Hiding unnecessary elements from missing node details panel. * 👌 Updating missing node type descriptions for custom and community nodes * 👌 Updating community node package name detection logic * 👌 Removing communityNode flag from node description * ✨ Adding `force` flag to credentials fetching (#3527) * ✨ Adding `force` flag to credentials fetching which can be used to skip check when loading credentials * ✨ Forcing credentials loading when opening nodeView * 👌 Minor updates to community nodes details panel * tests for post endpoint * duplicate comments * Add Patch and Delete enpoints tests * 🔒 Using `pageCategory`prop to assemble the list of settings routes instead of hard-coded array (#3562) * 📈 Added front-end telemetry events for community nodes * 📈 Updating community nodes telemetry events * 💄 Updating community nodes settings UI elements based on product/design review * 💄 Updating node view & node details view for community nodes based on product/design feedback * 💄 Fixing community node text capitalisation * ✨ Adding community node install error message under the package name input field * Fixed and improved tests * Fix lint issue * feat: Migrated to npm release of riot-tmpl fork. * 📈 Updating community nodes telemetry events based on the product review * 💄 Updating community nodes UI based on the design feedback * 🔀 Merging recent node draggable panels changes * Implement self healing process * Improve error messages for package name requirement and disk space * 💄 Removing front-end error message override since appropriate response is available from the back-end * Fix lint issues * Fix installed node name * 💄 Removed additional node name parsing * 📈 Updating community nodes telemetry events * Fix postgres migration for cascading nodes when package is removed * Remove postman mock for banned packages * 📈 Adding missing telemetry event for community node documentation click * 🐛 Fixing community nodes UI bugs reported during the bug bash * Fix issue with uninstalling packages not reflecting UI * 🐛 Fixing a missing node type bug when trying to run a workflow. * Improve error detection for installing packages * 💄 Updating community nodes components styling and wording based on the product feedback * Implement telemetry be events * Add author name and email to packages * Fix telemetry be events for community packages * 📈 Updating front-end telemetry events with community nodes author data * 💄 Updating credentials documentation link logic to handle community nodes credentials * 🐛 Fixing draggable panels logic * Fix duplicate wrong import * 💄 Hiding community nodes credentials documentation links when they don't contain an absolute URL * Fix issue with detection of missing packages * 💄 Adding the `Docs` tab to community nodes * 💄 Adding a failed loading indicator to community nodes list * Prevent n8n from crashing on startup * Refactor and improve code quality * ⚡ Remove not needed depenedency Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: Milorad Filipović <milorad@n8n.io> Co-authored-by: Milorad FIlipović <miloradfilipovic19@gmail.com> Co-authored-by: agobrech <ael.gobrecht@gmail.com> Co-authored-by: Alex Grozav <alex@grozav.com>
867 lines
24 KiB
TypeScript
867 lines
24 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
/* eslint-disable import/no-cycle */
|
|
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
|
/* eslint-disable no-param-reassign */
|
|
/* eslint-disable no-continue */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
/* eslint-disable no-await-in-loop */
|
|
/* eslint-disable no-restricted-syntax */
|
|
import get from 'lodash.get';
|
|
import merge from 'lodash.merge';
|
|
import set from 'lodash.set';
|
|
|
|
import {
|
|
ICredentialDataDecryptedObject,
|
|
ICredentialsDecrypted,
|
|
IHttpRequestOptions,
|
|
IN8nHttpFullResponse,
|
|
INode,
|
|
INodeExecuteFunctions,
|
|
INodeExecutionData,
|
|
INodeParameters,
|
|
INodePropertyOptions,
|
|
INodeType,
|
|
DeclarativeRestApiSettings,
|
|
IRunExecutionData,
|
|
ITaskDataConnections,
|
|
IWorkflowDataProxyAdditionalKeys,
|
|
IWorkflowExecuteAdditionalData,
|
|
NodeApiError,
|
|
NodeHelpers,
|
|
NodeOperationError,
|
|
NodeParameterValue,
|
|
Workflow,
|
|
WorkflowExecuteMode,
|
|
} from '.';
|
|
|
|
import {
|
|
IDataObject,
|
|
IExecuteData,
|
|
IExecuteSingleFunctions,
|
|
IN8nRequestOperations,
|
|
INodeProperties,
|
|
INodePropertyCollection,
|
|
PostReceiveAction,
|
|
} from './Interfaces';
|
|
|
|
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;
|
|
}
|
|
|
|
async runNode(
|
|
inputData: ITaskDataConnections,
|
|
runIndex: number,
|
|
nodeType: INodeType,
|
|
executeData: IExecuteData,
|
|
nodeExecuteFunctions: INodeExecuteFunctions,
|
|
credentialsDecrypted?: ICredentialsDecrypted,
|
|
): Promise<INodeExecutionData[][] | null | undefined> {
|
|
const items = inputData.main[0] as INodeExecutionData[];
|
|
const returnData: INodeExecutionData[] = [];
|
|
let responseData;
|
|
|
|
let credentialType: string | undefined;
|
|
|
|
if (nodeType.description.credentials?.length) {
|
|
credentialType = nodeType.description.credentials[0].name;
|
|
}
|
|
const executeFunctions = nodeExecuteFunctions.getExecuteFunctions(
|
|
this.workflow,
|
|
this.runExecutionData,
|
|
runIndex,
|
|
this.connectionInputData,
|
|
inputData,
|
|
this.node,
|
|
this.additionalData,
|
|
executeData,
|
|
this.mode,
|
|
);
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Think about how batching could be handled for REST APIs which support it
|
|
for (let i = 0; i < items.length; i++) {
|
|
try {
|
|
const thisArgs = nodeExecuteFunctions.getExecuteSingleFunctions(
|
|
this.workflow,
|
|
this.runExecutionData,
|
|
runIndex,
|
|
this.connectionInputData,
|
|
inputData,
|
|
this.node,
|
|
i,
|
|
this.additionalData,
|
|
executeData,
|
|
this.mode,
|
|
);
|
|
const requestData: DeclarativeRestApiSettings.ResultOptions = {
|
|
options: {
|
|
qs: {},
|
|
body: {},
|
|
headers: {},
|
|
},
|
|
preSend: [],
|
|
postReceive: [],
|
|
requestOperations: {},
|
|
};
|
|
|
|
if (nodeType.description.requestOperations) {
|
|
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<string, any>)[key];
|
|
// If the value is an expression resolve it
|
|
value = this.getParameterValue(
|
|
value,
|
|
i,
|
|
runIndex,
|
|
executeData,
|
|
{ $credentials: credentials },
|
|
false,
|
|
) as string;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(requestData.options as Record<string, any>)[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,
|
|
i,
|
|
runIndex,
|
|
executeData,
|
|
{ $credentials: credentials },
|
|
true,
|
|
) as string | NodeParameterValue;
|
|
|
|
const tempOptions = this.getRequestOptionsFromParameters(
|
|
thisArgs,
|
|
property,
|
|
i,
|
|
runIndex,
|
|
'',
|
|
{ $credentials: credentials, $value: value },
|
|
);
|
|
|
|
this.mergeOptions(requestData, tempOptions);
|
|
}
|
|
|
|
// TODO: Change to handle some requests in parallel (should be configurable)
|
|
responseData = await this.makeRoutingRequest(
|
|
requestData,
|
|
thisArgs,
|
|
i,
|
|
runIndex,
|
|
credentialType,
|
|
requestData.requestOperations,
|
|
credentialsDecrypted,
|
|
);
|
|
|
|
if (requestData.maxResults) {
|
|
// Remove not needed items in case APIs return to many
|
|
responseData.splice(requestData.maxResults as number);
|
|
}
|
|
|
|
returnData.push(...responseData);
|
|
} catch (error) {
|
|
if (get(this.node, 'continueOnFail', false)) {
|
|
returnData.push({ json: {}, error: error.message });
|
|
continue;
|
|
}
|
|
throw new NodeApiError(this.node, error, { runIndex, itemIndex: i });
|
|
}
|
|
}
|
|
|
|
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 = 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<INodeExecutionData[]> {
|
|
if (typeof action === 'function') {
|
|
return action.call(executeSingleFunctions, inputData, responseData);
|
|
}
|
|
if (action.type === 'rootProperty') {
|
|
try {
|
|
return inputData.flatMap((item) => {
|
|
// let itemContent = item.json[action.properties.property];
|
|
let itemContent = get(item.json, action.properties.property);
|
|
|
|
if (!Array.isArray(itemContent)) {
|
|
itemContent = [itemContent];
|
|
}
|
|
return (itemContent as IDataObject[]).map((json) => {
|
|
return {
|
|
json,
|
|
};
|
|
});
|
|
});
|
|
} catch (e) {
|
|
throw new NodeOperationError(
|
|
this.node,
|
|
`The rootProperty "${action.properties.property}" could not be found on item.`,
|
|
{ runIndex, itemIndex },
|
|
);
|
|
}
|
|
}
|
|
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 },
|
|
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]
|
|
? (a.json[sortKey]?.toString().toLowerCase() as string)
|
|
: '';
|
|
const bSortValue = b.json[sortKey]
|
|
? (b.json[sortKey]?.toString().toLowerCase() as string)
|
|
: '';
|
|
if (aSortValue < bSortValue) {
|
|
return -1;
|
|
}
|
|
if (aSortValue > bSortValue) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
return inputData;
|
|
}
|
|
if (action.type === 'setKeyValue') {
|
|
const returnData: INodeExecutionData[] = [];
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
|
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,
|
|
},
|
|
true,
|
|
) as string;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(returnItem as Record<string, any>)[key] = propertyValue;
|
|
}
|
|
returnData.push({ json: returnItem });
|
|
});
|
|
|
|
return returnData;
|
|
}
|
|
if (action.type === 'binaryData') {
|
|
responseData.body = Buffer.from(responseData.body as string);
|
|
let { destinationProperty } = action.properties;
|
|
|
|
destinationProperty = this.getParameterValue(
|
|
destinationProperty,
|
|
itemIndex,
|
|
runIndex,
|
|
executeSingleFunctions.getExecuteData(),
|
|
{ $response: responseData, $value: parameterValue },
|
|
false,
|
|
) as string;
|
|
|
|
const binaryData = await executeSingleFunctions.helpers.prepareBinaryData(responseData.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 rawRoutingRequest(
|
|
executeSingleFunctions: IExecuteSingleFunctions,
|
|
requestData: DeclarativeRestApiSettings.ResultOptions,
|
|
itemIndex: number,
|
|
runIndex: number,
|
|
credentialType?: string,
|
|
credentialsDecrypted?: ICredentialsDecrypted,
|
|
): Promise<INodeExecutionData[]> {
|
|
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;
|
|
}
|
|
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
|
|
// eslint-disable-next-line no-lonely-if
|
|
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 makeRoutingRequest(
|
|
requestData: DeclarativeRestApiSettings.ResultOptions,
|
|
executeSingleFunctions: IExecuteSingleFunctions,
|
|
itemIndex: number,
|
|
runIndex: number,
|
|
credentialType?: string,
|
|
requestOperations?: IN8nRequestOperations,
|
|
credentialsDecrypted?: ICredentialsDecrypted,
|
|
): Promise<INodeExecutionData[]> {
|
|
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 this.rawRoutingRequest(
|
|
executeSingleFunctions,
|
|
requestOptions,
|
|
itemIndex,
|
|
runIndex,
|
|
credentialType,
|
|
credentialsDecrypted,
|
|
);
|
|
},
|
|
};
|
|
|
|
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
|
|
const { properties } = requestOperations.pagination;
|
|
responseData = [];
|
|
if (!requestData.options.qs) {
|
|
requestData.options.qs = {};
|
|
}
|
|
|
|
// Different predefined pagination types
|
|
if (requestOperations.pagination.type === 'offset') {
|
|
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,
|
|
itemIndex,
|
|
runIndex,
|
|
credentialType,
|
|
credentialsDecrypted,
|
|
);
|
|
|
|
(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,
|
|
itemIndex,
|
|
runIndex,
|
|
credentialType,
|
|
credentialsDecrypted,
|
|
);
|
|
}
|
|
return responseData;
|
|
}
|
|
|
|
getParameterValue(
|
|
parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
|
|
itemIndex: number,
|
|
runIndex: number,
|
|
executeData: IExecuteData,
|
|
additionalKeys?: IWorkflowDataProxyAdditionalKeys,
|
|
returnObjectAsString = false,
|
|
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | string {
|
|
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,
|
|
this.additionalData.timezone,
|
|
additionalKeys ?? {},
|
|
executeData,
|
|
returnObjectAsString,
|
|
);
|
|
}
|
|
|
|
return parameterValue;
|
|
}
|
|
|
|
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) {
|
|
parameterValue = executeSingleFunctions.getNodeParameter(
|
|
basePath + nodeProperties.name,
|
|
) 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<string, any>)[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<string, any>)[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 },
|
|
true,
|
|
) as string;
|
|
}
|
|
|
|
if (nodeProperties.routing.send.type === 'body') {
|
|
// Send in "body"
|
|
// eslint-disable-next-line no-lonely-if
|
|
if (nodeProperties.routing.send.propertyInDotNotation === false) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(returnData.options.body as Record<string, any>)![propertyName] = value;
|
|
} else {
|
|
set(returnData.options.body as object, propertyName, value);
|
|
}
|
|
} else {
|
|
// Send in "query"
|
|
// eslint-disable-next-line no-lonely-if
|
|
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) {
|
|
returnData.postReceive.push({
|
|
data: {
|
|
parameterValue,
|
|
},
|
|
actions: nodeProperties.routing.output.postReceive,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 },
|
|
);
|
|
|
|
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}`,
|
|
);
|
|
|
|
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;
|
|
}
|
|
}
|