Merge branch 'master' of github.com:n8n-io/n8n into n8n-2283-implement-design-system

This commit is contained in:
Mutasem 2021-08-02 13:51:47 +02:00
commit ec079abd01
64 changed files with 9399 additions and 22 deletions

View file

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

View file

@ -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 <code>https://n8n-org.myfreshworks.com</code>, the domain is <code>n8n-org</code>.',
},
];
}

View file

@ -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(' '),
},
];
}

View file

@ -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).',
},
];
}

View file

@ -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',
},
];
}

View file

@ -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/"
}
]
}
}

View file

@ -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/"
}
]
}
}

View file

@ -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/"
}
]
}
}

View file

@ -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/"
}
]
}
}

View file

@ -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/"
}
]
}
}

View file

@ -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<INodePropertyOptions[]> {
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) {

View file

@ -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<any> { // 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);
}

View file

@ -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<LoadedCurrency>;
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<LoadedUser>;
const key = Object.keys(response)[0];
return response[key].map(
({ display_name, id }) => ({ name: display_name, value: id }),
);
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
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)];
}
}

View file

@ -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<LoadedResource>;
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.`,
);
}

View file

@ -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[];

View file

@ -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[];

View file

@ -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[];

View file

@ -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[];

View file

@ -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[];

View file

@ -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[];

View file

@ -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[];

View file

@ -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';

View file

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="layer"
viewBox="0 0 102.6 102.6"
xml:space="preserve"
sodipodi:docname="freshworksCrm.svg"
width="102.6"
height="102.6"
inkscape:version="1.1 (c4e8f9e, 2021-05-24)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview156"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="1.7642045"
inkscape:cx="166.36393"
inkscape:cy="122.15137"
inkscape:window-width="1312"
inkscape:window-height="847"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="layer" /><defs
id="defs173" />
<style
type="text/css"
id="style108">
.st0{fill:#4D4D4D;}
.st1{fill:#9B65C3;}
.st2{fill:#BE63C5;}
.st3{fill:#BF63C6;}
.st4{fill:#3278B1;}
.st5{fill:#3278B2;}
.st6{fill:#45A4EC;}
.st7{fill:#19BB7D;}
.st8{fill:#08C7FB;}
.st9{fill:#59F2F6;}
.st10{fill:#FFA700;}
.st11{fill:#DA3757;}
.st12{fill:#D33C4E;}
.st13{fill:#EE5A24;}
.st14{fill:#8BDF55;}
.st15{fill:#25C16F;}
.st16{fill:#FFBB00;}
.st17{fill:#FFA800;}
</style>
<g
id="g168"
transform="translate(-21.6,-279.3)">
<path
class="st1"
d="m 22.2,338.2 c 1.7,11.1 6.9,21.1 14.4,28.6 l 17.5,-17.5 c 0,-17.7 9.2,-34.9 25.6,-44.4 z"
id="path130" />
<path
class="st1"
d="m 54.1,349.3 c -8.7,0 -17.5,-2.2 -25.6,-6.9 -2.2,-1.3 -4.3,-2.7 -6.3,-4.3 1.7,11.1 6.9,21.1 14.4,28.6 z"
id="path132" />
<path
class="st2"
d="m 54.1,349.3 v 0 l -17.5,17.5 c 7.6,7.6 17.5,12.8 28.6,14.4 L 98.4,323.7 C 89,340.1 71.8,349.3 54.1,349.3"
id="path134" />
<path
class="st3"
d="m 61,375 c -4.7,-8.1 -6.9,-16.9 -6.9,-25.6 l -17.5,17.5 c 7.6,7.6 17.5,12.8 28.6,14.4 -1.5,-2 -2.9,-4.1 -4.2,-6.3"
id="path136" />
<path
class="st4"
d="M 119.3,282.1 79.8,305 c -16.4,9.5 -35.9,8.8 -51.2,0 -4.4,7.5 -6.9,16.3 -6.9,25.6 0,2.6 0.2,5.1 0.6,7.6 2,1.6 4.1,3 6.3,4.3 8.1,4.7 16.9,6.9 25.6,6.9 l 66.2,-66.2 c -0.4,-0.4 -0.7,-0.8 -1.1,-1.1"
id="path138" />
<path
class="st5"
d="m 28.5,342.5 c 8.1,4.7 16.9,6.9 25.6,6.9 0,-17.7 9.2,-34.9 25.6,-44.4 l -57.6,33.2 c 2.1,1.5 4.2,3 6.4,4.3"
id="path140" />
<path
class="st6"
d="m 28.5,305 c -4.4,7.5 -6.9,16.3 -6.9,25.6 0,2.6 0.2,5.1 0.6,7.6 L 79.8,305 c -16.5,9.4 -36,8.8 -51.3,0"
id="path142" />
<path
class="st7"
d="m 120.4,283.1 c -0.3,-0.3 -0.7,-0.7 -1.1,-1 -0.5,-0.4 -1,-0.8 -1.6,-1.1 -1.9,-1.1 -4.1,-1.7 -6.4,-1.7 H 72.9 c -19,0 -35.5,10.3 -44.4,25.6 15.3,8.8 34.8,9.5 51.2,0 -16.4,9.5 -25.6,26.7 -25.6,44.4 17.7,0 34.9,-9.2 44.4,-25.6 l 22.8,-39.5 c -0.3,-0.4 -0.6,-0.8 -0.9,-1.1"
id="path144" />
<path
class="st8"
d="m 79.8,305 39.5,-22.8 c -0.5,-0.4 -1,-0.8 -1.6,-1.1 L 28.5,305 c 15.3,8.8 34.8,9.4 51.3,0"
id="path146" />
<path
class="st9"
d="M 111.3,279.3 H 72.9 c -19,0 -35.5,10.3 -44.4,25.6 L 117.7,281 c -1.9,-1 -4,-1.7 -6.4,-1.7"
id="path148" />
<path
class="st10"
d="M 54.1,349.3"
id="path150" />
<path
class="st11"
d="m 98.5,323.7 22.8,-39.5 c -0.3,-0.4 -0.6,-0.7 -1,-1.1 l -66.2,66.2 c 0,8.7 2.2,17.5 6.9,25.6 1.3,2.2 2.7,4.3 4.3,6.3 2.5,0.4 5,0.6 7.6,0.6 9.3,0 18.1,-2.5 25.6,-6.9 -8.8,-15.3 -9.5,-34.8 0,-51.2"
id="path152" />
<path
class="st12"
d="m 54.1,349.3 v 0 c 0,8.7 2.2,17.5 6.9,25.6 1.3,2.2 2.7,4.3 4.3,6.3 L 98.5,323.7 C 89,340.1 71.8,349.3 54.1,349.3"
id="path154" />
<path
class="st13"
d="m 98.5,323.7 -33.2,57.6 c 2.5,0.4 5,0.6 7.6,0.6 9.3,0 18.1,-2.5 25.6,-6.9 -8.8,-15.4 -9.5,-34.9 0,-51.3"
id="path156" />
<path
class="st14"
d="m 122.4,285.8 c -0.3,-0.6 -0.7,-1.1 -1.1,-1.6 -0.3,-0.4 -0.6,-0.7 -1,-1.1 -0.3,-0.3 -0.7,-0.7 -1.1,-1 L 79.8,305 c -16.4,9.5 -25.6,26.7 -25.6,44.4 17.7,0 34.9,-9.2 44.4,-25.6 -9.5,16.4 -8.8,35.9 0,51.2 15.3,-8.9 25.6,-25.4 25.6,-44.4 v -38.4 c -0.1,-2.4 -0.7,-4.6 -1.8,-6.4"
id="path158" />
<path
class="st15"
d="M 119.3,282.1 79.8,305 c -16.4,9.5 -25.6,26.7 -25.6,44.4 l 66.2,-66.2 c -0.4,-0.4 -0.7,-0.8 -1.1,-1.1"
id="path160" />
<path
class="st14"
d="m 54.1,349.3 c 17.7,0 34.9,-9.2 44.4,-25.6 l 22.8,-39.5 c -0.3,-0.4 -0.6,-0.7 -1,-1.1 z"
id="path162" />
<path
class="st16"
d="m 121.3,284.2 -22.8,39.5 c -9.5,16.4 -8.8,35.9 0,51.2 l 23.9,-89.2 c -0.3,-0.5 -0.7,-1 -1.1,-1.5"
id="path164" />
<path
class="st17"
d="m 98.5,375 c 15.3,-8.9 25.6,-25.4 25.6,-44.4 v -38.4 c 0,-2.3 -0.6,-4.5 -1.7,-6.4 z"
id="path166" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -0,0 +1,43 @@
export type FreshworksCrmApiCredentials = {
apiKey: string;
domain: string;
}
export type FreshworksConfigResponse<T> = {
[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[];
};

View file

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

View file

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

View file

@ -277,6 +277,16 @@ export const messageFields = [
placeholder: 'info@example.com',
default: [],
},
{
displayName: 'Sender Name',
name: 'senderName',
type: 'string',
placeholder: 'Name <test@gmail.com>',
default: '',
description: `The name displayed in your contacts inboxes.</br>
It has to be in the format: "Display-Name &#60;name@gmail.com&#62;".</br>
The email address has to match the email address of the logged in user for the API`,
},
],
},
{

View file

@ -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);
}
}

View file

@ -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/"
}
]
}
}

View file

@ -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 <a target="_blank" href="https://developers.perspectiveapi.com/s/about-the-api-attributes-and-languages">here</a>',
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<INodePropertyOptions[]> {
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<INodeExecutionData[][]> {
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<RequestedAttributes>((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)];
}
}

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="6 10 125 125"
version="1.1"
id="svg4"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs8">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath937">
<rect
style="fill:#0000ff;fill-rule:evenodd"
id="rect939"
width="123.4947"
height="119.02773"
x="12.25"
y="12.482271" />
</clipPath>
</defs>
<g
id="g532"
clip-path="url(#clipPath937)">
<path
d="M131.53 72a59.79 59.79 0 0 1-53.81 59.49v-119A59.79 59.79 0 0 1 131.53 72zM12.25 78v53.51h53.52A59.81 59.81 0 0 0 12.25 78zm0-12h53.52V12.51A59.81 59.81 0 0 0 12.25 66zm176.83-37.06H223c21.77 0 35.8 11.32 35.8 29.65s-14 29.64-35.8 29.64h-15.22v26.82h-18.7zm33.34 44c11.81 0 17.34-4.55 17.34-14.39s-5.53-14.4-17.34-14.4h-14.64V73zm37.14 11.72v-.61c0-19.93 12.91-33 32-33 18 0 31 12.67 31 33.46v4.18h-44.9c1 9.72 6 14.27 14.14 14.27 5.91 0 10.09-2.58 11.94-7h18.2c-2.7 12.42-14.64 21-30.26 21-19.82.04-32.12-11.96-32.12-32.3zm18.57-7.87h26.2c-1.72-8.12-6.27-12.18-12.91-12.18s-11.69 3.94-13.29 12.18zm54.73-23.74h17.59v9.47C354.26 55.88 361 52.31 370 52.31h3.56v17h-15.5c-4.67 0-7.13 2.46-7.13 7v38.75h-18.07zM378.12 96H395c.62 4.92 4.55 7.5 13.29 7.5 7.38 0 10.94-2.33 10.94-5.65 0-2.71-1.72-3.94-6.15-4.8l-14.79-2.85c-13.65-2.46-19.06-8.12-19.06-18.08 0-12.3 10-21 27.92-21 16.73 0 28.17 8.37 29 20.54h-16.82c-.74-4.42-4.8-7-12.3-7-7 0-10.46 2.22-10.46 5.54 0 2.58 1.72 3.81 6 4.67l14.88 2.83c13.53 2.46 18.94 8.12 18.94 18.21 0 12.91-10.45 21.15-28.29 21.15-18.79-.06-29.37-8.78-29.98-21.06zm68.13-42.95h17.59v9.84c3.7-7.38 10.71-11.56 19.56-11.56 14.89 0 26.33 11.68 26.33 32v.62c0 20.17-12.06 32.84-26.94 32.84-8.12 0-14.76-3.32-18.45-9.47v30.63h-18.09zM491 84.42v-1.6c0-11.32-4.92-17.35-13.16-17.35-8.61 0-13.53 5.91-13.53 17.23v2.7c0 12.18 5 17.22 13.16 17.22 7.9 0 13.53-6.62 13.53-18.2zm26.1.24v-.61c0-19.93 12.92-33 32-33 18 0 31 12.67 31 33.46v4.18h-44.9c1 9.72 6 14.27 14.15 14.27 5.9 0 10.08-2.58 11.93-7h18.2c-2.7 12.42-14.63 21-30.25 21-19.83.04-32.13-11.96-32.13-32.3zm18.57-7.87h26.21c-1.73-8.12-6.28-12.18-12.92-12.18s-11.69 3.94-13.29 12.18zm51.53 7.75v-.74c0-19.31 13.16-32.72 31.37-32.72 16.24 0 27.8 9.35 29.4 24.6h-17.71c-.74-6.64-5.42-10.45-11.44-10.45-7.88 0-13 6.52-13 18.2v1.48c0 11.81 5.16 18 12.79 18 6.52 0 10.95-4.06 11.81-11.2h17.71c-1.6 15.75-13 25.34-29.77 25.34C600 117 587.2 104.34 587.2 84.54zm76.01 9.35V67.32h-10.7V53.05h10.7v-18h18v18h18.21v14.27h-18.25v30.75c0 1.72.74 2.46 2.46 2.46h15.75v14.52H685.6c-14.52 0-22.39-7.51-22.39-21.16zm46-58.31a10.09 10.09 0 0 1 20.17 0 10.09 10.09 0 1 1-20.17 0zm1 17.47h18.08v62h-18.1zm25.19 0H754l14.39 45.27 14.27-45.27h17.71l-20.91 62h-23.15zm68.88 31.61v-.61c0-19.93 12.92-33 32-33 18 0 31 12.67 31 33.46v4.18h-44.9c1 9.72 6 14.27 14.15 14.27 5.9 0 10.09-2.58 11.93-7h18.21c-2.71 12.42-14.64 21-30.26 21-19.83.04-32.13-11.96-32.13-32.3zm18.57-7.87h26.21c-1.73-8.12-6.28-12.18-12.92-12.18s-11.69 3.94-13.29 12.18z"
fill="#451735"
id="path2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -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;
};

View file

@ -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);

View file

@ -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).',
);
}
}

View file

@ -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<INodeExecutionData[][]> {
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)];
}
}

View file

@ -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. <code>AAPL</code> or <code>AAPL,MSFT</code>',
},
{
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 <a target="_blank" href="https://en.wikipedia.org/wiki/Market_Identifier_Code">Market Identifier Code</a>, e.g. <code>XNAS</code>',
},
{
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. <code>2020-01-01</code>, or in ISO-8601 date format, e.g. <code>2020-05-21T00:00:00+0000</code>',
},
{
displayName: 'Timeframe Start Date',
name: 'dateFrom',
type: 'dateTime',
default: '',
description: 'Timeframe start date in YYYY-MM-DD format, e.g. <code>2020-01-01</code>, or in ISO-8601 date format, e.g. <code>2020-05-21T00:00:00+0000</code>',
},
{
displayName: 'Timeframe End Date',
name: 'dateTo',
type: 'dateTime',
default: '',
description: 'Timeframe end date in YYYY-MM-DD format, e.g. <code>2020-01-01</code>, or in ISO-8601 date format, e.g. <code>2020-05-21T00:00:00+0000</code>',
},
],
},
];

View file

@ -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 <a target="_blank" href="https://en.wikipedia.org/wiki/Market_Identifier_Code">Market Identifier Code</a>, e.g. <code>XNAS</code>',
},
];

View file

@ -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. <code>AAPL</code>',
},
];

View file

@ -0,0 +1,3 @@
export * from './EndOfDayDataDescription';
export * from './TickerDescription';
export * from './ExchangeDescription';

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="-2457.7 882.5 59.999998 60"
xml:space="preserve"
sodipodi:docname="marketstack.svg"
width="60"
height="60"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
id="defs77" /><sodipodi:namedview
id="namedview75"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
showgrid="false"
width="60px"
inkscape:zoom="6.8111408"
inkscape:cx="145.49692"
inkscape:cy="32.44684"
inkscape:window-width="3840"
inkscape:window-height="2066"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<style
type="text/css"
id="style44">
.st0{enable-background:new ;}
.st1{fill:#FFFFFF;}
</style>
<title
id="title46">Marketstack</title>
<path
class="st1"
d="m -2398.9017,942.5 c 0,-0.38462 0.096,-0.67308 0.096,-0.86538 0,-1.63462 0,-3.36539 0,-5 0,-17.69231 0,-35.48077 0,-53.17308 0,-0.96154 0,-0.96154 -0.9616,-0.96154 -7.1154,0 -14.2307,0 -21.3461,0 -0.6731,0 -0.8654,0.19231 -0.8654,0.86538 0,3.36539 0,6.63462 0,10 0,0.67308 0.2884,0.86539 0.8654,0.86539 3.2692,0 6.5384,0 9.7115,0 0.9615,0 0.9615,0 0.9615,0.96154 0,6.53846 0,13.07692 0,19.61538 0,7.30769 0,14.51923 0,21.82693 0,0.48076 0,0.86538 0.577,1.15384 2.8846,1.25 5.7692,2.59616 8.5577,3.84616 0.7692,0.1923 1.5384,0.48076 2.4038,0.86538 z m -17.2115,-21.44231 c 0,0 0,0 0,0 0,-6.82692 0,-13.65384 0,-20.48077 0,-0.86538 0,-0.86538 -0.8654,-0.86538 -7.2116,0 -14.3269,0 -21.5385,0 -0.6731,0 -0.8654,0.19231 -0.8654,0.86538 0,3.36539 0,6.63462 0,10 0,0.67308 0.2885,0.86539 0.8654,0.86539 3.1731,0 6.3462,0 9.5193,0 1.25,0 1.25,-0.19231 1.25,1.25 0,7.98077 0,15.86538 0,23.84615 0,0.67308 0.1923,1.05769 0.8653,1.25 1.4423,0.57692 2.7885,1.25 4.2308,1.92308 2.0192,0.86538 3.9423,1.73077 5.9615,2.69231 0.4808,0.1923 0.6731,0.0961 0.6731,-0.38462 0,-0.19231 0,-0.38461 0,-0.57692 -0.096,-6.92308 -0.096,-13.65385 -0.096,-20.38462 z m -17.3077,8.75 c 0,0 0,0 0,0 0,-3.94231 0,-7.98077 0,-11.92307 0,-0.57693 -0.1923,-0.86539 -0.8654,-0.86539 -7.1154,0 -14.3269,0 -21.4423,0 -0.8654,0 -0.8654,0 -0.8654,0.86539 0,5.1923 0,10.48076 0,15.67307 0,0.67308 0.1923,0.96154 0.7692,1.15385 2.1154,0.67308 4.1346,1.44231 6.25,2.21154 5.0962,1.73077 10.1923,3.55769 15.2885,5.28846 0.8654,0.28846 0.8654,0.28846 0.8654,-0.67308 0,-3.84615 0,-7.78846 0,-11.73077 z"
id="path72"
style="fill:#02283e;fill-opacity:1;stroke-width:0.961538" />
<metadata
id="metadata1072"><rdf:RDF><cc:Work
rdf:about=""><dc:title>Marketstack</dc:title></cc:Work></rdf:RDF></metadata></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -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;
};

View file

@ -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<any>}
*/
export async function apiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, method: string, endpoint: string, body: object, query?: IDataObject, uri?: string, option: IDataObject = {}): Promise<any> { // 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<any>}
*/
export async function apiRequestAllItems(this: IHookFunctions | IExecuteFunctions | IPollFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject): Promise<any> { // 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<INodeExecutionData[]> {
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;
}

View file

@ -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": [
]
}
}

View file

@ -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<INodeExecutionData[][]> {
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)];
}
}

View file

@ -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[];

View file

@ -0,0 +1,425 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="460px" height="460px" viewBox="0 0 460 460" enable-background="new 0 0 460 460" xml:space="preserve"> <image id="image0" width="460" height="460" x="0" y="0"
href="data:image/png;base64,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
OjAwTpa81QAAAABJRU5ErkJggg==" />
</svg>

After

Width:  |  Height:  |  Size: 32 KiB

View file

@ -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);

View file

@ -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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
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<INodePropertyOptions[]> {
@ -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';

View file

@ -1,9 +1,9 @@
export interface IAttachment {
ParentId?: string;
Name?: string;
Body?: string;
OwnerId?: string;
IsPrivate?: boolean;
ContentType?: string;
Description?: string;
Body?: string;
}

View file

@ -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<br />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[];

View file

@ -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<any> { // 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;
}

View file

@ -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<INodePropertyOptions[]> {
// 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') {

View file

@ -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/"
}
]
}
}

View file

@ -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/"
}
]
}
}

View file

@ -7,6 +7,11 @@ type LoadedResource = {
name: string;
};
type LoadOption = {
value: string;
name: string;
};
type LoadedUser = {
id: string;
full_name_display: string;

View file

@ -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<any> { // 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 }>;
}
};

View file

@ -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;

View file

@ -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 {
],
};
}
}
}

View file

@ -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[];

View file

@ -0,0 +1 @@
export * from './CustomerDescription';

View file

@ -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,
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 999 B

View file

@ -0,0 +1,14 @@
<svg preserveAspectRatio="xMidYMid" version="1.1" viewBox="0 -50 256 253" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<title>WooCommerce Logo</title>
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<path d="m23.759 0h208.38c13.187 0 23.863 10.675 23.863 23.863v79.542c0 13.187-10.675 23.863-23.863 23.863h-74.727l10.257 25.118-45.109-25.118h-98.695c-13.187 0-23.863-10.675-23.863-23.863v-79.542c-0.10466-13.083 10.571-23.863 23.758-23.863z" fill="#7f54b3"/>
<path d="m14.578 21.75c1.4569-1.9772 3.6423-3.0179 6.5561-3.226 5.3073-0.41626 8.3252 2.0813 9.0537 7.4927 3.226 21.75 6.7642 40.169 10.511 55.259l22.79-43.395c2.0813-3.9545 4.6829-6.0358 7.8049-6.2439 4.5789-0.3122 7.3886 2.6016 8.5333 8.7415 2.6016 13.841 5.9317 25.6 9.8862 35.59 2.7057-26.433 7.2846-45.476 13.737-57.236 1.561-2.9138 3.8504-4.3707 6.8683-4.5789 2.3935-0.20813 4.5789 0.52033 6.5561 2.0813 1.9772 1.561 3.0179 3.5382 3.226 5.9317 0.10406 1.8732-0.20813 3.4341-1.0407 4.9951-4.0585 7.4927-7.3886 20.085-10.094 37.567-2.6016 16.963-3.5382 30.179-2.9138 39.649 0.20813 2.6016-0.20813 4.8911-1.2488 6.8683-1.2488 2.2894-3.122 3.5382-5.5154 3.7463-2.7057 0.20813-5.5154-1.0406-8.2211-3.8504-9.678-9.8862-17.379-24.663-22.998-44.332-6.7642 13.32-11.759 23.311-14.985 29.971-6.1398 11.759-11.343 17.795-15.714 18.107-2.8098 0.20813-5.2033-2.1854-7.2846-7.1805-5.3073-13.633-11.031-39.961-17.171-78.985-0.41626-2.7057 0.20813-5.0992 1.665-6.9724zm223.64 16.338c-3.7463-6.5561-9.2618-10.511-16.65-12.072-1.9772-0.41626-3.8504-0.62439-5.6195-0.62439-9.9902 0-18.107 5.2033-24.455 15.61-5.4114 8.8455-8.1171 18.628-8.1171 29.346 0 8.013 1.665 14.881 4.9951 20.605 3.7463 6.5561 9.2618 10.511 16.65 12.072 1.9772 0.41626 3.8504 0.62439 5.6195 0.62439 10.094 0 18.211-5.2033 24.455-15.61 5.4114-8.9496 8.1171-18.732 8.1171-29.45 0.10406-8.1171-1.665-14.881-4.9951-20.501zm-13.112 28.826c-1.4569 6.8683-4.0585 11.967-7.9089 15.402-3.0179 2.7057-5.8276 3.8504-8.4293 3.3301-2.4976-0.52033-4.5789-2.7057-6.1398-6.7642-1.2488-3.226-1.8732-6.452-1.8732-9.4699 0-2.6016 0.20813-5.2033 0.72846-7.5967 0.93659-4.2667 2.7057-8.4293 5.5154-12.384 3.4341-5.0992 7.0764-7.1805 10.823-6.452 2.4976 0.52033 4.5789 2.7057 6.1398 6.7642 1.2488 3.226 1.8732 6.452 1.8732 9.4699 0 2.7057-0.20813 5.3073-0.72846 7.7008zm-52.033-28.826c-3.7463-6.5561-9.3659-10.511-16.65-12.072-1.9772-0.41626-3.8504-0.62439-5.6195-0.62439-9.9902 0-18.107 5.2033-24.455 15.61-5.4114 8.8455-8.1171 18.628-8.1171 29.346 0 8.013 1.665 14.881 4.9951 20.605 3.7463 6.5561 9.2618 10.511 16.65 12.072 1.9772 0.41626 3.8504 0.62439 5.6195 0.62439 10.094 0 18.211-5.2033 24.455-15.61 5.4114-8.9496 8.1171-18.732 8.1171-29.45 0-8.1171-1.665-14.881-4.9951-20.501zm-13.216 28.826c-1.4569 6.8683-4.0585 11.967-7.9089 15.402-3.0179 2.7057-5.8276 3.8504-8.4293 3.3301-2.4976-0.52033-4.5789-2.7057-6.1398-6.7642-1.2488-3.226-1.8732-6.452-1.8732-9.4699 0-2.6016 0.20813-5.2033 0.72846-7.5967 0.93658-4.2667 2.7057-8.4293 5.5154-12.384 3.4341-5.0992 7.0764-7.1805 10.823-6.452 2.4976 0.52033 4.5789 2.7057 6.1398 6.7642 1.2488 3.226 1.8732 6.452 1.8732 9.4699 0.10406 2.7057-0.20813 5.3073-0.72846 7.7008z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

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