import type { JsonObject, IDataObject, IExecuteFunctions, IHookFunctions, ILoadOptionsFunctions, INodePropertyOptions, } from 'n8n-workflow'; import { NodeApiError, NodeOperationError } from 'n8n-workflow'; import type { OptionsWithUri } from 'request'; export interface ICustomInterface { name: string; key: string; field_type: string; options?: Array<{ id: number; label: string; }>; } export interface ICustomProperties { [key: string]: ICustomInterface; } /** * Make an API request to Pipedrive * */ export async function pipedriveApiRequest( this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: IDataObject, query: IDataObject = {}, formData?: IDataObject, downloadFile?: boolean, ): Promise { const authenticationMethod = this.getNodeParameter('authentication', 0); const options: OptionsWithUri = { headers: { Accept: 'application/json', }, method, qs: query, uri: `https://api.pipedrive.com/v1${endpoint}`, }; if (downloadFile === true) { options.encoding = null; } else { options.json = true; } if (Object.keys(body).length !== 0) { options.body = body; } if (formData !== undefined && Object.keys(formData).length !== 0) { options.formData = formData; } if (query === undefined) { query = {}; } try { const credentialType = authenticationMethod === 'apiToken' ? 'pipedriveApi' : 'pipedriveOAuth2Api'; const responseData = await this.helpers.requestWithAuthentication.call( this, credentialType, options, ); if (downloadFile === true) { return { data: responseData, }; } if (responseData.success === false) { throw new NodeApiError(this.getNode(), responseData as JsonObject); } return { additionalData: responseData.additional_data, data: responseData.data === null ? [] : responseData.data, }; } catch (error) { throw new NodeApiError(this.getNode(), error as JsonObject); } } /** * Make an API request to paginated Pipedrive endpoint * and return all results * * @param {(IHookFunctions | IExecuteFunctions)} this */ export async function pipedriveApiRequestAllItems( this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject, ): Promise { if (query === undefined) { query = {}; } query.limit = 100; query.start = 0; const returnData: IDataObject[] = []; let responseData; do { responseData = await pipedriveApiRequest.call(this, method, endpoint, body, query); // the search path returns data diferently if (responseData.data.items) { returnData.push.apply(returnData, responseData.data.items as IDataObject[]); } else { returnData.push.apply(returnData, responseData.data as IDataObject[]); } query.start = responseData.additionalData.pagination.next_start; } while (responseData.additionalData?.pagination?.more_items_in_collection === true); return { data: returnData, }; } /** * Gets the custom properties from Pipedrive * * @param {(IHookFunctions | IExecuteFunctions)} this */ export async function pipedriveGetCustomProperties( this: IHookFunctions | IExecuteFunctions, resource: string, ): Promise { const endpoints: { [key: string]: string } = { activity: '/activityFields', deal: '/dealFields', organization: '/organizationFields', person: '/personFields', product: '/productFields', }; if (endpoints[resource] === undefined) { throw new NodeOperationError( this.getNode(), `The resource "${resource}" is not supported for resolving custom values!`, ); } const requestMethod = 'GET'; const body = {}; const qs = {}; // Get the custom properties and their values const responseData = await pipedriveApiRequest.call( this, requestMethod, endpoints[resource], body, qs, ); const customProperties: ICustomProperties = {}; for (const customPropertyData of responseData.data) { customProperties[customPropertyData.key] = customPropertyData; } return customProperties; } /** * Converts names and values of custom properties from their actual values to the * Pipedrive internal ones * */ export function pipedriveEncodeCustomProperties( customProperties: ICustomProperties, item: IDataObject, ): void { let customPropertyData; for (const key of Object.keys(item)) { customPropertyData = Object.values(customProperties).find( (propertyData) => propertyData.name === key, ); if (customPropertyData !== undefined) { // Is a custom property // Check if also the value has to be resolved or just the key if ( item[key] !== null && item[key] !== undefined && customPropertyData.options !== undefined && Array.isArray(customPropertyData.options) ) { // Has an option key so get the actual option-value const propertyOption = customPropertyData.options.find( (option) => option.label.toString() === item[key]!.toString(), ); if (propertyOption !== undefined) { item[customPropertyData.key] = propertyOption.id; delete item[key]; } } else { // Does already represent the actual value or is null item[customPropertyData.key] = item[key]; delete item[key]; } } } } /** * Converts names and values of custom properties to their actual values * */ export function pipedriveResolveCustomProperties( customProperties: ICustomProperties, item: IDataObject, ): void { let customPropertyData; const json = item.json as IDataObject; // Iterate over all keys and replace the custom ones for (const key of Object.keys(json)) { if (customProperties[key] !== undefined) { // Is a custom property customPropertyData = customProperties[key]; // value is not set, so nothing to resolve if (json[key] === null) { json[customPropertyData.name] = json[key]; delete json[key]; continue; } if ( [ 'date', 'address', 'double', 'monetary', 'org', 'people', 'phone', 'text', 'time', 'user', 'varchar', 'varchar_auto', 'int', 'time', 'timerange', ].includes(customPropertyData.field_type) ) { json[customPropertyData.name] = json[key]; delete json[key]; // type options } else if ( ['enum', 'visible_to'].includes(customPropertyData.field_type) && customPropertyData.options ) { const propertyOption = customPropertyData.options.find( (option) => option.id.toString() === json[key]!.toString(), ); if (propertyOption !== undefined) { json[customPropertyData.name] = propertyOption.label; delete json[key]; } // type multioptions } else if (['set'].includes(customPropertyData.field_type) && customPropertyData.options) { const selectedIds = (json[key] as string).split(','); const selectedLabels = customPropertyData.options .filter((option) => selectedIds.includes(option.id.toString())) .map((option) => option.label); json[customPropertyData.name] = selectedLabels; delete json[key]; } } } item.json = json; } export function sortOptionParameters( optionParameters: INodePropertyOptions[], ): INodePropertyOptions[] { optionParameters.sort((a, b) => { const aName = a.name.toLowerCase(); const bName = b.name.toLowerCase(); if (aName < bName) { return -1; } if (aName > bName) { return 1; } return 0; }); return optionParameters; }