sync upstream

This commit is contained in:
Ricardo Espinoza 2019-12-01 10:51:44 -05:00
commit c1edcc9e56
56 changed files with 4844 additions and 283 deletions

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Feature request
url: https://community.n8n.io
about: Suggest an idea for this project
- name: Question / Problem
url: https://community.n8n.io
about: Questions and problems with n8n

View file

@ -1,15 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Please do not create a GitHub issue for feature requests**
Post all of them to the forum:
[https://community.n8n.io](https://community.n8n.io)
They get all collected there and people can upvote existing requests. That makes it then easier to know how to prioritize them. Thanks!

View file

@ -1,15 +0,0 @@
---
name: Question / Problem
about: Questions and problems with n8n
title: ''
labels: ''
assignees: ''
---
**Please do not create a GitHub issue for questions or problems**
Post all of them to the forum:
[https://community.n8n.io](https://community.n8n.io)
Check there first if it got answered already before and if not create a new topic.

View file

@ -38,7 +38,7 @@ The most important directories:
- [/packages/editor-ui](/packages/editor-ui) - Vue frontend workflow editor
- [/packages/node-dev](/packages/node-dev) - Simple CLI to create new n8n-nodes
- [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes
- [/packages/worflow](/packages/worflow) - Workflow code with interfaces which
- [/packages/workflow](/packages/workflow) - Workflow code with interfaces which
get used by front- & backend

View file

@ -123,7 +123,7 @@ Some third-party services have their own libraries on npm which make it a little
const response = await this.helpers.request(options);
```
That is simply using the npm package `request-promise-native` which is the basic npm `request` module but with promises.
That is simply using the npm package [`request-promise-native`](https://github.com/request/request-promise-native) which is the basic npm `request` module but with promises. For a full set of `options` consider looking at [the underlying `request` options documentation](https://github.com/request/request#requestoptions-callback).
### Reuse parameter names

View file

@ -5,7 +5,8 @@ import {
} from "n8n-core";
import { Command, flags } from '@oclif/command';
const open = require('open');
import { promisify } from "util";
import { promisify } from 'util';
import { dirname } from 'path';
import * as config from '../config';
import {
@ -21,6 +22,10 @@ import {
const tunnel = promisify(localtunnel);
// // Add support for internationalization
// const fullIcuPath = require.resolve('full-icu');
// process.env.NODE_ICU_DATA = dirname(fullIcuPath);
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
let processExistCode = 0;

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.35.0",
"version": "0.36.1",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -59,6 +59,7 @@
"@types/express": "^4.16.1",
"@types/jest": "^24.0.18",
"@types/localtunnel": "^1.9.0",
"@types/lodash.get": "^4.4.2",
"@types/node": "^10.10.1",
"@types/open": "^6.1.0",
"@types/parseurl": "^1.3.1",
@ -71,9 +72,9 @@
"typescript": "~3.5.2"
},
"dependencies": {
"@types/jsonwebtoken": "^8.3.4",
"@oclif/command": "^1.5.18",
"@oclif/errors": "^1.2.2",
"@types/jsonwebtoken": "^8.3.4",
"basic-auth": "^2.0.1",
"body-parser": "^1.18.3",
"compression": "^1.7.4",
@ -88,11 +89,12 @@
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^1.6.0",
"localtunnel": "^1.9.1",
"lodash.get": "^4.4.2",
"mongodb": "^3.2.3",
"n8n-core": "~0.15.0",
"n8n-editor-ui": "~0.25.0",
"n8n-nodes-base": "~0.30.0",
"n8n-workflow": "~0.15.0",
"n8n-core": "~0.16.0",
"n8n-editor-ui": "~0.26.0",
"n8n-nodes-base": "~0.31.0",
"n8n-workflow": "~0.16.0",
"open": "^6.1.0",
"pg": "^7.11.0",
"request-promise-native": "^1.0.7",

View file

@ -28,6 +28,7 @@ import {
IRunExecutionData,
ITaskData,
IWebhookData,
IWebhookResponseData,
IWorkflowExecuteAdditionalData,
NodeHelpers,
Workflow,
@ -76,6 +77,8 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
let parentNodes: string[] | undefined;
if (destinationNode !== undefined) {
parentNodes = workflow.getParentNodes(destinationNode);
// Also add the destination node in case it itself is a webhook node
parentNodes.push(destinationNode);
}
for (const node of Object.values(workflow.nodes)) {
@ -122,7 +125,7 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
// If the mode is not known we error. Is probably best like that instead of using
// the default that people know as early as possible (probably already testing phase)
// that something does not resolve properly.
const errorMessage = `The response mode ${responseMode} is not valid!.`;
const errorMessage = `The response mode ${responseMode} is not valid!`;
responseCallback(new Error(errorMessage), {});
throw new ResponseHelper.ResponseError(errorMessage, 500, 500);
}
@ -136,12 +139,41 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
additionalData.httpResponse = res;
let didSendResponse = false;
let runExecutionDataMerge = {};
try {
// Run the webhook function to see what should be returned and if
// the workflow should be executed or not
const webhookResultData = await webhookData.workflow.runWebhook(webhookData, workflowStartNode, additionalData, NodeExecuteFunctions, executionMode);
let webhookResultData: IWebhookResponseData;
if (webhookResultData.noWebhookResponse === true) {
try {
webhookResultData = await webhookData.workflow.runWebhook(webhookData, workflowStartNode, additionalData, NodeExecuteFunctions, executionMode);
} catch (e) {
// Send error response to webhook caller
const errorMessage = 'Workflow Webhook Error: Workflow could not be started!';
responseCallback(new Error(errorMessage), {});
didSendResponse = true;
// Add error to execution data that it can be logged and send to Editor-UI
runExecutionDataMerge = {
resultData: {
runData: {},
lastNodeExecuted: workflowStartNode.name,
error: {
message: e.message,
stack: e.stack,
},
},
};
webhookResultData = {
noWebhookResponse: true,
// Add empty data that it at least tries to "execute" the webhook
// which then so gets the chance to throw the error.
workflowData: [[{json: {}}]],
};
}
if (webhookResultData.noWebhookResponse === true && didSendResponse === false) {
// The response got already send
responseCallback(null, {
noWebhookResponse: true,
@ -153,18 +185,24 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
// Workflow should not run
if (webhookResultData.webhookResponse !== undefined) {
// Data to respond with is given
responseCallback(null, {
data: webhookResultData.webhookResponse,
responseCode,
});
if (didSendResponse === false) {
responseCallback(null, {
data: webhookResultData.webhookResponse,
responseCode,
});
didSendResponse = true;
}
} else {
// Send default response
responseCallback(null, {
data: {
message: 'Webhook call got received.',
},
responseCode,
});
if (didSendResponse === false) {
responseCallback(null, {
data: {
message: 'Webhook call got received.',
},
responseCode,
});
didSendResponse = true;
}
}
return;
}
@ -215,6 +253,11 @@ export function getWorkflowWebhooks(workflow: Workflow, additionalData: IWorkflo
},
};
if (Object.keys(runExecutionDataMerge).length !== 0) {
// If data to merge got defined add it to the execution data
Object.assign(runExecutionData, runExecutionDataMerge);
}
const runData: IWorkflowExecutionDataProcess = {
credentials,
executionMode,

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.15.0",
"version": "0.16.0",
"description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -42,7 +42,7 @@
"crypto-js": "^3.1.9-1",
"lodash.get": "^4.4.2",
"mmmagic": "^0.5.2",
"n8n-workflow": "~0.15.0",
"n8n-workflow": "~0.16.0",
"request-promise-native": "^1.0.7"
},
"jest": {

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.25.0",
"version": "0.26.0",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -59,7 +59,7 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"n8n-workflow": "~0.15.0",
"n8n-workflow": "~0.16.0",
"node-sass": "^4.12.0",
"prismjs": "^1.17.1",
"quill": "^2.0.0-dev.3",
@ -71,7 +71,7 @@
"typescript": "~3.5.2",
"vue": "^2.6.9",
"vue-cli-plugin-webpack-bundle-analyzer": "^1.3.0",
"vue-json-pretty": "^1.4.1",
"vue-json-tree": "^0.4.1",
"vue-prism-editor": "^0.3.0",
"vue-router": "^3.0.6",
"vue-template-compiler": "^2.5.17",

View file

@ -22,8 +22,12 @@
</el-tooltip>
</div>
<el-row v-for="parameter in credentialTypeData.properties" :key="parameter.name" class="parameter-wrapper">
<el-col :span="6">
<el-col :span="6" class="parameter-name">
{{parameter.displayName}}:
<el-tooltip placement="top" class="parameter-info" v-if="parameter.description" effect="light">
<div slot="content" v-html="parameter.description"></div>
<font-awesome-icon icon="question-circle"/>
</el-tooltip>
</el-col>
<el-col :span="18">
<parameter-input :parameter="parameter" :value="propertyValue[parameter.name]" :path="parameter.name" :isCredential="true" @valueChanged="valueChanged" />
@ -299,6 +303,20 @@ export default mixins(
.parameter-wrapper {
line-height: 3em;
.parameter-name {
position: relative;
&:hover {
.parameter-info {
display: inline;
}
}
.parameter-info {
display: none;
}
}
}
.credentials-info {

View file

@ -326,11 +326,20 @@ export default mixins(
// and the error is not displayed on the node in the workflow
const validOptions = this.parameterOptions!.map((options: INodePropertyOptions) => options.value);
if (this.displayValue === null || !validOptions.includes(this.displayValue as string)) {
if (issues.parameters === undefined) {
issues.parameters = {};
const checkValues: string[] = [];
if (Array.isArray(this.displayValue)) {
checkValues.push.apply(checkValues, this.displayValue);
} else {
checkValues.push(this.displayValue as string);
}
for (const checkValue of checkValues) {
if (checkValue === null || !validOptions.includes(checkValue)) {
if (issues.parameters === undefined) {
issues.parameters = {};
}
issues.parameters[this.parameter.name] = [`The value "${checkValue}" is not supported!`];
}
issues.parameters[this.parameter.name] = [`The value "${this.displayValue}" is not supported!`];
}
} else if (this.remoteParameterOptionsLoadingIssues !== null) {
if (issues.parameters === undefined) {

View file

@ -95,11 +95,12 @@
</tr>
</table>
</div>
<vue-json-pretty
<json-tree
v-else-if="displayMode === 'JSON'"
:data="jsonData"
:level="10"
class="json-data"
:data="jsonData">
</vue-json-pretty>
/>
</div>
<div v-else-if="displayMode === 'Binary'">
<div v-if="binaryData.length === 0" class="no-data">
@ -162,7 +163,7 @@
<script lang="ts">
import Vue from 'vue';
// @ts-ignore
import VueJsonPretty from 'vue-json-pretty';
import JsonTree from 'vue-json-tree';
import {
GenericValue,
IBinaryData,
@ -200,7 +201,7 @@ export default mixins(
name: 'RunData',
components: {
BinaryDataDisplay,
VueJsonPretty,
JsonTree,
},
data () {
return {
@ -572,9 +573,16 @@ export default mixins(
}
.json-data {
overflow-x: hidden;
white-space: initial;
word-wrap: break-word;
.json-tree {
color: $--custom-input-font;
.json-tree-value-number {
color: #b03030;
}
.json-tree-value-string {
color: #8aab1a;
}
}
}
.error-display,

View file

@ -1,7 +1,8 @@
{
"linterOptions": {
"exclude": [
"node_modules/**/*"
"node_modules/**/*",
"../../node_modules/**/*"
]
},
"defaultSeverity": "error",

View file

@ -135,6 +135,15 @@ Method get called when the workflow gets executed
- `execute`: Executed once no matter how many items
- `executeSingle`: Executed once for every item
By default always `execute` should be used especially when creating a
third-party integration. The reason for that is that it is way more flexible
and allows to, for example, return a different amount of items than it received
as input. This is very important when a node should query data like return
all users. In that case, does the node normally just receive one input-item
but returns as many as users exist. So in doubt always `execute` should be
used!
**Trigger node**
Method gets called once when the workflow gets activated. It can then trigger

View file

@ -23,7 +23,6 @@
"build": "tsc",
"postpack": "rm -f oclif.manifest.json",
"prepack": "echo \"Building project...\" && rm -rf dist && tsc -b && oclif-dev manifest",
"test": "jest",
"tslint": "tslint -p tsconfig.json -c tslint.json",
"watch": "tsc --watch"
},
@ -65,23 +64,5 @@
"request": "^2.88.0",
"tmp-promise": "^2.0.2",
"typescript": "~3.5.2"
},
"jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testURL": "http://localhost/",
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"testPathIgnorePatterns": [
"/dist/",
"/node_modules/"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"json",
"node"
]
}
}

View file

@ -1,7 +0,0 @@
describe('Placeholder', () => {
test('example', () => {
expect(1 + 1).toEqual(2);
});
});

View file

@ -4,8 +4,7 @@
"es2017"
],
"types": [
"node",
"jest"
"node"
],
"module": "commonjs",
"esModuleInterop": true,

View file

@ -0,0 +1,39 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class FileMaker implements ICredentialType {
name = 'fileMaker';
displayName = 'FileMaker API';
properties = [
{
displayName: 'Host',
name: 'host',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Database',
name: 'db',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Login',
name: 'login',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
default: '',
},
];
}

View file

@ -13,6 +13,7 @@ export class GoogleApi implements ICredentialType {
name: 'email',
type: 'string' as NodePropertyTypes,
default: '',
description: 'The Google Service account similar to user-808@project.iam.gserviceaccount.com.<br />See the <a href="https://github.com/jovotech/learn-jovo/blob/master/tutorials/google-spreadsheet-private-cms/README.md#google-api-console">tutorial</a> on how to create one.',
},
{
@ -21,6 +22,7 @@ export class GoogleApi implements ICredentialType {
lines: 5,
type: 'string' as NodePropertyTypes,
default: '',
description: 'Use the multiline editor. Make sure there are exactly 3 lines.<br />-----BEGIN PRIVATE KEY-----<br />KEY IN A SINGLE LINE<br />-----END PRIVATE KEY-----',
},
];
}

View file

@ -0,0 +1,18 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class IntercomApi implements ICredentialType {
name = 'intercomApi';
displayName = 'Intercom API';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MailchimpApi implements ICredentialType {
name = 'mailchimpApi';
displayName = 'Mailchimp API';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -12,15 +12,14 @@ export class NextCloudApi implements ICredentialType {
displayName: 'Web DAV URL',
name: 'webDavUrl',
type: 'string' as NodePropertyTypes,
placeholder: 'https://nextcloud.example.com/remote.php/webdav/',
default: '',
},
{
displayName: 'User',
name: 'user',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Password',

View file

@ -5,7 +5,7 @@ import {
export class PayPalApi implements ICredentialType {
name = 'paypalApi';
name = 'payPalApi';
displayName = 'PayPal API';
properties = [
{

View file

@ -13,6 +13,7 @@ export class TelegramApi implements ICredentialType {
name: 'accessToken',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Chat with the <a href="https://telegram.me/botfather">bot father</a> to obtain the access token.',
},
];
}

View file

@ -53,6 +53,14 @@ export class AsanaTrigger implements INodeType {
required: true,
description: 'The resource ID to subscribe to. The resource can be a task or project.',
},
{
displayName: 'Workspace',
name: 'workspace',
type: 'string',
default: '',
required: false,
description: 'The workspace ID the resource is registered under. This is only required if you want to allow overriding existing webhooks.',
},
],
};
@ -93,6 +101,8 @@ export class AsanaTrigger implements INodeType {
const resource = this.getNodeParameter('resource') as string;
const workspace = this.getNodeParameter('workspace') as string;
const endpoint = `webhooks`;
const body = {
@ -100,7 +110,22 @@ export class AsanaTrigger implements INodeType {
target: webhookUrl,
};
const responseData = await asanaApiRequest.call(this, 'POST', endpoint, body);
let responseData;
try {
responseData = await asanaApiRequest.call(this, 'POST', endpoint, body);
} catch(error) {
// delete webhook if it already exists
if (error.statusCode === 403) {
const webhookData = await asanaApiRequest.call(this, 'GET', endpoint, {}, { workspace });
const webhook = webhookData.data.find((webhook: any) => { // tslint:disable-line:no-any
return webhook.target === webhookUrl && webhook.resource.gid === resource;
});
await asanaApiRequest.call(this, 'DELETE', `${endpoint}/${webhook.gid}`, {});
responseData = await asanaApiRequest.call(this, 'POST', endpoint, body);
} else {
throw error;
}
}
if (responseData.data === undefined || responseData.data.id === undefined) {
// Required data is missing so was not successful

View file

@ -0,0 +1,909 @@
import {IExecuteFunctions} from 'n8n-core';
import {
ILoadOptionsFunctions,
INodeExecutionData, INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {OptionsWithUri} from 'request';
import {
layoutsApiRequest,
getFields,
getPortals,
getScripts,
getToken,
parseSort,
parsePortals,
parseQuery,
parseScripts,
parseFields,
logout
} from "./GenericFunctions";
export class FileMaker implements INodeType {
description: INodeTypeDescription = {
displayName: 'FileMaker',
name: 'filemaker',
icon: 'file:filemaker.png',
group: ['input'],
version: 1,
description: 'Retrieve data from FileMaker data API.',
defaults: {
name: 'FileMaker',
color: '#665533',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'fileMaker',
required: true,
},
],
properties: [
{
displayName: 'Action',
name: 'action',
type: 'options',
default: 'record',
options: [
/*{
name: 'Login',
value: 'login',
},
{
name: 'Logout',
value: 'logout',
},*/
{
name: 'Find Records',
value: 'find',
},
{
name: 'Get Records',
value: 'records',
},
{
name: 'Get Records By Id',
value: 'record',
},
{
name: 'Perform Script',
value: 'performscript',
},
{
name: 'Create Record',
value: 'create',
},
{
name: 'Edit Record',
value: 'edit',
},
{
name: 'Duplicate Record',
value: 'duplicate',
},
{
name: 'Delete Record',
value: 'delete',
},
],
description: 'Action to perform.',
},
// ----------------------------------
// shared
// ----------------------------------
{
displayName: 'Layout',
name: 'layout',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLayouts',
},
options: [],
default: '',
required: true,
displayOptions: {},
placeholder: 'Layout Name',
description: 'FileMaker Layout Name.',
},
{
displayName: 'Record Id',
name: 'recid',
type: 'number',
default: '',
required: true,
displayOptions: {
show: {
action: [
'record',
'edit',
'delete',
'duplicate',
],
},
},
placeholder: 'Record ID',
description: 'Internal Record ID returned by get (recordid)',
},
{
displayName: 'Offset',
name: 'offset',
placeholder: '0',
description: 'The record number of the first record in the range of records.',
type: 'number',
default: '1',
displayOptions: {
show: {
action: [
'find',
'records',
],
},
}
},
{
displayName: 'Limit',
name: 'limit',
placeholder: '100',
description: 'The maximum number of records that should be returned. If not specified, the default value is 100.',
type: 'number',
default: '100',
displayOptions: {
show: {
action: [
'find',
'records',
],
},
}
},
{
displayName: 'Get portals',
name: 'getPortals',
type: 'boolean',
default: false,
description: 'Should we get portal data as well ?',
displayOptions: {
show: {
action: [
'record',
'records',
'find',
],
},
},
},
{
displayName: 'Portals',
name: 'portals',
type: 'options',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add portal',
loadOptionsMethod: 'getPortals',
},
options: [],
default: [],
displayOptions: {
show: {
action: [
'record',
'records',
'find',
],
getPortals: [
true,
],
},
},
placeholder: 'Portals',
description: 'The portal result set to return. Use the portal object name or portal<br />table name. If this parameter is omitted, the API will return all<br />portal objects and records in the layout. For best performance<br />, pass the portal object name or portal table name.',
},
// ----------------------------------
// find/records
// ----------------------------------
{
displayName: 'Response Layout',
name: 'responseLayout',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getResponseLayouts',
},
options: [],
default: '',
required: false,
displayOptions: {
show: {
action: [
'find'
],
},
},
},
{
displayName: 'Queries',
name: 'queries',
placeholder: 'Add query',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
action: [
'find',
],
},
},
description: 'Queries ',
default: {},
options: [
{
name: 'query',
displayName: 'Query',
values: [
{
displayName: 'Fields',
name: 'fields',
placeholder: 'Add field',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
options: [{
name: 'field',
displayName: 'Field',
values: [
{
displayName: 'Field',
name: 'name',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getFields',
},
options: [],
description: 'Search Field',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to search',
},
]
}
],
description: 'Field Name',
},
{
displayName: 'Omit',
name: 'omit',
type: 'boolean',
default: false
},
]
},
],
},
{
displayName: 'Sort data?',
name: 'setSort',
type: 'boolean',
default: false,
description: 'Should we sort data ?',
displayOptions: {
show: {
action: [
'find',
'record',
'records',
],
},
},
},
{
displayName: 'Sort',
name: 'sortParametersUi',
placeholder: 'Add Sort Rules',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
setSort: [
true,
],
action: [
'find',
'records',
],
},
},
description: 'Sort rules',
default: {},
options: [
{
name: 'rules',
displayName: 'Rules',
values: [
{
displayName: 'Field',
name: 'name',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getFields',
},
options: [],
description: 'Field Name.',
},
{
displayName: 'Order',
name: 'value',
type: 'options',
default: 'ascend',
options: [
{
name: 'Ascend',
value: 'ascend'
},
{
name: 'Descend',
value: 'descend'
},
],
description: 'Sort order.',
},
]
},
],
},
{
displayName: 'Before find script',
name: 'setScriptBefore',
type: 'boolean',
default: false,
description: 'Define a script to be run before the action specified by the API call and after the subsequent sort.',
displayOptions: {
show: {
action: [
'find',
'record',
'records',
],
}
},
},
{
displayName: 'Script Name',
name: 'scriptBefore',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getScripts',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
action: [
'find',
'record',
'records',
],
setScriptBefore: [
true
],
},
},
placeholder: 'Script Name',
description: 'The name of the FileMaker script to be run after the action specified by the API call and after the subsequent sort.',
},
{
displayName: 'Script Parameter',
name: 'scriptBeforeParam',
type: 'string',
default: '',
required: false,
displayOptions: {
show: {
action: [
'find',
'record',
'records',
],
setScriptBefore: [
true
],
},
},
placeholder: 'Script Parameters',
description: 'A parameter for the FileMaker script.',
},
{
displayName: 'Before sort script',
name: 'setScriptSort',
type: 'boolean',
default: false,
description: 'Define a script to be run after the action specified by the API call but before the subsequent sort.',
displayOptions: {
show: {
action: [
'find',
'record',
'records',
],
}
},
},
{
displayName: 'Script Name',
name: 'scriptSort',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getScripts',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
action: [
'find',
'record',
'records',
],
setScriptSort: [
true
],
},
},
placeholder: 'Script Name',
description: 'The name of the FileMaker script to be run after the action specified by the API call but before the subsequent sort.',
},
{
displayName: 'Script Parameter',
name: 'scriptSortParam',
type: 'string',
default: '',
required: false,
displayOptions: {
show: {
action: [
'find',
'record',
'records',
],
setScriptSort: [
true
],
},
},
placeholder: 'Script Parameters',
description: 'A parameter for the FileMaker script.',
},
{
displayName: 'After sort script',
name: 'setScriptAfter',
type: 'boolean',
default: false,
description: 'Define a script to be run after the action specified by the API call but before the subsequent sort.',
displayOptions: {
show: {
action: [
'find',
'record',
'records',
],
}
},
},
{
displayName: 'Script Name',
name: 'scriptAfter',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getScripts',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
action: [
'find',
'record',
'records',
],
setScriptAfter: [
true
],
},
},
placeholder: 'Script Name',
description: 'The name of the FileMaker script to be run after the action specified by the API call and after the subsequent sort.',
},
{
displayName: 'Script Parameter',
name: 'scriptAfterParam',
type: 'string',
default: '',
required: false,
displayOptions: {
show: {
action: [
'find',
'record',
'records',
],
setScriptAfter: [
true
],
},
},
placeholder: 'Script Parameters',
description: 'A parameter for the FileMaker script.',
},
// ----------------------------------
// create/edit
// ----------------------------------
/*{
displayName: 'fieldData',
name: 'fieldData',
placeholder: '{"field1": "value", "field2": "value", ...}',
description: 'Additional fields to add.',
type: 'string',
default: '{}',
displayOptions: {
show: {
action: [
'create',
'edit',
],
},
}
},*/
{
displayName: 'Mod Id',
name: 'modId',
description: 'The last modification ID. When you use modId, a record is edited only when the modId matches.',
type: 'number',
default: '',
displayOptions: {
show: {
action: [
'edit',
],
},
}
},
{
displayName: 'Fields',
name: 'fieldsParametersUi',
placeholder: 'Add field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
action: [
'create',
'edit',
],
},
},
description: 'Fields to define',
default: {},
options: [
{
name: 'fields',
displayName: 'Fields',
values: [
{
displayName: 'Field',
name: 'name',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getFields',
},
options: [],
description: 'Field Name.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
]
},
],
},
// ----------------------------------
// performscript
// ----------------------------------
{
displayName: 'Script Name',
name: 'script',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getScripts',
},
options: [],
default: '',
required: true,
displayOptions: {
show: {
action: [
'performscript'
],
},
},
placeholder: 'Script Name',
description: 'The name of the FileMaker script to be run.',
},
{
displayName: 'Script Parameter',
name: 'scriptParam',
type: 'string',
default: '',
required: false,
displayOptions: {
show: {
action: [
'performscript'
],
},
},
placeholder: 'Script Parameters',
description: 'A parameter for the FileMaker script.',
},
]
};
methods = {
loadOptions: {
// Get all the available topics to display them to user so that he can
// select them easily
async getLayouts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
let returnData: INodePropertyOptions[];
try {
returnData = await layoutsApiRequest.call(this);
} catch (err) {
throw new Error(`FileMaker Error: ${err}`);
}
return returnData;
},
async getResponseLayouts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
returnData.push({
name: 'Use main layout',
value: '',
});
let layouts;
try {
layouts = await layoutsApiRequest.call(this);
} catch (err) {
throw new Error(`FileMaker Error: ${err}`);
}
for (const layout of layouts) {
returnData.push({
name: layout.name,
value: layout.name,
});
}
return returnData;
},
async getFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let fields;
try {
fields = await getFields.call(this);
} catch (err) {
throw new Error(`FileMaker Error: ${err}`);
}
for (const field of fields) {
returnData.push({
name: field.name,
value: field.name,
});
}
return returnData;
},
async getScripts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let scripts;
try {
scripts = await getScripts.call(this);
} catch (err) {
throw new Error(`FileMaker Error: ${err}`);
}
for (const script of scripts) {
if (!script.isFolder) {
returnData.push({
name: script.name,
value: script.name,
});
}
}
return returnData;
},
async getPortals(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let portals;
try {
portals = await getPortals.call(this);
} catch (err) {
throw new Error(`FileMaker Error: ${err}`);
}
Object.keys(portals).forEach((portal) => {
returnData.push({
name: portal,
value: portal,
});
});
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const credentials = this.getCredentials('fileMaker');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
let token;
try {
token = await getToken.call(this);
} catch (e) {
throw new Error(`Login fail: ${e}`);
}
let requestOptions: OptionsWithUri;
const host = credentials.host as string;
const database = credentials.db as string;
const url = `https://${host}/fmi/data/v1`;
const action = this.getNodeParameter('action', 0) as string;
for (let i = 0; i < items.length; i++) {
// Reset all values
requestOptions = {
uri: '',
headers: {
'Authorization': `Bearer ${token}`,
},
method: 'GET',
json: true
};
const layout = this.getNodeParameter('layout', 0) as string;
if (action === 'record') {
const recid = this.getNodeParameter('recid', 0) as string;
requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`;
requestOptions.qs = {
'portal': JSON.stringify(parsePortals.call(this)),
...parseScripts.call(this)
};
} else if (action === 'records') {
requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`;
requestOptions.qs = {
'_offset': this.getNodeParameter('offset', 0),
'_limit': this.getNodeParameter('limit', 0),
'portal': JSON.stringify(parsePortals.call(this)),
...parseScripts.call(this)
};
const sort = parseSort.call(this);
if (sort) {
requestOptions.body.sort = sort;
}
} else if (action === 'find') {
requestOptions.uri = url + `/databases/${database}/layouts/${layout}/_find`;
requestOptions.method = 'POST';
requestOptions.body = {
'query': parseQuery.call(this),
'offset': this.getNodeParameter('offset', 0),
'limit': this.getNodeParameter('limit', 0),
'layout.response': this.getNodeParameter('responseLayout', 0),
...parseScripts.call(this)
};
const sort = parseSort.call(this);
if (sort) {
requestOptions.body.sort = sort;
}
} else if (action === 'create') {
requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records`;
requestOptions.method = 'POST';
requestOptions.headers!['Content-Type'] = 'application/json';
//TODO: handle portalData
requestOptions.body = {
fieldData: {...parseFields.call(this)},
portalData: {},
...parseScripts.call(this)
};
} else if (action === 'edit') {
const recid = this.getNodeParameter('recid', 0) as string;
requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`;
requestOptions.method = 'PATCH';
requestOptions.headers!['Content-Type'] = 'application/json';
//TODO: handle portalData
requestOptions.body = {
fieldData: {...parseFields.call(this)},
portalData: {},
...parseScripts.call(this)
};
} else if (action === 'performscript') {
const scriptName = this.getNodeParameter('script', 0) as string;
requestOptions.uri = url + `/databases/${database}/layouts/${layout}/script/${scriptName}`;
requestOptions.qs = {
'script.param': this.getNodeParameter('scriptParam', 0),
};
} else if (action === 'duplicate') {
const recid = this.getNodeParameter('recid', 0) as string;
requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`;
requestOptions.method = 'POST';
requestOptions.headers!['Content-Type'] = 'application/json';
requestOptions.qs = {
...parseScripts.call(this)
};
} else if (action === 'delete') {
const recid = this.getNodeParameter('recid', 0) as string;
requestOptions.uri = url + `/databases/${database}/layouts/${layout}/records/${recid}`;
requestOptions.method = 'DELETE';
requestOptions.qs = {
...parseScripts.call(this)
};
} else {
throw new Error(`The action "${action}" is not implemented yet!`);
}
// Now that the options are all set make the actual http request
let response;
try {
response = await this.helpers.request(requestOptions);
} catch (error) {
response = error.response.body;
}
if (typeof response === 'string') {
throw new Error('Response body is not valid JSON. Change "Response Format" to "String"');
}
await logout.call(this, token);
returnData.push({json: response});
}
return this.prepareOutputData(returnData);
}
}

View file

@ -0,0 +1,390 @@
import {
IExecuteFunctions,
ILoadOptionsFunctions,
IExecuteSingleFunctions
} from 'n8n-core';
import {
IDataObject, INodePropertyOptions,
} from 'n8n-workflow';
import {OptionsWithUri} from 'request';
import {Url} from "url";
interface ScriptsOptions {
script?: any; //tslint:disable-line:no-any
'script.param'?: any; //tslint:disable-line:no-any
'script.prerequest'?: any; //tslint:disable-line:no-any
'script.prerequest.param'?: any; //tslint:disable-line:no-any
'script.presort'?: any; //tslint:disable-line:no-any
'script.presort.param'?: any; //tslint:disable-line:no-any
}
interface LayoutObject {
name: string;
isFolder?: boolean;
folderLayoutNames?:LayoutObject[];
}
/**
* Make an API request to ActiveCampaign
*
* @param {IHookFunctions} this
* @param {string} method
* @returns {Promise<any>}
*/
export async function layoutsApiRequest(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise<INodePropertyOptions[]> { // tslint:disable-line:no-any
const token = await getToken.call(this);
const credentials = this.getCredentials('fileMaker');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const host = credentials.host as string;
const db = credentials.db as string;
const url = `https://${host}/fmi/data/v1/databases/${db}/layouts`;
const options: OptionsWithUri = {
headers: {
'Authorization': `Bearer ${token}`,
},
method: 'GET',
uri: url,
json: true
};
try {
const responseData = await this.helpers.request!(options);
const items = parseLayouts(responseData.response.layouts);
items.sort((a, b) => a.name > b.name ? 0 : 1);
return items;
} catch (error) {
// If that data does not exist for some reason return the actual error
throw error;
}
}
function parseLayouts(layouts: LayoutObject[]): INodePropertyOptions[] {
const returnData: INodePropertyOptions[] = [];
for (const layout of layouts) {
if (layout.isFolder!) {
returnData.push(...parseLayouts(layout.folderLayoutNames!));
} else {
returnData.push({
name: layout.name,
value: layout.name,
});
}
}
return returnData;
}
/**
* Make an API request to ActiveCampaign
*
* @returns {Promise<any>}
*/
export async function getFields(this: ILoadOptionsFunctions): Promise<any> { // tslint:disable-line:no-any
const token = await getToken.call(this);
const credentials = this.getCredentials('fileMaker');
const layout = this.getCurrentNodeParameter('layout') as string;
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const host = credentials.host as string;
const db = credentials.db as string;
const url = `https://${host}/fmi/data/v1/databases/${db}/layouts/${layout}`;
const options: OptionsWithUri = {
headers: {
'Authorization': `Bearer ${token}`,
},
method: 'GET',
uri: url,
json: true
};
try {
const responseData = await this.helpers.request!(options);
return responseData.response.fieldMetaData;
} catch (error) {
// If that data does not exist for some reason return the actual error
throw error;
}
}
/**
* Make an API request to ActiveCampaign
*
* @returns {Promise<any>}
*/
export async function getPortals(this: ILoadOptionsFunctions): Promise<any> { // tslint:disable-line:no-any
const token = await getToken.call(this);
const credentials = this.getCredentials('fileMaker');
const layout = this.getCurrentNodeParameter('layout') as string;
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const host = credentials.host as string;
const db = credentials.db as string;
const url = `https://${host}/fmi/data/v1/databases/${db}/layouts/${layout}`;
const options: OptionsWithUri = {
headers: {
'Authorization': `Bearer ${token}`,
},
method: 'GET',
uri: url,
json: true
};
try {
const responseData = await this.helpers.request!(options);
return responseData.response.portalMetaData;
} catch (error) {
// If that data does not exist for some reason return the actual error
throw error;
}
}
/**
* Make an API request to ActiveCampaign
*
* @returns {Promise<any>}
*/
export async function getScripts(this: ILoadOptionsFunctions): Promise<any> { // tslint:disable-line:no-any
const token = await getToken.call(this);
const credentials = this.getCredentials('fileMaker');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const host = credentials.host as string;
const db = credentials.db as string;
const url = `https://${host}/fmi/data/v1/databases/${db}/scripts`;
const options: OptionsWithUri = {
headers: {
'Authorization': `Bearer ${token}`,
},
method: 'GET',
uri: url,
json: true
};
try {
const responseData = await this.helpers.request!(options);
return responseData.response.scripts;
} catch (error) {
// If that data does not exist for some reason return the actual error
throw error;
}
}
export async function getToken(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('fileMaker');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const host = credentials.host as string;
const db = credentials.db as string;
const login = credentials.login as string;
const password = credentials.password as string;
const url = `https://${host}/fmi/data/v1/databases/${db}/sessions`;
let requestOptions: OptionsWithUri;
// Reset all values
requestOptions = {
uri: url,
headers: {},
method: 'POST',
json: true
//rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean,
};
requestOptions.auth = {
user: login as string,
pass: password as string,
};
requestOptions.body = {
"fmDataSource": [
{
"database": host,
"username": login as string,
"password": password as string
}
]
};
try {
const response = await this.helpers.request!(requestOptions);
if (typeof response === 'string') {
throw new Error('Response body is not valid JSON. Change "Response Format" to "String"');
}
return response.response.token;
} catch (error) {
console.error(error);
let errorMessage;
if (error.response) {
errorMessage = error.response.body.messages[0].message + '(' + error.response.body.messages[0].message + ')';
} else {
errorMessage = `${error.message} (${error.name})`;
}
if (errorMessage !== undefined) {
throw errorMessage;
}
throw error.message;
}
}
export async function logout(this: ILoadOptionsFunctions | IExecuteFunctions | IExecuteSingleFunctions, token: string): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('fileMaker');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const host = credentials.host as string;
const db = credentials.db as string;
const url = `https://${host}/fmi/data/v1/databases/${db}/sessions/${token}`;
let requestOptions: OptionsWithUri;
// Reset all values
requestOptions = {
uri: url,
headers: {},
method: 'DELETE',
json: true
//rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean,
};
try {
const response = await this.helpers.request!(requestOptions);
if (typeof response === 'string') {
throw new Error('Response body is not valid JSON. Change "Response Format" to "String"');
}
return response;
} catch (error) {
console.error(error);
const errorMessage = error.response.body.messages[0].message + '(' + error.response.body.messages[0].message + ')';
if (errorMessage !== undefined) {
throw errorMessage;
}
throw error.response.body;
}
}
export function parseSort(this: IExecuteFunctions): object | null {
let sort;
const setSort = this.getNodeParameter('setSort', 0, false);
if (!setSort) {
sort = null;
} else {
sort = [];
const sortParametersUi = this.getNodeParameter('sortParametersUi', 0, {}) as IDataObject;
if (sortParametersUi.rules !== undefined) {
// @ts-ignore
for (const parameterData of sortParametersUi!.rules as IDataObject[]) {
// @ts-ignore
sort.push({
'fieldName': parameterData!.name as string,
'sortOrder': parameterData!.value
});
}
}
}
return sort;
}
export function parseScripts(this: IExecuteFunctions): object | null {
const setScriptAfter = this.getNodeParameter('setScriptAfter', 0, false);
const setScriptBefore = this.getNodeParameter('setScriptBefore', 0, false);
const setScriptSort = this.getNodeParameter('setScriptSort', 0, false);
if (!setScriptAfter && setScriptBefore && setScriptSort) {
return {};
} else {
const scripts = {} as ScriptsOptions;
if (setScriptAfter) {
scripts.script = this.getNodeParameter('scriptAfter', 0);
scripts!['script.param'] = this.getNodeParameter('scriptAfter', 0);
}
if (setScriptBefore) {
scripts['script.prerequest'] = this.getNodeParameter('scriptBefore', 0);
scripts['script.prerequest.param'] = this.getNodeParameter('scriptBeforeParam', 0);
}
if (setScriptSort) {
scripts['script.presort'] = this.getNodeParameter('scriptSort', 0);
scripts['script.presort.param'] = this.getNodeParameter('scriptSortParam', 0);
}
return scripts;
}
}
export function parsePortals(this: IExecuteFunctions): object | null {
let portals;
const getPortals = this.getNodeParameter('getPortals', 0);
if (!getPortals) {
portals = [];
} else {
portals = this.getNodeParameter('portals', 0);
}
// @ts-ignore
return portals;
}
export function parseQuery(this: IExecuteFunctions): object | null {
let queries;
const queriesParamUi = this.getNodeParameter('queries', 0, {}) as IDataObject;
if (queriesParamUi.query !== undefined) {
// @ts-ignore
queries = [];
for (const queryParam of queriesParamUi!.query as IDataObject[]) {
const query = {
'omit': queryParam.omit ? 'true' : 'false',
};
// @ts-ignore
for (const field of queryParam!.fields!.field as IDataObject[]) {
// @ts-ignore
query[field.name] = field!.value;
}
queries.push(query);
}
} else {
queries = null;
}
// @ts-ignore
return queries;
}
export function parseFields(this: IExecuteFunctions): object | null {
let fieldData;
const fieldsParametersUi = this.getNodeParameter('fieldsParametersUi', 0, {}) as IDataObject;
if (fieldsParametersUi.fields !== undefined) {
// @ts-ignore
fieldData = {};
for (const field of fieldsParametersUi!.fields as IDataObject[]) {
// @ts-ignore
fieldData[field.name] = field!.value;
}
} else {
fieldData = null;
}
// @ts-ignore
return fieldData;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,5 +1,5 @@
import { IDataObject } from 'n8n-workflow';
import { google } from 'googleapis';
import { google, sheets_v4 } from 'googleapis';
import { JWT } from 'google-auth-library';
import { getAuthenticationClient } from './GoogleApi';
@ -24,6 +24,18 @@ export interface ILookupValues {
lookupValue: string;
}
export interface IToDeleteRange {
amount: number;
startIndex: number;
sheetId: number;
}
export interface IToDelete {
[key: string]: IToDeleteRange[] | undefined;
columns?: IToDeleteRange[];
rows?: IToDeleteRange[];
}
export type ValueInputOption = 'RAW' | 'USER_ENTERED';
export type ValueRenderOption = 'FORMATTED_VALUE' | 'FORMULA' | 'UNFORMATTED_VALUE';
@ -85,6 +97,44 @@ export class GoogleSheet {
}
/**
* Returns the sheets in a Spreadsheet
*/
async spreadsheetGetSheets() {
const client = await this.getAuthenticationClient();
const response = await Sheets.spreadsheets.get(
{
auth: client,
spreadsheetId: this.id,
fields: 'sheets.properties'
}
);
return response.data;
}
/**
* Sets values in one or more ranges of a spreadsheet.
*/
async spreadsheetBatchUpdate(requests: sheets_v4.Schema$Request[]) { // tslint:disable-line:no-any
const client = await this.getAuthenticationClient();
const response = await Sheets.spreadsheets.batchUpdate(
{
auth: client,
spreadsheetId: this.id,
requestBody: {
requests,
},
}
);
return response.data;
}
/**
* Sets the cell values
*/

View file

@ -1,7 +1,11 @@
import { sheets_v4 } from 'googleapis';
import { IExecuteFunctions } from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
@ -11,6 +15,7 @@ import {
IGoogleAuthCredentials,
ILookupValues,
ISheetUpdateData,
IToDelete,
ValueInputOption,
ValueRenderOption,
} from './GoogleSheet';
@ -52,6 +57,11 @@ export class GoogleSheets implements INodeType {
value: 'clear',
description: 'Clears data from a Sheet',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete columns and rows from a Sheet',
},
{
name: 'Lookup',
value: 'lookup',
@ -81,17 +91,126 @@ export class GoogleSheets implements INodeType {
type: 'string',
default: '',
required: true,
description: 'The ID of the Google Sheet.',
description: 'The ID of the Google Sheet.<br />Found as part of the sheet URL https://docs.google.com/spreadsheets/d/{ID}/',
},
{
displayName: 'Range',
name: 'range',
type: 'string',
displayOptions: {
hide: {
operation: [
'delete'
],
},
},
default: 'A:F',
required: true,
description: 'The columns to read and append data to.<br />If it contains multiple sheets it can also be<br />added like this: "MySheet!A:F"',
description: 'The table range to read from or to append data to. See the Google <a href="https://developers.google.com/sheets/api/guides/values#writing">documentation</a> for the details.<br />If it contains multiple sheets it can also be<br />added like this: "MySheet!A:F"',
},
// ----------------------------------
// Delete
// ----------------------------------
{
displayName: 'To Delete',
name: 'toDelete',
placeholder: 'Add Columns/Rows to delete',
description: 'Deletes colums and rows from a sheet.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
operation: [
'delete'
],
},
},
default: {},
options: [
{
displayName: 'Columns',
name: 'columns',
values: [
{
displayName: 'Sheet',
name: 'sheetId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getSheets',
},
options: [],
default: '',
required: true,
description: 'The sheet to delete columns from',
},
{
displayName: 'Start Index',
name: 'startIndex',
type: 'number',
typeOptions: {
minValue: 0,
},
default: 0,
description: 'The start index (0 based and inclusive) of column to delete.',
},
{
displayName: 'Amount',
name: 'amount',
type: 'number',
typeOptions: {
minValue: 1,
},
default: 1,
description: 'Number of columns to delete.',
},
]
},
{
displayName: 'Rows',
name: 'rows',
values: [
{
displayName: 'Sheet',
name: 'sheetId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getSheets',
},
options: [],
default: '',
required: true,
description: 'The sheet to delete columns from',
},
{
displayName: 'Start Index',
name: 'startIndex',
type: 'number',
typeOptions: {
minValue: 0,
},
default: 0,
description: 'The start index (0 based and inclusive) of row to delete.',
},
{
displayName: 'Amount',
name: 'amount',
type: 'number',
typeOptions: {
minValue: 1,
},
default: 1,
description: 'Number of rows to delete.',
},
]
},
],
},
// ----------------------------------
// Read
// ----------------------------------
@ -178,6 +297,7 @@ export class GoogleSheets implements INodeType {
operation: [
'append',
'clear',
'delete',
],
rawData: [
true
@ -201,6 +321,7 @@ export class GoogleSheets implements INodeType {
hide: {
operation: [
'clear',
'delete',
],
rawData: [
true
@ -208,7 +329,7 @@ export class GoogleSheets implements INodeType {
},
},
default: 0,
description: 'Index of the row which contains the key. Starts with 0.',
description: 'Index of the row which contains the keys. Starts at 0.<br />The incoming node data is matched to the keys for assignment. The matching is case sensitve.',
},
@ -401,6 +522,48 @@ export class GoogleSheets implements INodeType {
};
methods = {
loadOptions: {
// Get all the sheets in a Spreadsheet
async getSheets(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const spreadsheetId = this.getCurrentNodeParameter('sheetId') as string;
const credentials = this.getCredentials('googleApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const googleCredentials = {
email: credentials.email,
privateKey: credentials.privateKey,
} as IGoogleAuthCredentials;
const sheet = new GoogleSheet(spreadsheetId, googleCredentials);
const responseData = await sheet.spreadsheetGetSheets();
if (responseData === undefined) {
throw new Error('No data got returned');
}
const returnData: INodePropertyOptions[] = [];
for (const sheet of responseData.sheets!) {
if (sheet.properties!.sheetType !== 'GRID') {
continue;
}
returnData.push({
name: sheet.properties!.title as string,
value: sheet.properties!.sheetId as unknown as string,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const spreadsheetId = this.getNodeParameter('sheetId', 0) as string;
const credentials = this.getCredentials('googleApi');
@ -416,10 +579,13 @@ export class GoogleSheets implements INodeType {
const sheet = new GoogleSheet(spreadsheetId, googleCredentials);
const range = this.getNodeParameter('range', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
let range = '';
if (operation !== 'delete') {
range = this.getNodeParameter('range', 0) as string;
}
const options = this.getNodeParameter('options', 0, {}) as IDataObject;
const valueInputMode = (options.valueInputMode || 'RAW') as ValueInputOption;
@ -452,6 +618,41 @@ export class GoogleSheets implements INodeType {
await sheet.clearData(range);
const items = this.getInputData();
return this.prepareOutputData(items);
} else if (operation === 'delete') {
// ----------------------------------
// delete
// ----------------------------------
const requests: sheets_v4.Schema$Request[] = [];
const toDelete = this.getNodeParameter('toDelete', 0) as IToDelete;
const deletePropertyToDimensions: IDataObject = {
'columns': 'COLUMNS',
'rows': 'ROWS',
};
for (const propertyName of Object.keys(deletePropertyToDimensions)) {
if (toDelete[propertyName] !== undefined) {
toDelete[propertyName]!.forEach(range => {
requests.push({
deleteDimension: {
range: {
sheetId: range.sheetId,
dimension: deletePropertyToDimensions[propertyName] as string,
startIndex: range.startIndex,
endIndex: range.startIndex + range.amount,
}
}
});
});
}
}
const data = await sheet.spreadsheetBatchUpdate(requests);
const items = this.getInputData();
return this.prepareOutputData(items);
} else if (operation === 'lookup') {

View file

@ -360,7 +360,6 @@ export class HttpRequest implements INodeType {
},
],
},
// Body Parameter
{
displayName: 'Body Parameters',

View file

@ -0,0 +1,411 @@
import { INodeProperties } from "n8n-workflow";
export const companyOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'company',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new company',
},
{
name: 'Get',
value: 'get',
description: 'Get data of a company',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get data of all companies',
},
{
name: 'Update',
value: 'update',
description: 'Update a company',
},
{
name: 'Users',
value: 'users',
description: `List company's users`,
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const companyFields = [
/* -------------------------------------------------------------------------- */
/* company:users */
/* -------------------------------------------------------------------------- */
{
displayName: 'List By',
name: 'listBy',
type: 'options',
displayOptions: {
show: {
resource: [
'company',
],
operation: [
'users',
],
},
},
options: [
{
name: 'ID',
value: 'id',
description: 'The Intercom defined id representing the company',
},
{
name: 'Company ID',
value: 'companyId',
description: 'The company_id you have given to the company',
},
],
default: '',
description: 'List by',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'company',
],
operation: [
'users',
],
},
},
description: 'View by value',
},
/* -------------------------------------------------------------------------- */
/* company:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'company',
],
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: {
resource: [
'company',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 60,
},
default: 50,
description: 'How many results to return.',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
resource: [
'company',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Segment ID',
name: 'segment_id',
type: 'string',
default: '',
description: 'Segment representing the Lead',
},
{
displayName: 'Tag ID',
name: 'tag_id',
type: 'string',
default: '',
description: 'Tag representing the Lead',
},
]
},
/* -------------------------------------------------------------------------- */
/* company:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Select By',
name: 'selectBy',
type: 'options',
displayOptions: {
show: {
resource: [
'company',
],
operation: [
'get',
],
},
},
options: [
{
name: 'Company ID',
value: 'companyId',
description: 'The company_id you have given to the company',
},
{
name: 'ID',
value: 'id',
description: 'The Intercom defined id representing the company',
},
{
name: 'Name',
value: 'name',
description: 'The name of the company',
},
],
default: '',
description: 'What property to use to query the company.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'company',
],
operation: [
'get',
],
},
},
description: 'View by value',
},
/* -------------------------------------------------------------------------- */
/* company:create/update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Company Id',
name: 'companyId',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'company',
],
operation: [
'create',
'update',
],
},
},
description: 'The company id you have defined for the company',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
operation: [
'create',
'update',
],
resource: [
'company',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: [
'create',
'update',
],
resource: [
'company',
],
},
},
options: [
{
displayName: 'Industry',
name: 'industry',
type: 'string',
default: '',
description: 'The industry that this company operates in',
},
{
displayName: 'Monthly Spend',
name: 'monthlySpend',
type: 'string',
default: '',
description: 'The phone number of the user',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: '',
description: 'Name of the user',
},
{
displayName: 'Plan',
name: 'plan',
type: 'string',
default: '',
placeholder: '',
description: 'The name of the plan you have associated with the company',
},
{
displayName: 'Size',
name: 'size',
type: 'number',
default: '',
description: 'The number of employees in this company',
},
{
displayName: 'Website',
name: 'website',
type: 'string',
default: '',
description: `The URL for this company's website. Please note that the value<br />specified here is not validated. Accepts any string.`,
},
]
},
{
displayName: 'Custom Attributes',
name: 'customAttributesJson',
type: 'json',
required: false,
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
resource: [
'company',
],
operation: [
'create',
'update',
],
jsonParameters: [
true,
],
},
},
default: '',
description: 'A hash of key/value pairs to represent custom data you want to attribute to a user.',
},
{
displayName: 'Custom Attributes',
name: 'customAttributesUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Attribute',
typeOptions: {
multipleValues: true,
},
required: false,
displayOptions: {
show: {
resource: [
'company',
],
operation: [
'create',
'update',
],
jsonParameters: [
false,
],
},
},
options: [
{
name: 'customAttributesValues',
displayName: 'Attributes',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
}
],
description: 'A hash of key/value pairs to represent custom data you want to attribute to a user.',
},
] as INodeProperties[];

View file

@ -0,0 +1,13 @@
import { IDataObject } from "n8n-workflow";
export interface ICompany {
remote_created_at?: string;
company_id?: string;
name?: string;
monthly_spend?: number;
plan?: string;
size?: number;
website?: string;
industry?: string;
custom_attributes?: IDataObject;
}

View file

@ -0,0 +1,82 @@
import { OptionsWithUri } from 'request';
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IExecuteSingleFunctions
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function intercomApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('intercomApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const headerWithAuthentication = Object.assign({},
{ Authorization: `Bearer ${credentials.apiKey}`, Accept: 'application/json' });
const options: OptionsWithUri = {
headers: headerWithAuthentication,
method,
qs: query,
uri: uri || `https://api.intercom.io${endpoint}`,
body,
json: true
};
try {
return await this.helpers.request!(options);
} catch (error) {
const errorMessage = error.response.body.message || error.response.body.Message;
if (errorMessage !== undefined) {
throw errorMessage;
}
throw error.response.body;
}
}
/**
* Make an API request to paginated intercom endpoint
* and return all results
*/
export async function intercomApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.per_page = 60;
let uri: string | undefined;
do {
responseData = await intercomApiRequest.call(this, endpoint, method, body, query, uri);
uri = responseData.pages.next;
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData.pages !== undefined &&
responseData.pages.next !== undefined &&
responseData.pages.next !== null
);
return returnData;
}
export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = '';
}
return result;
}

View file

@ -0,0 +1,529 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
INodeExecutionData,
INodeType,
ILoadOptionsFunctions,
INodePropertyOptions,
} from 'n8n-workflow';
import {
leadOpeations,
leadFields,
} from './LeadDescription';
import {
intercomApiRequest,
intercomApiRequestAllItems,
validateJSON,
} from './GenericFunctions';
import {
ILead,
ILeadCompany,
IAvatar,
} from './LeadInterface';
import { userOpeations, userFields } from './UserDescription';
import { IUser, IUserCompany } from './UserInterface';
import { companyOperations, companyFields } from './CompanyDescription';
import { ICompany } from './CompanyInteface';
export class Intercom implements INodeType {
description: INodeTypeDescription = {
displayName: 'Intercom',
name: 'intercom',
icon: 'file:intercom.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume intercom API',
defaults: {
name: 'Intercom',
color: '#c02428',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'intercomApi',
required: true,
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Company',
value: 'company',
description: 'Companies allow you to represent commercial organizations using your product.',
},
{
name: 'Lead',
value: 'lead',
description: 'Leads are useful for representing logged-out users of your application.',
},
{
name: 'User',
value: 'user',
description: 'The Users resource is the primary way of interacting with Intercom',
},
],
default: 'user',
description: 'Resource to consume.',
},
...leadOpeations,
...userOpeations,
...companyOperations,
...userFields,
...leadFields,
...companyFields,
],
};
methods = {
loadOptions: {
// Get all the available companies to display them to user so that he can
// select them easily
async getCompanies(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let companies, response;
try {
response = await intercomApiRequest.call(this, '/companies', 'GET');
} catch (err) {
throw new Error(`Intercom Error: ${err}`);
}
companies = response.companies;
for (const company of companies) {
const companyName = company.name;
const companyId = company.company_id;
returnData.push({
name: companyName,
value: companyId,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
let qs: IDataObject;
let responseData;
for (let i = 0; i < length; i++) {
qs = {};
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
//https://developers.intercom.com/intercom-api-reference/reference#leads
if (resource === 'lead') {
if (operation === 'create' || operation === 'update') {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const jsonActive = this.getNodeParameter('jsonParameters', i) as boolean;
const body: ILead = {};
if (operation === 'create') {
body.email = this.getNodeParameter('email', i) as string;
}
if (additionalFields.email) {
body.email = additionalFields.email as string;
}
if (additionalFields.phone) {
body.phone = additionalFields.phone as string;
}
if (additionalFields.name) {
body.name = additionalFields.name as string;
}
if (additionalFields.unsubscribedFromEmails) {
body.unsubscribed_from_emails = additionalFields.unsubscribedFromEmails as boolean;
}
if (additionalFields.updateLastRequestAt) {
body.update_last_request_at = additionalFields.updateLastRequestAt as boolean;
}
if (additionalFields.utmSource) {
body.utm_source = additionalFields.utmSource as string;
}
if (additionalFields.utmMedium) {
body.utm_medium = additionalFields.utmMedium as string;
}
if (additionalFields.utmCampaign) {
body.utm_campaign = additionalFields.utmCampaign as string;
}
if (additionalFields.utmTerm) {
body.utm_term = additionalFields.utmTerm as string;
}
if (additionalFields.utmContent) {
body.utm_content = additionalFields.utmContent as string;
}
if (additionalFields.avatar) {
const avatar: IAvatar = {
type: 'avatar',
image_url: additionalFields.avatar as string,
};
body.avatar = avatar;
}
if (additionalFields.companies) {
const companies: ILeadCompany[] = [];
// @ts-ignore
additionalFields.companies.forEach( o => {
const company: ILeadCompany = {};
company.company_id = o;
companies.push(company);
});
body.companies = companies;
}
if (!jsonActive) {
const customAttributesValues = (this.getNodeParameter('customAttributesUi', i) as IDataObject).customAttributesValues as IDataObject[];
if (customAttributesValues) {
const customAttributes = {};
for (let i = 0; i < customAttributesValues.length; i++) {
// @ts-ignore
customAttributes[customAttributesValues[i].name] = customAttributesValues[i].value;
}
body.custom_attributes = customAttributes;
}
} else {
const customAttributesJson = validateJSON(this.getNodeParameter('customAttributesJson', i) as string);
if (customAttributesJson) {
body.custom_attributes = customAttributesJson;
}
}
if (operation === 'update') {
const updateBy = this.getNodeParameter('updateBy', 0) as string;
const value = this.getNodeParameter('value', i) as string;
if (updateBy === 'userId') {
body.user_id = value;
}
if (updateBy === 'id') {
body.id = value;
}
}
try {
responseData = await intercomApiRequest.call(this, '/contacts', 'POST', body);
} catch (err) {
throw new Error(`Intercom Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'get') {
const selectBy = this.getNodeParameter('selectBy', 0) as string;
const value = this.getNodeParameter('value', i) as string;
if (selectBy === 'email') {
qs.email = value;
}
if (selectBy === 'userId') {
qs.user_id = value;
}
if (selectBy === 'phone') {
qs.phone = value;
}
try {
if (selectBy === 'id') {
responseData = await intercomApiRequest.call(this, `/contacts/${value}`, 'GET');
} else {
responseData = await intercomApiRequest.call(this, '/contacts', 'GET', {}, qs);
responseData = responseData.contacts;
}
} catch (err) {
throw new Error(`Intercom Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const filters = this.getNodeParameter('filters', i) as IDataObject;
Object.assign(qs, filters);
try {
if (returnAll === true) {
responseData = await intercomApiRequestAllItems.call(this, 'contacts', '/contacts', 'GET', {}, qs);
} else {
qs.per_page = this.getNodeParameter('limit', i) as number;
responseData = await intercomApiRequest.call(this, '/contacts', 'GET', {}, qs);
responseData = responseData.contacts;
}
} catch (err) {
throw new Error(`Intercom Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'delete') {
const deleteBy = this.getNodeParameter('deleteBy', 0) as string;
const value = this.getNodeParameter('value', i) as string;
try {
if (deleteBy === 'id') {
responseData = await intercomApiRequest.call(this, `/contacts/${value}`, 'DELETE');
} else {
qs.user_id = value;
responseData = await intercomApiRequest.call(this, '/contacts', 'DELETE', {}, qs);
}
} catch (err) {
throw new Error(`Intercom Error: ${JSON.stringify(err)}`);
}
}
}
//https://developers.intercom.com/intercom-api-reference/reference#users
if (resource === 'user') {
if (operation === 'create' || operation === 'update') {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const jsonActive = this.getNodeParameter('jsonParameters', i) as boolean;
const body: IUser = {};
if (operation === 'create') {
const identifierType = this.getNodeParameter('identifierType', i) as string;
if (identifierType === 'email') {
body.email = this.getNodeParameter('idValue', i) as string;
} else if (identifierType === 'userId') {
body.user_id = this.getNodeParameter('idValue', i) as string;
}
}
if (additionalFields.email) {
body.email = additionalFields.email as string;
}
if (additionalFields.userId) {
body.user_id = additionalFields.userId as string;
}
if (additionalFields.phone) {
body.phone = additionalFields.phone as string;
}
if (additionalFields.name) {
body.name = additionalFields.name as string;
}
if (additionalFields.unsubscribedFromEmails) {
body.unsubscribed_from_emails = additionalFields.unsubscribedFromEmails as boolean;
}
if (additionalFields.updateLastRequestAt) {
body.update_last_request_at = additionalFields.updateLastRequestAt as boolean;
}
if (additionalFields.sessionCount) {
body.session_count = additionalFields.sessionCount as number;
}
if (additionalFields.avatar) {
const avatar: IAvatar = {
type: 'avatar',
image_url: additionalFields.avatar as string,
};
body.avatar = avatar;
}
if (additionalFields.utmSource) {
body.utm_source = additionalFields.utmSource as string;
}
if (additionalFields.utmMedium) {
body.utm_medium = additionalFields.utmMedium as string;
}
if (additionalFields.utmCampaign) {
body.utm_campaign = additionalFields.utmCampaign as string;
}
if (additionalFields.utmTerm) {
body.utm_term = additionalFields.utmTerm as string;
}
if (additionalFields.utmContent) {
body.utm_content = additionalFields.utmContent as string;
}
if (additionalFields.companies) {
const companies: IUserCompany[] = [];
// @ts-ignore
additionalFields.companies.forEach( o => {
const company: IUserCompany = {};
company.company_id = o;
companies.push(company);
});
body.companies = companies;
}
if (additionalFields.sessionCount) {
body.session_count = additionalFields.sessionCount as number;
}
if (!jsonActive) {
const customAttributesValues = (this.getNodeParameter('customAttributesUi', i) as IDataObject).customAttributesValues as IDataObject[];
if (customAttributesValues) {
const customAttributes = {};
for (let i = 0; i < customAttributesValues.length; i++) {
// @ts-ignore
customAttributes[customAttributesValues[i].name] = customAttributesValues[i].value;
}
body.custom_attributes = customAttributes;
}
} else {
const customAttributesJson = validateJSON(this.getNodeParameter('customAttributesJson', i) as string);
if (customAttributesJson) {
body.custom_attributes = customAttributesJson;
}
}
if (operation === 'update') {
const updateBy = this.getNodeParameter('updateBy', 0) as string;
const value = this.getNodeParameter('value', i) as string;
if (updateBy === 'userId') {
body.user_id = value;
}
if (updateBy === 'id') {
body.id = value;
}
if (updateBy === 'email') {
body.email = value;
}
}
try {
responseData = await intercomApiRequest.call(this, '/users', 'POST', body, qs);
} catch (err) {
throw new Error(`Intercom Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'get') {
const selectBy = this.getNodeParameter('selectBy', 0) as string;
const value = this.getNodeParameter('value', i) as string;
if (selectBy === 'userId') {
qs.user_id = value;
}
try {
if (selectBy === 'id') {
responseData = await intercomApiRequest.call(this, `/users/${value}`, 'GET', {}, qs);
} else {
responseData = await intercomApiRequest.call(this, '/users', 'GET', {}, qs);
}
} catch (err) {
throw new Error(`Intercom Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const filters = this.getNodeParameter('filters', i) as IDataObject;
Object.assign(qs, filters);
try {
if (returnAll === true) {
responseData = await intercomApiRequestAllItems.call(this, 'users', '/users', 'GET', {}, qs);
} else {
qs.per_page = this.getNodeParameter('limit', i) as number;
responseData = await intercomApiRequest.call(this, '/users', 'GET', {}, qs);
responseData = responseData.users;
}
} catch (err) {
throw new Error(`Intercom Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'delete') {
const id = this.getNodeParameter('id', i) as string;
try {
responseData = await intercomApiRequest.call(this, `/users/${id}`, 'DELETE');
} catch (err) {
throw new Error(`Intercom Error: ${JSON.stringify(err)}`);
}
}
}
//https://developers.intercom.com/intercom-api-reference/reference#companies
if (resource === 'company') {
if (operation === 'create' || operation === 'update') {
const id = this.getNodeParameter('companyId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const jsonActive = this.getNodeParameter('jsonParameters', i) as boolean;
const body: ICompany = {
company_id: id,
};
if (additionalFields.monthlySpend) {
body.monthly_spend = additionalFields.monthlySpend as number;
}
if (additionalFields.name) {
body.name = additionalFields.name as string;
}
if (additionalFields.plan) {
body.plan = additionalFields.plan as string;
}
if (additionalFields.size) {
body.size = additionalFields.size as number;
}
if (additionalFields.website) {
body.website = additionalFields.website as string;
}
if (additionalFields.industry) {
body.industry = additionalFields.industry as string;
}
if (!jsonActive) {
const customAttributesValues = (this.getNodeParameter('customAttributesUi', i) as IDataObject).customAttributesValues as IDataObject[];
if (customAttributesValues) {
const customAttributes = {};
for (let i = 0; i < customAttributesValues.length; i++) {
// @ts-ignore
customAttributes[customAttributesValues[i].name] = customAttributesValues[i].value;
}
body.custom_attributes = customAttributes;
}
} else {
const customAttributesJson = validateJSON(this.getNodeParameter('customAttributesJson', i) as string);
if (customAttributesJson) {
body.custom_attributes = customAttributesJson;
}
}
try {
responseData = await intercomApiRequest.call(this, '/companies', 'POST', body, qs);
} catch (err) {
throw new Error(`Intercom Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'get') {
const selectBy = this.getNodeParameter('selectBy', 0) as string;
const value = this.getNodeParameter('value', i) as string;
if (selectBy === 'companyId') {
qs.company_id = value;
}
if (selectBy === 'name') {
qs.name = value;
}
try {
if (selectBy === 'id') {
responseData = await intercomApiRequest.call(this, `/companies/${value}`, 'GET', {}, qs);
} else {
responseData = await intercomApiRequest.call(this, '/companies', 'GET', {}, qs);
}
} catch (err) {
throw new Error(`Intercom Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const filters = this.getNodeParameter('filters', i) as IDataObject;
Object.assign(qs, filters);
try {
if (returnAll === true) {
responseData = await intercomApiRequestAllItems.call(this, 'companies', '/companies', 'GET', {}, qs);
} else {
qs.per_page = this.getNodeParameter('limit', i) as number;
responseData = await intercomApiRequest.call(this, '/companies', 'GET', {}, qs);
responseData = responseData.companies;
}
} catch (err) {
throw new Error(`Intercom Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'users') {
const filterBy = this.getNodeParameter('filterBy', 0) as string;
const value = this.getNodeParameter('value', i) as string;
if (filterBy === 'companyId') {
qs.company_id = value;
}
try {
if (filterBy === 'id') {
responseData = await intercomApiRequest.call(this, `/companies/${value}/users`, 'GET', {}, qs);
} else {
qs.type = 'users';
responseData = await intercomApiRequest.call(this, '/companies', 'GET', {}, qs);
}
} catch (err) {
throw new Error(`Intercom Error: ${JSON.stringify(err)}`);
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,521 @@
import { INodeProperties } from "n8n-workflow";
export const leadOpeations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'lead',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new lead',
},
{
name: 'Get',
value: 'get',
description: 'Get data of a lead',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get data of all leads',
},
{
name: 'Update',
value: 'update',
description: 'Update new lead',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a lead',
}
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const leadFields = [
/* -------------------------------------------------------------------------- */
/* lead:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Delete By',
name: 'deleteBy',
type: 'options',
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'delete',
],
},
},
options: [
{
name: 'ID',
value: 'id',
description: 'The Intercom defined id representing the Lead',
},
{
name: 'User ID',
value: 'userId',
description: 'Automatically generated identifier for the Lead',
},
],
default: '',
description: 'Delete by',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'delete',
],
},
},
description: 'Delete by value',
},
/* -------------------------------------------------------------------------- */
/* lead:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Select By',
name: 'selectBy',
type: 'options',
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'get',
],
},
},
options: [
{
name: 'Email',
value: 'email',
description: 'Email representing the Lead',
},
{
name: 'ID',
value: 'id',
description: 'The Intercom defined id representing the Lead',
},
{
name: 'User ID',
value: 'userId',
description: 'Automatically generated identifier for the Lead',
},
{
name: 'Phone',
value: 'phone',
description: 'Phone representing the Lead',
},
],
default: '',
description: 'The property to select the lead by.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'get',
],
},
},
description: 'View by value',
},
/* -------------------------------------------------------------------------- */
/* lead:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'lead',
],
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: {
resource: [
'lead',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 60,
},
default: 50,
description: 'How many results to return.',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: 'The email address of the lead',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
description: 'The phone number of the lead',
},
]
},
/* -------------------------------------------------------------------------- */
/* lead:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Update By',
name: 'updateBy',
type: 'options',
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'update',
],
},
},
options: [
{
name: 'User ID',
value: 'userId',
description: 'Automatically generated identifier for the Lead',
},
{
name: 'ID',
value: 'id',
description: 'The Intercom defined id representing the Lead',
},
],
default: 'id',
description: 'The property via which to query the lead.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'update',
],
},
},
description: 'Value of the property to identify the lead to update',
},
/* -------------------------------------------------------------------------- */
/* lead:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'create',
],
},
},
description: 'The email of the user.',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
operation: [
'create',
'update',
],
resource: [
'lead',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: [
'create',
'update',
],
resource: [
'lead',
],
},
},
options: [
{
displayName: 'Avatar',
name: 'avatar',
type: 'string',
default: '',
description: 'An avatar image URL. note: the image url needs to be https.',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the user',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
description: 'The phone number of the user',
},
{
displayName: 'Unsubscribed From Emails',
name: 'unsubscribedFromEmails',
type: 'boolean',
default: false,
description: 'Whether the Lead is unsubscribed from emails',
},
{
displayName: 'Update Last Request At',
name: 'updateLastRequestAt',
type: 'boolean',
default: false,
description: 'A boolean value, which if true, instructs Intercom to update the<br />users last_request_at value to the current API service time in<br />UTC. default value if not sent is false.',
},
{
displayName: 'Companies',
name: 'companies',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getCompanies',
},
default: [],
description: 'Identifies the companies this user belongs to.',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
displayOptions: {
show: {
'/resource': [
'lead',
],
'/operation': [
'update',
],
},
},
description: 'The email of the user.',
},
{
displayName: 'UTM Source',
name: 'utmSource',
type: 'string',
default: '',
description: 'An avatar image URL. note: the image url needs to be https.',
},
{
displayName: 'UTM Medium',
name: 'utmMedium',
type: 'string',
default: '',
description: 'Identifies what type of link was used',
},
{
displayName: 'UTM Campaign',
name: 'utmCampaign',
type: 'string',
default: '',
description: 'Identifies a specific product promotion or strategic campaign',
},
{
displayName: 'UTM Term',
name: 'utmTerm',
type: 'string',
default: '',
description: 'Identifies search terms',
},
{
displayName: 'UTM Content',
name: 'utmContent',
type: 'string',
default: '',
description: 'Identifies what specifically was clicked to bring the user to the site',
},
]
},
{
displayName: 'Custom Attributes',
name: 'customAttributesJson',
type: 'json',
required: false,
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'create',
'update',
],
jsonParameters: [
true,
],
},
},
default: '',
description: 'A hash of key/value pairs to represent custom data you want to attribute to a user.',
},
{
displayName: 'Custom Attributes',
name: 'customAttributesUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Attribute',
typeOptions: {
multipleValues: true,
},
required: false,
displayOptions: {
show: {
resource: [
'lead',
],
operation: [
'create',
'update',
],
jsonParameters: [
false,
],
},
},
options: [
{
name: 'customAttributesValues',
displayName: 'Attributes',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
}
],
description: 'A hash of key/value pairs to represent custom data you want to attribute to a user.',
},
] as INodeProperties[];

View file

@ -0,0 +1,29 @@
import { IDataObject } from "n8n-workflow";
export interface ILeadCompany {
company_id?: string;
}
export interface IAvatar {
type?: string;
image_url?: string;
}
export interface ILead {
user_id?: string;
id?: string;
email?: string;
phone?: string;
name?: string;
custom_attributes?: IDataObject;
companies?: ILeadCompany[];
last_request_at?: number;
unsubscribed_from_emails?: boolean;
update_last_request_at?: boolean;
avatar?: IAvatar;
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
utm_term?: string;
utm_content?: string;
}

View file

@ -0,0 +1,574 @@
import { INodeProperties } from "n8n-workflow";
export const userOpeations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'user',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new user',
},
{
name: 'Get',
value: 'get',
description: 'Get data of a user',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get data of all users',
},
{
name: 'Update',
value: 'update',
description: 'Update a user',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a user',
}
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const userFields = [
/* -------------------------------------------------------------------------- */
/* user:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'ID',
name: 'id',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'delete',
],
},
},
default: '',
description: 'The Intercom defined id representing the Lead',
},
/* -------------------------------------------------------------------------- */
/* user:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'user',
],
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: {
resource: [
'user',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 60,
},
default: 50,
description: 'How many results to return.',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Company ID',
name: 'company_id',
type: 'string',
default: '',
description: 'Company ID representing the user',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: 'The email address of the user',
},
{
displayName: 'Tag ID',
name: 'tag_id',
type: 'string',
default: '',
description: 'Tag representing the user',
},
{
displayName: 'Segment ID',
name: 'segment_id',
type: 'string',
default: '',
description: 'Segment representing the user',
},
],
},
/* -------------------------------------------------------------------------- */
/* user:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Select By',
name: 'selectBy',
type: 'options',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'get',
],
},
},
options: [
{
name: 'ID',
value: 'id',
default: '',
description: 'The Intercom defined id representing the Lead',
},
{
name: 'User ID',
value: 'userId',
default: '',
description: 'Automatically generated identifier for the Lead',
},
],
default: '',
description: 'The property to select the user by.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'get',
],
},
},
description: 'View by value',
},
/* -------------------------------------------------------------------------- */
/* user:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Update By',
name: 'updateBy',
type: 'options',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'update',
],
},
},
options: [
{
name: 'ID',
value: 'id',
description: 'The Intercom defined id representing the user',
},
{
name: 'Email',
value: 'email',
description: 'The email address of user',
},
{
name: 'User ID',
value: 'userId',
description: 'Automatically generated identifier for the user',
},
],
default: 'id',
description: 'The property via which to query the user.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'update',
],
},
},
description: 'Value of the property to identify the user to update',
},
/* -------------------------------------------------------------------------- */
/* user:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Identifier Type',
name: 'identifierType',
type: 'options',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
],
},
},
options: [
{
name: 'User ID',
value: 'userId',
description: 'A unique string identifier for the user. It is required on creation if an email is not supplied.',
},
{
name: 'Email',
value: 'email',
description: `The user's email address. It is required on creation if a user_id is not supplied.`,
},
],
default: '',
description: 'Unique string identifier',
},
{
displayName: 'Value',
name: 'idValue',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
],
},
},
description: 'Unique string identifier value',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
operation: [
'create',
'update',
],
resource: [
'user'
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: [
'create',
'update',
],
resource: [
'user'
],
},
},
options: [
{
displayName: 'Email',
name: 'email',
displayOptions: {
show: {
'/operation': [
'update',
],
'/resource': [
'user'
],
},
hide: {
'/updateBy': [
'email',
]
},
},
type: 'string',
default: '',
description: 'Email of the user',
},
{
displayName: 'User ID',
name: 'userId',
displayOptions: {
show: {
'/operation': [
'update',
],
'/resource': [
'user'
],
},
hide: {
'/updateBy': [
'email',
'userId',
]
},
},
type: 'string',
default: '',
description: 'Email of the user',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
description: 'The phone number of the user',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: '',
description: 'Name of the user',
},
{
displayName: 'Unsubscribed From Emails',
name: 'unsubscribedFromEmails',
type: 'boolean',
default: '',
placeholder: '',
description: 'Whether the user is unsubscribed from emails',
},
{
displayName: 'Update Last Request At',
name: 'updateLastRequestAt',
type: 'boolean',
default: false,
options: [],
description: 'A boolean value, which if true, instructs Intercom to update the users<br />last_request_at value to the current API service time in UTC.',
},
{
displayName: 'Session Count',
name: 'sessionCount',
type: 'number',
default: false,
options: [],
description: `How many sessions the user has recorded`,
},
{
displayName: 'Companies',
name: 'companies',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getCompanies',
},
default: [],
description: 'Identifies the companies this user belongs to.',
},
{
displayName: 'Avatar',
name: 'avatar',
type: 'string',
default: '',
description: 'An avatar image URL. note: the image url needs to be https.',
},
{
displayName: 'UTM Source',
name: 'utmSource',
type: 'string',
default: '',
description: 'An avatar image URL. note: the image url needs to be https.',
},
{
displayName: 'UTM Medium',
name: 'utmMedium',
type: 'string',
default: '',
description: 'Identifies what type of link was used',
},
{
displayName: 'UTM Campaign',
name: 'utmCampaign',
type: 'string',
default: '',
description: 'Identifies a specific product promotion or strategic campaign',
},
{
displayName: 'UTM Term',
name: 'utmTerm',
type: 'string',
default: '',
description: 'Identifies search terms',
},
{
displayName: 'UTM Content',
name: 'utmContent',
type: 'string',
default: '',
description: 'Identifies what specifically was clicked to bring the user to the site',
},
]
},
{
displayName: 'Custom Attributes',
name: 'customAttributesJson',
type: 'json',
required: false,
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
'update',
],
jsonParameters: [
true,
],
},
},
default: '',
description: 'A hash of key/value pairs to represent custom data you want to attribute to a user.',
},
{
displayName: 'Custom Attributes',
name: 'customAttributesUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Attribute',
typeOptions: {
multipleValues: true,
},
required: false,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
'update',
],
jsonParameters: [
false,
],
},
},
options: [
{
name: 'customAttributesValues',
displayName: 'Attributes',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
}
],
description: 'A hash of key/value pairs to represent custom data you want to attribute to a user.',
},
] as INodeProperties[];

View file

@ -0,0 +1,32 @@
import { IDataObject } from "n8n-workflow";
export interface IUserCompany {
company_id?: string;
}
export interface IAvatar {
type?: string;
image_url?: string;
}
export interface IUser {
user_id?: string;
id?: string;
email?: string;
phone?: string;
name?: string;
custom_attributes?: IDataObject;
companies?: IUserCompany[];
last_request_at?: number;
signed_up_at?: string;
unsubscribed_from_emails?: boolean;
update_last_request_at?: boolean;
last_seen_user_agent?: boolean;
session_count?: number;
avatar?: IAvatar;
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
utm_term?: string;
utm_content?: string;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -0,0 +1,58 @@
import { OptionsWithUri } from 'request';
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IExecuteSingleFunctions
} from 'n8n-core';
export async function mailchimpApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, endpoint: string, method: string, body: any = {}, headers?: object): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('mailchimpApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const headerWithAuthentication = Object.assign({}, headers, { Authorization: `apikey ${credentials.apiKey}` });
if (!(credentials.apiKey as string).includes('-')) {
throw new Error('The API key is not valid!');
}
const datacenter = (credentials.apiKey as string).split('-').pop();
const host = 'api.mailchimp.com/3.0';
const options: OptionsWithUri = {
headers: headerWithAuthentication,
method,
uri: `https://${datacenter}.${host}${endpoint}`,
json: true,
};
if (Object.keys(body).length !== 0) {
options.body = body;
}
try {
return await this.helpers.request!(options);
} catch (error) {
const errorMessage = error.response.body.message || error.response.body.Message;
if (errorMessage !== undefined) {
throw errorMessage;
}
throw error.response.body;
}
}
export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = '';
}
return result;
}

View file

@ -0,0 +1,558 @@
import * as moment from 'moment';
import {
IExecuteSingleFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeTypeDescription,
INodeExecutionData,
INodeType,
INodePropertyOptions,
} from 'n8n-workflow';
import {
mailchimpApiRequest,
validateJSON,
} from './GenericFunctions';
enum Status {
subscribe = 'subscribe',
unsubscribed = 'unsubscribe',
cleaned = 'cleaned',
pending = 'pending',
transactional = 'transactional',
}
interface ILocation {
latitude?: number;
longitude?: number;
}
interface ICreateMemberBody {
listId: string;
email_address: string;
email_type?: string;
status?: Status;
language?: string;
vip?: boolean;
location?: ILocation;
ips_signup?: string;
timestamp_signup?: string;
ip_opt?: string;
timestamp_opt?: string;
tags?: string[];
merge_fields?: IDataObject;
}
export class Mailchimp implements INodeType {
description: INodeTypeDescription = {
displayName: 'Mailchimp',
name: 'mailchimp',
icon: 'file:mailchimp.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Mailchimp API',
defaults: {
name: 'Mailchimp',
color: '#c02428',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'mailchimpApi',
required: true,
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Member',
value: 'member',
description: 'Add member to list',
},
],
default: 'member',
required: true,
description: 'Resource to consume.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
required: true,
displayOptions: {
show: {
resource: [
'member',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new member on list',
},
],
default: 'create',
description: 'The operation to perform.',
},
{
displayName: 'List',
name: 'list',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLists',
},
displayOptions: {
show: {
resource: [
'member',
],
operation: [
'create',
],
},
},
default: '',
options: [],
required: true,
description: 'List of lists',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'member',
],
operation: [
'create',
],
},
},
default: '',
description: 'Email address for a subscriber.',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
required: true,
displayOptions: {
show: {
resource: [
'member',
],
operation: [
'create',
],
},
},
options: [
{
name: 'Subscribed',
value: 'subscribed',
description: '',
},
{
name: 'Unsubscribed',
value: 'unsubscribed',
description: '',
},
{
name: 'Cleaned',
value: 'cleaned',
description: '',
},
{
name: 'Pending',
value: 'pending',
description: '',
},
{
name: 'Transactional',
value: 'transactional',
description: '',
},
],
default: '',
description: `Subscriber's current status.`,
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource:[
'member'
],
operation: [
'create',
],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource:[
'member',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Email Type',
name: 'emailType',
type: 'options',
options: [
{
name: 'Email',
value: 'email',
description: '',
},
{
name: 'Text',
value: 'text',
description: '',
},
],
default: '',
description: 'Type of email this member asked to get',
},
{
displayName: 'Signup IP',
name: 'ipSignup',
type: 'string',
default: '',
description: 'IP address the subscriber signed up from.',
},
{
displayName: 'Opt-in IP',
name: 'ipOptIn',
type: 'string',
default: '',
description: 'The IP address the subscriber used to confirm their opt-in status.',
},
{
displayName: 'Signup Timestamp',
name: 'timestampSignup',
type: 'dateTime',
default: '',
description: 'The date and time the subscriber signed up for the list in ISO 8601 format.',
},
{
displayName: 'Language',
name: 'language',
type: 'string',
default: '',
description: `If set/detected, the subscriber's language.`,
},
{
displayName: 'Vip',
name: 'vip',
type: 'boolean',
default: false,
description: `Vip status for subscribers`,
},
{
displayName: 'Opt-in Timestamp',
name: 'timestampOpt',
type: 'dateTime',
default: '',
description: `The date and time the subscribe confirmed their opt-in status in ISO 8601 format.`,
},
{
displayName: 'Tags',
name: 'tags',
type: 'string',
default: '',
description: `The tags that are associated with a member separeted by ,.`,
},
]
},
{
displayName: 'Location',
name: 'locationFieldsUi',
type: 'fixedCollection',
placeholder: 'Add Location',
default: {},
description: `Subscriber location information.n`,
displayOptions: {
show: {
resource:[
'member',
],
operation: [
'create',
],
jsonParameters: [
false,
],
},
},
options: [
{
name: 'locationFieldsValues',
displayName: 'Location',
values: [
{
displayName: 'Latitude',
name: 'latitude',
type: 'string',
required: true,
description: 'The location latitude.',
default: '',
},
{
displayName: 'Longitude',
name: 'longitude',
type: 'string',
required: true,
description: 'The location longitude.',
default: '',
},
],
}
],
},
{
displayName: 'Merge Fields',
name: 'mergeFieldsUi',
placeholder: 'Add Merge Fields',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource:[
'member'
],
operation: [
'create',
],
jsonParameters: [
false,
],
},
},
description: 'An individual merge var and value for a member.',
options: [
{
name: 'mergeFieldsValues',
displayName: 'Field',
typeOptions: {
multipleValueButtonText: 'Add Field',
},
values: [
{
displayName: 'Field Name',
name: 'name',
type: 'string',
required: true,
description: 'Merge Field name',
default: '',
},
{
displayName: 'Field Value',
name: 'value',
required: true,
type: 'string',
default: '',
description: 'Merge field value.',
},
],
},
],
},
{
displayName: 'Merge Fields',
name: 'mergeFieldsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: '',
displayOptions: {
show: {
resource:[
'member',
],
operation: [
'create',
],
jsonParameters: [
true,
],
},
},
},
{
displayName: 'Location',
name: 'locationJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: '',
displayOptions: {
show: {
resource:[
'member',
],
operation: [
'create',
],
jsonParameters: [
true,
],
},
},
},
]
};
methods = {
loadOptions: {
// Get all the available lists to display them to user so that he can
// select them easily
async getLists(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let lists, response;
try {
response = await mailchimpApiRequest.call(this, '/lists', 'GET');
lists = response.lists;
} catch (err) {
throw new Error(`Mailchimp Error: ${err}`);
}
for (const list of lists) {
const listName = list.name;
const listId = list.id;
returnData.push({
name: listName,
value: listId,
});
}
return returnData;
},
}
};
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
let response = {};
const resource = this.getNodeParameter('resource') as string;
const operation = this.getNodeParameter('operation') as string;
if (resource === 'member') {
if (operation === 'create') {
const listId = this.getNodeParameter('list') as string;
const email = this.getNodeParameter('email') as string;
const status = this.getNodeParameter('status') as Status;
const options = this.getNodeParameter('options') as IDataObject;
const jsonActive = this.getNodeParameter('jsonParameters') as IDataObject;
const body: ICreateMemberBody = {
listId,
email_address: email,
status
};
if (options.emailType) {
body.email_type = options.emailType as string;
}
if (options.languaje) {
body.language = options.language as string;
}
if (options.vip) {
body.vip = options.vip as boolean;
}
if (options.ipSignup) {
body.ips_signup = options.ipSignup as string;
}
if (options.ipOptIn) {
body.ip_opt = options.ipOptIn as string;
}
if (options.timestampOpt) {
body.timestamp_opt = moment(options.timestampOpt as string).format('YYYY-MM-DD HH:MM:SS') as string;
}
if (options.timestampSignup) {
body.timestamp_signup = moment(options.timestampSignup as string).format('YYYY-MM-DD HH:MM:SS') as string;
}
if (options.tags) {
// @ts-ignore
body.tags = options.tags.split(',') as string[];
}
if (!jsonActive) {
const locationValues = (this.getNodeParameter('locationFieldsUi') as IDataObject).locationFieldsValues as IDataObject;
if (locationValues) {
const location: ILocation = {};
for (const key of Object.keys(locationValues)) {
if (key === 'latitude') {
location.latitude = parseInt(locationValues[key] as string, 10) as number;
} else if (key === 'longitude') {
location.longitude = parseInt(locationValues[key] as string, 10) as number;
}
}
body.location = location;
}
const mergeFieldsValues = (this.getNodeParameter('mergeFieldsUi') as IDataObject).mergeFieldsValues as IDataObject[];
if (mergeFieldsValues) {
const mergeFields = {};
for (let i = 0; i < mergeFieldsValues.length; i++) {
// @ts-ignore
mergeFields[mergeFieldsValues[i].name] = mergeFieldsValues[i].value;
}
body.merge_fields = mergeFields;
}
} else {
const locationJson = validateJSON(this.getNodeParameter('locationJson') as string);
const mergeFieldsJson = validateJSON(this.getNodeParameter('mergeFieldsJson') as string);
if (locationJson) {
body.location = locationJson;
}
if (mergeFieldsJson) {
body.merge_fields = mergeFieldsJson;
}
}
try {
response = await mailchimpApiRequest.call(this, `/lists/${listId}/members`, 'POST', body);
} catch (err) {
throw new Error(`Mailchimp Error: ${JSON.stringify(err)}`);
}
}
}
return {
json: response,
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -54,7 +54,7 @@ export class Merge implements INodeType {
{
name: 'Pass-through',
value: 'passThrough',
description: 'Passes through data of one input. The output will conain only items of the defined input.',
description: 'Passes through data of one input. The output will contain only items of the defined input.',
},
{
name: 'Remove Key Matches',

View file

@ -71,11 +71,12 @@ async function getAccessToken(this: IHookFunctions | IExecuteFunctions | IExecut
throw error.response.body;
}
}
/**
* Make an API request to paginated paypal endpoint
* and return all results
*/
export async function paypalApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise<any> { // tslint:disable-line:no-any
export async function payPalApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
@ -84,7 +85,7 @@ export async function paypalApiRequestAllItems(this: IHookFunctions | IExecuteFu
query!.page_size = 1000;
do {
responseData = await paypalApiRequest.call(this, endpoint, method, body, query, uri);
responseData = await payPalApiRequest.call(this, endpoint, method, body, query, uri);
uri = getNext(responseData.links);
returnData.push.apply(returnData, responseData[propertyName]);
} while (

View file

@ -1,7 +1,6 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
@ -9,8 +8,10 @@ import {
INodeType,
} from 'n8n-workflow';
import {
payoutOpeations,
payoutOperations,
payoutItemOperations,
payoutFields,
payoutItemFields,
} from './PaymentDescription';
import {
IPaymentBatch,
@ -21,14 +22,14 @@ import {
} from './PaymentInteface';
import {
validateJSON,
paypalApiRequest,
paypalApiRequestAllItems
payPalApiRequest,
payPalApiRequestAllItems
} from './GenericFunctions';
export class PayPal implements INodeType {
description: INodeTypeDescription = {
displayName: 'PayPal',
name: 'paypal',
name: 'payPal',
icon: 'file:paypal.png',
group: ['output'],
version: 1,
@ -42,7 +43,7 @@ export class PayPal implements INodeType {
outputs: ['main'],
credentials: [
{
name: 'paypalApi',
name: 'payPalApi',
required: true,
}
],
@ -55,14 +56,21 @@ export class PayPal implements INodeType {
{
name: 'Payout',
value: 'payout',
description: 'Use the Payouts API to make payments to multiple PayPal or Venmo recipients. The Payouts API is a fast, convenient way to send commissions, rebates, rewards, and general disbursements. You can send up to 15,000 payments per call. If you integrated the Payouts API before September 1, 2017, you receive transaction reports through Mass Payments Reporting.',
},
{
name: 'Payout Item',
value: 'payoutItem',
},
],
default: 'payout',
description: 'Resource to consume.',
},
...payoutOpeations,
// Payout
...payoutOperations,
...payoutItemOperations,
...payoutFields,
...payoutItemFields,
],
};
@ -71,10 +79,12 @@ export class PayPal implements INodeType {
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
let responseData;
let qs: IDataObject = {};
const qs: IDataObject = {};
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
if (resource === 'payout') {
if (operation === 'create') {
const body: IPaymentBatch = {};
@ -97,18 +107,18 @@ export class PayPal implements INodeType {
const payoutItems: IItem[] = [];
const itemsValues = (this.getNodeParameter('itemsUi', i) as IDataObject).itemsValues as IDataObject[];
if (itemsValues && itemsValues.length > 0) {
itemsValues.forEach( o => {
const payoutItem: IItem = {};
const amount: IAmount = {};
amount.currency = o.currency as string;
amount.value = parseFloat(o.amount as string);
payoutItem.amount = amount;
payoutItem.note = o.note as string || '';
payoutItem.receiver = o.receiverValue as string;
payoutItem.recipient_type = o.recipientType as RecipientType;
payoutItem.recipient_wallet = o.recipientWallet as RecipientWallet;
payoutItem.sender_item_id = o.senderItemId as string || '';
payoutItems.push(payoutItem);
itemsValues.forEach(o => {
const payoutItem: IItem = {};
const amount: IAmount = {};
amount.currency = o.currency as string;
amount.value = parseFloat(o.amount as string);
payoutItem.amount = amount;
payoutItem.note = o.note as string || '';
payoutItem.receiver = o.receiverValue as string;
payoutItem.recipient_type = o.recipientType as RecipientType;
payoutItem.recipient_wallet = o.recipientWallet as RecipientWallet;
payoutItem.sender_item_id = o.senderItemId as string || '';
payoutItems.push(payoutItem);
});
body.items = payoutItems;
} else {
@ -119,40 +129,41 @@ export class PayPal implements INodeType {
body.items = itemsJson;
}
try {
responseData = await paypalApiRequest.call(this, '/payments/payouts', 'POST', body);
responseData = await payPalApiRequest.call(this, '/payments/payouts', 'POST', body);
} catch (err) {
throw new Error(`Paypal Error: ${JSON.stringify(err)}`);
throw new Error(`PayPal Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'get') {
const payoutItemId = this.getNodeParameter('payoutItemId', i) as string;
try {
responseData = await paypalApiRequest.call(this,`/payments/payouts-item/${payoutItemId}`, 'GET', {}, qs);
} catch (err) {
throw new Error(`Paypal Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'getAll') {
const payoutBatchId = this.getNodeParameter('payoutBatchId', i) as string;
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
try {
if (returnAll === true) {
responseData = await paypalApiRequestAllItems.call(this, 'items', `/payments/payouts/${payoutBatchId}`, 'GET', {}, qs);
responseData = await payPalApiRequestAllItems.call(this, 'items', `/payments/payouts/${payoutBatchId}`, 'GET', {}, qs);
} else {
qs.page_size = this.getNodeParameter('limit', i) as number;
responseData = await paypalApiRequest.call(this,`/payments/payouts/${payoutBatchId}`, 'GET', {}, qs);
responseData = await payPalApiRequest.call(this, `/payments/payouts/${payoutBatchId}`, 'GET', {}, qs);
responseData = responseData.items;
}
} catch (err) {
throw new Error(`Paypal Error: ${JSON.stringify(err)}`);
throw new Error(`PayPal Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'delete') {
} else if (resource === 'payoutItem') {
if (operation === 'get') {
const payoutItemId = this.getNodeParameter('payoutItemId', i) as string;
try {
responseData = await paypalApiRequest.call(this,`/payments/payouts-item/${payoutItemId}/cancel`, 'POST', {}, qs);
responseData = await payPalApiRequest.call(this,`/payments/payouts-item/${payoutItemId}`, 'GET', {}, qs);
} catch (err) {
throw new Error(`Paypal Error: ${JSON.stringify(err)}`);
throw new Error(`PayPal Error: ${JSON.stringify(err)}`);
}
}
if (operation === 'cancel') {
const payoutItemId = this.getNodeParameter('payoutItemId', i) as string;
try {
responseData = await payPalApiRequest.call(this,`/payments/payouts-item/${payoutItemId}/cancel`, 'POST', {}, qs);
} catch (err) {
throw new Error(`PayPal Error: ${JSON.stringify(err)}`);
}
}
}

View file

@ -1,6 +1,6 @@
import { INodeProperties } from "n8n-workflow";
export const payoutOpeations = [
export const payoutOperations = [
{
displayName: 'Operation',
name: 'operation',
@ -21,17 +21,7 @@ export const payoutOpeations = [
{
name: 'Get',
value: 'get',
description: 'Show payout item details',
},
{
name: 'Get All',
value: 'getAll',
description: 'Show payout batch details',
},
{
name: 'Delete',
value: 'delete',
description: 'Cancels an unclaimed payout item, by ID.',
description: 'Show batch payout details',
},
],
default: 'create',
@ -120,7 +110,7 @@ export const payoutFields = [
{
name: 'Email',
value: 'email',
description: 'The unencrypted email. Value is a string of up to 127 single-byte characters.',
description: 'The unencrypted email.',
},
{
name: 'PayPal ID',
@ -137,7 +127,7 @@ export const payoutFields = [
type: 'string',
required: true,
default: '',
description: 'The receiver of the payment. Corresponds to the recipient_type value in the request. Max value of up to 127 single-byte characters.',
description: 'The receiver of the payment. Corresponds to the recipient_type value<br />in the request. Max length: 127 characters.',
},
{
displayName: 'Currency',
@ -190,7 +180,7 @@ export const payoutFields = [
type: 'string',
required: false,
default: '',
description: 'The sender-specified note for notifications. Supports up to 4000 ASCII characters and 1000 non-ASCII characters.',
description: 'The sender-specified note for notifications. Supports up to<br />4000 ASCII characters and 1000 non-ASCII characters.',
},
{
displayName: 'Sender Item ID',
@ -266,128 +256,164 @@ export const payoutFields = [
name: 'emailSubject',
type: 'string',
default: '',
description: 'The subject line for the email that PayPal sends when payment for a payout item completes. The subject line is the same for all recipients. Value is an alphanumeric string of up to 255 single-byte characters.',
description: 'The subject line for the email that PayPal sends when payment<br />for a payout item completes. The subject line is the same for all<br />recipients. Max length: 255 characters.',
},
{
displayName: 'Email Message',
name: 'emailMessage',
type: 'string',
default: '',
description: 'The email message that PayPal sends when the payout item completes. The message is the same for all recipients.',
description: 'The email message that PayPal sends when the payout item completes.<br />The message is the same for all recipients.',
},
{
displayName: 'Note',
name: 'note',
type: 'string',
default: '',
description: 'The payouts and item-level notes are concatenated in the email. The maximum combined length of the notes is 1000 characters.',
description: 'The payouts and item-level notes are concatenated in the email.<br />Max length: 1000 characters.',
},
],
},
/* -------------------------------------------------------------------------- */
/* payout:getAll */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* payout:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Payout Batch Id',
name: 'payoutBatchId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'getAll',
],
{
displayName: 'Payout Batch Id',
name: 'payoutBatchId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'get',
],
},
},
description: 'The ID of the payout for which to show details.',
},
description: 'The ID of the payout for which to show details.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'getAll',
],
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'get',
],
},
},
description: 'If all results should be returned or only up to a given limit.',
},
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
typeOptions: {
maxValue: 1000,
minValue: 1
},
default: 100,
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'getAll',
],
returnAll: [
false,
],
{
displayName: 'Limit',
name: 'limit',
type: 'number',
typeOptions: {
maxValue: 1000,
minValue: 1
},
default: 100,
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'get',
],
returnAll: [
false,
],
},
},
description: 'If all results should be returned or only up to a given limit.',
},
description: 'If all results should be returned or only up to a given limit.',
},
/* -------------------------------------------------------------------------- */
/* payout:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Payout Item Id',
name: 'payoutItemId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'get',
],
},
},
description: 'The ID of the payout item for which to show details.',
},
/* -------------------------------------------------------------------------- */
/* payout:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Payout Item Id',
name: 'payoutItemId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'payout',
],
operation: [
'delete',
],
},
},
description: 'The ID of the payout item to cancel.',
},
] as INodeProperties[];
export const payoutItemOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'payoutItem',
],
},
},
options: [
{
name: 'Cancel',
value: 'cancel',
description: 'Cancels an unclaimed payout item',
},
{
name: 'Get',
value: 'get',
description: 'Show payout item details',
},
],
default: 'get',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const payoutItemFields = [
/* -------------------------------------------------------------------------- */
/* payoutItem:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Payout Item Id',
name: 'payoutItemId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'payoutItem',
],
operation: [
'get',
],
},
},
description: 'The ID of the payout item for which to show details.',
},
/* -------------------------------------------------------------------------- */
/* payoutItem:cancel */
/* -------------------------------------------------------------------------- */
{
displayName: 'Payout Item Id',
name: 'payoutItemId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'payoutItem',
],
operation: [
'cancel',
],
},
},
description: 'The ID of the payout item to cancel.',
},
] as INodeProperties[];

View file

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -199,11 +199,11 @@ export class Telegram implements INodeType {
displayOptions: {
show: {
operation: [
'getChat',
'leaveChat',
'getChatMember',
'setChatDescription',
'setChatTitle',
'get',
'leave',
'member',
'setDescription',
'setTitle',
'sendAudio',
'sendChatAction',
'sendDocument',
@ -433,7 +433,7 @@ export class Telegram implements INodeType {
},
},
required: true,
description: 'Unique identifier for the target chat or username of the target<br />channel (in the format @channelusername).',
description: 'Unique identifier for the target chat or username of the target<br />channel (in the format @channelusername). To find your chat id ask @get_id_bot.',
},
{
displayName: 'Message ID',

View file

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-base",
"version": "0.30.0",
"version": "0.31.0",
"description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -34,14 +34,17 @@
"dist/credentials/ChargebeeApi.credentials.js",
"dist/credentials/DropboxApi.credentials.js",
"dist/credentials/FreshdeskApi.credentials.js",
"dist/credentials/FileMaker.credentials.js",
"dist/credentials/GithubApi.credentials.js",
"dist/credentials/GitlabApi.credentials.js",
"dist/credentials/GoogleApi.credentials.js",
"dist/credentials/HttpBasicAuth.credentials.js",
"dist/credentials/HttpDigestAuth.credentials.js",
"dist/credentials/HttpHeaderAuth.credentials.js",
"dist/credentials/IntercomApi.credentials.js",
"dist/credentials/Imap.credentials.js",
"dist/credentials/LinkFishApi.credentials.js",
"dist/credentials/MailchimpApi.credentials.js",
"dist/credentials/MailgunApi.credentials.js",
"dist/credentials/MandrillApi.credentials.js",
"dist/credentials/MattermostApi.credentials.js",
@ -49,8 +52,8 @@
"dist/credentials/NextCloudApi.credentials.js",
"dist/credentials/OpenWeatherMapApi.credentials.js",
"dist/credentials/PipedriveApi.credentials.js",
"dist/credentials/Postgres.credentials.js",
"dist/credentials/PayPalApi.credentials.js",
"dist/credentials/Postgres.credentials.js",
"dist/credentials/PayPalApi.credentials.js",
"dist/credentials/Redis.credentials.js",
"dist/credentials/RocketchatApi.credentials.js",
"dist/credentials/SlackApi.credentials.js",
@ -84,6 +87,7 @@
"dist/nodes/EmailSend.node.js",
"dist/nodes/ErrorTrigger.node.js",
"dist/nodes/ExecuteCommand.node.js",
"dist/nodes/FileMaker/FileMaker.node.js",
"dist/nodes/Freshdesk/Freshdesk.node.js",
"dist/nodes/Function.node.js",
"dist/nodes/FunctionItem.node.js",
@ -97,7 +101,9 @@
"dist/nodes/HttpRequest.node.js",
"dist/nodes/If.node.js",
"dist/nodes/Interval.node.js",
"dist/nodes/Intercom/Intercom.node.js",
"dist/nodes/LinkFish/LinkFish.node.js",
"dist/nodes/Mailchimp/Mailchimp.node.js",
"dist/nodes/Mailgun/Mailgun.node.js",
"dist/nodes/Mandrill/Mandrill.node.js",
"dist/nodes/Mattermost/Mattermost.node.js",
@ -109,9 +115,14 @@
"dist/nodes/OpenWeatherMap.node.js",
"dist/nodes/Pipedrive/Pipedrive.node.js",
"dist/nodes/Pipedrive/PipedriveTrigger.node.js",
<<<<<<< HEAD
"dist/nodes/Postgres/Postgres.node.js",
"dist/nodes/Paypal/PayPal.node.js",
"dist/nodes/Paypal/PayPalTrigger.node.js",
=======
"dist/nodes/Postgres/Postgres.node.js",
"dist/nodes/PayPal/PayPal.node.js",
>>>>>>> master
"dist/nodes/Rocketchat/Rocketchat.node.js",
"dist/nodes/ReadBinaryFile.node.js",
"dist/nodes/ReadBinaryFiles.node.js",
@ -153,11 +164,11 @@
"@types/node": "^10.10.1",
"@types/nodemailer": "^4.6.5",
"@types/redis": "^2.8.11",
"@types/request-promise-native": "^1.0.15",
"@types/request-promise-native": "~1.0.15",
"@types/xml2js": "^0.4.3",
"gulp": "^4.0.0",
"jest": "^24.9.0",
"n8n-workflow": "~0.15.0",
"n8n-workflow": "~0.16.0",
"ts-jest": "^24.0.2",
"tslint": "^5.17.0",
"typescript": "~3.5.2"
@ -174,7 +185,7 @@
"lodash.set": "^4.3.2",
"lodash.unset": "^4.5.2",
"mongodb": "^3.3.2",
"n8n-core": "~0.15.0",
"n8n-core": "~0.16.0",
"nodemailer": "^5.1.1",
"pdf-parse": "^1.1.1",
"pg-promise": "^9.0.3",

View file

@ -1,6 +1,6 @@
{
"name": "n8n-workflow",
"version": "0.15.0",
"version": "0.16.0",
"description": "Workflow base code of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",

View file

@ -343,7 +343,7 @@ export interface INodeProperties {
export interface INodePropertyOptions {
name: string;
value: string;
value: string | number;
description?: string;
}

View file

@ -321,8 +321,8 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
if (returnDefaults === true) {
// Set also when it has the default value
if (['boolean', 'number'].includes(nodeProperties.type)) {
// Boolean and numbers are special as false and 0 are valid values
if (['boolean', 'number', 'options'].includes(nodeProperties.type)) {
// Boolean, numbers and options are special as false and 0 are valid values
// and should not be replaced with default value
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name] !== undefined ? nodeValues[nodeProperties.name] : nodeProperties.default;
} else {

View file

@ -1061,6 +1061,15 @@ export class Workflow {
return null;
}
if (runExecutionData.resultData.lastNodeExecuted === node.name && runExecutionData.resultData.error !== undefined) {
// The node did already fail. So throw an error here that it displays and logs it correctly.
// Does get used by webhook and trigger nodes in case they throw an error that it is possible
// to log the error and display in Editor-UI.
const error = new Error(runExecutionData.resultData.error.message);
error.stack = runExecutionData.resultData.error.stack;
throw error;
}
if (nodeType.executeSingle) {
const returnPromises: Array<Promise<INodeExecutionData>> = [];