diff --git a/packages/cli/package.json b/packages/cli/package.json index 2c0d0e4ab8..833123d8a7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.131.0", + "version": "0.132.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -110,7 +110,7 @@ "mysql2": "~2.2.0", "n8n-core": "~0.78.0", "n8n-editor-ui": "~0.100.0", - "n8n-nodes-base": "~0.128.0", + "n8n-nodes-base": "~0.129.0", "n8n-workflow": "~0.64.0", "oauth-1.0a": "^2.2.6", "open": "^7.0.0", diff --git a/packages/nodes-base/credentials/FreshworksCrmApi.credentials.ts b/packages/nodes-base/credentials/FreshworksCrmApi.credentials.ts new file mode 100644 index 0000000000..115fb998f4 --- /dev/null +++ b/packages/nodes-base/credentials/FreshworksCrmApi.credentials.ts @@ -0,0 +1,27 @@ +import { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class FreshworksCrmApi implements ICredentialType { + name = 'freshworksCrmApi'; + displayName = 'Freshworks CRM API'; + documentationUrl = 'freshdesk'; + properties: INodeProperties[] = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string', + default: '', + placeholder: 'BDsTn15vHezBlt_XGp3Tig', + }, + { + displayName: 'Domain', + name: 'domain', + type: 'string', + default: '', + placeholder: 'n8n-org', + description: 'Domain in the Freshworks CRM org URL. For example, in https://n8n-org.myfreshworks.com, the domain is n8n-org.', + }, + ]; +} diff --git a/packages/nodes-base/credentials/GooglePerspectiveOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GooglePerspectiveOAuth2Api.credentials.ts new file mode 100644 index 0000000000..b3cd378767 --- /dev/null +++ b/packages/nodes-base/credentials/GooglePerspectiveOAuth2Api.credentials.ts @@ -0,0 +1,25 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +const scopes = [ + 'https://www.googleapis.com/auth/userinfo.email', +]; + +export class GooglePerspectiveOAuth2Api implements ICredentialType { + name = 'googlePerspectiveOAuth2Api'; + extends = [ + 'googleOAuth2Api', + ]; + displayName = 'Google Perspective OAuth2 API'; + documentationUrl = 'google'; + properties = [ + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: scopes.join(' '), + }, + ]; +} diff --git a/packages/nodes-base/credentials/MarketstackApi.credentials.ts b/packages/nodes-base/credentials/MarketstackApi.credentials.ts new file mode 100644 index 0000000000..5c5a33c832 --- /dev/null +++ b/packages/nodes-base/credentials/MarketstackApi.credentials.ts @@ -0,0 +1,25 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class MarketstackApi implements ICredentialType { + name = 'marketstackApi'; + displayName = 'Marketstack API'; + documentationUrl = 'marketstack'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Use HTTPS', + name: 'useHttps', + type: 'boolean' as NodePropertyTypes, + default: false, + description: 'Use HTTPS (paid plans only).', + }, + ]; +} diff --git a/packages/nodes-base/credentials/NocoDb.credentials.ts b/packages/nodes-base/credentials/NocoDb.credentials.ts new file mode 100644 index 0000000000..f001db77b9 --- /dev/null +++ b/packages/nodes-base/credentials/NocoDb.credentials.ts @@ -0,0 +1,26 @@ +import { + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + + +export class NocoDb implements ICredentialType { + name = 'nocoDb'; + displayName = 'NocoDB'; + documentationUrl = 'nocoDb'; + properties: INodeProperties[] = [ + { + displayName: 'API Token', + name: 'apiToken', + type: 'string', + default: '', + }, + { + displayName: 'Host', + name: 'host', + type: 'string', + default: '', + placeholder: 'http(s)://localhost:8080', + }, + ]; +} diff --git a/packages/nodes-base/nodes/ActionNetwork/ActionNetwork.node.json b/packages/nodes-base/nodes/ActionNetwork/ActionNetwork.node.json new file mode 100644 index 0000000000..f48280b500 --- /dev/null +++ b/packages/nodes-base/nodes/ActionNetwork/ActionNetwork.node.json @@ -0,0 +1,21 @@ +{ + "node": "n8n-nodes-base.actionNetwork", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Sales", + "Marketing & Content" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/actionNetwork" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.actionNetwork/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Aws/DynamoDB/AwsDynamoDB.node.json b/packages/nodes-base/nodes/Aws/DynamoDB/AwsDynamoDB.node.json new file mode 100644 index 0000000000..8c86a9e881 --- /dev/null +++ b/packages/nodes-base/nodes/Aws/DynamoDB/AwsDynamoDB.node.json @@ -0,0 +1,21 @@ +{ + "node": "n8n-nodes-base.awsDynamoDb", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Data & Storage", + "Development" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/aws" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.awsDynamoDb/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Cisco/Webex/CiscoWebex.node.json b/packages/nodes-base/nodes/Cisco/Webex/CiscoWebex.node.json new file mode 100644 index 0000000000..f357d84766 --- /dev/null +++ b/packages/nodes-base/nodes/Cisco/Webex/CiscoWebex.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.ciscoWebex", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Communication" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/ciscoWebex" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.ciscoWebex/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Cisco/Webex/CiscoWebexTrigger.node.json b/packages/nodes-base/nodes/Cisco/Webex/CiscoWebexTrigger.node.json new file mode 100644 index 0000000000..da0c127f6a --- /dev/null +++ b/packages/nodes-base/nodes/Cisco/Webex/CiscoWebexTrigger.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.ciscoWebexTrigger", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Communication" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/ciscoWebex" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.ciscoWebexTrigger/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Elasticsearch/Elasticsearch.node.json b/packages/nodes-base/nodes/Elasticsearch/Elasticsearch.node.json new file mode 100644 index 0000000000..8fbac02f06 --- /dev/null +++ b/packages/nodes-base/nodes/Elasticsearch/Elasticsearch.node.json @@ -0,0 +1,21 @@ +{ + "node": "n8n-nodes-base.elasticsearch", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Development", + "Data & Storage" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/elasticsearch" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.elasticsearch/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts b/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts index 738134a81e..7e0977ba54 100644 --- a/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts +++ b/packages/nodes-base/nodes/Facebook/FacebookTrigger.node.ts @@ -5,6 +5,8 @@ import { import { IDataObject, + ILoadOptionsFunctions, + INodePropertyOptions, INodeType, INodeTypeDescription, IWebhookResponseData, @@ -18,7 +20,7 @@ import { } from 'change-case'; import { - facebookApiRequest, + facebookApiRequest, getAllFields, getFields, } from './GenericFunctions'; import { @@ -61,6 +63,14 @@ export class FacebookTrigger implements INodeType { }, ], properties: [ + { + displayName: 'APP ID', + name: 'appId', + type: 'string', + required: true, + default: '', + description: 'Facebook APP ID', + }, { displayName: 'Object', name: 'object', @@ -126,13 +136,20 @@ export class FacebookTrigger implements INodeType { default: 'user', description: 'The object to subscribe to', }, + //https://developers.facebook.com/docs/graph-api/webhooks/reference/page { - displayName: 'App ID', - name: 'appId', - type: 'string', - required: true, - default: '', - description: 'Facebook APP ID', + displayName: 'Fields', + name: 'fields', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getObjectFields', + loadOptionsDependsOn: [ + 'object', + ], + }, + required: false, + default: [], + description: 'The set of fields in this object that are subscribed to', }, { displayName: 'Options', @@ -153,6 +170,18 @@ export class FacebookTrigger implements INodeType { ], }; + + methods = { + loadOptions: { + // Get all the available organizations to display them to user so that he can + // select them easily + async getObjectFields(this: ILoadOptionsFunctions): Promise { + const object = this.getCurrentNodeParameter('object') as string; + return getFields(object) as INodePropertyOptions[]; + }, + }, + }; + // @ts-ignore (because of request) webhookMethods = { default: { @@ -175,12 +204,14 @@ export class FacebookTrigger implements INodeType { const webhookUrl = this.getNodeWebhookUrl('default') as string; const object = this.getNodeParameter('object') as string; const appId = this.getNodeParameter('appId') as string; + const fields = this.getNodeParameter('fields') as string[]; const options = this.getNodeParameter('options') as IDataObject; const body = { object: snakeCase(object), callback_url: webhookUrl, verify_token: uuid(), + fields: (fields.includes('*')) ? getAllFields(object) : fields, } as IDataObject; if (options.includeValues !== undefined) { diff --git a/packages/nodes-base/nodes/Facebook/GenericFunctions.ts b/packages/nodes-base/nodes/Facebook/GenericFunctions.ts index 609cc14042..c80a12c574 100644 --- a/packages/nodes-base/nodes/Facebook/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Facebook/GenericFunctions.ts @@ -14,6 +14,10 @@ import { IDataObject, NodeApiError, } from 'n8n-workflow'; +import { + capitalCase, +} from 'change-case'; + export async function facebookApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any let credentials; @@ -34,7 +38,7 @@ export async function facebookApiRequest(this: IHookFunctions | IExecuteFunction qs, body, gzip: true, - uri: uri ||`https://graph.facebook.com/v8.0${resource}`, + uri: uri || `https://graph.facebook.com/v8.0${resource}`, json: true, }; @@ -44,3 +48,506 @@ export async function facebookApiRequest(this: IHookFunctions | IExecuteFunction throw new NodeApiError(this.getNode(), error); } } + +export function getFields(object: string) { + const data = { + 'adAccount': [ + { + value: 'in_process_ad_objects', + }, + { + value: 'with_issues_ad_objects', + }, + ], + 'page': [ + { + value: 'affiliation', + description: `Describes changes to a page's Affliation profile field`, + }, + { + value: 'attire', + description: `Describes changes to a page's Attire profile field`, + }, + { + value: 'awards', + description: `Describes changes to a page's Awards profile field`, + }, + { + value: 'bio', + description: `Describes changes to a page's Biography profile field`, + }, + { + value: 'birthday', + description: `Describes changes to a page's Birthday profile field`, + }, + { + value: 'category', + description: `Describes changes to a page's Birthday profile field`, + }, + { + value: 'company_overview', + description: `Describes changes to a page's Company Overview profile field`, + }, + { + value: 'culinary_team', + description: `Describes changes to a page's Culinary Team profile field`, + }, + { + value: 'current_location', + description: `Describes changes to a page's Current Location profile field`, + }, + { + value: 'description', + description: `Describes changes to a page's Story Description profile field`, + }, + { + value: 'email', + description: `Describes changes to a page's Email profile field`, + }, + { + value: 'feed', + description: `Describes nearly all changes to a Page's feed, such as Posts, shares, likes, etc`, + }, + { + value: 'founded', + description: `Describes changes to a page's Founded profile field. This is different from the Start Date field`, + }, + { + value: 'general_info', + description: `Describes changes to a page's General Information profile field`, + }, + { + value: 'general_manager', + description: `Describes changes to a page's General Information profile field`, + }, + { + value: 'hometown', + description: `Describes changes to a page's Homewtown profile field`, + }, + { + value: 'hours', + description: `Describes changes to a page's Hours profile field`, + }, + { + value: 'leadgen', + description: `Describes changes to a page's leadgen settings`, + }, + { + value: 'live_videos', + description: `Describes changes to a page's live video status`, + }, + { + value: 'location', + description: `Describes changes to a page's Location profile field`, + }, + { + value: 'members', + description: `Describes changes to a page's Members profile field`, + }, + { + value: 'mention', + description: `Describes new mentions of a page, including mentions in comments, posts, etc`, + }, + { + value: 'merchant_review', + description: `Describes changes to a page's merchant review settings`, + }, + { + value: 'mission', + description: `Describes changes to a page's Mission profile field`, + }, + { + value: 'name', + description: `Describes changes to a page's Name profile field.`, + }, + { + value: 'page_about_story', + }, + { + value: 'page_change_proposal', + description: `Data for page change proposal.`, + }, + { + value: 'page_upcoming_change', + description: `Webhooks data for page upcoming changes`, + }, + { + value: 'parking', + description: `Describes changes to a page's Parking profile field`, + }, + { + value: 'payment_options', + description: `Describes change to a page's Payment profile field`, + }, + { + value: 'personal_info', + description: `Describes changes to a page's Personal Information profile field.`, + }, + { + value: 'personal_interests', + description: `Describes changes to a page's Personal Interests profile field.`, + }, + { + value: 'phone', + description: `Describes changes to a page's Phone profile field`, + }, + { + value: 'picture', + description: `Describes changes to a page's profile picture`, + }, + { + value: 'price_range', + description: `Describes changes to a page's Price Range profile field`, + }, + { + value: 'product_review', + description: `Describes changes to a page's product review settings`, + }, + { + value: 'products', + description: `Describes changes to a page's Products profile field`, + }, + { + value: 'public_transit', + description: `Describes changes to a page's Public Transit profile field`, + }, + { + value: 'ratings', + description: `Describes changes to a page's ratings, including new ratings or a user's comments or reactions on a rating`, + }, + { + value: 'videos', + description: `Describes changes to the encoding status of a video on a page`, + }, + { + value: 'website', + description: `Describes changes to a page's Website profile field`, + }, + ], + 'application': [ + { + value: 'ad_account', + }, + { + value: 'ads_rules_engine', + }, + { + value: 'async_requests', + }, + { + value: 'async_sessions', + }, + { + value: 'group_install', + }, + { + value: 'oe_reseller_onboarding_request_created', + }, + { + value: 'plugin_comment', + }, + { + value: 'plugin_comment_reply', + }, + { + value: 'plugin_comment_reply', + }, + ], + 'certificateTransparency': [ + { + value: 'certificate', + }, + { + value: 'phishing', + }, + ], + 'instagram': [ + { + value: 'comments', + description: 'Notifies you when an Instagram User comments on a media object that you own', + }, + { + value: 'messaging_handover', + }, + { + value: 'mentions', + description: 'Notifies you when an Instagram User @mentions you in a comment or caption on a media object that you do not own', + }, + { + value: 'messages', + }, + { + value: 'messaging_seen', + }, + { + value: 'standby', + }, + { + value: 'story_insights', + }, + ], + 'permissions': [ + { + value: 'bookmarked', + description: 'Whether the user has added or removed the app bookmark', + }, + { + value: 'connected', + description: 'Whether the user is connected or disconnected from the app', + }, + { + value: 'user_birthday', + }, + { + value: 'user_hometown', + }, + { + value: 'user_location', + }, + { + value: 'user_likes', + }, + { + value: 'user_managed_groups', + }, + { + value: 'user_events', + }, + { + value: 'user_photos', + }, + { + value: 'user_videos', + }, + { + value: 'user_friends', + }, + { + value: 'user_posts', + }, + { + value: 'user_gender', + }, + { + value: 'user_link', + }, + { + value: 'user_age_range', + }, + { + value: 'email', + }, + { + value: 'read_insights', + }, + { + value: 'read_page_mailboxes', + }, + { + value: 'pages_show_list', + }, + { + value: 'pages_manage_cta', + }, + { + value: 'business_management', + }, + { + value: 'pages_messaging', + }, + { + value: 'pages_messaging_phone_number', + }, + { + value: 'pages_messaging_subscriptions', + }, + { + value: 'read_audience_network_insights', + }, + { + value: 'pages_manage_instant_articles', + }, + { + value: 'publish_video', + }, + { + value: 'openid', + }, + { + value: 'catalog_management', + }, + { + value: 'gaming_user_locale', + }, + { + value: 'groups_show_list', + }, + { + value: 'instagram_basic', + }, + { + value: 'instagram_manage_comments', + }, + { + value: 'instagram_manage_insights', + }, + { + value: 'instagram_content_publish', + }, + { + value: 'publish_to_groups', + }, + { + value: 'groups_access_member_info', + }, + { + value: 'leads_retrieval', + }, + { + value: 'whatsapp_business_management', + }, + { + value: 'instagram_manage_messages', + }, + { + value: 'attribution_read', + }, + { + value: 'page_events', + }, + { + value: 'ads_management', + }, + { + value: 'ads_read', + }, + { + value: 'pages_read_engagement', + }, + { + value: 'pages_manage_metadata', + }, + { + value: 'pages_read_user_content', + }, + { + value: 'pages_manage_ads', + }, + { + value: 'pages_manage_posts', + }, + { + value: 'pages_manage_engagement', + }, + { + value: 'public_search', + }, + { + value: 'social_ads', + }, + ], + 'users': [ + { + value: 'about', + }, + { + value: 'birthday', + }, + { + value: 'books', + }, + { + value: 'email', + }, + { + value: 'feed', + }, + { + value: 'first_name', + }, + { + value: 'friends', + }, + { + value: 'gender', + }, + { + value: 'hometown', + }, + { + value: 'last_name', + }, + { + value: 'likes', + }, + { + value: 'live_videos', + }, + { + value: 'location', + }, + { + value: 'music', + }, + { + value: 'name', + }, + { + value: 'photos', + }, + { + value: 'pic_big_https', + }, + { + value: 'pic_https', + }, + { + value: 'pic_small_https', + }, + { + value: 'pic_square_https', + }, + { + value: 'platform', + }, + { + value: 'quotes', + }, + { + value: 'status', + }, + { + value: 'television', + }, + { + value: 'videos', + }, + ], + 'whatsappBusinessAccount': [ + { + value: 'message_template_status_update', + }, + { + value: 'phone_number_name_update', + }, + { + value: 'phone_number_quality_update', + }, + { + value: 'account_review_update', + }, + { + value: 'account_update', + }, + ], + // tslint:disable-next-line: no-any + } as { [key: string]: any }; + + return [{ name: '*', value: '*' }].concat(data[object as string] || []) + .map((fieldObject: IDataObject) => + ({ ...fieldObject, name: (fieldObject.value !== '*') ? capitalCase(fieldObject.value as string) : fieldObject.value })); +} + +export function getAllFields(object: string) { + return getFields(object).filter((field: IDataObject) => field.value !== '*').map((field: IDataObject) => field.value); +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/FreshworksCrm/FreshworksCrm.node.ts b/packages/nodes-base/nodes/FreshworksCrm/FreshworksCrm.node.ts new file mode 100644 index 0000000000..b4b6255c1a --- /dev/null +++ b/packages/nodes-base/nodes/FreshworksCrm/FreshworksCrm.node.ts @@ -0,0 +1,996 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + adjustAccounts, + adjustAttendees, + freshworksCrmApiRequest, + getAllItemsViewId, + handleListing, + loadResource, + throwOnEmptyFilter, + throwOnEmptyUpdate, +} from './GenericFunctions'; + +import { + accountFields, + accountOperations, + appointmentFields, + appointmentOperations, + contactFields, + contactOperations, + dealFields, + dealOperations, + noteFields, + noteOperations, + salesActivityFields, + salesActivityOperations, + taskFields, + taskOperations, +} from './descriptions'; + +import { + FreshworksConfigResponse, + LoadedCurrency, + LoadedUser, + LoadOption, +} from './types'; + +import { + tz, +} from 'moment-timezone'; + +export class FreshworksCrm implements INodeType { + description: INodeTypeDescription = { + displayName: 'Freshworks CRM', + name: 'freshworksCrm', + icon: 'file:freshworksCrm.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume the Freshworks CRM API', + defaults: { + name: 'Freshworks CRM', + color: '#ffa800', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'freshworksCrmApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Account', + value: 'account', + }, + { + name: 'Appointment', + value: 'appointment', + }, + { + name: 'Contact', + value: 'contact', + }, + { + name: 'Deal', + value: 'deal', + }, + { + name: 'Note', + value: 'note', + }, + { + name: 'Sales Activity', + value: 'salesActivity', + }, + { + name: 'Task', + value: 'task', + }, + ], + default: 'account', + }, + ...accountOperations, + ...accountFields, + ...appointmentOperations, + ...appointmentFields, + ...contactOperations, + ...contactFields, + ...dealOperations, + ...dealFields, + ...noteOperations, + ...noteFields, + ...salesActivityOperations, + ...salesActivityFields, + ...taskOperations, + ...taskFields, + ], + }; + + methods = { + loadOptions: { + async getAccounts(this: ILoadOptionsFunctions) { + const viewId = await getAllItemsViewId.call(this, { fromLoadOptions: true }); + const responseData = await handleListing.call(this, 'GET', `/sales_accounts/view/${viewId}`); + + return responseData.map(({ name, id }) => ({ name, value: id })) as LoadOption[]; + }, + + async getAccountViews(this: ILoadOptionsFunctions) { + const responseData = await handleListing.call(this, 'GET', '/sales_accounts/filters'); + return responseData.map(({ name, id }) => ({ name, value: id })) as LoadOption[]; + }, + + async getBusinessTypes(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'business_types'); + }, + + async getCampaigns(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'campaigns'); + }, + + async getContactStatuses(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'contact_statuses'); + }, + + async getContactViews(this: ILoadOptionsFunctions) { + const responseData = await handleListing.call(this, 'GET', '/contacts/filters'); + + return responseData.map(({ name, id }) => ({ name, value: id })) as LoadOption[]; + }, + + async getCurrencies(this: ILoadOptionsFunctions) { + const response = await freshworksCrmApiRequest.call( + this, 'GET', '/selector/currencies', + ) as FreshworksConfigResponse; + + const key = Object.keys(response)[0]; + + return response[key].map(({ currency_code, id }) => ({ name: currency_code, value: id })); + }, + + async getDealPaymentStatuses(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'deal_payment_statuses'); + }, + + async getDealPipelines(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'deal_pipelines'); + }, + + async getDealProducts(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'deal_products'); + }, + + async getDealReasons(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'deal_reasons'); + }, + + async getDealStages(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'deal_stages'); + }, + + async getDealTypes(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'deal_types'); + }, + + async getDealViews(this: ILoadOptionsFunctions) { + const responseData = await handleListing.call(this, 'GET', '/deals/filters'); + + return responseData.map(({ name, id }) => ({ name, value: id })) as LoadOption[]; + }, + + async getIndustryTypes(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'industry_types'); + }, + + async getLifecycleStages(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'lifecycle_stages'); + }, + + async getOutcomes(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'sales_activity_outcomes'); + }, + + async getSalesActivityTypes(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'sales_activity_types'); + }, + + async getTerritories(this: ILoadOptionsFunctions) { + return await loadResource.call(this, 'territories'); + }, + + async getUsers(this: ILoadOptionsFunctions) { // for attendees, owners, and creators + const response = await freshworksCrmApiRequest.call( + this, 'GET', `/selector/owners`, + ) as FreshworksConfigResponse; + + const key = Object.keys(response)[0]; + + return response[key].map( + ({ display_name, id }) => ({ name: display_name, value: id }), + ); + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + const defaultTimezone = this.getTimezone(); + + let responseData; + + for (let i = 0; i < items.length; i++) { + + try { + + if (resource === 'account') { + + // ********************************************************************** + // account + // ********************************************************************** + + // https://developers.freshworks.com/crm/api/#accounts + + if (operation === 'create') { + + // ---------------------------------------- + // account: create + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#create_account + + const body = { + name: this.getNodeParameter('name', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, additionalFields); + } + + responseData = await freshworksCrmApiRequest.call(this, 'POST', '/sales_accounts', body); + responseData = responseData.sales_account; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // account: delete + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#delete_account + + const accountId = this.getNodeParameter('accountId', i); + + const endpoint = `/sales_accounts/${accountId}`; + await freshworksCrmApiRequest.call(this, 'DELETE', endpoint); + responseData = { success: true }; + + } else if (operation === 'get') { + + // ---------------------------------------- + // account: get + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#view_account + + const accountId = this.getNodeParameter('accountId', i); + + const endpoint = `/sales_accounts/${accountId}`; + responseData = await freshworksCrmApiRequest.call(this, 'GET', endpoint); + responseData = responseData.sales_account; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // account: getAll + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#list_all_accounts + + const view = this.getNodeParameter('view', i) as string; + + responseData = await handleListing.call(this, 'GET', `/sales_accounts/view/${view}`); + + } else if (operation === 'update') { + + // ---------------------------------------- + // account: update + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#update_a_account + + const body = {} as IDataObject; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, updateFields); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const accountId = this.getNodeParameter('accountId', i); + + const endpoint = `/sales_accounts/${accountId}`; + responseData = await freshworksCrmApiRequest.call(this, 'PUT', endpoint, body); + responseData = responseData.sales_account; + + } + + } else if (resource === 'appointment') { + + // ********************************************************************** + // appointment + // ********************************************************************** + + // https://developers.freshworks.com/crm/api/#appointments + + if (operation === 'create') { + + // ---------------------------------------- + // appointment: create + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#create_appointment + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject & { + time_zone: string; + is_allday: boolean; + }; + + const startDate = this.getNodeParameter('fromDate', i) as string; + const endDate = this.getNodeParameter('endDate', i) as string; + const attendees = this.getNodeParameter('attendees.attendee', i, []) as [{ type: string, contactId: string, userId: string }]; + + const timezone = additionalFields.time_zone ?? defaultTimezone; + + let allDay = false; + + if (additionalFields.is_allday) { + allDay = additionalFields.is_allday as boolean; + } + + const start = tz(startDate, timezone); + const end = tz(endDate, timezone); + + const body = { + title: this.getNodeParameter('title', i), + from_date: start.format(), + end_date: (allDay) ? start.format() : end.format(), + } as IDataObject; + + Object.assign(body, additionalFields); + + if (attendees.length) { + body['appointment_attendees_attributes'] = adjustAttendees(attendees); + } + responseData = await freshworksCrmApiRequest.call(this, 'POST', '/appointments', body); + responseData = responseData.appointment; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // appointment: delete + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#delete_a_appointment + + const appointmentId = this.getNodeParameter('appointmentId', i); + + const endpoint = `/appointments/${appointmentId}`; + await freshworksCrmApiRequest.call(this, 'DELETE', endpoint); + responseData = { success: true }; + + } else if (operation === 'get') { + + // ---------------------------------------- + // appointment: get + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#view_a_appointment + + const appointmentId = this.getNodeParameter('appointmentId', i); + + const endpoint = `/appointments/${appointmentId}`; + responseData = await freshworksCrmApiRequest.call(this, 'GET', endpoint); + responseData = responseData.appointment; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // appointment: getAll + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#list_all_appointments + + const { filter, include } = this.getNodeParameter('filters', i) as { + filter: string; + include: string[]; + }; + + const qs: IDataObject = {}; + + if (filter) { + qs.filter = filter; + } + + if (include) { + qs.include = include; + } + responseData = await handleListing.call(this, 'GET', '/appointments', {}, qs); + + } else if (operation === 'update') { + + // ---------------------------------------- + // appointment: update + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#update_a_appointment + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject & { + from_date: string; + end_date: string; + time_zone: string; + }; + + const attendees = this.getNodeParameter('updateFields.attendees.attendee', i, []) as [{ type: string, contactId: string, userId: string }]; + + if (!Object.keys(updateFields).length) { + throwOnEmptyUpdate.call(this, resource); + } + + const body = {} as IDataObject; + const { from_date, end_date, ...rest } = updateFields; + + const timezone = rest.time_zone ?? defaultTimezone; + + if (from_date) { + body.from_date = tz(from_date, timezone).format(); + } + + if (end_date) { + body.end_date = tz(end_date, timezone).format(); + } + + Object.assign(body, rest); + + if (attendees.length) { + body['appointment_attendees_attributes'] = adjustAttendees(attendees); + delete body.attendees; + } + + const appointmentId = this.getNodeParameter('appointmentId', i); + + const endpoint = `/appointments/${appointmentId}`; + responseData = await freshworksCrmApiRequest.call(this, 'PUT', endpoint, body); + responseData = responseData.appointment; + + } + + } else if (resource === 'contact') { + + // ********************************************************************** + // contact + // ********************************************************************** + + // https://developers.freshworks.com/crm/api/#contacts + + if (operation === 'create') { + + // ---------------------------------------- + // contact: create + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#create_contact + + const body = { + first_name: this.getNodeParameter('firstName', i), + last_name: this.getNodeParameter('lastName', i), + emails: this.getNodeParameter('emails', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustAccounts(additionalFields)); + } + + responseData = await freshworksCrmApiRequest.call(this, 'POST', '/contacts', body); + responseData = responseData.contact; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // contact: delete + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#delete_a_contact + + const contactId = this.getNodeParameter('contactId', i); + + const endpoint = `/contacts/${contactId}`; + await freshworksCrmApiRequest.call(this, 'DELETE', endpoint); + responseData = { success: true }; + + } else if (operation === 'get') { + + // ---------------------------------------- + // contact: get + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#view_a_contact + + const contactId = this.getNodeParameter('contactId', i); + + const endpoint = `/contacts/${contactId}`; + responseData = await freshworksCrmApiRequest.call(this, 'GET', endpoint); + responseData = responseData.contact; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // contact: getAll + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#list_all_contacts + + const view = this.getNodeParameter('view', i) as string; + + responseData = await handleListing.call(this, 'GET', `/contacts/view/${view}`); + + } else if (operation === 'update') { + + // ---------------------------------------- + // contact: update + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#update_a_contact + + const body = {} as IDataObject; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustAccounts(updateFields)); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const contactId = this.getNodeParameter('contactId', i); + + const endpoint = `/contacts/${contactId}`; + responseData = await freshworksCrmApiRequest.call(this, 'PUT', endpoint, body); + responseData = responseData.contact; + + } + + } else if (resource === 'deal') { + + // ********************************************************************** + // deal + // ********************************************************************** + + // https://developers.freshworks.com/crm/api/#deals + + if (operation === 'create') { + + // ---------------------------------------- + // deal: create + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#create_deal + + const body = { + name: this.getNodeParameter('name', i), + amount: this.getNodeParameter('amount', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustAccounts(additionalFields)); + } + + responseData = await freshworksCrmApiRequest.call(this, 'POST', '/deals', body); + responseData = responseData.deal; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // deal: delete + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#delete_a_deal + + const dealId = this.getNodeParameter('dealId', i); + + await freshworksCrmApiRequest.call(this, 'DELETE', `/deals/${dealId}`); + responseData = { success: true }; + + } else if (operation === 'get') { + + // ---------------------------------------- + // deal: get + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#view_a_deal + + const dealId = this.getNodeParameter('dealId', i); + + responseData = await freshworksCrmApiRequest.call(this, 'GET', `/deals/${dealId}`); + responseData = responseData.deal; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // deal: getAll + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#list_all_deals + + const view = this.getNodeParameter('view', i) as string; + + responseData = await handleListing.call(this, 'GET', `/deals/view/${view}`); + + } else if (operation === 'update') { + + // ---------------------------------------- + // deal: update + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#update_a_deal + + const body = {} as IDataObject; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustAccounts(updateFields)); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const dealId = this.getNodeParameter('dealId', i); + + responseData = await freshworksCrmApiRequest.call(this, 'PUT', `/deals/${dealId}`, body); + responseData = responseData.deal; + + } + + } else if (resource === 'note') { + + // ********************************************************************** + // note + // ********************************************************************** + + // https://developers.freshworks.com/crm/api/#notes + + if (operation === 'create') { + + // ---------------------------------------- + // note: create + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#create_note + + const body = { + description: this.getNodeParameter('description', i), + targetable_id: this.getNodeParameter('targetable_id', i), + targetable_type: this.getNodeParameter('targetableType', i), + } as IDataObject; + + responseData = await freshworksCrmApiRequest.call(this, 'POST', '/notes', body); + responseData = responseData.note; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // note: delete + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#delete_a_note + + const noteId = this.getNodeParameter('noteId', i); + + await freshworksCrmApiRequest.call(this, 'DELETE', `/notes/${noteId}`); + responseData = { success: true }; + + } else if (operation === 'update') { + + // ---------------------------------------- + // note: update + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#update_a_note + + const body = {} as IDataObject; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, updateFields); + } else { + throwOnEmptyUpdate.call(this, resource); + } + + const noteId = this.getNodeParameter('noteId', i); + + responseData = await freshworksCrmApiRequest.call(this, 'PUT', `/notes/${noteId}`, body); + responseData = responseData.note; + + } + + } else if (resource === 'salesActivity') { + + // ********************************************************************** + // salesActivity + // ********************************************************************** + + // https://developers.freshworks.com/crm/api/#sales-activities + + if (operation === 'create') { + + // ---------------------------------------- + // salesActivity: create + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#create_sales_activity + + const startDate = this.getNodeParameter('from_date', i) as string; + const endDate = this.getNodeParameter('end_date', i) as string; + + const body = { + sales_activity_type_id: this.getNodeParameter('sales_activity_type_id', i), + title: this.getNodeParameter('title', i), + owner_id: this.getNodeParameter('ownerId', i), + start_date: tz(startDate, defaultTimezone).format(), + end_date: tz(endDate, defaultTimezone).format(), + targetable_type: this.getNodeParameter('targetableType', i), + targetable_id: this.getNodeParameter('targetable_id', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, additionalFields); + } + + responseData = await freshworksCrmApiRequest.call(this, 'POST', '/sales_activities', { sales_activity: body }); + responseData = responseData.sales_activity; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // salesActivity: delete + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#delete_a_sales_activity + + const salesActivityId = this.getNodeParameter('salesActivityId', i); + + const endpoint = `/sales_activities/${salesActivityId}`; + await freshworksCrmApiRequest.call(this, 'DELETE', endpoint); + responseData = { success: true }; + + } else if (operation === 'get') { + + // ---------------------------------------- + // salesActivity: get + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#view_a_sales_activity + + const salesActivityId = this.getNodeParameter('salesActivityId', i); + + const endpoint = `/sales_activities/${salesActivityId}`; + responseData = await freshworksCrmApiRequest.call(this, 'GET', endpoint); + responseData = responseData.sales_activity; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // salesActivity: getAll + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#list_all_sales_activities + + responseData = await handleListing.call(this, 'GET', '/sales_activities'); + + } else if (operation === 'update') { + + // ---------------------------------------- + // salesActivity: update + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#update_a_sales_activity + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject & { + from_date: string; + end_date: string; + time_zone: string; + }; + + if (!Object.keys(updateFields).length) { + throwOnEmptyUpdate.call(this, resource); + } + + const body = {} as IDataObject; + const { from_date, end_date, ...rest } = updateFields; + + if (from_date) { + body.from_date = tz(from_date, defaultTimezone).format(); + } + + if (end_date) { + body.end_date = tz(end_date, defaultTimezone).format(); + } + + if (Object.keys(rest).length) { + Object.assign(body, rest); + } + + const salesActivityId = this.getNodeParameter('salesActivityId', i); + + const endpoint = `/sales_activities/${salesActivityId}`; + responseData = await freshworksCrmApiRequest.call(this, 'PUT', endpoint, body); + responseData = responseData.sales_activity; + + } + + } else if (resource === 'task') { + + // ********************************************************************** + // task + // ********************************************************************** + + // https://developers.freshworks.com/crm/api/#tasks + + if (operation === 'create') { + + // ---------------------------------------- + // task: create + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#create_task + + const dueDate = this.getNodeParameter('dueDate', i); + + const body = { + title: this.getNodeParameter('title', i), + owner_id: this.getNodeParameter('ownerId', i), + due_date: tz(dueDate, defaultTimezone).format(), + targetable_type: this.getNodeParameter('targetableType', i), + targetable_id: this.getNodeParameter('targetable_id', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, additionalFields); + } + + responseData = await freshworksCrmApiRequest.call(this, 'POST', '/tasks', body); + responseData = responseData.task; + + } else if (operation === 'delete') { + + // ---------------------------------------- + // task: delete + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#delete_a_task + + const taskId = this.getNodeParameter('taskId', i); + + await freshworksCrmApiRequest.call(this, 'DELETE', `/tasks/${taskId}`); + responseData = { success: true }; + + } else if (operation === 'get') { + + // ---------------------------------------- + // task: get + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#view_a_task + + const taskId = this.getNodeParameter('taskId', i); + + responseData = await freshworksCrmApiRequest.call(this, 'GET', `/tasks/${taskId}`); + responseData = responseData.task; + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // task: getAll + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#list_all_tasks + + const { filter, include } = this.getNodeParameter('filters', i) as { + filter: string; + include: string; + }; + + const qs: IDataObject = { + filter: 'open', + }; + + if (filter) { + qs.filter = filter; + } + + if (include) { + qs.include = include; + } + + responseData = await handleListing.call(this, 'GET', '/tasks', {}, qs); + + } else if (operation === 'update') { + + // ---------------------------------------- + // task: update + // ---------------------------------------- + + // https://developers.freshworks.com/crm/api/#update_a_task + + const body = {} as IDataObject; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (!Object.keys(updateFields).length) { + throwOnEmptyUpdate.call(this, resource); + } + + const { dueDate, ...rest } = updateFields; + + if (dueDate) { + body.due_date = tz(dueDate, defaultTimezone).format(); + } + + if (Object.keys(rest).length) { + Object.assign(body, rest); + } + + const taskId = this.getNodeParameter('taskId', i); + + responseData = await freshworksCrmApiRequest.call(this, 'PUT', `/tasks/${taskId}`, body); + responseData = responseData.task; + + } + + } + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ json: { error: error.message } }); + continue; + } + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/FreshworksCrm/GenericFunctions.ts b/packages/nodes-base/nodes/FreshworksCrm/GenericFunctions.ts new file mode 100644 index 0000000000..b7d27d093b --- /dev/null +++ b/packages/nodes-base/nodes/FreshworksCrm/GenericFunctions.ts @@ -0,0 +1,215 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +import { + OptionsWithUri, +} from 'request'; + +import { + FreshworksConfigResponse, + FreshworksCrmApiCredentials, + SalesAccounts, + ViewsResponse, +} from './types'; + +import { + omit, +} from 'lodash'; + +export async function freshworksCrmApiRequest( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const { apiKey, domain } = this.getCredentials('freshworksCrmApi') as FreshworksCrmApiCredentials; + + const options: OptionsWithUri = { + headers: { + Authorization: `Token token=${apiKey}`, + }, + method, + body, + qs, + uri: `https://${domain}.myfreshworks.com/crm/sales/api${endpoint}`, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + if (!Object.keys(qs).length) { + delete options.qs; + } + try { + return await this.helpers.request!(options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + +export async function getAllItemsViewId( + this: IExecuteFunctions | ILoadOptionsFunctions, + { fromLoadOptions } = { fromLoadOptions: false }, +) { + let resource = this.getNodeParameter('resource', 0) as string; + let keyword = 'All'; + + if (resource === 'account' || fromLoadOptions) { + resource = 'sales_account'; // adjust resource to endpoint + } + + if (resource === 'deal') { + keyword = 'My Deals'; // no 'All Deals' available + } + + const response = await freshworksCrmApiRequest.call(this, 'GET', `/${resource}s/filters`) as ViewsResponse; + + const view = response.filters.find(v => v.name.includes(keyword)); + + if (!view) { + throw new NodeOperationError(this.getNode(), 'Failed to get all items view'); + } + + return view.id.toString(); +} + +export async function freshworksCrmApiRequestAllItems( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const returnData: IDataObject[] = []; + let response: any; // tslint:disable-line: no-any + + qs.page = 1; + + do { + response = await freshworksCrmApiRequest.call(this, method, endpoint, body, qs); + const key = Object.keys(response)[0]; + returnData.push(...response[key]); + qs.page++; + } while ( + response.meta.total_pages && qs.page <= response.meta.total_pages + ); + + return returnData; +} + +export async function handleListing( + this: IExecuteFunctions | ILoadOptionsFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const returnAll = this.getNodeParameter('returnAll', 0) as boolean; + + if (returnAll) { + return await freshworksCrmApiRequestAllItems.call(this, method, endpoint, body, qs); + } + + const responseData = await freshworksCrmApiRequestAllItems.call(this, method, endpoint, body, qs); + const limit = this.getNodeParameter('limit', 0) as number; + + if (limit) return responseData.slice(0, limit); + + return responseData; +} + +/** + * Load resources for options, except users. + * + * See: https://developers.freshworks.com/crm/api/#admin_configuration + */ +export async function loadResource( + this: ILoadOptionsFunctions, + resource: string, +) { + const response = await freshworksCrmApiRequest.call( + this, 'GET', `/selector/${resource}`, + ) as FreshworksConfigResponse; + + const key = Object.keys(response)[0]; + return response[key].map(({ name, id }) => ({ name, value: id })); +} + +export function adjustAttendees(attendees: [{ type: string, contactId: string, userId: string }]) { + return attendees.map((attendee) => { + if (attendee.type === 'contact') { + return { + attendee_type: 'Contact', + attendee_id: attendee.contactId.toString(), + }; + } else if (attendee.type === 'user') { + return { + attendee_type: 'FdMultitenant::User', + attendee_id: attendee.userId.toString(), + }; + } + }); +} + + +// /** +// * Adjust attendee data from n8n UI to the format expected by Freshworks CRM API. +// */ +// export function adjustAttendees(additionalFields: IDataObject & Attendees) { +// if (!additionalFields?.appointment_attendees_attributes) return additionalFields; + +// return { +// ...omit(additionalFields, ['appointment_attendees_attributes']), +// appointment_attendees_attributes: additionalFields.appointment_attendees_attributes.map(attendeeId => { +// return { type: 'user', id: attendeeId }; +// }), +// }; +// } + +/** + * Adjust account data from n8n UI to the format expected by Freshworks CRM API. + */ +export function adjustAccounts(additionalFields: IDataObject & SalesAccounts) { + if (!additionalFields?.sales_accounts) return additionalFields; + + const adjusted = additionalFields.sales_accounts.map(accountId => { + return { id: accountId, is_primary: false }; + }); + + adjusted[0].is_primary = true; + + return { + ...omit(additionalFields, ['sales_accounts']), + sales_accounts: adjusted, + }; +} + +export function throwOnEmptyUpdate( + this: IExecuteFunctions, + resource: string, +) { + throw new NodeOperationError( + this.getNode(), + `Please enter at least one field to update for the ${resource}.`, + ); +} + +export function throwOnEmptyFilter( + this: IExecuteFunctions, +) { + throw new NodeOperationError( + this.getNode(), + `Please select at least one filter.`, + ); +} diff --git a/packages/nodes-base/nodes/FreshworksCrm/descriptions/AccountDescription.ts b/packages/nodes-base/nodes/FreshworksCrm/descriptions/AccountDescription.ts new file mode 100644 index 0000000000..645aae75e2 --- /dev/null +++ b/packages/nodes-base/nodes/FreshworksCrm/descriptions/AccountDescription.ts @@ -0,0 +1,507 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const accountOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'account', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an account', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an account', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve an account', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all accounts', + }, + { + name: 'Update', + value: 'update', + description: 'Update an account', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const accountFields = [ + // ---------------------------------------- + // account: create + // ---------------------------------------- + { + displayName: 'Name', + name: 'name', + description: 'Name of the account', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Address', + name: 'address', + type: 'string', + default: '', + description: 'Address of the account', + }, + { + displayName: 'Annual Revenue', + name: 'annual_revenue', + type: 'number', + default: 0, + description: 'Annual revenue of the account', + }, + { + displayName: 'Business Type ID', + name: 'business_type_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getBusinessTypes', + }, + description: 'ID of the business that the account belongs to', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + description: 'City that the account belongs to', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + description: 'Country that the account belongs to', + }, + { + displayName: 'Facebook', + name: 'facebook', + type: 'string', + default: '', + description: 'Facebook username of the account', + }, + { + displayName: 'Industry Type ID', + name: 'industry_type_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getIndustryTypes', + }, + description: 'ID of the industry that the account belongs to', + }, + { + displayName: 'LinkedIn', + name: 'linkedin', + type: 'string', + default: '', + description: 'LinkedIn account of the account', + }, + { + displayName: 'Number of Employees', + name: 'number_of_employees', + type: 'number', + default: 0, + description: 'Number of employees in the account', + }, + { + displayName: 'Owner ID', + name: 'owner_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user to whom the account is assigned', + }, + { + displayName: 'Parent Sales Account ID', + name: 'parent_sales_account_id', + type: 'string', + default: '', + description: 'Parent account ID of the account', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number of the account', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'State that the account belongs to', + }, + { + displayName: 'Territory ID', + name: 'territory_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTerritories', + }, + description: 'ID of the territory that the account belongs to', + }, + { + displayName: 'Twitter', + name: 'twitter', + type: 'string', + default: '', + description: 'Twitter username of the account', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + description: 'Website of the account', + }, + { + displayName: 'Zipcode', + name: 'zipcode', + type: 'string', + default: '', + description: 'Zipcode of the region that the account belongs to', + }, + ], + }, + + // ---------------------------------------- + // account: delete + // ---------------------------------------- + { + displayName: 'Account ID', + name: 'accountId', + description: 'ID of the account to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // account: get + // ---------------------------------------- + { + displayName: 'Account ID', + name: 'accountId', + description: 'ID of the account to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // account: getAll + // ---------------------------------------- + { + displayName: 'View', + name: 'view', + type: 'options', + required: true, + typeOptions: { + loadOptionsMethod: 'getAccountViews', + }, + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getAll', + ], + }, + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'How many results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------------- + // account: update + // ---------------------------------------- + { + displayName: 'Account ID', + name: 'accountId', + description: 'ID of the account to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'account', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Address', + name: 'address', + type: 'string', + default: '', + description: 'Address of the account', + }, + { + displayName: 'Annual Revenue', + name: 'annual_revenue', + type: 'number', + default: 0, + description: 'Annual revenue of the account', + }, + { + displayName: 'Business Type ID', + name: 'business_type_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getBusinessTypes', + }, + description: 'ID of the business that the account belongs to', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + description: 'City that the account belongs to', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + description: 'Country that the account belongs to', + }, + { + displayName: 'Facebook', + name: 'facebook', + type: 'string', + default: '', + description: 'Facebook username of the account', + }, + { + displayName: 'Industry Type ID', + name: 'industry_type_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getIndustryTypes', + }, + description: 'ID of the industry that the account belongs to', + }, + { + displayName: 'LinkedIn', + name: 'linkedin', + type: 'string', + default: '', + description: 'LinkedIn account of the account', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the account', + }, + { + displayName: 'Number of Employees', + name: 'number_of_employees', + type: 'number', + default: 0, + description: 'Number of employees in the account', + }, + { + displayName: 'Owner ID', + name: 'owner_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user to whom the account is assigned', + }, + { + displayName: 'Parent Sales Account ID', + name: 'parent_sales_account_id', + type: 'string', + default: '', + description: 'Parent account ID of the account', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + description: 'Phone number of the account', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'State that the account belongs to', + }, + { + displayName: 'Territory ID', + name: 'territory_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTerritories', + }, + description: 'ID of the territory that the account belongs to', + }, + { + displayName: 'Twitter', + name: 'twitter', + type: 'string', + default: '', + description: 'Twitter username of the account', + }, + { + displayName: 'Website', + name: 'website', + type: 'string', + default: '', + description: 'Website of the account', + }, + { + displayName: 'Zipcode', + name: 'zipcode', + type: 'string', + default: '', + description: 'Zipcode of the region that the account belongs to', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/FreshworksCrm/descriptions/AppointmentDescription.ts b/packages/nodes-base/nodes/FreshworksCrm/descriptions/AppointmentDescription.ts new file mode 100644 index 0000000000..172d4c993f --- /dev/null +++ b/packages/nodes-base/nodes/FreshworksCrm/descriptions/AppointmentDescription.ts @@ -0,0 +1,636 @@ +import { + tz, +} from 'moment-timezone'; + +import { + INodeProperties, +} from 'n8n-workflow'; + +export const appointmentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'appointment', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an appointment', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an appointment', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve an appointment', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all appointments', + }, + { + name: 'Update', + value: 'update', + description: 'Update an appointment', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const appointmentFields = [ + // ---------------------------------------- + // appointment: create + // ---------------------------------------- + { + displayName: 'Title', + name: 'title', + description: 'Title of the appointment', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'appointment', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Start Date', + name: 'fromDate', + description: 'Timestamp that denotes the start of appointment. Start date if this is an all-day appointment.', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'appointment', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'End Date', + name: 'endDate', + description: 'Timestamp that denotes the end of appointment. End date if this is an all-day appointment.', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'appointment', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Attendees', + name: 'attendees', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'appointment', + ], + operation: [ + 'create', + ], + }, + }, + placeholder: 'Add Attendee', + default: {}, + options: [ + { + name: 'attendee', + displayName: 'Attendee', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Contact', + value: 'contact', + }, + { + name: 'User', + value: 'user', + }, + ], + default: 'contact', + }, + { + displayName: 'User ID', + name: 'userId', + type: 'options', + displayOptions: { + show: { + type: [ + 'user', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + }, + { + displayName: 'Contact ID', + name: 'contactId', + displayOptions: { + show: { + type: [ + 'contact', + ], + }, + }, + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'appointment', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Creator ID', + name: 'creater_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user who created the appointment', + }, + { + displayName: 'Is All-Day', + name: 'is_allday', + type: 'boolean', + default: false, + description: 'Whether it is an all-day appointment or not', + }, + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + default: '', + description: 'Latitude of the location when you check in for an appointment', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Location of the appointment', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + default: '', + description: 'Longitude of the location when you check in for an appointment', + }, + { + displayName: 'Outcome ID', + name: 'outcome_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getOutcomes', + }, + description: 'ID of outcome of Appointment sales activity type', + }, + { + displayName: 'Target ID', + name: 'targetable_id', + type: 'string', + default: '', + description: 'ID of contact/account against whom appointment is created', + }, + { + displayName: 'Target Type', + name: 'targetable_type', + type: 'options', + default: 'Contact', + options: [ + { + name: 'Contact', + value: 'Contact', + }, + { + name: 'Deal', + value: 'Deal', + }, + { + name: 'SalesAccount', + value: 'SalesAccount', + }, + ], + }, + { + displayName: 'Time Zone', + name: 'time_zone', + type: 'options', + default: '', + description: 'Timezone that the appointment is scheduled in', + options: tz.names().map(tz => ({ name: tz, value: tz })), + }, + ], + }, + + // ---------------------------------------- + // appointment: delete + // ---------------------------------------- + { + displayName: 'Appointment ID', + name: 'appointmentId', + description: 'ID of the appointment to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'appointment', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // appointment: get + // ---------------------------------------- + { + displayName: 'Appointment ID', + name: 'appointmentId', + description: 'ID of the appointment to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'appointment', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // appointment: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'appointment', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'How many results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'appointment', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + default: '', + placeholder: 'Add Filter', + displayOptions: { + show: { + resource: [ + 'appointment', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Include', + name: 'include', + type: 'options', + default: 'creater', + options: [ + { + name: 'Appointment Attendees', + value: 'appointment_attendees', + }, + { + name: 'Creator', + value: 'creater', + }, + { + name: 'Target', + value: 'targetable', + }, + ], + }, + { + displayName: 'Time', + name: 'filter', + type: 'options', + default: 'upcoming', + options: [ + { + name: 'Past', + value: 'past', + }, + { + name: 'Upcoming', + value: 'upcoming', + }, + ], + }, + ], + }, + + // ---------------------------------------- + // appointment: update + // ---------------------------------------- + { + displayName: 'Appointment ID', + name: 'appointmentId', + description: 'ID of the appointment to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'appointment', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'appointment', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Attendees', + name: 'attendees', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Attendee', + default: {}, + options: [ + { + name: 'attendee', + displayName: 'Attendee', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + options: [ + { + name: 'Contact', + value: 'contact', + }, + { + name: 'User', + value: 'user', + }, + ], + default: 'contact', + }, + { + displayName: 'User ID', + name: 'userId', + type: 'options', + displayOptions: { + show: { + type: [ + 'user', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + }, + { + displayName: 'Contact ID', + name: 'contactId', + displayOptions: { + show: { + type: [ + 'contact', + ], + }, + }, + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Creator ID', + name: 'creater_id', + type: 'options', + default: [], + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user who created the appointment', + }, + { + displayName: 'End Date', + name: 'endDate', + description: 'Timestamp that denotes the end of appointment. End date if this is an all-day appointment.', + type: 'dateTime', + default: '', + }, + { + displayName: 'Is All-Day', + name: 'is_allday', + type: 'boolean', + default: false, + description: 'Whether it is an all-day appointment or not', + }, + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + default: '', + description: 'Latitude of the location when you check in for an appointment', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Location of the appointment', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + default: '', + description: 'Longitude of the location when you check in for an appointment', + }, + { + displayName: 'Outcome ID', + name: 'outcome_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getOutcomes', + }, + description: 'ID of outcome of Appointment sales activity type', + }, + { + displayName: 'Start Date', + name: 'fromDate', + description: 'Timestamp that denotes the start of appointment. Start date if this is an all-day appointment.', + type: 'dateTime', + default: '', + }, + { + displayName: 'Target ID', + name: 'targetable_id', + type: 'string', + default: '', + description: 'ID of contact/account against whom appointment is created', + }, + { + displayName: 'Target Type', + name: 'targetable_type', + type: 'options', + default: 'Contact', + options: [ + { + name: 'Contact', + value: 'Contact', + }, + { + name: 'Deal', + value: 'Deal', + }, + { + name: 'SalesAccount', + value: 'SalesAccount', + }, + ], + }, + { + displayName: 'Time Zone', + name: 'time_zone', + type: 'options', + default: '', + description: 'Timezone that the appointment is scheduled in', + options: tz.names().map(tz => ({ name: tz, value: tz })), + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the appointment', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/FreshworksCrm/descriptions/ContactDescription.ts b/packages/nodes-base/nodes/FreshworksCrm/descriptions/ContactDescription.ts new file mode 100644 index 0000000000..9d53155360 --- /dev/null +++ b/packages/nodes-base/nodes/FreshworksCrm/descriptions/ContactDescription.ts @@ -0,0 +1,668 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a contact', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a contact', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all contacts', + }, + { + name: 'Update', + value: 'update', + description: 'Update a contact', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const contactFields = [ + // ---------------------------------------- + // contact: create + // ---------------------------------------- + { + displayName: 'First Name', + name: 'firstName', + description: 'First name of the contact', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Last Name', + name: 'lastName', + description: 'Last name of the contact', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Email Address', + name: 'emails', + type: 'string', + default: '', + description: 'Email addresses of the contact', + required: true, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Address', + name: 'address', + type: 'string', + default: '', + description: 'Address of the contact', + }, + { + displayName: 'Campaign ID', + name: 'campaign_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: 'ID of the campaign that led your contact to your webapp', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + description: 'City that the contact belongs to', + }, + { + displayName: 'Contact Status ID', + name: 'contact_status_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getContactStatuses', + }, + description: 'ID of the contact status that the contact belongs to', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + description: 'Country that the contact belongs to', + }, + { + displayName: 'External ID', + name: 'external_id', + type: 'string', + default: '', + description: 'External ID of the contact', + }, + { + displayName: 'Facebook', + name: 'facebook', + type: 'string', + default: '', + description: 'Facebook username of the contact', + }, + { + displayName: 'Job Title', + name: 'job_title', + type: 'string', + default: '', + description: 'Designation of the contact in the account they belong to', + }, + { + displayName: 'Keywords', + name: 'keyword', + type: 'string', + default: '', + description: 'Keywords that the contact used to reach your website/web app', + }, + { + displayName: 'Lead Source ID', + name: 'lead_source_id', + type: 'string', // not obtainable from API + default: '', + description: 'ID of the source where contact came from', + }, + { + displayName: 'Lifecycle Stage ID', + name: 'lifecycle_stage_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getLifecycleStages', + }, + description: 'ID of the lifecycle stage that the contact belongs to', + }, + { + displayName: 'LinkedIn', + name: 'linkedin', + type: 'string', + default: '', + description: 'LinkedIn account of the contact', + }, + { + displayName: 'Medium', + name: 'medium', + type: 'string', + default: '', + description: 'Medium that led your contact to your website/webapp', + }, + { + displayName: 'Mobile Number', + name: 'mobile_number', + type: 'string', + default: '', + description: 'Mobile phone number of the contact', + }, + { + displayName: 'Owner ID', + name: 'owner_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user to whom the contact is assigned', + }, + { + displayName: 'Sales Accounts', + name: 'sales_accounts', + type: 'multiOptions', + default: [], + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + description: 'Accounts which contact belongs to', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'State that the contact belongs to', + }, + { + displayName: 'Subscription Status', + name: 'subscription_status', + type: 'string', // not obtainable from API + default: '', + description: 'Status of subscription that the contact is in', + }, + { + displayName: 'Subscription Types', + name: 'subscription_types', + type: 'string', // not obtainable from API + default: '', + description: 'Type of subscription that the contact is in', + }, + { + displayName: 'Territory ID', + name: 'territory_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTerritories', + }, + description: 'ID of the territory that the contact belongs to', + }, + { + displayName: 'Time Zone', + name: 'time_zone', + type: 'string', + default: '', + description: 'Timezone that the contact belongs to', + }, + { + displayName: 'Twitter', + name: 'twitter', + type: 'string', + default: '', + description: 'Twitter username of the contact', + }, + { + displayName: 'Work Number', + name: 'work_number', + type: 'string', + default: '', + description: 'Work phone number of the contact', + }, + { + displayName: 'Zipcode', + name: 'zipcode', + type: 'string', + default: '', + description: 'Zipcode of the region that the contact belongs to', + }, + ], + }, + + // ---------------------------------------- + // contact: delete + // ---------------------------------------- + { + displayName: 'Contact ID', + name: 'contactId', + description: 'ID of the contact to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // contact: get + // ---------------------------------------- + { + displayName: 'Contact ID', + name: 'contactId', + description: 'ID of the contact to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // contact: getAll + // ---------------------------------------- + { + displayName: 'View', + name: 'view', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getContactViews', + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'How many results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------------- + // contact: update + // ---------------------------------------- + { + displayName: 'Contact ID', + name: 'contactId', + description: 'ID of the contact to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Address', + name: 'address', + type: 'string', + default: '', + description: 'Address of the contact', + }, + { + displayName: 'Campaign ID', + name: 'campaign_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: 'ID of the campaign that led your contact to your webapp', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + description: 'City that the contact belongs to', + }, + { + displayName: 'Contact Status ID', + name: 'contact_status_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getContactStatuses', + }, + description: 'ID of the contact status that the contact belongs to', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + description: 'Country that the contact belongs to', + }, + { + displayName: 'External ID', + name: 'external_id', + type: 'string', + default: '', + description: 'External ID of the contact', + }, + { + displayName: 'Facebook', + name: 'facebook', + type: 'string', + default: '', + description: 'Facebook username of the contact', + }, + { + displayName: 'First Name', + name: 'first_name', + type: 'string', + default: '', + description: 'First name of the contact', + }, + { + displayName: 'Job Title', + name: 'job_title', + type: 'string', + default: '', + description: 'Designation of the contact in the account they belong to', + }, + { + displayName: 'Keywords', + name: 'keyword', + type: 'string', + default: '', + description: 'Keywords that the contact used to reach your website/web app', + }, + { + displayName: 'Last Name', + name: 'last_name', + type: 'string', + default: '', + description: 'Last name of the contact', + }, + { + displayName: 'Lead Source ID', + name: 'lead_source_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getLeadSources', + }, + description: 'ID of the source where contact came from', + }, + { + displayName: 'Lifecycle Stage ID', + name: 'lifecycle_stage_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getLifecycleStages', + }, + description: 'ID of the lifecycle stage that the contact belongs to', + }, + { + displayName: 'LinkedIn', + name: 'linkedin', + type: 'string', + default: '', + description: 'LinkedIn account of the contact', + }, + { + displayName: 'Medium', + name: 'medium', + type: 'string', + default: '', + description: 'Medium that led your contact to your website/webapp', + }, + { + displayName: 'Mobile Number', + name: 'mobile_number', + type: 'string', + default: '', + description: 'Mobile phone number of the contact', + }, + { + displayName: 'Owner ID', + name: 'owner_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user to whom the contact is assigned', + }, + { + displayName: 'Sales Accounts', + name: 'sales_accounts', + type: 'multiOptions', + default: [], + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + description: 'Accounts which contact belongs to', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + description: 'State that the contact belongs to', + }, + { + displayName: 'Subscription Status', + name: 'subscription_status', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getSubscriptionStatuses', + }, + description: 'Status of subscription that the contact is in', + }, + { + displayName: 'Subscription Types', + name: 'subscription_types', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getSubscriptionTypes', + }, + description: 'Type of subscription that the contact is in', + }, + { + displayName: 'Territory ID', + name: 'territory_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTerritories', + }, + description: 'ID of the territory that the contact belongs to', + }, + { + displayName: 'Time Zone', + name: 'time_zone', + type: 'string', + default: '', + description: 'Timezone that the contact belongs to', + }, + { + displayName: 'Twitter', + name: 'twitter', + type: 'string', + default: '', + description: 'Twitter username of the contact', + }, + { + displayName: 'Work Number', + name: 'work_number', + type: 'string', + default: '', + description: 'Work phone number of the contact', + }, + { + displayName: 'Zipcode', + name: 'zipcode', + type: 'string', + default: '', + description: 'Zipcode of the region that the contact belongs to', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/FreshworksCrm/descriptions/DealDescription.ts b/packages/nodes-base/nodes/FreshworksCrm/descriptions/DealDescription.ts new file mode 100644 index 0000000000..ad5ca251d6 --- /dev/null +++ b/packages/nodes-base/nodes/FreshworksCrm/descriptions/DealDescription.ts @@ -0,0 +1,545 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const dealOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'deal', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a deal', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a deal', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a deal', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all deals', + }, + { + name: 'Update', + value: 'update', + description: 'Update a deal', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const dealFields = [ + // ---------------------------------------- + // deal: create + // ---------------------------------------- + { + displayName: 'Amount', + name: 'amount', + description: 'Value of the deal', + type: 'number', + required: true, + default: 0, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Name', + name: 'name', + description: 'Name of the deal', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Base Currency Amount', + name: 'base_currency_amount', + type: 'number', + default: 0, + description: 'Value of the deal in base currency', + }, + { + displayName: 'Campaign ID', + name: 'campaign_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: 'ID of the campaign that landed this deal', + }, + { + displayName: 'Currency ID', + name: 'currency_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getCurrencies', + }, + description: 'ID of the currency that the deal belongs to', + }, + { + displayName: 'Deal Payment Status ID', + name: 'deal_payment_status_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDealPaymentStatuses', + }, + description: 'ID of the mode of payment for the deal', + }, + { + displayName: 'Deal Pipeline ID', + name: 'deal_pipeline_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDealPipelines', + }, + description: 'ID of the deal pipeline that it belongs to', + }, + { + displayName: 'Deal Product ID', + name: 'deal_product_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDealProducts', + }, + description: 'ID of the product that the deal belongs to (in a multi-product company)', + }, + { + displayName: 'Deal Reason ID', + name: 'deal_reason_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDealReasons', + }, + description: 'ID of the reason for losing the deal. Can only be set if the deal is in \'Lost\' stage.', + }, + { + displayName: 'Deal Stage ID', + name: 'deal_stage_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDealStages', + }, + description: 'ID of the deal stage that the deal belongs to', + }, + { + displayName: 'Deal Type ID', + name: 'deal_type_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDealTypes', + }, + description: 'ID of the deal type that the deal belongs to', + }, + { + displayName: 'Lead Source ID', + name: 'lead_source_id', + type: 'string', // not obtainable from API + default: '', + description: 'ID of the source where deal came from', + }, + { + displayName: 'Owner ID', + name: 'owner_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user to whom the deal is assigned', + }, + { + displayName: 'Probability', + name: 'probability', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + maxValue: 100, + }, + description: 'Probability of winning the deal as a number between 0 and 100', + }, + { + displayName: 'Sales Account ID', + name: 'sales_account_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + description: 'ID of the account that the deal belongs to', + }, + { + displayName: 'Territory ID', + name: 'territory_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTerritories', + }, + description: 'ID of the territory that the deal belongs to', + }, + ], + }, + + // ---------------------------------------- + // deal: delete + // ---------------------------------------- + { + displayName: 'Deal ID', + name: 'dealId', + description: 'ID of the deal to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // deal: get + // ---------------------------------------- + { + displayName: 'Deal ID', + name: 'dealId', + description: 'ID of the deal to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // deal: getAll + // ---------------------------------------- + { + displayName: 'View', + name: 'view', + type: 'options', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'getAll', + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getDealViews', + }, + default: '', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'How many results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------------- + // deal: update + // ---------------------------------------- + { + displayName: 'Deal ID', + name: 'dealId', + description: 'ID of the deal to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'deal', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Amount', + name: 'amount', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Value of the deal', + }, + { + displayName: 'Base Currency Amount', + name: 'base_currency_amount', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + }, + description: 'Value of the deal in base currency', + }, + { + displayName: 'Campaign ID', + name: 'campaign_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + description: 'ID of the campaign that landed this deal', + }, + { + displayName: 'Currency ID', + name: 'currency_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getCurrencies', + }, + description: 'ID of the currency that the deal belongs to', + }, + { + displayName: 'Deal Payment Status ID', + name: 'deal_payment_status_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDealPaymentStatuses', + }, + description: 'ID of the mode of payment for the deal', + }, + { + displayName: 'Deal Pipeline ID', + name: 'deal_pipeline_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDealPipelines', + }, + description: 'ID of the deal pipeline that it belongs to', + }, + { + displayName: 'Deal Product ID', + name: 'deal_product_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDealProducts', + }, + description: 'ID of the product that the deal belongs to (in a multi-product company)', + }, + { + displayName: 'Deal Reason ID', + name: 'deal_reason_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDealReasons', + }, + description: 'ID of the reason for losing the deal. Can only be set if the deal is in \'Lost\' stage.', + }, + { + displayName: 'Deal Stage ID', + name: 'deal_stage_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDealStages', + }, + description: 'ID of the deal stage that the deal belongs to', + }, + { + displayName: 'Deal Type ID', + name: 'deal_type_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDealTypes', + }, + description: 'ID of the deal type that the deal belongs to', + }, + { + displayName: 'Lead Source ID', + name: 'lead_source_id', + type: 'string', // not obtainable from API + default: '', + description: 'ID of the source where deal came from', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + description: 'Name of the deal', + }, + { + displayName: 'Owner ID', + name: 'owner_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user to whom the deal is assigned', + }, + { + displayName: 'Probability', + name: 'probability', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + maxValue: 100, + }, + description: 'Probability of winning the deal as a number between 0 and 100', + }, + { + displayName: 'Sales Account ID', + name: 'sales_account_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getAccounts', + }, + description: 'ID of the account that the deal belongs to', + }, + { + displayName: 'Territory ID', + name: 'territory_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getTerritories', + }, + description: 'ID of the territory that the deal belongs to', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/FreshworksCrm/descriptions/NoteDescription.ts b/packages/nodes-base/nodes/FreshworksCrm/descriptions/NoteDescription.ts new file mode 100644 index 0000000000..d5f48085f1 --- /dev/null +++ b/packages/nodes-base/nodes/FreshworksCrm/descriptions/NoteDescription.ts @@ -0,0 +1,214 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const noteOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'note', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a note', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a note', + }, + { + name: 'Update', + value: 'update', + description: 'Update a note', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const noteFields = [ + // ---------------------------------------- + // note: create + // ---------------------------------------- + { + displayName: 'Content', + name: 'description', + description: 'Content of the note', + type: 'string', + required: true, + typeOptions: { + rows: 5, + }, + default: '', + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Target Type', + name: 'targetableType', + description: 'Type of the entity for which the note is created', + type: 'options', + required: true, + default: 'Contact', + options: [ + { + name: 'Contact', + value: 'Contact', + }, + { + name: 'Deal', + value: 'Deal', + }, + { + name: 'Sales Account', + value: 'SalesAccount', + }, + ], + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Target ID', + name: 'targetable_id', + description: 'ID of the entity for which note is created. The type of entity is selected in "Target Type".', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'create', + ], + }, + }, + }, + + // ---------------------------------------- + // note: delete + // ---------------------------------------- + { + displayName: 'Note ID', + name: 'noteId', + description: 'ID of the note to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // note: update + // ---------------------------------------- + { + displayName: 'Note ID', + name: 'noteId', + description: 'ID of the note to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'note', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Content', + name: 'description', + type: 'string', + typeOptions: { + rows: 5, + }, + default: '', + description: 'Content of the note', + }, + { + displayName: 'Target ID', + name: 'targetable_id', + type: 'string', + default: '', + description: 'ID of the entity for which the note is updated', + }, + { + displayName: 'Target Type', + name: 'targetable_type', + type: 'options', + default: 'Contact', + description: 'Type of the entity for which the note is updated', + options: [ + { + name: 'Contact', + value: 'Contact', + }, + { + name: 'Deal', + value: 'Deal', + }, + { + name: 'Sales Account', + value: 'SalesAccount', + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/FreshworksCrm/descriptions/SalesActivityDescription.ts b/packages/nodes-base/nodes/FreshworksCrm/descriptions/SalesActivityDescription.ts new file mode 100644 index 0000000000..d410df0bbf --- /dev/null +++ b/packages/nodes-base/nodes/FreshworksCrm/descriptions/SalesActivityDescription.ts @@ -0,0 +1,508 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const salesActivityOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + }, + }, + options: [ + // { + // name: 'Create', + // value: 'create', + // description: 'Create a sales activity', + // }, + // { + // name: 'Delete', + // value: 'delete', + // description: 'Delete a sales activity', + // }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a sales activity', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all sales activities', + }, + // { + // name: 'Update', + // value: 'update', + // description: 'Update a sales activity', + // }, + ], + default: 'get', + }, +] as INodeProperties[]; + +export const salesActivityFields = [ + // ---------------------------------------- + // salesActivity: create + // ---------------------------------------- + { + displayName: 'Sales Activity Type ID', + name: 'sales_activity_type_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getSalesActivityTypes', + }, + description: 'ID of a sales activity type for which the sales activity is created', + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Title', + name: 'title', + description: 'Title of the sales activity to create', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Owner ID', + name: 'ownerId', + description: 'ID of the user who owns the sales activity', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + required: true, + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Start Date', + name: 'from_date', + description: 'Timestamp that denotes the end of sales activity', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'End Date', + name: 'end_date', + description: 'Timestamp that denotes the end of sales activity', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Target Type', + name: 'targetableType', + description: 'Type of the entity for which the sales activity is created', + type: 'options', + required: true, + default: 'Contact', + options: [ + { + name: 'Contact', + value: 'Contact', + }, + { + name: 'Deal', + value: 'Deal', + }, + { + name: 'Sales Account', + value: 'SalesAccount', + }, + ], + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Target ID', + name: 'targetable_id', + description: 'ID of the entity for which the sales activity is created. The type of entity is selected in "Target Type".', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Creator ID', + name: 'creater_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user who created the sales activity', + }, + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + default: '', + description: 'Latitude of the location when you check in on a sales activity', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Location of the sales activity', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + default: '', + description: 'Longitude of the location when you check in for a sales activity', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'Description about the sales activity', + }, + { + displayName: 'Sales Activity Outcome ID', + name: 'sales_activity_outcome_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getOutcomes', + }, + description: 'ID of a sales activity\'s outcome', + }, + ], + }, + + // ---------------------------------------- + // salesActivity: delete + // ---------------------------------------- + { + displayName: 'Sales Activity ID', + name: 'salesActivityId', + description: 'ID of the salesActivity to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // salesActivity: get + // ---------------------------------------- + { + displayName: 'Sales Activity ID', + name: 'salesActivityId', + description: 'ID of the salesActivity to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // salesActivity: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'How many results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + + // ---------------------------------------- + // salesActivity: update + // ---------------------------------------- + { + displayName: 'Sales Activity ID', + name: 'salesActivityId', + description: 'ID of the salesActivity to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'salesActivity', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Creator ID', + name: 'creater_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user who created the sales activity', + }, + { + displayName: 'Start Date', + name: 'end_date', + description: 'Timestamp that denotes the start of the sales activity', + type: 'dateTime', + }, + { + displayName: 'Latitude', + name: 'latitude', + type: 'string', + default: '', + description: 'Latitude of the location when you check in on a sales activity', + }, + { + displayName: 'Location', + name: 'location', + type: 'string', + default: '', + description: 'Location of the sales activity', + }, + { + displayName: 'Longitude', + name: 'longitude', + type: 'string', + default: '', + description: 'Longitude of the location when you check in for a sales activity', + }, + { + displayName: 'Notes', + name: 'notes', + type: 'string', + default: '', + description: 'Description about the sales activity', + }, + { + displayName: 'Owner ID', + name: 'owner_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user who owns the sales activity', + }, + { + displayName: 'Sales Activity Outcome ID', + name: 'sales_activity_outcome_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getOutcomes', + }, + description: 'ID of a sales activity\'s outcome', + }, + { + displayName: 'Sales Activity Type ID', + name: 'sales_activity_type_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getSalesActivityTypes', + }, + description: 'ID of a sales activity type for which the sales activity is updated', + }, + { + displayName: 'Start Date', + name: 'from_date', + description: 'Timestamp that denotes the start of the sales activity', + type: 'dateTime', + }, + { + displayName: 'Target ID', + name: 'targetable_id', + type: 'string', + default: '', + description: 'ID of the entity for which the sales activity is updated. The type of entity is selected in "Target Type".', + }, + { + displayName: 'Target Type', + name: 'targetable_type', + type: 'options', + default: 'Contact', + description: 'Type of the entity for which the sales activity is updated', + options: [ + { + name: 'Contact', + value: 'Contact', + }, + { + name: 'Deal', + value: 'Deal', + }, + { + name: 'SalesAccount', + value: 'SalesAccount', + }, + ], + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the sales activity to update', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/FreshworksCrm/descriptions/TaskDescription.ts b/packages/nodes-base/nodes/FreshworksCrm/descriptions/TaskDescription.ts new file mode 100644 index 0000000000..0f8fc6de33 --- /dev/null +++ b/packages/nodes-base/nodes/FreshworksCrm/descriptions/TaskDescription.ts @@ -0,0 +1,480 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const taskOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'task', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a task', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a task', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a task', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all tasks', + }, + { + name: 'Update', + value: 'update', + description: 'Update a task', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const taskFields = [ + // ---------------------------------------- + // task: create + // ---------------------------------------- + { + displayName: 'Title', + name: 'title', + description: 'Title of the task', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Due Date', + name: 'dueDate', + description: 'Timestamp that denotes when the task is due to be completed', + type: 'dateTime', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Owner ID', + name: 'ownerId', + description: 'ID of the user to whom the task is assigned', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Target Type', + name: 'targetableType', + description: 'Type of the entity for which the task is updated', + type: 'options', + required: true, + default: 'Contact', + options: [ + { + name: 'Contact', + value: 'Contact', + }, + { + name: 'Deal', + value: 'Deal', + }, + { + name: 'SalesAccount', + value: 'SalesAccount', + }, + ], + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Target ID', + name: 'targetable_id', + description: 'ID of the entity for which the task is created. The type of entity is selected in "Target Type".', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Creator ID', + name: 'creater_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user who created the task', + }, + { + displayName: 'Outcome ID', + name: 'outcome_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getOutcomes', + }, + description: 'ID of the outcome of the task', + }, + { + displayName: 'Task Type ID', + name: 'task_type_id', + type: 'string', // not obtainable from API + default: '', + description: 'ID of the type of task', + }, + ], + }, + + // ---------------------------------------- + // task: delete + // ---------------------------------------- + { + displayName: 'Task ID', + name: 'taskId', + description: 'ID of the task to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // task: get + // ---------------------------------------- + { + displayName: 'Task ID', + name: 'taskId', + description: 'ID of the task to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // task: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'How many results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + default: false, + placeholder: 'Add Filter', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Include', + name: 'include', + type: 'options', + default: 'owner', + options: [ + { + name: 'Owner', + value: 'owner', + }, + { + name: 'Target', + value: 'targetable', + }, + { + name: 'Users', + value: 'users', + }, + ], + }, + { + displayName: 'Status', + name: 'filter', + type: 'options', + default: 'open', + options: [ + { + name: 'Completed', + value: 'completed', + }, + { + name: 'Due Today', + value: 'due_today', + }, + { + name: 'Due Tomorrow', + value: 'due_tomorrow', + }, + { + name: 'Open', + value: 'open', + }, + { + name: 'Overdue', + value: 'overdue', + }, + ], + }, + ], + }, + + // ---------------------------------------- + // task: update + // ---------------------------------------- + { + displayName: 'Task ID', + name: 'taskId', + description: 'ID of the task to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update', + ], + }, + }, + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'task', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Creator ID', + name: 'creater_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user who created the sales activity', + }, + { + displayName: 'Due Date', + name: 'dueDate', + description: 'Timestamp that denotes when the task is due to be completed', + type: 'dateTime', + default: '', + }, + { + displayName: 'Outcome ID', + name: 'outcome_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getOutcomes', + }, + description: 'ID of the outcome of the task', + }, + { + displayName: 'Owner ID', + name: 'owner_id', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + description: 'ID of the user to whom the task is assigned', + }, + { + displayName: 'Target ID', + name: 'targetable_id', + type: 'string', + default: '', + description: 'ID of the entity for which the task is updated. The type of entity is selected in "Target Type".', + }, + { + displayName: 'Target Type', + name: 'targetable_type', + description: 'Type of the entity for which the task is updated', + type: 'options', + default: 'Contact', + options: [ + { + name: 'Contact', + value: 'Contact', + }, + { + name: 'Deal', + value: 'Deal', + }, + { + name: 'SalesAccount', + value: 'SalesAccount', + }, + ], + }, + { + displayName: 'Task Type ID', + name: 'task_type_id', + type: 'string', // not obtainable from API + default: '', + description: 'ID of the type of task', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: 'Title of the task', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/FreshworksCrm/descriptions/index.ts b/packages/nodes-base/nodes/FreshworksCrm/descriptions/index.ts new file mode 100644 index 0000000000..70957a2c38 --- /dev/null +++ b/packages/nodes-base/nodes/FreshworksCrm/descriptions/index.ts @@ -0,0 +1,7 @@ +export * from './AccountDescription'; +export * from './AppointmentDescription'; +export * from './ContactDescription'; +export * from './DealDescription'; +export * from './NoteDescription'; +export * from './SalesActivityDescription'; +export * from './TaskDescription'; diff --git a/packages/nodes-base/nodes/FreshworksCrm/freshworksCrm.svg b/packages/nodes-base/nodes/FreshworksCrm/freshworksCrm.svg new file mode 100644 index 0000000000..06e0cf9acf --- /dev/null +++ b/packages/nodes-base/nodes/FreshworksCrm/freshworksCrm.svg @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nodes-base/nodes/FreshworksCrm/types.d.ts b/packages/nodes-base/nodes/FreshworksCrm/types.d.ts new file mode 100644 index 0000000000..4d25342366 --- /dev/null +++ b/packages/nodes-base/nodes/FreshworksCrm/types.d.ts @@ -0,0 +1,43 @@ +export type FreshworksCrmApiCredentials = { + apiKey: string; + domain: string; +} + +export type FreshworksConfigResponse = { + [key: string]: T[]; +}; + +export type LoadOption = { + name: string; + value: string; +}; + +export type LoadedCurrency = { + currency_code: string; + id: string; +}; + +export type LoadedUser = { + id: string; + display_name: string; +}; + +export type SalesAccounts = { + sales_accounts?: number[]; +}; + +export type ViewsResponse = { + filters: View[]; + meta: object; +} + +export type View = { + id: number; + name: string; + model_class_name: string; + user_id: number; + is_default: boolean; + updated_at: string; + user_name: string; + current_user_permissions: string[]; +}; diff --git a/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts index 778f6ea2f6..d76dd48d28 100644 --- a/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Google/Gmail/GenericFunctions.ts @@ -142,6 +142,7 @@ export async function encodeEmail(email: IEmail) { let mailBody: Buffer; const mailOptions = { + from: email.from, to: email.to, cc: email.cc, bcc: email.bcc, diff --git a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts index 1107630f05..5b72a2c80e 100644 --- a/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts +++ b/packages/nodes-base/nodes/Google/Gmail/Gmail.node.ts @@ -45,6 +45,7 @@ import { } from 'lodash'; export interface IEmail { + from?: string; to?: string; cc?: string; bcc?: string; @@ -355,6 +356,7 @@ export class Gmail implements INodeType { } const email: IEmail = { + from: additionalFields.senderName as string || '', to: toStr, cc: ccStr, bcc: bccStr, @@ -455,6 +457,7 @@ export class Gmail implements INodeType { } const email: IEmail = { + from: additionalFields.senderName as string || '', to: toStr, cc: ccStr, bcc: bccStr, diff --git a/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts b/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts index a860580951..a257985509 100644 --- a/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts +++ b/packages/nodes-base/nodes/Google/Gmail/MessageDescription.ts @@ -277,6 +277,16 @@ export const messageFields = [ placeholder: 'info@example.com', default: [], }, + { + displayName: 'Sender Name', + name: 'senderName', + type: 'string', + placeholder: 'Name ', + default: '', + description: `The name displayed in your contacts inboxes.
+ It has to be in the format: "Display-Name <name@gmail.com>".
+ The email address has to match the email address of the logged in user for the API`, + }, ], }, { diff --git a/packages/nodes-base/nodes/Google/Perspective/GenericFunctions.ts b/packages/nodes-base/nodes/Google/Perspective/GenericFunctions.ts new file mode 100644 index 0000000000..ad93b1a0ec --- /dev/null +++ b/packages/nodes-base/nodes/Google/Perspective/GenericFunctions.ts @@ -0,0 +1,40 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + NodeApiError, +} from 'n8n-workflow'; + +export async function googleApiRequest( + this: IExecuteFunctions, + method: 'POST', + endpoint: string, + body: IDataObject = {}, +) { + const options: OptionsWithUri = { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + method, + body, + uri: `https://commentanalyzer.googleapis.com${endpoint}`, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + try { + return await this.helpers.requestOAuth2.call(this, 'googlePerspectiveOAuth2Api', options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} diff --git a/packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.json b/packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.json new file mode 100644 index 0000000000..b785a9d94a --- /dev/null +++ b/packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.json @@ -0,0 +1,20 @@ +{ + "node": "n8n-nodes-base.perspective", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Development" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/perspective" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.perspective/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.ts b/packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.ts new file mode 100644 index 0000000000..662573de01 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Perspective/GooglePerspective.node.ts @@ -0,0 +1,292 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, + NodeOperationError, +} from 'n8n-workflow'; + +import { + AttributesValuesUi, + CommentAnalyzeBody, + Language, + RequestedAttributes, +} from './types'; + +import { + googleApiRequest, +} from './GenericFunctions'; + +const ISO6391 = require('iso-639-1'); + +export class GooglePerspective implements INodeType { + description: INodeTypeDescription = { + displayName: 'Google Perspective', + name: 'googlePerspective', + icon: 'file:perspective.svg', + group: [ + 'transform', + ], + version: 1, + description: 'Consume Google Perspective API', + subtitle: '={{$parameter["operation"]}}', + defaults: { + name: 'Google Perspective', + color: '#200647', + }, + inputs: [ + 'main', + ], + outputs: [ + 'main', + ], + credentials: [ + { + name: 'googlePerspectiveOAuth2Api', + required: true, + }, + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Analyze Comment', + value: 'analyzeComment', + }, + ], + default: 'analyzeComment', + }, + { + displayName: 'Text', + name: 'text', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + operation: [ + 'analyzeComment', + ], + }, + }, + }, + { + displayName: 'Attributes to Analyze', + name: 'requestedAttributesUi', + type: 'fixedCollection', + default: '', + typeOptions: { + multipleValues: true, + }, + placeholder: 'Add Atrribute', + required: true, + displayOptions: { + show: { + operation: [ + 'analyzeComment', + ], + }, + }, + options: [ + { + displayName: 'Properties', + name: 'requestedAttributesValues', + values: [ + { + displayName: 'Attribute Name', + name: 'attributeName', + type: 'options', + options: [ + { + name: 'Flirtation', + value: 'flirtation', + }, + { + name: 'Identity Attack', + value: 'identity_attack', + }, + { + name: 'Insult', + value: 'insult', + }, + { + name: 'Profanity', + value: 'profanity', + }, + { + name: 'Severe Toxicity', + value: 'severe_toxicity', + }, + { + name: 'Sexually Explicit', + value: 'sexually_explicit', + }, + { + name: 'Threat', + value: 'threat', + }, + { + name: 'Toxicity', + value: 'toxicity', + }, + ], + description: 'Attribute to analyze in the text. Details here', + default: 'flirtation', + }, + { + displayName: 'Score Threshold', + name: 'scoreThreshold', + type: 'number', + typeOptions: { + numberStepSize: 0.1, + numberPrecision: 2, + minValue: 0, + maxValue: 1, + }, + description: 'Score above which to return results. At zero, all scores are returned.', + default: 0, + }, + ], + }, + ], + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'analyzeComment', + ], + }, + }, + default: {}, + placeholder: 'Add Option', + options: [ + { + displayName: 'Languages', + name: 'languages', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getLanguages', + }, + default: '', + description: 'Languages of the text input. If unspecified, the API will auto-detect the comment language', + }, + ], + }, + ], + }; + + methods = { + loadOptions: { + // Get all the available languages to display them to user so that he can + // select them easily + async getLanguages(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const supportedLanguages = [ + 'English', + 'Spanish', + 'French', + 'German', + 'Portuguese', + 'Italian', + 'Russian', + ]; + + const languages = ISO6391.getAllNames().filter((language: string) => supportedLanguages.includes(language)); + for (const language of languages) { + const languageName = language; + const languageId = ISO6391.getCode(language); + returnData.push({ + name: languageName, + value: languageId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const operation = this.getNodeParameter('operation', 0); + + const returnData: IDataObject[] = []; + let responseData; + + for (let i = 0; i < items.length; i++) { + + try { + + + if (operation === 'analyzeComment') { + + // https://developers.perspectiveapi.com/s/about-the-api-methods + + const attributes = this.getNodeParameter( + 'requestedAttributesUi.requestedAttributesValues', i, [], + ) as AttributesValuesUi[]; + + if (!attributes.length) { + throw new NodeOperationError( + this.getNode(), + 'Please enter at least one attribute to analyze.', + ); + } + + const requestedAttributes = attributes.reduce((acc, cur) => { + return Object.assign(acc, { + [cur.attributeName.toUpperCase()]: { + scoreType: 'probability', + scoreThreshold: cur.scoreThreshold, + }, + }); + }, {}); + + const body: CommentAnalyzeBody = { + comment: { + type: 'PLAIN_TEXT', + text: this.getNodeParameter('text', i) as string, + }, + requestedAttributes, + }; + + const { languages } = this.getNodeParameter('options', i) as { languages: Language }; + + if (languages?.length) { + body.languages = languages; + } + + responseData = await googleApiRequest.call(this, 'POST', '/v1alpha1/comments:analyze', body); + } + + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + + } + + return [this.helpers.returnJsonArray(responseData)]; + } +} diff --git a/packages/nodes-base/nodes/Google/Perspective/perspective.svg b/packages/nodes-base/nodes/Google/Perspective/perspective.svg new file mode 100644 index 0000000000..2cfbaf8a3d --- /dev/null +++ b/packages/nodes-base/nodes/Google/Perspective/perspective.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/packages/nodes-base/nodes/Google/Perspective/types.d.ts b/packages/nodes-base/nodes/Google/Perspective/types.d.ts new file mode 100644 index 0000000000..bb4ade830b --- /dev/null +++ b/packages/nodes-base/nodes/Google/Perspective/types.d.ts @@ -0,0 +1,26 @@ +export type CommentAnalyzeBody = { + comment: Comment; + requestedAttributes: RequestedAttributes; + languages?: Language; +}; + +export type Language = 'de' | 'en' | 'fr' | 'ar' | 'es' | 'it' | 'pt' | 'ru'; + +export type Comment = { + text?: string; + type?: string; +}; + +export type RequestedAttributes = { + [key: string]: { + scoreType?: string; + scoreThreshold?: { + value: number + }; + }; +}; + +export type AttributesValuesUi = { + attributeName: string; + scoreThreshold: number; +}; diff --git a/packages/nodes-base/nodes/Interval.node.ts b/packages/nodes-base/nodes/Interval.node.ts index cd7bcbd21e..55592236e9 100644 --- a/packages/nodes-base/nodes/Interval.node.ts +++ b/packages/nodes-base/nodes/Interval.node.ts @@ -78,7 +78,13 @@ export class Interval implements INodeType { this.emit([this.helpers.returnJsonArray([{}])]); }; - const intervalObj = setInterval(executeTrigger, intervalValue * 1000); + intervalValue *= 1000; + + if (intervalValue > Number.MAX_SAFE_INTEGER) { + throw new Error('The interval value is too large.'); + } + + const intervalObj = setInterval(executeTrigger, ); async function closeFunction() { clearInterval(intervalObj); diff --git a/packages/nodes-base/nodes/Marketstack/GenericFunctions.ts b/packages/nodes-base/nodes/Marketstack/GenericFunctions.ts new file mode 100644 index 0000000000..3a20c1ca26 --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/GenericFunctions.ts @@ -0,0 +1,96 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +export async function marketstackApiRequest( + this: IExecuteFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const credentials = this.getCredentials('marketstackApi') as IDataObject; + const protocol = credentials.useHttps ? 'https' : 'http'; // Free API does not support HTTPS + + const options: OptionsWithUri = { + method, + uri: `${protocol}://api.marketstack.com/v1${endpoint}`, + qs: { + access_key: credentials.apiKey, + ...qs, + }, + json: true, + }; + + if (!Object.keys(body).length) { + delete options.body; + } + + try { + return await this.helpers.request(options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + +export async function marketstackApiRequestAllItems( + this: IExecuteFunctions, + method: string, + endpoint: string, + body: IDataObject = {}, + qs: IDataObject = {}, +) { + const returnAll = this.getNodeParameter('returnAll', 0, false) as boolean; + const limit = this.getNodeParameter('limit', 0, 0) as number; + + let responseData; + const returnData: IDataObject[] = []; + + qs.offset = 0; + + do { + responseData = await marketstackApiRequest.call(this, method, endpoint, body, qs); + returnData.push(...responseData.data); + + if (!returnAll && returnData.length > limit) { + return returnData.slice(0, limit); + } + + qs.offset += responseData.count; + } while ( + responseData.total > returnData.length + ); + + return returnData; +} + +export const format = (datetime?: string) => datetime?.split('T')[0]; + +export function validateTimeOptions( + this: IExecuteFunctions, + timeOptions: boolean[], +) { + if (timeOptions.every(o => !o)) { + throw new NodeOperationError( + this.getNode(), + 'Please filter by latest, specific date or timeframe (start and end dates).', + ); + } + + if (timeOptions.filter(Boolean).length > 1) { + throw new NodeOperationError( + this.getNode(), + 'Please filter by one of latest, specific date, or timeframe (start and end dates).', + ); + } +} diff --git a/packages/nodes-base/nodes/Marketstack/Marketstack.node.ts b/packages/nodes-base/nodes/Marketstack/Marketstack.node.ts new file mode 100644 index 0000000000..325855713a --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/Marketstack.node.ts @@ -0,0 +1,203 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + NodeOperationError, +} from 'n8n-workflow'; + +import { + endOfDayDataFields, + endOfDayDataOperations, + exchangeFields, + exchangeOperations, + tickerFields, + tickerOperations, +} from './descriptions'; + +import { + format, + marketstackApiRequest, + marketstackApiRequestAllItems, + validateTimeOptions, +} from './GenericFunctions'; + +import { + EndOfDayDataFilters, + Operation, + Resource, +} from './types'; + +export class Marketstack implements INodeType { + description: INodeTypeDescription = { + displayName: 'Marketstack', + name: 'marketstack', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + icon: 'file:marketstack.svg', + group: ['transform'], + version: 1, + description: 'Consume Marketstack API', + defaults: { + name: 'Marketstack', + color: '#02283e', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'marketstackApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'End-of-Day Data', + value: 'endOfDayData', + description: 'Stock market closing data', + }, + { + name: 'Exchange', + value: 'exchange', + description: 'Stock market exchange', + }, + { + name: 'Ticker', + value: 'ticker', + description: 'Stock market symbol', + }, + ], + default: 'endOfDayData', + required: true, + }, + ...endOfDayDataOperations, + ...endOfDayDataFields, + ...exchangeOperations, + ...exchangeFields, + ...tickerOperations, + ...tickerFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + + const resource = this.getNodeParameter('resource', 0) as Resource; + const operation = this.getNodeParameter('operation', 0) as Operation; + + let responseData: any; // tslint:disable-line: no-any + const returnData: IDataObject[] = []; + + for (let i = 0; i < items.length; i++) { + + try { + + if (resource === 'endOfDayData') { + + if (operation === 'getAll') { + + // ---------------------------------- + // endOfDayData: getAll + // ---------------------------------- + + const qs: IDataObject = { + symbols: this.getNodeParameter('symbols', i), + }; + + const { + latest, + specificDate, + dateFrom, + dateTo, + ...rest + } = this.getNodeParameter('filters', i) as EndOfDayDataFilters; + + validateTimeOptions.call(this, [ + latest !== undefined && latest !== false, + specificDate !== undefined, + dateFrom !== undefined && dateTo !== undefined, + ]); + + if (Object.keys(rest).length) { + Object.assign(qs, rest); + } + + let endpoint: string; + + if (latest) { + endpoint = '/eod/latest'; + } else if (specificDate) { + endpoint = `/eod/${format(specificDate)}`; + } else { + if (!dateFrom || !dateTo) { + throw new NodeOperationError( + this.getNode(), + 'Please enter a start and end date to filter by timeframe.', + ); + } + endpoint = '/eod'; + qs.date_from = format(dateFrom); + qs.date_to = format(dateTo); + } + + responseData = await marketstackApiRequestAllItems.call(this, 'GET', endpoint, {}, qs); + + } + + } else if (resource === 'exchange') { + + if (operation === 'get') { + + // ---------------------------------- + // exchange: get + // ---------------------------------- + + const exchange = this.getNodeParameter('exchange', i); + const endpoint = `/exchanges/${exchange}`; + + responseData = await marketstackApiRequest.call(this, 'GET', endpoint); + + } + + } else if (resource === 'ticker') { + + if (operation === 'get') { + + // ---------------------------------- + // ticker: get + // ---------------------------------- + + const symbol = this.getNodeParameter('symbol', i); + const endpoint = `/tickers/${symbol}`; + + responseData = await marketstackApiRequest.call(this, 'GET', endpoint); + + } + + } + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.message }); + continue; + } + throw error; + } + + Array.isArray(responseData) + ? returnData.push(...responseData) + : returnData.push(responseData); + + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Marketstack/descriptions/EndOfDayDataDescription.ts b/packages/nodes-base/nodes/Marketstack/descriptions/EndOfDayDataDescription.ts new file mode 100644 index 0000000000..7b36201441 --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/descriptions/EndOfDayDataDescription.ts @@ -0,0 +1,157 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const endOfDayDataOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'getAll', + displayOptions: { + show: { + resource: [ + 'endOfDayData', + ], + }, + }, + }, +]; + +export const endOfDayDataFields: INodeProperties[] = [ + { + displayName: 'Ticker', + name: 'symbols', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'endOfDayData', + ], + operation: [ + 'getAll', + ], + }, + }, + default: '', + description: 'One or multiple comma-separated stock symbols (tickers) to retrieve, e.g. AAPL or AAPL,MSFT', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'endOfDayData', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'How many results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'endOfDayData', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'endOfDayData', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Exchange', + name: 'exchange', + type: 'string', + default: '', + description: 'Stock exchange to filter results by, specified by Market Identifier Code, e.g. XNAS', + }, + { + displayName: 'Latest', + name: 'latest', + type: 'boolean', + default: false, + description: 'Whether to fetch the most recent stock market data', + }, + { + displayName: 'Sort Order', + name: 'sort', + description: 'Order to sort results in', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ASC', + }, + { + name: 'Descending', + value: 'DESC', + }, + ], + default: 'DESC', + }, + { + displayName: 'Specific Date', + name: 'specificDate', + type: 'dateTime', + default: '', + description: 'Date in YYYY-MM-DD format, e.g. 2020-01-01, or in ISO-8601 date format, e.g. 2020-05-21T00:00:00+0000', + }, + { + displayName: 'Timeframe Start Date', + name: 'dateFrom', + type: 'dateTime', + default: '', + description: 'Timeframe start date in YYYY-MM-DD format, e.g. 2020-01-01, or in ISO-8601 date format, e.g. 2020-05-21T00:00:00+0000', + }, + { + displayName: 'Timeframe End Date', + name: 'dateTo', + type: 'dateTime', + default: '', + description: 'Timeframe end date in YYYY-MM-DD format, e.g. 2020-01-01, or in ISO-8601 date format, e.g. 2020-05-21T00:00:00+0000', + }, + ], + }, +]; diff --git a/packages/nodes-base/nodes/Marketstack/descriptions/ExchangeDescription.ts b/packages/nodes-base/nodes/Marketstack/descriptions/ExchangeDescription.ts new file mode 100644 index 0000000000..5973020ab7 --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/descriptions/ExchangeDescription.ts @@ -0,0 +1,46 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const exchangeOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Get', + value: 'get', + }, + ], + default: 'get', + displayOptions: { + show: { + resource: [ + 'exchange', + ], + }, + }, + }, +]; + +export const exchangeFields: INodeProperties[] = [ + { + displayName: 'Exchange', + name: 'exchange', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'exchange', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Stock exchange to retrieve, specified by Market Identifier Code, e.g. XNAS', + }, +]; diff --git a/packages/nodes-base/nodes/Marketstack/descriptions/TickerDescription.ts b/packages/nodes-base/nodes/Marketstack/descriptions/TickerDescription.ts new file mode 100644 index 0000000000..d0e8839f05 --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/descriptions/TickerDescription.ts @@ -0,0 +1,46 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const tickerOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + options: [ + { + name: 'Get', + value: 'get', + }, + ], + default: 'get', + displayOptions: { + show: { + resource: [ + 'ticker', + ], + }, + }, + }, +]; + +export const tickerFields: INodeProperties[] = [ + { + displayName: 'Ticker', + name: 'symbol', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'ticker', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Stock symbol (ticker) to retrieve, e.g. AAPL', + }, +]; diff --git a/packages/nodes-base/nodes/Marketstack/descriptions/index.ts b/packages/nodes-base/nodes/Marketstack/descriptions/index.ts new file mode 100644 index 0000000000..8015eaae6f --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/descriptions/index.ts @@ -0,0 +1,3 @@ +export * from './EndOfDayDataDescription'; +export * from './TickerDescription'; +export * from './ExchangeDescription'; diff --git a/packages/nodes-base/nodes/Marketstack/marketstack.svg b/packages/nodes-base/nodes/Marketstack/marketstack.svg new file mode 100644 index 0000000000..25ad681cc9 --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/marketstack.svg @@ -0,0 +1,57 @@ + + + + + +Marketstack + + +Marketstack diff --git a/packages/nodes-base/nodes/Marketstack/types.d.ts b/packages/nodes-base/nodes/Marketstack/types.d.ts new file mode 100644 index 0000000000..17760df15f --- /dev/null +++ b/packages/nodes-base/nodes/Marketstack/types.d.ts @@ -0,0 +1,12 @@ +export type Resource = 'endOfDayData' | 'exchange' | 'ticker'; + +export type Operation = 'get' | 'getAll'; + +export type EndOfDayDataFilters = { + latest?: boolean; + sort?: 'ASC' | 'DESC'; + specificDate?: string; + dateFrom?: string; + dateTo?: string; + exchange?: string; +}; diff --git a/packages/nodes-base/nodes/NocoDB/GenericFunctions.ts b/packages/nodes-base/nodes/NocoDB/GenericFunctions.ts new file mode 100644 index 0000000000..abdb64c770 --- /dev/null +++ b/packages/nodes-base/nodes/NocoDB/GenericFunctions.ts @@ -0,0 +1,135 @@ +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + OptionsWithUri, +} from 'request'; + +import { + IBinaryKeyData, + IDataObject, + INodeExecutionData, + IPollFunctions, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + + +interface IAttachment { + url: string; + title: string; + mimetype: string; + size: number; +} + +/** + * Make an API request to NocoDB + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} url + * @param {object} body + * @returns {Promise} + */ +export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, method: string, endpoint: string, body: object, query?: IDataObject, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('nocoDb'); + + if (credentials === undefined) { + throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); + } + + query = query || {}; + + const options: OptionsWithUri = { + headers: { + 'xc-auth': credentials.apiToken, + }, + method, + body, + qs: query, + uri: uri || `${credentials.host}${endpoint}`, + json: true, + + }; + + if (Object.keys(option).length !== 0) { + Object.assign(options, option); + } + + if (Object.keys(body).length === 0) { + delete options.body; + } + + try { + return await this.helpers.request!(options); + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + + +/** + * Make an API request to paginated NocoDB endpoint + * and return all results + * + * @export + * @param {(IHookFunctions | IExecuteFunctions)} this + * @param {string} method + * @param {string} endpoint + * @param {IDataObject} body + * @param {IDataObject} [query] + * @returns {Promise} + */ +export async function apiRequestAllItems(this: IHookFunctions | IExecuteFunctions | IPollFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject): Promise { // tslint:disable-line:no-any + + if (query === undefined) { + query = {}; + } + query.limit = 100; + query.offset = query?.offset ? query.offset as number : 0; + const returnData: IDataObject[] = []; + + let responseData; + + do { + responseData = await apiRequest.call(this, method, endpoint, body, query); + + returnData.push(...responseData); + + query.offset += query.limit; + + } while ( + responseData.length === 0 + ); + + return returnData; +} + +export async function downloadRecordAttachments(this: IExecuteFunctions | IPollFunctions, records: IDataObject[], fieldNames: string[]): Promise { + const elements: INodeExecutionData[] = []; + + for (const record of records) { + const element: INodeExecutionData = { json: {}, binary: {} }; + element.json = record as unknown as IDataObject; + for (const fieldName of fieldNames) { + if (record[fieldName]) { + for (const [index, attachment] of (JSON.parse(record[fieldName] as string) as IAttachment[]).entries()) { + const file = await apiRequest.call(this, 'GET', '', {}, {}, attachment.url, { json: false, encoding: null }); + element.binary![`${fieldName}_${index}`] = { + data: Buffer.from(file).toString('base64'), + fileName: attachment.title, + mimeType: attachment.mimetype, + }; + } + } + } + if (Object.keys(element.binary as IBinaryKeyData).length === 0) { + delete element.binary; + } + elements.push(element); + } + return elements; +} diff --git a/packages/nodes-base/nodes/NocoDB/NocoDB.node.json b/packages/nodes-base/nodes/NocoDB/NocoDB.node.json new file mode 100644 index 0000000000..0e9c652a98 --- /dev/null +++ b/packages/nodes-base/nodes/NocoDB/NocoDB.node.json @@ -0,0 +1,22 @@ +{ + "node": "n8n-nodes-base.nocoDb", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Data & Storage" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/nocoDb" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.nocoDb/" + } + ], + "generic": [ + ] + } +} diff --git a/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts b/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts new file mode 100644 index 0000000000..a32eb12db8 --- /dev/null +++ b/packages/nodes-base/nodes/NocoDB/NocoDB.node.ts @@ -0,0 +1,380 @@ +import { + BINARY_ENCODING, + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryData, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + NodeApiError, + NodeOperationError, +} from 'n8n-workflow'; + +import { + apiRequest, + apiRequestAllItems, + downloadRecordAttachments, +} from './GenericFunctions'; + +import { + operationFields +} from './OperationDescription'; + +export class NocoDB implements INodeType { + description: INodeTypeDescription = { + displayName: 'NocoDB', + name: 'nocoDb', + icon: 'file:nocodb.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Read, update, write and delete data from NocoDB', + defaults: { + name: 'NocoDB', + color: '#0989ff', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'nocoDb', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Row', + value: 'row', + }, + ], + default: 'row', + description: 'The Resource to operate on', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'row', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a row', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a row', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all rows', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a row', + }, + { + name: 'Update', + value: 'update', + description: 'Update a row', + }, + ], + default: 'get', + description: 'The operation to perform', + }, + ...operationFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + let responseData; + + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + const projectId = this.getNodeParameter('projectId', 0) as string; + const table = this.getNodeParameter('table', 0) as string; + + let returnAll = false; + let endpoint = ''; + let requestMethod = ''; + + let qs: IDataObject = {}; + + if (resource === 'row') { + + if (operation === 'create') { + + requestMethod = 'POST'; + endpoint = `/nc/${projectId}/api/v1/${table}/bulk`; + + const body: IDataObject[] = []; + + for (let i = 0; i < items.length; i++) { + const newItem: IDataObject = {}; + const dataToSend = this.getNodeParameter('dataToSend', i) as 'defineBelow' | 'autoMapInputData'; + + if (dataToSend === 'autoMapInputData') { + const incomingKeys = Object.keys(items[i].json); + const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string; + const inputDataToIgnore = rawInputsToIgnore.split(',').map(c => c.trim()); + for (const key of incomingKeys) { + if (inputDataToIgnore.includes(key)) continue; + newItem[key] = items[i].json[key]; + } + } else { + const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as Array<{ + fieldName: string; + binaryData: boolean; + fieldValue?: string; + binaryProperty?: string; + }>; + + for (const field of fields) { + if (!field.binaryData) { + newItem[field.fieldName] = field.fieldValue; + } else if (field.binaryProperty) { + if (!items[i].binary) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!'); + } + const binaryPropertyName = field.binaryProperty; + if (binaryPropertyName && !items[i].binary![binaryPropertyName]) { + throw new NodeOperationError(this.getNode(), `Binary property ${binaryPropertyName} does not exist on item!`); + } + const binaryData = items[i].binary![binaryPropertyName] as IBinaryData; + + const formData = { + file: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + contentType: binaryData.mimeType, + }, + }, + json: JSON.stringify({ + api: 'xcAttachmentUpload', + project_id: projectId, + dbAlias: 'db', + args: {}, + }), + }; + const qs = { project_id: projectId }; + + responseData = await apiRequest.call(this, 'POST', '/dashboard', {}, qs, undefined, { formData }); + newItem[field.fieldName] = JSON.stringify([responseData]); + } + } + } + body.push(newItem); + } + try { + responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); + + // Calculate ID manually and add to return data + let id = responseData[0]; + for (let i = body.length - 1; i >= 0; i--) { + body[i].id = id--; + } + + returnData.push(...body); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.toString() }); + } + throw new NodeApiError(this.getNode(), error); + } + } else if (operation === 'delete') { + + requestMethod = 'DELETE'; + endpoint = `/nc/${projectId}/api/v1/${table}/bulk`; + const body: IDataObject[] = []; + + for (let i = 0; i < items.length; i++) { + const id = this.getNodeParameter('id', i) as string; + body.push({ id }); + } + try { + responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); + returnData.push(...items.map(item => item.json)); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.toString() }); + } + throw new NodeApiError(this.getNode(), error); + } + } else if (operation === 'getAll') { + const data = []; + const downloadAttachments = this.getNodeParameter('downloadAttachments', 0) as boolean; + try { + for (let i = 0; i < items.length; i++) { + requestMethod = 'GET'; + endpoint = `/nc/${projectId}/api/v1/${table}`; + + returnAll = this.getNodeParameter('returnAll', 0) as boolean; + qs = this.getNodeParameter('options', i, {}) as IDataObject; + + if (qs.sort) { + const properties = (qs.sort as IDataObject).property as Array<{ field: string, direction: string }>; + qs.sort = properties.map(prop => `${prop.direction === 'asc' ? '' : '-'}${prop.field}`).join(','); + } + + if (qs.fields) { + qs.fields = (qs.fields as IDataObject[]).join(','); + } + + if (returnAll === true) { + responseData = await apiRequestAllItems.call(this, requestMethod, endpoint, {}, qs); + } else { + qs.limit = this.getNodeParameter('limit', 0) as number; + responseData = await apiRequest.call(this, requestMethod, endpoint, {}, qs); + } + + returnData.push.apply(returnData, responseData); + + if (downloadAttachments === true) { + const downloadFieldNames = (this.getNodeParameter('downloadFieldNames', 0) as string).split(','); + const response = await downloadRecordAttachments.call(this, responseData, downloadFieldNames); + data.push(...response); + } + } + + if (downloadAttachments) { + return [data]; + } + + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.toString() }); + } + throw error; + } + } else if (operation === 'get') { + + requestMethod = 'GET'; + const newItems: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const id = this.getNodeParameter('id', i) as string; + endpoint = `/nc/${projectId}/api/v1/${table}/${id}`; + responseData = await apiRequest.call(this, requestMethod, endpoint, {}, qs); + const newItem: INodeExecutionData = { json: responseData }; + + const downloadAttachments = this.getNodeParameter('downloadAttachments', i) as boolean; + + if (downloadAttachments === true) { + const downloadFieldNames = (this.getNodeParameter('downloadFieldNames', i) as string).split(','); + const data = await downloadRecordAttachments.call(this, [responseData], downloadFieldNames); + newItem.binary = data[0].binary; + } + + newItems.push(newItem); + } catch (error) { + if (this.continueOnFail()) { + newItems.push({ json: { error: error.toString() } }); + continue; + } + throw new NodeApiError(this.getNode(), error); + } + } + return this.prepareOutputData(newItems); + + } else if (operation === 'update') { + + requestMethod = 'PUT'; + endpoint = `/nc/${projectId}/api/v1/${table}/bulk`; + + const body: IDataObject[] = []; + + for (let i = 0; i < items.length; i++) { + + const id = this.getNodeParameter('id', i) as string; + const newItem: IDataObject = { id }; + const dataToSend = this.getNodeParameter('dataToSend', i) as 'defineBelow' | 'autoMapInputData'; + + if (dataToSend === 'autoMapInputData') { + const incomingKeys = Object.keys(items[i].json); + const rawInputsToIgnore = this.getNodeParameter('inputsToIgnore', i) as string; + const inputDataToIgnore = rawInputsToIgnore.split(',').map(c => c.trim()); + for (const key of incomingKeys) { + if (inputDataToIgnore.includes(key)) continue; + newItem[key] = items[i].json[key]; + } + } else { + const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as Array<{ + fieldName: string; + upload: boolean; + fieldValue?: string; + binaryProperty?: string; + }>; + + for (const field of fields) { + if (!field.upload) { + newItem[field.fieldName] = field.fieldValue; + } else if (field.binaryProperty) { + if (!items[i].binary) { + throw new NodeOperationError(this.getNode(), 'No binary data exists on item!'); + } + const binaryPropertyName = field.binaryProperty; + if (binaryPropertyName && !items[i].binary![binaryPropertyName]) { + throw new NodeOperationError(this.getNode(), `Binary property ${binaryPropertyName} does not exist on item!`); + } + const binaryData = items[i].binary![binaryPropertyName] as IBinaryData; + + const formData = { + file: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + contentType: binaryData.mimeType, + }, + }, + json: JSON.stringify({ + api: 'xcAttachmentUpload', + project_id: projectId, + dbAlias: 'db', + args: {}, + }), + }; + const qs = { project_id: projectId }; + + responseData = await apiRequest.call(this, 'POST', '/dashboard', {}, qs, undefined, { formData }); + newItem[field.fieldName] = JSON.stringify([responseData]); + } + } + } + body.push(newItem); + } + + try { + responseData = await apiRequest.call(this, requestMethod, endpoint, body, qs); + returnData.push(...body); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: error.toString() }); + } + throw new NodeApiError(this.getNode(), error); + } + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/NocoDB/OperationDescription.ts b/packages/nodes-base/nodes/NocoDB/OperationDescription.ts new file mode 100644 index 0000000000..c679e4c051 --- /dev/null +++ b/packages/nodes-base/nodes/NocoDB/OperationDescription.ts @@ -0,0 +1,383 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const operationFields = [ + // ---------------------------------- + // Shared + // ---------------------------------- + { + displayName: 'Project ID', + name: 'projectId', + type: 'string', + default: '', + required: true, + description: 'The ID of the project', + }, + { + displayName: 'Table', + name: 'table', + type: 'string', + default: '', + required: true, + description: 'The name of the table', + }, + // ---------------------------------- + // delete + // ---------------------------------- + { + displayName: 'Row ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + operation: [ + 'delete', + ], + }, + }, + default: '', + required: true, + description: 'ID of the row to delete', + }, + // ---------------------------------- + // getAll + // ---------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 100, + description: 'The max number of results to return', + }, + { + displayName: 'Download Attachments', + name: 'downloadAttachments', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: `When set to true the attachment fields define in 'Download Fields' will be downloaded.`, + }, + { + displayName: 'Download Fields', + name: 'downloadFieldNames', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + downloadAttachments: [ + true, + ], + }, + }, + default: '', + description: `Name of the fields of type 'attachment' that should be downloaded. Multiple ones can be defined separated by comma. Case sensitive.`, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + }, + }, + default: {}, + placeholder: 'Add Option', + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Field', + }, + default: [], + placeholder: 'Name', + description: 'The select fields of the returned rows', + }, + { + displayName: 'Filter By Formula', + name: 'where', + type: 'string', + default: '', + placeholder: '(name,like,example%)~or(name,eq,test)', + description: 'A formula used to filter rows', + }, + { + displayName: 'Sort', + name: 'sort', + placeholder: 'Add Sort Rule', + description: 'The sorting rules for the returned rows', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'property', + displayName: 'Property', + values: [ + { + displayName: 'Field', + name: 'field', + type: 'string', + default: '', + description: 'Name of the field to sort on', + }, + { + displayName: 'Direction', + name: 'direction', + type: 'options', + options: [ + { + name: 'ASC', + value: 'asc', + description: 'Sort in ascending order (small -> large)', + }, + { + name: 'DESC', + value: 'desc', + description: 'Sort in descending order (large -> small)', + }, + ], + default: 'asc', + description: 'The sort direction', + }, + ], + }, + ], + }, + + ], + }, + // ---------------------------------- + // get + // ---------------------------------- + { + displayName: 'Row ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + operation: [ + 'get', + ], + }, + }, + default: '', + required: true, + description: 'ID of the row to return', + }, + { + displayName: 'Download Attachments', + name: 'downloadAttachments', + type: 'boolean', + displayOptions: { + show: { + operation: [ + 'get', + ], + }, + }, + default: false, + description: `When set to true the attachment fields define in 'Download Fields' will be downloaded.`, + }, + { + displayName: 'Download Fields', + name: 'downloadFieldNames', + type: 'string', + required: true, + displayOptions: { + show: { + operation: [ + 'get', + ], + downloadAttachments: [ + true, + ], + }, + }, + default: '', + description: `Name of the fields of type 'attachment' that should be downloaded. Multiple ones can be defined separated by comma. Case sensitive.`, + }, + // ---------------------------------- + // update + // ---------------------------------- + { + displayName: 'Row ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + operation: [ + 'update', + ], + }, + }, + default: '', + required: true, + description: 'ID of the row to update', + }, + // ---------------------------------- + // Shared + // ---------------------------------- + { + displayName: 'Data to Send', + name: 'dataToSend', + type: 'options', + options: [ + { + name: 'Auto-map Input Data to Columns', + value: 'autoMapInputData', + description: 'Use when node input properties match destination column names', + }, + { + name: 'Define Below for Each Column', + value: 'defineBelow', + description: 'Set the value for each destination column', + }, + ], + displayOptions: { + show: { + operation: [ + 'create', + 'update', + ], + }, + }, + default: 'defineBelow', + description: 'Whether to insert the input data this node receives in the new row', + }, + { + displayName: 'Inputs to Ignore', + name: 'inputsToIgnore', + type: 'string', + displayOptions: { + show: { + operation: [ + 'create', + 'update', + ], + dataToSend: [ + 'autoMapInputData', + ], + }, + }, + default: '', + required: false, + description: 'List of input properties to avoid sending, separated by commas. Leave empty to send all properties', + placeholder: 'Enter properties...', + }, + { + displayName: 'Fields to Send', + name: 'fieldsUi', + placeholder: 'Add Field', + type: 'fixedCollection', + typeOptions: { + multipleValueButtonText: 'Add Field to Send', + multipleValues: true, + }, + displayOptions: { + show: { + operation: [ + 'create', + 'update', + ], + dataToSend: [ + 'defineBelow', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Field', + name: 'fieldValues', + values: [ + { + displayName: 'Field Name', + name: 'fieldName', + type: 'string', + default: '', + }, + { + displayName: 'Is Binary Data', + name: 'binaryData', + type: 'boolean', + default: false, + description: 'If the field data to set is binary and should be taken from a binary property', + }, + { + displayName: 'Field Value', + name: 'fieldValue', + type: 'string', + default: '', + displayOptions: { + show: { + binaryData: [ + false, + ], + }, + }, + }, + { + displayName: 'Take Input From Field', + name: 'binaryProperty', + type: 'string', + description: 'The field containing the binary file data to be uploaded', + default: '', + displayOptions: { + show: { + binaryData: [ + true, + ], + }, + }, + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/NocoDB/nocodb.svg b/packages/nodes-base/nodes/NocoDB/nocodb.svg new file mode 100644 index 0000000000..42a90146ba --- /dev/null +++ b/packages/nodes-base/nodes/NocoDB/nocodb.svg @@ -0,0 +1,425 @@ + + + + diff --git a/packages/nodes-base/nodes/Pipedrive/GenericFunctions.ts b/packages/nodes-base/nodes/Pipedrive/GenericFunctions.ts index fae5a70ad7..9df80ee928 100644 --- a/packages/nodes-base/nodes/Pipedrive/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Pipedrive/GenericFunctions.ts @@ -96,7 +96,7 @@ export async function pipedriveApiRequest(this: IHookFunctions | IExecuteFunctio return { additionalData: responseData.additional_data, - data: responseData.data, + data: (responseData.data === null) ? [] : responseData.data, }; } catch (error) { throw new NodeApiError(this.getNode(), error); diff --git a/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts b/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts index 8d4e809992..44dce1e4c0 100644 --- a/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts +++ b/packages/nodes-base/nodes/Pipedrive/Pipedrive.node.ts @@ -118,6 +118,10 @@ export class Pipedrive implements INodeType { name: 'Deal', value: 'deal', }, + { + name: 'Deal Product', + value: 'dealProduct', + }, { name: 'File', value: 'file', @@ -246,6 +250,42 @@ export class Pipedrive implements INodeType { description: 'The operation to perform.', }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'dealProduct', + ], + }, + }, + options: [ + { + name: 'Add', + value: 'add', + description: 'Add a product to a deal', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all products in a deal', + }, + { + name: 'Remove', + value: 'remove', + description: 'Remove a product from a deal', + }, + { + name: 'Update', + value: 'update', + description: 'Update a product in a deal', + }, + ], + default: 'add', + }, + { displayName: 'Operation', name: 'operation', @@ -1425,6 +1465,330 @@ export class Pipedrive implements INodeType { }, ], }, + // ---------------------------------- + // dealProduct:add + // ---------------------------------- + { + displayName: 'Deal ID', + name: 'dealId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDeals', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'dealProduct', + ], + }, + }, + description: 'The ID of the deal to add a product to', + }, + { + displayName: 'Product ID', + name: 'productId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getProducts', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'dealProduct', + ], + }, + }, + description: 'The ID of the product to add to a deal', + }, + { + displayName: 'Item Price', + name: 'item_price', + type: 'number', + typeOptions: { + numberPrecision: 2, + }, + default: 0.00, + required: true, + description: 'Price at which to add or update this product in a deal', + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'dealProduct', + ], + }, + }, + }, + { + displayName: 'Quantity', + name: 'quantity', + type: 'number', + default: 1, + typeOptions: { + minValue: 1, + }, + required: true, + description: 'How many items of this product to add/update in a deal', + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'dealProduct', + ], + }, + }, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'add', + ], + resource: [ + 'dealProduct', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Comments', + name: 'comments', + type: 'string', + typeOptions: { + rows: 4, + }, + default: '', + description: 'Text to describe this product-deal attachment', + }, + { + displayName: 'Discount Percentage', + name: 'discount_percentage', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + maxValue: 100, + }, + description: 'Percentage of discount to apply', + }, + { + displayName: 'Product Variation ID', + name: 'product_variation_id', + type: 'string', + default: '', + description: 'ID of the product variation to use', + }, + ], + }, + // ---------------------------------- + // dealProduct:update + // ---------------------------------- + { + displayName: 'Deal ID', + name: 'dealId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDeals', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'dealProduct', + ], + }, + }, + description: 'The ID of the deal whose product to update', + }, + { + displayName: 'Product Attachment ID', + name: 'productAttachmentId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getProductsDeal', + loadOptionsDependsOn: [ + 'dealId', + ], + }, + required: true, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'dealProduct', + ], + }, + }, + description: 'ID of the deal-product (the ID of the product attached to the deal)', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'dealProduct', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Comments', + name: 'comments', + type: 'string', + typeOptions: { + rows: 4, + }, + default: '', + description: 'Text to describe this product-deal attachment', + }, + { + displayName: 'Discount Percentage', + name: 'discount_percentage', + type: 'number', + default: 0, + typeOptions: { + minValue: 0, + maxValue: 100, + }, + description: 'Percentage of discount to apply', + }, + { + displayName: 'Item Price', + name: 'item_price', + type: 'number', + typeOptions: { + numberPrecision: 2, + }, + default: 0.00, + required: true, + description: 'Price at which to add or update this product in a deal', + }, + { + displayName: 'Quantity', + name: 'quantity', + type: 'number', + default: 1, + typeOptions: { + minValue: 1, + }, + required: true, + description: 'How many items of this product to add/update in a deal', + }, + { + displayName: 'Product Variation ID', + name: 'product_variation_id', + type: 'string', + default: '', + description: 'ID of the product variation to use', + }, + ], + }, + // ---------------------------------- + // dealProduct:remove + // ---------------------------------- + { + displayName: 'Deal ID', + name: 'dealId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDeals', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'remove', + ], + resource: [ + 'dealProduct', + ], + }, + }, + description: 'The ID of the deal whose product to remove', + }, + { + displayName: 'Product Attachment ID', + name: 'productAttachmentId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getProductsDeal', + loadOptionsDependsOn: [ + 'dealId', + ], + }, + required: true, + displayOptions: { + show: { + operation: [ + 'remove', + ], + resource: [ + 'dealProduct', + ], + }, + }, + description: 'ID of the deal-product (the ID of the product attached to the deal)', + }, + // ---------------------------------- + // dealProduct:getAll + // ---------------------------------- + { + displayName: 'Deal ID', + name: 'dealId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDeals', + }, + required: true, + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'dealProduct', + ], + }, + }, + description: 'The ID of the deal whose products to retrieve', + }, + // ---------------------------------- // deal:search // ---------------------------------- @@ -3400,6 +3764,32 @@ export class Pipedrive implements INodeType { return returnData; }, + // Get all Deals to display them to user so that he can + // select them easily + async getDeals(this: ILoadOptionsFunctions): Promise { + const { data } = await pipedriveApiRequest.call(this, 'GET', '/deals', {}) as { + data: Array<{ id: string; title: string; }> + }; + return data.map(({ id, title }) => ({ value: id, name: title })); + }, + // Get all Products to display them to user so that he can + // select them easily + async getProducts(this: ILoadOptionsFunctions): Promise { + const { data } = await pipedriveApiRequest.call(this, 'GET', '/products', {}) as { + data: Array<{ id: string; name: string; }> + }; + return data.map(({ id, name }) => ({ value: id, name })); + }, + // Get all Products related to a deal and display them to user so that he can + // select them easily + async getProductsDeal(this: ILoadOptionsFunctions): Promise { + + const dealId = this.getCurrentNodeParameter('dealId'); + const { data } = await pipedriveApiRequest.call(this, 'GET', `/deals/${dealId}/products`, {}) as { + data: Array<{ id: string; name: string; }> + }; + return data.map(({ id, name }) => ({ value: id, name })); + }, // Get all Stages to display them to user so that he can // select them easily async getStageIds(this: ILoadOptionsFunctions): Promise { @@ -3885,12 +4275,67 @@ export class Pipedrive implements INodeType { endpoint = `/deals/search`; } + + } else if (resource === 'dealProduct') { + + if (operation === 'add') { + // ---------------------------------- + // dealProduct: add + // ---------------------------------- + + requestMethod = 'POST'; + const dealId = this.getNodeParameter('dealId', i) as string; + + endpoint = `/deals/${dealId}/products`; + + body.product_id = this.getNodeParameter('productId', i) as string; + body.item_price = this.getNodeParameter('item_price', i) as string; + body.quantity = this.getNodeParameter('quantity', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + addAdditionalFields(body, additionalFields); + + } else if (operation === 'getAll') { + // ---------------------------------- + // dealProduct: getAll + // ---------------------------------- + + requestMethod = 'GET'; + const dealId = this.getNodeParameter('dealId', i) as string; + + endpoint = `/deals/${dealId}/products`; + + } else if (operation === 'remove') { + // ---------------------------------- + // dealProduct: remove + // ---------------------------------- + + requestMethod = 'DELETE'; + const dealId = this.getNodeParameter('dealId', i) as string; + const productAttachmentId = this.getNodeParameter('productAttachmentId', i) as string; + + endpoint = `/deals/${dealId}/products/${productAttachmentId}`; + + } else if (operation === 'update') { + // ---------------------------------- + // dealProduct: update + // ---------------------------------- + + requestMethod = 'PUT'; + const dealId = this.getNodeParameter('dealId', i) as string; + const productAttachmentId = this.getNodeParameter('productAttachmentId', i) as string; + + endpoint = `/deals/${dealId}/products/${productAttachmentId}`; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + addAdditionalFields(body, updateFields); + } + } else if (resource === 'file') { if (operation === 'create') { // ---------------------------------- // file:create // ---------------------------------- - requestMethod = 'POST'; endpoint = '/files'; diff --git a/packages/nodes-base/nodes/Salesforce/AttachmentInterface.ts b/packages/nodes-base/nodes/Salesforce/AttachmentInterface.ts index e26b2a9c87..e0868cd20a 100644 --- a/packages/nodes-base/nodes/Salesforce/AttachmentInterface.ts +++ b/packages/nodes-base/nodes/Salesforce/AttachmentInterface.ts @@ -1,9 +1,9 @@ export interface IAttachment { ParentId?: string; Name?: string; - Body?: string; OwnerId?: string; IsPrivate?: boolean; ContentType?: string; Description?: string; + Body?: string; } diff --git a/packages/nodes-base/nodes/Salesforce/DocumentDescription.ts b/packages/nodes-base/nodes/Salesforce/DocumentDescription.ts new file mode 100644 index 0000000000..819b2eb036 --- /dev/null +++ b/packages/nodes-base/nodes/Salesforce/DocumentDescription.ts @@ -0,0 +1,107 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const documentOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'document', + ], + }, + }, + options: [ + { + name: 'Upload', + value: 'upload', + description: 'Upload a document', + }, + ], + default: 'upload', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const documentFields = [ + + /* -------------------------------------------------------------------------- */ + /* document:upload */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'upload', + ], + }, + }, + description: 'Name of the file', + }, + { + displayName: 'Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'upload', + ], + }, + }, + placeholder: '', + description: 'Name of the binary property which contains
the data for the file to be uploaded.', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'document', + ], + operation: [ + 'upload', + ], + }, + }, + options: [ + { + displayName: 'Link To Object ID', + name: 'linkToObjectId', + type: 'string', + default: '', + description: 'ID of the object you want to link this document to', + }, + { + displayName: 'Owner ID', + name: 'ownerId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + default: '', + description: 'ID of the owner of this document', + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts index 1200b9e703..1671e54c94 100644 --- a/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Salesforce/GenericFunctions.ts @@ -24,7 +24,6 @@ import { export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const authenticationMethod = this.getNodeParameter('authentication', 0, 'oAuth2') as string; - try { if (authenticationMethod === 'jwt') { // https://help.salesforce.com/articleView?id=remoteaccess_oauth_jwt_flow.htm&type=5 @@ -35,6 +34,7 @@ export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSin const options = getOptions.call(this, method, (uri || endpoint), body, qs, instance_url as string); Logger.debug(`Authentication for "Salesforce" node is using "jwt". Invoking URI ${options.uri}`); options.headers!.Authorization = `Bearer ${access_token}`; + Object.assign(options, option); //@ts-ignore return await this.helpers.request(options); } else { @@ -43,6 +43,7 @@ export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSin const credentials = this.getCredentials(credentialsType) as { oauthTokenData: { instance_url: string } }; const options = getOptions.call(this, method, (uri || endpoint), body, qs, credentials.oauthTokenData.instance_url); Logger.debug(`Authentication for "Salesforce" node is using "OAuth2". Invoking URI ${options.uri}`); + Object.assign(options, option); //@ts-ignore return await this.helpers.requestOAuth2.call(this, credentialsType, options); } @@ -90,12 +91,16 @@ function getOptions(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOpt 'Content-Type': 'application/json', }, method, - body: method === 'GET' ? undefined : body, + body, qs, uri: `${instanceUrl}/services/data/v39.0${endpoint}`, json: true, }; + if (!Object.keys(options.body).length) { + delete options.body; + } + //@ts-ignore return options; } diff --git a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts index f9b41623bc..b91ad6cb16 100644 --- a/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts +++ b/packages/nodes-base/nodes/Salesforce/Salesforce.node.ts @@ -1,4 +1,5 @@ import { + BINARY_ENCODING, IExecuteFunctions, } from 'n8n-core'; @@ -112,6 +113,11 @@ import { userOperations, } from './UserDescription'; +import { + documentFields, + documentOperations, +} from './DocumentDescription'; + import { LoggerProxy as Logger, } from 'n8n-workflow'; @@ -203,6 +209,11 @@ export class Salesforce implements INodeType { value: 'customObject', description: 'Represents a custom object.', }, + { + name: 'Document', + value: 'document', + description: 'Represents a document.', + }, { name: 'Flow', value: 'flow', @@ -243,6 +254,8 @@ export class Salesforce implements INodeType { ...contactFields, ...customObjectOperations, ...customObjectFields, + ...documentOperations, + ...documentFields, ...opportunityOperations, ...opportunityFields, ...accountOperations, @@ -936,6 +949,27 @@ export class Salesforce implements INodeType { sortOptions(returnData); return returnData; }, + // // Get all folders to display them to user so that he can + // // select them easily + // async getFolders(this: ILoadOptionsFunctions): Promise { + // const returnData: INodePropertyOptions[] = []; + // const fields = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/sobjects/folder/describe'); + // console.log(JSON.stringify(fields, undefined, 2)) + // const qs = { + // //ContentFolderItem ContentWorkspace ContentFolder + // q: `SELECT Id, Title FROM ContentVersion`, + // //q: `SELECT Id FROM Folder where Type = 'Document'`, + + // }; + // const folders = await salesforceApiRequestAllItems.call(this, 'records', 'GET', '/query', {}, qs); + // for (const folder of folders) { + // returnData.push({ + // name: folder.Name, + // value: folder.Id, + // }); + // } + // return returnData; + // }, }, }; @@ -1588,6 +1622,49 @@ export class Salesforce implements INodeType { } } } + if (resource === 'document') { + //https://developer.salesforce.com/docs/atlas.en-us.206.0.api_rest.meta/api_rest/dome_sobject_insert_update_blob.htm + if (operation === 'upload') { + const title = this.getNodeParameter('title', i) as string; + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + let data; + const body: { entity_content: { [key: string]: string } } = { + entity_content: { + Title: title, + ContentLocation: 'S', + }, + }; + if (additionalFields.ownerId) { + body.entity_content['ownerId'] = additionalFields.ownerId as string; + } + if (additionalFields.linkToObjectId) { + body.entity_content['FirstPublishLocationId'] = additionalFields.linkToObjectId as string; + } + if (items[i].binary && items[i].binary![binaryPropertyName]) { + const binaryData = items[i].binary![binaryPropertyName]; + body.entity_content['PathOnClient'] = `${title}.${binaryData.fileExtension}`; + data = { + entity_content: { + value: JSON.stringify(body.entity_content), + options: { + contentType: 'application/json', + }, + }, + VersionData: { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + contentType: binaryData.mimeType, + }, + }, + }; + } else { + throw new NodeOperationError(this.getNode(), `The property ${binaryPropertyName} does not exist`); + } + responseData = await salesforceApiRequest.call(this, 'POST', '/sobjects/ContentVersion', {}, {}, undefined, { formData: data }); + } + } if (resource === 'opportunity') { //https://developer.salesforce.com/docs/api-explorer/sobject/Opportunity/post-opportunity if (operation === 'create' || operation === 'upsert') { diff --git a/packages/nodes-base/nodes/ServiceNow/ServiceNow.node.json b/packages/nodes-base/nodes/ServiceNow/ServiceNow.node.json new file mode 100644 index 0000000000..84e6b75a81 --- /dev/null +++ b/packages/nodes-base/nodes/ServiceNow/ServiceNow.node.json @@ -0,0 +1,21 @@ +{ + "node": "n8n-nodes-base.serviceNow", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Productivity", + "Communication" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/serviceNow" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.serviceNow/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Stripe/Stripe.node.json b/packages/nodes-base/nodes/Stripe/Stripe.node.json new file mode 100644 index 0000000000..a8129f5a09 --- /dev/null +++ b/packages/nodes-base/nodes/Stripe/Stripe.node.json @@ -0,0 +1,21 @@ +{ + "node": "n8n-nodes-base.stripe", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": [ + "Finance & Accounting", + "Sales" + ], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/credentials/stripe" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/nodes/n8n-nodes-base.stripe/" + } + ] + } +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Taiga/types.d.ts b/packages/nodes-base/nodes/Taiga/types.d.ts index 49c5f0e0f0..4a136dc822 100644 --- a/packages/nodes-base/nodes/Taiga/types.d.ts +++ b/packages/nodes-base/nodes/Taiga/types.d.ts @@ -7,6 +7,11 @@ type LoadedResource = { name: string; }; +type LoadOption = { + value: string; + name: string; +}; + type LoadedUser = { id: string; full_name_display: string; diff --git a/packages/nodes-base/nodes/WooCommerce/GenericFunctions.ts b/packages/nodes-base/nodes/WooCommerce/GenericFunctions.ts index 3e42c88505..7be0c77221 100644 --- a/packages/nodes-base/nodes/WooCommerce/GenericFunctions.ts +++ b/packages/nodes-base/nodes/WooCommerce/GenericFunctions.ts @@ -32,6 +32,10 @@ import { snakeCase, } from 'change-case'; +import { + omit +} from 'lodash'; + export async function woocommerceApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any const credentials = this.getCredentials('wooCommerceApi'); if (credentials === undefined) { @@ -144,3 +148,18 @@ export function toSnakeCase(data: } } } + +export function adjustMetadata(fields: IDataObject & Metadata) { + if (!fields.meta_data) return fields; + + return { + ...omit(fields, ['meta_data']), + meta_data: fields.meta_data.meta_data_fields, + }; +} + +type Metadata = { + meta_data?: { + meta_data_fields: Array<{ key: string; value: string }>; + } +}; diff --git a/packages/nodes-base/nodes/WooCommerce/WooCommerce.node.ts b/packages/nodes-base/nodes/WooCommerce/WooCommerce.node.ts index 025318bdcf..9b7361402b 100644 --- a/packages/nodes-base/nodes/WooCommerce/WooCommerce.node.ts +++ b/packages/nodes-base/nodes/WooCommerce/WooCommerce.node.ts @@ -10,6 +10,7 @@ import { INodeTypeDescription, } from 'n8n-workflow'; import { + adjustMetadata, setMetadata, toSnakeCase, woocommerceApiRequest, @@ -37,11 +38,16 @@ import { IShoppingLine, } from './OrderInterface'; +import { + customerFields, + customerOperations, +} from './descriptions'; + export class WooCommerce implements INodeType { description: INodeTypeDescription = { displayName: 'WooCommerce', name: 'wooCommerce', - icon: 'file:wooCommerce.png', + icon: 'file:wooCommerce.svg', group: ['output'], version: 1, subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', @@ -64,6 +70,10 @@ export class WooCommerce implements INodeType { name: 'resource', type: 'options', options: [ + { + name: 'Customer', + value: 'customer', + }, { name: 'Order', value: 'order', @@ -76,6 +86,8 @@ export class WooCommerce implements INodeType { default: 'product', description: 'Resource to consume.', }, + ...customerOperations, + ...customerFields, ...productOperations, ...productFields, ...orderOperations, @@ -128,7 +140,111 @@ export class WooCommerce implements INodeType { const operation = this.getNodeParameter('operation', 0) as string; for (let i = 0; i < length; i++) { - if (resource === 'product') { + + if (resource === 'customer') { + + // ********************************************************************** + // customer + // ********************************************************************** + + // https://woocommerce.github.io/woocommerce-rest-api-docs/?shell#customer-properties + + if (operation === 'create') { + + // ---------------------------------------- + // customer: create + // ---------------------------------------- + + // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#create-a-customer + + const body = { + email: this.getNodeParameter('email', i), + } as IDataObject; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + if (Object.keys(additionalFields).length) { + Object.assign(body, adjustMetadata(additionalFields)); + } + + responseData = await woocommerceApiRequest.call(this, 'POST', '/customers', body); + + } else if (operation === 'delete') { + + // ---------------------------------------- + // customer: delete + // ---------------------------------------- + + // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#delete-a-customer + + const customerId = this.getNodeParameter('customerId', i); + + const qs: IDataObject = { + force: true, // required, customers do not support trashing + }; + + const endpoint = `/customers/${customerId}`; + responseData = await woocommerceApiRequest.call(this, 'DELETE', endpoint, {}, qs); + + } else if (operation === 'get') { + + // ---------------------------------------- + // customer: get + // ---------------------------------------- + + // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#retrieve-a-customer + + const customerId = this.getNodeParameter('customerId', i); + + const endpoint = `/customers/${customerId}`; + responseData = await woocommerceApiRequest.call(this, 'GET', endpoint); + + } else if (operation === 'getAll') { + + // ---------------------------------------- + // customer: getAll + // ---------------------------------------- + + // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#list-all-customers + + const qs = {} as IDataObject; + const filters = this.getNodeParameter('filters', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (Object.keys(filters).length) { + Object.assign(qs, filters); + } + + if (returnAll) { + responseData = await woocommerceApiRequestAllItems.call(this, 'GET', '/customers', {}, qs); + } else { + qs.per_page = this.getNodeParameter('limit', i) as number; + responseData = await woocommerceApiRequest.call(this, 'GET', '/customers', {}, qs); + } + + } else if (operation === 'update') { + + // ---------------------------------------- + // customer: update + // ---------------------------------------- + + // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#update-a-customer + + const body = {} as IDataObject; + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + if (Object.keys(updateFields).length) { + Object.assign(body, adjustMetadata(updateFields)); + } + + const customerId = this.getNodeParameter('customerId', i); + + const endpoint = `/customers/${customerId}`; + responseData = await woocommerceApiRequest.call(this, 'PUT', endpoint, body); + + } + + } else if (resource === 'product') { //https://woocommerce.github.io/woocommerce-rest-api-docs/#create-a-product if (operation === 'create') { const name = this.getNodeParameter('name', i) as string; diff --git a/packages/nodes-base/nodes/WooCommerce/WooCommerceTrigger.node.ts b/packages/nodes-base/nodes/WooCommerce/WooCommerceTrigger.node.ts index 1bea61ff6c..6c4deb2e80 100644 --- a/packages/nodes-base/nodes/WooCommerce/WooCommerceTrigger.node.ts +++ b/packages/nodes-base/nodes/WooCommerce/WooCommerceTrigger.node.ts @@ -23,7 +23,7 @@ export class WooCommerceTrigger implements INodeType { description: INodeTypeDescription = { displayName: 'WooCommerce Trigger', name: 'wooCommerceTrigger', - icon: 'file:wooCommerce.png', + icon: 'file:wooCommerce.svg', group: ['trigger'], version: 1, description: 'Handle WooCommerce events via webhooks', @@ -118,7 +118,7 @@ export class WooCommerceTrigger implements INodeType { const webhookData = this.getWorkflowStaticData('node'); const currentEvent = this.getNodeParameter('event') as string; const endpoint = `/webhooks`; - + const webhooks = await woocommerceApiRequest.call(this, 'GET', endpoint, {}, { status: 'active', per_page: 100 }); for (const webhook of webhooks) { @@ -185,4 +185,4 @@ export class WooCommerceTrigger implements INodeType { ], }; } -} \ No newline at end of file +} diff --git a/packages/nodes-base/nodes/WooCommerce/descriptions/CustomerDescription.ts b/packages/nodes-base/nodes/WooCommerce/descriptions/CustomerDescription.ts new file mode 100644 index 0000000000..4a52553fea --- /dev/null +++ b/packages/nodes-base/nodes/WooCommerce/descriptions/CustomerDescription.ts @@ -0,0 +1,254 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + customerCreateFields, + customerUpdateFields, +} from './shared'; + +export const customerOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'customer', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a customer', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a customer', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve a customer', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Retrieve all customers', + }, + { + name: 'Update', + value: 'update', + description: 'Update a customer', + }, + ], + default: 'create', + }, +] as INodeProperties[]; + +export const customerFields = [ + // ---------------------------------------- + // customer: create + // ---------------------------------------- + { + displayName: 'Email', + name: 'email', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'create', + ], + }, + }, + }, + customerCreateFields, + + // ---------------------------------------- + // customer: delete + // ---------------------------------------- + { + displayName: 'Customer ID', + name: 'customerId', + description: 'ID of the customer to delete', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'delete', + ], + }, + }, + }, + + // ---------------------------------------- + // customer: get + // ---------------------------------------- + { + displayName: 'Customer ID', + name: 'customerId', + description: 'ID of the customer to retrieve', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'get', + ], + }, + }, + }, + + // ---------------------------------------- + // customer: getAll + // ---------------------------------------- + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + description: 'How many results to return', + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + description: 'Email address to filter customers by', + }, + { + displayName: 'Sort Order', + name: 'order', + description: 'Order to sort customers in', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'asc', + }, + { + name: 'Descending', + value: 'desc', + }, + ], + default: 'asc', + }, + { + displayName: 'Order By', + name: 'orderby', + description: 'Field to sort customers by', + type: 'options', + options: [ + { + name: 'ID', + value: 'id', + }, + { + name: 'Include', + value: 'include', + }, + { + name: 'Name', + value: 'name', + }, + { + name: 'Registered Date', + value: 'registered_date', + }, + ], + default: 'id', + }, + ], + }, + + // ---------------------------------------- + // customer: update + // ---------------------------------------- + { + displayName: 'Customer ID', + name: 'customerId', + description: 'ID of the customer to update', + type: 'string', + required: true, + default: '', + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'update', + ], + }, + }, + }, + customerUpdateFields, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/WooCommerce/descriptions/index.ts b/packages/nodes-base/nodes/WooCommerce/descriptions/index.ts new file mode 100644 index 0000000000..184b55e69e --- /dev/null +++ b/packages/nodes-base/nodes/WooCommerce/descriptions/index.ts @@ -0,0 +1 @@ +export * from './CustomerDescription'; diff --git a/packages/nodes-base/nodes/WooCommerce/descriptions/shared.ts b/packages/nodes-base/nodes/WooCommerce/descriptions/shared.ts new file mode 100644 index 0000000000..14e3fa1f94 --- /dev/null +++ b/packages/nodes-base/nodes/WooCommerce/descriptions/shared.ts @@ -0,0 +1,177 @@ +const customerAddressOptions = [ + { + displayName: 'First Name', + name: 'first_name', + type: 'string', + default: '', + }, + { + displayName: 'Last Name', + name: 'last_name', + type: 'string', + default: '', + }, + { + displayName: 'Company', + name: 'company', + type: 'string', + default: '', + }, + { + displayName: 'Address 1', + name: 'address_1', + type: 'string', + default: '', + }, + { + displayName: 'Address 2', + name: 'address_2', + type: 'string', + default: '', + }, + { + displayName: 'City', + name: 'city', + type: 'string', + default: '', + }, + { + displayName: 'State', + name: 'state', + type: 'string', + default: '', + }, + { + displayName: 'Postcode', + name: 'postcode', + type: 'string', + default: '', + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + }, + { + displayName: 'Phone', + name: 'phone', + type: 'string', + default: '', + }, +]; + +const customerUpdateOptions = [ + { + displayName: 'Billing Address', + name: 'billing', + type: 'collection', + default: {}, + placeholder: 'Add Field', + options: customerAddressOptions, + }, + { + displayName: 'First Name', + name: 'first_name', + type: 'string', + default: '', + }, + { + displayName: 'Last Name', + name: 'last_name', + type: 'string', + default: '', + }, + { + displayName: 'Metadata', + name: 'meta_data', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + placeholder: 'Add Metadata Field', + options: [ + { + displayName: 'Metadata Fields', + name: 'meta_data_fields', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Shipping Address', + name: 'shipping', + type: 'collection', + default: {}, + placeholder: 'Add Field', + options: customerAddressOptions, + }, +]; + +const customerCreateOptions = [ + ...customerUpdateOptions, + { + displayName: 'Username', + name: 'username', + type: 'string', + default: '', + }, +]; + +export const customerCreateFields = { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'create', + ], + }, + }, + options: customerCreateOptions, +}; + +export const customerUpdateFields = { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'customer', + ], + operation: [ + 'update', + ], + }, + }, + options: customerUpdateOptions, +}; diff --git a/packages/nodes-base/nodes/WooCommerce/wooCommerce.png b/packages/nodes-base/nodes/WooCommerce/wooCommerce.png deleted file mode 100644 index 187de2a2f0..0000000000 Binary files a/packages/nodes-base/nodes/WooCommerce/wooCommerce.png and /dev/null differ diff --git a/packages/nodes-base/nodes/WooCommerce/wooCommerce.svg b/packages/nodes-base/nodes/WooCommerce/wooCommerce.svg new file mode 100644 index 0000000000..9cde2a9d41 --- /dev/null +++ b/packages/nodes-base/nodes/WooCommerce/wooCommerce.svg @@ -0,0 +1,14 @@ + +WooCommerce Logo + + + +image/svg+xml + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index a27bb6eeda..c5a34fb047 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.128.0", + "version": "0.129.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -85,6 +85,7 @@ "dist/credentials/FacebookGraphApi.credentials.js", "dist/credentials/FacebookGraphAppApi.credentials.js", "dist/credentials/FreshdeskApi.credentials.js", + "dist/credentials/FreshworksCrmApi.credentials.js", "dist/credentials/FileMaker.credentials.js", "dist/credentials/FlowApi.credentials.js", "dist/credentials/Ftp.credentials.js", @@ -110,6 +111,7 @@ "dist/credentials/GoogleFirebaseCloudFirestoreOAuth2Api.credentials.js", "dist/credentials/GoogleFirebaseRealtimeDatabaseOAuth2Api.credentials.js", "dist/credentials/GoogleOAuth2Api.credentials.js", + "dist/credentials/GooglePerspectiveOAuth2Api.credentials.js", "dist/credentials/GoogleSheetsOAuth2Api.credentials.js", "dist/credentials/GoogleSlidesOAuth2Api.credentials.js", "dist/credentials/GSuiteAdminOAuth2Api.credentials.js", @@ -153,6 +155,7 @@ "dist/credentials/MailjetEmailApi.credentials.js", "dist/credentials/MailjetSmsApi.credentials.js", "dist/credentials/MandrillApi.credentials.js", + "dist/credentials/MarketstackApi.credentials.js", "dist/credentials/MatrixApi.credentials.js", "dist/credentials/MattermostApi.credentials.js", "dist/credentials/MauticApi.credentials.js", @@ -179,6 +182,7 @@ "dist/credentials/NasaApi.credentials.js", "dist/credentials/NextCloudApi.credentials.js", "dist/credentials/NextCloudOAuth2Api.credentials.js", + "dist/credentials/NocoDb.credentials.js", "dist/credentials/NotionApi.credentials.js", "dist/credentials/NotionOAuth2Api.credentials.js", "dist/credentials/OAuth1Api.credentials.js", @@ -375,6 +379,7 @@ "dist/nodes/FileMaker/FileMaker.node.js", "dist/nodes/Ftp.node.js", "dist/nodes/Freshdesk/Freshdesk.node.js", + "dist/nodes/FreshworksCrm/FreshworksCrm.node.js", "dist/nodes/Flow/Flow.node.js", "dist/nodes/Flow/FlowTrigger.node.js", "dist/nodes/Function.node.js", @@ -399,6 +404,7 @@ "dist/nodes/Google/Firebase/RealtimeDatabase/RealtimeDatabase.node.js", "dist/nodes/Google/Gmail/Gmail.node.js", "dist/nodes/Google/GSuiteAdmin/GSuiteAdmin.node.js", + "dist/nodes/Google/Perspective/GooglePerspective.node.js", "dist/nodes/Google/Sheet/GoogleSheets.node.js", "dist/nodes/Google/Slides/GoogleSlides.node.js", "dist/nodes/Google/Task/GoogleTasks.node.js", @@ -448,6 +454,7 @@ "dist/nodes/Mailjet/Mailjet.node.js", "dist/nodes/Mailjet/MailjetTrigger.node.js", "dist/nodes/Mandrill/Mandrill.node.js", + "dist/nodes/Marketstack/Marketstack.node.js", "dist/nodes/Matrix/Matrix.node.js", "dist/nodes/Mattermost/Mattermost.node.js", "dist/nodes/Mautic/Mautic.node.js", @@ -475,6 +482,7 @@ "dist/nodes/Nasa/Nasa.node.js", "dist/nodes/NextCloud/NextCloud.node.js", "dist/nodes/NoOp.node.js", + "dist/nodes/NocoDB/NocoDB.node.js", "dist/nodes/Notion/Notion.node.js", "dist/nodes/Notion/NotionTrigger.node.js", "dist/nodes/N8nTrainingCustomerDatastore.node.js",