feat(ERPNext Node): Add credential test and add support for unauthorized certs (#3732)

*  Add cred injection, cred testing, allow unauthorized certs

* Add support for skipping SSL for cred testing

* 📘 Add partial override for request options types (#3739)

* Change field names and fix error handling

* Fix typo

Co-authored-by: Omar Ajoue <krynble@gmail.com>
Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
agobrech 2022-07-20 13:50:16 +02:00 committed by GitHub
parent 1965407030
commit a02b206170
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 71 additions and 39 deletions

View file

@ -1,4 +1,6 @@
import { import {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType, ICredentialType,
INodeProperties, INodeProperties,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -66,5 +68,27 @@ export class ERPNextApi implements ICredentialType {
}, },
}, },
}, },
{
displayName: 'Ignore SSL Issues',
name: 'allowUnauthorizedCerts',
type: 'boolean',
description: 'Whether to connect even if SSL certificate validation is not possible',
default: false,
},
]; ];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=token {{$credentials.apiKey}}:{{$credentials.apiSecret}}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: '={{$credentials.environment === "cloudHosted" ? "https://" + $credentials.subdomain + ".erpnext.com" : $credentials.domain}}',
url: '/api/resource/Doctype',
skipSslCertificateValidation: '={{ $credentials.allowUnauthorizedCerts }}',
},
};
} }

View file

@ -12,7 +12,6 @@ import {
IHookFunctions, IHookFunctions,
IWebhookFunctions, IWebhookFunctions,
NodeApiError, NodeApiError,
NodeOperationError
} from 'n8n-workflow'; } from 'n8n-workflow';
export async function erpNextApiRequest( export async function erpNextApiRequest(
@ -31,13 +30,13 @@ export async function erpNextApiRequest(
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: `token ${credentials.apiKey}:${credentials.apiSecret}`,
}, },
method, method,
body, body,
qs: query, qs: query,
uri: uri || `${baseUrl}${resource}`, uri: uri || `${baseUrl}${resource}`,
json: true, json: true,
rejectUnauthorized: !credentials.allowUnauthorizedCerts as boolean,
}; };
options = Object.assign({}, options, option); options = Object.assign({}, options, option);
@ -50,7 +49,7 @@ export async function erpNextApiRequest(
delete options.qs; delete options.qs;
} }
try { try {
return await this.helpers.request!(options); return await this.helpers.requestWithAuthentication.call(this, 'erpNextApi',options);
} catch (error) { } catch (error) {
if (error.statusCode === 403) { if (error.statusCode === 403) {
throw new NodeApiError(this.getNode(), { message: 'DocType unavailable.' }); throw new NodeApiError(this.getNode(), { message: 'DocType unavailable.' });
@ -105,4 +104,5 @@ type ERPNextApiCredentials = {
environment: 'cloudHosted' | 'selfHosted'; environment: 'cloudHosted' | 'selfHosted';
subdomain?: string; subdomain?: string;
domain?: string; domain?: string;
allowUnauthorizedCerts?: boolean;
}; };

View file

@ -261,8 +261,34 @@ export interface IAuthenticateRuleResponseSuccessBody extends IAuthenticateRuleB
value: any; value: any;
}; };
} }
type Override<A extends object, B extends object> = Omit<A, keyof B> & B;
export namespace DeclarativeRestApiSettings {
// The type below might be extended
// with new options that need to be parsed as expressions
export type HttpRequestOptions = Override<
IHttpRequestOptions,
{ skipSslCertificateValidation?: string | boolean; url?: string }
>;
export type ResultOptions = {
maxResults?: number | string;
options: HttpRequestOptions;
paginate?: boolean | string;
preSend: PreSendAction[];
postReceive: Array<{
data: {
parameterValue: string | IDataObject | undefined;
};
actions: PostReceiveAction[];
}>;
requestOperations?: IN8nRequestOperations;
};
}
export interface ICredentialTestRequest { export interface ICredentialTestRequest {
request: IHttpRequestOptions; request: DeclarativeRestApiSettings.HttpRequestOptions;
rules?: IAuthenticateRuleResponseCode[] | IAuthenticateRuleResponseSuccessBody[]; rules?: IAuthenticateRuleResponseCode[] | IAuthenticateRuleResponseSuccessBody[];
} }
@ -487,7 +513,7 @@ export interface IN8nRequestOperations {
| IN8nRequestOperationPaginationOffset | IN8nRequestOperationPaginationOffset
| (( | ((
this: IExecutePaginationFunctions, this: IExecutePaginationFunctions,
requestOptions: IRequestOptionsFromParameters, requestOptions: DeclarativeRestApiSettings.ResultOptions,
) => Promise<INodeExecutionData[]>); ) => Promise<INodeExecutionData[]>);
} }
@ -600,7 +626,7 @@ export interface IExecuteSingleFunctions {
export interface IExecutePaginationFunctions extends IExecuteSingleFunctions { export interface IExecutePaginationFunctions extends IExecuteSingleFunctions {
makeRoutingRequest( makeRoutingRequest(
this: IAllExecuteFunctions, this: IAllExecuteFunctions,
requestOptions: IRequestOptionsFromParameters, requestOptions: DeclarativeRestApiSettings.ResultOptions,
): Promise<INodeExecutionData[]>; ): Promise<INodeExecutionData[]>;
} }
export interface IExecuteWorkflowInfo { export interface IExecuteWorkflowInfo {
@ -889,7 +915,7 @@ export interface ILoadOptions {
routing?: { routing?: {
operations?: IN8nRequestOperations; operations?: IN8nRequestOperations;
output?: INodeRequestOutput; output?: INodeRequestOutput;
request?: IHttpRequestOptionsFromParameters; request?: DeclarativeRestApiSettings.HttpRequestOptions;
}; };
} }
@ -1069,7 +1095,7 @@ export interface INodeTypeBaseDescription {
export interface INodePropertyRouting { export interface INodePropertyRouting {
operations?: IN8nRequestOperations; // Should be changed, does not sound right operations?: IN8nRequestOperations; // Should be changed, does not sound right
output?: INodeRequestOutput; output?: INodeRequestOutput;
request?: IHttpRequestOptionsFromParameters; request?: DeclarativeRestApiSettings.HttpRequestOptions;
send?: INodeRequestSend; send?: INodeRequestSend;
} }
@ -1147,24 +1173,6 @@ export interface IPostReceiveSort extends IPostReceiveBase {
}; };
} }
export interface IHttpRequestOptionsFromParameters extends Partial<IHttpRequestOptions> {
url?: string;
}
export interface IRequestOptionsFromParameters {
maxResults?: number | string;
options: IHttpRequestOptionsFromParameters;
paginate?: boolean | string;
preSend: PreSendAction[];
postReceive: Array<{
data: {
parameterValue: string | IDataObject | undefined;
};
actions: PostReceiveAction[];
}>;
requestOperations?: IN8nRequestOperations;
}
export interface INodeTypeDescription extends INodeTypeBaseDescription { export interface INodeTypeDescription extends INodeTypeBaseDescription {
version: number | number[]; version: number | number[];
defaults: INodeParameters; defaults: INodeParameters;
@ -1178,7 +1186,7 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription {
credentials?: INodeCredentialDescription[]; credentials?: INodeCredentialDescription[];
maxNodes?: number; // How many nodes of that type can be created in a workflow maxNodes?: number; // How many nodes of that type can be created in a workflow
polling?: boolean; polling?: boolean;
requestDefaults?: IHttpRequestOptionsFromParameters; requestDefaults?: DeclarativeRestApiSettings.HttpRequestOptions;
requestOperations?: IN8nRequestOperations; requestOperations?: IN8nRequestOperations;
hooks?: { hooks?: {
[key: string]: INodeHookDescription[] | undefined; [key: string]: INodeHookDescription[] | undefined;

View file

@ -292,7 +292,7 @@ export class NodeApiError extends NodeError {
} }
// if it's an error generated by axios // if it's an error generated by axios
// look for descriptions in the response object // look for descriptions in the response object
if (error.isAxiosError) { if (error.isAxiosError && error.response) {
error = error.response as JsonObject; error = error.response as JsonObject;
} }

View file

@ -24,7 +24,7 @@ import {
INodeParameters, INodeParameters,
INodePropertyOptions, INodePropertyOptions,
INodeType, INodeType,
IRequestOptionsFromParameters, DeclarativeRestApiSettings,
IRunExecutionData, IRunExecutionData,
ITaskDataConnections, ITaskDataConnections,
IWorkflowDataProxyAdditionalKeys, IWorkflowDataProxyAdditionalKeys,
@ -127,7 +127,7 @@ export class RoutingNode {
executeData, executeData,
this.mode, this.mode,
); );
const requestData: IRequestOptionsFromParameters = { const requestData: DeclarativeRestApiSettings.ResultOptions = {
options: { options: {
qs: {}, qs: {},
body: {}, body: {},
@ -214,8 +214,8 @@ export class RoutingNode {
} }
mergeOptions( mergeOptions(
destinationOptions: IRequestOptionsFromParameters, destinationOptions: DeclarativeRestApiSettings.ResultOptions,
sourceOptions?: IRequestOptionsFromParameters, sourceOptions?: DeclarativeRestApiSettings.ResultOptions,
): void { ): void {
if (sourceOptions) { if (sourceOptions) {
destinationOptions.paginate = destinationOptions.paginate ?? sourceOptions.paginate; destinationOptions.paginate = destinationOptions.paginate ?? sourceOptions.paginate;
@ -375,7 +375,7 @@ export class RoutingNode {
async rawRoutingRequest( async rawRoutingRequest(
executeSingleFunctions: IExecuteSingleFunctions, executeSingleFunctions: IExecuteSingleFunctions,
requestData: IRequestOptionsFromParameters, requestData: DeclarativeRestApiSettings.ResultOptions,
itemIndex: number, itemIndex: number,
runIndex: number, runIndex: number,
credentialType?: string, credentialType?: string,
@ -434,7 +434,7 @@ export class RoutingNode {
} }
async makeRoutingRequest( async makeRoutingRequest(
requestData: IRequestOptionsFromParameters, requestData: DeclarativeRestApiSettings.ResultOptions,
executeSingleFunctions: IExecuteSingleFunctions, executeSingleFunctions: IExecuteSingleFunctions,
itemIndex: number, itemIndex: number,
runIndex: number, runIndex: number,
@ -452,7 +452,7 @@ export class RoutingNode {
const executePaginationFunctions = { const executePaginationFunctions = {
...executeSingleFunctions, ...executeSingleFunctions,
makeRoutingRequest: async (requestOptions: IRequestOptionsFromParameters) => { makeRoutingRequest: async (requestOptions: DeclarativeRestApiSettings.ResultOptions) => {
return this.rawRoutingRequest( return this.rawRoutingRequest(
executeSingleFunctions, executeSingleFunctions,
requestOptions, requestOptions,
@ -591,8 +591,8 @@ export class RoutingNode {
runIndex: number, runIndex: number,
path: string, path: string,
additionalKeys?: IWorkflowDataProxyAdditionalKeys, additionalKeys?: IWorkflowDataProxyAdditionalKeys,
): IRequestOptionsFromParameters | undefined { ): DeclarativeRestApiSettings.ResultOptions | undefined {
const returnData: IRequestOptionsFromParameters = { const returnData: DeclarativeRestApiSettings.ResultOptions = {
options: { options: {
qs: {}, qs: {},
body: {}, body: {},

View file

@ -2,7 +2,7 @@ import {
INode, INode,
INodeExecutionData, INodeExecutionData,
INodeParameters, INodeParameters,
IRequestOptionsFromParameters, DeclarativeRestApiSettings,
IRunExecutionData, IRunExecutionData,
RoutingNode, RoutingNode,
Workflow, Workflow,
@ -46,7 +46,7 @@ describe('RoutingNode', () => {
nodeParameters: INodeParameters; nodeParameters: INodeParameters;
nodeTypeProperties: INodeProperties; nodeTypeProperties: INodeProperties;
}; };
output: IRequestOptionsFromParameters | undefined; output: DeclarativeRestApiSettings.ResultOptions | undefined;
}> = [ }> = [
{ {
description: 'single parameter, only send defined, fixed value', description: 'single parameter, only send defined, fixed value',