mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
feat(HTTP Request Node): Add pagination support (#5993)
Is still WIP and does not implement the correct UI yet. Github issue / Community forum post (link here to close automatically): https://community.n8n.io/t/pagination-included-into-http-node/15080 https://community.n8n.io/t/how-to-paginate-through-data-in-http-requests/28103
This commit is contained in:
parent
9bdb85c4ce
commit
cc2bd2e19c
|
@ -1,6 +1,7 @@
|
|||
export const CUSTOM_EXTENSION_ENV = 'N8N_CUSTOM_EXTENSIONS';
|
||||
export const PLACEHOLDER_EMPTY_EXECUTION_ID = '__UNKNOWN__';
|
||||
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';
|
||||
export const HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest';
|
||||
|
||||
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ import { extension, lookup } from 'mime-types';
|
|||
import type {
|
||||
BinaryHelperFunctions,
|
||||
ConnectionTypes,
|
||||
ContextType,
|
||||
ExecutionError,
|
||||
FieldType,
|
||||
FileSystemHelperFunctions,
|
||||
|
@ -88,6 +89,7 @@ import type {
|
|||
NodeExecutionWithMetadata,
|
||||
NodeHelperFunctions,
|
||||
NodeParameterValueType,
|
||||
PaginationOptions,
|
||||
RequestHelperFunctions,
|
||||
Workflow,
|
||||
WorkflowActivateMode,
|
||||
|
@ -110,13 +112,14 @@ import {
|
|||
isResourceMapperValue,
|
||||
validateFieldType,
|
||||
ExecutionBaseError,
|
||||
jsonParse,
|
||||
} from 'n8n-workflow';
|
||||
import type { Token } from 'oauth-1.0a';
|
||||
import clientOAuth1 from 'oauth-1.0a';
|
||||
import path from 'path';
|
||||
import { stringify } from 'qs';
|
||||
import type { OptionsWithUri, OptionsWithUrl } from 'request';
|
||||
import type { RequestPromiseOptions } from 'request-promise-native';
|
||||
import type { OptionsWithUrl } from 'request';
|
||||
import type { OptionsWithUri, RequestPromiseOptions } from 'request-promise-native';
|
||||
import { Readable } from 'stream';
|
||||
import url, { URL, URLSearchParams } from 'url';
|
||||
|
||||
|
@ -126,6 +129,7 @@ import {
|
|||
BLOCK_FILE_ACCESS_TO_N8N_FILES,
|
||||
CONFIG_FILES,
|
||||
CUSTOM_EXTENSION_ENV,
|
||||
HTTP_REQUEST_NODE_TYPE,
|
||||
PLACEHOLDER_EMPTY_EXECUTION_ID,
|
||||
RESTRICT_FILE_ACCESS_TO,
|
||||
UM_EMAIL_TEMPLATES_INVITE,
|
||||
|
@ -143,6 +147,7 @@ import {
|
|||
import { getSecretsProxy } from './Secrets';
|
||||
import Container from 'typedi';
|
||||
import type { BinaryData } from './BinaryData/types';
|
||||
import merge from 'lodash/merge';
|
||||
import { InstanceSettings } from './InstanceSettings';
|
||||
|
||||
axios.defaults.timeout = 300000;
|
||||
|
@ -1866,7 +1871,7 @@ export async function getCredentials(
|
|||
|
||||
// Hardcode for now for security reasons that only a single node can access
|
||||
// all credentials
|
||||
const fullAccess = ['n8n-nodes-base.httpRequest'].includes(node.type);
|
||||
const fullAccess = [HTTP_REQUEST_NODE_TYPE].includes(node.type);
|
||||
|
||||
let nodeCredentialDescription: INodeCredentialDescription | undefined;
|
||||
if (!fullAccess) {
|
||||
|
@ -2239,6 +2244,7 @@ export function getNodeParameter(
|
|||
}
|
||||
|
||||
let returnData;
|
||||
|
||||
try {
|
||||
returnData = workflow.expression.getParameterValue(
|
||||
value,
|
||||
|
@ -2506,70 +2512,303 @@ const getRequestHelperFunctions = (
|
|||
workflow: Workflow,
|
||||
node: INode,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
): RequestHelperFunctions => ({
|
||||
httpRequest,
|
||||
): RequestHelperFunctions => {
|
||||
const getResolvedValue = (
|
||||
parameterValue: NodeParameterValueType,
|
||||
itemIndex: number,
|
||||
runIndex: number,
|
||||
executeData: IExecuteData,
|
||||
additionalKeys?: IWorkflowDataProxyAdditionalKeys,
|
||||
returnObjectAsString = false,
|
||||
): NodeParameterValueType => {
|
||||
const runExecutionData: IRunExecutionData | null = null;
|
||||
const connectionInputData: INodeExecutionData[] = [];
|
||||
const mode: WorkflowExecuteMode = 'internal';
|
||||
|
||||
async httpRequestWithAuthentication(
|
||||
this,
|
||||
credentialsType,
|
||||
requestOptions,
|
||||
additionalCredentialOptions,
|
||||
): Promise<any> {
|
||||
return httpRequestWithAuthentication.call(
|
||||
if (
|
||||
typeof parameterValue === 'object' ||
|
||||
(typeof parameterValue === 'string' && parameterValue.charAt(0) === '=')
|
||||
) {
|
||||
return workflow.expression.getParameterValue(
|
||||
parameterValue,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
itemIndex,
|
||||
node.name,
|
||||
connectionInputData,
|
||||
mode,
|
||||
additionalKeys ?? {},
|
||||
executeData,
|
||||
returnObjectAsString,
|
||||
);
|
||||
}
|
||||
|
||||
return parameterValue;
|
||||
};
|
||||
|
||||
return {
|
||||
httpRequest,
|
||||
async requestWithAuthenticationPaginated(
|
||||
this: IExecuteFunctions,
|
||||
requestOptions: OptionsWithUri,
|
||||
itemIndex: number,
|
||||
paginationOptions: PaginationOptions,
|
||||
credentialsType?: string,
|
||||
additionalCredentialOptions?: IAdditionalCredentialOptions,
|
||||
): Promise<any[]> {
|
||||
const responseData = [];
|
||||
if (!requestOptions.qs) {
|
||||
requestOptions.qs = {};
|
||||
}
|
||||
requestOptions.resolveWithFullResponse = true;
|
||||
requestOptions.simple = false;
|
||||
|
||||
let tempResponseData: IN8nHttpFullResponse;
|
||||
let makeAdditionalRequest: boolean;
|
||||
let paginateRequestData: IHttpRequestOptions;
|
||||
|
||||
const runIndex = 0;
|
||||
|
||||
const additionalKeys = {
|
||||
$request: requestOptions,
|
||||
$response: {} as IN8nHttpFullResponse,
|
||||
$version: node.typeVersion,
|
||||
$pageCount: 0,
|
||||
};
|
||||
|
||||
const executeData: IExecuteData = {
|
||||
data: {},
|
||||
node,
|
||||
source: null,
|
||||
};
|
||||
|
||||
const hashData = {
|
||||
identicalCount: 0,
|
||||
previousLength: 0,
|
||||
previousHash: '',
|
||||
};
|
||||
do {
|
||||
paginateRequestData = getResolvedValue(
|
||||
paginationOptions.request as unknown as NodeParameterValueType,
|
||||
itemIndex,
|
||||
runIndex,
|
||||
executeData,
|
||||
additionalKeys,
|
||||
false,
|
||||
) as object as IHttpRequestOptions;
|
||||
|
||||
const tempRequestOptions = merge(requestOptions, paginateRequestData);
|
||||
|
||||
if (credentialsType) {
|
||||
tempResponseData = await this.helpers.requestWithAuthentication.call(
|
||||
this,
|
||||
credentialsType,
|
||||
tempRequestOptions,
|
||||
additionalCredentialOptions,
|
||||
);
|
||||
} else {
|
||||
tempResponseData = await this.helpers.request(tempRequestOptions);
|
||||
}
|
||||
|
||||
const newResponse: IN8nHttpFullResponse = Object.assign(
|
||||
{
|
||||
body: {},
|
||||
headers: {},
|
||||
statusCode: 0,
|
||||
},
|
||||
pick(tempResponseData, ['body', 'headers', 'statusCode']),
|
||||
);
|
||||
|
||||
let contentBody: Exclude<IN8nHttpResponse, Buffer>;
|
||||
|
||||
if (
|
||||
newResponse.body?.constructor.name === 'IncomingMessage' &&
|
||||
paginationOptions.binaryResult !== true
|
||||
) {
|
||||
const data = await this.helpers
|
||||
.binaryToBuffer(newResponse.body as Buffer | Readable)
|
||||
.then((body) => body.toString());
|
||||
// Keep the original string version that we can use it to hash if needed
|
||||
contentBody = data;
|
||||
|
||||
const responseContentType = newResponse.headers['content-type']?.toString() ?? '';
|
||||
if (responseContentType.includes('application/json')) {
|
||||
newResponse.body = jsonParse(data, { fallbackValue: {} });
|
||||
} else {
|
||||
newResponse.body = data;
|
||||
}
|
||||
tempResponseData.__bodyResolved = true;
|
||||
tempResponseData.body = newResponse.body;
|
||||
} else {
|
||||
contentBody = newResponse.body;
|
||||
}
|
||||
|
||||
if (paginationOptions.binaryResult !== true || tempResponseData.headers.etag) {
|
||||
// If the data is not binary (and so not a stream), or an etag is present,
|
||||
// we check via etag or hash if identical data is received
|
||||
|
||||
let contentLength = 0;
|
||||
if ('content-length' in tempResponseData.headers) {
|
||||
contentLength = parseInt(tempResponseData.headers['content-length'] as string) || 0;
|
||||
}
|
||||
|
||||
if (hashData.previousLength === contentLength) {
|
||||
let hash: string;
|
||||
if (tempResponseData.headers.etag) {
|
||||
// If an etag is provided, we use it as "hash"
|
||||
hash = tempResponseData.headers.etag as string;
|
||||
} else {
|
||||
// If there is no etag, we calculate a hash from the data in the body
|
||||
if (typeof contentBody !== 'string') {
|
||||
contentBody = JSON.stringify(contentBody);
|
||||
}
|
||||
hash = crypto.createHash('md5').update(contentBody).digest('base64');
|
||||
}
|
||||
|
||||
if (hashData.previousHash === hash) {
|
||||
hashData.identicalCount += 1;
|
||||
if (hashData.identicalCount > 2) {
|
||||
// Length was identical 5x and hash 3x
|
||||
throw new NodeOperationError(
|
||||
node,
|
||||
'The returned response was identical 5x, so requests got stopped',
|
||||
{
|
||||
itemIndex,
|
||||
description:
|
||||
'Check if "Pagination Completed When" has been configured correctly.',
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
hashData.identicalCount = 0;
|
||||
}
|
||||
hashData.previousHash = hash;
|
||||
} else {
|
||||
hashData.identicalCount = 0;
|
||||
}
|
||||
hashData.previousLength = contentLength;
|
||||
}
|
||||
|
||||
responseData.push(tempResponseData);
|
||||
|
||||
additionalKeys.$response = newResponse;
|
||||
additionalKeys.$pageCount = additionalKeys.$pageCount + 1;
|
||||
|
||||
if (
|
||||
paginationOptions.maxRequests &&
|
||||
additionalKeys.$pageCount >= paginationOptions.maxRequests
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
makeAdditionalRequest = getResolvedValue(
|
||||
paginationOptions.continue,
|
||||
itemIndex,
|
||||
runIndex,
|
||||
executeData,
|
||||
additionalKeys,
|
||||
false,
|
||||
) as boolean;
|
||||
|
||||
if (makeAdditionalRequest) {
|
||||
if (tempResponseData.statusCode < 200 || tempResponseData.statusCode >= 300) {
|
||||
// We have it configured to let all requests pass no matter the response code
|
||||
// via "requestOptions.simple = false" to not by default fail if it is for example
|
||||
// configured to stop on 404 response codes. For that reason we have to throw here
|
||||
// now an error manually if the response code is not a success one.
|
||||
let data = tempResponseData.body;
|
||||
if (
|
||||
data?.constructor.name === 'IncomingMessage' &&
|
||||
paginationOptions.binaryResult !== true
|
||||
) {
|
||||
data = await this.helpers
|
||||
.binaryToBuffer(tempResponseData.body as Buffer | Readable)
|
||||
.then((body) => body.toString());
|
||||
} else if (typeof data === 'object') {
|
||||
data = JSON.stringify(data);
|
||||
}
|
||||
|
||||
throw Object.assign(
|
||||
new Error(`${tempResponseData.statusCode} - "${data?.toString()}"`),
|
||||
{
|
||||
statusCode: tempResponseData.statusCode,
|
||||
error: data,
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
headers: tempResponseData.headers,
|
||||
status: tempResponseData.statusCode,
|
||||
statusText: tempResponseData.statusMessage,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
} while (makeAdditionalRequest);
|
||||
|
||||
return responseData;
|
||||
},
|
||||
async httpRequestWithAuthentication(
|
||||
this,
|
||||
credentialsType,
|
||||
requestOptions,
|
||||
workflow,
|
||||
node,
|
||||
additionalData,
|
||||
additionalCredentialOptions,
|
||||
);
|
||||
},
|
||||
): Promise<any> {
|
||||
return httpRequestWithAuthentication.call(
|
||||
this,
|
||||
credentialsType,
|
||||
requestOptions,
|
||||
workflow,
|
||||
node,
|
||||
additionalData,
|
||||
additionalCredentialOptions,
|
||||
);
|
||||
},
|
||||
|
||||
request: async (uriOrObject, options) =>
|
||||
proxyRequestToAxios(workflow, additionalData, node, uriOrObject, options),
|
||||
request: async (uriOrObject, options) =>
|
||||
proxyRequestToAxios(workflow, additionalData, node, uriOrObject, options),
|
||||
|
||||
async requestWithAuthentication(
|
||||
this,
|
||||
credentialsType,
|
||||
requestOptions,
|
||||
additionalCredentialOptions,
|
||||
): Promise<any> {
|
||||
return requestWithAuthentication.call(
|
||||
async requestWithAuthentication(
|
||||
this,
|
||||
credentialsType,
|
||||
requestOptions,
|
||||
workflow,
|
||||
node,
|
||||
additionalData,
|
||||
additionalCredentialOptions,
|
||||
);
|
||||
},
|
||||
): Promise<any> {
|
||||
return requestWithAuthentication.call(
|
||||
this,
|
||||
credentialsType,
|
||||
requestOptions,
|
||||
workflow,
|
||||
node,
|
||||
additionalData,
|
||||
additionalCredentialOptions,
|
||||
);
|
||||
},
|
||||
|
||||
async requestOAuth1(
|
||||
this: IAllExecuteFunctions,
|
||||
credentialsType: string,
|
||||
requestOptions: OptionsWithUrl | RequestPromiseOptions,
|
||||
): Promise<any> {
|
||||
return requestOAuth1.call(this, credentialsType, requestOptions);
|
||||
},
|
||||
async requestOAuth1(
|
||||
this: IAllExecuteFunctions,
|
||||
credentialsType: string,
|
||||
requestOptions: OptionsWithUrl | RequestPromiseOptions,
|
||||
): Promise<any> {
|
||||
return requestOAuth1.call(this, credentialsType, requestOptions);
|
||||
},
|
||||
|
||||
async requestOAuth2(
|
||||
this: IAllExecuteFunctions,
|
||||
credentialsType: string,
|
||||
requestOptions: OptionsWithUri | RequestPromiseOptions,
|
||||
oAuth2Options?: IOAuth2Options,
|
||||
): Promise<any> {
|
||||
return requestOAuth2.call(
|
||||
this,
|
||||
credentialsType,
|
||||
requestOptions,
|
||||
node,
|
||||
additionalData,
|
||||
oAuth2Options,
|
||||
);
|
||||
},
|
||||
});
|
||||
async requestOAuth2(
|
||||
this: IAllExecuteFunctions,
|
||||
credentialsType: string,
|
||||
requestOptions: OptionsWithUri | RequestPromiseOptions,
|
||||
oAuth2Options?: IOAuth2Options,
|
||||
): Promise<any> {
|
||||
return requestOAuth2.call(
|
||||
this,
|
||||
credentialsType,
|
||||
requestOptions,
|
||||
node,
|
||||
additionalData,
|
||||
oAuth2Options,
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getAllowedPaths = () => {
|
||||
const restrictFileAccessTo = process.env[RESTRICT_FILE_ACCESS_TO];
|
||||
|
@ -2899,7 +3138,7 @@ export function getExecuteFunctions(
|
|||
),
|
||||
);
|
||||
},
|
||||
getContext(type: string): IContextObject {
|
||||
getContext(type: ContextType): IContextObject {
|
||||
return NodeHelpers.getContext(runExecutionData, type, node);
|
||||
},
|
||||
async getInputConnectionData(
|
||||
|
@ -3293,7 +3532,7 @@ export function getExecuteSingleFunctions(
|
|||
executeData,
|
||||
);
|
||||
},
|
||||
getContext(type: string): IContextObject {
|
||||
getContext(type: ContextType): IContextObject {
|
||||
return NodeHelpers.getContext(runExecutionData, type, node);
|
||||
},
|
||||
getCredentials: async (type) =>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
EnterpriseEditionFeature,
|
||||
HTTP_REQUEST_NODE_TYPE,
|
||||
MODAL_CONFIRM,
|
||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||
|
@ -49,7 +50,7 @@ import { externalHooks } from '@/mixins/externalHooks';
|
|||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
||||
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { get, isEqual } from 'lodash-es';
|
||||
|
||||
import type { IPermissions } from '@/permissions';
|
||||
import { getWorkflowPermissions } from '@/permissions';
|
||||
|
@ -194,6 +195,16 @@ export function resolveParameter(
|
|||
...opts.additionalKeys,
|
||||
};
|
||||
|
||||
if (activeNode?.type === HTTP_REQUEST_NODE_TYPE) {
|
||||
// Add $response for HTTP Request-Nodes as it is used
|
||||
// in pagination expressions
|
||||
additionalKeys.$response = get(
|
||||
executionData,
|
||||
`data.executionData.contextData['node:${activeNode!.name}'].response`,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
let runIndexCurrent = opts?.targetItem?.runIndex ?? 0;
|
||||
if (
|
||||
opts?.targetItem === undefined &&
|
||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
|||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
IRequestOptionsSimplified,
|
||||
PaginationOptions,
|
||||
JsonObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
@ -927,6 +928,228 @@ export class HttpRequestV3 implements INodeType {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Pagination',
|
||||
name: 'pagination',
|
||||
placeholder: 'Add pagination',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
default: {
|
||||
pagination: {},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Pagination',
|
||||
name: 'pagination',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Pagination Mode',
|
||||
name: 'paginationMode',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
noDataExpression: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Off',
|
||||
value: 'off',
|
||||
},
|
||||
{
|
||||
name: 'Update a Parameter in Each Request',
|
||||
value: 'updateAParameterInEachRequest',
|
||||
},
|
||||
{
|
||||
name: 'Response Contains Next URL',
|
||||
value: 'responseContainsNextURL',
|
||||
},
|
||||
],
|
||||
default: 'updateAParameterInEachRequest',
|
||||
description: 'If pagination should be used',
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'Use the $response variables to access the data of the previous response. <a href="https://docs.n8n.io/code/builtin/http-node-variables/?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=n8n-nodes-base.httpRequest" target="_blank">More info</a>',
|
||||
name: 'webhookNotice',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
paginationMode: ['off'],
|
||||
},
|
||||
},
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Next URL',
|
||||
name: 'nextURL',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
paginationMode: ['responseContainsNextURL'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Should evaluate to true when pagination is complete. More info.',
|
||||
},
|
||||
{
|
||||
displayName: 'Parameters',
|
||||
name: 'parameters',
|
||||
type: 'fixedCollection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
paginationMode: ['updateAParameterInEachRequest'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
noExpression: true,
|
||||
},
|
||||
placeholder: 'Add Parameter',
|
||||
default: {
|
||||
parameters: [
|
||||
{
|
||||
type: 'qs',
|
||||
name: '',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'parameters',
|
||||
displayName: 'Parameter',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'type',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Body',
|
||||
value: 'body',
|
||||
},
|
||||
{
|
||||
name: 'Header',
|
||||
value: 'headers',
|
||||
},
|
||||
{
|
||||
name: 'Query',
|
||||
value: 'qs',
|
||||
},
|
||||
],
|
||||
default: 'qs',
|
||||
description: 'Where the parameter should be set',
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Pagination Complete When',
|
||||
name: 'paginationCompleteWhen',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
noDataExpression: true,
|
||||
},
|
||||
displayOptions: {
|
||||
hide: {
|
||||
paginationMode: ['off'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Response Is Empty',
|
||||
value: 'responseIsEmpty',
|
||||
},
|
||||
{
|
||||
name: 'Receive Specific Status Code(s)',
|
||||
value: 'receiveSpecificStatusCodes',
|
||||
},
|
||||
{
|
||||
name: 'Other',
|
||||
value: 'other',
|
||||
},
|
||||
],
|
||||
default: 'responseIsEmpty',
|
||||
description: 'When should no further requests be made?',
|
||||
},
|
||||
{
|
||||
displayName: 'Status Code(s) when Complete',
|
||||
name: 'statusCodesWhenComplete',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
noDataExpression: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
paginationCompleteWhen: ['receiveSpecificStatusCodes'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Accepts comma-separated values',
|
||||
},
|
||||
{
|
||||
displayName: 'Complete Expression',
|
||||
name: 'completeExpression',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
paginationCompleteWhen: ['other'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'Should evaluate to true when pagination is complete. More info.',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit Pages Fetched',
|
||||
name: 'limitPagesFetched',
|
||||
type: 'boolean',
|
||||
typeOptions: {
|
||||
noDataExpression: true,
|
||||
},
|
||||
displayOptions: {
|
||||
hide: {
|
||||
paginationMode: ['off'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
noDataExpression: true,
|
||||
description: 'Whether the number of requests should be limited',
|
||||
},
|
||||
{
|
||||
displayName: 'Max Pages',
|
||||
name: 'maxRequests',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
noDataExpression: true,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
limitPagesFetched: [true],
|
||||
},
|
||||
},
|
||||
default: 100,
|
||||
description: 'Maximum amount of request to be make',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Proxy',
|
||||
name: 'proxy',
|
||||
|
@ -1033,6 +1256,26 @@ export class HttpRequestV3 implements INodeType {
|
|||
|
||||
let autoDetectResponseFormat = false;
|
||||
|
||||
// Can not be defined on a per item level
|
||||
const pagination = this.getNodeParameter('options.pagination.pagination', 0, null, {
|
||||
rawExpressions: true,
|
||||
}) as {
|
||||
paginationMode: 'off' | 'updateAParameterInEachRequest' | 'responseContainsNextURL';
|
||||
nextURL?: string;
|
||||
parameters: {
|
||||
parameters: Array<{
|
||||
type: 'body' | 'headers' | 'qs';
|
||||
name: string;
|
||||
value: string;
|
||||
}>;
|
||||
};
|
||||
paginationCompleteWhen: 'responseIsEmpty' | 'receiveSpecificStatusCodes' | 'other';
|
||||
statusCodesWhenComplete: string;
|
||||
completeExpression: string;
|
||||
limitPagesFetched: boolean;
|
||||
maxRequests: number;
|
||||
};
|
||||
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
const requestMethod = this.getNodeParameter('method', itemIndex) as string;
|
||||
|
||||
|
@ -1117,15 +1360,9 @@ export class HttpRequestV3 implements INodeType {
|
|||
gzip: true,
|
||||
rejectUnauthorized: !allowUnauthorizedCerts || false,
|
||||
followRedirect: false,
|
||||
resolveWithFullResponse: true,
|
||||
};
|
||||
|
||||
// When response format is set to auto-detect,
|
||||
// we need to access to response header content-type
|
||||
// and the only way is using "resolveWithFullResponse"
|
||||
if (autoDetectResponseFormat || fullResponse) {
|
||||
requestOptions.resolveWithFullResponse = true;
|
||||
}
|
||||
|
||||
if (requestOptions.method !== 'GET' && nodeVersion >= 4.1) {
|
||||
requestOptions = { ...requestOptions, followAllRedirects: false };
|
||||
}
|
||||
|
@ -1321,7 +1558,7 @@ export class HttpRequestV3 implements INodeType {
|
|||
requestOptions.json = true;
|
||||
}
|
||||
|
||||
// // Add Content Type if any are set
|
||||
// Add Content Type if any are set
|
||||
if (bodyContentType === 'raw') {
|
||||
if (requestOptions.headers === undefined) {
|
||||
requestOptions.headers = {};
|
||||
|
@ -1392,7 +1629,64 @@ export class HttpRequestV3 implements INodeType {
|
|||
try {
|
||||
this.sendMessageToUI(sanitizeUiMessage(requestOptions, authDataKeys));
|
||||
} catch (e) {}
|
||||
if (authentication === 'genericCredentialType' || authentication === 'none') {
|
||||
|
||||
if (pagination && pagination.paginationMode !== 'off') {
|
||||
let continueExpression = '={{false}}';
|
||||
if (pagination.paginationCompleteWhen === 'receiveSpecificStatusCodes') {
|
||||
// Split out comma separated list of status codes into array
|
||||
const statusCodesWhenCompleted = pagination.statusCodesWhenComplete
|
||||
.split(',')
|
||||
.map((item) => parseInt(item.trim()));
|
||||
|
||||
continueExpression = `={{ !${JSON.stringify(
|
||||
statusCodesWhenCompleted,
|
||||
)}.includes($response.statusCode) }}`;
|
||||
} else if (pagination.paginationCompleteWhen === 'responseIsEmpty') {
|
||||
continueExpression =
|
||||
'={{ Array.isArray($response.body) ? $response.body.length : !!$response.body }}';
|
||||
} else {
|
||||
// Other
|
||||
if (!pagination.completeExpression.length || pagination.completeExpression[0] !== '=') {
|
||||
throw new NodeOperationError(this.getNode(), 'Invalid or empty Complete Expression');
|
||||
}
|
||||
continueExpression = `={{ !(${pagination.completeExpression.trim().slice(3, -2)}) }}`;
|
||||
}
|
||||
|
||||
const paginationData: PaginationOptions = {
|
||||
continue: continueExpression,
|
||||
request: {},
|
||||
};
|
||||
|
||||
if (pagination.paginationMode === 'updateAParameterInEachRequest') {
|
||||
// Iterate over all parameters and add them to the request
|
||||
paginationData.request = {};
|
||||
pagination.parameters.parameters.forEach((parameter) => {
|
||||
if (!paginationData.request[parameter.type]) {
|
||||
paginationData.request[parameter.type] = {};
|
||||
}
|
||||
paginationData.request[parameter.type]![parameter.name] = parameter.value;
|
||||
});
|
||||
} else if (pagination.paginationMode === 'responseContainsNextURL') {
|
||||
paginationData.request.url = pagination.nextURL;
|
||||
}
|
||||
|
||||
if (pagination.limitPagesFetched) {
|
||||
paginationData.maxRequests = pagination.maxRequests;
|
||||
}
|
||||
|
||||
if (responseFormat === 'file') {
|
||||
paginationData.binaryResult = true;
|
||||
}
|
||||
|
||||
const requestPromise = this.helpers.requestWithAuthenticationPaginated.call(
|
||||
this,
|
||||
requestOptions,
|
||||
itemIndex,
|
||||
paginationData,
|
||||
nodeCredentialType,
|
||||
);
|
||||
requestPromises.push(requestPromise);
|
||||
} else if (authentication === 'genericCredentialType' || authentication === 'none') {
|
||||
if (oAuth1Api) {
|
||||
const requestOAuth1 = this.helpers.requestOAuth1.call(this, 'oAuth1Api', requestOptions);
|
||||
requestOAuth1.catch(() => {});
|
||||
|
@ -1426,25 +1720,25 @@ export class HttpRequestV3 implements INodeType {
|
|||
}
|
||||
const promisesResponses = await Promise.allSettled(requestPromises);
|
||||
|
||||
let response: any;
|
||||
let responseData: any;
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
response = promisesResponses.shift();
|
||||
if (response!.status !== 'fulfilled') {
|
||||
if (response.reason.statusCode === 429) {
|
||||
response.reason.message =
|
||||
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'";
|
||||
}
|
||||
if (!this.continueOnFail()) {
|
||||
if (autoDetectResponseFormat && response.reason.error instanceof Buffer) {
|
||||
response.reason.error = Buffer.from(response.reason.error as Buffer).toString();
|
||||
if (autoDetectResponseFormat && responseData.reason.error instanceof Buffer) {
|
||||
responseData.reason.error = Buffer.from(responseData.reason.error as Buffer).toString();
|
||||
}
|
||||
throw new NodeApiError(this.getNode(), response as JsonObject, { itemIndex });
|
||||
throw new NodeApiError(this.getNode(), responseData as JsonObject, { itemIndex });
|
||||
} else {
|
||||
removeCircularRefs(response.reason as JsonObject);
|
||||
removeCircularRefs(responseData.reason as JsonObject);
|
||||
// Return the actual reason as error
|
||||
returnItems.push({
|
||||
json: {
|
||||
error: response.reason,
|
||||
error: responseData.reason,
|
||||
},
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
|
@ -1454,7 +1748,12 @@ export class HttpRequestV3 implements INodeType {
|
|||
}
|
||||
}
|
||||
|
||||
response = response.value;
|
||||
let responses: any[];
|
||||
if (Array.isArray(responseData.value)) {
|
||||
responses = responseData.value;
|
||||
} else {
|
||||
responses = [responseData.value];
|
||||
}
|
||||
|
||||
let responseFormat = this.getNodeParameter(
|
||||
'options.response.response.responseFormat',
|
||||
|
@ -1468,172 +1767,194 @@ export class HttpRequestV3 implements INodeType {
|
|||
false,
|
||||
) as boolean;
|
||||
|
||||
if (autoDetectResponseFormat) {
|
||||
const responseContentType = response.headers['content-type'] ?? '';
|
||||
if (responseContentType.includes('application/json')) {
|
||||
responseFormat = 'json';
|
||||
const neverError = this.getNodeParameter(
|
||||
'options.response.response.neverError',
|
||||
0,
|
||||
false,
|
||||
) as boolean;
|
||||
|
||||
const data = await this.helpers
|
||||
.binaryToBuffer(response.body as Buffer | Readable)
|
||||
.then((body) => body.toString());
|
||||
response.body = jsonParse(data, {
|
||||
...(neverError
|
||||
? { fallbackValue: {} }
|
||||
: { errorMessage: 'Invalid JSON in response body' }),
|
||||
});
|
||||
} else if (binaryContentTypes.some((e) => responseContentType.includes(e))) {
|
||||
responseFormat = 'file';
|
||||
} else {
|
||||
responseFormat = 'text';
|
||||
const data = await this.helpers
|
||||
.binaryToBuffer(response.body as Buffer | Readable)
|
||||
.then((body) => body.toString());
|
||||
response.body = !data ? undefined : data;
|
||||
}
|
||||
}
|
||||
|
||||
if (autoDetectResponseFormat && !fullResponse) {
|
||||
delete response.headers;
|
||||
delete response.statusCode;
|
||||
delete response.statusMessage;
|
||||
response = response.body;
|
||||
requestOptions.resolveWithFullResponse = false;
|
||||
}
|
||||
|
||||
if (responseFormat === 'file') {
|
||||
const outputPropertyName = this.getNodeParameter(
|
||||
'options.response.response.outputPropertyName',
|
||||
0,
|
||||
'data',
|
||||
) as string;
|
||||
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {},
|
||||
binary: {},
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
};
|
||||
|
||||
if (items[itemIndex].binary !== undefined) {
|
||||
// Create a shallow copy of the binary data so that the old
|
||||
// data references which do not get changed still stay behind
|
||||
// but the incoming data does not get changed.
|
||||
Object.assign(newItem.binary as IBinaryKeyData, items[itemIndex].binary);
|
||||
}
|
||||
|
||||
let binaryData: Buffer | Readable;
|
||||
if (fullResponse) {
|
||||
const returnItem: IDataObject = {};
|
||||
for (const property of fullResponseProperties) {
|
||||
if (property === 'body') {
|
||||
continue;
|
||||
}
|
||||
returnItem[property] = response![property];
|
||||
}
|
||||
|
||||
newItem.json = returnItem;
|
||||
binaryData = response!.body;
|
||||
} else {
|
||||
newItem.json = items[itemIndex].json;
|
||||
binaryData = response;
|
||||
}
|
||||
newItem.binary![outputPropertyName] = await this.helpers.prepareBinaryData(binaryData);
|
||||
|
||||
returnItems.push(newItem);
|
||||
} else if (responseFormat === 'text') {
|
||||
const outputPropertyName = this.getNodeParameter(
|
||||
'options.response.response.outputPropertyName',
|
||||
0,
|
||||
'data',
|
||||
) as string;
|
||||
if (fullResponse) {
|
||||
const returnItem: IDataObject = {};
|
||||
for (const property of fullResponseProperties) {
|
||||
if (property === 'body') {
|
||||
returnItem[outputPropertyName] = toText(response![property]);
|
||||
continue;
|
||||
}
|
||||
|
||||
returnItem[property] = response![property];
|
||||
}
|
||||
returnItems.push({
|
||||
json: returnItem,
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
returnItems.push({
|
||||
json: {
|
||||
[outputPropertyName]: toText(response),
|
||||
},
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// responseFormat: 'json'
|
||||
if (requestOptions.resolveWithFullResponse === true) {
|
||||
const returnItem: IDataObject = {};
|
||||
for (const property of fullResponseProperties) {
|
||||
returnItem[property] = response![property];
|
||||
}
|
||||
|
||||
if (responseFormat === 'json' && typeof returnItem.body === 'string') {
|
||||
try {
|
||||
returnItem.body = JSON.parse(returnItem.body);
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Response body is not valid JSON. Change "Response Format" to "Text"',
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
returnItems.push({
|
||||
json: returnItem,
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (responseFormat === 'json' && typeof response === 'string') {
|
||||
try {
|
||||
response = JSON.parse(response);
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Response body is not valid JSON. Change "Response Format" to "Text"',
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
response.forEach((item) =>
|
||||
returnItems.push({
|
||||
json: item,
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
}),
|
||||
);
|
||||
// eslint-disable-next-line prefer-const
|
||||
for (let [index, response] of Object.entries(responses)) {
|
||||
if (this.getMode() === 'manual' && index === '0') {
|
||||
// For manual executions save the first response in the context
|
||||
// so that we can use it in the frontend and so make it easier for
|
||||
// the users to create the required pagination expressions
|
||||
const nodeContext = this.getContext('node');
|
||||
if (pagination && pagination.paginationMode !== 'off') {
|
||||
nodeContext.response = responseData.value[0];
|
||||
} else {
|
||||
nodeContext.response = responseData.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (autoDetectResponseFormat) {
|
||||
const responseContentType = response.headers['content-type'] ?? '';
|
||||
if (responseContentType.includes('application/json')) {
|
||||
responseFormat = 'json';
|
||||
if (!response.__bodyResolved) {
|
||||
const neverError = this.getNodeParameter(
|
||||
'options.response.response.neverError',
|
||||
0,
|
||||
false,
|
||||
) as boolean;
|
||||
|
||||
const data = await this.helpers
|
||||
.binaryToBuffer(response.body as Buffer | Readable)
|
||||
.then((body) => body.toString());
|
||||
response.body = jsonParse(data, {
|
||||
...(neverError
|
||||
? { fallbackValue: {} }
|
||||
: { errorMessage: 'Invalid JSON in response body' }),
|
||||
});
|
||||
}
|
||||
} else if (binaryContentTypes.some((e) => responseContentType.includes(e))) {
|
||||
responseFormat = 'file';
|
||||
} else {
|
||||
responseFormat = 'text';
|
||||
if (!response.__bodyResolved) {
|
||||
const data = await this.helpers
|
||||
.binaryToBuffer(response.body as Buffer | Readable)
|
||||
.then((body) => body.toString());
|
||||
response.body = !data ? undefined : data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (autoDetectResponseFormat && !fullResponse) {
|
||||
delete response.headers;
|
||||
delete response.statusCode;
|
||||
delete response.statusMessage;
|
||||
}
|
||||
if (!fullResponse) {
|
||||
response = response.body;
|
||||
}
|
||||
|
||||
if (responseFormat === 'file') {
|
||||
const outputPropertyName = this.getNodeParameter(
|
||||
'options.response.response.outputPropertyName',
|
||||
0,
|
||||
'data',
|
||||
) as string;
|
||||
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {},
|
||||
binary: {},
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
};
|
||||
|
||||
if (items[itemIndex].binary !== undefined) {
|
||||
// Create a shallow copy of the binary data so that the old
|
||||
// data references which do not get changed still stay behind
|
||||
// but the incoming data does not get changed.
|
||||
Object.assign(newItem.binary as IBinaryKeyData, items[itemIndex].binary);
|
||||
}
|
||||
|
||||
let binaryData: Buffer | Readable;
|
||||
if (fullResponse) {
|
||||
const returnItem: IDataObject = {};
|
||||
for (const property of fullResponseProperties) {
|
||||
if (property === 'body') {
|
||||
continue;
|
||||
}
|
||||
returnItem[property] = response[property];
|
||||
}
|
||||
|
||||
newItem.json = returnItem;
|
||||
binaryData = response?.body;
|
||||
} else {
|
||||
newItem.json = items[itemIndex].json;
|
||||
binaryData = response;
|
||||
}
|
||||
newItem.binary![outputPropertyName] = await this.helpers.prepareBinaryData(binaryData);
|
||||
|
||||
returnItems.push(newItem);
|
||||
} else if (responseFormat === 'text') {
|
||||
const outputPropertyName = this.getNodeParameter(
|
||||
'options.response.response.outputPropertyName',
|
||||
0,
|
||||
'data',
|
||||
) as string;
|
||||
if (fullResponse) {
|
||||
const returnItem: IDataObject = {};
|
||||
for (const property of fullResponseProperties) {
|
||||
if (property === 'body') {
|
||||
returnItem[outputPropertyName] = toText(response[property]);
|
||||
continue;
|
||||
}
|
||||
|
||||
returnItem[property] = response[property];
|
||||
}
|
||||
returnItems.push({
|
||||
json: response,
|
||||
json: returnItem,
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
returnItems.push({
|
||||
json: {
|
||||
[outputPropertyName]: toText(response),
|
||||
},
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// responseFormat: 'json'
|
||||
if (fullResponse) {
|
||||
const returnItem: IDataObject = {};
|
||||
for (const property of fullResponseProperties) {
|
||||
returnItem[property] = response[property];
|
||||
}
|
||||
|
||||
if (responseFormat === 'json' && typeof returnItem.body === 'string') {
|
||||
try {
|
||||
returnItem.body = JSON.parse(returnItem.body);
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Response body is not valid JSON. Change "Response Format" to "Text"',
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
returnItems.push({
|
||||
json: returnItem,
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (responseFormat === 'json' && typeof response === 'string') {
|
||||
try {
|
||||
if (typeof response !== 'object') {
|
||||
response = JSON.parse(response);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Response body is not valid JSON. Change "Response Format" to "Text"',
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
response.forEach((item) =>
|
||||
returnItems.push({
|
||||
json: item,
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
returnItems.push({
|
||||
json: response,
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
workflowToTests,
|
||||
getWorkflowFilenames,
|
||||
} from '@test/nodes/Helpers';
|
||||
import { parse as parseUrl } from 'url';
|
||||
|
||||
describe('Test HTTP Request Node', () => {
|
||||
const workflows = getWorkflowFilenames(__dirname);
|
||||
|
@ -117,6 +118,48 @@ describe('Test HTTP Request Node', () => {
|
|||
isDeleted: true,
|
||||
deletedOn: '2023-02-09T05:37:31.720Z',
|
||||
});
|
||||
|
||||
// Pagination - Data not identical to dummyjson.com
|
||||
nock(baseUrl)
|
||||
.persist()
|
||||
.get('/users')
|
||||
.query(true)
|
||||
.reply(function (uri) {
|
||||
const data = parseUrl(uri, true);
|
||||
const skip = parseInt((data.query.skip as string) || '0', 10);
|
||||
const limit = parseInt((data.query.limit as string) || '10', 10);
|
||||
const nextUrl = `${baseUrl}/users?skip=${skip + limit}&limit=${limit}`;
|
||||
|
||||
const response = [];
|
||||
for (let i = skip; i < skip + limit; i++) {
|
||||
if (i > 14) {
|
||||
break;
|
||||
}
|
||||
response.push({
|
||||
id: i,
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.length) {
|
||||
return [
|
||||
404,
|
||||
response,
|
||||
{
|
||||
'next-url': nextUrl,
|
||||
'content-type': this.req.headers['content-type'] || 'application/json',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
200,
|
||||
response,
|
||||
{
|
||||
'next-url': nextUrl,
|
||||
'content-type': this.req.headers['content-type'] || 'application/json',
|
||||
},
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -511,10 +511,18 @@ export interface IHttpRequestOptions {
|
|||
json?: boolean;
|
||||
}
|
||||
|
||||
export interface PaginationOptions {
|
||||
binaryResult?: boolean;
|
||||
continue: boolean | string;
|
||||
request: IRequestOptionsSimplifiedAuth;
|
||||
maxRequests?: number;
|
||||
}
|
||||
|
||||
export type IN8nHttpResponse = IDataObject | Buffer | GenericValue | GenericValue[] | null;
|
||||
|
||||
export interface IN8nHttpFullResponse {
|
||||
body: IN8nHttpResponse | Readable;
|
||||
__bodyResolved?: boolean;
|
||||
headers: IDataObject;
|
||||
statusCode: number;
|
||||
statusMessage?: string;
|
||||
|
@ -708,6 +716,14 @@ export interface RequestHelperFunctions {
|
|||
requestOptions: IHttpRequestOptions,
|
||||
additionalCredentialOptions?: IAdditionalCredentialOptions,
|
||||
): Promise<any>;
|
||||
requestWithAuthenticationPaginated(
|
||||
this: IAllExecuteFunctions,
|
||||
requestOptions: OptionsWithUri,
|
||||
itemIndex: number,
|
||||
paginationOptions: PaginationOptions,
|
||||
credentialsType?: string,
|
||||
additionalCredentialOptions?: IAdditionalCredentialOptions,
|
||||
): Promise<any[]>;
|
||||
|
||||
requestOAuth1(
|
||||
this: IAllExecuteFunctions,
|
||||
|
@ -745,10 +761,12 @@ type FunctionsBaseWithRequiredKeys<Keys extends keyof FunctionsBase> = Functions
|
|||
[K in Keys]: NonNullable<FunctionsBase[K]>;
|
||||
};
|
||||
|
||||
export type ContextType = 'flow' | 'node';
|
||||
|
||||
type BaseExecutionFunctions = FunctionsBaseWithRequiredKeys<'getMode'> & {
|
||||
continueOnFail(): boolean;
|
||||
evaluateExpression(expression: string, itemIndex: number): NodeParameterValueType;
|
||||
getContext(type: string): IContextObject;
|
||||
getContext(type: ContextType): IContextObject;
|
||||
getExecuteData(): IExecuteData;
|
||||
getWorkflowDataProxy(itemIndex: number): IWorkflowDataProxyData;
|
||||
getInputSourceData(inputIndex?: number, inputName?: string): ISourceData;
|
||||
|
|
Loading…
Reference in a new issue