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 @@
+
+
+
+
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 @@
+
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",