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

View file

@ -261,8 +261,34 @@ export interface IAuthenticateRuleResponseSuccessBody extends IAuthenticateRuleB
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 {
request: IHttpRequestOptions;
request: DeclarativeRestApiSettings.HttpRequestOptions;
rules?: IAuthenticateRuleResponseCode[] | IAuthenticateRuleResponseSuccessBody[];
}
@ -487,7 +513,7 @@ export interface IN8nRequestOperations {
| IN8nRequestOperationPaginationOffset
| ((
this: IExecutePaginationFunctions,
requestOptions: IRequestOptionsFromParameters,
requestOptions: DeclarativeRestApiSettings.ResultOptions,
) => Promise<INodeExecutionData[]>);
}
@ -600,7 +626,7 @@ export interface IExecuteSingleFunctions {
export interface IExecutePaginationFunctions extends IExecuteSingleFunctions {
makeRoutingRequest(
this: IAllExecuteFunctions,
requestOptions: IRequestOptionsFromParameters,
requestOptions: DeclarativeRestApiSettings.ResultOptions,
): Promise<INodeExecutionData[]>;
}
export interface IExecuteWorkflowInfo {
@ -889,7 +915,7 @@ export interface ILoadOptions {
routing?: {
operations?: IN8nRequestOperations;
output?: INodeRequestOutput;
request?: IHttpRequestOptionsFromParameters;
request?: DeclarativeRestApiSettings.HttpRequestOptions;
};
}
@ -1069,7 +1095,7 @@ export interface INodeTypeBaseDescription {
export interface INodePropertyRouting {
operations?: IN8nRequestOperations; // Should be changed, does not sound right
output?: INodeRequestOutput;
request?: IHttpRequestOptionsFromParameters;
request?: DeclarativeRestApiSettings.HttpRequestOptions;
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 {
version: number | number[];
defaults: INodeParameters;
@ -1178,7 +1186,7 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription {
credentials?: INodeCredentialDescription[];
maxNodes?: number; // How many nodes of that type can be created in a workflow
polling?: boolean;
requestDefaults?: IHttpRequestOptionsFromParameters;
requestDefaults?: DeclarativeRestApiSettings.HttpRequestOptions;
requestOperations?: IN8nRequestOperations;
hooks?: {
[key: string]: INodeHookDescription[] | undefined;

View file

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

View file

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

View file

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