mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-10 14:44:05 -08:00
5f76a5dc72
* ⚡ Initial refactor of Zoho node
* ⚡ Refactor out extra credentials parameter
* 🔥 Remove unused filters
* ⚡ Fix date of birth fields
* ⚡ Fix param casing
* ⚡ Adjust param types
* ⚡ Adjust invoice operations
* ⚡ Refactor types in adjusters
* ⚡ Add product resource
* ⚡ Refactor product details field
* ⚡ Adjust purchase order params
* ⚡ Adjust quote params
* ⚡ Adjust sales orders params
* 🔥 Remove old unused files
* ⚡ Add vendor resource
* ⚡ Fix minor details
* ⚡ Implement continueOnFail
* 🐛 Fix empty response for getAll
* ⚡ Simplify response for single item
* 🔥 Remove unused import
* 🔨 Restore old node name
* ⚡ Prevent request on empty update
* ⚡ Apply Dali's suggestions
* ⚡ Improvements
* ⚡ Add filters for lead:getAll
* ⚡ Add upsert to all resources
* ⚡ Add filters to all getAll operations
* 🔨 Restore continue on fail
* 🔨 Refactor upsert addition
* 🔨 Refactor getFields for readability
* ⚡ Add custom fields to all create-update ops
* ⚡ Implement custom fields adjuster
* 🔥 Remove logging
* 👕 Appease linter
* 👕 Refactor type helper for linter
* ⚡ Fix refactored type
* 🔨 Refactor reduce for simplicity
* ⚡ Fix vendor:getAll filter options
* ⚡ Fix custom fields for product operations
* ⚡ Make sort_by into options param
* 🚚 Rename upsert operation
* ✏️ Add descriptions to upsert
* ⚡ Deduplicate system-defined check fields
* 🔨 Re-order address fields
* ✏️ Generalize references in getAll fields
* 🔥 Remove extra comma
* ⚡ Make getFields helper more readable
* ✏️ Touch up description for account ID
* 🔥 Remove currency from contacts
* 🔨 Resort emails and phones for contact
* 🐛 Fix sales cycle duration param type
* ✏️ Clarify descriptions with percentages
* 🔨 Reorder total fields
* ✏️ Clarify percentages for discounts
* ✏️ Clarify percentages for commissions
* 🔨 Convert currency to picklist
* ✏️ Add documentation links
* ⚡ Add resource loaders for picklists
* ⚡ Fix build
* 🔨 Refactor product details
* ⚡ Add resolve data to all resources
* ⚡ Change resolve data toggle default
* ⚡ Restore lead:getFields operation
* 🔥 Remove upsert descriptions
* 🔨 Change casing for upsert operations
* ⚡ Add operation descriptions
* 🔨 Restore makeResolve default value
* 🔨 Return nested details
* ⚡ Reposition Resolve Data toggles
* ✏️ Document breaking changes
* Revert "Reposition Resolve Data toggles"
This reverts commit 72ac41780b
.
* ⚡ Improvements
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
435 lines
11 KiB
TypeScript
435 lines
11 KiB
TypeScript
import {
|
|
OptionsWithUri,
|
|
} from 'request';
|
|
|
|
import {
|
|
IExecuteFunctions,
|
|
IHookFunctions,
|
|
} from 'n8n-core';
|
|
|
|
import {
|
|
IDataObject,
|
|
ILoadOptionsFunctions,
|
|
NodeApiError,
|
|
NodeOperationError,
|
|
} from 'n8n-workflow';
|
|
|
|
import {
|
|
flow,
|
|
sortBy,
|
|
} from 'lodash';
|
|
|
|
import {
|
|
AllFields,
|
|
CamelCaseResource,
|
|
DateType,
|
|
GetAllFilterOptions,
|
|
IdType,
|
|
LoadedFields,
|
|
LoadedLayouts,
|
|
LocationType,
|
|
NameType,
|
|
ProductDetails,
|
|
ResourceItems,
|
|
SnakeCaseResource,
|
|
ZohoOAuth2ApiCredentials,
|
|
} from './types';
|
|
|
|
export async function zohoApiRequest(
|
|
this: IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions,
|
|
method: string,
|
|
endpoint: string,
|
|
body: IDataObject = {},
|
|
qs: IDataObject = {},
|
|
uri?: string,
|
|
) {
|
|
const { oauthTokenData } = this.getCredentials('zohoOAuth2Api') as ZohoOAuth2ApiCredentials;
|
|
|
|
const options: OptionsWithUri = {
|
|
body: {
|
|
data: [
|
|
body,
|
|
],
|
|
},
|
|
method,
|
|
qs,
|
|
uri: uri ?? `${oauthTokenData.api_domain}/crm/v2${endpoint}`,
|
|
json: true,
|
|
};
|
|
|
|
if (!Object.keys(body).length) {
|
|
delete options.body;
|
|
}
|
|
|
|
if (!Object.keys(qs).length) {
|
|
delete options.qs;
|
|
}
|
|
|
|
try {
|
|
const responseData = await this.helpers.requestOAuth2?.call(this, 'zohoOAuth2Api', options);
|
|
|
|
if (responseData === undefined) return [];
|
|
|
|
throwOnErrorStatus.call(this, responseData);
|
|
|
|
return responseData;
|
|
} catch (error) {
|
|
throw new NodeApiError(this.getNode(), error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Make an authenticated API request to Zoho CRM API and return all items.
|
|
*/
|
|
export async function zohoApiRequestAllItems(
|
|
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
|
|
method: string,
|
|
endpoint: string,
|
|
body: IDataObject = {},
|
|
qs: IDataObject = {},
|
|
) {
|
|
const returnData: IDataObject[] = [];
|
|
|
|
let responseData;
|
|
let uri: string | undefined;
|
|
qs.per_page = 200;
|
|
qs.page = 0;
|
|
|
|
do {
|
|
responseData = await zohoApiRequest.call(this, method, endpoint, body, qs, uri);
|
|
if (Array.isArray(responseData) && !responseData.length) return returnData;
|
|
returnData.push(...responseData.data);
|
|
uri = responseData.info.more_records;
|
|
qs.page++;
|
|
} while (
|
|
responseData.info.more_records !== undefined &&
|
|
responseData.info.more_records === true
|
|
);
|
|
|
|
return returnData;
|
|
}
|
|
|
|
/**
|
|
* Handle a Zoho CRM API listing by returning all items or up to a limit.
|
|
*/
|
|
export async function handleListing(
|
|
this: IExecuteFunctions,
|
|
method: string,
|
|
endpoint: string,
|
|
body: IDataObject = {},
|
|
qs: IDataObject = {},
|
|
) {
|
|
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
|
|
|
|
if (returnAll) {
|
|
return await zohoApiRequestAllItems.call(this, method, endpoint, body, qs);
|
|
}
|
|
|
|
const responseData = await zohoApiRequestAllItems.call(this, method, endpoint, body, qs);
|
|
const limit = this.getNodeParameter('limit', 0) as number;
|
|
|
|
return responseData.slice(0, limit);
|
|
}
|
|
|
|
export function throwOnEmptyUpdate(this: IExecuteFunctions, resource: CamelCaseResource) {
|
|
throw new NodeOperationError(
|
|
this.getNode(),
|
|
`Please enter at least one field to update for the ${resource}.`,
|
|
);
|
|
}
|
|
|
|
export function throwOnMissingProducts(
|
|
this: IExecuteFunctions,
|
|
resource: CamelCaseResource,
|
|
productDetails: ProductDetails,
|
|
) {
|
|
if (!productDetails.length) {
|
|
throw new NodeOperationError(
|
|
this.getNode(),
|
|
`Please enter at least one product for the ${resource}.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function throwOnErrorStatus(
|
|
this: IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions,
|
|
responseData: { data?: Array<{ status: string, message: string }> },
|
|
) {
|
|
if (responseData?.data?.[0].status === 'error') {
|
|
throw new NodeOperationError(this.getNode(), responseData as Error);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------
|
|
// required field adjusters
|
|
// ----------------------------------------
|
|
|
|
/**
|
|
* Place a product ID at a nested position in a product details field.
|
|
*/
|
|
export const adjustProductDetails = (productDetails: ProductDetails) => {
|
|
return productDetails.map(p => {
|
|
return {
|
|
...omit('product', p),
|
|
product: { id: p.id },
|
|
quantity: p.quantity || 1,
|
|
};
|
|
});
|
|
};
|
|
|
|
// ----------------------------------------
|
|
// additional field adjusters
|
|
// ----------------------------------------
|
|
|
|
/**
|
|
* Place a product ID at a nested position in a product details field.
|
|
*
|
|
* Only for updating products from Invoice, Purchase Order, Quote, and Sales Order.
|
|
*/
|
|
export const adjustProductDetailsOnUpdate = (allFields: AllFields) => {
|
|
if (!allFields.Product_Details) return allFields;
|
|
|
|
return allFields.Product_Details.map(p => {
|
|
return {
|
|
...omit('product', p),
|
|
product: { id: p.id },
|
|
quantity: p.quantity || 1,
|
|
};
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Place a location field's contents at the top level of the payload.
|
|
*/
|
|
const adjustLocationFields = (locationType: LocationType) => (allFields: AllFields) => {
|
|
const locationField = allFields[locationType];
|
|
|
|
if (!locationField) return allFields;
|
|
|
|
return {
|
|
...omit(locationType, allFields),
|
|
...locationField.address_fields,
|
|
};
|
|
};
|
|
|
|
const adjustAddressFields = adjustLocationFields('Address');
|
|
const adjustBillingAddressFields = adjustLocationFields('Billing_Address');
|
|
const adjustMailingAddressFields = adjustLocationFields('Mailing_Address');
|
|
const adjustShippingAddressFields = adjustLocationFields('Shipping_Address');
|
|
const adjustOtherAddressFields = adjustLocationFields('Other_Address');
|
|
|
|
/**
|
|
* Remove from a date field the timestamp set by the datepicker.
|
|
*/
|
|
const adjustDateField = (dateType: DateType) => (allFields: AllFields) => {
|
|
const dateField = allFields[dateType];
|
|
|
|
if (!dateField) return allFields;
|
|
|
|
allFields[dateType] = dateField.split('T')[0];
|
|
|
|
return allFields;
|
|
};
|
|
|
|
const adjustDateOfBirthField = adjustDateField('Date_of_Birth');
|
|
const adjustClosingDateField = adjustDateField('Closing_Date');
|
|
const adjustInvoiceDateField = adjustDateField('Invoice_Date');
|
|
const adjustDueDateField = adjustDateField('Due_Date');
|
|
const adjustPurchaseOrderDateField = adjustDateField('PO_Date');
|
|
const adjustValidTillField = adjustDateField('Valid_Till');
|
|
|
|
/**
|
|
* Place an ID field's value nested inside the payload.
|
|
*/
|
|
const adjustIdField = (idType: IdType, nameProperty: NameType) => (allFields: AllFields) => {
|
|
const idValue = allFields[idType];
|
|
|
|
if (!idValue) return allFields;
|
|
|
|
return {
|
|
...omit(idType, allFields),
|
|
[nameProperty]: { id: idValue },
|
|
};
|
|
};
|
|
|
|
const adjustAccountIdField = adjustIdField('accountId', 'Account_Name');
|
|
const adjustContactIdField = adjustIdField('contactId', 'Full_Name');
|
|
const adjustDealIdField = adjustIdField('dealId', 'Deal_Name');
|
|
|
|
const adjustCustomFields = (allFields: AllFields) => {
|
|
const { customFields, ...rest } = allFields;
|
|
|
|
if (!customFields?.customFields.length) return allFields;
|
|
|
|
return customFields.customFields.reduce((acc, cur) => {
|
|
acc[cur.fieldId] = cur.value;
|
|
return acc;
|
|
}, rest);
|
|
};
|
|
|
|
// ----------------------------------------
|
|
// payload adjusters
|
|
// ----------------------------------------
|
|
|
|
export const adjustAccountPayload = flow(
|
|
adjustBillingAddressFields,
|
|
adjustShippingAddressFields,
|
|
adjustCustomFields,
|
|
);
|
|
|
|
export const adjustContactPayload = flow(
|
|
adjustMailingAddressFields,
|
|
adjustOtherAddressFields,
|
|
adjustDateOfBirthField,
|
|
adjustCustomFields,
|
|
);
|
|
|
|
export const adjustDealPayload = flow(
|
|
adjustClosingDateField,
|
|
adjustCustomFields,
|
|
);
|
|
|
|
export const adjustInvoicePayload = flow(
|
|
adjustBillingAddressFields,
|
|
adjustShippingAddressFields,
|
|
adjustInvoiceDateField,
|
|
adjustDueDateField,
|
|
adjustAccountIdField,
|
|
adjustCustomFields,
|
|
);
|
|
|
|
export const adjustInvoicePayloadOnUpdate = flow(
|
|
adjustInvoicePayload,
|
|
adjustProductDetailsOnUpdate,
|
|
);
|
|
|
|
export const adjustLeadPayload = flow(
|
|
adjustAddressFields,
|
|
adjustCustomFields,
|
|
);
|
|
|
|
export const adjustPurchaseOrderPayload = flow(
|
|
adjustBillingAddressFields,
|
|
adjustShippingAddressFields,
|
|
adjustDueDateField,
|
|
adjustPurchaseOrderDateField,
|
|
adjustCustomFields,
|
|
);
|
|
|
|
export const adjustQuotePayload = flow(
|
|
adjustBillingAddressFields,
|
|
adjustShippingAddressFields,
|
|
adjustValidTillField,
|
|
adjustCustomFields,
|
|
);
|
|
|
|
export const adjustSalesOrderPayload = flow(
|
|
adjustBillingAddressFields,
|
|
adjustShippingAddressFields,
|
|
adjustDueDateField,
|
|
adjustAccountIdField,
|
|
adjustContactIdField,
|
|
adjustDealIdField,
|
|
adjustCustomFields,
|
|
);
|
|
|
|
export const adjustVendorPayload = flow(
|
|
adjustAddressFields,
|
|
adjustCustomFields,
|
|
);
|
|
|
|
export const adjustProductPayload = adjustCustomFields;
|
|
|
|
// ----------------------------------------
|
|
// helpers
|
|
// ----------------------------------------
|
|
|
|
/**
|
|
* Create a copy of an object without a specific property.
|
|
*/
|
|
const omit = (propertyToOmit: string, { [propertyToOmit]: _, ...remainingObject }) => remainingObject;
|
|
|
|
/**
|
|
* Convert items in a Zoho CRM API response into n8n load options.
|
|
*/
|
|
export const toLoadOptions = (items: ResourceItems, nameProperty: NameType) =>
|
|
items.map((item) => ({ name: item[nameProperty], value: item.id }));
|
|
|
|
/**
|
|
* Retrieve all fields for a resource, sorted alphabetically.
|
|
*/
|
|
export async function getFields(
|
|
this: ILoadOptionsFunctions,
|
|
resource: SnakeCaseResource,
|
|
{ onlyCustom } = { onlyCustom: false },
|
|
) {
|
|
const qs = { module: getModuleName(resource) };
|
|
|
|
let { fields } = await zohoApiRequest.call(this, 'GET', '/settings/fields', {}, qs) as LoadedFields;
|
|
|
|
if (onlyCustom) {
|
|
fields = fields.filter(({ custom_field }) => custom_field);
|
|
}
|
|
|
|
const options = fields.map(({ field_label, api_name }) => ({ name: field_label, value: api_name }));
|
|
|
|
return sortBy(options, o => o.name);
|
|
}
|
|
|
|
export function getModuleName(resource: string) {
|
|
const map: { [key: string]: string } = {
|
|
account: 'Accounts',
|
|
contact: 'Contacts',
|
|
deal: 'Deals',
|
|
invoice: 'Invoices',
|
|
lead: 'Leads',
|
|
product: 'Products',
|
|
purchaseOrder: 'Purchase_Orders',
|
|
salesOrder: 'Sales_Orders',
|
|
vendor: 'Vendors',
|
|
quote: 'Quotes',
|
|
};
|
|
|
|
return map[resource];
|
|
}
|
|
|
|
export async function getPicklistOptions(
|
|
this: ILoadOptionsFunctions,
|
|
resource: string,
|
|
targetField: string,
|
|
) {
|
|
const qs = { module: getModuleName(resource) };
|
|
const responseData = await zohoApiRequest.call(this, 'GET', '/settings/layouts', {}, qs) as LoadedLayouts;
|
|
|
|
const pickListOptions = responseData.layouts[0]
|
|
.sections.find(section => section.api_name === getSectionApiName(resource))
|
|
?.fields.find(f => f.api_name === targetField)
|
|
?.pick_list_values;
|
|
|
|
if (!pickListOptions) return [];
|
|
|
|
return pickListOptions.map(
|
|
(option) => ({ name: option.display_value, value: option.actual_value }),
|
|
);
|
|
}
|
|
|
|
|
|
function getSectionApiName(resource: string) {
|
|
if (resource === 'purchaseOrder') return 'Purchase Order Information';
|
|
if (resource === 'salesOrder') return 'Sales Order Information';
|
|
|
|
return `${capitalizeInitial(resource)} Information`;
|
|
}
|
|
|
|
/**
|
|
* Add filter options to a query string object.
|
|
*/
|
|
export const addGetAllFilterOptions = (qs: IDataObject, options: GetAllFilterOptions) => {
|
|
if (Object.keys(options).length) {
|
|
const { fields, ...rest } = options;
|
|
Object.assign(qs, fields && { fields: fields.join(',') }, rest);
|
|
}
|
|
};
|
|
|
|
export const capitalizeInitial = (str: string) => str[0].toUpperCase() + str.slice(1);
|