Files
Automata/packages/workflow/src/RoutingNode.ts
Omar Ajoue 088daf952e feat(Google Ads Node): Add new node (#3526)
* Add basic layout with icon for Google Ads

* Add node versioning(V1)

* Add node and credential to package

* Add basic layout with icon for Google Ads

* Add node versioning(V1)

* Add node and credential to package

* Add api call to getall

* Fix formdata in the body for the request

* N8N-2928 Added custom queries to campaign

* Fix header bug and add developer-token field

* Add operation and fields to campaign new format

* Add more configurations and queries

* Add Invoice ressources and operations

* Remov old version from the node

* Fixed bud with typo

* Correctly prepends the baseURL

* add query to invocie request

* Fixes header not parsing the expression

* Invoice param changes

* Fixes bug related to headers not being parsed, and bug with auth

* Remove useless imports

* Added analytics to google ad node and removed useless header

* Removed url for testing

* Fixed inconsistent behaviour with the access token not being refreshed

* Added placeholders to help user

* Removed useless comments

* Resolved name confusion

* Added support for body in a GET method

* Removed hyphens, parse body's expression

* Renamed operation for clarity

* Remove unused code

* Removed invoice resource and fixed bug with body and headers

The invoice operation was removed since it does not reflect
what a user would expect from it. Google ADS invoices are
only used for big advertisers where invoicing is performed
after the end of the month and for big sums. This would
be misleading for the majority of the users expecting
an expenses report.

Also fixed a bug with header and body being sent since it
was broken for multiple input rows. The first execution
would override all others.

Lastly, made some improvements to the node itself by
transforming data, adding filters and operations.

* Improve campagin operation and remove analytics; fix tests

* Improve tooltips and descriptions

* Fix lint issues

* Improve tooltip to explain amounts in micros

* Change wording for micros

* Change the fix to a more elegant solution

Co-authored-by: Cyril Gobrecht <cyril.gobrecht@gmail.com>
Co-authored-by: Aël Gobrecht <ael.gobrecht@gmail.com>
2022-07-04 22:47:50 +02:00

854 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,
IRequestOptionsFromParameters,
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) {
credentials = (await executeFunctions.getCredentials(credentialType)) || {};
}
// 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: IRequestOptionsFromParameters = {
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: IRequestOptionsFromParameters,
sourceOptions?: IRequestOptionsFromParameters,
): 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: IRequestOptionsFromParameters,
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: IRequestOptionsFromParameters,
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: IRequestOptionsFromParameters) => {
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,
): IRequestOptionsFromParameters | undefined {
const returnData: IRequestOptionsFromParameters = {
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;
}
}