Merge branch 'master' of github.com:n8n-io/n8n into unify-execution-id

This commit is contained in:
Omar Ajoue 2020-12-29 19:48:03 +01:00
commit d433661ae9
133 changed files with 12604 additions and 1282 deletions

View file

@ -0,0 +1,38 @@
name: Docker Nightly Image CI
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: ./docker/images/n8n-custom/Dockerfile
platforms: linux/amd64
push: true
tags: n8nio/n8n:nightly

View file

@ -26,8 +26,9 @@ jobs:
- name: Run Buildx (push image)
if: success()
run: |
docker buildx build --platform linux/arm/v7 --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-rpi --output type=image,push=true docker/images/n8n-rpi
- name: Tag Docker image with latest
run: docker tag n8nio/n8n:${{steps.vars.outputs.tag}}-rpi n8nio/n8n:latest-rpi
- name: Push docker images of latest
run: docker push n8nio/n8n:latest-rpi
docker buildx build \
--platform linux/arm/v7 \
--build-arg N8N_VERSION=${{steps.vars.outputs.tag}} \
-t ${{ secrets.DOCKER_USERNAME }}/n8n:${{steps.vars.outputs.tag}}-rpi \
-t ${{ secrets.DOCKER_USERNAME }}/n8n:latest-rpi \
--output type=image,push=true docker/images/n8n-rpi

View file

@ -28,7 +28,7 @@ jobs:
- name: Push docker images of latest
run: docker push n8nio/n8n:latest
- name: Build the Docker image of version (Ubuntu)
run: docker build --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-ubuntu docker/images/n8n-ubuntu
- name: Push Docker image of version (Ubuntu)
run: docker push n8nio/n8n:${{steps.vars.outputs.tag}}-ubuntu
- name: Build the Docker image of version (Debian)
run: docker build --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-debian docker/images/n8n-debian
- name: Push Docker image of version (Debian)
run: docker push n8nio/n8n:${{steps.vars.outputs.tag}}-debian

View file

@ -2,7 +2,7 @@
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png)
n8n is an extendable workflow automation tool. With a [fair-code](http://faircode.io) distribution model, n8n will always have visible source code, be available to self-host, and allow you to add your own custom functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect anything to everything.
n8n is an extendable workflow automation tool. With a [fair-code](http://faircode.io) distribution model, n8n will always have visible source code, be available to self-host, and allow you to add your own custom functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect anything to everything.
<a href="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png"><img src="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png" width="550" alt="n8n.io - Screenshot"></a>
@ -16,7 +16,7 @@ received or lost a star.
## Available integrations
n8n has 170+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
n8n has 200+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
## Documentation
@ -91,6 +91,6 @@ Have you found a bug :bug: ? Or maybe you have a nice feature :sparkles: to cont
## License
n8n is [fair-code](http://faircode.io) licensed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md).
n8n is [fair-code](http://faircode.io) distributed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) license.
Additional information about license can be found in the [FAQ](https://docs.n8n.io/#/faq?id=license).

View file

@ -1,6 +1,6 @@
## n8n - Ubuntu Docker Image
## n8n - Debian Docker Image
Dockerfile to build n8n with Ubuntu.
Dockerfile to build n8n with Debian.
For information about how to run n8n with Docker check the generic
[Docker-Readme](https://github.com/n8n-io/n8n/tree/master/docker/images/n8n/README.md)
@ -10,12 +10,12 @@ For information about how to run n8n with Docker check the generic
docker build --build-arg N8N_VERSION=<VERSION> -t n8nio/n8n:<VERSION> .
# For example:
docker build --build-arg N8N_VERSION=0.43.0 -t n8nio/n8n:0.43.0-ubuntu .
docker build --build-arg N8N_VERSION=0.43.0 -t n8nio/n8n:0.43.0-debian .
```
```
docker run -it --rm \
--name n8n \
-p 5678:5678 \
n8nio/n8n:0.43.0-ubuntu
n8nio/n8n:0.43.0-debian
```

View file

@ -17,5 +17,6 @@ docker build --build-arg N8N_VERSION=0.43.0 -t n8nio/n8n:0.43.0-rpi .
docker run -it --rm \
--name n8n \
-p 5678:5678 \
-v ~/.n8n:/home/node/.n8n \
n8nio/n8n:0.70.0-rpi
```

View file

@ -2,7 +2,7 @@
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png)
n8n is a free and open [fair-code](http://faircode.io) licensed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools.
n8n is a free and open [fair-code](http://faircode.io) distributed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools.
<a href="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png"><img src="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png" width="550" alt="n8n.io - Screenshot"></a>
@ -33,7 +33,7 @@ Slack notification every time a Github repository received or lost a star.
## Available integrations
n8n has 100+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
n8n has 200+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
## Documentation
@ -305,6 +305,6 @@ Before you upgrade to the latest version make sure to check here if there are an
## License
n8n is [fair-code](http://faircode.io) licensed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md)
n8n is [fair-code](http://faircode.io) distributed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) license
Additional information about license can be found in the [FAQ](https://docs.n8n.io/#/faq?id=license)

View file

@ -6,6 +6,8 @@ if [ -d /root/.n8n ] ; then
ln -s /root/.n8n /home/node/
fi
chown -R node /home/node
if [ "$#" -gt 0 ]; then
# Got started with arguments
exec su-exec node "$@"

View file

@ -2,7 +2,7 @@
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png)
n8n is a free and open [fair-code](http://faircode.io) licensed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools.
n8n is a free and open [fair-code](http://faircode.io) distributed node based Workflow Automation Tool. It can be self-hosted, easily extended, and so also used with internal tools.
<a href="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png"><img src="https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-screenshot.png" width="550" alt="n8n.io - Screenshot"></a>
@ -32,7 +32,7 @@ Slack notification every time a Github repository received or lost a star.
## Available integrations
n8n has 100+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
n8n has 200+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes)
## Documentation
@ -100,7 +100,7 @@ Before you upgrade to the latest version make sure to check here if there are an
## License
n8n is [fair-code](http://faircode.io) licensed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md)
n8n is [fair-code](http://faircode.io) distributed under [**Apache 2.0 with Commons Clause**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) license
Additional information about license can be found in the [FAQ](https://docs.n8n.io/#/faq?id=license)

View file

@ -121,10 +121,16 @@ export class Start extends Command {
const { flags } = this.parse(Start);
// Wrap that the process does not close but we can still use async
(async () => {
await (async () => {
try {
// Start directly with the init of the database to improve startup time
const startDbInitPromise = Db.init();
const startDbInitPromise = Db.init().catch(error => {
console.error(`There was an error initializing DB: ${error.message}`);
processExistCode = 1;
// @ts-ignore
process.emit('SIGINT');
});
// Make sure the settings exist
const userSettings = await UserSettings.prepareUserSettings();

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.96.0",
"version": "0.99.1",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -103,9 +103,9 @@
"lodash.get": "^4.4.2",
"mongodb": "^3.5.5",
"mysql2": "~2.1.0",
"n8n-core": "~0.54.0",
"n8n-editor-ui": "~0.66.0",
"n8n-nodes-base": "~0.91.0",
"n8n-core": "~0.56.0",
"n8n-editor-ui": "~0.68.1",
"n8n-nodes-base": "~0.94.0",
"n8n-workflow": "~0.47.0",
"oauth-1.0a": "^2.2.6",
"open": "^7.0.0",

View file

@ -64,7 +64,11 @@ export function sendSuccessResponse(res: Response, data: any, raw?: boolean, res
}
if (raw === true) {
res.json(data);
if (typeof data === 'string') {
res.send(data);
} else {
res.json(data);
}
} else {
res.json({
data,

View file

@ -44,9 +44,11 @@ import * as config from '../config';
import { LessThanOrEqual } from "typeorm";
const ERROR_TRIGGER_TYPE = config.get('nodes.errorTriggerType') as string;
/**
* Checks if there was an error and if errorWorkflow is defined. If so it collects
* Checks if there was an error and if errorWorkflow or a trigger is defined. If so it collects
* all the data and executes it
*
* @param {IWorkflowBase} workflowData The workflow which got executed
@ -55,14 +57,14 @@ import { LessThanOrEqual } from "typeorm";
* @param {string} [executionId] The id the execution got saved as
*/
function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mode: WorkflowExecuteMode, executionId?: string, retryOf?: string): void {
// Check if there was an error and if so if an errorWorkflow is set
// Check if there was an error and if so if an errorWorkflow or a trigger is set
let pastExecutionUrl: string | undefined = undefined;
if (executionId !== undefined) {
pastExecutionUrl = `${WebhookHelpers.getWebhookBaseUrl()}execution/${executionId}`;
}
if (fullRunData.data.resultData.error !== undefined && workflowData.settings !== undefined && workflowData.settings.errorWorkflow) {
if (fullRunData.data.resultData.error !== undefined) {
const workflowErrorData = {
execution: {
id: executionId,
@ -77,8 +79,16 @@ function executeErrorWorkflow(workflowData: IWorkflowBase, fullRunData: IRun, mo
name: workflowData.name,
},
};
// Run the error workflow
WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData);
// To avoid an infinite loop do not run the error workflow again if the error-workflow itself failed and it is its own error-workflow.
if (workflowData.settings !== undefined && workflowData.settings.errorWorkflow && !(mode === 'error' && workflowData.id && workflowData.settings.errorWorkflow.toString() === workflowData.id.toString())) {
// If a specific error workflow is set run only that one
WorkflowHelpers.executeErrorWorkflow(workflowData.settings.errorWorkflow as string, workflowErrorData);
} else if (mode !== 'error' && workflowData.id !== undefined && workflowData.nodes.some((node) => node.type === ERROR_TRIGGER_TYPE)) {
// If the workflow contains
WorkflowHelpers.executeErrorWorkflow(workflowData.id.toString(), workflowErrorData);
}
}
}

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.54.0",
"version": "0.56.0",
"description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",

View file

@ -172,6 +172,10 @@ export function requestOAuth2(this: IAllExecuteFunctions, credentialsType: strin
client_secret: credentials.clientSecret as string,
};
tokenRefreshOptions.body = body;
// Override authorization property so the credentails are not included in it
tokenRefreshOptions.headers = {
Authorization: '',
};
}
const newToken = await token.refresh(tokenRefreshOptions);
@ -241,6 +245,15 @@ export function requestOAuth1(this: IAllExecuteFunctions, credentialsType: strin
//@ts-ignore
requestOptions.data = { ...requestOptions.qs, ...requestOptions.form };
// Fixes issue that OAuth1 library only works with "url" property and not with "uri"
// @ts-ignore
if (requestOptions.uri && !requestOptions.url) {
// @ts-ignore
requestOptions.url = requestOptions.uri;
// @ts-ignore
delete requestOptions.uri;
}
//@ts-ignore
requestOptions.headers = oauth.toHeader(oauth.authorize(requestOptions, token));

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.66.0",
"version": "0.68.1",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -61,7 +61,7 @@
"flatted": "^2.0.0",
"jquery": "^3.4.1",
"jshint": "^2.9.7",
"jsplumb": "^2.10.0",
"jsplumb": "2.15.4",
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
@ -79,7 +79,7 @@
"uuid": "^8.1.0",
"vue": "^2.6.9",
"vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0",
"vue-json-tree": "^0.4.1",
"vue-json-pretty": "^1.7.1",
"vue-prism-editor": "^0.3.0",
"vue-router": "^3.0.6",
"vue-template-compiler": "^2.5.17",

View file

@ -11,10 +11,10 @@
</el-button>
<div class="binary-data-window-wrapper">
<div v-if="binaryData === null">
<div v-if="!binaryData">
Data to display did not get found
</div>
<embed :src="'data:' + binaryData.mimeType + ';base64,' + binaryData.data" class="binary-data" :class="embedClass"/>
<embed v-else :src="'data:' + binaryData.mimeType + ';base64,' + binaryData.data" class="binary-data" :class="embedClass"/>
</div>
</div>

View file

@ -10,8 +10,12 @@
<div v-if="multipleValues === true">
<div v-for="(value, index) in values[property.name]" :key="property.name + index" class="parameter-item">
<div class="parameter-item-wrapper">
<div class="delete-option" title="Delete" v-if="!isReadOnly">
<div class="delete-option" v-if="!isReadOnly">
<font-awesome-icon icon="trash" class="reset-icon clickable" title="Delete Item" @click="deleteOption(property.name, index)" />
<div v-if="sortable">
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" title="Move up" @click="moveOptionUp(property.name, index)" />
<font-awesome-icon v-if="index !== (values[property.name].length -1)" icon="angle-down" class="clickable" title="Move down" @click="moveOptionDown(property.name, index)" />
</div>
</div>
<parameter-input-list :parameters="property.values" :nodeValues="nodeValues" :path="getPropertyPath(property.name, index)" :hideDelete="true" @valueChanged="valueChanged" />
</div>
@ -19,7 +23,7 @@
</div>
<div v-else class="parameter-item">
<div class="parameter-item-wrapper">
<div class="delete-option" title="Delete" v-if="!isReadOnly">
<div class="delete-option" v-if="!isReadOnly">
<font-awesome-icon icon="trash" class="reset-icon clickable" title="Delete Item" @click="deleteOption(property.name)" />
</div>
<parameter-input-list :parameters="property.values" :nodeValues="nodeValues" :path="getPropertyPath(property.name)" class="parameter-item" @valueChanged="valueChanged" :hideDelete="true" />
@ -111,6 +115,9 @@ export default mixins(genericHelpers)
}
return [];
},
sortable (): string {
return this.parameter.typeOptions && this.parameter.typeOptions.sortable;
},
},
methods: {
deleteOption (optionName: string, index?: number) {
@ -133,6 +140,26 @@ export default mixins(genericHelpers)
return undefined;
},
moveOptionDown (optionName: string, index: number) {
this.values[optionName].splice(index + 1, 0, this.values[optionName].splice(index, 1)[0]);
const parameterData = {
name: this.getPropertyPath(optionName),
value: this.values[optionName],
};
this.$emit('valueChanged', parameterData);
},
moveOptionUp (optionName: string, index: number) {
this.values[optionName].splice(index - 1, 0, this.values[optionName].splice(index, 1)[0]);
const parameterData = {
name: this.getPropertyPath(optionName),
value: this.values[optionName],
};
this.$emit('valueChanged', parameterData);
},
optionSelected (optionName: string) {
const option = this.getOptionProperties(optionName);
if (option === undefined) {

View file

@ -10,8 +10,12 @@
</div>
<div v-for="(value, index) in values" :key="index" class="duplicate-parameter-item" :class="parameter.type">
<div class="delete-item clickable" v-if="!isReadOnly" title="Delete Item" @click="deleteItem(index)">
<font-awesome-icon icon="trash" />
<div class="delete-item clickable" v-if="!isReadOnly">
<font-awesome-icon icon="trash" title="Delete Item" @click="deleteItem(index)" />
<div v-if="sortable">
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" title="Move up" @click="moveOptionUp(index)" />
<font-awesome-icon v-if="index !== (values.length -1)" icon="angle-down" class="clickable" title="Move down" @click="moveOptionDown(index)" />
</div>
</div>
<div v-if="parameter.type === 'collection'">
<collection-parameter :parameter="parameter" :values="value" :nodeValues="nodeValues" :path="getPath(index)" :hideDelete="hideDelete" @valueChanged="valueChanged" />
@ -67,6 +71,9 @@ export default mixins(genericHelpers)
hideDelete (): boolean {
return this.parameter.options.length === 1;
},
sortable (): string {
return this.parameter.typeOptions && this.parameter.typeOptions.sortable;
},
},
methods: {
addItem () {
@ -97,6 +104,26 @@ export default mixins(genericHelpers)
getPath (index?: number): string {
return this.path + (index !== undefined ? `[${index}]` : '');
},
moveOptionDown (index: number) {
this.values.splice(index + 1, 0, this.values.splice(index, 1)[0]);
const parameterData = {
name: this.path,
value: this.values,
};
this.$emit('valueChanged', parameterData);
},
moveOptionUp (index: number) {
this.values.splice(index - 1, 0, this.values.splice(index, 1)[0]);
const parameterData = {
name: this.path,
value: this.values,
};
this.$emit('valueChanged', parameterData);
},
valueChanged (parameterData: IUpdateInformation) {
this.$emit('valueChanged', parameterData);
},
@ -125,6 +152,7 @@ export default mixins(genericHelpers)
top: .3em;
z-index: 999;
color: #f56c6c;
width: 15px;
:hover {
color: #ff0000;

View file

@ -12,7 +12,7 @@
<div class="node-executing-info" title="Node is executing">
<font-awesome-icon icon="sync-alt" spin />
</div>
<div class="node-options" v-if="!isReadOnly">
<div class="node-options no-select-on-click" v-if="!isReadOnly">
<div v-touch:tap="deleteNode" class="option" title="Delete Node" >
<font-awesome-icon icon="trash" />
</div>

View file

@ -62,6 +62,21 @@
<el-radio-button label="Binary" v-if="binaryData.length !== 0"></el-radio-button>
</el-radio-group>
</div>
<div class="select-button" v-if="displayMode === 'JSON' && state.path !== deselectedPlaceholder">
<el-dropdown trigger="click" @command="handleCopyClick">
<span class="el-dropdown-link">
<el-button class="retry-button" circle type="text" size="small" title="Copy">
<font-awesome-icon icon="copy" />
</el-button>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{command: 'itemPath'}">Copy Item Path</el-dropdown-item>
<el-dropdown-item :command="{command: 'parameterPath'}">Copy Parameter Path</el-dropdown-item>
<el-dropdown-item :command="{command: 'value'}">Copy Value</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
<div class="data-display-content">
<span v-if="node && workflowRunData !== null && workflowRunData.hasOwnProperty(node.name)">
@ -104,10 +119,18 @@
</tr>
</table>
</div>
<json-tree
<vue-json-pretty
v-else-if="displayMode === 'JSON'"
:data="jsonData"
:level="10"
:deep="10"
v-model="state.path"
:showLine="true"
:showLength="true"
selectableType="single"
path=""
:highlightSelectedNode="true"
:selectOnClickNode="true"
@click="dataItemClicked"
class="json-data"
/>
</div>
@ -171,8 +194,8 @@
<script lang="ts">
import Vue from 'vue';
// @ts-ignore
import JsonTree from 'vue-json-tree';
//@ts-ignore
import VueJsonPretty from 'vue-json-pretty';
import {
GenericValue,
IBinaryData,
@ -200,13 +223,18 @@ import {
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
import { copyPaste } from '@/components/mixins/copyPaste';
import { genericHelpers } from '@/components/mixins/genericHelpers';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { workflowRun } from '@/components/mixins/workflowRun';
import mixins from 'vue-typed-mixins';
// A path that does not exist so that nothing is selected by default
const deselectedPlaceholder = '_!^&*';
export default mixins(
copyPaste,
genericHelpers,
nodeHelpers,
workflowRun,
@ -215,13 +243,18 @@ export default mixins(
name: 'RunData',
components: {
BinaryDataDisplay,
JsonTree,
VueJsonPretty,
},
data () {
return {
binaryDataPreviewActive: false,
dataSize: 0,
deselectedPlaceholder,
displayMode: 'Table',
state: {
value: '' as object | number | string,
path: deselectedPlaceholder,
},
runIndex: 0,
showData: false,
outputIndex: 0,
@ -380,18 +413,6 @@ export default mixins(
},
},
methods: {
getOutputName (outputIndex: number) {
if (this.node === null) {
return outputIndex + 1;
}
const nodeType = this.$store.getters.nodeType(this.node.type);
if (!nodeType.hasOwnProperty('outputNames') || nodeType.outputNames.length <= outputIndex) {
return outputIndex + 1;
}
return nodeType.outputNames[outputIndex];
},
closeBinaryDataDisplay () {
this.binaryDataDisplayVisible = false;
this.binaryDataDisplayData = null;
@ -465,7 +486,9 @@ export default mixins(
this.$store.commit('setWorkflowExecutionData', null);
this.updateNodesExecutionIssues();
},
// displayBinaryData (binaryData: IBinaryData) {
dataItemClicked (path: string, data: object | number | string) {
this.state.value = data;
},
displayBinaryData (index: number, key: string) {
this.binaryDataDisplayVisible = true;
@ -477,6 +500,85 @@ export default mixins(
key,
};
},
getOutputName (outputIndex: number) {
if (this.node === null) {
return outputIndex + 1;
}
const nodeType = this.$store.getters.nodeType(this.node.type);
if (!nodeType.hasOwnProperty('outputNames') || nodeType.outputNames.length <= outputIndex) {
return outputIndex + 1;
}
return nodeType.outputNames[outputIndex];
},
convertPath (path: string): string {
// TODO: That can for sure be done fancier but for now it works
const placeholder = '*___~#^#~___*';
let inBrackets = path.match(/\[(.*?)\]/g);
if (inBrackets === null) {
inBrackets = [];
} else {
inBrackets = inBrackets.map(item => item.slice(1, -1)).map(item => {
if (item.startsWith('"') && item.endsWith('"')) {
return item.slice(1, -1);
}
return item;
});
}
const withoutBrackets = path.replace(/\[(.*?)\]/g, placeholder);
const pathParts = withoutBrackets.split('.');
const allParts = [] as string[];
pathParts.forEach(part => {
let index = part.indexOf(placeholder);
while(index !== -1) {
if (index === 0) {
allParts.push(inBrackets!.shift() as string);
part = part.substr(placeholder.length);
} else {
allParts.push(part.substr(0, index));
part = part.substr(index);
}
index = part.indexOf(placeholder);
}
if (part !== '') {
allParts.push(part);
}
});
return '["' + allParts.join('"]["') + '"]';
},
handleCopyClick (commandData: { command: string }) {
const newPath = this.convertPath(this.state.path);
let value: string;
if (commandData.command === 'value') {
if (typeof this.state.value === 'object') {
value = JSON.stringify(this.state.value, null, 2);
} else {
value = this.state.value.toString();
}
} else {
let startPath = '';
let path = '';
if (commandData.command === 'itemPath') {
const pathParts = newPath.split(']');
const index = pathParts[0].slice(1);
path = pathParts.slice(1).join(']');
startPath = `$item(${index}).$node["${this.node!.name}"].json`;
} else if (commandData.command === 'parameterPath') {
path = newPath.split(']').slice(1).join(']');
startPath = `$node["${this.node!.name}"].json`;
}
if (!path.startsWith('[') && !path.startsWith('.') && path) {
path += '.';
}
value = `{{ ${startPath + path} }}`;
}
this.copyToClipboard(value);
},
refreshDataSize () {
// Hide by default the data from being displayed
this.showData = false;
@ -492,10 +594,6 @@ export default mixins(
// Data is reasonable small (< 200kb) so display it directly
this.showData = true;
}
if (this.displayMode === 'Binary' && this.binaryData.length === 0) {
this.displayMode = 'Table';
}
},
},
watch: {
@ -504,6 +602,12 @@ export default mixins(
this.outputIndex = 0;
this.maxDisplayItems = 25;
this.refreshDataSize();
if (this.displayMode === 'Binary') {
this.closeBinaryDataDisplay();
if (this.binaryData.length === 0) {
this.displayMode = 'Table';
}
}
},
jsonData () {
this.refreshDataSize();
@ -610,15 +714,8 @@ export default mixins(
}
.json-data {
.json-tree {
&.vjs-tree {
color: $--custom-input-font;
.json-tree-value-number {
color: #b03030;
}
.json-tree-value-string {
color: #8aab1a;
}
}
}
@ -694,6 +791,16 @@ export default mixins(
padding-top: 10px;
padding-left: 10px;
.select-button {
height: 30px;
top: 50px;
right: 30px;
position: absolute;
text-align: right;
width: 200px;
z-index: 10;
}
.title-text {
display: inline-block;
line-height: 30px;

View file

@ -341,6 +341,14 @@ export const nodeBase = mixins(
}
},
mouseLeftClick (e: MouseEvent) {
// @ts-ignore
const path = e.path || (e.composedPath && e.composedPath());
for (let index = 0; index < path.length; index++) {
if (path[index].className && typeof path[index].className === 'string' && path[index].className.includes('no-select-on-click')) {
return;
}
}
if (!this.isTouchDevice) {
if (this.$store.getters.isActionActive('dragActive')) {
this.$store.commit('removeActiveAction', 'dragActive');

View file

@ -5,6 +5,7 @@ import Vue from 'vue';
import 'prismjs';
import 'prismjs/themes/prism.css';
import 'vue-prism-editor/dist/VuePrismEditor.css';
import 'vue-json-pretty/lib/styles.css';
import Vue2TouchEvents from 'vue2-touch-events';
import * as ElementUI from 'element-ui';

View file

@ -138,14 +138,13 @@ export const store = new Vuex.Store({
state.activeWorkflows = newActiveWorkflows;
},
setWorkflowActive (state, workflowId: string) {
state.stateIsDirty = true;
state.stateIsDirty = false;
const index = state.activeWorkflows.indexOf(workflowId);
if (index === -1) {
state.activeWorkflows.push(workflowId);
}
},
setWorkflowInactive (state, workflowId: string) {
state.stateIsDirty = true;
const index = state.activeWorkflows.indexOf(workflowId);
if (index !== -1) {
state.selectedNodes.splice(index, 1);

View file

@ -9,6 +9,7 @@
"noImplicitReturns": true,
"strict": true,
"jsx": "preserve",
"skipLibCheck": true,
"importHelpers": true,
"moduleResolution": "node",
"esModuleInterop": true,

View file

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

View file

@ -7,6 +7,7 @@ import {
export class CortexApi implements ICredentialType {
name = 'cortexApi';
displayName = 'Cortex API';
documentationUrl = 'cortex';
properties = [
{
displayName: 'API Key',

View file

@ -3,6 +3,11 @@ import {
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'files.content.write',
'files.content.read',
'sharing.read',
];
export class DropboxOAuth2Api implements ICredentialType {
name = 'dropboxOAuth2Api';
@ -30,13 +35,13 @@ export class DropboxOAuth2Api implements ICredentialType {
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: '',
default: scopes.join(' '),
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
default: 'token_access_type=offline',
},
{
displayName: 'Authentication',

View file

@ -0,0 +1,22 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class EgoiApi implements ICredentialType {
name = 'egoiApi';
displayName = 'e-goi API';
documentationUrl = 'egoi';
properties = [
// The credentials to get from user and save encrypted.
// Properties can be defined exactly in the same way
// as node properties.
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -23,14 +23,14 @@ export class GithubOAuth2Api implements ICredentialType {
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://github.com/login/oauth/authorize',
default: '={{$parameter["server"] === "https://api.github.com" ? "https://github.com" : $parameter["server"]}}/login/oauth/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://github.com/login/oauth/access_token',
default: '={{$parameter["server"] === "https://api.github.com" ? "https://github.com" : $parameter["server"]}}/login/oauth/access_token',
required: true,
},
{

View file

@ -22,14 +22,14 @@ export class GitlabOAuth2Api implements ICredentialType {
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://gitlab.com/oauth/authorize',
default: '={{$parameter["server"]}}/oauth/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://gitlab.com/oauth/token',
default: '={{$parameter["server"]}}/oauth/token',
required: true,
},
{

View file

@ -50,7 +50,7 @@ export class MauticOAuth2Api implements ICredentialType {
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'header',
default: 'body',
},
];
}

View file

@ -0,0 +1,22 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MicrosoftOutlookOAuth2Api implements ICredentialType {
name = 'microsoftOutlookOAuth2Api';
extends = [
'microsoftOAuth2Api',
];
displayName = 'Microsoft Outlook OAuth2 API';
documentationUrl = 'microsoft';
properties = [
//https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: 'openid offline_access Mail.ReadWrite Mail.Send MailboxSettings.Read',
},
];
}

View file

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

View file

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

View file

@ -0,0 +1,157 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class RabbitMQ implements ICredentialType {
name = 'rabbitmq';
displayName = 'RabbitMQ';
documentationUrl = 'rabbitmq';
properties = [
{
displayName: 'Hostname',
name: 'hostname',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'localhost',
},
{
displayName: 'Port',
name: 'port',
type: 'number' as NodePropertyTypes,
default: 5672,
},
{
displayName: 'User',
name: 'username',
type: 'string' as NodePropertyTypes,
default: '',
placeholder: 'guest',
},
{
displayName: 'Password',
name: 'password',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
default: '',
placeholder: 'guest',
},
{
displayName: 'Vhost',
name: 'vhost',
type: 'string' as NodePropertyTypes,
default: '/',
},
{
displayName: 'SSL',
name: 'ssl',
type: 'boolean' as NodePropertyTypes,
default: false,
},
{
displayName: 'Client Certificate',
name: 'cert',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
displayOptions: {
show: {
ssl: [
true,
],
},
},
default: '',
description: 'SSL Client Certificate to use.',
},
{
displayName: 'Client Key',
name: 'key',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
displayOptions: {
show: {
ssl: [
true,
],
},
},
default: '',
description: 'SSL Client Key to use.',
},
{
displayName: 'Passphrase',
name: 'passphrase',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
displayOptions: {
show: {
ssl: [
true,
],
},
},
default: '',
description: 'SSL passphrase to use.',
},
{
displayName: 'CA Certificates',
name: 'ca',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
// typeOptions: {
// multipleValues: true,
// multipleValueButtonText: 'Add Certificate',
// },
displayOptions: {
show: {
ssl: [
true,
],
},
},
default: '',
description: 'SSL CA Certificates to use.',
},
// {
// displayName: 'Client ID',
// name: 'clientId',
// type: 'string' as NodePropertyTypes,
// default: '',
// placeholder: 'my-app',
// },
// {
// displayName: 'Brokers',
// name: 'brokers',
// type: 'string' as NodePropertyTypes,
// default: '',
// placeholder: 'kafka1:9092,kafka2:9092',
// },
// {
// displayName: 'Username',
// name: 'username',
// type: 'string' as NodePropertyTypes,
// default: '',
// description: 'Optional username if authenticated is required.',
// },
// {
// displayName: 'Password',
// name: 'password',
// type: 'string' as NodePropertyTypes,
// typeOptions: {
// password: true,
// },
// default: '',
// description: 'Optional password if authenticated is required.',
// },
];
}

View file

@ -8,13 +8,15 @@ const userScopes = [
'chat:write',
'files:read',
'files:write',
'groups:read',
'im:read',
'mpim:read',
'reactions:read',
'reactions:write',
'stars:read',
'stars:write',
'users.profile:read',
'users.profile:write',
'groups:read',
'im:read',
'mpim:read',
];
export class SlackOAuth2Api implements ICredentialType {

View file

@ -0,0 +1,70 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class Snowflake implements ICredentialType {
name = 'snowflake';
displayName = 'Snowflake';
documentationUrl = 'snowflake';
properties = [
{
displayName: 'Account',
name: 'account',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Enter the name of your Snowflake account.',
},
{
displayName: 'Database',
name: 'database',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Specify the database you want to use after creating the connection.',
},
{
displayName: 'Warehouse',
name: 'warehouse',
type: 'string' as NodePropertyTypes,
default: '',
description: 'The default virtual warehouse to use for the session after connecting. Used for performing queries, loading data, etc.',
},
{
displayName: 'Username',
name: 'username',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
default: '',
},
{
displayName: 'Schema',
name: 'schema',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Enter the schema you want to use after creating the connection',
},
{
displayName: 'Role',
name: 'role',
type: 'string' as NodePropertyTypes,
default: '',
description: 'Enter the security role you want to use after creating the connection',
},
{
displayName: 'Client Session Keep Alive',
name: 'clientSessionKeepAlive',
type: 'boolean' as NodePropertyTypes,
default: false,
description: `By default, client connections typically time out approximately 3-4 hours after the most recent query was executed.<br>
If the parameter clientSessionKeepAlive is set to true, the clients connection to the server will be kept alive indefinitely, even if no queries are executed.`,
},
];
}

View file

@ -6,6 +6,7 @@ import {
export class TheHiveApi implements ICredentialType {
name = 'theHiveApi';
displayName = 'The Hive API';
documentationUrl = 'theHive';
properties = [
{
displayName: 'API Key',

View file

@ -0,0 +1,55 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
const scopes = [
'attachments:write',
'channels:remove',
'messages:remove',
'workspaces:read',
];
export class TwistOAuth2Api implements ICredentialType {
name = 'twistOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Twist OAuth2 API';
documentationUrl = 'twist';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://twist.com/oauth/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://twist.com/oauth/access_token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: scopes.join(','),
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'body',
description: 'Resource to consume.',
},
];
}

View file

@ -0,0 +1,23 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class UProcApi implements ICredentialType {
name = 'uprocApi';
displayName = 'uProc API';
properties = [
{
displayName: 'Email',
name: 'email',
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -272,7 +272,7 @@ export class ActiveCampaign implements INodeType {
// select them easily
async getContactCustomFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { fields } = await activeCampaignApiRequest.call(this, 'GET', '/api/3/fields', {});
const { fields } = await activeCampaignApiRequest.call(this, 'GET', '/api/3/fields', {}, { limit: 100 });
for (const field of fields) {
const fieldName = field.title;
const fieldId = field.id;
@ -287,7 +287,7 @@ export class ActiveCampaign implements INodeType {
// select them easily
async getAccountCustomFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { accountCustomFieldMeta: fields } = await activeCampaignApiRequest.call(this, 'GET', '/api/3/accountCustomFieldMeta', {});
const { accountCustomFieldMeta: fields } = await activeCampaignApiRequest.call(this, 'GET', '/api/3/accountCustomFieldMeta', {}, { limit: 100 });
for (const field of fields) {
const fieldName = field.fieldLabel;
const fieldId = field.id;
@ -298,6 +298,19 @@ export class ActiveCampaign implements INodeType {
}
return returnData;
},
// Get all the available 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 activeCampaignApiRequest.call(this, 'GET', '/api/3/tags', {}, { limit: 100 });
for (const tag of tags) {
returnData.push({
name: tag.tag,
value: tag.id,
});
}
return returnData;
},
},
};

View file

@ -38,7 +38,10 @@ export const contactTagFields = [
{
displayName: 'Tag ID',
name: 'tagId',
type: 'number',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: '',
required: true,
displayOptions: {

View file

@ -14,9 +14,16 @@ import {
import {
asanaApiRequest,
asanaApiRequestAllItems,
getTaskFields,
getWorkspaces,
} from './GenericFunctions';
import * as moment from 'moment-timezone';
import {
snakeCase,
} from 'change-case';
export class Asana implements INodeType {
description: INodeTypeDescription = {
displayName: 'Asana',
@ -83,6 +90,10 @@ export class Asana implements INodeType {
name: 'Project',
value: 'project',
},
{
name: 'Subtask',
value: 'subtask',
},
{
name: 'Task',
value: 'task',
@ -103,7 +114,273 @@ export class Asana implements INodeType {
default: 'task',
description: 'The resource to operate on.',
},
// ----------------------------------
// subtask
// ----------------------------------
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'subtask',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a subtask',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all substasks',
},
],
default: 'create',
description: 'The operation to perform.',
},
// ----------------------------------
// subtask:create
// ----------------------------------
{
displayName: 'Parent Task ID',
name: 'taskId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'subtask',
],
},
},
description: 'The task to operate on.',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'subtask',
],
},
},
description: 'The name of the subtask to create',
},
{
displayName: 'Additional Fields',
name: 'otherProperties',
type: 'collection',
displayOptions: {
show: {
resource: [
'subtask',
],
operation: [
'create',
],
},
},
default: {},
placeholder: 'Add Field',
options: [
{
displayName: 'Assignee',
name: 'assignee',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
default: '',
description: 'Set Assignee on the subtask',
},
{
displayName: 'Assignee Status',
name: 'assignee_status',
type: 'options',
options: [
{
name: 'Inbox',
value: 'inbox',
},
{
name: 'Today',
value: 'today',
},
{
name: 'Upcoming',
value: 'upcoming',
},
{
name: 'Later',
value: 'later',
},
],
default: 'inbox',
description: 'Set Assignee status on the subtask (requires Assignee)',
},
{
displayName: 'Completed',
name: 'completed',
type: 'boolean',
default: false,
description: 'If the subtask should be marked completed.',
},
{
displayName: 'Due On',
name: 'due_on',
type: 'dateTime',
default: '',
description: 'Date on which the time is due.',
},
{
displayName: 'Liked',
name: 'liked',
type: 'boolean',
default: false,
description: 'If the task is liked by the authorized user.',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
rows: 5,
},
default: '',
description: 'The task notes',
},
{
displayName: 'Workspace',
name: 'workspace',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getWorkspaces',
},
default: '',
description: 'The workspace to create the subtask in',
},
],
},
// ----------------------------------
// subtask:getAll
// ----------------------------------
{
displayName: 'Parent Task ID',
name: 'taskId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'subtask',
],
},
},
description: 'The task to operate on.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'subtask',
],
},
},
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: [
'subtask',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'subtask',
],
},
},
default: {},
placeholder: 'Add Field',
options: [
{
displayName: 'Fields',
name: 'opt_fields',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTaskFields',
},
default: [
'gid',
'name',
'resource_type',
],
description: 'Defines fields to return.',
},
{
displayName: 'Pretty',
name: 'opt_pretty',
type: 'boolean',
default: false,
description: 'Provides “pretty” output.',
},
],
},
// ----------------------------------
// task
// ----------------------------------
@ -134,6 +411,11 @@ export class Asana implements INodeType {
value: 'get',
description: 'Get a task',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all tasks',
},
{
name: 'Move',
value: 'move',
@ -241,6 +523,145 @@ export class Asana implements INodeType {
},
description: 'The ID of the task to get the data of.',
},
// ----------------------------------
// task:getAll
// ----------------------------------
{
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',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'task',
],
},
},
default: {},
description: 'Properties to search for',
placeholder: 'Add Filter',
options: [
{
displayName: 'Assignee',
name: 'assignee',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
default: '',
description: 'The assignee to filter tasks on. Note: If you specify assignee, you must also specify the workspace to filter on.',
},
{
displayName: 'Fields',
name: 'opt_fields',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTaskFields',
},
default: [
'gid',
'name',
'resource_type',
],
description: 'Defines fields to return.',
},
{
displayName: 'Pretty',
name: 'opt_pretty',
type: 'boolean',
default: false,
description: 'Provides “pretty” output.',
},
{
displayName: 'Project',
name: 'project',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getProjects',
},
default: '',
description: 'The project to filter tasks on.',
},
{
displayName: 'Section',
name: 'section',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getSections',
},
default: '',
description: 'The section to filter tasks on.',
},
{
displayName: 'Workspace',
name: 'workspace',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getWorkspaces',
},
default: '',
description: 'The workspace to filter tasks on. Note: If you specify workspace, you must also specify the assignee to filter on.',
},
{
displayName: 'Completed Since',
name: 'completed_since',
type: 'dateTime',
default: '',
description: 'Only return tasks that are either incomplete or that have been completed since this time.',
},
{
displayName: 'Modified Since',
name: 'modified_since',
type: 'dateTime',
default: '',
description: 'Only return tasks that have been modified since the given time.',
},
],
},
// ----------------------------------
// task:move
@ -1219,12 +1640,25 @@ export class Asana implements INodeType {
return returnData;
},
async getTaskFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
for (const field of getTaskFields()) {
const value = snakeCase(field);
returnData.push({
name: field,
value: (value === '') ? '*' : value,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const timezone = this.getTimezone();
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
@ -1240,6 +1674,61 @@ export class Asana implements INodeType {
body = {};
qs = {};
if (resource === 'subtask') {
if (operation === 'create') {
// ----------------------------------
// subtask:create
// ----------------------------------
const taskId = this.getNodeParameter('taskId', i) as string;
requestMethod = 'POST';
endpoint = `/tasks/${taskId}/subtasks`;
body.name = this.getNodeParameter('name', i) as string;
const otherProperties = this.getNodeParameter('otherProperties', i) as IDataObject;
Object.assign(body, otherProperties);
responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = responseData.data;
}
if (operation === 'getAll') {
// ----------------------------------
// subtask:getAll
// ----------------------------------
const taskId = this.getNodeParameter('taskId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const options = this.getNodeParameter('options', i) as IDataObject;
requestMethod = 'GET';
endpoint = `/tasks/${taskId}/subtasks`;
Object.assign(qs, options);
if (qs.opt_fields) {
const fields = qs.opt_fields as string[];
if (fields.includes('*')) {
qs.opt_fields = getTaskFields().map((e) => snakeCase(e)).join(',');
} else {
qs.opt_fields = (qs.opt_fields as string[]).join(',');
}
}
responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = responseData.data;
if (returnAll === false) {
const limit = this.getNodeParameter('limit', i) as boolean;
responseData = responseData.splice(0, limit);
}
}
}
if (resource === 'task') {
if (operation === 'create') {
// ----------------------------------
@ -1286,6 +1775,47 @@ export class Asana implements INodeType {
responseData = responseData.data;
} else if (operation === 'getAll') {
// ----------------------------------
// task:getAll
// ----------------------------------
const filters = this.getNodeParameter('filters', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
requestMethod = 'GET';
endpoint = `/tasks`;
Object.assign(qs, filters);
if (qs.opt_fields) {
const fields = qs.opt_fields as string[];
if (fields.includes('*')) {
qs.opt_fields = getTaskFields().map((e) => snakeCase(e)).join(',');
} else {
qs.opt_fields = (qs.opt_fields as string[]).join(',');
}
}
if (qs.modified_since) {
qs.modified_since = moment.tz(qs.modified_since as string, timezone).format();
}
if (qs.completed_since) {
qs.completed_since = moment.tz(qs.completed_since as string, timezone).format();
}
if (returnAll) {
responseData = await asanaApiRequestAllItems.call(this, requestMethod, endpoint, body, qs);
} else {
qs.limit = this.getNodeParameter('limit', i) as boolean;
responseData = await asanaApiRequest.call(this, requestMethod, endpoint, body, qs);
responseData = responseData.data;
}
} else if (operation === 'move') {
// ----------------------------------
// task:move

View file

@ -121,3 +121,40 @@ export async function getWorkspaces(this: ILoadOptionsFunctions): Promise < INod
return returnData;
}
export function getTaskFields() {
return [
'*',
'GID',
'Resource Type',
'name',
'Approval Status',
'Assignee Status',
'Completed',
'Completed At',
'Completed By',
'Created At',
'Dependencies',
'Dependents',
'Due At',
'Due On',
'External',
'HTML Notes',
'Liked',
'Likes',
'Memberships',
'Modified At',
'Notes',
'Num Likes',
'Resource Subtype',
'Start On',
'Assignee',
'Custom Fields',
'Followers',
'Parent',
'Permalink URL',
'Projects',
'Tags',
'Workspace',
];
}

View file

@ -21,7 +21,7 @@ export class BoxTrigger implements INodeType {
icon: 'file:box.png',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when a Github events occurs.',
description: 'Starts the workflow when a Box events occurs.',
defaults: {
name: 'Box Trigger',
color: '#00aeef',

View file

@ -0,0 +1,271 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
brandfetchApiRequest,
} from './GenericFunctions';
export class Brandfetch implements INodeType {
description: INodeTypeDescription = {
displayName: 'Brandfetch',
name: 'Brandfetch',
icon: 'file:brandfetch.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"]}}',
description: 'Consume Brandfetch API',
defaults: {
name: 'Brandfetch',
color: '#1f1f1f',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'brandfetchApi',
required: true,
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Color',
value: 'color',
description: 'Return a company\'s colors',
},
{
name: 'Company',
value: 'company',
description: 'Return a company\'s data',
},
{
name: 'Font',
value: 'font',
description: 'Return a company\'s fonts',
},
{
name: 'Industry',
value: 'industry',
description: 'Return a company\'s industry',
},
{
name: 'Logo',
value: 'logo',
description: 'Return a company\'s logo & icon',
},
],
default: 'logo',
description: 'The operation to perform',
},
// ----------------------------------
// All
// ----------------------------------
{
displayName: 'Domain',
name: 'domain',
type: 'string',
default: '',
description: 'The domain name of the company.',
required: true,
},
{
displayName: 'Download',
name: 'download',
type: 'boolean',
default: false,
required: true,
displayOptions: {
show: {
operation: [
'logo',
],
},
},
description: 'Name of the binary property to which to<br />write the data of the read file.',
},
{
displayName: 'Image Type',
name: 'imageTypes',
type: 'multiOptions',
displayOptions: {
show: {
operation: [
'logo',
],
download: [
true,
],
},
},
options: [
{
name: 'Icon',
value: 'icon',
},
{
name: 'Logo',
value: 'logo',
},
],
default: [
'logo',
'icon',
],
required: true,
},
{
displayName: 'Image Format',
name: 'imageFormats',
type: 'multiOptions',
displayOptions: {
show: {
operation: [
'logo',
],
download: [
true,
],
},
},
options: [
{
name: 'PNG',
value: 'png',
},
{
name: 'SVG',
value: 'svg',
},
],
default: [
'png',
],
description: 'The image format in which the logo should be returned as.',
required: true,
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const length = items.length as unknown as number;
const operation = this.getNodeParameter('operation', 0) as string;
const responseData = [];
for (let i = 0; i < length; i++) {
if (operation === 'logo') {
const domain = this.getNodeParameter('domain', i) as string;
const download = this.getNodeParameter('download', i) as boolean;
const body: IDataObject = {
domain,
};
const response = await brandfetchApiRequest.call(this, 'POST', `/logo`, body);
if (download === true) {
const imageTypes = this.getNodeParameter('imageTypes', i) as string[];
const imageFormats = this.getNodeParameter('imageFormats', i) as string[];
const newItem: INodeExecutionData = {
json: {},
binary: {},
};
if (items[i].binary !== undefined) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary, items[i].binary);
}
newItem.json = response.response;
for (const imageType of imageTypes) {
for (const imageFormat of imageFormats) {
const url = response.response[imageType][(imageFormat === 'png') ? 'image' : imageFormat] as string;
if (url !== null) {
const data = await brandfetchApiRequest.call(this, 'GET', '', {}, {}, url, { json: false, encoding: null });
newItem.binary![`${imageType}_${imageFormat}`] = await this.helpers.prepareBinaryData(data, `${imageType}_${domain}.${imageFormat}`);
items[i] = newItem;
}
items[i] = newItem;
}
}
if (Object.keys(items[i].binary!).length === 0) {
delete items[i].binary;
}
} else {
responseData.push(response.response);
}
}
if (operation === 'color') {
const domain = this.getNodeParameter('domain', i) as string;
const body: IDataObject = {
domain,
};
const response = await brandfetchApiRequest.call(this, 'POST', `/color`, body);
responseData.push(response.response);
}
if (operation === 'font') {
const domain = this.getNodeParameter('domain', i) as string;
const body: IDataObject = {
domain,
};
const response = await brandfetchApiRequest.call(this, 'POST', `/font`, body);
responseData.push(response.response);
}
if (operation === 'company') {
const domain = this.getNodeParameter('domain', i) as string;
const body: IDataObject = {
domain,
};
const response = await brandfetchApiRequest.call(this, 'POST', `/company`, body);
responseData.push(response.response);
}
if (operation === 'industry') {
const domain = this.getNodeParameter('domain', i) as string;
const body: IDataObject = {
domain,
};
const response = await brandfetchApiRequest.call(this, 'POST', `/industry`, body);
responseData.push.apply(responseData, response.response);
}
}
if (operation === 'logo' && this.getNodeParameter('download', 0) === true) {
// For file downloads the files get attached to the existing items
return this.prepareOutputData(items);
} else {
return [this.helpers.returnJsonArray(responseData)];
}
}
}

View file

@ -0,0 +1,65 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function brandfetchApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
try {
const credentials = this.getCredentials('brandfetchApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
let options: OptionsWithUri = {
headers: {
'x-api-key': credentials.apiKey,
},
method,
qs,
body,
uri: uri || `https://api.brandfetch.io/v1${resource}`,
json: true,
};
options = Object.assign({}, options, option);
if (this.getNodeParameter('operation', 0) === 'logo' && options.json === false) {
delete options.headers;
}
if (!Object.keys(body).length) {
delete options.body;
}
if (!Object.keys(qs).length) {
delete options.qs;
}
const response = await this.helpers.request!(options);
if (response.statusCode && response.statusCode !== 200) {
throw new Error(`Brandfetch error response [${response.statusCode}]: ${response.response}`);
}
return response;
} catch (error) {
if (error.response && error.response.body && error.response.body.message) {
// Try to return the error prettier
const errorBody = error.response.body;
throw new Error(`Brandfetch error response [${error.statusCode}]: ${errorBody.message}`);
}
// Expected error data did not get returned so throw the actual error
throw error;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,767 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
egoiApiRequest,
egoiApiRequestAllItems,
simplify,
} from './GenericFunctions';
import {
ICreateMemberBody,
} from './Interfaces';
import * as moment from 'moment-timezone';
export class Egoi implements INodeType {
description: INodeTypeDescription = {
displayName: 'e-goi',
name: 'egoi',
icon: 'file:egoi.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume e-goi API',
defaults: {
name: 'e-goi',
color: '#4cacd6',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'egoiApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
required: true,
options: [
{
name: 'Contact',
value: 'contact',
},
],
default: 'contact',
description: 'The resource to operate on.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
required: true,
options: [
{
name: 'Create',
value: 'create',
description: 'Create a member',
},
{
name: 'Get',
value: 'get',
description: 'Get a member',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all members',
},
{
name: 'Update',
value: 'update',
description: 'Update a member',
},
],
default: 'create',
description: 'The operation to perform.',
},
{
displayName: 'List ID',
name: 'list',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLists',
},
displayOptions: {
show: {
operation: [
'getAll',
'create',
'update',
'get',
],
},
},
default: '',
description: 'ID of list to operate on.',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
displayOptions: {
show: {
operation: [
'create',
],
},
},
default: '',
description: 'Email address for a subscriber.',
},
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'update',
],
},
},
default: '',
description: 'Contact ID of the subscriber.',
},
{
displayName: 'Resolve Data',
name: 'resolveData',
type: 'boolean',
displayOptions: {
show: {
operation: [
'create',
'update',
],
},
},
default: true,
description: 'By default the response just includes the contact id. If this option gets activated it<br />will resolve the data automatically.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'contact',
],
},
},
default: {},
options: [
{
displayName: 'Birth Date',
name: 'birth_date',
type: 'dateTime',
default: '',
description: 'Birth date of a subscriber.',
},
{
displayName: 'Cellphone',
name: 'cellphone',
type: 'string',
default: '',
description: 'Cellphone of a subscriber.',
},
{
displayName: 'Extra Fields',
name: 'extraFieldsUi',
type: 'fixedCollection',
placeholder: 'Add Field',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
displayName: 'Extra Field',
name: 'extraFieldValues',
typeOptions: {
multipleValueButtonText: 'Add Field',
},
values: [
{
displayName: 'Field ID',
name: 'field_id',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getExtraFields',
loadOptionsDependsOn: [
'list',
],
},
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'First Name',
name: 'first_name',
type: 'string',
default: '',
description: 'Name of a subscriber.',
},
{
displayName: 'Last Name',
name: 'last_name',
type: 'string',
default: '',
description: 'Name of a subscriber.',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Unconfirmed',
value: 'unconfirmed',
},
{
name: 'Active',
value: 'active',
},
{
name: 'Inactive',
value: 'inactive',
},
{
name: 'Removed',
value: 'removed',
},
],
default: 'active',
description: `Subscriber's current status.`,
},
{
displayName: 'Tags IDs',
name: 'tagIds',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getListTags',
},
default: [],
description: 'List of tag ids to be added',
},
],
},
//--------------------
//----UPDATE MEMBER---
//--------------------
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: [
'update',
],
},
},
options: [
{
displayName: 'Birth Date',
name: 'birth_date',
type: 'dateTime',
default: '',
description: 'Birth date of subscriber.',
},
{
displayName: 'Cellphone',
name: 'cellphone',
type: 'string',
default: '',
description: 'Cellphone of subscriber.',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
description: 'Email address for subscriber.',
},
{
displayName: 'Extra Fields',
name: 'extraFieldsUi',
type: 'fixedCollection',
placeholder: 'Add Field',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
displayName: 'Extra Field',
name: 'extraFieldValues',
typeOptions: {
multipleValueButtonText: 'Add Field',
},
values: [
{
displayName: 'Field ID',
name: 'field_id',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getExtraFields',
loadOptionsDependsOn: [
'list',
],
},
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'First Name',
name: 'first_name',
type: 'string',
default: '',
description: 'Name of subscriber.',
},
{
displayName: 'Last Name',
name: 'last_name',
type: 'string',
default: '',
description: 'Name of subscriber.',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Unconfirmed',
value: 'unconfirmed',
},
{
name: 'Active',
value: 'active',
},
{
name: 'Inactive',
value: 'inactive',
},
{
name: 'Removed',
value: 'removed',
},
],
default: 'active',
description: `Subscriber's current status.`,
},
{
displayName: 'Tags IDs',
name: 'tagIds',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getListTags',
},
default: [],
description: 'List of tag ids to be added',
},
],
},
{
displayName: 'By',
name: 'by',
type: 'options',
options: [
{
name: 'Contact ID',
value: 'id',
},
{
name: 'Email',
value: 'email',
},
],
displayOptions: {
show: {
operation: [
'get',
],
resource: [
'contact',
],
},
},
default: 'id',
description: 'Search by',
},
{
displayName: 'Contact ID',
name: 'contactId',
type: 'string',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'get',
],
by: [
'id',
],
},
},
default: '',
description: 'Contact ID of the subscriber.',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
displayOptions: {
show: {
resource: [
'contact',
],
operation: [
'get',
],
by: [
'email',
],
},
},
default: '',
description: 'Email address for subscriber.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'contact',
],
},
},
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: [
'contact',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
{
displayName: 'Simple',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
operation: [
'get',
'getAll',
],
resource: [
'contact',
],
},
},
default: true,
description: 'When set to true a simple version of the response will be returned else the RAW data.',
},
],
};
methods = {
loadOptions: {
async getLists(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const lists = await egoiApiRequestAllItems.call(this, 'items', 'GET', '/lists');
for (const list of lists) {
const listName = list.internal_name;
const listId = list.list_id;
returnData.push({
name: listName,
value: listId,
});
}
return returnData;
},
async getExtraFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const listId = this.getCurrentNodeParameter('list');
const extraFields = await egoiApiRequest.call(this, 'GET', `/lists/${listId}/fields`);
for (const field of extraFields) {
if (field.type === 'extra') {
const fieldName = field.name;
const fieldId = field.field_id;
returnData.push({
name: fieldName,
value: fieldId,
});
}
}
return returnData;
},
async getListTags(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const tagList = await egoiApiRequestAllItems.call(this, 'items', 'GET', '/tags');
for (const tag of tagList) {
const tagName = tag.name;
const tagId = tag.tag_id;
returnData.push({
name: tagName,
value: tagId,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
let responseData;
const returnData: IDataObject[] = [];
const items = this.getInputData();
const length = items.length as unknown as number;
const operation = this.getNodeParameter('operation', 0) as string;
const resource = this.getNodeParameter('resource', 0) as string;
for (let i = 0; i < length; i++) {
try {
if (resource === 'contact') {
if (operation === 'create') {
const listId = this.getNodeParameter('list', i) as string;
const email = this.getNodeParameter('email', i) as string;
const resolveData = this.getNodeParameter('resolveData', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: ICreateMemberBody = {
base: {
email,
},
extra: [],
};
if (additionalFields.birth_date) {
additionalFields.birth_date = moment(additionalFields.birth_date as string).format('YYYY-MM-DD');
}
if (additionalFields.extraFieldsUi) {
const extraFields = (additionalFields.extraFieldsUi as IDataObject).extraFieldValues as IDataObject[];
if (extraFields) {
body.extra = extraFields as unknown as [];
}
}
Object.assign(body.base, additionalFields);
responseData = await egoiApiRequest.call(this, 'POST', `/lists/${listId}/contacts`, body);
const contactId = responseData.contact_id;
if (additionalFields.tagIds) {
const tags = additionalFields.tagIds as string[];
for (const tag of tags) {
await egoiApiRequest.call(this, 'POST', `/lists/${listId}/contacts/actions/attach-tag`, { tag_id: tag, contacts: [contactId] });
}
}
if (resolveData) {
responseData = await egoiApiRequest.call(this, 'GET', `/lists/${listId}/contacts/${contactId}`);
}
}
if (operation === 'get') {
const listId = this.getNodeParameter('list', i) as string;
const simple = this.getNodeParameter('simple', i) as boolean;
const by = this.getNodeParameter('by', 0) as string;
let endpoint = '';
if (by === 'id') {
const contactId = this.getNodeParameter('contactId', i) as string;
endpoint = `/lists/${listId}/contacts/${contactId}`;
} else {
const email = this.getNodeParameter('email', i) as string;
endpoint = `/lists/${listId}/contacts?email=${email}`;
}
responseData = await egoiApiRequest.call(this, 'GET', endpoint, {});
if (responseData.items) {
responseData = responseData.items;
}
if (simple === true) {
const data = (await simplify.call(this, [responseData], listId))[0];
responseData = {
...data,
email_stats: responseData.email_stats,
sms_stats: responseData.sms_stats,
push_stats: responseData.push_stats,
webpush_stats: responseData.webpush_stats,
voice_stats: responseData.voice_stats,
};
}
}
if (operation === 'getAll') {
const listId = this.getNodeParameter('list', i) as string;
const returnAll = this.getNodeParameter('returnAll', 0) as boolean;
const simple = this.getNodeParameter('simple', i) as boolean;
if (returnAll) {
responseData = await egoiApiRequestAllItems.call(this, 'items', 'GET', `/lists/${listId}/contacts`, {});
} else {
const limit = this.getNodeParameter('limit', i) as number;
responseData = await egoiApiRequest.call(this, 'GET', `/lists/${listId}/contacts`, {}, { limit });
responseData = responseData.items;
}
if (simple === true) {
responseData = await simplify.call(this, responseData, listId);
}
}
if (operation === 'update') {
const listId = this.getNodeParameter('list', i) as string;
const contactId = this.getNodeParameter('contactId', i) as string;
const resolveData = this.getNodeParameter('resolveData', i) as boolean;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const body: ICreateMemberBody = {
base: {
},
extra: [],
};
if (updateFields.birth_date) {
updateFields.birth_date = moment(updateFields.birth_date as string).format('YYYY-MM-DD');
}
if (updateFields.extraFieldsUi) {
const extraFields = (updateFields.extraFieldsUi as IDataObject).extraFieldValues as IDataObject[];
if (extraFields) {
body.extra = extraFields as unknown as [];
}
}
Object.assign(body.base, updateFields);
responseData = await egoiApiRequest.call(this, 'PATCH', `/lists/${listId}/contacts/${contactId}`, body);
if (updateFields.tagIds) {
const tags = updateFields.tagIds as string[];
for (const tag of tags) {
await egoiApiRequest.call(this, 'POST', `/lists/${listId}/contacts/actions/attach-tag`, { tag_id: tag, contacts: [contactId] });
}
}
if (resolveData) {
responseData = await egoiApiRequest.call(this, 'GET', `/lists/${listId}/contacts/${contactId}`);
}
}
}
} catch (error) {
if (this.continueOnFail() !== true) {
throw error;
} else {
// Return the actual reason as error
returnData.push(
{
error: error.message,
},
);
continue;
}
}
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,126 @@
import {
OptionsWithUrl,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
interface IContact {
tags: [];
base: IDataObject;
extra: IDataObject[];
}
const fieldCache: {
[key: string]: IDataObject[];
} = {};
export async function getFields(this: IExecuteFunctions, listId: string) {
if (fieldCache[listId]) {
return fieldCache[listId];
}
fieldCache[listId] = await egoiApiRequest.call(this, 'GET', `/lists/${listId}/fields`);
return fieldCache[listId];
}
export async function egoiApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, headers?: object): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('egoiApi') as IDataObject;
const options: OptionsWithUrl = {
headers: {
'accept': 'application/json',
'Apikey': `${credentials.apiKey}`,
},
method,
qs,
body,
url: `https://api.egoiapp.com${endpoint}`,
json: true,
};
if (Object.keys(body).length === 0) {
delete options.body;
}
try {
return await this.helpers.request!(options);
} catch (error) {
let errorMessage;
if (error.response && error.response.body) {
if (Array.isArray(error.response.body.errors)) {
const errors = error.response.body.errors;
errorMessage = errors.map((e: IDataObject) => e.detail);
} else {
errorMessage = error.response.body.detail;
}
throw new Error(`e-goi Error response [${error.statusCode}]: ${errorMessage}`);
}
throw error;
}
}
export async function egoiApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.offset = 0;
query.count = 500;
do {
responseData = await egoiApiRequest.call(this, method, endpoint, body, query);
returnData.push.apply(returnData, responseData[propertyName]);
query.offset += query.count;
} while (
responseData[propertyName] && responseData[propertyName].length !== 0
);
return returnData;
}
export async function simplify(this: IExecuteFunctions, contacts: IContact[], listId: string) {
let fields = await getFields.call(this, listId);
fields = fields.filter((element: IDataObject) => element.type === 'extra');
const fieldsKeyValue: IDataObject = {};
for (const field of fields) {
fieldsKeyValue[field.field_id as string] = field.name;
}
const data: IDataObject[] = [];
for (const contact of contacts) {
const extras = contact.extra.reduce(
(acumulator: IDataObject, currentValue: IDataObject): any => { // tslint:disable-line:no-any
const key = fieldsKeyValue[currentValue.field_id as string] as string;
return { [key]: currentValue.value, ...acumulator };
},
{},
);
data.push({
...contact.base,
...extras,
tags: contact.tags,
});
}
return data;
}

View file

@ -0,0 +1,12 @@
export interface ICreateMemberBody {
base: {
email?: string;
first_name?: string;
last_name?: string;
cellphone?: string;
birth_date?: string;
subscription_status?: string;
};
extra: [];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,011 B

View file

@ -1,68 +0,0 @@
import { IExecuteFunctions } from 'n8n-core';
import {
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
export class Example implements INodeType {
description: INodeTypeDescription = {
displayName: 'Example',
name: 'example',
group: ['input'],
version: 1,
description: 'Example',
defaults: {
name: 'Example',
color: '#0000FF',
},
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: 'Age',
name: 'age',
type: 'number',
default: 0,
},
{
displayName: 'Brand Name',
name: 'brandName',
type: 'string',
default: '',
},
],
};
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
if (items.length === 0) {
items.push({json: {}});
}
const returnData: INodeExecutionData[] = [];
let item: INodeExecutionData;
for (let i = 0; i < items.length; i++) {
const age = this.getNodeParameter('age', i) as number | undefined;
const brandName = this.getNodeParameter('brandName', i) as string | undefined;
item = items[i];
const newItem: INodeExecutionData = {
json: {
age: age || item.json.age,
brandName: brandName || item.json.brandName,
},
};
returnData.push(newItem);
}
return this.prepareOutputData(returnData);
}
}

View file

@ -97,6 +97,11 @@ export class Ftp implements INodeType {
name: 'operation',
type: 'options',
options: [
{
name: 'Delete',
value: 'delete',
description: 'Delete a file.',
},
{
name: 'Download',
value: 'download',
@ -107,6 +112,11 @@ export class Ftp implements INodeType {
value: 'list',
description: 'List folder content.',
},
{
name: 'Rename',
value: 'rename',
description: 'Rename/move oldPath to newPath.',
},
{
name: 'Upload',
value: 'upload',
@ -117,6 +127,25 @@ export class Ftp implements INodeType {
description: 'Operation to perform.',
},
// ----------------------------------
// delete
// ----------------------------------
{
displayName: 'Path',
displayOptions: {
show: {
operation: [
'delete',
],
},
},
name: 'path',
type: 'string',
default: '',
description: 'The file path of the file to delete. Has to contain the full path.',
required: true,
},
// ----------------------------------
// download
// ----------------------------------
@ -152,6 +181,40 @@ export class Ftp implements INodeType {
required: true,
},
// ----------------------------------
// rename
// ----------------------------------
{
displayName: 'Old Path',
displayOptions: {
show: {
operation: [
'rename',
],
},
},
name: 'oldPath',
type: 'string',
default: '',
description: 'The old path',
required: true,
},
{
displayName: 'New Path',
displayOptions: {
show: {
operation: [
'rename',
],
},
},
name: 'newPath',
type: 'string',
default: '',
description: 'The new path',
required: true,
},
// ----------------------------------
// upload
// ----------------------------------
@ -318,9 +381,10 @@ export class Ftp implements INodeType {
items[i] = newItem;
if (protocol === 'sftp') {
const path = this.getNodeParameter('path', i) as string;
if (operation === 'list') {
const path = this.getNodeParameter('path', i) as string;
const recursive = this.getNodeParameter('recursive', i) as boolean;
if (recursive) {
@ -333,7 +397,27 @@ export class Ftp implements INodeType {
}
}
if (operation === 'delete') {
const path = this.getNodeParameter('path', i) as string;
responseData = await sftp!.delete(path);
returnItems.push({ json: { success: true } });
}
if (operation === 'rename') {
const oldPath = this.getNodeParameter('oldPath', i) as string;
const newPath = this.getNodeParameter('newPath', i) as string;
responseData = await sftp!.rename(oldPath, newPath);
returnItems.push({ json: { success: true } });
}
if (operation === 'download') {
const path = this.getNodeParameter('path', i) as string;
responseData = await sftp!.get(path);
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
@ -385,9 +469,9 @@ export class Ftp implements INodeType {
if (protocol === 'ftp') {
const path = this.getNodeParameter('path', i) as string;
if (operation === 'list') {
const path = this.getNodeParameter('path', i) as string;
const recursive = this.getNodeParameter('recursive', i) as boolean;
if (recursive) {
@ -400,7 +484,17 @@ export class Ftp implements INodeType {
}
}
if (operation === 'delete') {
const path = this.getNodeParameter('path', i) as string;
responseData = await ftp!.delete(path);
returnItems.push({ json: { success: true } });
}
if (operation === 'download') {
const path = this.getNodeParameter('path', i) as string;
responseData = await ftp!.get(path);
// Convert readable stream to buffer so that can be displayed properly
@ -420,6 +514,17 @@ export class Ftp implements INodeType {
returnItems.push(items[i]);
}
if (operation === 'rename') {
const oldPath = this.getNodeParameter('oldPath', i) as string;
const newPath = this.getNodeParameter('newPath', i) as string;
responseData = await ftp!.rename(oldPath, newPath);
returnItems.push({ json: { success: true } });
}
if (operation === 'upload') {
const remotePath = this.getNodeParameter('path', i) as string;
const fileName = basename(remotePath);

View file

@ -121,6 +121,13 @@ export class CloudFirestore implements INodeType {
{ documents: documentList },
);
responseData = responseData.map((element: { found: { id: string, name: string } }) => {
if (element.found) {
element.found.id = (element.found.name as string).split('/').pop() as string;
}
return element;
});
if (simple === false) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
@ -148,6 +155,9 @@ export class CloudFirestore implements INodeType {
`/${projectId}/databases/${database}/documents/${collection}`,
document,
);
responseData.id = (responseData.name as string).split('/').pop();
if (simple === false) {
returnData.push(responseData);
} else {
@ -179,11 +189,16 @@ export class CloudFirestore implements INodeType {
) as IDataObject;
responseData = getAllResponse.documents;
}
responseData = responseData.map((element: IDataObject) => {
element.id = (element.name as string).split('/').pop();
return element;
});
if (simple === false) {
returnData.push.apply(returnData, responseData);
} else {
returnData.push.apply(returnData, responseData.map((element: IDataObject) => fullDocumentToJson(element as IDataObject)));
}
} else if (operation === 'delete') {
const responseData: IDataObject[] = [];
@ -295,6 +310,14 @@ export class CloudFirestore implements INodeType {
`/${projectId}/databases/${database}/documents:runQuery`,
JSON.parse(query),
);
responseData = responseData.map((element: { document: { id: string, name: string } }) => {
if (element.document) {
element.document.id = (element.document.name as string).split('/').pop() as string;
}
return element;
});
if (simple === false) {
returnData.push.apply(returnData, responseData);
} else {

View file

@ -117,6 +117,7 @@ export function fullDocumentToJson(data: IDataObject): IDataObject {
return {
_name: data.name,
_id: data.id,
_createTime: data.createTime,
_updateTime: data.updateTime,
...documentToJson(data.fields as IDataObject),

View file

@ -18,7 +18,7 @@ import {
export class RealtimeDatabase implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Firebase Realtime Database',
displayName: 'Google Cloud Realtime Database',
name: 'googleFirebaseRealtimeDatabase',
icon: 'file:googleFirebaseRealtimeDatabase.png',
group: ['input'],

View file

@ -21,6 +21,10 @@ import {
ValueRenderOption,
} from './GoogleSheet';
import {
googleApiRequest,
} from './GenericFunctions';
export class GoogleSheets implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Sheets ',
@ -28,6 +32,7 @@ export class GoogleSheets implements INodeType {
icon: 'file:googlesheets.png',
group: ['input', 'output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Read, update and write data to Google Sheets',
defaults: {
name: 'Google Sheets',
@ -76,10 +81,35 @@ export class GoogleSheets implements INodeType {
],
default: 'serviceAccount',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Spreadsheet',
value: 'spreadsheet',
},
{
name: 'Sheet',
value: 'sheet',
},
],
default: 'sheet',
description: 'The operation to perform.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'sheet',
],
},
},
options: [
{
name: 'Append',
@ -123,6 +153,13 @@ export class GoogleSheets implements INodeType {
displayName: 'Sheet ID',
name: 'sheetId',
type: 'string',
displayOptions: {
show: {
resource: [
'sheet',
],
},
},
default: '',
required: true,
description: 'The ID of the Google Sheet.<br />Found as part of the sheet URL https://docs.google.com/spreadsheets/d/{ID}/',
@ -132,6 +169,11 @@ export class GoogleSheets implements INodeType {
name: 'range',
type: 'string',
displayOptions: {
show: {
resource: [
'sheet',
],
},
hide: {
operation: [
'delete',
@ -158,6 +200,9 @@ export class GoogleSheets implements INodeType {
},
displayOptions: {
show: {
resource: [
'sheet',
],
operation: [
'delete',
],
@ -254,6 +299,9 @@ export class GoogleSheets implements INodeType {
type: 'boolean',
displayOptions: {
show: {
resource: [
'sheet',
],
operation: [
'read',
],
@ -269,6 +317,9 @@ export class GoogleSheets implements INodeType {
default: 'data',
displayOptions: {
show: {
resource: [
'sheet',
],
operation: [
'read',
],
@ -289,6 +340,9 @@ export class GoogleSheets implements INodeType {
type: 'boolean',
displayOptions: {
show: {
resource: [
'sheet',
],
operation: [
'update',
],
@ -304,6 +358,9 @@ export class GoogleSheets implements INodeType {
default: 'data',
displayOptions: {
show: {
resource: [
'sheet',
],
operation: [
'update',
],
@ -327,6 +384,11 @@ export class GoogleSheets implements INodeType {
},
default: 1,
displayOptions: {
show: {
resource: [
'sheet',
],
},
hide: {
operation: [
'append',
@ -352,6 +414,11 @@ export class GoogleSheets implements INodeType {
minValue: 0,
},
displayOptions: {
show: {
resource: [
'sheet',
],
},
hide: {
operation: [
'clear',
@ -379,6 +446,9 @@ export class GoogleSheets implements INodeType {
required: true,
displayOptions: {
show: {
resource: [
'sheet',
],
operation: [
'lookup',
],
@ -394,6 +464,9 @@ export class GoogleSheets implements INodeType {
placeholder: 'frank@example.com',
displayOptions: {
show: {
resource: [
'sheet',
],
operation: [
'lookup',
],
@ -412,6 +485,9 @@ export class GoogleSheets implements INodeType {
default: 'id',
displayOptions: {
show: {
resource: [
'sheet',
],
operation: [
'update',
],
@ -431,6 +507,9 @@ export class GoogleSheets implements INodeType {
default: {},
displayOptions: {
show: {
resource: [
'sheet',
],
operation: [
'append',
'lookup',
@ -566,6 +645,154 @@ export class GoogleSheets implements INodeType {
],
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'spreadsheet',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a spreadsheet',
},
],
default: 'create',
description: 'The operation to perform.',
},
// ----------------------------------
// spreadsheet:create
// ----------------------------------
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'spreadsheet',
],
operation: [
'create',
],
},
},
description: 'The title of the spreadsheet.',
},
{
displayName: 'Sheets',
name: 'sheetsUi',
placeholder: 'Add Sheet',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
displayOptions: {
show: {
resource: [
'spreadsheet',
],
operation: [
'create',
],
},
},
options: [
{
name: 'sheetValues',
displayName: 'Sheet',
values: [
{
displayName: 'Sheet Properties',
name: 'propertiesUi',
placeholder: 'Add Property',
type: 'collection',
default: {},
options: [
{
displayName: 'Hidden',
name: 'hidden',
type: 'boolean',
default: false,
description: 'If the Sheet should be hidden in the UI',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'Title of the property to create',
},
],
},
],
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: [
'spreadsheet',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Locale',
name: 'locale',
type: 'string',
default: '',
placeholder: 'en_US',
description: 'The locale of the spreadsheet in one of the following formats:<br /><ul><li>en (639-1)</li><li>fil (639-2 if no 639-1 format exists)</li><li>en_US (combination of ISO language an country)</li><ul>',
},
{
displayName: 'Recalculation Interval',
name: 'autoRecalc',
type: 'options',
options: [
{
name: 'Default',
value: '',
description: 'Default value',
},
{
name: 'On Change',
value: 'ON_CHANGE',
description: 'Volatile functions are updated on every change.',
},
{
name: 'Minute',
value: 'MINUTE',
description: 'Volatile functions are updated on every change and every minute.',
},
{
name: 'Hour',
value: 'HOUR',
description: ' Volatile functions are updated on every change and hourly.',
},
],
default: '',
description: 'Cell recalculation interval options.',
},
],
},
],
};
@ -602,188 +829,247 @@ export class GoogleSheets implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const spreadsheetId = this.getNodeParameter('sheetId', 0) as string;
const sheet = new GoogleSheet(spreadsheetId, this);
const operation = this.getNodeParameter('operation', 0) as string;
const resource = this.getNodeParameter('resource', 0) as string;
let range = '';
if (operation !== 'delete') {
range = this.getNodeParameter('range', 0) as string;
}
if (resource === 'sheet') {
const options = this.getNodeParameter('options', 0, {}) as IDataObject;
const spreadsheetId = this.getNodeParameter('sheetId', 0) as string;
const valueInputMode = (options.valueInputMode || 'RAW') as ValueInputOption;
const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption;
const sheet = new GoogleSheet(spreadsheetId, this);
if (operation === 'append') {
// ----------------------------------
// append
// ----------------------------------
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
const items = this.getInputData();
const setData: IDataObject[] = [];
items.forEach((item) => {
setData.push(item.json);
});
// Convert data into array format
const data = await sheet.appendSheetData(setData, sheet.encodeRange(range), keyRow, valueInputMode);
// TODO: Should add this data somewhere
// TODO: Should have something like add metadata which does not get passed through
return this.prepareOutputData(items);
} else if (operation === 'clear') {
// ----------------------------------
// clear
// ----------------------------------
await sheet.clearData(sheet.encodeRange(range));
const items = this.getInputData();
return this.prepareOutputData(items);
} else if (operation === 'delete') {
// ----------------------------------
// delete
// ----------------------------------
const requests: IDataObject[] = [];
const toDelete = this.getNodeParameter('toDelete', 0) as IToDelete;
const deletePropertyToDimensions: IDataObject = {
'columns': 'COLUMNS',
'rows': 'ROWS',
};
for (const propertyName of Object.keys(deletePropertyToDimensions)) {
if (toDelete[propertyName] !== undefined) {
toDelete[propertyName]!.forEach(range => {
requests.push({
deleteDimension: {
range: {
sheetId: range.sheetId,
dimension: deletePropertyToDimensions[propertyName] as string,
startIndex: range.startIndex,
endIndex: parseInt(range.startIndex.toString(), 10) + parseInt(range.amount.toString(), 10),
},
},
});
});
}
let range = '';
if (operation !== 'delete') {
range = this.getNodeParameter('range', 0) as string;
}
const data = await sheet.spreadsheetBatchUpdate(requests);
const options = this.getNodeParameter('options', 0, {}) as IDataObject;
const items = this.getInputData();
return this.prepareOutputData(items);
} else if (operation === 'lookup') {
// ----------------------------------
// lookup
// ----------------------------------
const valueInputMode = (options.valueInputMode || 'RAW') as ValueInputOption;
const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption;
const sheetData = await sheet.getData(sheet.encodeRange(range), valueRenderMode);
if (sheetData === undefined) {
return [];
}
const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
const items = this.getInputData();
const lookupValues: ILookupValues[] = [];
for (let i = 0; i < items.length; i++) {
lookupValues.push({
lookupColumn: this.getNodeParameter('lookupColumn', i) as string,
lookupValue: this.getNodeParameter('lookupValue', i) as string,
});
}
let returnData = await sheet.lookupValues(sheetData, keyRow, dataStartRow, lookupValues, options.returnAllMatches as boolean | undefined);
if (returnData.length === 0 && options.continue && options.returnAllMatches) {
returnData = [{}];
} else if (returnData.length === 1 && Object.keys(returnData[0]).length === 0 && !options.continue && !options.returnAllMatches) {
returnData = [];
}
return [this.helpers.returnJsonArray(returnData)];
} else if (operation === 'read') {
// ----------------------------------
// read
// ----------------------------------
const rawData = this.getNodeParameter('rawData', 0) as boolean;
const sheetData = await sheet.getData(sheet.encodeRange(range), valueRenderMode);
let returnData: IDataObject[];
if (!sheetData) {
returnData = [];
} else if (rawData === true) {
const dataProperty = this.getNodeParameter('dataProperty', 0) as string;
returnData = [
{
[dataProperty]: sheetData,
},
];
} else {
const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
if (operation === 'append') {
// ----------------------------------
// append
// ----------------------------------
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
returnData = sheet.structureArrayDataByColumn(sheetData, keyRow, dataStartRow);
}
if (returnData.length === 0 && options.continue) {
returnData = [{}];
}
return [this.helpers.returnJsonArray(returnData)];
} else if (operation === 'update') {
// ----------------------------------
// update
// ----------------------------------
const rawData = this.getNodeParameter('rawData', 0) as boolean;
const items = this.getInputData();
if (rawData === true) {
const dataProperty = this.getNodeParameter('dataProperty', 0) as string;
const updateData: ISheetUpdateData[] = [];
for (let i = 0; i < items.length; i++) {
updateData.push({
range,
values: items[i].json[dataProperty] as string[][],
});
}
const data = await sheet.batchUpdate(updateData, valueInputMode);
} else {
const keyName = this.getNodeParameter('key', 0) as string;
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
const items = this.getInputData();
const setData: IDataObject[] = [];
items.forEach((item) => {
setData.push(item.json);
});
const data = await sheet.updateSheetData(setData, keyName, range, keyRow, dataStartRow, valueInputMode, valueRenderMode);
// Convert data into array format
const data = await sheet.appendSheetData(setData, sheet.encodeRange(range), keyRow, valueInputMode);
// TODO: Should add this data somewhere
// TODO: Should have something like add metadata which does not get passed through
return this.prepareOutputData(items);
} else if (operation === 'clear') {
// ----------------------------------
// clear
// ----------------------------------
await sheet.clearData(sheet.encodeRange(range));
const items = this.getInputData();
return this.prepareOutputData(items);
} else if (operation === 'delete') {
// ----------------------------------
// delete
// ----------------------------------
const requests: IDataObject[] = [];
const toDelete = this.getNodeParameter('toDelete', 0) as IToDelete;
const deletePropertyToDimensions: IDataObject = {
'columns': 'COLUMNS',
'rows': 'ROWS',
};
for (const propertyName of Object.keys(deletePropertyToDimensions)) {
if (toDelete[propertyName] !== undefined) {
toDelete[propertyName]!.forEach(range => {
requests.push({
deleteDimension: {
range: {
sheetId: range.sheetId,
dimension: deletePropertyToDimensions[propertyName] as string,
startIndex: range.startIndex,
endIndex: parseInt(range.startIndex.toString(), 10) + parseInt(range.amount.toString(), 10),
},
},
});
});
}
}
const data = await sheet.spreadsheetBatchUpdate(requests);
const items = this.getInputData();
return this.prepareOutputData(items);
} else if (operation === 'lookup') {
// ----------------------------------
// lookup
// ----------------------------------
const sheetData = await sheet.getData(sheet.encodeRange(range), valueRenderMode);
if (sheetData === undefined) {
return [];
}
const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
const items = this.getInputData();
const lookupValues: ILookupValues[] = [];
for (let i = 0; i < items.length; i++) {
lookupValues.push({
lookupColumn: this.getNodeParameter('lookupColumn', i) as string,
lookupValue: this.getNodeParameter('lookupValue', i) as string,
});
}
let returnData = await sheet.lookupValues(sheetData, keyRow, dataStartRow, lookupValues, options.returnAllMatches as boolean | undefined);
if (returnData.length === 0 && options.continue && options.returnAllMatches) {
returnData = [{}];
} else if (returnData.length === 1 && Object.keys(returnData[0]).length === 0 && !options.continue && !options.returnAllMatches) {
returnData = [];
}
return [this.helpers.returnJsonArray(returnData)];
} else if (operation === 'read') {
// ----------------------------------
// read
// ----------------------------------
const rawData = this.getNodeParameter('rawData', 0) as boolean;
const sheetData = await sheet.getData(sheet.encodeRange(range), valueRenderMode);
let returnData: IDataObject[];
if (!sheetData) {
returnData = [];
} else if (rawData === true) {
const dataProperty = this.getNodeParameter('dataProperty', 0) as string;
returnData = [
{
[dataProperty]: sheetData,
},
];
} else {
const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
returnData = sheet.structureArrayDataByColumn(sheetData, keyRow, dataStartRow);
}
if (returnData.length === 0 && options.continue) {
returnData = [{}];
}
return [this.helpers.returnJsonArray(returnData)];
} else if (operation === 'update') {
// ----------------------------------
// update
// ----------------------------------
const rawData = this.getNodeParameter('rawData', 0) as boolean;
const items = this.getInputData();
if (rawData === true) {
const dataProperty = this.getNodeParameter('dataProperty', 0) as string;
const updateData: ISheetUpdateData[] = [];
for (let i = 0; i < items.length; i++) {
updateData.push({
range,
values: items[i].json[dataProperty] as string[][],
});
}
const data = await sheet.batchUpdate(updateData, valueInputMode);
} else {
const keyName = this.getNodeParameter('key', 0) as string;
const keyRow = parseInt(this.getNodeParameter('keyRow', 0) as string, 10);
const dataStartRow = parseInt(this.getNodeParameter('dataStartRow', 0) as string, 10);
const setData: IDataObject[] = [];
items.forEach((item) => {
setData.push(item.json);
});
const data = await sheet.updateSheetData(setData, keyName, range, keyRow, dataStartRow, valueInputMode, valueRenderMode);
}
// TODO: Should add this data somewhere
// TODO: Should have something like add metadata which does not get passed through
return this.prepareOutputData(items);
}
// TODO: Should add this data somewhere
// TODO: Should have something like add metadata which does not get passed through
}
return this.prepareOutputData(items);
if (resource === 'spreadsheet') {
const returnData: IDataObject[] = [];
let responseData;
if (operation === 'create') {
// ----------------------------------
// create
// ----------------------------------
// https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/create
for (let i = 0; i < this.getInputData().length; i++) {
const title = this.getNodeParameter('title', i) as string;
const sheetsUi = this.getNodeParameter('sheetsUi', i, {}) as IDataObject;
const body = {
properties: {
title,
autoRecalc: undefined as undefined | string,
locale: undefined as undefined | string,
},
sheets: [] as IDataObject[],
};
const options = this.getNodeParameter('options', i, {}) as IDataObject;
if (Object.keys(sheetsUi).length) {
const data = [];
const sheets = sheetsUi.sheetValues as IDataObject[];
for (const sheet of sheets) {
const properties = sheet.propertiesUi as IDataObject;
if (properties) {
data.push({ properties });
}
}
body.sheets = data;
}
body.properties!.autoRecalc = options.autoRecalc ? (options.autoRecalc as string) : undefined;
body.properties!.locale = options.locale ? (options.locale as string) : undefined;
responseData = await googleApiRequest.call(this, 'POST', `/v4/spreadsheets`, body);
returnData.push(responseData);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
return [];

View file

@ -355,6 +355,13 @@ export class HttpRequest implements INodeType {
default: 10000,
description: 'Time in ms to wait for the server to send response headers (and start the response body) before aborting the request.',
},
{
displayName: 'Use Querystring',
name: 'useQueryString',
type: 'boolean',
default: false,
description: 'Set this option to true if you need arrays to be serialized as foo=bar&foo=baz instead of the default foo[0]=bar&foo[1]=baz.',
},
],
},
@ -683,6 +690,10 @@ export class HttpRequest implements INodeType {
requestOptions.timeout = options.timeout as number;
}
if (options.useQueryString === true) {
requestOptions.useQuerystring = true;
}
if (parametersAreJson === true) {
// Parameters are defined as JSON
let optionData: OptionData;
@ -927,7 +938,7 @@ export class HttpRequest implements INodeType {
newItem.binary![dataPropertyName] = await this.helpers.prepareBinaryData(response!, fileName);
}
items[itemIndex] = newItem;
returnItems.push(newItem);
} else if (responseFormat === 'string') {
const dataPropertyName = this.getNodeParameter('dataPropertyName', 0) as string;
@ -980,12 +991,6 @@ export class HttpRequest implements INodeType {
}
}
if (responseFormat === 'file') {
// For file downloads the files get attached to the existing items
return this.prepareOutputData(items);
} else {
// For all other ones does the output items get replaced
return this.prepareOutputData(returnItems);
}
return this.prepareOutputData(returnItems);
}
}

View file

@ -112,6 +112,10 @@ export class If implements INodeType {
name: 'Larger Equal',
value: 'largerEqual',
},
{
name: 'Is Empty',
value: 'isEmpty',
},
],
default: 'smaller',
description: 'Operation to decide where the the data should be mapped to.',
@ -120,6 +124,13 @@ export class If implements INodeType {
displayName: 'Value 2',
name: 'value2',
type: 'number',
displayOptions: {
hide: {
operation: [
'isEmpty',
],
},
},
default: 0,
description: 'The value to compare with the first one.',
},
@ -145,6 +156,10 @@ export class If implements INodeType {
name: 'Contains',
value: 'contains',
},
{
name: 'Ends With',
value: 'endsWith',
},
{
name: 'Equal',
value: 'equal',
@ -161,6 +176,14 @@ export class If implements INodeType {
name: 'Regex',
value: 'regex',
},
{
name: 'Starts With',
value: 'startsWith',
},
{
name: 'Is Empty',
value: 'isEmpty',
},
],
default: 'equal',
description: 'Operation to decide where the the data should be mapped to.',
@ -172,6 +195,7 @@ export class If implements INodeType {
displayOptions: {
hide: {
operation: [
'isEmpty',
'regex',
],
},
@ -236,12 +260,15 @@ export class If implements INodeType {
} = {
contains: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || '').toString().includes((value2 || '').toString()),
notContains: (value1: NodeParameterValue, value2: NodeParameterValue) => !(value1 || '').toString().includes((value2 || '').toString()),
endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).endsWith(value2 as string),
equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2,
notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2,
larger: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) > (value2 || 0),
largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) >= (value2 || 0),
smaller: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) < (value2 || 0),
smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 || 0) <= (value2 || 0),
startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) => (value1 as string).startsWith(value2 as string),
isEmpty: (value1: NodeParameterValue) => [undefined, null, ''].includes(value1 as string),
regex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimy]*)$'));

View file

@ -4,7 +4,9 @@ import {
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
@ -23,6 +25,11 @@ import {
userOperations,
} from './UserDescription';
import {
userListFields,
userListOperations,
} from './UserListDescription';
import * as moment from 'moment-timezone';
export class Iterable implements INodeType {
@ -60,6 +67,10 @@ export class Iterable implements INodeType {
name: 'User',
value: 'user',
},
{
name: 'User List',
value: 'userList',
},
],
default: 'user',
description: 'The resource to operate on.',
@ -68,9 +79,28 @@ export class Iterable implements INodeType {
...eventFields,
...userOperations,
...userFields,
...userListOperations,
...userListFields,
],
};
methods = {
loadOptions: {
// Get all the lists available channels
async getLists(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const { lists } = await iterableApiRequest.call(this, 'GET', '/lists');
const returnData: INodePropertyOptions[] = [];
for (const list of lists) {
returnData.push({
name: list.name,
value: list.id,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
@ -181,7 +211,7 @@ export class Iterable implements INodeType {
if (by === 'email') {
const email = this.getNodeParameter('email', i) as string;
endpoint = `/users/${email}`;
endpoint = `/users/${email}`;
} else {
const userId = this.getNodeParameter('userId', i) as string;
endpoint = `/users/byUserId/${userId}`;
@ -212,7 +242,7 @@ export class Iterable implements INodeType {
if (by === 'email') {
const email = this.getNodeParameter('email', i) as string;
endpoint = `/users/getByEmail`;
endpoint = `/users/getByEmail`;
qs.email = email;
} else {
const userId = this.getNodeParameter('userId', i) as string;
@ -234,6 +264,74 @@ export class Iterable implements INodeType {
}
}
}
if (resource === 'userList') {
if (operation === 'add') {
//https://api.iterable.com/api/docs#lists_subscribe
const listId = this.getNodeParameter('listId', 0) as string;
const identifier = this.getNodeParameter('identifier', 0) as string;
const body: IDataObject = {
listId: parseInt(listId, 10),
subscribers: [],
};
const subscribers: IDataObject[] = [];
for (let i = 0; i < length; i++) {
const value = this.getNodeParameter('value', i) as string;
if (identifier === 'email') {
subscribers.push({ email: value });
} else {
subscribers.push({ userId: value });
}
}
body.subscribers = subscribers;
responseData = await iterableApiRequest.call(this, 'POST', '/lists/subscribe', body);
returnData.push(responseData);
}
if (operation === 'remove') {
//https://api.iterable.com/api/docs#lists_unsubscribe
const listId = this.getNodeParameter('listId', 0) as string;
const identifier = this.getNodeParameter('identifier', 0) as string;
const additionalFields = this.getNodeParameter('additionalFields', 0) as IDataObject;
const body: IDataObject = {
listId: parseInt(listId, 10),
subscribers: [],
campaignId: additionalFields.campaignId as number,
channelUnsubscribe: additionalFields.channelUnsubscribe as boolean,
};
const subscribers: IDataObject[] = [];
for (let i = 0; i < length; i++) {
const value = this.getNodeParameter('value', i) as string;
if (identifier === 'email') {
subscribers.push({ email: value });
} else {
subscribers.push({ userId: value });
}
}
body.subscribers = subscribers;
responseData = await iterableApiRequest.call(this, 'POST', '/lists/unsubscribe', body);
returnData.push(responseData);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -1,6 +1,6 @@
import {
INodeProperties,
} from 'n8n-workflow';
} from 'n8n-workflow';
export const userOperations = [
{
@ -38,9 +38,9 @@ export const userOperations = [
export const userFields = [
/* -------------------------------------------------------------------------- */
/* user:upsert */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* user:upsert */
/* -------------------------------------------------------------------------- */
{
displayName: 'Identifier',
name: 'identifier',
@ -167,9 +167,10 @@ export const userFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* user:delete */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* user:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'By',
name: 'by',
@ -240,9 +241,10 @@ export const userFields = [
default: '',
description: 'Email for a particular user',
},
/* -------------------------------------------------------------------------- */
/* user:get */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* user:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'By',
name: 'by',

View file

@ -0,0 +1,208 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const userListOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'userList',
],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add user to list',
},
{
name: 'Remove',
value: 'remove',
description: 'Remove a user from a list',
},
],
default: 'add',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const userListFields = [
/* -------------------------------------------------------------------------- */
/* userList:add */
/* -------------------------------------------------------------------------- */
{
displayName: 'List ID',
name: 'listId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLists',
},
required: true,
displayOptions: {
show: {
resource: [
'userList',
],
operation: [
'add',
],
},
},
default: '',
description: 'Identifier to be used',
},
{
displayName: 'Identifier',
name: 'identifier',
type: 'options',
required: true,
options: [
{
name: 'Email',
value: 'email',
},
{
name: 'User ID',
value: 'userId',
},
],
displayOptions: {
show: {
resource: [
'userList',
],
operation: [
'add',
],
},
},
default: '',
description: 'Identifier to be used',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'userList',
],
operation: [
'add',
],
},
},
default: '',
},
/* -------------------------------------------------------------------------- */
/* userList:remove */
/* -------------------------------------------------------------------------- */
{
displayName: 'List ID',
name: 'listId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLists',
},
required: true,
displayOptions: {
show: {
resource: [
'userList',
],
operation: [
'remove',
],
},
},
default: '',
description: 'Identifier to be used',
},
{
displayName: 'Identifier',
name: 'identifier',
type: 'options',
required: true,
options: [
{
name: 'Email',
value: 'email',
},
{
name: 'User ID',
value: 'userId',
},
],
displayOptions: {
show: {
resource: [
'userList',
],
operation: [
'remove',
],
},
},
default: '',
description: 'Identifier to be used',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'userList',
],
operation: [
'remove',
],
},
},
default: '',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'userList',
],
operation: [
'remove',
],
},
},
options: [
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'number',
default: 0,
description: 'Attribute unsubscribe to a campaign',
},
{
displayName: 'Channel Unsubscribe',
name: 'channelUnsubscribe',
type: 'boolean',
default: false,
description: `Unsubscribe email from list's associated channel - essentially a global unsubscribe`,
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,488 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const issueCommentOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'issueComment',
],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add comment to issue',
},
{
name: 'Get',
value: 'get',
description: 'Get a comment',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all comments',
},
{
name: 'Remove',
value: 'remove',
description: 'Remove a comment',
},
{
name: 'Update',
value: 'update',
description: 'Update a comment',
},
],
default: 'add',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const issueCommentFields = [
/* -------------------------------------------------------------------------- */
/* issueComment:add */
/* -------------------------------------------------------------------------- */
{
displayName: 'Issue Key',
name: 'issueKey',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'add',
],
},
},
default: '',
description: 'issueComment Key',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'add',
],
},
},
},
{
displayName: 'Comment',
name: 'comment',
type: 'string',
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'add',
],
jsonParameters: [
false,
],
},
},
description: `Comment's text`,
},
{
displayName: 'Document Format (JSON)',
name: 'commentJson',
type: 'json',
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'add',
],
jsonParameters: [
true,
],
},
},
description: `The Atlassian Document Format (ADF). Online builder can be found <a href="https://developer.atlassian.com/cloud/jira/platform/apis/document/playground/" target="_blank">here</a>.`,
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'add',
],
},
},
options: [
{
displayName: 'Expand',
name: 'expand',
type: 'options',
options: [
{
name: 'Rendered Body',
value: 'renderedBody',
},
],
default: '',
description: 'Use expand to include additional information about comments<br />in the response. This parameter accepts Rendered Body, which<br />returns the comment body rendered in HTML.',
},
],
},
/* -------------------------------------------------------------------------- */
/* issueComment:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Issue Key',
name: 'issueKey',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'get',
],
},
},
default: '',
description: 'The ID or key of the issue.',
},
{
displayName: 'Comment ID',
name: 'commentId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'get',
],
},
},
description: 'The ID of the comment',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'get',
],
},
},
options: [
{
displayName: 'Expand',
name: 'expand',
type: 'options',
options: [
{
name: 'Rendered Body',
value: 'renderedBody',
},
],
default: '',
description: 'Use expand to include additional information about comments in<br />the response. This parameter accepts Rendered Body, which<br />returns the comment body rendered in HTML.',
},
],
},
/* -------------------------------------------------------------------------- */
/* issueComment:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Issue Key',
name: 'issueKey',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'getAll',
],
},
},
default: '',
description: 'The ID or key of the issue.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'issueComment',
],
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: [
'issueComment',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Expand',
name: 'expand',
type: 'options',
options: [
{
name: 'Rendered Body',
value: 'renderedBody',
},
],
default: '',
description: 'Use expand to include additional information about comments in the<br />response. This parameter accepts Rendered Body, which returns the comment<br />body rendered in HTML.',
},
],
},
/* -------------------------------------------------------------------------- */
/* issueComment:remove */
/* -------------------------------------------------------------------------- */
{
displayName: 'Issue Key',
name: 'issueKey',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'remove',
],
},
},
default: '',
description: 'The ID or key of the issue.',
},
{
displayName: 'Comment ID',
name: 'commentId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'remove',
],
},
},
description: 'The ID of the comment.',
},
/* -------------------------------------------------------------------------- */
/* issueComment:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Issue Key',
name: 'issueKey',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'update',
],
},
},
default: '',
description: 'The Issue Comment key.',
},
{
displayName: 'Comment ID',
name: 'commentId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'update',
],
},
},
description: 'The ID of the comment.',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'update',
],
},
},
},
{
displayName: 'Comment',
name: 'comment',
type: 'string',
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'update',
],
jsonParameters: [
false,
],
},
},
description: `Comment's text.`,
},
{
displayName: 'Document Format (JSON)',
name: 'commentJson',
type: 'json',
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'update',
],
jsonParameters: [
true,
],
},
},
description: `The Atlassian Document Format (ADF). Online builder can be found <a href="https://developer.atlassian.com/cloud/jira/platform/apis/document/playground/" target="_blank">here</a>.`,
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'issueComment',
],
operation: [
'update',
],
},
},
options: [
{
displayName: 'Expand',
name: 'expand',
type: 'options',
options: [
{
name: 'Rendered Body',
value: 'renderedBody',
},
],
default: '',
description: 'Use expand to include additional information about comments in the response. This parameter accepts Rendered Body, which returns the comment body rendered in HTML.',
},
],
},
] as INodeProperties[];

View file

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

View file

@ -17,6 +17,11 @@ import {
validateJSON,
} from './GenericFunctions';
import {
issueCommentFields,
issueCommentOperations,
} from './IssueCommentDescription';
import {
issueFields,
issueOperations,
@ -96,12 +101,19 @@ export class Jira implements INodeType {
value: 'issue',
description: 'Creates an issue or, where the option to create subtasks is enabled in Jira, a subtask',
},
{
name: 'Issue Comment',
value: 'issueComment',
description: 'Get, create, update, and delete a comment from an issue.',
},
],
default: 'issue',
description: 'Resource to consume.',
},
...issueOperations,
...issueFields,
...issueCommentOperations,
...issueCommentFields,
],
};
@ -642,6 +654,125 @@ export class Jira implements INodeType {
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/2/issue/${issueKey}`, 'DELETE', {}, qs);
}
}
if (resource === 'issueComment') {
//https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-post
if (operation === 'add') {
const jsonParameters = this.getNodeParameter('jsonParameters', 0) as boolean;
const issueKey = this.getNodeParameter('issueKey', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
const body: IDataObject = {};
if (options.expand) {
qs.expand = options.expand as string;
delete options.expand;
}
Object.assign(body, options);
if (jsonParameters === false) {
const comment = this.getNodeParameter('comment', i) as string;
Object.assign(body, {
body: {
type: 'doc',
version: 1,
content: [
{
type: "paragraph",
content: [
{
type: 'text',
text: comment,
},
],
},
],
},
});
} else {
const commentJson = this.getNodeParameter('commentJson', i) as string;
const json = validateJSON(commentJson);
if (json === '') {
throw new Error('Document Format must be a valid JSON');
}
Object.assign(body, { body: json });
}
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment`, 'POST', body, qs);
}
//https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-get
if (operation === 'get') {
const issueKey = this.getNodeParameter('issueKey', i) as string;
const commentId = this.getNodeParameter('commentId', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
Object.assign(qs, options);
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment/${commentId}`, 'GET', {}, qs);
}
//https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-get
if (operation === 'getAll') {
const issueKey = this.getNodeParameter('issueKey', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const options = this.getNodeParameter('options', i) as IDataObject;
const body: IDataObject = {};
Object.assign(qs, options);
if (returnAll) {
responseData = await jiraSoftwareCloudApiRequestAllItems.call(this, 'comments', `/api/3/issue/${issueKey}/comment`, 'GET', body, qs);
} else {
const limit = this.getNodeParameter('limit', i) as number;
body.maxResults = limit;
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment`, 'GET', body, qs);
responseData = responseData.comments;
}
}
//https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-id-delete
if (operation === 'remove') {
const issueKey = this.getNodeParameter('issueKey', i) as string;
const commentId = this.getNodeParameter('commentId', i) as string;
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment/${commentId}`, 'DELETE', {}, qs);
responseData = { success: true };
}
//https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-id-put
if (operation === 'update') {
const issueKey = this.getNodeParameter('issueKey', i) as string;
const commentId = this.getNodeParameter('commentId', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
const jsonParameters = this.getNodeParameter('jsonParameters', 0) as boolean;
const body: IDataObject = {};
if (options.expand) {
qs.expand = options.expand as string;
delete options.expand;
}
Object.assign(qs, options);
if (jsonParameters === false) {
const comment = this.getNodeParameter('comment', i) as string;
Object.assign(body, {
body: {
type: 'doc',
version: 1,
content: [
{
type: "paragraph",
content: [
{
type: 'text',
text: comment,
},
],
},
],
},
});
} else {
const commentJson = this.getNodeParameter('commentJson', i) as string;
const json = validateJSON(commentJson);
if (json === '') {
throw new Error('Document Format must be a valid JSON');
}
Object.assign(body, { body: json });
}
responseData = await jiraSoftwareCloudApiRequest.call(this, `/api/3/issue/${issueKey}/comment/${commentId}`, 'PUT', body, qs);
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {

View file

@ -63,7 +63,7 @@ export async function mauticApiRequest(this: IHookFunctions | IExecuteFunctions
options.uri = `${credentials.url}${options.uri}`;
//@ts-ignore
returnData = await this.helpers.requestOAuth2.call(this, 'mauticOAuth2Api', options);
returnData = await this.helpers.requestOAuth2.call(this, 'mauticOAuth2Api', options, { includeCredentialsOnRefreshOnBody: true });
}
if (returnData.errors) {

View file

@ -43,8 +43,10 @@ export async function messageBirdApiRequest(
uri: `https://rest.messagebird.com${resource}`,
json: true,
};
try {
if (Object.keys(body).length === 0) {
delete options.body;
}
return await this.helpers.request(options);
} catch (error) {
if (error.statusCode === 401) {

View file

@ -1,6 +1,6 @@
import {
IExecuteFunctions,
} from 'n8n-core';
} from 'n8n-core';
import {
IDataObject,
@ -44,6 +44,10 @@ export class MessageBird implements INodeType {
name: 'SMS',
value: 'sms',
},
{
name: 'Balance',
value: 'balance',
},
],
default: 'sms',
description: 'The resource to operate on.',
@ -69,6 +73,27 @@ export class MessageBird implements INodeType {
default: 'send',
description: 'The operation to perform.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'balance',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get the balance',
},
],
default: 'get',
description: 'The operation to perform.',
},
// ----------------------------------
// sms:send
@ -133,6 +158,16 @@ export class MessageBird implements INodeType {
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
displayOptions: {
show: {
operation: [
'send',
],
resource: [
'sms',
],
},
},
placeholder: 'Add Fields',
default: {},
options: [
@ -269,11 +304,12 @@ export class MessageBird implements INodeType {
let resource: string;
// For POST
let bodyRequest: IDataObject;
let bodyRequest: IDataObject = {};
// For Query string
let qs: IDataObject;
let requestMethod;
let requestPath;
for (let i = 0; i < items.length; i++) {
qs = {};
@ -289,6 +325,7 @@ export class MessageBird implements INodeType {
// ----------------------------------
requestMethod = 'POST';
requestPath = '/messages';
const originator = this.getNodeParameter('originator', i) as string;
const body = this.getNodeParameter('message', i) as string;
@ -337,21 +374,27 @@ export class MessageBird implements INodeType {
}
const receivers = this.getNodeParameter('recipients', i) as string;
bodyRequest.recipients = receivers.split(',').map(item => {
return parseInt(item, 10);
});
} else {
}
else {
throw new Error(`The operation "${operation}" is not known!`);
}
} else {
} else if (resource === 'balance') {
requestMethod = 'GET';
requestPath = '/balance';
}
else {
throw new Error(`The resource "${resource}" is not known!`);
}
const responseData = await messageBirdApiRequest.call(
this,
requestMethod,
'/messages',
requestPath,
bodyRequest,
qs,
);

View file

@ -0,0 +1,304 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const draftOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'draft',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new email draft',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a draft',
},
{
name: 'Get',
value: 'get',
description: 'Get a single draft',
},
{
name: 'Send',
value: 'send',
description: 'Send an existing draft message',
},
{
name: 'Update',
value: 'update',
description: 'Update a draft',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const draftFields = [
{
displayName: 'Message ID',
name: 'messageId',
description: 'Message ID',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'draft',
],
operation: [
'delete',
'get',
'send',
'update',
],
},
},
},
// draft:create
{
displayName: 'Subject',
name: 'subject',
description: 'The subject of the message.',
displayOptions: {
show: {
resource: [
'draft',
],
operation: [
'create',
],
},
},
type: 'string',
default: '',
},
{
displayName: 'Body Content',
name: 'bodyContent',
description: 'Message body content.',
type: 'string',
displayOptions: {
show: {
resource: [
'draft',
],
operation: [
'create',
],
},
},
default: '',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'draft',
],
operation: [
'create',
],
},
},
options: [
{
displayName: 'Attachments',
name: 'attachments',
type: 'fixedCollection',
placeholder: 'Add Attachment',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'attachments',
displayName: 'Attachment',
values: [
{
displayName: 'Binary Property Name',
name: 'binaryPropertyName',
type: 'string',
default: '',
description: 'Name of the binary property containing the data to be added to the email as an attachment',
},
],
},
],
},
{
displayName: 'BCC Recipients',
name: 'bccRecipients',
description: 'Email addresses of BCC recipients.',
type: 'string',
default: '',
},
{
displayName: 'Body Content Type',
name: 'bodyContentType',
description: 'Message body content type.',
type: 'options',
options: [
{
name: 'HTML',
value: 'html',
},
{
name: 'Text',
value: 'Text',
},
],
default: 'html',
},
{
displayName: 'Categories',
name: 'categories',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getCategories',
},
default: [],
},
{
displayName: 'CC Recipients',
name: 'ccRecipients',
description: 'Email addresses of CC recipients.',
type: 'string',
default: '',
},
{
displayName: 'Custom Headers',
name: 'internetMessageHeaders',
placeholder: 'Add Header',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'headers',
displayName: 'Header',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the header.',
},
],
},
],
},
{
displayName: 'From',
name: 'from',
description: 'The owner of the mailbox which the message is sent.<br>Must correspond to the actual mailbox used.',
type: 'string',
default: '',
},
{
displayName: 'Importance',
name: 'importance',
description: 'The importance of the message.',
type: 'options',
options: [
{
name: 'Low',
value: 'Low',
},
{
name: 'Normal',
value: 'Normal',
},
{
name: 'High',
value: 'High',
},
],
default: 'Low',
},
{
displayName: 'Read Receipt Requested',
name: 'isReadReceiptRequested',
description: 'Indicates whether a read receipt is requested for the message.',
type: 'boolean',
default: false,
},
{
displayName: 'Recipients',
name: 'toRecipients',
description: 'Email addresses of recipients. Multiple can be added separated by comma.',
type: 'string',
default: '',
},
{
displayName: 'Reply To',
name: 'replyTo',
description: 'Email addresses to use when replying.',
type: 'string',
default: '',
},
],
},
// draft:send
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'draft',
],
operation: [
'send',
],
},
},
options: [
{
displayName: 'Recipients',
name: 'recipients',
description: 'Email addresses of recipients. Mutiple can be set separated by comma.',
type: 'string',
default: '',
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,209 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const draftMessageSharedFields = [
// Get & Get All operations
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'draft',
'message',
],
operation: [
'get',
'getAll',
],
},
},
options: [
{
displayName: 'Attachments Prefix',
name: 'dataPropertyAttachmentsPrefixName',
type: 'string',
default: 'attachment_',
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: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'Fields the response will contain. Multiple can be added separated by comma.',
},
{
displayName: 'Filter',
name: 'filter',
type: 'string',
default: '',
placeholder: 'isRead eq false',
description: 'Microsoft Graph API OData $filter query. Information about the syntax can be found <a href="https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter" target="_blank">here</a>.',
},
],
},
// Update operation
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'draft',
'message',
],
operation: [
'update',
],
},
},
options: [
{
displayName: 'BCC Recipients',
name: 'bccRecipients',
description: 'Email addresses of BCC recipients.',
type: 'string',
default: '',
},
{
displayName: 'Body Content',
name: 'bodyContent',
description: 'Message body content.',
type: 'string',
default: '',
},
{
displayName: 'Body Content Type',
name: 'bodyContentType',
description: 'Message body content type.',
type: 'options',
options: [
{
name: 'HTML',
value: 'html',
},
{
name: 'Text',
value: 'Text',
},
],
default: 'html',
},
{
displayName: 'Categories',
name: 'categories',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getCategories',
},
default: [],
},
{
displayName: 'CC Recipients',
name: 'ccRecipients',
description: 'Email addresses of CC recipients.',
type: 'string',
default: '',
},
{
displayName: 'Custom Headers',
name: 'internetMessageHeaders',
placeholder: 'Add Header',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'headers',
displayName: 'Header',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the header.',
},
],
},
],
},
{
displayName: 'From',
name: 'from',
description: 'The owner of the mailbox which the message is sent.<br>Must correspond to the actual mailbox used.',
type: 'string',
default: '',
},
{
displayName: 'Importance',
name: 'importance',
description: 'The importance of the message.',
type: 'options',
options: [
{
name: 'Low',
value: 'Low',
},
{
name: 'Normal',
value: 'Normal',
},
{
name: 'High',
value: 'High',
},
],
default: 'Low',
},
{
displayName: 'Read Receipt Requested',
name: 'isReadReceiptRequested',
description: 'Indicates whether a read receipt is requested for the message.',
type: 'boolean',
default: false,
},
{
displayName: 'Recipients',
name: 'toRecipients',
description: 'Email addresses of recipients. Multiple can be added separated by comma.',
type: 'string',
default: '',
},
{
displayName: 'Reply To',
name: 'replyTo',
description: 'Email addresses to use when replying.',
type: 'string',
default: '',
},
{
displayName: 'Subject',
name: 'subject',
description: 'The subject of the message.',
type: 'string',
default: '',
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,311 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const folderOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'folder',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new mail folder in the root folder of the user\'s mailbox',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a folder',
},
{
name: 'Get',
value: 'get',
description: 'Get a single folder details',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all folders under the root folder of the signed-in user',
},
{
name: 'Get Children',
value: 'getChildren',
description: 'Lists all child folders under the folder',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const folderFields = [
{
displayName: 'Folder ID',
name: 'folderId',
description: 'Folder ID',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'folder',
],
operation: [
'delete',
'get',
'getChildren',
'update',
],
},
},
},
// folder:list, getChildren, listMessages
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'folder',
],
operation: [
'getAll',
'getChildren',
],
},
},
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: [
'folder',
],
operation: [
'getAll',
'getChildren',
],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
// folder:create
{
displayName: 'Type',
name: 'folderType',
description: 'Folder Type',
type: 'options',
options: [
{
name: 'Folder',
value: 'folder',
},
{
name: 'Search Folder',
value: 'searchFolder',
},
],
displayOptions: {
show: {
resource: [
'folder',
],
operation: [
'create',
],
},
},
default: 'folder',
},
{
displayName: 'Display Name',
name: 'displayName',
description: 'Name of the folder.',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'folder',
],
operation: [
'create',
],
},
},
},
{
displayName: 'Include Nested Folders',
name: 'includeNestedFolders',
description: 'Include child folders in the search.',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'folder',
],
operation: [
'create',
],
folderType: [
'searchFolder',
],
},
},
},
{
displayName: 'Source Folder IDs',
name: 'sourceFolderIds',
description: 'The mailbox folders that should be mined.',
type: 'string',
typeOptions: {
multipleValues: true,
},
default: [],
displayOptions: {
show: {
resource: [
'folder',
],
operation: [
'create',
],
folderType: [
'searchFolder',
],
},
},
},
{
displayName: 'Filter Query',
name: 'filterQuery',
description: 'The OData query to filter the messages.',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'folder',
],
operation: [
'create',
],
folderType: [
'searchFolder',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'folder',
],
operation: [
'get',
'getAll',
'getChildren',
],
},
},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'Fields the response will contain. Multiple can be added separated by ,.',
},
{
displayName: 'Filter',
name: 'filter',
type: 'string',
default: '',
description: 'Microsoft Graph API OData $filter query.',
},
],
},
// folder:update
{
displayName: 'Update Fields',
name: 'updateFields',
description: 'Fields to update.',
type: 'collection',
default: {},
displayOptions: {
show: {
resource: [
'folder',
],
operation: [
'update',
],
},
},
options: [
{
displayName: 'Display Name',
name: 'displayName',
description: 'Name of the folder.',
type: 'string',
default: '',
},
{
displayName: 'Filter Query',
name: 'filterQuery',
description: 'The OData query to filter the messages. Only for search folders.',
type: 'string',
default: '',
},
{
displayName: 'Include Nested Folders',
name: 'includeNestedFolders',
description: 'Include child folders in the search. Only for search folders.',
type: 'boolean',
default: false,
},
{
displayName: 'Source Folder IDs',
name: 'sourceFolderIds',
description: 'The mailbox folders that should be mined. Only for search folders.',
type: 'string',
typeOptions: {
multipleValues: true,
},
default: [],
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,122 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const folderMessageOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'folderMessage',
],
},
},
options: [
{
name: 'Get All',
value: 'getAll',
description: 'Get all the messages in a folder',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const folderMessageFields = [
{
displayName: 'Folder ID',
name: 'folderId',
description: 'Folder ID',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'folderMessage',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'folderMessage',
],
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: [
'folderMessage',
],
operation: [
'getAll',
],
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: [
'folderMessage',
],
operation: [
'getAll',
],
},
},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'Fields the response will contain. Multiple can be added separated by ,.',
},
{
displayName: 'Filter',
name: 'filter',
type: 'string',
default: '',
description: 'Microsoft Graph API OData $filter query.',
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,175 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}, option: IDataObject = { json: true }): Promise<any> { // tslint:disable-line:no-any
const options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
uri: uri || `https://graph.microsoft.com/v1.0/me${resource}`,
};
try {
Object.assign(options, option);
if (Object.keys(headers).length !== 0) {
options.headers = Object.assign({}, options.headers, headers);
}
if (Object.keys(body).length === 0) {
delete options.body;
}
//@ts-ignore
return await this.helpers.requestOAuth2.call(this, 'microsoftOutlookOAuth2Api', options);
} catch (error) {
if (error.response && error.response.body && error.response.body.error && error.response.body.error.message) {
// Try to return the error prettier
throw new Error(`Microsoft error response [${error.statusCode}]: ${error.response.body.error.message}`);
}
throw error;
}
}
export async function microsoftApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, headers: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
let uri: string | undefined;
query['$top'] = 100;
do {
responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri, headers);
uri = responseData['@odata.nextLink'];
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData['@odata.nextLink'] !== undefined
);
return returnData;
}
export async function microsoftApiRequestAllItemsSkip(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}, headers: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query['$top'] = 100;
query['$skip'] = 0;
do {
responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, undefined, headers);
query['$skip'] += query['$top'];
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData['value'].length !== 0
);
return returnData;
}
export function makeRecipient(email: string) {
return {
emailAddress: {
address: email,
},
};
}
export function createMessage(fields: IDataObject) {
const message: IDataObject = {};
// Create body object
if (fields.bodyContent || fields.bodyContentType) {
const bodyObject = {
content: fields.bodyContent,
contentType: fields.bodyContentType,
};
message['body'] = bodyObject;
delete fields['bodyContent'];
delete fields['bodyContentType'];
}
// Handle custom headers
if ('internetMessageHeaders' in fields && 'headers' in (fields.internetMessageHeaders as IDataObject)) {
fields.internetMessageHeaders = (fields.internetMessageHeaders as IDataObject).headers;
}
// Handle recipient fields
['bccRecipients', 'ccRecipients', 'replyTo', 'sender', 'toRecipients'].forEach(key => {
if (Array.isArray(fields[key])) {
fields[key] = (fields[key] as string[]).map(email => makeRecipient(email));
} else if (fields[key] !== undefined) {
fields[key] = (fields[key] as string).split(',').map((recipient: string) => makeRecipient(recipient));
}
});
['from', 'sender'].forEach(key => {
if (fields[key] !== undefined) {
fields[key] = makeRecipient(fields[key] as string);
}
});
Object.assign(message, fields);
return message;
}
export async function downloadAttachments(this: IExecuteFunctions, messages: IDataObject[] | IDataObject, prefix: string) {
const elements: INodeExecutionData[] = [];
if (!Array.isArray(messages)) {
messages = [messages];
}
for (const message of messages) {
const element: INodeExecutionData = {
json: message,
binary: {},
};
if (message.hasAttachments === true) {
const attachments = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
`/messages/${message.id}/attachments`,
{},
);
for (const [index, attachment] of attachments.entries()) {
const response = await microsoftApiRequest.call(
this,
'GET',
`/messages/${message.id}/attachments/${attachment.id}/$value`,
undefined,
{},
undefined,
{},
{ encoding: null, resolveWithFullResponse: true },
);
const data = Buffer.from(response.body as string, 'utf8');
element.binary![`${prefix}${index}`] = await this.helpers.prepareBinaryData(data as unknown as Buffer, attachment.name, attachment.contentType);
}
}
if (Object.keys(element.binary!).length === 0) {
delete element.binary;
}
elements.push(element);
}
return elements;
}

View file

@ -0,0 +1,216 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const messageAttachmentOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'messageAttachment',
],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add an attachment to a message',
},
{
name: 'Download',
value: 'download',
description: 'Download attachment content',
},
{
name: 'Get',
value: 'get',
description: 'Get an attachment from a message',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all the message\'s attachments',
},
],
default: 'add',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const messageAttachmentFields = [
{
displayName: 'Message ID',
name: 'messageId',
description: 'Message ID',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'messageAttachment',
],
operation: [
'add',
'download',
'get',
'getAll',
],
},
},
},
{
displayName: 'Attachment ID',
name: 'attachmentId',
description: 'Attachment ID',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'messageAttachment',
],
operation: [
'download',
'get',
],
},
},
},
// messageAttachment:getAll, messageAttachment:listAttachments
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'messageAttachment',
],
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: [
'messageAttachment',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
// messageAttachment:create, messageAttachment:update, messageAttachment:send
// File operations
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
description: 'Name of the binary property to which to<br />write the data of the read file.',
type: 'string',
required: true,
default: 'data',
displayOptions: {
show: {
resource: [
'messageAttachment',
],
operation: [
'add',
'download',
],
},
},
},
// messageAttachment:add
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'messageAttachment',
],
operation: [
'add',
],
},
},
options: [
{
displayName: 'File Name',
name: 'fileName',
description: 'Filename of the attachment. If not set will the file-name of the binary property be used, if it exists.',
type: 'string',
default: '',
},
],
},
// Get & Get All operations
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'messageAttachment',
],
operation: [
'get',
'getAll',
],
},
},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'string',
default: '',
description: 'Fields the response will contain. Multiple can be added separated by ,.',
},
{
displayName: 'Filter',
name: 'filter',
type: 'string',
default: '',
description: 'Microsoft Graph API OData $filter query.',
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,633 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const messageOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'message',
],
},
},
options: [
{
name: 'Delete',
value: 'delete',
description: 'Delete a message',
},
{
name: 'Get',
value: 'get',
description: 'Get a single message',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all messages in the signed-in user\'s mailbox',
},
{
name: 'Get MIME Content',
value: 'getMime',
description: 'Get MIME content of a message',
},
{
name: 'Reply',
value: 'reply',
description: 'Create reply to a message',
},
{
name: 'Send',
value: 'send',
description: 'Send a message',
},
{
name: 'Update',
value: 'update',
description: 'Update a message',
},
],
default: 'send',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const messageFields = [
{
displayName: 'Message ID',
name: 'messageId',
description: 'Message ID',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'addAttachment',
'delete',
'get',
'getAttachment',
'getMime',
'update',
'reply',
],
},
},
},
// message:reply
{
displayName: 'Reply Type',
name: 'replyType',
type: 'options',
options: [
{
name: 'Reply',
value: 'reply',
},
{
name: 'Reply All',
value: 'replyAll',
},
],
default: 'reply',
required: true,
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'reply',
],
},
},
},
{
displayName: 'Comment',
name: 'comment',
description: 'A comment to include. Can be an empty string.',
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'reply',
],
},
},
type: 'string',
default: '',
},
{
displayName: 'Send',
name: 'send',
description: 'Send the reply message directly. If not set, it will be saved as draft.',
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'reply',
],
},
},
type: 'boolean',
default: true,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'reply',
],
replyType: [
'reply',
],
},
},
options: [
{
displayName: 'Attachments',
name: 'attachments',
type: 'fixedCollection',
placeholder: 'Add Attachment',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'attachments',
displayName: 'Attachment',
values: [
{
displayName: 'Binary Property Name',
name: 'binaryPropertyName',
type: 'string',
default: '',
description: 'Name of the binary property containing the data to be added to the email as an attachment',
},
],
},
],
},
{
displayName: 'BCC Recipients',
name: 'bccRecipients',
description: 'Email addresses of BCC recipients.',
type: 'string',
default: '',
},
{
displayName: 'Body Content',
name: 'bodyContent',
description: 'Message body content.',
type: 'string',
default: '',
},
{
displayName: 'Body Content Type',
name: 'bodyContentType',
description: 'Message body content type.',
type: 'options',
options: [
{
name: 'HTML',
value: 'html',
},
{
name: 'Text',
value: 'Text',
},
],
default: 'html',
},
{
displayName: 'CC Recipients',
name: 'ccRecipients',
description: 'Email addresses of CC recipients.',
type: 'string',
default: '',
},
{
displayName: 'Custom Headers',
name: 'internetMessageHeaders',
placeholder: 'Add Header',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'headers',
displayName: 'Header',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the header.',
},
],
},
],
},
{
displayName: 'From',
name: 'from',
description: 'The owner of the mailbox which the message is sent.<br>Must correspond to the actual mailbox used.',
type: 'string',
default: '',
},
{
displayName: 'Importance',
name: 'importance',
description: 'The importance of the message.',
type: 'options',
options: [
{
name: 'Low',
value: 'Low',
},
{
name: 'Normal',
value: 'Normal',
},
{
name: 'High',
value: 'High',
},
],
default: 'Low',
},
{
displayName: 'Read Receipt Requested',
name: 'isReadReceiptRequested',
description: 'Indicates whether a read receipt is requested for the message.',
type: 'boolean',
default: false,
},
{
displayName: 'Recipients',
name: 'toRecipients',
description: 'Email addresses of recipients. Multiple can be added separated by comma.',
type: 'string',
default: '',
},
{
displayName: 'Reply To',
name: 'replyTo',
description: 'Email addresses to use when replying.',
type: 'string',
default: '',
},
{
displayName: 'Subject',
name: 'subject',
description: 'The subject of the message.',
type: 'string',
default: '',
},
],
},
// message:getAll
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'message',
],
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: [
'message',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 500,
},
default: 100,
description: 'How many results to return.',
},
// message:create, message:update, message:send
{
displayName: 'Subject',
name: 'subject',
description: 'The subject of the message.',
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'create',
'send',
],
},
},
type: 'string',
default: '',
},
{
displayName: 'Body Content',
name: 'bodyContent',
description: 'Message body content.',
type: 'string',
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'create',
'send',
],
},
},
default: '',
},
{
displayName: 'Recipients',
name: 'toRecipients',
description: 'Email addresses of recipients. Multiple can be added separated by comma.',
type: 'string',
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'send',
],
},
},
required: true,
default: '',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'send',
],
},
},
options: [
{
displayName: 'Attachments',
name: 'attachments',
type: 'fixedCollection',
placeholder: 'Add Attachment',
default: {},
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'attachments',
displayName: 'Attachment',
values: [
{
displayName: 'Binary Property Name',
name: 'binaryPropertyName',
type: 'string',
default: '',
description: 'Name of the binary property containing the data to be added to the email as an attachment',
},
],
},
],
},
{
displayName: 'BCC Recipients',
name: 'bccRecipients',
description: 'Email addresses of BCC recipients.',
type: 'string',
default: '',
},
{
displayName: 'Body Content Type',
name: 'bodyContentType',
description: 'Message body content type.',
type: 'options',
options: [
{
name: 'HTML',
value: 'html',
},
{
name: 'Text',
value: 'Text',
},
],
default: 'html',
},
{
displayName: 'Categories',
name: 'categories',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getCategories',
},
default: [],
},
{
displayName: 'CC Recipients',
name: 'ccRecipients',
description: 'Email addresses of CC recipients.',
type: 'string',
default: '',
},
{
displayName: 'Custom Headers',
name: 'internetMessageHeaders',
placeholder: 'Add Header',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'headers',
displayName: 'Header',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'Name of the header.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the header.',
},
],
},
],
},
{
displayName: 'From',
name: 'from',
description: 'The owner of the mailbox which the message is sent.<br>Must correspond to the actual mailbox used.',
type: 'string',
default: '',
},
{
displayName: 'Importance',
name: 'importance',
description: 'The importance of the message.',
type: 'options',
options: [
{
name: 'Low',
value: 'Low',
},
{
name: 'Normal',
value: 'Normal',
},
{
name: 'High',
value: 'High',
},
],
default: 'Low',
},
{
displayName: 'Read Receipt Requested',
name: 'isReadReceiptRequested',
description: 'Indicates whether a read receipt is requested for the message.',
type: 'boolean',
default: false,
},
{
displayName: 'Recipients',
name: 'toRecipients',
description: 'Email addresses of recipients. Multiple can be added separated by comma.',
type: 'string',
default: '',
},
{
displayName: 'Reply To',
name: 'replyTo',
description: 'Email addresses to use when replying.',
type: 'string',
default: '',
},
{
displayName: 'Save To Sent Items',
name: 'saveToSentItems',
description: 'Indicates whether to save the message in Sent Items.',
type: 'boolean',
default: true,
},
],
},
// File operations
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
description: 'Name of the binary property to which to<br />write the data of the read file.',
type: 'string',
required: true,
default: 'data',
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'getMime',
],
},
},
},
// message:move
{
displayName: 'Folder ID',
name: 'folderId',
description: 'Folder ID',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'message',
],
operation: [
'move',
],
},
},
},
] as INodeProperties[];

View file

@ -0,0 +1,961 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IBinaryKeyData,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
createMessage,
downloadAttachments,
makeRecipient,
microsoftApiRequest,
microsoftApiRequestAllItems
} from './GenericFunctions';
import {
draftFields,
draftOperations,
} from './DraftDescription';
import {
draftMessageSharedFields,
} from './DraftMessageSharedDescription';
import {
messageFields,
messageOperations,
} from './MessageDescription';
import {
messageAttachmentFields,
messageAttachmentOperations,
} from './MessageAttachmentDescription';
import {
folderFields,
folderOperations,
} from './FolderDescription';
import {
folderMessageFields,
folderMessageOperations,
} from './FolderMessageDecription';
export class MicrosoftOutlook implements INodeType {
description: INodeTypeDescription = {
displayName: 'Microsoft Outlook',
name: 'microsoftOutlook',
group: ['transform'],
icon: 'file:outlook.svg',
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Microsoft Outlook API',
defaults: {
name: 'Microsoft Outlook',
color: '#3a71b5',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'microsoftOutlookOAuth2Api',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
default: 'message',
options: [
{
name: 'Draft',
value: 'draft',
},
{
name: 'Folder',
value: 'folder',
},
{
name: 'Folder Message',
value: 'folderMessage',
},
{
name: 'Message',
value: 'message',
},
{
name: 'Message Attachment',
value: 'messageAttachment',
},
],
},
// Draft
...draftOperations,
...draftFields,
// Message
...messageOperations,
...messageFields,
// Message Attachment
...messageAttachmentOperations,
...messageAttachmentFields,
// Folder
...folderOperations,
...folderFields,
// Folder Message
...folderMessageOperations,
...folderMessageFields,
// Draft & Message
...draftMessageSharedFields,
],
};
methods = {
loadOptions: {
// Get all the categories to display them to user so that he can
// select them easily
async getCategories(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const categories = await microsoftApiRequestAllItems.call(this, 'value', 'GET', '/outlook/masterCategories');
for (const category of categories) {
returnData.push({
name: category.displayName as string,
value: category.id as string,
});
}
return returnData;
},
},
};
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;
if (['draft', 'message'].includes(resource)) {
if (operation === 'delete') {
for (let i = 0; i < length; i++) {
const messageId = this.getNodeParameter('messageId', i) as string;
responseData = await microsoftApiRequest.call(
this,
'DELETE',
`/messages/${messageId}`,
);
returnData.push({ success: true });
}
}
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const messageId = this.getNodeParameter('messageId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.fields) {
qs['$select'] = additionalFields.fields;
}
if (additionalFields.filter) {
qs['$filter'] = additionalFields.filter;
}
responseData = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}`,
undefined,
qs,
);
if (additionalFields.dataPropertyAttachmentsPrefixName) {
const prefix = additionalFields.dataPropertyAttachmentsPrefixName as string;
const data = await downloadAttachments.call(this, responseData, prefix);
returnData.push.apply(returnData, data as unknown as IDataObject[]);
} else {
returnData.push(responseData);
}
if (additionalFields.dataPropertyAttachmentsPrefixName) {
return [returnData as INodeExecutionData[]];
}
}
}
if (operation === 'update') {
for (let i = 0; i < length; i++) {
const messageId = this.getNodeParameter('messageId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
// Create message from optional fields
const body: IDataObject = createMessage(updateFields);
responseData = await microsoftApiRequest.call(
this,
'PATCH',
`/messages/${messageId}`,
body,
{},
);
returnData.push(responseData);
}
}
}
if (resource === 'draft') {
if (operation === 'create') {
for (let i = 0; i < length; i++) {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const subject = this.getNodeParameter('subject', i) as string;
const bodyContent = this.getNodeParameter('bodyContent', i, '') as string;
additionalFields.subject = subject;
additionalFields.bodyContent = bodyContent || ' ';
// Create message object from optional fields
const body: IDataObject = createMessage(additionalFields);
if (additionalFields.attachments) {
const attachments = (additionalFields.attachments as IDataObject).attachments as IDataObject[];
// // Handle attachments
body['attachments'] = attachments.map(attachment => {
const binaryPropertyName = attachment.binaryPropertyName as string;
if (items[i].binary === undefined) {
throw new Error('No binary data exists on item!');
}
//@ts-ignore
if (items[i].binary[binaryPropertyName] === undefined) {
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`);
}
const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName];
return {
'@odata.type': '#microsoft.graph.fileAttachment',
name: binaryData.fileName,
contentBytes: binaryData.data,
};
});
}
responseData = await microsoftApiRequest.call(
this,
'POST',
`/messages`,
body,
{},
);
returnData.push(responseData);
}
}
if (operation === 'send') {
for (let i = 0; i < length; i++) {
const messageId = this.getNodeParameter('messageId', i);
const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject;
if (additionalFields && additionalFields.recipients) {
const recipients = ((additionalFields.recipients as string).split(',') as string[]).filter(email => !!email);
if (recipients.length !== 0) {
await microsoftApiRequest.call(
this,
'PATCH',
`/messages/${messageId}`,
{ toRecipients: recipients.map((recipient: string) => makeRecipient(recipient)) },
);
}
}
responseData = await microsoftApiRequest.call(
this,
'POST',
`/messages/${messageId}/send`,
);
returnData.push({ success: true });
}
}
}
if (resource === 'message') {
if (operation === 'reply') {
for (let i = 0; i < length; i++) {
const messageId = this.getNodeParameter('messageId', i) as string;
const replyType = this.getNodeParameter('replyType', i) as string;
const comment = this.getNodeParameter('comment', i) as string;
const send = this.getNodeParameter('send', i, false) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i, {}) as IDataObject;
const body: IDataObject = {};
let action = 'createReply';
if (replyType === 'replyAll') {
body.comment = comment;
action = 'createReplyAll';
} else {
body.comment = comment;
body.message = {};
Object.assign(body.message, createMessage(additionalFields));
//@ts-ignore
delete body.message.attachments;
}
responseData = await microsoftApiRequest.call(
this,
'POST',
`/messages/${messageId}/${action}`,
body,
);
if (additionalFields.attachments) {
const attachments = (additionalFields.attachments as IDataObject).attachments as IDataObject[];
// // Handle attachments
const data = attachments.map(attachment => {
const binaryPropertyName = attachment.binaryPropertyName as string;
if (items[i].binary === undefined) {
throw new Error('No binary data exists on item!');
}
//@ts-ignore
if (items[i].binary[binaryPropertyName] === undefined) {
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`);
}
const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName];
return {
'@odata.type': '#microsoft.graph.fileAttachment',
name: binaryData.fileName,
contentBytes: binaryData.data,
};
});
for (const attachment of data) {
await microsoftApiRequest.call(
this,
'POST',
`/messages/${responseData.id}/attachments`,
attachment,
{},
);
}
}
if (send === true) {
await microsoftApiRequest.call(
this,
'POST',
`/messages/${responseData.id}/send`,
);
}
returnData.push(responseData);
}
}
if (operation === 'getMime') {
for (let i = 0; i < length; i++) {
const messageId = this.getNodeParameter('messageId', i) as string;
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
const response = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/$value`,
undefined,
{},
undefined,
{},
{ encoding: null, resolveWithFullResponse: true },
);
let mimeType: string | undefined;
if (response.headers['content-type']) {
mimeType = response.headers['content-type'];
}
const newItem: INodeExecutionData = {
json: items[i].json,
binary: {},
};
if (items[i].binary !== undefined) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary, items[i].binary);
}
items[i] = newItem;
const fileName = `${messageId}.eml`;
const data = Buffer.from(response.body as string, 'utf8');
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, fileName, mimeType);
}
}
if (operation === 'getAll') {
let additionalFields: IDataObject = {};
for (let i = 0; i < length; i++) {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.fields) {
qs['$select'] = additionalFields.fields;
}
if (additionalFields.filter) {
qs['$filter'] = additionalFields.filter;
}
const endpoint = '/messages';
if (returnAll === true) {
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
endpoint,
undefined,
qs,
);
} else {
qs['$top'] = this.getNodeParameter('limit', i) as number;
responseData = await microsoftApiRequest.call(
this,
'GET',
endpoint,
undefined,
qs,
);
responseData = responseData.value;
}
if (additionalFields.dataPropertyAttachmentsPrefixName) {
const prefix = additionalFields.dataPropertyAttachmentsPrefixName as string;
const data = await downloadAttachments.call(this, responseData, prefix);
returnData.push.apply(returnData, data as unknown as IDataObject[]);
} else {
returnData.push.apply(returnData, responseData);
}
}
if (additionalFields.dataPropertyAttachmentsPrefixName) {
return [returnData as INodeExecutionData[]];
}
}
if (operation === 'move') {
for (let i = 0; i < length; i++) {
const messageId = this.getNodeParameter('messageId', i) as string;
const destinationId = this.getNodeParameter('folderId', i) as string;
const body: IDataObject = {
destinationId,
};
responseData = await microsoftApiRequest.call(
this,
'POST',
`/messages/${messageId}/move`,
body,
);
returnData.push({ success: true });
}
}
if (operation === 'send') {
for (let i = 0; i < length; i++) {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const toRecipients = this.getNodeParameter('toRecipients', i) as string;
const subject = this.getNodeParameter('subject', i) as string;
const bodyContent = this.getNodeParameter('bodyContent', i, '') as string;
additionalFields.subject = subject;
additionalFields.bodyContent = bodyContent || ' ';
additionalFields.toRecipients = toRecipients;
const saveToSentItems = additionalFields.saveToSentItems === undefined ? true : additionalFields.saveToSentItems;
delete additionalFields.saveToSentItems;
// Create message object from optional fields
const message: IDataObject = createMessage(additionalFields);
if (additionalFields.attachments) {
const attachments = (additionalFields.attachments as IDataObject).attachments as IDataObject[];
// // Handle attachments
message['attachments'] = attachments.map(attachment => {
const binaryPropertyName = attachment.binaryPropertyName as string;
if (items[i].binary === undefined) {
throw new Error('No binary data exists on item!');
}
//@ts-ignore
if (items[i].binary[binaryPropertyName] === undefined) {
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`);
}
const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName];
return {
'@odata.type': '#microsoft.graph.fileAttachment',
name: binaryData.fileName,
contentBytes: binaryData.data,
};
});
}
const body: IDataObject = {
message,
saveToSentItems,
};
responseData = await microsoftApiRequest.call(
this,
'POST',
`/sendMail`,
body,
{},
);
returnData.push({ success: true });
}
}
}
if (resource === 'messageAttachment') {
if (operation === 'add') {
for (let i = 0; i < length; i++) {
const messageId = this.getNodeParameter('messageId', i) as string;
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (items[i].binary === undefined) {
throw new Error('No binary data exists on item!');
}
//@ts-ignore
if (items[i].binary[binaryPropertyName] === undefined) {
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`);
}
const binaryData = (items[i].binary as IBinaryKeyData)[binaryPropertyName];
const dataBuffer = Buffer.from(binaryData.data, 'base64');
const fileName = additionalFields.fileName === undefined ? binaryData.fileName : additionalFields.fileName;
if (!fileName) {
throw new Error('File name is not set. It has either to be set via "Additional Fields" or has to be set on the binary property!');
}
// Check if the file is over 3MB big
if (dataBuffer.length > 3e6) {
// Maximum chunk size is 4MB
const chunkSize = 4e6;
const body: IDataObject = {
AttachmentItem: {
attachmentType: 'file',
name: fileName,
size: dataBuffer.length,
},
};
// Create upload session
responseData = await microsoftApiRequest.call(
this,
'POST',
`/messages/${messageId}/attachments/createUploadSession`,
body,
);
const uploadUrl = responseData.uploadUrl;
if (uploadUrl === undefined) {
throw new Error('Failed to get upload session');
}
for (let bytesUploaded = 0; bytesUploaded < dataBuffer.length; bytesUploaded += chunkSize) {
// Upload the file chunk by chunk
const nextChunk = Math.min(bytesUploaded + chunkSize, dataBuffer.length);
const contentRange = `bytes ${bytesUploaded}-${nextChunk - 1}/${dataBuffer.length}`;
const data = dataBuffer.subarray(bytesUploaded, nextChunk);
responseData = await this.helpers.request(
uploadUrl,
{
method: 'PUT',
headers: {
'Content-Type': 'application/octet-stream',
'Content-Length': data.length,
'Content-Range': contentRange,
},
body: data,
});
}
} else {
const body: IDataObject = {
'@odata.type': '#microsoft.graph.fileAttachment',
name: fileName,
contentBytes: binaryData.data,
};
responseData = await microsoftApiRequest.call(
this,
'POST',
`/messages/${messageId}/attachments`,
body,
{},
);
}
returnData.push({ success: true });
}
}
if (operation === 'download') {
for (let i = 0; i < length; i++) {
const messageId = this.getNodeParameter('messageId', i) as string;
const attachmentId = this.getNodeParameter('attachmentId', i) as string;
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string;
// Get attachment details first
const attachmentDetails = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/attachments/${attachmentId}`,
undefined,
{ '$select': 'id,name,contentType' },
);
let mimeType: string | undefined;
if (attachmentDetails.contentType) {
mimeType = attachmentDetails.contentType;
}
const fileName = attachmentDetails.name;
const response = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/attachments/${attachmentId}/$value`,
undefined,
{},
undefined,
{},
{ encoding: null, resolveWithFullResponse: true },
);
const newItem: INodeExecutionData = {
json: items[i].json,
binary: {},
};
if (items[i].binary !== undefined) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary, items[i].binary);
}
items[i] = newItem;
const data = Buffer.from(response.body as string, 'utf8');
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(data as unknown as Buffer, fileName, mimeType);
}
}
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const messageId = this.getNodeParameter('messageId', i) as string;
const attachmentId = this.getNodeParameter('attachmentId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
// Have sane defaults so we don't fetch attachment data in this operation
qs['$select'] = 'id,lastModifiedDateTime,name,contentType,size,isInline';
if (additionalFields.fields) {
qs['$select'] = additionalFields.fields;
}
responseData = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/attachments/${attachmentId}`,
undefined,
qs,
);
returnData.push(responseData);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const messageId = this.getNodeParameter('messageId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
// Have sane defaults so we don't fetch attachment data in this operation
qs['$select'] = 'id,lastModifiedDateTime,name,contentType,size,isInline';
if (additionalFields.fields) {
qs['$select'] = additionalFields.fields;
}
if (additionalFields.filter) {
qs['$filter'] = additionalFields.filter;
}
const endpoint = `/messages/${messageId}/attachments`;
if (returnAll === true) {
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
endpoint,
undefined,
qs,
);
} else {
qs['$top'] = this.getNodeParameter('limit', i) as number;
responseData = await microsoftApiRequest.call(
this,
'GET',
endpoint,
undefined,
qs,
);
responseData = responseData.value;
}
returnData.push.apply(returnData, responseData as IDataObject[]);
}
}
}
if (resource === 'folder') {
if (operation === 'create') {
for (let i = 0; i < length; i++) {
const displayName = this.getNodeParameter('displayName', i) as string;
const folderType = this.getNodeParameter('folderType', i) as string;
const body: IDataObject = {
displayName,
};
let endpoint = '/mailFolders';
if (folderType === 'searchFolder') {
endpoint = '/mailFolders/searchfolders/childFolders';
const includeNestedFolders = this.getNodeParameter('includeNestedFolders', i);
const sourceFolderIds = this.getNodeParameter('sourceFolderIds', i);
const filterQuery = this.getNodeParameter('filterQuery', i);
Object.assign(body, {
'@odata.type': 'microsoft.graph.mailSearchFolder',
includeNestedFolders,
sourceFolderIds,
filterQuery,
});
}
responseData = await microsoftApiRequest.call(
this,
'POST',
endpoint,
body,
);
returnData.push(responseData);
}
}
if (operation === 'delete') {
for (let i = 0; i < length; i++) {
const folderId = this.getNodeParameter('folderId', i) as string;
responseData = await microsoftApiRequest.call(
this,
'DELETE',
`/mailFolders/${folderId}`,
);
returnData.push({ success: true });
}
}
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const folderId = this.getNodeParameter('folderId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.fields) {
qs['$select'] = additionalFields.fields;
}
if (additionalFields.filter) {
qs['$filter'] = additionalFields.filter;
}
responseData = await microsoftApiRequest.call(
this,
'GET',
`/mailFolders/${folderId}`,
{},
qs,
);
returnData.push(responseData);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.fields) {
qs['$select'] = additionalFields.fields;
}
if (additionalFields.filter) {
qs['$filter'] = additionalFields.filter;
}
if (returnAll === true) {
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
'/mailFolders',
{},
qs,
);
} else {
qs['$top'] = this.getNodeParameter('limit', i) as number;
responseData = await microsoftApiRequest.call(
this,
'GET',
'/mailFolders',
{},
qs,
);
responseData = responseData.value;
}
returnData.push.apply(returnData, responseData as IDataObject[]);
}
}
if (operation === 'getChildren') {
for (let i = 0; i < length; i++) {
const folderId = this.getNodeParameter('folderId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.fields) {
qs['$select'] = additionalFields.fields;
}
if (additionalFields.filter) {
qs['$filter'] = additionalFields.filter;
}
if (returnAll) {
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
`/mailFolders/${folderId}/childFolders`,
qs,
);
} else {
qs['$top'] = this.getNodeParameter('limit', i) as number;
responseData = await microsoftApiRequest.call(
this,
'GET',
`/mailFolders/${folderId}/childFolders`,
undefined,
qs,
);
responseData = responseData.value;
}
returnData.push.apply(returnData, responseData as IDataObject[]);
}
}
if (operation === 'update') {
for (let i = 0; i < length; i++) {
const folderId = this.getNodeParameter('folderId', i) as string;
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
const body: IDataObject = {
...updateFields,
};
responseData = await microsoftApiRequest.call(
this,
'PATCH',
`/mailFolders/${folderId}`,
body,
);
returnData.push(responseData);
}
}
}
if (resource === 'folderMessage') {
for (let i = 0; i < length; i++) {
if (operation === 'getAll') {
const folderId = this.getNodeParameter('folderId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
if (additionalFields.fields) {
qs['$select'] = additionalFields.fields;
}
const endpoint = `/mailFolders/${folderId}/messages`;
if (returnAll) {
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
endpoint,
qs,
);
}
else {
qs['$top'] = this.getNodeParameter('limit', i) as number;
responseData = await microsoftApiRequest.call(
this,
'GET',
endpoint,
undefined,
qs,
);
responseData = responseData.value;
}
returnData.push.apply(returnData, responseData as IDataObject[]);
}
}
}
if ((resource === 'message' && operation === 'getMime') || (resource === 'messageAttachment' && operation === 'download')) {
return this.prepareOutputData(items);
} else {
return [this.helpers.returnJsonArray(returnData)];
}
}
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 81 81" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round"><use xlink:href="#A" x=".5" y=".5"/><symbol id="A" overflow="visible"><path d="M77.9 19.2H46.86V0L0 8.232V71.98L46.86 80V61.847H77.9c1.266 0 2.1-1.055 2.1-2.1V21.32c0-1.266-.844-2.1-2.1-2.1zM21.952 54.46c-15.4-1.055-14.776-29.13.422-29.974 16.675-.844 16.253 31.03-.422 29.974zm55.515 4.644H46.86V35.04l9.5 9.077c.422.422.844.633 1.477.633s1.055-.2 1.478-.633L77.467 27.23v31.873zm0-35.462l-19.63 18.153L46.86 31.45v-9.7h30.396v1.9h.2zm-55.092 6.543c-8.232.422-8.443 18.364-.2 18.575 8.654.422 8.654-18.997.2-18.575z" fill="#2372ba" stroke="none"/></symbol></svg>

After

Width:  |  Height:  |  Size: 759 B

View file

@ -0,0 +1,71 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IHookFunctions,
} from 'n8n-core';
import {
IDataObject,
} from 'n8n-workflow';
export async function nasaApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, qs: IDataObject, option: IDataObject = {}, uri?: string | undefined): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('nasaApi') as IDataObject;
qs.api_key = credentials['api_key'] as string;
const options: OptionsWithUri = {
method,
qs,
uri: uri || `https://api.nasa.gov${endpoint}`,
json: true,
};
if (Object.keys(option)) {
Object.assign(options, option);
}
try {
return await this.helpers.request(options);
} catch (error) {
if (error.statusCode === 401) {
// Return a clear error
throw new Error('The NASA credentials are not valid!');
}
if (error.response && error.response.body && error.response.body.msg) {
// Try to return the error prettier
throw new Error(`NASA error response [${error.statusCode}]: ${error.response.body.msg}`);
}
// If that data does not exist for some reason return the actual error
throw error;
}
}
export async function nasaApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, method: string, resource: string, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.size = 20;
let uri: string | undefined = undefined;
do {
responseData = await nasaApiRequest.call(this, method, resource, query, {}, uri);
uri = responseData.links.next;
returnData.push.apply(returnData, responseData[propertyName]);
} while (
responseData.links.next !== undefined
);
return returnData;
}

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,50 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
IHookFunctions,
} from 'n8n-workflow';
export async function pushcutApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, uri?: string | undefined, option = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('pushcutApi') as IDataObject;
const options: OptionsWithUri = {
headers: {
'API-Key': credentials.apiKey,
},
method,
body,
qs,
uri: uri || `https://api.pushcut.io/v1${path}`,
json: true,
};
try {
if (Object.keys(body).length === 0) {
delete options.body;
}
if (Object.keys(option).length !== 0) {
Object.assign(options, option);
}
//@ts-ignore
return await this.helpers.request.call(this, options);
} catch (error) {
if (error.response && error.response.body && error.response.body.error) {
const message = error.response.body.error;
// Try to return the error prettier
throw new Error(
`Pushcut error response [${error.statusCode}]: ${message}`,
);
}
throw error;
}
}

View file

@ -0,0 +1,215 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
pushcutApiRequest,
} from './GenericFunctions';
export class Pushcut implements INodeType {
description: INodeTypeDescription = {
displayName: 'Pushcut',
name: 'pushcut',
icon: 'file:pushcut.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Pushcut API.',
defaults: {
name: 'Pushcut',
color: '#1f2957',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'pushcutApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Notification',
value: 'notification',
},
],
default: 'notification',
description: 'The resource to operate on.',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'notification',
],
},
},
options: [
{
name: 'Send',
value: 'send',
description: 'Send a notification',
},
],
default: 'send',
description: 'The resource to operate on.',
},
{
displayName: 'Notification Name',
name: 'notificationName',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getNotifications',
},
displayOptions: {
show: {
resource: [
'notification',
],
operation: [
'send',
],
},
},
default: '',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'send',
],
resource: [
'notification',
],
},
},
default: {},
options: [
{
displayName: 'Devices',
name: 'devices',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getDevices',
},
default: '',
description: 'List of devices this notification is sent to. (default is all devices)',
},
{
displayName: 'Input',
name: 'input',
type: 'string',
default: '',
description: 'Value that is passed as input to the notification action.',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
default: '',
description: 'Text that is used instead of the one defined in the app.',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'Title that is used instead of the one defined in the app.',
},
],
},
],
};
methods = {
loadOptions: {
// Get all the available devices to display them to user so that he can
// select them easily
async getDevices(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const devices = await pushcutApiRequest.call(this, 'GET', '/devices');
for (const device of devices) {
returnData.push({
name: device.id,
value: device.id,
});
}
return returnData;
},
// Get all the available notifications to display them to user so that he can
// select them easily
async getNotifications(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const notifications = await pushcutApiRequest.call(this, 'GET', '/notifications');
for (const notification of notifications) {
returnData.push({
name: notification.title,
value: notification.id,
});
}
return returnData;
},
},
};
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 === 'notification') {
if (operation === 'send') {
const notificationName = this.getNodeParameter('notificationName', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {};
Object.assign(body, additionalFields);
responseData = await pushcutApiRequest.call(
this,
'POST',
`/notifications/${encodeURI(notificationName)}`,
body,
);
}
}
}
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,130 @@
import {
IHookFunctions,
IWebhookFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
} from 'n8n-workflow';
import {
pushcutApiRequest,
} from './GenericFunctions';
export class PushcutTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Pushcut Trigger',
name: 'pushcutTrigger',
icon: 'file:pushcut.png',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when a Pushcut events occurs.',
defaults: {
name: 'Pushcut Trigger',
color: '#1f2957',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'pushcutApi',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Action Name',
name: 'actionName',
type: 'string',
description: 'Choose any name you would like. It will show up as a server action in the app',
default: '',
},
],
};
// @ts-ignore (because of request)
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookData = this.getWorkflowStaticData('node');
const actionName = this.getNodeParameter('actionName');
// Check all the webhooks which exist already if it is identical to the
// one that is supposed to get created.
const endpoint = '/subscriptions';
const webhooks = await pushcutApiRequest.call(this, 'GET', endpoint, {});
for (const webhook of webhooks) {
if (webhook.url === webhookUrl &&
webhook.actionName === actionName) {
webhookData.webhookId = webhook.id;
return true;
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default');
const actionName = this.getNodeParameter('actionName');
const endpoint = '/subscriptions';
const body = {
actionName,
url: webhookUrl,
};
const responseData = await pushcutApiRequest.call(this, 'POST', endpoint, body);
if (responseData.id === undefined) {
// Required data is missing so was not successful
return false;
}
webhookData.webhookId = responseData.id as string;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
const endpoint = `/subscriptions/${webhookData.webhookId}`;
try {
await pushcutApiRequest.call(this, 'DELETE', endpoint);
} catch (e) {
return false;
}
// Remove from the static workflow data so that it is clear
// that no webhooks are registred anymore
delete webhookData.webhookId;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const body = this.getBodyData() as IDataObject;
return {
workflowData: [
this.helpers.returnJsonArray(body),
],
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,60 @@
import {
INodeProperties,
INodePropertyCollection,
INodePropertyOptions,
} from 'n8n-workflow';
export const rabbitDefaultOptions: Array<INodePropertyOptions | INodeProperties | INodePropertyCollection> = [
{
displayName: 'Arguments',
name: 'arguments',
placeholder: 'Add Argument',
description: 'Arguments to add.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'argument',
displayName: 'Argument',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Auto Delete',
name: 'autoDelete',
type: 'boolean',
default: false,
description: 'The queue will be deleted when the number of consumers drops to zero .',
},
{
displayName: 'Durable',
name: 'durable',
type: 'boolean',
default: true,
description: 'The queue will survive broker restarts.',
},
{
displayName: 'Exclusive',
name: 'exclusive',
type: 'boolean',
default: false,
description: 'Scopes the queue to the connection.',
},
];

View file

@ -0,0 +1,62 @@
import {
IDataObject,
IExecuteFunctions,
ITriggerFunctions,
} from 'n8n-workflow';
const amqplib = require('amqplib');
export async function rabbitmqConnect(this: IExecuteFunctions | ITriggerFunctions, queue: string, options: IDataObject): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('rabbitmq') as IDataObject;
const credentialKeys = [
'hostname',
'port',
'username',
'password',
'vhost',
];
const credentialData: IDataObject = {};
credentialKeys.forEach(key => {
credentialData[key] = credentials[key] === '' ? undefined : credentials[key];
});
const optsData: IDataObject = {};
if (credentials.ssl === true) {
credentialData.protocol = 'amqps';
optsData.cert = credentials.cert === '' ? undefined : Buffer.from(credentials.cert as string);
optsData.key = credentials.key === '' ? undefined : Buffer.from(credentials.key as string);
optsData.passphrase = credentials.passphrase === '' ? undefined : credentials.passphrase;
optsData.ca = credentials.ca === '' ? undefined : [Buffer.from(credentials.ca as string)];
optsData.credentials = amqplib.credentials.external();
}
return new Promise(async (resolve, reject) => {
try {
const connection = await amqplib.connect(credentialData, optsData);
connection.on('error', (error: Error) => {
reject(error);
});
const channel = await connection.createChannel().catch(console.warn);
if (options.arguments && ((options.arguments as IDataObject).argument! as IDataObject[]).length) {
const additionalArguments: IDataObject = {};
((options.arguments as IDataObject).argument as IDataObject[]).forEach((argument: IDataObject) => {
additionalArguments[argument.key as string] = argument.value;
});
options.arguments = additionalArguments;
}
await channel.assertQueue(queue, options);
resolve(channel);
} catch (error) {
reject(error);
}
});
}

View file

@ -0,0 +1,147 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
rabbitDefaultOptions,
} from './DefaultOptions';
import {
rabbitmqConnect,
} from './GenericFunctions';
export class RabbitMQ implements INodeType {
description: INodeTypeDescription = {
displayName: 'RabbitMQ',
name: 'rabbitmq',
icon: 'file:rabbitmq.png',
group: ['transform'],
version: 1,
description: 'Sends messages to a RabbitMQ topic',
defaults: {
name: 'RabbitMQ',
color: '#ff6600',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'rabbitmq',
required: true,
},
],
properties: [
{
displayName: 'Queue / Topic',
name: 'queue',
type: 'string',
default: '',
placeholder: 'queue-name',
description: 'Name of the queue to publish to.',
},
{
displayName: 'Send Input Data',
name: 'sendInputData',
type: 'boolean',
default: true,
description: 'Send the the data the node receives as JSON to Kafka.',
},
{
displayName: 'Message',
name: 'message',
type: 'string',
displayOptions: {
show: {
sendInputData: [
false,
],
},
},
default: '',
description: 'The message to be sent.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
default: {},
placeholder: 'Add Option',
options: rabbitDefaultOptions,
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
let channel;
try {
const items = this.getInputData();
const queue = this.getNodeParameter('queue', 0) as string;
const options = this.getNodeParameter('options', 0, {}) as IDataObject;
channel = await rabbitmqConnect.call(this, queue, options);
const sendInputData = this.getNodeParameter('sendInputData', 0) as boolean;
let message: string;
const queuePromises = [];
for (let i = 0; i < items.length; i++) {
if (sendInputData === true) {
message = JSON.stringify(items[i].json);
} else {
message = this.getNodeParameter('message', i) as string;
}
queuePromises.push(channel.sendToQueue(queue, Buffer.from(message)));
}
// @ts-ignore
const promisesResponses = await Promise.allSettled(queuePromises);
const returnItems: INodeExecutionData[] = [];
promisesResponses.forEach((response: IDataObject) => {
if (response!.status !== 'fulfilled') {
if (this.continueOnFail() !== true) {
throw new Error(response!.reason as string);
} else {
// Return the actual reason as error
returnItems.push(
{
json: {
error: response.reason,
},
},
);
return;
}
}
returnItems.push({
json: {
success: response.value,
},
});
});
await channel.close();
return this.prepareOutputData(returnItems);
} catch (error) {
if (channel) {
await channel.close();
}
throw error;
}
}
}

View file

@ -0,0 +1,172 @@
import {
IDataObject,
INodeExecutionData,
INodeProperties,
INodeType,
INodeTypeDescription,
ITriggerFunctions,
ITriggerResponse,
} from 'n8n-workflow';
import {
rabbitDefaultOptions,
} from './DefaultOptions';
import {
rabbitmqConnect,
} from './GenericFunctions';
export class RabbitMQTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'RabbitMQ Trigger',
name: 'rabbitmqTrigger',
icon: 'file:rabbitmq.png',
group: ['trigger'],
version: 1,
description: 'Listens to RabbitMQ messages',
defaults: {
name: 'RabbitMQ',
color: '#ff6600',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'rabbitmq',
required: true,
},
],
properties: [
{
displayName: 'Queue / Topic',
name: 'queue',
type: 'string',
default: '',
placeholder: 'queue-name',
description: 'Name of the queue to publish to.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
default: {},
placeholder: 'Add Option',
options: [
{
displayName: 'Content is Binary',
name: 'contentIsBinary',
type: 'boolean',
default: false,
description: 'Saves the content as binary.',
},
{
displayName: 'JSON Parse Body',
name: 'jsonParseBody',
type: 'boolean',
displayOptions: {
hide: {
contentIsBinary: [
true,
],
},
},
default: false,
description: 'Parse the body to an object.',
},
{
displayName: 'Only Content',
name: 'onlyContent',
type: 'boolean',
displayOptions: {
hide: {
contentIsBinary: [
true,
],
},
},
default: false,
description: 'Returns only the content property.',
},
...rabbitDefaultOptions,
].sort((a, b) => {
if ((a as INodeProperties).displayName.toLowerCase() < (b as INodeProperties).displayName.toLowerCase()) { return -1; }
if ((a as INodeProperties).displayName.toLowerCase() > (b as INodeProperties).displayName.toLowerCase()) { return 1; }
return 0;
}) as INodeProperties[],
},
],
};
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
const queue = this.getNodeParameter('queue') as string;
const options = this.getNodeParameter('options', {}) as IDataObject;
const channel = await rabbitmqConnect.call(this, queue, options);
const self = this;
const item: INodeExecutionData = {
json: {},
};
const startConsumer = async () => {
await channel.consume(queue, async (message: IDataObject) => {
if (message !== null) {
let content: IDataObject | string = message!.content!.toString();
if (options.contentIsBinary === true) {
item.binary = {
data: await this.helpers.prepareBinaryData(message.content),
};
item.json = message;
message.content = undefined;
} else {
if (options.jsonParseBody === true) {
content = JSON.parse(content as string);
}
if (options.onlyContent === true) {
item.json = content as IDataObject;
} else {
message.content = content;
item.json = message;
}
}
self.emit([
[
item,
],
]);
channel.ack(message);
}
});
};
startConsumer();
// The "closeFunction" function gets called by n8n whenever
// the workflow gets deactivated and can so clean up.
async function closeFunction() {
await channel.close();
}
// The "manualTriggerFunction" function gets called by n8n
// when a user is in the workflow editor and starts the
// workflow manually. So the function has to make sure that
// the emit() gets called with similar data like when it
// would trigger by itself so that the user knows what data
// to expect.
async function manualTriggerFunction() {
startConsumer();
}
return {
closeFunction,
manualTriggerFunction,
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

View file

@ -46,7 +46,7 @@ export class RssFeedRead implements INodeType {
const parser = new Parser();
let feed: Parser.Output;
let feed: Parser.Output<IDataObject>;
try {
feed = await parser.parseURL(url);
} catch (e) {

View file

@ -105,11 +105,11 @@ export class S3 implements INodeType {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const qs: IDataObject = {};
const headers: 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++) {
const headers: IDataObject = {};
if (resource === 'bucket') {
//https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html
if (operation === 'create') {

View file

@ -1,4 +1,6 @@
import { INodeProperties } from 'n8n-workflow';
import {
INodeProperties,
} from 'n8n-workflow';
export const channelOperations = [
{
@ -63,6 +65,11 @@ export const channelOperations = [
value: 'leave',
description: 'Leaves a conversation.',
},
{
name: 'Member',
value: 'member',
description: 'List members of a conversation.',
},
{
name: 'Open',
value: 'open',
@ -101,9 +108,9 @@ export const channelOperations = [
export const channelFields = [
/* -------------------------------------------------------------------------- */
/* channel:archive */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:archive */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -125,9 +132,10 @@ export const channelFields = [
required: true,
description: 'The name of the channel to archive.',
},
/* -------------------------------------------------------------------------- */
/* channel:close */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:close */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -149,9 +157,10 @@ export const channelFields = [
required: true,
description: 'The name of the channel to close.',
},
/* -------------------------------------------------------------------------- */
/* channel:create */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -197,9 +206,10 @@ export const channelFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:invite */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:invite */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -242,9 +252,10 @@ export const channelFields = [
required: true,
description: 'The ID of the user to invite into channel.',
},
/* -------------------------------------------------------------------------- */
/* channel:get */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -288,9 +299,10 @@ export const channelFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:kick */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:kick */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -332,9 +344,10 @@ export const channelFields = [
},
default: '',
},
/* -------------------------------------------------------------------------- */
/* channel:join */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:join */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -356,9 +369,10 @@ export const channelFields = [
},
required: true,
},
/* -------------------------------------------------------------------------- */
/* channel:getAll */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
@ -451,9 +465,10 @@ export const channelFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:history */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:history */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -557,9 +572,10 @@ export const channelFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:leave */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:leave */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -581,9 +597,89 @@ export const channelFields = [
required: true,
description: 'The name of the channel to leave.',
},
/* -------------------------------------------------------------------------- */
/* channel:open */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:member */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChannels',
},
default: '',
displayOptions: {
show: {
operation: [
'member',
],
resource: [
'channel',
],
},
},
required: true,
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'channel',
],
operation: [
'member',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 100,
placeholder: 'Limit',
displayOptions: {
show: {
operation: [
'member',
],
resource: [
'channel',
],
returnAll: [
false,
],
},
},
required: false,
},
{
displayName: 'Resolve Data',
name: 'resolveData',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: [
'channel',
],
operation: [
'member',
],
},
},
description: 'By default the response only contain the ID to resource. If this<br />option gets activated it will resolve the data automatically.',
},
/* -------------------------------------------------------------------------- */
/* channel:open */
/* -------------------------------------------------------------------------- */
{
displayName: 'Options',
name: 'options',
@ -627,9 +723,10 @@ export const channelFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:rename */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:rename */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -669,9 +766,10 @@ export const channelFields = [
required: true,
description: 'New name for conversation.',
},
/* -------------------------------------------------------------------------- */
/* channel:replies */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:replies */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -793,9 +891,10 @@ export const channelFields = [
},
],
},
/* -------------------------------------------------------------------------- */
/* channel:setPurpose */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:setPurpose */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -835,9 +934,10 @@ export const channelFields = [
required: true,
description: 'A new, specialer purpose',
},
/* -------------------------------------------------------------------------- */
/* channel:setTopic */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:setTopic */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',
@ -877,9 +977,10 @@ export const channelFields = [
required: true,
description: 'The new topic string. Does not support formatting or linkification.',
},
/* -------------------------------------------------------------------------- */
/* channel:unarchive */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* channel:unarchive */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',

View file

@ -36,9 +36,9 @@ export const fileOperations = [
export const fileFields = [
/* -------------------------------------------------------------------------- */
/* file:upload */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* file:upload */
/* -------------------------------------------------------------------------- */
{
displayName: 'Binary Data',
name: 'binaryData',
@ -159,9 +159,10 @@ export const fileFields = [
},
],
},
/* ----------------------------------------------------------------------- */
/* file:getAll */
/* ----------------------------------------------------------------------- */
/* ----------------------------------------------------------------------- */
/* file:getAll */
/* ----------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
@ -261,29 +262,29 @@ export const fileFields = [
value: 'all',
},
{
name: 'Spaces',
value: 'spaces',
},
{
name: 'Snippets',
value: 'snippets',
name: 'Google Docs',
value: 'gdocs',
},
{
name: 'Images',
value: 'images',
},
{
name: 'Google Docs',
value: 'gdocs',
name: 'Snippets',
value: 'snippets',
},
{
name: 'Zips',
value: 'zips',
name: 'Spaces',
value: 'spaces',
},
{
name: 'pdfs',
value: 'pdfs',
},
{
name: 'Zips',
value: 'zips',
},
],
default: ['all'],
description: 'Filter files by type',
@ -300,9 +301,10 @@ export const fileFields = [
},
],
},
/* ----------------------------------------------------------------------- */
/* file:get */
/* ----------------------------------------------------------------------- */
/* ----------------------------------------------------------------------- */
/* file:get */
/* ----------------------------------------------------------------------- */
{
displayName: 'File ID',
name: 'fileId',

View file

@ -76,7 +76,7 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu
}
}
export async function slackApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string ,method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
export async function slackApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
query.page = 1;
@ -88,13 +88,13 @@ export async function slackApiRequestAllItems(this: IExecuteFunctions | ILoadOpt
returnData.push.apply(returnData, responseData[propertyName]);
} while (
(responseData.response_metadata !== undefined &&
responseData.response_metadata.mext_cursor !== undefined &&
responseData.response_metadata.next_cursor !== '' &&
responseData.response_metadata.next_cursor !== null) ||
responseData.response_metadata.mext_cursor !== undefined &&
responseData.response_metadata.next_cursor !== '' &&
responseData.response_metadata.next_cursor !== null) ||
(responseData.paging !== undefined &&
responseData.paging.pages !== undefined &&
responseData.paging.page !== undefined &&
responseData.paging.page < responseData.paging.pages
responseData.paging.pages !== undefined &&
responseData.paging.page !== undefined &&
responseData.paging.page < responseData.paging.pages
)
);

View file

@ -31,9 +31,9 @@ export const messageOperations = [
export const messageFields = [
/* -------------------------------------------------------------------------- */
/* message:post */
/* -------------------------------------------------------------------------- */
/* -------------------------------------------------------------------------- */
/* message:post */
/* -------------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channel',
@ -382,6 +382,13 @@ export const messageFields = [
default: '',
description: 'URL to an image to use as the icon for this message.',
},
{
displayName: 'Link Names',
name: 'link_names',
type: 'boolean',
default: false,
description: 'Find and link channel names and usernames.',
},
{
displayName: 'Make Reply',
name: 'thread_ts',
@ -389,20 +396,6 @@ export const messageFields = [
default: '',
description: 'Provide another message\'s ts value to make this message a reply.',
},
{
displayName: 'Unfurl Links',
name: 'unfurl_links',
type: 'boolean',
default: false,
description: 'Pass true to enable unfurling of primarily text-based content.',
},
{
displayName: 'Unfurl Media',
name: 'unfurl_media',
type: 'boolean',
default: true,
description: 'Pass false to disable unfurling of media content.',
},
{
displayName: 'Markdown',
name: 'mrkdwn',
@ -418,17 +411,25 @@ export const messageFields = [
description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.',
},
{
displayName: 'Link Names',
name: 'link_names',
displayName: 'Unfurl Links',
name: 'unfurl_links',
type: 'boolean',
default: false,
description: 'Find and link channel names and usernames.',
description: 'Pass true to enable unfurling of primarily text-based content.',
},
{
displayName: 'Unfurl Media',
name: 'unfurl_media',
type: 'boolean',
default: true,
description: 'Pass false to disable unfurling of media content.',
},
],
},
/* ----------------------------------------------------------------------- */
/* message:update */
/* ----------------------------------------------------------------------- */
/* ----------------------------------------------------------------------- */
/* message:update */
/* ----------------------------------------------------------------------- */
{
displayName: 'Channel',
name: 'channelId',

Some files were not shown because too many files have changed in this diff Show more