🔀 Merge branch 'master' into Doc-Link-to-Node-Credentials-Modal

This commit is contained in:
Jan Oberhauser 2020-08-27 09:35:22 +02:00
commit d2110f677e
79 changed files with 7685 additions and 719 deletions

View file

@ -63,9 +63,9 @@ check out our job posts:
## What does n8n mean and how do you pronounce it
## What does n8n mean and how do you pronounce it?
**Short answer:** It means "nodemation"
**Short answer:** It means "nodemation" and it is pronounced as n-eight-n.
**Long answer:** I get that question quite often (more often than I expected)
so I decided it is probably best to answer it here. While looking for a

View file

@ -260,9 +260,9 @@ docker build --build-arg N8N_VERSION=0.18.1 -t n8nio/n8n:0.18.1 .
```
## What does n8n mean and how do you pronounce it
## What does n8n mean and how do you pronounce it?
**Short answer:** It means "nodemation"
**Short answer:** It means "nodemation" and it is pronounced as n-eight-n.
**Long answer:** I get that question quite often (more often than I expected)
so I decided it is probably best to answer it here. While looking for a

View file

@ -2,6 +2,26 @@
This list shows all the versions which include breaking changes and how to upgrade.
## 0.79.0
### What changed?
We have renamed the operations in the Todoist Node for consistency with the codebase. We also deleted the `close_match` and `delete_match` operations as these can be accomplished using the following operations: `getAll`, `close`, and `delete`.
### When is action necessary?
When one of the following operations is used:
- close_by
- close_match
- delete_id
- delete_match
### How to upgrade:
After upgrading, open all workflows which contain the Todoist Node. Set the corresponding operation, and then save the workflow.
If the operations `close_match` or `delete_match` are used, recreate them using the operations: `getAll`, `delete`, and `close`.
## 0.69.0

View file

@ -60,9 +60,9 @@ If you are interested in a hosted version of n8n on our infrastructure please co
## What does n8n mean and how do you pronounce it
## What does n8n mean and how do you pronounce it?
**Short answer:** It means "nodemation"
**Short answer:** It means "nodemation" and it is pronounced as n-eight-n.
**Long answer:** I get that question quite often (more often than I expected)
so I decided it is probably best to answer it here. While looking for a

View file

@ -417,3 +417,5 @@ export interface ITimeoutHMS {
minutes: number;
seconds: number;
}
export type WorkflowTitleStatus = 'EXECUTING' | 'IDLE' | 'ERROR';

View file

@ -1,6 +1,6 @@
<template>
<div v-if="dialogVisible" @keydown.stop>
<el-dialog :visible="dialogVisible" append-to-body width="55%" :title="title" :nodeType="nodeType" :before-close="closeDialog">
<el-dialog :visible="dialogVisible" append-to-body width="75%" :title="title" :nodeType="nodeType" :before-close="closeDialog">
<div name="title" class="titleContainer" slot="title">
<div id="left">{{title}}</div>
<div id="right">
@ -271,7 +271,7 @@ export default mixins(
this.$showMessage({
title: 'Credentials created',
message: `The credential "${eventData.data.name}" got created!`,
message: `"${eventData.data.name}" credentials were successfully created!`,
type: 'success',
});
@ -284,7 +284,7 @@ export default mixins(
this.$showMessage({
title: 'Credentials updated',
message: `The credential "${eventData.data.name}" got updated!`,
message: `"${eventData.data.name}" credentials were successfully updated!`,
type: 'success',
});

View file

@ -44,19 +44,19 @@
<el-button title="Connect OAuth Credentials" circle :disabled="true">
<font-awesome-icon icon="redo" />
</el-button>
Not all required credential properties are filled
Enter all required properties
</span>
<span v-else-if="isOAuthConnected === true">
<el-button title="Reconnect OAuth Credentials" @click.stop="oAuthCredentialAuthorize()" circle>
<font-awesome-icon icon="redo" />
</el-button>
Is connected
Connected
</span>
<span v-else>
<el-button title="Connect OAuth Credentials" @click.stop="oAuthCredentialAuthorize()" circle>
<font-awesome-icon icon="sign-in-alt" />
</el-button>
Is NOT connected
Not connected
</span>
<div v-if="credentialProperties.length">
@ -91,7 +91,7 @@
<div v-if="nodesAccess.length === 0" class="no-nodes-access">
<strong>
Important!
Important
</strong><br />
Add at least one node which has access to the credentials!
</div>
@ -163,8 +163,8 @@ export default mixins(
isMinimized: true,
helpTexts: {
credentialsData: 'The credentials to set.',
credentialsName: 'The name the credentials should be saved as. Use a name<br />which makes it clear to what exactly they give access to.<br />For credentials of an Email account that could be the Email address itself.',
nodesWithAccess: 'The nodes which allowed to use this credentials.',
credentialsName: 'A recognizable label for the credentials. Descriptive names work <br />best here, so you can easily select it from a list later.',
nodesWithAccess: 'Nodes with access to these credentials.',
},
credentialDataTemp: null as ICredentialsDecryptedResponse | null,
nodesAccess: [] as string[],
@ -256,7 +256,7 @@ export default mixins(
this.$showMessage({
title: 'Copied',
message: `The callback URL got copied!`,
message: `Callback URL was successfully copied!`,
type: 'success',
});
},
@ -401,7 +401,7 @@ export default mixins(
this.$showMessage({
title: 'Connected',
message: 'Got connected!',
message: 'Connected successfully!',
type: 'success',
});
}

View file

@ -124,7 +124,7 @@ export default mixins(
try {
this.credentials = JSON.parse(JSON.stringify(this.$store.getters.allCredentials));
} catch (error) {
this.$showError(error, 'Proble loading credentials', 'There was a problem loading the credentials:');
this.$showError(error, 'Problem loading credentials', 'There was a problem loading the credentials:');
this.isDataLoading = false;
return;
}
@ -138,7 +138,7 @@ export default mixins(
},
async deleteCredential (credential: ICredentialsResponse) {
const deleteConfirmed = await this.confirmMessage(`Are you sure that you want to delete the credentials "${credential.name}"?`, 'Delete Credentials?', 'warning', 'Yes, delete!');
const deleteConfirmed = await this.confirmMessage(`Are you sure you want to delete "${credential.name}" credentials?`, 'Delete Credentials?', 'warning', 'Yes, delete!');
if (deleteConfirmed === false) {
return;

View file

@ -12,14 +12,15 @@
<font-awesome-icon icon="check" class="execution-icon success" v-if="executionFinished" title="Execution was successful" />
<font-awesome-icon icon="times" class="execution-icon error" v-else title="Execution did fail" />
</span>
of Workflow
of
<span class="workflow-name clickable" title="Open Workflow">
<span @click="openWorkflow(workflowExecution.workflowId)">"{{workflowName}}"</span>
</span>
workflow
</span>
<span index="workflow-name" class="current-workflow" v-if="!isReadOnly">
<span v-if="currentWorkflow">Workflow: <span class="workflow-name">{{workflowName}}</span></span>
<span v-else class="workflow-not-saved">Workflow not saved!</span>
<span v-else class="workflow-not-saved">Workflow was not saved!</span>
</span>
<span class="saving-workflow" v-if="isWorkflowSaving">
@ -32,9 +33,9 @@
<div class="push-connection-lost" v-if="!isPushConnectionActive">
<el-tooltip placement="bottom-end" effect="light">
<div slot="content">
Server connection could not be established.<br />
The server is down or there is a connection problem.<br />
It will reconnect automatically as soon as the backend can be reached.
Cannot connect to server.<br />
It is either down or you have a connection issue. <br />
It should reconnect automatically once the issue is resolved.
</div>
<span>
<font-awesome-icon icon="exclamation-triangle" />&nbsp;
@ -50,9 +51,8 @@
<div class="read-only" v-if="isReadOnly">
<el-tooltip placement="bottom-end" effect="light">
<div slot="content">
A past execution gets displayed. For that reason no data<br />
can be changed. To make changes or to execute it again open<br />
the workflow by clicking on it`s name on the left.
You're viewing the log of a previous execution. You cannot<br />
make changes since this execution already occured. Make changes<br /> to this workflow by clicking on it`s name on the left.
</div>
<span>
<font-awesome-icon icon="exclamation-triangle" />
@ -84,6 +84,7 @@ import { genericHelpers } from '@/components/mixins/genericHelpers';
import { pushConnection } from '@/components/mixins/pushConnection';
import { restApi } from '@/components/mixins/restApi';
import { showMessage } from '@/components/mixins/showMessage';
import { titleChange } from '@/components/mixins/titleChange';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import { saveAs } from 'file-saver';
@ -95,6 +96,7 @@ export default mixins(
pushConnection,
restApi,
showMessage,
titleChange,
workflowHelpers,
)
.extend({
@ -155,6 +157,7 @@ export default mixins(
},
methods: {
async openWorkflow (workflowId: string) {
this.$titleSet(this.workflowName, 'IDLE');
// Change to other workflow
this.$router.push({
name: 'NodeViewExisting',
@ -162,7 +165,6 @@ export default mixins(
});
},
},
async mounted () {
// Initialize the push connection
this.pushConnect();

View file

@ -179,6 +179,7 @@ import WorkflowSettings from '@/components/WorkflowSettings.vue';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { restApi } from '@/components/mixins/restApi';
import { showMessage } from '@/components/mixins/showMessage';
import { titleChange } from '@/components/mixins/titleChange';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import { workflowSave } from '@/components/mixins/workflowSave';
import { workflowRun } from '@/components/mixins/workflowRun';
@ -191,6 +192,7 @@ export default mixins(
genericHelpers,
restApi,
showMessage,
titleChange,
workflowHelpers,
workflowRun,
workflowSave,
@ -417,7 +419,8 @@ export default mixins(
this.$showError(error, 'Problem deleting the workflow', 'There was a problem deleting the workflow:');
return;
}
// Reset tab title since workflow is deleted.
this.$titleReset();
this.$showMessage({
title: 'Workflow got deleted',
message: `The workflow "${this.workflowName}" got deleted!`,

View file

@ -13,7 +13,7 @@
<div class="node-create-list-wrapper">
<div class="node-create-list">
<div v-if="filteredNodeTypes.length === 0" class="no-results">
No node found which matches active filter!
🙃 no nodes matching your search criteria
</div>
<node-create-item :active="index === activeNodeTypeIndex" :nodeType="nodeType" v-for="(nodeType, index) in filteredNodeTypes" v-bind:key="nodeType.name" @nodeTypeSelected="nodeTypeSelected"></node-create-item>
</div>

View file

@ -5,7 +5,7 @@
<display-with-change :key-name="'name'" @valueChanged="valueChanged"></display-with-change>
<a v-if="nodeType" :href="'http://n8n.io/nodes/' + nodeType.name" target="_blank" class="node-info">
<el-tooltip class="clickable" placement="top" effect="light">
<div slot="content" v-html="'<strong>Node Description:</strong><br />' + nodeTypeDescription + '<br /><br /><strong>For more information and usage examples click!</strong>'"></div>
<div slot="content" v-html="'<strong>Node Description:</strong><br />' + nodeTypeDescription + '<br /><br /><strong>Click the \'?\' icon to open this node on n8n.io </strong>'"></div>
<font-awesome-icon icon="question-circle" />
</el-tooltip>
</a>
@ -22,7 +22,7 @@
<node-webhooks :node="node" :nodeType="nodeType" />
<parameter-input-list :parameters="parametersNoneSetting" :hideDelete="true" :nodeValues="nodeValues" path="parameters" @valueChanged="valueChanged" />
<div v-if="parametersNoneSetting.length === 0">
The node does not have any parameters.
This node does not have any parameters.
</div>
</el-tab-pane>
<el-tab-pane label="Settings">
@ -162,15 +162,15 @@ export default mixins(
},
default: '',
noDataExpression: true,
description: 'Notes to save with the node.',
description: 'Optional note to save with the node.',
},
{
displayName: 'Notes In Flow',
displayName: 'Display note in flow?',
name: 'notesInFlow',
type: 'boolean',
default: false,
noDataExpression: true,
description: 'If activated it will display the above notes in the flow as subtitle.',
description: 'If active, the note above will display in the flow as a subtitle.',
},
{
displayName: 'Node Color',
@ -186,7 +186,7 @@ export default mixins(
type: 'boolean',
default: false,
noDataExpression: true,
description: 'If activated and the node does not have any data for the first output,<br />it returns an empty item anyway. Be careful setting this on<br />IF-Nodes as it could easily cause an infinite loop.',
description: 'If active, the node will return an empty item even if the <br />node returns no data during an initial execution. Be careful setting <br />this on IF-Nodes as it could cause an infinite loop.',
},
{
displayName: 'Execute Once',
@ -194,7 +194,7 @@ export default mixins(
type: 'boolean',
default: false,
noDataExpression: true,
description: 'Instead of executing once per item does it only execute once with the data of the first item.',
description: 'If active, the node executes only once, with data<br /> from the first item it recieves. ',
},
{
displayName: 'Retry On Fail',
@ -202,7 +202,7 @@ export default mixins(
type: 'boolean',
default: false,
noDataExpression: true,
description: 'If activated it will automatically retry the node again multiple times.',
description: 'If active, the node tries to execute a failed attempt <br /> multiple times until it succeeds.',
},
{
displayName: 'Max. Tries',
@ -221,7 +221,7 @@ export default mixins(
},
},
noDataExpression: true,
description: 'How often it should try to execute the node before it should fail.',
description: 'Number of times Retry On Fail should attempt to execute the node <br />before stopping and returning the execution as failed.',
},
{
displayName: 'Wait Between Tries',
@ -240,7 +240,7 @@ export default mixins(
},
},
noDataExpression: true,
description: 'How long to wait between ties. Value in ms.',
description: 'How long to wait between each attempt. Value in ms.',
},
{
displayName: 'Continue On Fail',
@ -248,7 +248,7 @@ export default mixins(
type: 'boolean',
default: false,
noDataExpression: true,
description: 'If activated and the node fails the workflow will simply continue running.<br />It will then simply pass through the input data so the workflow has<br />to be set up to handle the case that different data gets returned.',
description: 'If active, the workflow continues even if this node\'s <br /execution fails. When this occurs, the node passes along input data from<br />previous nodes - so your workflow should account for unexpected output data.',
},
] as INodeProperties[],

View file

@ -88,7 +88,7 @@ export default mixins(
this.$showMessage({
title: 'Copied',
message: `The webhook URL got copied!`,
message: `The webhook URL was successfully copied!`,
type: 'success',
});
},

View file

@ -7,7 +7,7 @@
:disabled="workflowRunning"
@click.stop="runWorkflow(node.name)"
class="execute-node-button"
:title="`Executes node ${node.name} and all not already executed nodes before it.`"
:title="`Executes this ${node.name} node after executing any previous nodes that have not yet returned data`"
>
<div class="run-icon-button">
<font-awesome-icon v-if="!workflowRunning" icon="play-circle"/>
@ -72,14 +72,14 @@
<span v-else>
<div v-if="showData === false" class="to-much-data">
<h3>
Node contains large amount of data
Node returned a large amount of data
</h3>
<div class="text">
The node contains {{parseInt(dataSize/1024).toLocaleString()}} KB of data.<br />
Displaying it could cause problems!<br />
<br />
If you decide to display it anyway avoid the JSON view!
If you do decide to display it, avoid the JSON view!
</div>
<el-button size="small" @click="displayMode = 'Table';showData = true;">
@ -162,7 +162,7 @@
<div>
<strong>No data</strong><br />
<br />
To display data execute the node first by pressing the execute button above.
Data returned by this node will display here<br />
</div>
</div>
</div>

View file

@ -34,6 +34,7 @@ import WorkflowActivator from '@/components/WorkflowActivator.vue';
import { restApi } from '@/components/mixins/restApi';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { showMessage } from '@/components/mixins/showMessage';
import { titleChange } from '@/components/mixins/titleChange';
import { IWorkflowShortResponse } from '@/Interface';
import mixins from 'vue-typed-mixins';
@ -42,6 +43,7 @@ export default mixins(
genericHelpers,
restApi,
showMessage,
titleChange,
).extend({
name: 'WorkflowOpen',
props: [
@ -89,6 +91,7 @@ export default mixins(
},
openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any
if (column.label !== 'Active') {
this.$titleSet(data.name, 'IDLE');
this.$emit('openWorkflow', data.id);
}
},

View file

@ -10,12 +10,14 @@ import {
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { showMessage } from '@/components/mixins/showMessage';
import { titleChange } from '@/components/mixins/titleChange';
import mixins from 'vue-typed-mixins';
export const pushConnection = mixins(
nodeHelpers,
showMessage,
titleChange,
)
.extend({
data () {
@ -147,7 +149,6 @@ export const pushConnection = mixins(
*/
pushMessageReceived (event: Event, isRetry?: boolean): boolean {
const retryAttempts = 5;
let receivedData: IPushData;
try {
// @ts-ignore
@ -201,13 +202,15 @@ export const pushConnection = mixins(
const runDataExecuted = pushData.data;
// @ts-ignore
const workflow = this.getWorkflow();
if (runDataExecuted.finished !== true) {
// There was a problem with executing the workflow
let errorMessage = 'There was a problem executing the workflow!';
if (runDataExecuted.data.resultData.error && runDataExecuted.data.resultData.error.message) {
errorMessage = `There was a problem executing the workflow:<br /><strong>"${runDataExecuted.data.resultData.error.message}"</strong>`;
}
this.$titleSet(workflow.name, 'ERROR');
this.$showMessage({
title: 'Problem executing workflow',
message: errorMessage,
@ -215,6 +218,7 @@ export const pushConnection = mixins(
});
} else {
// Workflow did execute without a problem
this.$titleSet(workflow.name, 'IDLE');
this.$showMessage({
title: 'Workflow got executed',
message: 'Workflow did get executed successfully!',

View file

@ -0,0 +1,31 @@
import Vue from 'vue';
import {
WorkflowTitleStatus,
} from '../../Interface';
export const titleChange = Vue.extend({
methods: {
/**
* Change title of n8n tab
*
* @param {string} workflow Name of workflow
* @param {WorkflowTitleStatus} status Status of workflow
*/
$titleSet(workflow: string, status: WorkflowTitleStatus) {
let icon = '⚠️';
if (status === 'EXECUTING') {
icon = '🔄';
} else if (status === 'IDLE') {
icon = '▶️';
}
window.document.title = `n8n - ${icon} ${workflow}`;
},
$titleReset() {
document.title = `n8n - Workflow Automation`;
},
},
});

View file

@ -14,10 +14,12 @@ import { restApi } from '@/components/mixins/restApi';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import mixins from 'vue-typed-mixins';
import { titleChange } from './titleChange';
export const workflowRun = mixins(
restApi,
workflowHelpers,
titleChange,
).extend({
methods: {
// Starts to executes a workflow on server.
@ -27,6 +29,7 @@ export const workflowRun = mixins(
// because then it can not receive the data as it executes.
throw new Error('No active connection to server. It is maybe down.');
}
const workflow = this.getWorkflow();
this.$store.commit('addActiveAction', 'workflowRunning');
@ -55,6 +58,7 @@ export const workflowRun = mixins(
}
const workflow = this.getWorkflow();
this.$titleSet(workflow.name as string, 'EXECUTING');
try {
// Check first if the workflow has any issues before execute it
@ -78,6 +82,7 @@ export const workflowRun = mixins(
type: 'error',
duration: 0,
});
this.$titleSet(workflow.name as string, 'ERROR');
return;
}
}
@ -165,8 +170,9 @@ export const workflowRun = mixins(
};
this.$store.commit('setWorkflowExecutionData', executionData);
return await this.runWorkflowApi(startRunData);
return await this.runWorkflowApi(startRunData);
} catch (error) {
this.$titleSet(workflow.name as string, 'ERROR');
this.$showError(error, 'Problem running workflow', 'There was a problem running the workflow:');
return undefined;
}

View file

@ -115,6 +115,8 @@ import { mouseSelect } from '@/components/mixins/mouseSelect';
import { moveNodeWorkflow } from '@/components/mixins/moveNodeWorkflow';
import { restApi } from '@/components/mixins/restApi';
import { showMessage } from '@/components/mixins/showMessage';
import { titleChange } from '@/components/mixins/titleChange';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import { workflowRun } from '@/components/mixins/workflowRun';
@ -165,6 +167,7 @@ export default mixins(
moveNodeWorkflow,
restApi,
showMessage,
titleChange,
workflowHelpers,
workflowRun,
)
@ -1324,6 +1327,8 @@ export default mixins(
}
if (workflowId !== null) {
const workflow = await this.restApi().getWorkflow(workflowId);
this.$titleSet(workflow.name, 'IDLE');
// Open existing workflow
await this.openWorkflow(workflowId);
} else {

View file

@ -0,0 +1,34 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
//https://www.contentful.com/developers/docs/references/authentication/
export class ContentfulApi implements ICredentialType {
name = 'contentfulApi';
displayName = 'Contenful API';
properties = [
{
displayName: 'Space ID',
name: 'spaceId',
type: 'string' as NodePropertyTypes,
default: '',
required: true,
description: 'The id for the Contentful space.',
},
{
displayName: 'Content Delivery API Access token',
name: 'ContentDeliveryaccessToken',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Access token that has access to the space. Can be left empty if only Delivery API should be used.',
},
{
displayName: 'Content Preview API Access token',
name: 'ContentPreviewaccessToken',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Access token that has access to the space. Can be left empty if only Preview API should be used.',
},
];
}

View file

@ -0,0 +1,21 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class ConvertKitApi implements ICredentialType {
name = 'convertKitApi';
displayName = 'ConvertKit API';
properties = [
{
displayName: 'API Secret',
name: 'apiSecret',
type: 'string' as NodePropertyTypes,
default: '',
typeOptions: {
password: true,
},
},
];
}

View file

@ -0,0 +1,23 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class PaddleApi implements ICredentialType {
name = 'paddleApi';
displayName = 'Paddle API';
properties = [
{
displayName: 'Vendor Auth Code',
name: 'vendorAuthCode',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Vendor ID',
name: 'vendorId',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -36,10 +36,24 @@ export class Postgres implements ICredentialType {
},
default: '',
},
{
displayName: 'Ignore SSL Issues',
name: 'allowUnauthorizedCerts',
type: 'boolean' as NodePropertyTypes,
default: false,
description: 'Connect even if SSL certificate validation is not possible.',
},
{
displayName: 'SSL',
name: 'ssl',
type: 'options' as NodePropertyTypes,
displayOptions: {
show: {
allowUnauthorizedCerts: [
false,
],
},
},
options: [
{
name: 'disable',

View file

@ -0,0 +1,46 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class TodoistOAuth2Api implements ICredentialType {
name = 'todoistOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Todoist OAuth2 API';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://todoist.com/oauth/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://todoist.com/oauth/access_token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: 'data:read_write,data:delete',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'body',
},
];
}

View file

@ -13,7 +13,8 @@ export class ZendeskApi implements ICredentialType {
name: 'subdomain',
type: 'string' as NodePropertyTypes,
description: 'The subdomain of your Zendesk work environment.',
default: 'n8n',
placeholder: 'company',
default: '',
},
{
displayName: 'Email',

View file

@ -526,4 +526,4 @@ export const dealFields = [
description: 'The content of the deal note',
},
] as INodeProperties[];
] as INodeProperties[];

View file

@ -367,7 +367,7 @@ export class Airtable implements INodeType {
const operation = this.getNodeParameter('operation', 0) as string;
const application = this.getNodeParameter('application', 0) as string;
const table = this.getNodeParameter('table', 0) as string;
const table = encodeURI(this.getNodeParameter('table', 0) as string);
let returnAll = false;
let endpoint = '';

View file

@ -237,10 +237,6 @@ export class Coda implements INodeType {
const options = this.getNodeParameter('options', i) as IDataObject;
const endpoint = `/docs/${docId}/tables/${tableId}/rows`;
if (options.keyColumns) {
// @ts-ignore
items[i].json['keyColumns'] = options.keyColumns.split(',') as string[];
}
if (options.disableParsing) {
qs.disableParsing = options.disableParsing as boolean;
}
@ -264,6 +260,11 @@ export class Coda implements INodeType {
};
}
((sendData[endpoint]! as IDataObject).rows! as IDataObject[]).push({ cells });
if (options.keyColumns) {
// @ts-ignore
(sendData[endpoint]! as IDataObject).keyColumns! = options.keyColumns.split(',') as string[];
}
}
// Now that all data got collected make all the requests

View file

@ -0,0 +1,197 @@
import {
INodeProperties,
INodePropertyOptions,
} from 'n8n-workflow';
export const resource = {
name: 'Asset',
value: 'asset',
} as INodePropertyOptions;
export const operations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
resource.value,
],
},
},
options: [
{
name: 'Get',
value: 'get',
},
{
name: 'Get All',
value: 'getAll',
},
],
default: 'getAll',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const fields = [
{
displayName: 'Environment ID',
name: 'environmentId',
type: 'string',
displayOptions: {
show: {
resource: [
resource.value,
],
operation: [
'get',
'getAll',
],
},
},
default: 'master',
description: 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".'
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
resource.value,
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
resource.value,
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
resource.value,
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Equal',
name: 'equal',
type: 'string',
default: '',
placeholder: 'fields.title=n8n',
description: 'Search for all data that matches the condition: {attribute}={value}. Attribute can use dot notation.',
},
{
displayName: 'Exclude',
name: 'exclude',
type: 'string',
default: '',
placeholder: 'fields.tags[nin]=accessories,flowers',
description: 'Search for all data that matches the condition: {attribute}[nin]={value}. Attribute can use dot notation.',
},
{
displayName: 'Exist',
name: 'exist',
type: 'string',
default: '',
placeholder: 'fields.tags[exists]=true',
description: 'Search for all data that matches the condition: {attribute}[exists]={value}. Attribute can use dot notation.',
},
{
displayName: 'Fields',
name: 'select',
type: 'string',
placeholder: 'fields.title',
default: '',
description: 'The select operator allows you to choose what fields to return from an entity. You can choose multiple values by combining comma separated operators.',
},
{
displayName: 'Include',
name: 'include',
type: 'string',
default: '',
placeholder: 'fields.tags[in]=accessories,flowers',
description: 'Search for all data that matches the condition: {attribute}[in]={value}. Attribute can use dot notation.',
},
{
displayName: 'Not Equal',
name: 'notEqual',
type: 'string',
default: '',
placeholder: 'fields.title[ne]=n8n',
description: 'Search for all data that matches the condition: {attribute}[ne]={value}. Attribute can use dot notation.',
},
{
displayName: 'Order',
name: 'order',
type: 'string',
default: '',
placeholder: 'sys.createdAt',
description: 'You can order items in the response by specifying the order search parameter. You can use sys properties (such as sys.createdAt) or field values (such as fields.myCustomDateField) for ordering.',
},
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
description: ' Full-text search is case insensitive and might return more results than expected. A query will only take values with more than 1 character.',
},
],
},
{
displayName: 'Asset ID',
name: 'assetId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
resource.value
],
operation: [
'get',
],
},
},
},
] as INodeProperties[];

View file

@ -0,0 +1,69 @@
import {
INodeProperties,
INodePropertyOptions,
} from 'n8n-workflow';
export const resource = {
name: 'Content Type',
value: 'contentType',
} as INodePropertyOptions;
export const operations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
resource.value,
],
},
},
options: [
{
name: 'Get',
value: 'get',
},
],
default: 'get',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const fields = [
{
displayName: 'Environment ID',
name: 'environmentId',
type: 'string',
displayOptions: {
show: {
resource: [
resource.value,
],
operation: [
'get',
],
},
},
default: 'master',
description: 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".'
},
{
displayName: 'Content Type ID',
name: 'contentTypeId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
resource.value,
],
operation: [
'get',
],
},
},
},
] as INodeProperties[];

View file

@ -0,0 +1,266 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
contentfulApiRequest,
contenfulApiRequestAllItems,
} from './GenericFunctions';
import * as SpaceDescription from './SpaceDescription';
import * as ContentTypeDescription from './ContentTypeDescription';
import * as EntryDescription from './EntryDescription';
import * as AssetDescription from './AssetDescription';
import * as LocaleDescription from './LocaleDescription';
export class Contentful implements INodeType {
description: INodeTypeDescription = {
displayName: 'Contentful',
name: 'contentful',
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
icon: 'file:contentful.png',
group: ['input'],
version: 1,
description: 'Consume Contenful API',
defaults: {
name: 'Contentful',
color: '#2E75D4'
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'contentfulApi',
required: true
},
],
properties: [
{
displayName: 'Source',
name: 'source',
type: 'options',
default: 'deliveryApi',
description: 'Pick where your data comes from, delivery or preview API',
options: [
{
name: 'Delivery API',
value: 'deliveryApi'
},
{
name: 'Preview API',
value: 'previewApi'
},
],
},
// Resources:
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
AssetDescription.resource,
ContentTypeDescription.resource,
EntryDescription.resource,
LocaleDescription.resource,
SpaceDescription.resource,
],
default: 'entry',
description: 'The resource to operate on.'
},
// Operations:
...SpaceDescription.operations,
...ContentTypeDescription.operations,
...EntryDescription.operations,
...AssetDescription.operations,
...LocaleDescription.operations,
// Resource specific fields:
...SpaceDescription.fields,
...ContentTypeDescription.fields,
...EntryDescription.fields,
...AssetDescription.fields,
...LocaleDescription.fields,
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
let responseData;
const items = this.getInputData();
const returnData: IDataObject[] = [];
const qs: Record<string, string | number> = {};
for (let i = 0; i < items.length; i++) {
if (resource === 'space') {
if (operation === 'get') {
const credentials = this.getCredentials('contentfulApi');
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}`);
}
}
if (resource === 'contentType') {
if (operation === 'get') {
const credentials = this.getCredentials('contentfulApi');
const env = this.getNodeParameter('environmentId', 0) as string;
const id = this.getNodeParameter('contentTypeId', 0) as string;
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/content_types/${id}`);
}
}
if (resource === 'entry') {
if (operation === 'get') {
const credentials = this.getCredentials('contentfulApi');
const env = this.getNodeParameter('environmentId', 0) as string;
const id = this.getNodeParameter('entryId', 0) as string;
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/entries/${id}`, {}, qs);
} else if (operation === 'getAll') {
const credentials = this.getCredentials('contentfulApi');
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const env = this.getNodeParameter('environmentId', i) as string;
Object.assign(qs, additionalFields);
if (qs.equal) {
const [atribute, value] = (qs.equal as string).split('=');
qs[atribute] = value;
delete qs.equal;
}
if (qs.notEqual) {
const [atribute, value] = (qs.notEqual as string).split('=');
qs[atribute] = value;
delete qs.notEqual;
}
if (qs.include) {
const [atribute, value] = (qs.include as string).split('=');
qs[atribute] = value;
delete qs.include;
}
if (qs.exclude) {
const [atribute, value] = (qs.exclude as string).split('=');
qs[atribute] = value;
delete qs.exclude;
}
if (returnAll) {
responseData = await contenfulApiRequestAllItems.call(this, 'items', 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/entries`, {}, qs);
} else {
const limit = this.getNodeParameter('limit', 0) as number;
qs.limit = limit;
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/entries`, {}, qs);
responseData = responseData.items;
}
}
}
if (resource === 'asset') {
if (operation === 'get') {
const credentials = this.getCredentials('contentfulApi');
const env = this.getNodeParameter('environmentId', 0) as string;
const id = this.getNodeParameter('assetId', 0) as string;
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/assets/${id}`, {}, qs);
} else if (operation === 'getAll') {
const credentials = this.getCredentials('contentfulApi');
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const env = this.getNodeParameter('environmentId', i) as string;
Object.assign(qs, additionalFields);
if (qs.equal) {
const [atribute, value] = (qs.equal as string).split('=');
qs[atribute] = value;
delete qs.equal;
}
if (qs.notEqual) {
const [atribute, value] = (qs.notEqual as string).split('=');
qs[atribute] = value;
delete qs.notEqual;
}
if (qs.include) {
const [atribute, value] = (qs.include as string).split('=');
qs[atribute] = value;
delete qs.include;
}
if (qs.exclude) {
const [atribute, value] = (qs.exclude as string).split('=');
qs[atribute] = value;
delete qs.exclude;
}
if (returnAll) {
responseData = await contenfulApiRequestAllItems.call(this, 'items', 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/assets`, {}, qs);
} else {
const limit = this.getNodeParameter('limit', 0) as number;
qs.limit = limit;
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/assets`, {}, qs);
responseData = responseData.items;
}
}
}
if (resource === 'locale') {
if (operation === 'getAll') {
const credentials = this.getCredentials('contentfulApi');
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const env = this.getNodeParameter('environmentId', i) as string;
if (returnAll) {
responseData = await contenfulApiRequestAllItems.call(this, 'items', 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/locales`, {}, qs);
} else {
const limit = this.getNodeParameter('limit', 0) as number;
qs.limit = limit;
responseData = await contentfulApiRequest.call(this, 'GET', `/spaces/${credentials?.spaceId}/environments/${env}/locales`, {}, qs);
responseData = responseData.items;
}
}
}
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,204 @@
import {
INodeProperties,
INodePropertyOptions,
} from 'n8n-workflow';
export const resource = {
name: 'Entry',
value: 'entry',
} as INodePropertyOptions;
export const operations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
resource.value,
],
},
},
options: [
{
name: 'Get',
value: 'get',
},
{
name: 'Get All',
value: 'getAll',
},
],
default: 'get',
description: 'The operation to perform.'
}
] as INodeProperties[];
export const fields = [
{
displayName: 'Environment ID',
name: 'environmentId',
type: 'string',
displayOptions: {
show: {
resource: [
resource.value,
],
operation: [
'get',
'getAll',
],
},
},
default: 'master',
description: 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
resource.value,
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
resource.value,
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
resource.value,
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Content Type ID',
name: 'content_type',
type: 'string',
default: '',
description: 'To search for entries with a specific content type',
},
{
displayName: 'Equal',
name: 'equal',
type: 'string',
default: '',
placeholder: 'fields.title=n8n',
description: 'Search for all data that matches the condition: {attribute}={value}. Attribute can use dot notation.',
},
{
displayName: 'Exclude',
name: 'exclude',
type: 'string',
default: '',
placeholder: 'fields.tags[nin]=accessories,flowers',
description: 'Search for all data that matches the condition: {attribute}[nin]={value}. Attribute can use dot notation.',
},
{
displayName: 'Exist',
name: 'exist',
type: 'string',
default: '',
placeholder: 'fields.tags[exists]=true',
description: 'Search for all data that matches the condition: {attribute}[exists]={value}. Attribute can use dot notation.',
},
{
displayName: 'Fields',
name: 'select',
type: 'string',
placeholder: 'fields.title',
default: '',
description: 'The select operator allows you to choose what fields to return from an entity. You can choose multiple values by combining comma separated operators.',
},
{
displayName: 'Include',
name: 'include',
type: 'string',
default: '',
placeholder: 'fields.tags[in]=accessories,flowers',
description: 'Search for all data that matches the condition: {attribute}[in]={value}. Attribute can use dot notation.',
},
{
displayName: 'Not Equal',
name: 'notEqual',
type: 'string',
default: '',
placeholder: 'fields.title[ne]=n8n',
description: 'Search for all data that matches the condition: {attribute}[ne]={value}. Attribute can use dot notation.',
},
{
displayName: 'Order',
name: 'order',
type: 'string',
default: '',
placeholder: 'sys.createdAt',
description: 'You can order items in the response by specifying the order search parameter. You can use sys properties (such as sys.createdAt) or field values (such as fields.myCustomDateField) for ordering.',
},
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
description: ' Full-text search is case insensitive and might return more results than expected. A query will only take values with more than 1 character.',
},
],
},
{
displayName: 'Entry ID',
name: 'entryId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
resource.value,
],
operation: [
'get',
],
},
},
}
] as INodeProperties[];

View file

@ -0,0 +1,75 @@
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
OptionsWithUri,
} from 'request';
import {
IDataObject,
} from 'n8n-workflow';
export async function contentfulApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('contentfulApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const source = this.getNodeParameter('source', 0) as string;
const isPreview = source === 'previewApi';
const options: OptionsWithUri = {
method,
qs,
body,
uri: uri ||`https://${isPreview ? 'preview' : 'cdn'}.contentful.com${resource}`,
json: true
};
if (isPreview) {
qs.access_token = credentials.ContentPreviewaccessToken as string;
} else {
qs.access_token = credentials.ContentDeliveryaccessToken as string;
}
try {
return await this.helpers.request!(options);
} catch (error) {
let errorMessage = error;
if (error.response && error.response.body && error.response.body.details) {
const details = error.response.body.details;
errorMessage = details.errors.map((e: IDataObject) => e.details).join('|');
} else if (error.response && error.response.body && error.response.body.message) {
errorMessage = error.response.body.message;
}
throw new Error(`Contentful error response [${error.statusCode}]: ${errorMessage}`);
}
}
export async function contenfulApiRequestAllItems(this: ILoadOptionsFunctions | IExecuteFunctions, propertyName: string, method: string, resource: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.limit = 100;
query.skip = 0;
do {
responseData = await contentfulApiRequest.call(this, method, resource, body, query);
query.skip = (query.skip + 1) * query.limit;
returnData.push.apply(returnData, responseData[propertyName]);
} while (
returnData.length < responseData.total
);
return returnData;
}

View file

@ -0,0 +1,94 @@
import {
INodeProperties,
INodePropertyOptions
} from 'n8n-workflow';
export const resource = {
name: 'Locale',
value: 'locale',
} as INodePropertyOptions;
export const operations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
resource.value,
],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
},
],
default: 'getAll',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const fields = [
{
displayName: 'Environment ID',
name: 'environmentId',
type: 'string',
displayOptions: {
show: {
resource: [
resource.value,
],
operation: [
'get',
'getAll',
],
},
},
default: 'master',
description: 'The id for the Contentful environment (e.g. master, staging, etc.). Depending on your plan, you might not have environments. In that case use "master".'
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
resource.value,
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
resource.value,
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -0,0 +1,37 @@
import { INodeProperties } from 'n8n-workflow';
export const fields = [
{
displayName: 'Search Parameters',
name: 'search_parameters',
description: 'You can use a variety of query parameters to search and filter items.',
placeholder: 'Add parameter',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Parameters',
name: 'parameters',
values: [
{
displayName: 'Parameter Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the search parameter to set.'
},
{
displayName: 'Parameter Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the search parameter to set.'
},
],
},
],
}
] as INodeProperties[];

View file

@ -0,0 +1,33 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const resource = {
name: 'Space',
value: 'space',
};
export const operations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
resource.value,
],
},
},
options: [
{
name: 'Get',
value: 'get',
},
],
default: 'get',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const fields = [] as INodeProperties[];

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,486 @@
import {
IExecuteFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodePropertyOptions,
INodeTypeDescription,
INodeType,
} from 'n8n-workflow';
import {
convertKitApiRequest,
} from './GenericFunctions';
import {
customFieldFields,
customFieldOperations,
} from './CustomFieldDescription';
import {
formFields,
formOperations,
} from './FormDescription';
import {
sequenceFields,
sequenceOperations,
} from './SequenceDescription';
import {
tagFields,
tagOperations,
} from './TagDescription';
import {
tagSubscriberFields,
tagSubscriberOperations,
} from './TagSubscriberDescription';
export class ConvertKit implements INodeType {
description: INodeTypeDescription = {
displayName: 'ConvertKit',
name: 'convertKit',
icon: 'file:convertKit.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume ConvertKit API.',
defaults: {
name: 'ConvertKit',
color: '#fb6970',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'convertKitApi',
required: true,
}
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Custom Field',
value: 'customField',
},
{
name: 'Form',
value: 'form',
},
{
name: 'Sequence',
value: 'sequence',
},
{
name: 'Tag',
value: 'tag',
},
{
name: 'Tag Subscriber',
value: 'tagSubscriber',
},
],
default: 'form',
description: 'The resource to operate on.'
},
//--------------------
// Field Description
//--------------------
...customFieldOperations,
...customFieldFields,
//--------------------
// FormDescription
//--------------------
...formOperations,
...formFields,
//--------------------
// Sequence Description
//--------------------
...sequenceOperations,
...sequenceFields,
//--------------------
// Tag Description
//--------------------
...tagOperations,
...tagFields,
//--------------------
// Tag Subscriber Description
//--------------------
...tagSubscriberOperations,
...tagSubscriberFields,
],
};
methods = {
loadOptions: {
// Get all the tags to display them to user so that he can
// select them easily
async getTags(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { tags } = await convertKitApiRequest.call(this, 'GET', '/tags');
for (const tag of tags) {
const tagName = tag.name;
const tagId = tag.id;
returnData.push({
name: tagName,
value: tagId,
});
}
return returnData;
},
// Get all the forms to display them to user so that he can
// select them easily
async getForms(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { forms } = await convertKitApiRequest.call(this, 'GET', '/forms');
for (const form of forms) {
const formName = form.name;
const formId = form.id;
returnData.push({
name: formName,
value: formId,
});
}
return returnData;
},
// Get all the sequences to display them to user so that he can
// select them easily
async getSequences(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { courses } = await convertKitApiRequest.call(this, 'GET', '/sequences');
for (const course of courses) {
const courseName = course.name;
const courseId = course.id;
returnData.push({
name: courseName,
value: courseId,
});
}
return returnData;
},
}
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < items.length; i++) {
if (resource === 'customField') {
if (operation === 'create') {
const label = this.getNodeParameter('label', i) as string;
responseData = await convertKitApiRequest.call(this, 'POST', '/custom_fields', { label }, qs);
}
if (operation === 'delete') {
const id = this.getNodeParameter('id', i) as string;
responseData = await convertKitApiRequest.call(this, 'DELETE', `/custom_fields/${id}`);
}
if (operation === 'get') {
const id = this.getNodeParameter('id', i) as string;
responseData = await convertKitApiRequest.call(this, 'GET', `/custom_fields/${id}`);
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
responseData = await convertKitApiRequest.call(this, 'GET', `/custom_fields`);
responseData = responseData.custom_fields;
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.slice(0, limit);
}
}
if (operation === 'update') {
const id = this.getNodeParameter('id', i) as string;
const label = this.getNodeParameter('label', i) as string;
responseData = await convertKitApiRequest.call(this, 'PUT', `/custom_fields/${id}`, { label });
responseData = { success: true };
}
}
if (resource === 'form') {
if (operation === 'addSubscriber') {
const email = this.getNodeParameter('email', i) as string;
const formId = this.getNodeParameter('id', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {
email,
};
if (additionalFields.firstName) {
body.first_name = additionalFields.firstName as string;
}
if (additionalFields.tags) {
body.tags = additionalFields.tags as string[];
}
if (additionalFields.fieldsUi) {
const fieldValues = (additionalFields.fieldsUi as IDataObject).fieldsValues as IDataObject[];
if (fieldValues) {
body.fields = {};
for (const fieldValue of fieldValues) {
//@ts-ignore
body.fields[fieldValue.key] = fieldValue.value;
}
}
}
const { subscription } = await convertKitApiRequest.call(this, 'POST', `/forms/${formId}/subscribe`, body);
responseData = subscription;
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
responseData = await convertKitApiRequest.call(this, 'GET', `/forms`);
responseData = responseData.forms;
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.slice(0, limit);
}
}
if (operation === 'getSubscriptions') {
const formId = this.getNodeParameter('id', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.subscriberState) {
qs.subscriber_state = additionalFields.subscriberState as string;
}
responseData = await convertKitApiRequest.call(this, 'GET', `/forms/${formId}/subscriptions`, {}, qs);
responseData = responseData.subscriptions;
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.slice(0, limit);
}
}
}
if (resource === 'sequence') {
if (operation === 'addSubscriber') {
const email = this.getNodeParameter('email', i) as string;
const sequenceId = this.getNodeParameter('id', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {
email,
};
if (additionalFields.firstName) {
body.first_name = additionalFields.firstName as string;
}
if (additionalFields.tags) {
body.tags = additionalFields.tags as string[];
}
if (additionalFields.fieldsUi) {
const fieldValues = (additionalFields.fieldsUi as IDataObject).fieldsValues as IDataObject[];
if (fieldValues) {
body.fields = {};
for (const fieldValue of fieldValues) {
//@ts-ignore
body.fields[fieldValue.key] = fieldValue.value;
}
}
}
const { subscription } = await convertKitApiRequest.call(this, 'POST', `/sequences/${sequenceId}/subscribe`, body);
responseData = subscription;
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
responseData = await convertKitApiRequest.call(this, 'GET', `/sequences`);
responseData = responseData.courses;
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.slice(0, limit);
}
}
if (operation === 'getSubscriptions') {
const sequenceId = this.getNodeParameter('id', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.subscriberState) {
qs.subscriber_state = additionalFields.subscriberState as string;
}
responseData = await convertKitApiRequest.call(this, 'GET', `/sequences/${sequenceId}/subscriptions`, {}, qs);
responseData = responseData.subscriptions;
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.slice(0, limit);
}
}
}
if (resource === 'tag') {
if (operation === 'create') {
const names = ((this.getNodeParameter('name', i) as string).split(',') as string[]).map((e) => ({ name: e }));
const body: IDataObject = {
tag: names
};
responseData = await convertKitApiRequest.call(this, 'POST', '/tags', body);
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
responseData = await convertKitApiRequest.call(this, 'GET', `/tags`);
responseData = responseData.tags;
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.slice(0, limit);
}
}
}
if (resource === 'tagSubscriber') {
if (operation === 'add') {
const tagId = this.getNodeParameter('tagId', i) as string;
const email = this.getNodeParameter('email', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {
email,
};
if (additionalFields.firstName) {
body.first_name = additionalFields.firstName as string;
}
if (additionalFields.fieldsUi) {
const fieldValues = (additionalFields.fieldsUi as IDataObject).fieldsValues as IDataObject[];
if (fieldValues) {
body.fields = {};
for (const fieldValue of fieldValues) {
//@ts-ignore
body.fields[fieldValue.key] = fieldValue.value;
}
}
}
const { subscription } = await convertKitApiRequest.call(this, 'POST', `/tags/${tagId}/subscribe`, body);
responseData = subscription;
}
if (operation === 'getAll') {
const tagId = this.getNodeParameter('tagId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
responseData = await convertKitApiRequest.call(this, 'GET', `/tags/${tagId}/subscriptions`);
responseData = responseData.subscriptions;
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.slice(0, limit);
}
}
if (operation === 'delete') {
const tagId = this.getNodeParameter('tagId', i) as string;
const email = this.getNodeParameter('email', i) as string;
responseData = await convertKitApiRequest.call(this, 'POST', `/tags/${tagId}>/unsubscribe`, { email });
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else if (responseData !== undefined) {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,367 @@
import {
IHookFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodePropertyOptions,
INodeTypeDescription,
INodeType,
IWebhookResponseData,
} from 'n8n-workflow';
import {
convertKitApiRequest,
} from './GenericFunctions';
import {
snakeCase,
} from 'change-case';
export class ConvertKitTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'ConvertKit Trigger',
name: 'convertKitTrigger',
icon: 'file:convertKit.png',
subtitle: '={{$parameter["event"]}}',
group: ['trigger'],
version: 1,
description: 'Handle ConvertKit events via webhooks',
defaults: {
name: 'ConvertKit Trigger',
color: '#fb6970',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'convertKitApi',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Event',
name: 'event',
type: 'options',
required: true,
default: '',
description: 'The events that can trigger the webhook and whether they are enabled.',
options: [
{
name: 'Form Subscribe',
value: 'formSubscribe',
},
{
name: 'Link Click',
value: 'linkClick',
},
{
name: 'Product Purchase',
value: 'productPurchase',
},
{
name: 'Purchase Created',
value: 'purchaseCreate',
},
{
name: 'Sequence Complete',
value: 'courseComplete',
},
{
name: 'Sequence Subscribe',
value: 'courseSubscribe',
},
{
name: 'Subscriber Activated',
value: 'subscriberActivate',
},
{
name: 'Subscriber Unsubscribe',
value: 'subscriberUnsubscribe',
},
{
name: 'Tag Add',
value: 'tagAdd',
},
{
name: 'Tag Remove',
value: 'tagRemove',
},
],
},
{
displayName: 'Form ID',
name: 'formId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getForms',
},
required: true,
default: '',
displayOptions: {
show: {
event: [
'formSubscribe',
],
},
},
},
{
displayName: 'Sequence ID',
name: 'courseId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getSequences',
},
required: true,
default: '',
displayOptions: {
show: {
event: [
'courseSubscribe',
'courseComplete',
],
},
},
},
{
displayName: 'Initiating Link',
name: 'link',
type: 'string',
required: true,
default: '',
description: 'The URL of the initiating link',
displayOptions: {
show: {
event: [
'linkClick',
],
},
},
},
{
displayName: 'Product ID',
name: 'productId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
event: [
'productPurchase',
],
},
},
},
{
displayName: 'Tag ID',
name: 'tagId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTags',
},
required: true,
default: '',
displayOptions: {
show: {
event: [
'tagAdd',
'tagRemove',
],
},
},
},
],
};
methods = {
loadOptions: {
// Get all the tags to display them to user so that he can
// select them easily
async getTags(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { tags } = await convertKitApiRequest.call(this, 'GET', '/tags');
for (const tag of tags) {
const tagName = tag.name;
const tagId = tag.id;
returnData.push({
name: tagName,
value: tagId,
});
}
return returnData;
},
// Get all the forms to display them to user so that he can
// select them easily
async getForms(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { forms } = await convertKitApiRequest.call(this, 'GET', '/forms');
for (const form of forms) {
const formName = form.name;
const formId = form.id;
returnData.push({
name: formName,
value: formId,
});
}
return returnData;
},
// Get all the sequences to display them to user so that he can
// select them easily
async getSequences(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { courses } = await convertKitApiRequest.call(this, 'GET', '/sequences');
for (const course of courses) {
const courseName = course.name;
const courseId = course.id;
returnData.push({
name: courseName,
value: courseId,
});
}
return returnData;
},
}
};
// @ts-ignore (because of request)
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
// THe API does not have an endpoint to list all webhooks
if (webhookData.webhookId) {
return true;
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
let event = this.getNodeParameter('event', 0) as string;
const endpoint = '/automations/hooks';
if (event === 'purchaseCreate') {
event = `purchase.${snakeCase(event)}`;
} else {
event = `subscriber.${snakeCase(event)}`;
}
const body: IDataObject = {
target_url: webhookUrl as string,
event: {
name: event
},
};
if (event === 'subscriber.form_subscribe') {
//@ts-ignore
body.event['form_id'] = this.getNodeParameter('formId', 0);
}
if (event === 'subscriber.course_subscribe' || event === 'subscriber.course_complete') {
//@ts-ignore
body.event['sequence_id'] = this.getNodeParameter('courseId', 0);
}
if (event === 'subscriber.link_click') {
//@ts-ignore
body.event['initiator_value'] = this.getNodeParameter('link', 0);
}
if (event === 'subscriber.product_purchase') {
//@ts-ignore
body.event['product_id'] = this.getNodeParameter('productId', 0);
}
if (event === 'subscriber.tag_add' || event === 'subscriber.tag_remove') {
//@ts-ignore
body.event['tag_id'] = this.getNodeParameter('tagId', 0);
}
const webhook = await convertKitApiRequest.call(this, 'POST', endpoint, body);
if (webhook.rule.id === undefined) {
return false;
}
const webhookData = this.getWorkflowStaticData('node');
webhookData.webhookId = webhook.rule.id as string;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const endpoint = `/automations/hooks/${webhookData.webhookId}`;
try {
await convertKitApiRequest.call(this, 'DELETE', endpoint);
} catch (error) {
return false;
}
delete webhookData.webhookId;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const returnData: IDataObject[] = [];
returnData.push(this.getBodyData());
return {
workflowData: [
this.helpers.returnJsonArray(returnData),
],
};
}
}

View file

@ -0,0 +1,124 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const customFieldOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'customField',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a field',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a field',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all fields',
},
{
name: 'Update',
value: 'update',
description: 'Update a field',
},
],
default: 'update',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const customFieldFields = [
{
displayName: 'Field ID',
name: 'id',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'customField',
],
operation: [
'update',
'delete',
],
},
},
default: '',
description: 'The ID of your custom field.',
},
{
displayName: 'Label',
name: 'label',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'customField',
],
operation: [
'update',
'create',
],
},
},
default: '',
description: 'The label of the custom field.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'customField',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'customField',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -0,0 +1,220 @@
import {
INodeProperties
} from 'n8n-workflow';
export const formOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'form',
],
},
},
options: [
{
name: 'Add Subscriber',
value: 'addSubscriber',
description: 'Add a subscriber',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all forms',
},
{
name: 'Get Subscriptions',
value: 'getSubscriptions',
description: 'List subscriptions to a form including subscriber data',
},
],
default: 'addSubscriber',
description: 'The operations to perform.',
},
] as INodeProperties[];
export const formFields = [
{
displayName: 'Form ID',
name: 'id',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getForms',
},
required: true,
displayOptions: {
show: {
resource: [
'form',
],
operation: [
'addSubscriber',
'getSubscriptions',
],
},
},
default: '',
description: 'Form ID.',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'form',
],
operation: [
'addSubscriber',
],
},
},
default: '',
description: `The subscriber's email address.`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'form',
],
operation: [
'addSubscriber',
],
},
},
options: [
{
displayName: 'Custom Fields',
name: 'fieldsUi',
placeholder: 'Add Custom Field',
description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'fieldsValues',
displayName: 'Custom Field',
values: [
{
displayName: 'Field Key',
name: 'key',
type: 'string',
default: '',
placeholder: 'last_name',
description: `The field's key.`,
},
{
displayName: 'Field Value',
name: 'value',
type: 'string',
default: '',
placeholder: 'Doe',
description: 'Value of the field.',
},
],
},
],
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
description: `The subscriber's first name.`,
},
],
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
'getSubscriptions',
],
resource: [
'form',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
'getSubscriptions',
],
resource: [
'form',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'form',
],
operation: [
'getSubscriptions',
],
},
},
options: [
{
displayName: 'Subscriber State',
name: 'subscriberState',
type: 'options',
options: [
{
name: 'Active',
value: 'active',
},
{
name: 'Cancelled',
value: 'cancelled',
},
],
default: 'active',
},
],
description: 'Receive only active subscribers or cancelled subscribers.',
},
] as INodeProperties[];

View file

@ -0,0 +1,67 @@
import {
OptionsWithUri
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
IHookFunctions
} from 'n8n-workflow';
export async function convertKitApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions,
method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('convertKitApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
let options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
},
method,
qs,
body,
uri: uri || `https://api.convertkit.com/v3${endpoint}`,
json: true,
};
options = Object.assign({}, options, option);
if (Object.keys(options.body).length === 0) {
delete options.body;
}
// it's a webhook so include the api secret on the body
if ((options.uri as string).includes('/automations/hooks')) {
options.body['api_secret'] = credentials.apiSecret;
} else {
qs.api_secret = credentials.apiSecret;
}
if (Object.keys(options.qs).length === 0) {
delete options.qs;
}
try {
return await this.helpers.request!(options);
} catch (error) {
let errorMessage = error;
if (error.response && error.response.body && error.response.body.message) {
errorMessage = error.response.body.message;
}
throw new Error(`ConvertKit error response: ${errorMessage}`);
}
}

View file

@ -0,0 +1,230 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const sequenceOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'sequence',
],
},
},
options: [
{
name: 'Add Subscriber',
value: 'addSubscriber',
description: 'Add a subscriber',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all sequences',
},
{
name: 'Get Subscriptions',
value: 'getSubscriptions',
description: 'Get all subscriptions to a sequence including subscriber data',
},
],
default: 'addSubscriber',
description: 'The operations to perform.',
},
] as INodeProperties[];
export const sequenceFields = [
{
displayName: 'Sequence ID',
name: 'id',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getSequences',
},
required: true,
displayOptions: {
show: {
resource: [
'sequence',
],
operation: [
'addSubscriber',
'getSubscriptions',
],
},
},
default: '',
description: 'Sequence ID.',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'sequence',
],
operation: [
'addSubscriber',
],
},
},
default: '',
description: `The subscriber's email address.`,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
'getSubscriptions',
],
resource: [
'sequence',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
'getSubscriptions',
],
resource: [
'sequence',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'sequence',
],
operation: [
'addSubscriber',
],
},
},
options: [
{
displayName: 'Custom Fields',
name: 'fieldsUi',
placeholder: 'Add Custom Field',
description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'fieldsValues',
displayName: 'Custom Field',
values: [
{
displayName: 'Field Key',
name: 'key',
type: 'string',
default: '',
placeholder: 'last_name',
description: `The field's key.`,
},
{
displayName: 'Field Value',
name: 'value',
type: 'string',
default: '',
placeholder: 'Doe',
description: 'Value of the field.',
},
],
},
],
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
description: `The subscriber's first name.`,
},
{
displayName: 'Tag IDs',
name: 'tags',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: [],
description: 'Tags',
},
],
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'sequence',
],
operation: [
'getSubscriptions',
],
},
},
options: [
{
displayName: 'Subscriber State',
name: 'subscriberState',
type: 'options',
options: [
{
name: 'Active',
value: 'active',
},
{
name: 'Cancelled',
value: 'cancelled',
},
],
default: 'active',
},
],
description: 'Receive only active subscribers or cancelled subscribers.',
},
] as INodeProperties[];

View file

@ -0,0 +1,94 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const tagOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'tag',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a tag',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all tags',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const tagFields = [
{
displayName: 'Name',
name: 'name',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'tag',
],
operation: [
'create',
],
},
},
default: '',
description: 'Tag name, multiple can be added separated by comma',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'tag',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'tag',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -0,0 +1,219 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const tagSubscriberOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'tagSubscriber',
],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add a tag to a subscriber',
},
{
name: 'Get All',
value: 'getAll',
description: 'List subscriptions to a tag including subscriber data',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a tag from a subscriber',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const tagSubscriberFields = [
{
displayName: 'Tag ID',
name: 'tagId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTags',
},
required: true,
displayOptions: {
show: {
resource: [
'tagSubscriber',
],
operation: [
'add',
'getAll',
'delete',
],
},
},
default: '',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'tagSubscriber',
],
operation: [
'add',
'delete',
],
},
},
default: '',
description: 'Subscriber email address.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'tagSubscriber',
],
operation: [
'add',
],
},
},
options: [
{
displayName: 'Custom Fields',
name: 'fields',
placeholder: 'Add Custom Field',
description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'field',
displayName: 'Custom Field',
values: [
{
displayName: 'Field Key',
name: 'key',
type: 'string',
default: '',
placeholder: 'last_name',
description: `The field's key.`,
},
{
displayName: 'Field Value',
name: 'value',
type: 'string',
default: '',
placeholder: 'Doe',
description: 'Value of the field.',
},
],
},
],
},
{
displayName: 'First Name',
name: 'firstName',
type: 'string',
default: '',
description: 'Subscriber first name.',
},
],
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'tagSubscriber',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'tagSubscriber',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'tagSubscriber',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Subscriber State',
name: 'subscriberState',
type: 'options',
options: [
{
name: 'Active',
value: 'active',
},
{
name: 'Cancelled',
value: 'cancelled',
},
],
default: 'active',
},
],
description: 'Receive only active subscribers or cancelled subscribers.',
},
] as INodeProperties[];

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -1,6 +1,7 @@
import { ITriggerFunctions } from 'n8n-core';
import {
IBinaryData,
IBinaryKeyData,
IDataObject,
INodeExecutionData,
INodeType,
@ -8,7 +9,13 @@ import {
ITriggerResponse,
} from 'n8n-workflow';
import { connect as imapConnect, ImapSimple, ImapSimpleOptions, getParts, Message } from 'imap-simple';
import { connect as imapConnect, ImapSimple, ImapSimpleOptions, getParts, Message } from 'imap-simple';
import {
simpleParser,
Source as ParserSource,
} from 'mailparser';
import * as lodash from 'lodash';
export class EmailReadImap implements INodeType {
description: INodeTypeDescription = {
@ -44,11 +51,11 @@ export class EmailReadImap implements INodeType {
options: [
{
name: 'Mark as read',
value: 'read'
value: 'read',
},
{
name: 'Nothing',
value: 'nothing'
value: 'nothing',
},
],
default: 'read',
@ -59,8 +66,39 @@ export class EmailReadImap implements INodeType {
name: 'downloadAttachments',
type: 'boolean',
default: false,
displayOptions: {
show: {
format: [
'simple',
],
},
},
description: 'If attachments of emails should be downloaded.<br />Only set if needed as it increases processing.',
},
{
displayName: 'Format',
name: 'format',
type: 'options',
options: [
{
name: 'RAW',
value: 'raw',
description: 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used.'
},
{
name: 'Resolved',
value: 'resolved',
description: 'Returns the full email with all data resolved and attachments saved as binary data.',
},
{
name: 'Simple',
value: 'simple',
description: 'Returns the full email; do not use if you wish to gather inline attachments.',
},
],
default: 'simple',
description: 'The format to return the message in',
},
{
displayName: 'Property Prefix Name',
name: 'dataPropertyAttachmentsPrefixName',
@ -68,8 +106,25 @@ export class EmailReadImap implements INodeType {
default: 'attachment_',
displayOptions: {
show: {
format: [
'resolved',
],
},
},
description: 'Prefix for name of the binary property to which to<br />write the attachments. An index starting with 0 will be added.<br />So if name is "attachment_" the first attachment is saved to "attachment_0"',
},
{
displayName: 'Property Prefix Name',
name: 'dataPropertyAttachmentsPrefixName',
type: 'string',
default: 'attachment_',
displayOptions: {
show: {
format: [
'simple',
],
downloadAttachments: [
true
true,
],
},
},
@ -105,7 +160,6 @@ export class EmailReadImap implements INodeType {
const mailbox = this.getNodeParameter('mailbox') as string;
const postProcessAction = this.getNodeParameter('postProcessAction') as string;
const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean;
const options = this.getNodeParameter('options', {}) as IDataObject;
@ -156,16 +210,26 @@ export class EmailReadImap implements INodeType {
// Returns all the new unseen messages
const getNewEmails = async (connection: ImapSimple): Promise<INodeExecutionData[]> => {
const format = this.getNodeParameter('format', 0) as string;
const searchCriteria = [
'UNSEEN'
];
const fetchOptions = {
bodies: ['HEADER', 'TEXT'],
markSeen: postProcessAction === 'read',
struct: true,
};
let fetchOptions = {};
if (format === 'simple' || format === 'raw') {
fetchOptions = {
bodies: ['TEXT', 'HEADER'],
markSeen: postProcessAction === 'read',
struct: true,
};
} else if (format === 'resolved') {
fetchOptions = {
bodies: [''],
markSeen: postProcessAction === 'read',
struct: true,
};
}
const results = await connection.search(searchCriteria, fetchOptions);
@ -174,10 +238,7 @@ export class EmailReadImap implements INodeType {
let attachments: IBinaryData[];
let propertyName: string;
let dataPropertyAttachmentsPrefixName = '';
if (downloadAttachments === true) {
dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string;
}
// All properties get by default moved to metadata except the ones
// which are defined here which get set on the top level.
@ -188,45 +249,83 @@ export class EmailReadImap implements INodeType {
'subject',
'to',
];
for (const message of results) {
const parts = getParts(message.attributes.struct!);
if (format === 'resolved') {
const dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string;
newEmail = {
json: {
textHtml: await getText(parts, message, 'html'),
textPlain: await getText(parts, message, 'plain'),
metadata: {} as IDataObject,
for (const message of results) {
const part = lodash.find(message.parts, { which: '' });
if (part === undefined) {
throw new Error('Email part could not be parsed.');
}
};
const parsedEmail = await parseRawEmail.call(this, part.body, dataPropertyAttachmentsPrefixName);
messageHeader = message.parts.filter((part) => {
return part.which === 'HEADER';
});
messageBody = messageHeader[0].body;
for (propertyName of Object.keys(messageBody)) {
if (messageBody[propertyName].length) {
if (topLevelProperties.includes(propertyName)) {
newEmail.json[propertyName] = messageBody[propertyName][0];
} else {
(newEmail.json.metadata as IDataObject)[propertyName] = messageBody[propertyName][0];
}
}
newEmails.push(parsedEmail);
}
} else if (format === 'simple') {
const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean;
let dataPropertyAttachmentsPrefixName = '';
if (downloadAttachments === true) {
// Get attachments and add them if any get found
attachments = await getAttachment(connection, parts, message);
if (attachments.length) {
newEmail.binary = {};
for (let i = 0; i < attachments.length; i++) {
newEmail.binary[`${dataPropertyAttachmentsPrefixName}${i}`] = attachments[i];
}
}
dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string;
}
newEmails.push(newEmail);
for (const message of results) {
const parts = getParts(message.attributes.struct!);
newEmail = {
json: {
textHtml: await getText(parts, message, 'html'),
textPlain: await getText(parts, message, 'plain'),
metadata: {} as IDataObject,
}
};
messageHeader = message.parts.filter((part) => {
return part.which === 'HEADER';
});
messageBody = messageHeader[0].body;
for (propertyName of Object.keys(messageBody)) {
if (messageBody[propertyName].length) {
if (topLevelProperties.includes(propertyName)) {
newEmail.json[propertyName] = messageBody[propertyName][0];
} else {
(newEmail.json.metadata as IDataObject)[propertyName] = messageBody[propertyName][0];
}
}
}
if (downloadAttachments === true) {
// Get attachments and add them if any get found
attachments = await getAttachment(connection, parts, message);
if (attachments.length) {
newEmail.binary = {};
for (let i = 0; i < attachments.length; i++) {
newEmail.binary[`${dataPropertyAttachmentsPrefixName}${i}`] = attachments[i];
}
}
}
newEmails.push(newEmail);
}
} else if (format === 'raw') {
for (const message of results) {
const part = lodash.find(message.parts, { which: 'TEXT' });
if (part === undefined) {
throw new Error('Email part could not be parsed.');
}
// Return base64 string
newEmail = {
json: {
raw: part.body
}
};
newEmails.push(newEmail);
}
}
return newEmails;
@ -277,3 +376,32 @@ export class EmailReadImap implements INodeType {
}
}
export async function parseRawEmail(this: ITriggerFunctions, messageEncoded: ParserSource, dataPropertyNameDownload: string): Promise<INodeExecutionData> {
const responseData = await simpleParser(messageEncoded);
const headers: IDataObject = {};
for (const header of responseData.headerLines) {
headers[header.key] = header.line;
}
// @ts-ignore
responseData.headers = headers;
// @ts-ignore
responseData.headerLines = undefined;
const binaryData: IBinaryKeyData = {};
if (responseData.attachments) {
for (let i = 0; i < responseData.attachments.length; i++) {
const attachment = responseData.attachments[i];
binaryData[`${dataPropertyNameDownload}${i}`] = await this.helpers.prepareBinaryData(attachment.content, attachment.filename, attachment.contentType);
}
// @ts-ignore
responseData.attachments = undefined;
}
return {
json: responseData as unknown as IDataObject,
binary: Object.keys(binaryData).length ? binaryData : undefined,
} as INodeExecutionData;
}

View file

@ -821,7 +821,7 @@ export class HttpRequest implements INodeType {
}
else if (oAuth2Api !== undefined) {
//@ts-ignore
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions, 'Bearer');
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions, { tokenType: 'Bearer' });
} else {
response = await this.helpers.request(requestOptions);
}

View file

@ -40,6 +40,11 @@ export const contactOperations = [
value: 'getRecentlyCreatedUpdated',
description: 'Get recently created/updated contacts',
},
{
name: 'Search',
value: 'search',
description: 'Search contacts',
},
],
default: 'upsert',
description: 'The operation to perform.',
@ -166,6 +171,41 @@ export const contactFields = [
type: 'string',
default: '',
},
{
displayName: 'Custom Properties',
name: 'customPropertiesUi',
placeholder: 'Add Custom Property',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'customPropertiesValues',
displayName: 'Custom Property',
values: [
{
displayName: 'Property',
name: 'property',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getContactCustomProperties',
},
default: '',
description: 'Name of the property.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the property',
},
],
},
],
},
{
displayName: 'Date of Birth',
name: 'dateOfBirth',
@ -834,4 +874,242 @@ export const contactFields = [
},
],
},
//*-------------------------------------------------------------------------- */
/* contact:search */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'search',
],
},
},
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: [
'contact',
],
operation: [
'search',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 250,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Filter Groups',
name: 'filterGroupsUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Filter Group',
typeOptions: {
multipleValues: true,
},
required: false,
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'search',
],
},
},
options: [
{
name: 'filterGroupsValues',
displayName: 'Filter Group',
values: [
{
displayName: 'Filters',
name: 'filtersUi',
type: 'fixedCollection',
default: '',
placeholder: 'Add Filter',
typeOptions: {
multipleValues: true,
},
required: false,
options: [
{
name: 'filterValues',
displayName: 'Filter',
values: [
{
displayName: 'Property Name',
name: 'propertyName',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getContactProperties',
},
default: '',
},
{
displayName: 'Operator',
name: 'operator',
type: 'options',
options: [
{
name: 'Equal',
value: 'EQ',
},
{
name: 'Not Equal',
value: 'NEQ',
},
{
name: 'Less Than',
value: 'LT',
},
{
name: 'Less Than Or Equal',
value: 'LTE',
},
{
name: 'Greater Than',
value: 'GT',
},
{
name: 'Greater Than Or Equal',
value: 'GTE',
},
{
name: 'Is Known',
value: 'HAS_PROPERTY',
},
{
name: 'Is Unknown',
value: 'NOT_HAS_PROPERTY',
},
{
name: 'Contains Exactly',
value: 'CONSTAIN_TOKEN',
},
{
name: `Doesn't Contain Exactly`,
value: 'NOT_CONSTAIN_TOKEN',
},
],
default: 'EQ',
},
{
displayName: 'Value',
name: 'value',
displayOptions: {
hide: {
operator: [
'HAS_PROPERTY',
'NOT_HAS_PROPERTY',
],
},
},
type: 'string',
default: '',
},
],
}
],
description: 'Use filters to limit the results to only CRM objects with matching property values. More info <a href="https://developers.hubspot.com/docs/api/crm/search">here</a>',
},
],
}
],
description: `When multiple filters are provided within a filterGroup, they will be combined using a logical AND operator.<br>
When multiple filterGroups are provided, they will be combined using a logical OR operator.<br>
The system supports a maximum of three filterGroups with up to three filters each.<br>
More info <a href="https://developers.hubspot.com/docs/api/crm/search">here</a>`
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'search',
],
},
},
options: [
{
displayName: 'Direction',
name: 'direction',
type: 'options',
options: [
{
name: 'ASC',
value: 'ASCENDING',
},
{
name: 'DESC',
value: 'DESCENDING',
},
],
default: 'DESCENDING',
description: 'Defines the direction in which search results are ordered. Default value is DESC.',
},
{
displayName: 'Fields',
name: 'properties',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getContactProperties',
},
default: [
'firstname',
'lastname',
'email',
],
description: `Used to include specific company properties in the results.<br/>
By default, the results will only include company ID and will not include the values for any properties for your companys.<br/>
Including this parameter will include the data for the specified property in the results.<br/>
You can include this parameter multiple times to request multiple properties separed by ,.`,
},
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
description: 'Perform a text search against all property values for an object type',
},
{
displayName: 'Sort By',
name: 'sortBy',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getContactProperties',
},
default: 'createdate',
},
],
},
] as INodeProperties[];

View file

@ -307,6 +307,25 @@ export class Hubspot implements INodeType {
return returnData;
},
// Get all the contact properties to display them to user so that he can
// select them easily
async getContactCustomProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const endpoint = '/properties/v2/contacts/properties';
const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {});
for (const property of properties) {
if (property.hubspotDefined === null) {
const propertyName = property.label;
const propertyId = property.name;
returnData.push({
name: propertyName,
value: propertyId,
});
}
}
return returnData;
},
// Get all the contact number of employees options to display them to user so that he can
// select them easily
async getContactNumberOfEmployees(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
@ -1064,6 +1083,20 @@ export class Hubspot implements INodeType {
value: additionalFields.workEmail,
});
}
if (additionalFields.customPropertiesUi) {
const customProperties = (additionalFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[];
if (customProperties) {
for (const customProperty of customProperties) {
body.push({
property: customProperty.property,
value: customProperty.value,
});
}
}
}
const endpoint = `/contacts/v1/contact/createOrUpdate/email/${email}`;
responseData = await hubspotApiRequest.call(this, 'POST', endpoint, { properties: body });
@ -1166,6 +1199,53 @@ export class Hubspot implements INodeType {
const endpoint = `/contacts/v1/contact/vid/${contactId}`;
responseData = await hubspotApiRequest.call(this, 'DELETE', endpoint);
}
//https://developers.hubspot.com/docs/api/crm/search
if (operation === 'search') {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const filtersGroupsUi = this.getNodeParameter('filterGroupsUi', i) as IDataObject;
const sortBy = additionalFields.sortBy || 'createdate';
const direction = additionalFields.direction || 'DESCENDING';
const body: IDataObject = {
sorts: [
{
propertyName: sortBy,
direction,
},
],
};
if (filtersGroupsUi) {
const filterGroupValues = (filtersGroupsUi as IDataObject).filterGroupsValues as IDataObject[];
if (filterGroupValues) {
body.filterGroups = [];
for (const filterGroupValue of filterGroupValues) {
if (filterGroupValue.filtersUi) {
const filterValues = (filterGroupValue.filtersUi as IDataObject).filterValues as IDataObject[];
if (filterValues) {
//@ts-ignore
body.filterGroups.push({ filters: filterValues });
}
}
}
}
}
Object.assign(body, additionalFields);
const endpoint = '/crm/v3/objects/contacts/search';
if (returnAll) {
responseData = await hubspotApiRequestAllItems.call(this, 'results', 'POST', endpoint, body, qs);
} else {
qs.count = this.getNodeParameter('limit', 0) as number;
responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body, qs);
responseData = responseData.results;
}
}
}
//https://developers.hubspot.com/docs/methods/companies/companies-overview
if (resource === 'company') {

View file

@ -0,0 +1,870 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const couponOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'coupon',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a coupon.',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all coupons.',
},
{
name: 'Update',
value: 'update',
description: 'Update a coupon.',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const couponFields = [
/* -------------------------------------------------------------------------- */
/* coupon:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Coupon Type',
name: 'couponType',
type: 'options',
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
`create`
],
jsonParameters: [
false
]
},
},
default: 'checkout',
description: 'Either product (valid for specified products or subscription plans) or checkout (valid for any checkout).',
options: [
{
name: 'Checkout',
value: 'checkout'
},
{
name: 'Product',
value: 'product'
},
]
},
{
displayName: 'Product IDs',
name: 'productIds',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getProducts',
},
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
`create`
],
couponType: [
'product',
],
jsonParameters: [
false
]
},
},
default: '',
description: 'Comma-separated list of product IDs. Required if coupon_type is product.',
required: true,
},
{
displayName: 'Discount Type',
name: 'discountType',
type: 'options',
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
`create`
],
jsonParameters: [
false
]
},
},
default: 'flat',
description: 'Either flat or percentage.',
options: [
{
name: 'Flat',
value: 'flat'
},
{
name: 'Percentage',
value: 'percentage'
},
]
},
{
displayName: 'Discount Amount Currency',
name: 'discountAmount',
type: 'number',
default: '',
description: 'Discount amount in currency.',
typeOptions: {
minValue: 1
},
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
`create`
],
discountType: [
'flat',
],
jsonParameters: [
false
]
},
},
},
{
displayName: 'Discount Amount %',
name: 'discountAmount',
type: 'number',
default: '',
description: 'Discount amount in percentage.',
typeOptions: {
minValue: 1,
maxValue: 100
},
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
`create`
],
discountType: [
'percentage',
],
jsonParameters: [
false
]
},
},
},
{
displayName: 'Currency',
name: 'currency',
type: 'options',
default: 'EUR',
description: 'The currency must match the balance currency specified in your account.',
options: [
{
name: 'ARS',
value: 'ARS'
},
{
name: 'AUD',
value: 'AUD'
},
{
name: 'BRL',
value: 'BRL'
},
{
name: 'CAD',
value: 'CAD'
},
{
name: 'CHF',
value: 'CHF'
},
{
name: 'CNY',
value: 'CNY'
},
{
name: 'CZK',
value: 'CZK'
},
{
name: 'DKK',
value: 'DKK'
},
{
name: 'EUR',
value: 'EUR'
},
{
name: 'GBP',
value: 'GBP'
},
{
name: 'HKD',
value: 'HKD'
},
{
name: 'HUF',
value: 'HUF'
},
{
name: 'INR',
value: 'INR'
},
{
name: 'JPY',
value: 'JPY'
},
{
name: 'KRW',
value: 'KRW'
},
{
name: 'MXN',
value: 'MXN'
},
{
name: 'NOK',
value: 'NOK'
},
{
name: 'NZD',
value: 'NZD'
},
{
name: 'PLN',
value: 'PLN'
},
{
name: 'RUB',
value: 'RUB'
},
{
name: 'SEK',
value: 'SEK'
},
{
name: 'SGD',
value: 'SGD'
},
{
name: 'THB',
value: 'THB'
},
{
name: 'TWD',
value: 'TWD'
},
{
name: 'USD',
value: 'USD'
},
{
name: 'ZAR',
value: 'ZAR'
},
],
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
`create`
],
discountType: [
'flat',
],
jsonParameters: [
false
]
},
},
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
'create',
],
},
},
},
{
displayName: ' Additional Fields',
name: 'additionalFieldsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
'create',
],
jsonParameters: [
true,
],
},
},
description: `Attributes in JSON form.`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
'create',
],
jsonParameters: [
false
]
},
},
default: {},
options: [
{
displayName: 'Allowed Uses',
name: 'allowedUses',
type: 'number',
default: 1,
description: 'Number of times a coupon can be used in a checkout. This will be set to 999,999 by default, if not specified.',
},
{
displayName: 'Coupon Code',
name: 'couponCode',
type: 'string',
default: '',
description: 'Will be randomly generated if not specified.',
},
{
displayName: 'Coupon Prefix',
name: 'couponPrefix',
type: 'string',
default: '',
description: 'Prefix for generated codes. Not valid if coupon_code is specified.',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
description: 'Description of the coupon. This will be displayed in the Seller Dashboard.',
},
{
displayName: 'Expires',
name: 'expires',
type: 'dateTime',
default: '',
description: 'The coupon will expire on the date at 00:00:00 UTC.',
},
{
displayName: 'Group',
name: 'group',
type: 'string',
typeOptions: {
minValue: 1,
maxValue: 50
},
default: '',
description: 'The name of the coupon group this coupon should be assigned to.',
},
{
displayName: 'Number of Coupons',
name: 'numberOfCoupons',
type: 'number',
default: 1,
description: 'Number of coupons to generate. Not valid if coupon_code is specified.',
},
{
displayName: 'Recurring',
name: 'recurring',
type: 'boolean',
default: false,
description: 'If the coupon is used on subscription products, this indicates whether the discount should apply to recurring payments after the initial purchase.',
},
],
},
/* -------------------------------------------------------------------------- */
/* coupon:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Product ID',
name: 'productId',
type: 'string',
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
`getAll`
]
},
},
default: '',
required: true,
description: 'The specific product/subscription ID.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'coupon',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'coupon',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
/* -------------------------------------------------------------------------- */
/* coupon:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Update by',
name: 'updateBy',
type: 'options',
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
`update`
],
jsonParameters: [
false,
],
},
},
default: 'couponCode',
description: 'Either flat or percentage.',
options: [
{
name: 'Coupon Code',
value: 'couponCode'
},
{
name: 'Group',
value: 'group'
},
]
},
{
displayName: 'Coupon Code',
name: 'couponCode',
type: 'string',
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
'update'
],
updateBy: [
'couponCode'
],
jsonParameters: [
false,
],
},
},
default: '',
description: 'Identify the coupon to update',
},
{
displayName: 'Group',
name: 'group',
type: 'string',
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
'update'
],
updateBy: [
'group'
],
jsonParameters: [
false,
],
},
},
default: '',
description: 'The name of the group of coupons you want to update.',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
'update',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFieldsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
'update',
],
jsonParameters: [
true,
],
},
},
description: `Attributes in JSON form.`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'coupon',
],
operation: [
'update',
],
jsonParameters: [
false
]
},
},
default: {},
options: [
{
displayName: 'Allowed Uses',
name: 'allowedUses',
type: 'number',
default: 1,
description: 'Number of times a coupon can be used in a checkout. This will be set to 999,999 by default, if not specified.',
},
{
displayName: 'Discount',
name: 'discount',
type: 'fixedCollection',
default: 'discountProperties',
options: [
{
displayName: 'Discount Properties',
name: 'discountProperties',
values: [
{
displayName: 'Currency',
name: 'currency',
type: 'options',
default: 'EUR',
description: 'The currency must match the balance currency specified in your account.',
displayOptions: {
show: {
discountType: [
'flat',
],
},
},
options: [
{
name: 'ARS',
value: 'ARS'
},
{
name: 'AUD',
value: 'AUD'
},
{
name: 'BRL',
value: 'BRL'
},
{
name: 'CAD',
value: 'CAD'
},
{
name: 'CHF',
value: 'CHF'
},
{
name: 'CNY',
value: 'CNY'
},
{
name: 'CZK',
value: 'CZK'
},
{
name: 'DKK',
value: 'DKK'
},
{
name: 'EUR',
value: 'EUR'
},
{
name: 'GBP',
value: 'GBP'
},
{
name: 'HKD',
value: 'HKD'
},
{
name: 'HUF',
value: 'HUF'
},
{
name: 'INR',
value: 'INR'
},
{
name: 'JPY',
value: 'JPY'
},
{
name: 'KRW',
value: 'KRW'
},
{
name: 'MXN',
value: 'MXN'
},
{
name: 'NOK',
value: 'NOK'
},
{
name: 'NZD',
value: 'NZD'
},
{
name: 'PLN',
value: 'PLN'
},
{
name: 'RUB',
value: 'RUB'
},
{
name: 'SEK',
value: 'SEK'
},
{
name: 'SGD',
value: 'SGD'
},
{
name: 'THB',
value: 'THB'
},
{
name: 'TWD',
value: 'TWD'
},
{
name: 'USD',
value: 'USD'
},
{
name: 'ZAR',
value: 'ZAR'
},
],
},
{
displayName: 'Discount Amount Currency',
name: 'discountAmount',
type: 'number',
default: '',
description: 'Discount amount.',
displayOptions: {
show: {
discountType: [
'flat',
],
},
},
typeOptions: {
minValue: 0
},
},
{
displayName: 'Discount Amount Percentage',
name: 'discountAmount',
type: 'number',
default: '',
description: 'Discount amount.',
displayOptions: {
show: {
discountType: [
'percentage',
],
},
},
typeOptions: {
minValue: 0,
maxValue: 100
},
},
{
displayName: 'Discount Type',
name: 'discountType',
type: 'options',
default: 'flat',
description: 'Either flat or percentage.',
options: [
{
name: 'Flat',
value: 'flat'
},
{
name: 'Percentage',
value: 'percentage'
},
]
},
],
},
],
},
{
displayName: 'Expires',
name: 'expires',
type: 'dateTime',
default: '',
description: 'The coupon will expire on the date at 00:00:00 UTC.',
},
{
displayName: 'New Coupon Code',
name: 'newCouponCode',
type: 'string',
default: '',
description: 'New code to rename the coupon to.',
},
{
displayName: 'New Group Name',
name: 'newGroup',
type: 'string',
typeOptions: {
minValue: 1,
maxValue: 50
},
default: '',
description: 'New group name to move coupon to.',
},
{
displayName: 'Product IDs',
name: 'productIds',
type: 'string',
default: '',
description: 'Comma-separated list of products e.g. 499531,1234,123546. If blank then remove associated products.',
},
{
displayName: 'Recurring',
name: 'recurring',
type: 'boolean',
default: false,
description: 'If the coupon is used on subscription products, this indicates whether the discount should apply to recurring payments after the initial purchase.',
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,76 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IExecuteSingleFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function paddleApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('paddleApi');
if (credentials === undefined) {
throw new Error('Could not retrieve credentials!');
}
const options: OptionsWithUri = {
method,
headers: {
'content-type': 'application/json'
},
uri: `https://vendors.paddle.com/api${endpoint}`,
body,
json: true
};
body['vendor_id'] = credentials.vendorId;
body['vendor_auth_code'] = credentials.vendorAuthCode;
try {
const response = await this.helpers.request!(options);
if (!response.success) {
throw new Error(`Code: ${response.error.code}. Message: ${response.error.message}`);
}
return response;
} catch (error) {
throw new Error(`ERROR: Code: ${error.code}. Message: ${error.message}`);
}
}
export async function paddleApiRequestAllItems(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;
body.results_per_page = 200;
body.page = 1;
do {
responseData = await paddleApiRequest.call(this, endpoint, method, body, query);
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData[propertyName].length !== 0
);
return returnData;
}
export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = undefined;
}
return result;
}

View file

@ -0,0 +1,52 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const orderOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'order',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get an order',
}
],
default: 'get',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const orderFields = [
/* -------------------------------------------------------------------------- */
/* order:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Checkout ID',
name: 'checkoutId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'order',
],
operation: [
'get',
],
},
},
description: 'The identifier of the buyers checkout.',
},
] as INodeProperties[];

View file

@ -0,0 +1,517 @@
import {
IExecuteFunctions
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
couponFields,
couponOperations,
} from './CouponDescription';
import {
paddleApiRequest,
paddleApiRequestAllItems,
validateJSON
} from './GenericFunctions';
import {
paymentFields,
paymentOperations,
} from './PaymentDescription';
import {
planFields,
planOperations,
} from './PlanDescription';
import {
productFields,
productOperations,
} from './ProductDescription';
import {
userFields,
userOperations,
} from './UserDescription';
// import {
// orderOperations,
// orderFields,
// } from './OrderDescription';
import * as moment from 'moment';
export class Paddle implements INodeType {
description: INodeTypeDescription = {
displayName: 'Paddle',
name: 'paddle',
icon: 'file:paddle.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Paddle API',
defaults: {
name: 'Paddle',
color: '#45567c',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'paddleApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Coupon',
value: 'coupon',
},
{
name: 'Payment',
value: 'payment',
},
{
name: 'Plan',
value: 'plan',
},
{
name: 'Product',
value: 'product',
},
// {
// name: 'Order',
// value: 'order',
// },
{
name: 'User',
value: 'user',
},
],
default: 'coupon',
description: 'Resource to consume.',
},
// COUPON
...couponOperations,
...couponFields,
// PAYMENT
...paymentOperations,
...paymentFields,
// PLAN
...planOperations,
...planFields,
// PRODUCT
...productOperations,
...productFields,
// ORDER
// ...orderOperations,
// ...orderFields,
// USER
...userOperations,
...userFields
],
};
methods = {
loadOptions: {
/* -------------------------------------------------------------------------- */
/* PAYMENT */
/* -------------------------------------------------------------------------- */
// Get all payment so they can be selected in payment rescheduling
async getPayments(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const endpoint = '/2.0/subscription/payments';
const paymentResponse = await paddleApiRequest.call(this, endpoint, 'POST', {});
// Alert user if there's no payments present to be loaded into payments property
if (paymentResponse.response === undefined || paymentResponse.response.length === 0) {
throw Error('No payments on account.');
}
for (const payment of paymentResponse.response) {
const id = payment.id;
returnData.push({
name: id,
value: id,
});
}
return returnData;
},
/* -------------------------------------------------------------------------- */
/* PRODUCTS */
/* -------------------------------------------------------------------------- */
// Get all Products so they can be selected in coupon creation when assigning products
async getProducts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const endpoint = '/2.0/product/get_products';
const products = await paddleApiRequest.call(this, endpoint, 'POST', {});
// Alert user if there's no products present to be loaded into payments property
if (products.length === 0) {
throw Error('No products on account.');
}
for (const product of products) {
const name = product.name;
const id = product.id;
returnData.push({
name,
value: id,
});
}
return returnData;
},
}
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
let responseData;
const body: IDataObject = {};
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
if (resource === 'coupon') {
if (operation === 'create') {
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
if (jsonParameters) {
const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string;
if (additionalFieldsJson !== '') {
if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson));
} else {
throw new Error('Additional fields must be a valid JSON');
}
}
} else {
const discountType = this.getNodeParameter('discountType', i) as string;
const couponType = this.getNodeParameter('couponType', i) as string;
const discountAmount = this.getNodeParameter('discountAmount', i) as number;
if (couponType === 'product') {
body.product_ids = (this.getNodeParameter('productIds', i) as string[]).join();
}
if (discountType === 'flat') {
body.currency = this.getNodeParameter('currency', i) as string;
}
body.coupon_type = couponType;
body.discount_type = discountType;
body.discount_amount = discountAmount;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.allowedUses) {
body.allowed_uses = additionalFields.allowedUses as number;
}
if (additionalFields.couponCode) {
body.coupon_code = additionalFields.couponCode as string;
}
if (additionalFields.couponPrefix) {
body.coupon_prefix = additionalFields.couponPrefix as string;
}
if (additionalFields.expires) {
body.expires = moment(additionalFields.expires as Date).format('YYYY-MM-DD') as string;
}
if (additionalFields.group) {
body.group = additionalFields.group as string;
}
if (additionalFields.recurring) {
body.recurring = 1;
} else {
body.recurring = 0;
}
if (additionalFields.numberOfCoupons) {
body.num_coupons = additionalFields.numberOfCoupons as number;
}
if (additionalFields.description) {
body.description = additionalFields.description as string;
}
const endpoint = '/2.1/product/create_coupon';
responseData = await paddleApiRequest.call(this, endpoint, 'POST', body);
responseData = responseData.response.coupon_codes;
}
}
if (operation === 'getAll') {
const productId = this.getNodeParameter('productId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const endpoint = '/2.0/product/list_coupons';
body.product_id = productId as string;
responseData = await paddleApiRequest.call(this, endpoint, 'POST', body);
if (returnAll) {
responseData = responseData.response;
} else {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.response.splice(0, limit);
}
}
if (operation === 'update') {
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
if (jsonParameters) {
const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string;
if (additionalFieldsJson !== '') {
if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson));
} else {
throw new Error('Additional fields must be a valid JSON');
}
}
} else {
const updateBy = this.getNodeParameter('updateBy', i) as string;
if (updateBy === 'group') {
body.group = this.getNodeParameter('group', i) as string;
} else {
body.coupon_code = this.getNodeParameter('couponCode', i) as string;
}
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.allowedUses) {
body.allowed_uses = additionalFields.allowedUses as number;
}
if (additionalFields.currency) {
body.currency = additionalFields.currency as string;
}
if (additionalFields.newCouponCode) {
body.new_coupon_code = additionalFields.newCouponCode as string;
}
if (additionalFields.expires) {
body.expires = moment(additionalFields.expires as Date).format('YYYY-MM-DD') as string;
}
if (additionalFields.newGroup) {
body.new_group = additionalFields.newGroup as string;
}
if (additionalFields.recurring === true) {
body.recurring = 1;
} else if (additionalFields.recurring === false) {
body.recurring = 0;
}
if (additionalFields.productIds) {
body.product_ids = additionalFields.productIds as number;
}
if (additionalFields.discountAmount) {
body.discount_amount = additionalFields.discountAmount as number;
}
if (additionalFields.discount) {
//@ts-ignore
if (additionalFields.discount.discountProperties.discountType === 'percentage') {
// @ts-ignore
body.discount_amount = additionalFields.discount.discountProperties.discountAmount as number;
} else {
//@ts-ignore
body.currency = additionalFields.discount.discountProperties.currency as string;
//@ts-ignore
body.discount_amount = additionalFields.discount.discountProperties.discountAmount as number;
}
}
}
const endpoint = '/2.1/product/update_coupon';
responseData = await paddleApiRequest.call(this, endpoint, 'POST', body);
responseData = responseData.response;
}
}
if (resource === 'payment') {
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
if (jsonParameters) {
const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string;
if (additionalFieldsJson !== '') {
if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson));
} else {
throw new Error('Additional fields must be a valid JSON');
}
}
} else {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.subscriptionId) {
body.subscription_id = additionalFields.subscriptionId as number;
}
if (additionalFields.plan) {
body.plan = additionalFields.plan as string;
}
if (additionalFields.state) {
body.state = additionalFields.state as string;
}
if (additionalFields.isPaid) {
body.is_paid = 1;
} else {
body.is_paid = 0;
}
if (additionalFields.from) {
body.from = moment(additionalFields.from as Date).format('YYYY-MM-DD') as string;
}
if (additionalFields.to) {
body.to = moment(additionalFields.to as Date).format('YYYY-MM-DD') as string;
}
if (additionalFields.isOneOffCharge) {
body.is_one_off_charge = additionalFields.isOneOffCharge as boolean;
}
}
const endpoint = '/2.0/subscription/payments';
responseData = await paddleApiRequest.call(this, endpoint, 'POST', body);
if (returnAll) {
responseData = responseData.response;
} else {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.response.splice(0, limit);
}
}
if (operation === 'reschedule') {
const paymentId = this.getNodeParameter('paymentId', i) as number;
const date = this.getNodeParameter('date', i) as Date;
body.payment_id = paymentId;
body.date = body.to = moment(date as Date).format('YYYY-MM-DD') as string;
const endpoint = '/2.0/subscription/payments_reschedule';
responseData = await paddleApiRequest.call(this, endpoint, 'POST', body);
}
}
if (resource === 'plan') {
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const endpoint = '/2.0/subscription/plans';
responseData = await paddleApiRequest.call(this, endpoint, 'POST', body);
if (returnAll) {
responseData = responseData.response;
} else {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.response.splice(0, limit);
}
}
if (operation === 'get') {
const planId = this.getNodeParameter('planId', i) as string;
body.plan = planId;
const endpoint = '/2.0/subscription/plans';
responseData = await paddleApiRequest.call(this, endpoint, 'POST', body);
responseData = responseData.response;
}
}
if (resource === 'product') {
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const endpoint = '/2.0/product/get_products';
responseData = await paddleApiRequest.call(this, endpoint, 'POST', body);
if (returnAll) {
responseData = responseData.response.products;
} else {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.response.products.splice(0, limit);
}
}
}
if (resource === 'order') {
if (operation === 'get') {
const endpoint = '/1.0/order';
const checkoutId = this.getNodeParameter('checkoutId', i) as string;
body.checkout_id = checkoutId;
responseData = await paddleApiRequest.call(this, endpoint, 'GET', body);
}
}
if (resource === 'user') {
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
if (jsonParameters) {
const additionalFieldsJson = this.getNodeParameter('additionalFieldsJson', i) as string;
if (additionalFieldsJson !== '') {
if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson));
} else {
throw new Error('Additional fields must be a valid JSON');
}
}
} else {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.state) {
body.state = additionalFields.state as string;
}
if (additionalFields.planId) {
body.plan_id = additionalFields.planId as string;
}
if (additionalFields.subscriptionId) {
body.subscription_id = additionalFields.subscriptionId as string;
}
}
const endpoint = '/2.0/subscription/users';
if (returnAll) {
responseData = await paddleApiRequestAllItems.call(this, 'response', endpoint, 'POST', body);
} else {
body.results_per_page = this.getNodeParameter('limit', i) as number;
responseData = await paddleApiRequest.call(this, endpoint, 'POST', body);
responseData = responseData.response;
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as unknown as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,248 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const paymentOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'payment',
],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all payment.',
},
{
name: 'Reschedule',
value: 'reschedule',
description: 'Reschedule payment.',
}
],
default: 'getAll',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const paymentFields = [
/* -------------------------------------------------------------------------- */
/* payment:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'payment',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'payment',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
'payment',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFieldsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
resource: [
'payment',
],
operation: [
'getAll',
],
jsonParameters: [
true,
],
},
},
description: `Attributes in JSON form.`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'payment',
],
operation: [
'getAll',
],
jsonParameters: [
false
]
},
},
default: {},
options: [
{
displayName: 'Date From',
name: 'from',
type: 'dateTime',
default: '',
description: 'payment starting from date.',
},
{
displayName: 'Date To',
name: 'to',
type: 'dateTime',
default: '',
description: 'payment up until date.',
},
{
displayName: 'Is Paid',
name: 'isPaid',
type: 'boolean',
default: false,
description: 'payment is paid.',
},
{
displayName: 'Plan ID',
name: 'plan',
type: 'string',
default: '',
description: 'Filter: The product/plan ID (single or comma-separated values).',
},
{
displayName: 'Subscription ID',
name: 'subscriptionId',
type: 'number',
default: '',
description: 'A specific user subscription ID.',
},
{
displayName: 'State',
name: 'state',
type: 'options',
default: 'active',
description: 'Filter: The user subscription status. Returns all active, past_due, trialing and paused subscription plans if not specified.',
options: [
{
name: 'Active',
value: 'active'
},
{
name: 'Past Due',
value: 'past_due'
},
{
name: 'Paused',
value: 'paused'
},
{
name: 'Trialing',
value: 'trialing'
},
]
},
{
displayName: 'One off charge',
name: 'isOneOffCharge',
type: 'boolean',
default: false,
},
],
},
/* -------------------------------------------------------------------------- */
/* payment:reschedule */
/* -------------------------------------------------------------------------- */
{
displayName: 'Payment ID',
name: 'paymentId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getpayment',
},
default: '',
required: true,
displayOptions: {
show: {
resource: [
'payment',
],
operation: [
'reschedule',
],
},
},
description: 'The upcoming subscription payment ID.',
},
{
displayName: 'Date',
name: 'date',
type: 'dateTime',
default: '',
displayOptions: {
show: {
resource: [
'payment',
],
operation: [
'reschedule',
],
},
},
description: 'Date you want to move the payment to.',
},
] as INodeProperties[];

View file

@ -0,0 +1,98 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const planOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'plan',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get a plan.',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all plans.',
}
],
default: 'get',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const planFields = [
/* -------------------------------------------------------------------------- */
/* plan:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Plan ID',
name: 'planId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'plan',
],
operation: [
'get',
],
},
},
description: 'Filter: The subscription plan ID.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'plan',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'plan',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -0,0 +1,71 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const productOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'product',
],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all products.',
}
],
default: 'getAll',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const productFields = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'product',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'product',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -0,0 +1,176 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const userOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'user',
],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all users',
}
],
default: 'getAll',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const userFields = [
/* -------------------------------------------------------------------------- */
/* 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',
default: 100,
required: true,
typeOptions: {
minValue: 1,
maxValue: 200
},
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
returnAll: [
false
]
},
},
description: 'Number of subscription records to return per page.',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFieldsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
jsonParameters: [
true,
],
},
},
description: `Attributes in JSON form.`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
jsonParameters: [
false
]
},
},
default: {},
options: [
{
displayName: 'Plan ID',
name: 'planId',
type: 'string',
default: '',
description: 'Filter: The subscription plan ID.',
},
{
displayName: 'Subscription ID',
name: 'subscriptionId',
type: 'string',
default: '',
description: 'A specific user subscription ID.',
},
{
displayName: 'State',
name: 'state',
type: 'options',
default: 'active',
description: 'Filter: The user subscription status. Returns all active, past_due, trialing and paused subscription plans if not specified.',
options: [
{
name: 'Active',
value: 'active'
},
{
name: 'Past Due',
value: 'past_due'
},
{
name: 'Paused',
value: 'paused'
},
{
name: 'Trialing',
value: 'trialing'
},
]
},
],
},
] as INodeProperties[];

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -189,16 +189,23 @@ export class Postgres implements INodeType {
const pgp = pgPromise();
const config = {
const config: IDataObject = {
host: credentials.host as string,
port: credentials.port as number,
database: credentials.database as string,
user: credentials.user as string,
password: credentials.password as string,
ssl: !['disable', undefined].includes(credentials.ssl as string | undefined),
sslmode: (credentials.ssl as string) || 'disable',
};
if (credentials.allowUnauthorizedCerts === true) {
config.ssl = {
rejectUnauthorized: false,
};
} else {
config.ssl = !['disable', undefined].includes(credentials.ssl as string | undefined);
config.sslmode = (credentials.ssl as string) || 'disable';
}
const db = pgp(config);
let returnItems = [];

View file

@ -10,10 +10,10 @@ import {
export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('salesforceOAuth2Api');
const subdomain = (credentials!.accessTokenUrl as string).split('.')[0].split('/')[2];
const subdomain = ((credentials!.accessTokenUrl as string).match(/https:\/\/(.+).salesforce\.com/) || [])[1]
const options: OptionsWithUri = {
method,
body,
body: method === "GET" ? undefined : body,
qs,
uri: uri || `https://${subdomain}.salesforce.com/services/data/v39.0${resource}`,
json: true

File diff suppressed because it is too large Load diff

View file

@ -1,66 +1,37 @@
import { OptionsWithUri } from 'request';
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IExecuteSingleFunctions
} from 'n8n-core';
import * as _ from 'lodash';
export const filterAndExecuteForEachTask = async function(
this: IExecuteSingleFunctions,
taskCallback: (t: any) => any
) {
const expression = this.getNodeParameter('expression') as string;
const projectId = this.getNodeParameter('project') as number;
// Enable regular expressions
const reg = new RegExp(expression);
const tasks = await todoistApiRequest.call(this, '/tasks', 'GET');
const filteredTasks = tasks.filter(
// Make sure that project will match no matter what the type is. If project was not selected match all projects
(el: any) => (!projectId || el.project_id) && el.content.match(reg)
);
return {
affectedTasks: (
await Promise.all(filteredTasks.map((t: any) => taskCallback(t)))
)
// This makes it more clear and informative. We pass the ID as a convention and content to give the user confirmation that his/her expression works as expected
.map(
(el, i) =>
el || { id: filteredTasks[i].id, content: filteredTasks[i].content }
)
};
};
import {
IDataObject,
} from 'n8n-workflow';
export async function todoistApiRequest(
this:
| IHookFunctions
| IExecuteFunctions
| IExecuteSingleFunctions
| ILoadOptionsFunctions,
resource: string,
method: string,
body: any = {},
headers?: object
): Promise<any> {
// tslint:disable-line:no-any
const credentials = this.getCredentials('todoistApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const headerWithAuthentication = Object.assign({}, headers, { Authorization: `Bearer ${credentials.apiKey}` });
resource: string,
body: any = {}, // tslint:disable-line:no-any
qs: IDataObject = {},
): Promise<any> { // tslint:disable-line:no-any
const authentication = this.getNodeParameter('authentication', 0, 'apiKey');
const endpoint = 'api.todoist.com/rest/v1';
const options: OptionsWithUri = {
headers: headerWithAuthentication,
headers: {},
method,
qs,
uri: `https://${endpoint}${resource}`,
json: true
json: true,
};
if (Object.keys(body).length !== 0) {
@ -68,13 +39,25 @@ export async function todoistApiRequest(
}
try {
return this.helpers.request!(options);
if (authentication === 'apiKey') {
const credentials = this.getCredentials('todoistApi') as IDataObject;
//@ts-ignore
options.headers['Authorization'] = `Bearer ${credentials.apiKey}`;
return this.helpers.request!(options);
} else {
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'todoistOAuth2Api', options);
}
} catch (error) {
const errorMessage = error.response.body.message || error.response.body.Message;
const errorMessage = error.response.body;
if (errorMessage !== undefined) {
throw errorMessage;
throw new Error(errorMessage);
}
throw error.response.body;
throw errorMessage;
}
}

View file

@ -1,6 +1,7 @@
import {
IExecuteSingleFunctions,
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
@ -9,12 +10,11 @@ import {
ILoadOptionsFunctions,
INodePropertyOptions,
} from 'n8n-workflow';
import {
todoistApiRequest,
filterAndExecuteForEachTask,
} from './GenericFunctions';
interface IBodyCreateTask {
content: string;
project_id?: number;
@ -48,9 +48,44 @@ export class Todoist implements INodeType {
{
name: 'todoistApi',
required: true,
}
displayOptions: {
show: {
authentication: [
'apiKey',
],
},
},
},
{
name: 'todoistOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: [
'oAuth2',
],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'API Key',
value: 'apiKey',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'apiKey',
description: 'The resource to operate on.',
},
{
displayName: 'Resource',
name: 'resource',
@ -85,24 +120,29 @@ export class Todoist implements INodeType {
description: 'Create a new task',
},
{
name: 'Close by ID',
value: 'close_id',
description: 'Close a task by passing an ID',
name: 'Close',
value: 'close',
description: 'Close a task',
},
{
name: 'Close matching',
value: 'close_match',
description: 'Close a task by passing a regular expression',
name: 'Delete',
value: 'delete',
description: 'Delete a task',
},
{
name: 'Delete by ID',
value: 'delete_id',
description: 'Delete a task by passing an ID',
name: 'Get',
value: 'get',
description: 'Get a task',
},
{
name: 'Delete matching',
value: 'delete_match',
description: 'Delete a task by passing a regular expression',
name: 'Get All',
value: 'getAll',
description: 'Get all tasks',
},
{
name: 'Reopen',
value: 'reopen',
description: 'Reopen a task',
},
],
default: 'create',
@ -122,9 +162,7 @@ export class Todoist implements INodeType {
],
operation: [
'create',
'close_match',
'delete_match',
]
],
},
},
default: '',
@ -144,7 +182,7 @@ export class Todoist implements INodeType {
],
operation: [
'create',
]
],
},
},
default: [],
@ -165,7 +203,7 @@ export class Todoist implements INodeType {
],
operation: [
'create',
]
],
},
},
default: '',
@ -173,32 +211,27 @@ export class Todoist implements INodeType {
description: 'Task content',
},
{
displayName: 'ID',
name: 'id',
displayName: 'Task ID',
name: 'taskId',
type: 'string',
default: '',
required: true,
typeOptions: { rows: 1 },
displayOptions: {
show: { resource: ['task'], operation: ['close_id', 'delete_id'] }
show: {
resource: [
'task',
],
operation: [
'delete',
'close',
'get',
'reopen',
],
},
},
},
{
displayName: 'Expression to match',
name: 'expression',
type: 'string',
default: '',
required: true,
typeOptions: { rows: 1 },
displayOptions: {
show: {
resource: ['task'],
operation: ['close_match', 'delete_match']
}
}
},
{
displayName: 'Options',
displayName: 'Additional Fields',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
@ -210,22 +243,10 @@ export class Todoist implements INodeType {
],
operation: [
'create',
]
],
},
},
options: [
{
displayName: 'Priority',
name: 'priority',
type: 'number',
typeOptions: {
numberStepSize: 1,
maxValue: 4,
minValue: 1,
},
default: 1,
description: 'Task priority from 1 (normal) to 4 (urgent).',
},
{
displayName: 'Due Date Time',
name: 'dueDateTime',
@ -240,24 +261,131 @@ export class Todoist implements INodeType {
default: '',
description: 'Human defined task due date (ex.: “next Monday”, “Tomorrow”). Value is set using local (not UTC) time.',
},
]
}
]
{
displayName: 'Priority',
name: 'priority',
type: 'number',
typeOptions: {
numberStepSize: 1,
maxValue: 4,
minValue: 1,
},
default: 1,
description: 'Task priority from 1 (normal) to 4 (urgent).',
},
],
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'task',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'task',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: [
'task',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Filter',
name: 'filter',
type: 'string',
default: '',
description: 'Filter by any <a href="https://get.todoist.help/hc/en-us/articles/205248842">supported filter.</a>',
},
{
displayName: 'IDs',
name: 'ids',
type: 'string',
default: '',
description: 'A list of the task IDs to retrieve, this should be a comma separated list.',
},
{
displayName: 'Label ID',
name: 'labelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLabels',
},
default: {},
description: 'Filter tasks by label.',
},
{
displayName: 'Lang',
name: 'lang',
type: 'string',
default: '',
description: 'IETF language tag defining what language filter is written in, if differs from default English',
},
{
displayName: 'Project ID',
name: 'projectId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
default: '',
description: 'Filter tasks by project id.',
},
],
},
],
};
methods = {
loadOptions: {
// Get all the available projects to display them to user so that he can
// select them easily
async getProjects(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let projects;
try {
projects = await todoistApiRequest.call(this, '/projects', 'GET');
} catch (err) {
throw new Error(`Todoist Error: ${err}`);
}
const projects = await todoistApiRequest.call(this, 'GET', '/projects');
for (const project of projects) {
const projectName = project.name;
const projectId = project.id;
@ -275,12 +403,8 @@ export class Todoist implements INodeType {
// select them easily
async getLabels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let labels;
try {
labels = await todoistApiRequest.call(this, '/labels', 'GET');
} catch (err) {
throw new Error(`Todoist Error: ${err}`);
}
const labels = await todoistApiRequest.call(this, 'GET', '/labels');
for (const label of labels) {
const labelName = label.name;
const labelId = label.id;
@ -296,67 +420,113 @@ export class Todoist implements INodeType {
}
};
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
const resource = this.getNodeParameter('resource') as string;
const operation = this.getNodeParameter('operation') as string;
try {
return {
json: { result: await OPERATIONS[resource]?.[operation]?.bind(this)() }
};
} catch (err) {
return { json: { error: `Todoist Error: ${err.message}` } };
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
if (resource === 'task') {
if (operation === 'create') {
//https://developer.todoist.com/rest/v1/#create-a-new-task
const content = this.getNodeParameter('content', i) as string;
const projectId = this.getNodeParameter('project', i) as number;
const labels = this.getNodeParameter('labels', i) as number[];
const options = this.getNodeParameter('options', i) as IDataObject;
const body: IBodyCreateTask = {
content,
project_id: projectId,
priority: (options.priority!) ? parseInt(options.priority as string, 10) : 1,
};
if (options.dueDateTime) {
body.due_datetime = options.dueDateTime as string;
}
if (options.dueString) {
body.due_string = options.dueString as string;
}
if (labels !== undefined && labels.length !== 0) {
body.label_ids = labels;
}
responseData = await todoistApiRequest.call(this, 'POST', '/tasks', body);
}
if (operation === 'close') {
//https://developer.todoist.com/rest/v1/#close-a-task
const id = this.getNodeParameter('taskId', i) as string;
responseData = await todoistApiRequest.call(this, 'POST', `/tasks/${id}/close`);
responseData = { success: true };
}
if (operation === 'delete') {
//https://developer.todoist.com/rest/v1/#delete-a-task
const id = this.getNodeParameter('taskId', i) as string;
responseData = await todoistApiRequest.call(this, 'DELETE', `/tasks/${id}`);
responseData = { success: true };
}
if (operation === 'get') {
//https://developer.todoist.com/rest/v1/#get-an-active-task
const id = this.getNodeParameter('taskId', i) as string;
responseData = await todoistApiRequest.call(this, 'GET', `/tasks/${id}`);
}
if (operation === 'getAll') {
//https://developer.todoist.com/rest/v1/#get-active-tasks
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const filters = this.getNodeParameter('filters', i) as IDataObject;
if (filters.projectId) {
qs.project_id = filters.projectId as string;
}
if (filters.labelId) {
qs.label_id = filters.labelId as string;
}
if (filters.filter) {
qs.filter = filters.filter as string;
}
if (filters.lang) {
qs.lang = filters.lang as string;
}
if (filters.ids) {
qs.ids = filters.ids as string;
}
responseData = await todoistApiRequest.call(this, 'GET', '/tasks', {}, qs);
if (!returnAll) {
const limit = this.getNodeParameter('limit', i) as number;
responseData = responseData.splice(0, limit);
}
}
if (operation === 'reopen') {
//https://developer.todoist.com/rest/v1/#get-an-active-task
const id = this.getNodeParameter('taskId', i) as string;
responseData = await todoistApiRequest.call(this, 'POST', `/tasks/${id}/reopen`);
responseData = { success: true };
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}
const OPERATIONS: {
[key: string]: { [key: string]: (this: IExecuteSingleFunctions) => any };
} = {
task: {
create(this: IExecuteSingleFunctions) {
//https://developer.todoist.com/rest/v1/#create-a-new-task
const content = this.getNodeParameter('content') as string;
const projectId = this.getNodeParameter('project') as number;
const labels = this.getNodeParameter('labels') as number[];
const options = this.getNodeParameter('options') as IDataObject;
const body: IBodyCreateTask = {
content,
project_id: projectId,
priority: (options.priority!) ? parseInt(options.priority as string, 10) : 1,
};
if (options.dueDateTime) {
body.due_datetime = options.dueDateTime as string;
}
if (options.dueString) {
body.due_string = options.dueString as string;
}
if (labels !== undefined && labels.length !== 0) {
body.label_ids = labels;
}
return todoistApiRequest.call(this, '/tasks', 'POST', body);
},
close_id(this: IExecuteSingleFunctions) {
const id = this.getNodeParameter('id') as string;
return todoistApiRequest.call(this, `/tasks/${id}/close`, 'POST');
},
delete_id(this: IExecuteSingleFunctions) {
const id = this.getNodeParameter('id') as string;
return todoistApiRequest.call(this, `/tasks/${id}`, 'DELETE');
},
close_match(this) {
return filterAndExecuteForEachTask.call(this, t =>
todoistApiRequest.call(this, `/tasks/${t.id}/close`, 'POST')
);
},
delete_match(this) {
return filterAndExecuteForEachTask.call(this, t =>
todoistApiRequest.call(this, `/tasks/${t.id}`, 'DELETE')
);
}
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -1,4 +1,6 @@
import { INodeProperties } from "n8n-workflow";
import {
INodeProperties,
} from 'n8n-workflow';
export const attachmentOperations = [
// ----------------------------------

View file

@ -1,4 +1,6 @@
import { INodeProperties } from "n8n-workflow";
import {
INodeProperties,
} from 'n8n-workflow';
export const boardOperations = [
// ----------------------------------

View file

@ -0,0 +1,177 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const cardCommentOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'cardComment',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a comment on a card',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a comment from a card',
},
{
name: 'Update',
value: 'update',
description: 'Update a comment on a card',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const cardCommentFields = [
// ----------------------------------
// cardComment:create
// ----------------------------------
{
displayName: 'Card ID',
name: 'cardId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'cardComment',
],
},
},
description: 'The id of the card',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'cardComment',
],
},
},
description: 'Text of the comment',
},
// ----------------------------------
// cardComment:remove
// ----------------------------------
{
displayName: 'Card ID',
name: 'cardId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'cardComment',
],
},
},
description: 'The ID of the card.',
},
{
displayName: 'Comment ID',
name: 'commentId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'delete',
],
resource: [
'cardComment',
],
},
},
description: 'The ID of the comment to delete.',
},
// ----------------------------------
// cardComment:update
// ----------------------------------
{
displayName: 'Card ID',
name: 'cardId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'cardComment',
],
},
},
description: 'The ID of the card to update.',
},
{
displayName: 'Comment ID',
name: 'commentId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'cardComment',
],
},
},
description: 'The ID of the comment to delete.',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'update',
],
resource: [
'cardComment',
],
},
},
description: 'Text of the comment',
},
] as INodeProperties[];

View file

@ -1,4 +1,6 @@
import { INodeProperties } from "n8n-workflow";
import {
INodeProperties,
} from 'n8n-workflow';
export const cardOperations = [
// ----------------------------------

View file

@ -1,4 +1,6 @@
import { INodeProperties } from "n8n-workflow";
import {
INodeProperties,
} from 'n8n-workflow';
export const checklistOperations = [
// ----------------------------------

View file

@ -4,9 +4,13 @@ import {
ILoadOptionsFunctions,
} from 'n8n-core';
import { OptionsWithUri } from 'request';
import { IDataObject } from 'n8n-workflow';
import {
OptionsWithUri,
} from 'request';
import {
IDataObject,
} from 'n8n-workflow';
/**
* Make an API request to Trello

View file

@ -1,4 +1,6 @@
import { INodeProperties } from "n8n-workflow";
import {
INodeProperties,
} from 'n8n-workflow';
export const labelOperations = [
// ----------------------------------

View file

@ -1,42 +1,46 @@
import { INodeProperties } from "n8n-workflow";
import {
INodeProperties,
} from 'n8n-workflow';
export const listOperations = [
// ----------------------------------
// list
// ----------------------------------
{
displayName: "Operation",
name: "operation",
type: "options",
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: ["list"]
}
resource: [
'list',
],
},
},
options: [
{
name: "Archive",
value: "archive",
description: "Archive/Unarchive a list"
name: 'Archive',
value: 'archive',
description: 'Archive/Unarchive a list'
},
{
name: "Create",
value: "create",
description: "Create a new list"
name: 'Create',
value: 'create',
description: 'Create a new list'
},
{
name: "Get",
value: "get",
description: "Get the data of a list"
name: 'Get',
value: 'get',
description: 'Get the data of a list'
},
{
name: "Update",
value: "update",
description: "Update a list"
name: 'Update',
value: 'update',
description: 'Update a list'
}
],
default: "create",
description: "The operation to perform."
default: 'create',
description: 'The operation to perform.'
}
] as INodeProperties[];
@ -45,92 +49,112 @@ export const listFields = [
// list:archive
// ----------------------------------
{
displayName: "List ID",
name: "id",
type: "string",
default: "",
displayName: 'List ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: ["archive"],
resource: ["list"]
}
operation: [
'archive',
],
resource: [
'list',
],
},
},
description: "The ID of the list to archive or unarchive."
description: 'The ID of the list to archive or unarchive.'
},
{
displayName: "Archive",
name: "archive",
type: "boolean",
displayName: 'Archive',
name: 'archive',
type: 'boolean',
default: false,
displayOptions: {
show: {
operation: ["archive"],
resource: ["list"]
}
operation: [
'archive',
],
resource: [
'list',
],
},
},
description: "If the list should be archived or unarchived."
description: 'If the list should be archived or unarchived.'
},
// ----------------------------------
// list:create
// ----------------------------------
{
displayName: "Board ID",
name: "idBoard",
type: "string",
default: "",
displayName: 'Board ID',
name: 'idBoard',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: ["create"],
resource: ["list"]
}
operation: [
'create',
],
resource: [
'list',
],
},
},
description: "The ID of the board the list should be created in"
description: 'The ID of the board the list should be created in'
},
{
displayName: "Name",
name: "name",
type: "string",
default: "",
placeholder: "My list",
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'My list',
required: true,
displayOptions: {
show: {
operation: ["create"],
resource: ["list"]
}
operation: [
'create',
],
resource: [
'list',
],
},
},
description: "The name of the list"
description: 'The name of the list'
},
{
displayName: "Additional Fields",
name: "additionalFields",
type: "collection",
placeholder: "Add Field",
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ["create"],
resource: ["list"]
}
operation: [
'create',
],
resource: [
'list',
],
},
},
default: {},
options: [
{
displayName: "List Source",
name: "idListSource",
type: "string",
default: "",
description: "ID of the list to copy into the new list."
displayName: 'List Source',
name: 'idListSource',
type: 'string',
default: '',
description: 'ID of the list to copy into the new list.'
},
{
displayName: "Position",
name: "pos",
type: "string",
default: "bottom",
displayName: 'Position',
name: 'pos',
type: 'string',
default: 'bottom',
description:
"The position of the new list. top, bottom, or a positive float."
'The position of the new list. top, bottom, or a positive float.'
}
]
},
@ -139,39 +163,46 @@ export const listFields = [
// list:get
// ----------------------------------
{
displayName: "List ID",
name: "id",
type: "string",
default: "",
displayName: 'List ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: ["get"],
resource: ["list"]
}
operation: [
'get',
],
resource: [
'list',
],
},
},
description: "The ID of the list to get."
description: 'The ID of the list to get.'
},
{
displayName: "Additional Fields",
name: "additionalFields",
type: "collection",
placeholder: "Add Field",
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ["get"],
resource: ["list"]
}
operation: [
'get',
],
resource: [
'list',
],
},
},
default: {},
options: [
{
displayName: "Fields",
name: "fields",
type: "string",
default: "all",
description:
'Fields to return. Either "all" or a comma-separated list of fields.'
displayName: 'Fields',
name: 'fields',
type: 'string',
default: 'all',
description: 'Fields to return. Either "all" or a comma-separated list of fields.'
}
]
},
@ -180,67 +211,75 @@ export const listFields = [
// list:update
// ----------------------------------
{
displayName: "List ID",
name: "id",
type: "string",
default: "",
displayName: 'List ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: ["update"],
resource: ["list"]
}
operation: [
'update',
],
resource: [
'list',
],
},
},
description: "The ID of the list to update."
description: 'The ID of the list to update.'
},
{
displayName: "Update Fields",
name: "updateFields",
type: "collection",
placeholder: "Add Field",
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: ["update"],
resource: ["list"]
}
operation: [
'update',
],
resource: [
'list',
],
},
},
default: {},
options: [
{
displayName: "Board ID",
name: "idBoard",
type: "string",
default: "",
description: "ID of a board the list should be moved to."
displayName: 'Board ID',
name: 'idBoard',
type: 'string',
default: '',
description: 'ID of a board the list should be moved to.'
},
{
displayName: "Closed",
name: "closed",
type: "boolean",
displayName: 'Closed',
name: 'closed',
type: 'boolean',
default: false,
description: "Whether the list is closed."
description: 'Whether the list is closed.'
},
{
displayName: "Name",
name: "name",
type: "string",
default: "",
description: "New name of the list"
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'New name of the list'
},
{
displayName: "Position",
name: "pos",
type: "string",
default: "bottom",
displayName: 'Position',
name: 'pos',
type: 'string',
default: 'bottom',
description:
"The position of the list. top, bottom, or a positive float."
'The position of the list. top, bottom, or a positive float.'
},
{
displayName: "Subscribed",
name: "subscribed",
type: "boolean",
displayName: 'Subscribed',
name: 'subscribed',
type: 'boolean',
default: false,
description: "Whether the acting user is subscribed to the list."
description: 'Whether the acting user is subscribed to the list.'
}
]
}

View file

@ -1,4 +1,7 @@
import { IExecuteFunctions } from "n8n-core";
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
@ -6,13 +9,44 @@ import {
INodeType,
} from 'n8n-workflow';
import { apiRequest } from "./GenericFunctions";
import { attachmentOperations, attachmentFields } from './AttachmentDescription';
import { boardOperations, boardFields } from './BoardDescription';
import { cardOperations, cardFields } from './CardDescription';
import { checklistOperations, checklistFields } from './ChecklistDescription';
import { labelOperations, labelFields } from './LabelDescription';
import { listOperations, listFields } from './ListDescription';
import {
apiRequest,
} from './GenericFunctions';
import {
attachmentOperations,
attachmentFields,
} from './AttachmentDescription';
import {
boardOperations,
boardFields,
} from './BoardDescription';
import {
cardOperations,
cardFields,
} from './CardDescription';
import {
cardCommentOperations,
cardCommentFields,
} from './CardCommentDescription';
import {
checklistOperations,
checklistFields,
} from './ChecklistDescription';
import {
labelOperations,
labelFields,
} from './LabelDescription';
import {
listOperations,
listFields,
} from './ListDescription';
export class Trello implements INodeType {
description: INodeTypeDescription = {
@ -33,7 +67,7 @@ export class Trello implements INodeType {
{
name: 'trelloApi',
required: true,
}
},
],
properties: [
{
@ -53,6 +87,10 @@ export class Trello implements INodeType {
name: 'Card',
value: 'card',
},
{
name: 'Card Comment',
value: 'cardComment',
},
{
name: 'Checklist',
value: 'checklist',
@ -76,6 +114,7 @@ export class Trello implements INodeType {
...attachmentOperations,
...boardOperations,
...cardOperations,
...cardCommentOperations,
...checklistOperations,
...labelOperations,
...listOperations,
@ -86,15 +125,14 @@ export class Trello implements INodeType {
...attachmentFields,
...boardFields,
...cardFields,
...cardCommentFields,
...checklistFields,
...labelFields,
...listFields
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
@ -236,6 +274,54 @@ export class Trello implements INodeType {
throw new Error(`The operation "${operation}" is not known!`);
}
} else if (resource === 'cardComment') {
if (operation === 'create') {
// ----------------------------------
// create
// ----------------------------------
const cardId = this.getNodeParameter('cardId', i) as string;
qs.text = this.getNodeParameter('text', i) as string;
requestMethod = 'POST';
endpoint = `cards/${cardId}/actions/comments`;
} else if (operation === 'delete') {
// ----------------------------------
// delete
// ----------------------------------
requestMethod = 'DELETE';
const cardId = this.getNodeParameter('cardId', i) as string;
const commentId = this.getNodeParameter('commentId', i) as string;
endpoint = `/cards/${cardId}/actions/${commentId}/comments`;
} else if (operation === 'update') {
// ----------------------------------
// update
// ----------------------------------
requestMethod = 'PUT';
const cardId = this.getNodeParameter('cardId', i) as string;
const commentId = this.getNodeParameter('commentId', i) as string;
qs.text = this.getNodeParameter('text', i) as string;
endpoint = `cards/${cardId}/actions/${commentId}/comments`;
} else {
throw new Error(`The operation "${operation}" is not known!`);
}
} else if (resource === 'list') {
if (operation === 'archive') {

View file

@ -23,7 +23,10 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions
body,
//@ts-ignore
uri,
json: true
json: true,
qsStringifyOptions: {
arrayFormat: 'brackets',
},
};
options = Object.assign({}, options, option);
@ -56,6 +59,7 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions
return await this.helpers.requestOAuth2!.call(this, 'zendeskOAuth2Api', options);
}
} catch(err) {
let errorMessage = err.message;
if (err.response && err.response.body && err.response.body.error) {
errorMessage = err.response.body.error;

View file

@ -0,0 +1,692 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const userOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'user',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a user',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a user',
},
{
name: 'Get',
value: 'get',
description: 'Get a user',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all users',
},
{
name: 'Update',
value: 'update',
description: 'Update a user',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const userFields = [
/* -------------------------------------------------------------------------- */
/* user:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
],
},
},
required: true,
description: `The user's name`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Alias',
name: 'alis',
type: 'string',
default: '',
description: `An alias displayed to end users`,
},
{
displayName: 'Custom Role ID',
name: 'custom_role_id',
type: 'number',
default: 0,
description: `A custom role if the user is an agent on the Enterprise plan`,
},
{
displayName: 'Details',
name: 'details',
type: 'string',
default: '',
description: 'Any details you want to store about the user, such as an address',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: `The user's primary email address`,
},
{
displayName: 'External ID',
name: 'externalId',
type: 'string',
default: '',
description: 'A unique identifier from another system',
},
{
displayName: 'Locale ID',
name: 'locale',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLocales',
},
default: '',
description: `The user's locale.`,
},
{
displayName: 'Moderator',
name: 'moderator',
type: 'boolean',
default: false,
description: 'Designates whether the user has forum moderation capabilities',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
default: '',
description: 'Any notes you want to store about the user',
},
{
displayName: 'Only Private Comments',
name: 'only_private_comments',
type: 'boolean',
default: false,
description: `true if the user can only create private comments`,
},
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'number',
default: 0,
description: `The id of the user's organization. If the user has more than one organization memberships, the id of the user's default organization`,
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
description: `The user's primary phone number.`,
},
{
displayName: 'Report CSV',
name: 'report_csv',
type: 'boolean',
default: false,
description: `Whether or not the user can access the CSV report on the Search tab of the Reporting page in the Support admin interface.`,
},
{
displayName: 'Restricted Agent',
name: 'restricted_agent',
type: 'boolean',
default: false,
description: `If the agent has any restrictions; false for admins and unrestricted agents, true for other agents`,
},
{
displayName: 'Role',
name: 'role',
type: 'options',
options: [
{
name: 'End User',
value: 'end-user',
},
{
name: 'Agent',
value: 'agent',
},
{
name: 'Admin',
value: 'admin',
},
],
default: '',
description: `The user's role`,
},
{
displayName: 'Signature',
name: 'signature',
type: 'string',
default: '',
description: `The user's signature. Only agents and admins can have signatures`,
},
{
displayName: 'Suspended',
name: 'suspended',
type: 'boolean',
default: false,
description: `If the agent is suspended. Tickets from suspended users are also suspended, and these users cannot sign in to the end user portal`,
},
{
displayName: 'Tags',
name: 'tags',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: [],
description: 'The array of tags applied to this user',
},
{
displayName: 'Ticket Restriction',
name: 'ticket_restriction',
type: 'options',
options: [
{
name: 'Organization',
value: 'organization',
},
{
name: 'Groups',
value: 'groups',
},
{
name: 'Assigned',
value: 'assigned',
},
{
name: 'Requested',
value: 'requested',
},
],
default: '',
description: `Specifies which tickets the user has access to`,
},
{
displayName: 'Timezone',
name: 'time_zone',
type: 'string',
default: '',
description: `The user's time zone.`,
},
{
displayName: 'User Fields',
name: 'userFieldsUi',
placeholder: 'Add User Field',
description: `Values of custom fields in the user's profile.`,
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'userFieldValues',
displayName: 'Field',
values: [
{
displayName: 'Field',
name: 'field',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUserFields',
},
default: '',
description: 'Name of the field to sort on.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the field.',
},
],
},
],
},
{
displayName: 'Verified',
name: 'verified',
type: 'boolean',
default: false,
description: `The user's primary identity is verified or not`,
},
],
},
/* -------------------------------------------------------------------------- */
/* user:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'update',
],
},
},
description: 'User ID',
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'update',
],
},
},
options: [
{
displayName: 'Alias',
name: 'alis',
type: 'string',
default: '',
description: `An alias displayed to end users`,
},
{
displayName: 'Custom Role ID',
name: 'custom_role_id',
type: 'number',
default: 0,
description: `A custom role if the user is an agent on the Enterprise plan`,
},
{
displayName: 'Details',
name: 'details',
type: 'string',
default: '',
description: 'Any details you want to store about the user, such as an address',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: `The user's primary email address`,
},
{
displayName: 'External ID',
name: 'externalId',
type: 'string',
default: '',
description: 'A unique identifier from another system',
},
{
displayName: 'Locale ID',
name: 'locale',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLocales',
},
default: '',
description: `The user's locale.`,
},
{
displayName: 'Moderator',
name: 'moderator',
type: 'boolean',
default: false,
description: 'Designates whether the user has forum moderation capabilities',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: `The user's name`,
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
default: '',
description: 'Any notes you want to store about the user',
},
{
displayName: 'Only Private Comments',
name: 'only_private_comments',
type: 'boolean',
default: false,
description: `true if the user can only create private comments`,
},
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'number',
default: 0,
description: `The id of the user's organization. If the user has more than one organization memberships, the id of the user's default organization`,
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
description: `The user's primary phone number.`,
},
{
displayName: 'Report CSV',
name: 'report_csv',
type: 'boolean',
default: false,
description: `Whether or not the user can access the CSV report on the Search tab of the Reporting page in the Support admin interface.`,
},
{
displayName: 'Restricted Agent',
name: 'restricted_agent',
type: 'boolean',
default: false,
description: `If the agent has any restrictions; false for admins and unrestricted agents, true for other agents`,
},
{
displayName: 'Role',
name: 'role',
type: 'options',
options: [
{
name: 'End User',
value: 'end-user',
},
{
name: 'Agent',
value: 'agent',
},
{
name: 'Admin',
value: 'admin',
},
],
default: '',
description: `The user's role`,
},
{
displayName: 'Signature',
name: 'signature',
type: 'string',
default: '',
description: `The user's signature. Only agents and admins can have signatures`,
},
{
displayName: 'Suspended',
name: 'suspended',
type: 'boolean',
default: false,
description: `If the agent is suspended. Tickets from suspended users are also suspended, and these users cannot sign in to the end user portal`,
},
{
displayName: 'Tags',
name: 'tags',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: [],
description: 'The array of tags applied to this user',
},
{
displayName: 'Ticket Restriction',
name: 'ticket_restriction',
type: 'options',
options: [
{
name: 'Organization',
value: 'organization',
},
{
name: 'Groups',
value: 'groups',
},
{
name: 'Assigned',
value: 'assigned',
},
{
name: 'Requested',
value: 'requested',
},
],
default: '',
description: `Specifies which tickets the user has access to`,
},
{
displayName: 'Timezone',
name: 'time_zone',
type: 'string',
default: '',
description: `The user's time zone.`,
},
{
displayName: 'User Fields',
name: 'userFieldsUi',
placeholder: 'Add User Field',
description: `Values of custom fields in the user's profile.`,
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'userFieldValues',
displayName: 'Field',
values: [
{
displayName: 'Field',
name: 'field',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUserFields',
},
default: '',
description: 'Name of the field to sort on.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the field.',
},
],
},
],
},
{
displayName: 'Verified',
name: 'verified',
type: 'boolean',
default: false,
description: `The user's primary identity is verified or not`,
},
],
},
/* -------------------------------------------------------------------------- */
/* user:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'get',
],
},
},
description: 'User ID',
},
/* -------------------------------------------------------------------------- */
/* 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: 100,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Roles',
name: 'role',
type: 'multiOptions',
options: [
{
name: 'End User',
value: 'end-user',
},
{
name: 'Agent',
value: 'agent',
},
{
name: 'Admin',
value: 'admin',
},
],
default: [],
},
],
},
/* -------------------------------------------------------------------------- */
/* user:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'user',
],
operation: [
'delete',
],
},
},
description: 'User ID',
},
] as INodeProperties[];

View file

@ -27,11 +27,15 @@ import {
ticketFieldOperations
} from './TicketFieldDescription';
import {
userFields,
userOperations
} from './UserDescription';
import {
ITicket,
IComment,
} from './TicketInterface';
import { response } from 'express';
export class Zendesk implements INodeType {
description: INodeTypeDescription = {
@ -105,6 +109,11 @@ export class Zendesk implements INodeType {
value: 'ticketField',
description: 'Manage system and custom ticket fields',
},
{
name: 'User',
value: 'user',
description: 'Manage users',
},
],
default: 'ticket',
description: 'Resource to consume.',
@ -112,9 +121,12 @@ export class Zendesk implements INodeType {
// TICKET
...ticketOperations,
...ticketFields,
// TICKET FIELDS
// TICKET FIELD
...ticketFieldOperations,
...ticketFieldFields,
// USER
...userOperations,
...userFields,
],
};
@ -177,6 +189,38 @@ export class Zendesk implements INodeType {
}
return returnData;
},
// Get all the locales to display them to user so that he can
// select them easily
async getLocales(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const locales = await zendeskApiRequestAllItems.call(this, 'locales', 'GET', '/locales');
for (const locale of locales) {
const localeName = `${locale.locale} - ${locale.name}`;
const localeId = locale.locale;
returnData.push({
name: localeName,
value: localeId,
});
}
return returnData;
},
// Get all the user fields to display them to user so that he can
// select them easily
async getUserFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const fields = await zendeskApiRequestAllItems.call(this, 'user_fields', 'GET', '/user_fields');
for (const field of fields) {
const fieldName = field.title;
const fieldId = field.key;
returnData.push({
name: fieldName,
value: fieldId,
});
}
return returnData;
},
}
};
@ -359,6 +403,87 @@ export class Zendesk implements INodeType {
}
}
}
//https://developer.zendesk.com/rest_api/docs/support/users
if (resource === 'user') {
//https://developer.zendesk.com/rest_api/docs/support/users#create-user
if (operation === 'create') {
const name = this.getNodeParameter('name', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {
name,
};
Object.assign(body, additionalFields);
if (body.userFieldsUi) {
const userFields = (body.userFieldsUi as IDataObject).userFieldValues as IDataObject[];
if (userFields) {
body.user_fields = {};
for (const userField of userFields) {
//@ts-ignore
body.user_fields[userField.field] = userField.value;
}
delete body.userFieldsUi;
}
}
responseData = await zendeskApiRequest.call(this, 'POST', '/users', { user: body });
responseData = responseData.user;
}
//https://developer.zendesk.com/rest_api/docs/support/tickets#update-ticket
if (operation === 'update') {
const userId = this.getNodeParameter('id', i) as string;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const body: IDataObject = {};
Object.assign(body, updateFields);
if (body.userFieldsUi) {
const userFields = (body.userFieldsUi as IDataObject).userFieldValues as IDataObject[];
if (userFields) {
body.user_fields = {};
for (const userField of userFields) {
//@ts-ignore
body.user_fields[userField.field] = userField.value;
}
delete body.userFieldsUi;
}
}
responseData = await zendeskApiRequest.call(this, 'PUT', `/users/${userId}`, { user: body });
responseData = responseData.user;
}
//https://developer.zendesk.com/rest_api/docs/support/users#show-user
if (operation === 'get') {
const userId = this.getNodeParameter('id', i) as string;
responseData = await zendeskApiRequest.call(this, 'GET', `/users/${userId}`, {});
responseData = responseData.user;
}
//https://developer.zendesk.com/rest_api/docs/support/users#list-users
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const options = this.getNodeParameter('filters', i) as IDataObject;
Object.assign(qs, options);
if (returnAll) {
responseData = await zendeskApiRequestAllItems.call(this, 'users', 'GET', `/users`, {}, qs);
} else {
const limit = this.getNodeParameter('limit', i) as number;
qs.per_page = limit;
responseData = await zendeskApiRequest.call(this, 'GET', `/users`, {}, qs);
responseData = responseData.users;
}
}
//https://developer.zendesk.com/rest_api/docs/support/users#delete-user
if (operation === 'delete') {
const userId = this.getNodeParameter('id', i) as string;
responseData = await zendeskApiRequest.call(this, 'DELETE', `/users/${userId}`, {});
responseData = responseData.user;
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {

View file

@ -375,14 +375,14 @@ export class ZendeskTrigger implements INodeType {
displayName: 'All',
values: [
...conditionFields,
]
],
},
{
name: 'any',
displayName: 'Any',
values: [
...conditionFields,
]
],
},
],
},
@ -435,17 +435,74 @@ export class ZendeskTrigger implements INodeType {
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId === undefined) {
const conditions = this.getNodeParameter('conditions') as IDataObject;
const conditionsAll = conditions.all as [IDataObject];
let endpoint = '';
const aux: IDataObject = {};
const resultAll = [], resultAny = [];
if (conditionsAll) {
for (const conditionAll of conditionsAll) {
aux.field = conditionAll.field;
aux.operator = conditionAll.operation;
if (conditionAll.operation !== 'changed'
&& conditionAll.operation !== 'not_changed') {
aux.value = conditionAll.value;
} else {
aux.value = null;
}
resultAll.push(aux);
}
}
const conditionsAny = conditions.any as [IDataObject];
if (conditionsAny) {
for (const conditionAny of conditionsAny) {
aux.field = conditionAny.field;
aux.operator = conditionAny.operation;
if (conditionAny.operation !== 'changed'
&& conditionAny.operation !== 'not_changed') {
aux.value = conditionAny.value;
} else {
aux.value = null;
}
resultAny.push(aux);
}
}
// check if there is a target already created
endpoint = `/targets`;
const targets = await zendeskApiRequestAllItems.call(this, 'targets', 'GET', endpoint);
for (const target of targets) {
if (target.target_url === webhookUrl) {
webhookData.targetId = target.id.toString();
break;
}
}
// no target was found
if (webhookData.targetId === undefined) {
return false;
}
const endpoint = `/triggers/${webhookData.webhookId}`;
try {
await zendeskApiRequest.call(this, 'GET', endpoint);
} catch (e) {
return false;
endpoint = `/triggers/active`;
const triggers = await zendeskApiRequestAllItems.call(this, 'triggers', 'GET', endpoint);
for (const trigger of triggers) {
const toDeleteTriggers = [];
// this trigger belong to the current target
if (trigger.actions[0].value[0].toString() === webhookData.targetId?.toString()) {
toDeleteTriggers.push(trigger.id);
}
// delete all trigger attach to this target;
if (toDeleteTriggers.length !== 0) {
await zendeskApiRequest.call(this, 'DELETE', '/triggers/destroy_many', {}, { ids: toDeleteTriggers.join(',') } );
}
}
return true;
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
@ -509,8 +566,8 @@ export class ZendeskTrigger implements INodeType {
{
field: 'notification_target',
value: [],
}
]
},
],
},
};
const bodyTarget: IDataObject = {
@ -523,9 +580,21 @@ export class ZendeskTrigger implements INodeType {
content_type: 'application/json',
},
};
const { target } = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget);
let target: IDataObject = {};
// if target id exists but trigger does not then reuse the target
// and create the trigger else create both
if (webhookData.targetId !== undefined) {
target.id = webhookData.targetId;
} else {
target = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget);
target = target.target as IDataObject;
}
// @ts-ignore
bodyTrigger.trigger.actions[0].value = [target.id, JSON.stringify(message)];
//@ts-ignore
const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger);
webhookData.webhookId = trigger.id;
webhookData.targetId = target.id;

View file

@ -47,6 +47,8 @@
"dist/credentials/ClockifyApi.credentials.js",
"dist/credentials/CockpitApi.credentials.js",
"dist/credentials/CodaApi.credentials.js",
"dist/credentials/ContentfulApi.credentials.js",
"dist/credentials/ConvertKitApi.credentials.js",
"dist/credentials/CopperApi.credentials.js",
"dist/credentials/CalendlyApi.credentials.js",
"dist/credentials/CustomerIoApi.credentials.js",
@ -120,6 +122,7 @@
"dist/credentials/OAuth1Api.credentials.js",
"dist/credentials/OAuth2Api.credentials.js",
"dist/credentials/OpenWeatherMapApi.credentials.js",
"dist/credentials/PaddleApi.credentials.js",
"dist/credentials/PagerDutyApi.credentials.js",
"dist/credentials/PagerDutyOAuth2Api.credentials.js",
"dist/credentials/PayPalApi.credentials.js",
@ -148,6 +151,7 @@
"dist/credentials/SurveyMonkeyOAuth2Api.credentials.js",
"dist/credentials/TelegramApi.credentials.js",
"dist/credentials/TodoistApi.credentials.js",
"dist/credentials/TodoistOAuth2Api.credentials.js",
"dist/credentials/TravisCiApi.credentials.js",
"dist/credentials/TrelloApi.credentials.js",
"dist/credentials/TwilioApi.credentials.js",
@ -204,6 +208,9 @@
"dist/nodes/Clockify/ClockifyTrigger.node.js",
"dist/nodes/Cockpit/Cockpit.node.js",
"dist/nodes/Coda/Coda.node.js",
"dist/nodes/Contentful/Contentful.node.js",
"dist/nodes/ConvertKit/ConvertKit.node.js",
"dist/nodes/ConvertKit/ConvertKitTrigger.node.js",
"dist/nodes/Copper/CopperTrigger.node.js",
"dist/nodes/CrateDb/CrateDb.node.js",
"dist/nodes/Cron.node.js",
@ -287,6 +294,7 @@
"dist/nodes/NextCloud/NextCloud.node.js",
"dist/nodes/NoOp.node.js",
"dist/nodes/OpenWeatherMap.node.js",
"dist/nodes/Paddle/Paddle.node.js",
"dist/nodes/PagerDuty/PagerDuty.node.js",
"dist/nodes/PayPal/PayPal.node.js",
"dist/nodes/PayPal/PayPalTrigger.node.js",