Improvements

This commit is contained in:
ricardo 2020-07-22 00:12:42 -04:00
parent be11ff473a
commit 1577322a2a
3 changed files with 199 additions and 120 deletions

View file

@ -1,4 +1,6 @@
import { INodeProperties } from 'n8n-workflow';
import {
INodeProperties,
} from 'n8n-workflow';
export const documentOperations = [
{
@ -51,7 +53,10 @@ export const documentFields = [
{
displayName: 'DocType',
name: 'docType',
type: 'string',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDocTypes',
},
default: '',
description: 'The DocType of which the documents you want to get.',
placeholder: 'Customer',
@ -98,8 +103,8 @@ export const documentFields = [
'getAll',
],
returnAll: [
false
]
false,
],
},
},
},
@ -123,7 +128,13 @@ export const documentFields = [
{
displayName: 'Fields',
name: 'fields',
type: 'string',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getDocFields',
loadOptionsDependsOn: [
'docType',
],
},
default: '',
description: 'Comma separated fields you wish returned.',
placeholder: 'name,country'
@ -141,14 +152,6 @@ export const documentFields = [
displayName: 'Property',
name: 'customProperty',
values: [
{
displayName: 'DocType',
name: 'docType',
type: 'string',
default: '',
description: 'The DocType you would like to receive.',
placeholder: 'Customer'
},
{
displayName: 'Field',
name: 'field',
@ -210,8 +213,11 @@ export const documentFields = [
{
displayName: 'DocType',
name: 'docType',
type: 'string',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getDocTypes',
},
description: 'DocType you would like to create.',
placeholder: 'Customer',
displayOptions: {
@ -220,7 +226,7 @@ export const documentFields = [
'document',
],
operation: [
'create'
'create',
],
},
},
@ -252,7 +258,13 @@ export const documentFields = [
{
displayName: 'Field',
name: 'field',
type: 'string',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDocFields',
loadOptionsDependsOn: [
'docType',
],
},
default: '',
description: 'Name of field.',
placeholder: 'Name'
@ -275,7 +287,10 @@ export const documentFields = [
{
displayName: 'DocType',
name: 'docType',
type: 'string',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDocTypes',
},
default: '',
description: 'The type of document you would like to get.',
displayOptions: {
@ -314,7 +329,10 @@ export const documentFields = [
{
displayName: 'DocType',
name: 'docType',
type: 'string',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDocTypes',
},
default: '',
description: 'The type of document you would like to delete.',
displayOptions: {
@ -353,7 +371,10 @@ export const documentFields = [
{
displayName: 'DocType',
name: 'docType',
type: 'string',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDocTypes',
},
default: '',
description: 'The type of document you would like to update',
displayOptions: {
@ -413,7 +434,13 @@ export const documentFields = [
{
displayName: 'Field',
name: 'field',
type: 'string',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDocFields',
loadOptionsDependsOn: [
'docType',
],
},
default: '',
description: 'Name of field.',
placeholder: 'Name'

View file

@ -1,15 +1,26 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
ILoadOptionsFunctions,
INodeTypeDescription,
INodeType,
INodeExecutionData,
IDataObject,
INodePropertyOptions,
} from 'n8n-workflow';
import { documentOperations, documentFields } from './DocumentDescription';
import { erpNextApiRequest, erpNextApiRequestAllItems } from './GenericFunctions';
import {
documentOperations,
documentFields
} from './DocumentDescription';
import {
erpNextApiRequest,
erpNextApiRequestAllItems
} from './GenericFunctions';
import { filter } from 'rhea';
export class ERPNext implements INodeType {
description: INodeTypeDescription = {
@ -74,13 +85,65 @@ export class ERPNext implements INodeType {
default: 'document',
description: 'Resource to consume.',
},
// DOCUMENT
...documentOperations,
...documentFields
],
};
methods = {
loadOptions: {
// Get all the doc types to display them to user so that he can
// select them easily
async getDocTypes(
this: ILoadOptionsFunctions
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const types = await erpNextApiRequestAllItems.call(
this,
'data',
'GET',
'/api/resource/DocType',
{},
);
for (const type of types) {
const typeName = type.name;
const typeId = type.name;
returnData.push({
name: typeName,
value: typeId
});
}
return returnData;
},
// Get all the doc fields to display them to user so that he can
// select them easily
async getDocFields(
this: ILoadOptionsFunctions
): Promise<INodePropertyOptions[]> {
const docId = this.getCurrentNodeParameter('docType') as string;
const returnData: INodePropertyOptions[] = [];
const { data } = await erpNextApiRequest.call(
this,
'GET',
`/api/resource/DocType/${docId}`,
{},
);
for (const field of data.fields) {
const fieldName = field.label;
const fieldId = field.fieldname;
returnData.push({
name: fieldName,
value: fieldId
});
}
return returnData;
},
}
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
@ -91,6 +154,8 @@ export class ERPNext implements INodeType {
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
//https://app.swaggerhub.com/apis-docs/alyf.de/ERPNext/11#/Resources/post_api_resource_Webhook
//https://frappeframework.com/docs/user/en/guides/integration/rest_api/manipulating_documents
if (resource === 'document') {
if (operation === 'get') {
const docType = this.getNodeParameter('docType', i) as string;
@ -99,6 +164,8 @@ export class ERPNext implements INodeType {
const endpoint = `/api/resource/${docType}/${documentName}`;
responseData = await erpNextApiRequest.call(this, 'GET', endpoint, {});
responseData = responseData.data;
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
@ -106,79 +173,49 @@ export class ERPNext implements INodeType {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
let endpoint = `/api/resource/${docType}`;
const endpoint = `/api/resource/${docType}`;
// Add field options for query. FORMAT: fields=["test", "example", "hi"]
if (additionalFields.fields as string) {
let newString : string = '';
let fields = (additionalFields.fields as string).split(',');
console.log(fields.length);
fields.map((field, idx) => {
newString = newString + `"${field}",`
});
// Remove excessive comma at end
newString = newString.substring(0, newString.length - 1);
endpoint = `${endpoint}/?fields=[${newString}]`;
if (additionalFields.fields) {
qs.fields = JSON.stringify(additionalFields.fields as string[]);
}
// Add filter options for query. FORMAT: filters=[["Person","first_name","=","Jane"]]
if (additionalFields.filters) {
let newString : string = '';
const filters = (additionalFields.filters as IDataObject).customProperty as IDataObject[];
filters.map(filter => {
let operator : string = '';
// Operators cannot be used as options in Document description, so must use words and then convert here
switch(filter.operator) {
case 'is':
operator = '=';
break;
case 'isNot':
operator = '!=';
break;
case 'greater':
operator = '>';
break;
case 'less':
operator = '<';
break;
case 'equalsGreater':
operator = '>=';
break;
case 'equalsLess':
operator = '<=';
break;
}
newString = newString + `["${filter.docType}","${filter.field}","${operator}","${filter.value}"],`
});
// Remove excessive comma at end
newString = newString.substring(0, newString.length - 1);
const operators: { [key: string]: string } = {
'is': '=',
'isNot': '!=',
'greater': '>',
'less': '<',
'equalsGreater': '>=',
'equalsLess': '<=',
};
// Ensure correct URL based on which queries active
if (additionalFields.fields) {
endpoint = `${endpoint}&filters=[${newString}]`;
} else {
endpoint = `${endpoint}/?filters=[${newString}]`;
const filterValues = (additionalFields.filters as IDataObject).customProperty as IDataObject[];
const filters: string[][] = [];
for (const filter of filterValues) {
const data = [
docType,
filter.field as string,
operators[filter.operator as string],
filter.value as string,
];
filters.push(data);
}
qs.filters = filters;
}
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
if (additionalFields.fields || additionalFields.filters) {
endpoint = `${endpoint}&limit_page_length=${limit}`
} else {
endpoint = `${endpoint}/?limit_page_length=${limit}`
}
responseData = await erpNextApiRequest.call(this, 'GET', endpoint, {});
qs.limit_page_lengt = limit;
qs.limit_start = 1;
responseData = await erpNextApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.data;
} else {
if (additionalFields.fields || additionalFields.filters) {
endpoint = `${endpoint}&limit_start=`
} else {
endpoint = `${endpoint}/?limit_start=`
}
responseData = await erpNextApiRequestAllItems.call(this, 'GET', endpoint, {});
responseData = await erpNextApiRequestAllItems.call(this, 'data', 'GET', endpoint, {}, qs);
}
}
if (operation === 'create') {
@ -186,17 +223,16 @@ export class ERPNext implements INodeType {
const endpoint = `/api/resource/${docType}`;
const properties = this.getNodeParameter('properties', i) as IDataObject;
if (properties) {
const fieldsValues = (properties as IDataObject).customProperty as IDataObject[];
fieldsValues.map(item => {
//@ts-ignore
body[item.field] = item.value;
});
for (const fieldValue of fieldsValues) {
body[fieldValue.field as string] = fieldValue.value;
}
}
responseData = await erpNextApiRequest.call(this, 'POST', endpoint, body);
responseData = responseData.data;
}
if (operation === 'delete') {
const docType = this.getNodeParameter('docType', i) as string;
@ -212,22 +248,19 @@ export class ERPNext implements INodeType {
const endpoint = `/api/resource/${docType}/${documentName}`;
const properties = this.getNodeParameter('properties', i) as IDataObject;
if (properties) {
const fieldsValues = (properties as IDataObject).customProperty as IDataObject[];
fieldsValues.map(item => {
//@ts-ignore
body[item.field] = item.value;
});
for (const fieldValue of fieldsValues) {
body[fieldValue.field as string] = fieldValue.value;
}
}
responseData = await erpNextApiRequest.call(this, 'PUT', endpoint, body);
responseData = responseData.data;
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {

View file

@ -1,4 +1,6 @@
import { OptionsWithUri } from 'request';
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
@ -12,16 +14,6 @@ import {
} from 'n8n-workflow';
export async function erpNextApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
let options: OptionsWithUri = {
headers: {'Accept': 'application/json'},
method,
body,
qs: query,
uri: uri || ``,
json: true
};
options = Object.assign({}, options, option);
const credentials = this.getCredentials('erpNextApi');
@ -29,31 +21,58 @@ export async function erpNextApiRequest(this: IExecuteFunctions | IWebhookFuncti
throw new Error('No credentials got returned!');
}
options.headers!['Authorization'] = `token ${credentials.apiKey}:${credentials.apiSecret}`;
options.uri = `https://${credentials.subdomain}.erpnext.com${resource}`;
let options: OptionsWithUri = {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
method,
body,
qs: query,
uri: uri || `https://${credentials.subdomain}.erpnext.com${resource}`,
json: true,
};
return await this.helpers.request!(options);
options = Object.assign({}, options, option);
console.log(options);
options.headers!['Authorization'] = `token ${credentials.apiKey}:${credentials.apiSecret}`;
if (Object.keys(body).length === 0) {
delete options.body;
}
try {
return await this.helpers.request!(options);
} catch (error) {
let errorMessages;
if (error.response && error.response.body && error.response.body._server_messages) {
const errors = JSON.parse(error.response.body._server_messages);
errorMessages = errors.map((e: string) => JSON.parse(e).message);
throw new Error(
`ARPNext error response [${error.statusCode}]: ${errorMessages.join('|')}`
);
}
throw error;
}
}
export async function erpNextApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject): Promise<any> { // tslint:disable-line:no-any
const limit : number = 100;
export async function erpNextApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions , propertyName: string, method: string, resource: string, body: IDataObject, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
let index : number = 0;
query!.limit_start = 1;
query!.limit_page_lengt = 20;
do {
endpoint = `${endpoint}${index}`;
responseData = await erpNextApiRequest.call(this, method, endpoint, body, query);
returnData.push.apply(returnData, responseData.data);
index = index + limit;
responseData = await erpNextApiRequest.call(this, method, resource, body, query);
returnData.push.apply(returnData, responseData[propertyName]);
query!.limit_start += query!.limit_page_lengt - 1;
} while (
responseData.data.length !== 0
);
return {
data: returnData
};
return returnData;
}