Merge branch 'master' into expand-zoho-node

This commit is contained in:
ricardo 2021-05-28 17:23:51 -04:00
commit 405ff8d75a
130 changed files with 5620 additions and 266 deletions

View file

@ -22,8 +22,10 @@ A clear and concise description of what you expected to happen.
**Environment (please complete the following information):** **Environment (please complete the following information):**
- OS: [e.g. Ubuntu Linux 18.04] - OS: [e.g. Ubuntu Linux 18.04]
- n8n Version [e.g. 0.26.0] - n8n Version [e.g. 0.119.0]
- Node.js Version [e.g. 10.16.0] - Node.js Version [e.g. 14.16.0]
- Database system [e.g. SQLite; n8n uses SQLite as default otherwise changed]
- Operation mode [e.g. own; operation modes are `own`, `main` and `queue`. Default is `own`]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

1
.gitignore vendored
View file

@ -13,3 +13,4 @@ _START_PACKAGE
.vscode .vscode
.idea .idea
.prettierrc.js .prettierrc.js
vetur.config.js

View file

@ -63,9 +63,7 @@ If you have problems or questions go to our forum, we will then try to help you
## Jobs ## Jobs
If you are interested in working for n8n and so shape the future of the project If you are interested in working for n8n and so shape the future of the project
check out our job posts: check out our [job posts](https://apply.workable.com/n8n/)
[https://n8n.join.com](https://n8n.join.com)

View file

@ -49,6 +49,7 @@ Additional information and example workflows on the n8n.io website: [https://n8n
docker run -it --rm \ docker run -it --rm \
--name n8n \ --name n8n \
-p 5678:5678 \ -p 5678:5678 \
-v ~/.n8n:/home/node/.n8n \
n8nio/n8n n8nio/n8n
``` ```
@ -262,9 +263,7 @@ If you have problems or questions go to our forum, we will then try to help you
## Jobs ## Jobs
If you are interested in working for n8n and so shape the future of the project If you are interested in working for n8n and so shape the future of the project
check out our job posts: check out our [job posts](https://apply.workable.com/n8n/)
[https://n8n.join.com](https://n8n.join.com)

View file

@ -2,7 +2,7 @@
![n8n.io - Workflow Automation](https://raw.githubusercontent.com/n8n-io/n8n/master/assets/n8n-logo.png) ![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) distributed 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. You can self-host n8n, easily extend it, and even use it 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> <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>
@ -11,100 +11,145 @@ n8n is a free and open [fair-code](http://faircode.io) distributed node based Wo
<!-- TOC --> <!-- TOC -->
- [Demo](#demo) - [Demo](#demo)
- [Getting Started](#getting-started)
- [Use npx](#use-npx)
- [Run with Docker](#run-with-docker)
- [Install with npm](#install-with-npm)
- [Sign-up on n8n.cloud](#sign-up-on-n8n.cloud)
- [Available integrations](#available-integrations) - [Available integrations](#available-integrations)
- [Documentation](#documentation) - [Documentation](#documentation)
- [Create Custom Nodes](#create-custom-nodes) - [Create Custom Nodes](#create-custom-nodes)
- [Hosted n8n](#hosted-n8n) - [Contributing](#contributing)
- [What does n8n mean and how do you pronounce it](#what-does-n8n-mean-and-how-do-you-pronounce-it) - [What does n8n mean and how do you pronounce it](#what-does-n8n-mean-and-how-do-you-pronounce-it)
- [Support](#support) - [Support](#support)
- [Jobs](#jobs) - [Jobs](#jobs)
- [Upgrading](#upgrading) - [Upgrading](#upgrading)
- [License](#license) - [License](#license)
- [Development](#development)
<!-- /TOC --> <!-- /TOC -->
## Demo ## Demo
[:tv: A short demo (< 3 min)](https://www.youtube.com/watch?v=3w7xIMKLVAg) 📺 Here's a [short demo (<3 min)](https://www.youtube.com/watch?v=3w7xIMKLVAg) that shows how to create a simple workflow to automatically sends a notification on Slack every time a GitHub repository gets starred or un-starred.
which shows how to create a simple workflow which automatically sends a new
Slack notification every time a Github repository received or lost a star.
## Getting Started
There are a couple of ways to get started with n8n.
### Use npx
To spin up n8n using npx, you can run:
```bash
npx n8n
```
It will download everything that is needed to start n8n.
You can then access n8n by opening:
[http://localhost:5678](http://localhost:5678)
**Note:** The minimum required version for Node.js is v14.15. Make sure to update Node.js to v14.15 or above.
### Run with Docker
To play around with n8n, you can also start it using Docker:
```bash
docker run -it --rm \
--name n8n \
-p 5678:5678 \
n8nio/n8n
```
Be aware that all the data will be lost once the Docker container gets removed. To persist the data mount the `~/.n8n` folder:
```bash
docker run -it --rm \
--name n8n \
-p 5678:5678 \
-v ~/.n8n:/home/node/.n8n \
n8nio/n8n
```
n8n also offers a Docker image for Raspberry Pi: `n8nio/n8n:latest-rpi`.
Refer to the [documentation](https://github.com/n8n-io/n8n/blob/master/docker/images/n8n/README.md) for more information on the Docker setup.
### Install with npm
To install n8n globally using npm:
```bash
npm install n8n -g
```
After the installation, start n8n running the following command:
```bash
n8n
# or
n8n start
```
### Sign-up on n8n.cloud
Sign-up for an [n8n.cloud](https://www.n8n.cloud/) account.
While n8n.cloud and n8n are the same in terms of features, n8n.cloud provides certain conveniences such as:
- Not having to set up and maintain your n8n instance
- Managed OAuth for authentication
- Easily upgrading to the newer n8n versions
## Available integrations ## Available integrations
n8n has 200+ different nodes to automate workflows. The list can be found on: [https://n8n.io/nodes](https://n8n.io/nodes) n8n has 280+ different nodes that allow you to connect various services and build your automation workflows. You can find the list of all the integrations at [https://n8n.io/integrations](https://n8n.io/integrations)
## Documentation ## Documentation
The official n8n documentation can be found under: [https://docs.n8n.io](https://docs.n8n.io) To learn more about n8n, refer to the official documentation here: [https://docs.n8n.io](https://docs.n8n.io)
Additional information and example workflows on the n8n.io website: [https://n8n.io](https://n8n.io) You can find additional information and example workflows on the [n8n.io](https://n8n.io) website.
## Create Custom Nodes ## Create Custom Nodes
It is very easy to create own nodes for n8n. More information about that can You can create custom nodes for n8n. Follow the instructions mentioned in the documentation to create your node: [Creating nodes](https://docs.n8n.io/nodes/creating-nodes/create-node.html)
be found in the documentation of "n8n-node-dev" which is a small CLI which
helps with n8n-node-development.
[To n8n-node-dev](https://github.com/n8n-io/n8n/tree/master/packages/node-dev) ## Contributing
Additional information can be found on the [ documentation page](https://docs.n8n.io/#/create-node). 🐛 Did you find a bug?
✨ Do you want to contribute a feature?
## Hosted n8n The [CONTRIBUTING guide](https://github.com/n8n-io/n8n/blob/master/CONTRIBUTING.md) will help you set up your development environment.
If you are interested in a hosted version of n8n on our infrastructure please contact us via: You can find more information on how you can contribute to the project on our documentation: [How can I contribute?](https://docs.n8n.io/reference/contributing.html)
[hosting@n8n.io](mailto:hosting@n8n.io)
## What does n8n mean, and how do you pronounce it?
**Short answer:** n8n is an abbreviation for "nodemation", and it is pronounced as n-eight-n.
## What does n8n mean and how do you pronounce it? **Long answer:** In n8n, you build your automation ("-mation") workflows by connecting different nodes in the Editor UI. The project is also built using Node.js. As a consequence, the project was named nodemation.
**Short answer:** It means "nodemation" and it is pronounced as n-eight-n.
**Long answer:** I get that question quite often (more often than I expected)
so I decided it is probably best to answer it here. While looking for a
good name for the project with a free domain I realized very quickly that all the
good ones I could think of were already taken. So, in the end, I chose
nodemation. "node-" in the sense that it uses a Node-View and that it uses
Node.js and "-mation" for "automation" which is what the project is supposed to help with.
However, I did not like how long the name was and I could not imagine writing
something that long every time in the CLI. That is when I then ended up on
"n8n". Sure does not work perfectly but does neither for Kubernetes (k8s) and
did not hear anybody complain there. So I guess it should be ok.
However, the name was long, and it wouldn't be a good idea to use such a long name in the CLI. Hence, nodemation got abbreviated as "n8n" (there are eight characters between the first and the last n!).
## Support ## Support
If you have problems or questions go to our forum, we will then try to help you asap: If you run into issues or have any questions reach out to us via our community forum: [https://community.n8n.io](https://community.n8n.io).
[https://community.n8n.io](https://community.n8n.io)
## Jobs ## Jobs
If you are interested in working for n8n and so shape the future of the project If you are interested in working at n8n and building the project, check out the [job openings](https://apply.workable.com/n8n/).
check out our job posts:
[https://n8n.join.com](https://n8n.join.com)
## Upgrading ## Upgrading
Before you upgrade to the latest version make sure to check here if there are any breaking changes which concern you: Before you upgrade to the latest version, make sure to check the changelogs: [Changelog](https://docs.n8n.io/reference/changelog.html)
[Breaking Changes](https://github.com/n8n-io/n8n/blob/master/packages/cli/BREAKING-CHANGES.md)
You can also find breaking changes here: [Breaking Changes](./BREAKING-CHANGES.md)
## License ## License
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 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) Additional information on the license can be found in the [FAQ](https://docs.n8n.io/reference/faq.html#license)
## Development
Have you found a bug :bug: ? Or maybe you have a nice feature :sparkles: to contribute ? The [CONTRIBUTING guide](https://github.com/n8n-io/n8n/blob/master/CONTRIBUTING.md) will help you get your development environment ready in minutes.

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n", "name": "n8n",
"version": "0.118.0", "version": "0.121.0",
"description": "n8n Workflow Automation Tool", "description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -55,6 +55,7 @@
"devDependencies": { "devDependencies": {
"@oclif/dev-cli": "^1.22.2", "@oclif/dev-cli": "^1.22.2",
"@types/basic-auth": "^1.1.2", "@types/basic-auth": "^1.1.2",
"@types/bcryptjs": "^2.4.2",
"@types/bull": "^3.3.10", "@types/bull": "^3.3.10",
"@types/compression": "1.0.1", "@types/compression": "1.0.1",
"@types/connect-history-api-fallback": "^1.3.1", "@types/connect-history-api-fallback": "^1.3.1",
@ -79,11 +80,11 @@
"typescript": "~3.9.7" "typescript": "~3.9.7"
}, },
"dependencies": { "dependencies": {
"@node-rs/bcrypt": "^1.2.0",
"@oclif/command": "^1.5.18", "@oclif/command": "^1.5.18",
"@oclif/errors": "^1.2.2", "@oclif/errors": "^1.2.2",
"@types/jsonwebtoken": "^8.3.4", "@types/jsonwebtoken": "^8.3.4",
"basic-auth": "^2.0.1", "basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",
"body-parser": "^1.18.3", "body-parser": "^1.18.3",
"body-parser-xml": "^1.1.0", "body-parser-xml": "^1.1.0",
"bull": "^3.19.0", "bull": "^3.19.0",
@ -104,10 +105,10 @@
"localtunnel": "^2.0.0", "localtunnel": "^2.0.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mysql2": "~2.2.0", "mysql2": "~2.2.0",
"n8n-core": "~0.69.0", "n8n-core": "~0.72.0",
"n8n-editor-ui": "~0.88.0", "n8n-editor-ui": "~0.91.0",
"n8n-nodes-base": "~0.115.0", "n8n-nodes-base": "~0.118.0",
"n8n-workflow": "~0.57.0", "n8n-workflow": "~0.59.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"open": "^7.0.0", "open": "^7.0.0",
"pg": "^8.3.0", "pg": "^8.3.0",

View file

@ -67,15 +67,15 @@ export class ActiveWorkflowRunner {
for (const workflowData of workflowsData) { for (const workflowData of workflowsData) {
console.log(` - ${workflowData.name}`); console.log(` - ${workflowData.name}`);
Logger.debug(`Initializing active workflow "${workflowData.name}" (startup)`, {workflowName: workflowData.name, workflowId: workflowData.id}); Logger.debug(`Initializing active workflow "${workflowData.name}" (startup)`, { workflowName: workflowData.name, workflowId: workflowData.id });
try { try {
await this.add(workflowData.id.toString(), 'init', workflowData); await this.add(workflowData.id.toString(), 'init', workflowData);
Logger.verbose(`Successfully started workflow "${workflowData.name}"`, {workflowName: workflowData.name, workflowId: workflowData.id}); Logger.verbose(`Successfully started workflow "${workflowData.name}"`, { workflowName: workflowData.name, workflowId: workflowData.id });
console.log(` => Started`); console.log(` => Started`);
} catch (error) { } catch (error) {
console.log(` => ERROR: Workflow could not be activated:`); console.log(` => ERROR: Workflow could not be activated:`);
console.log(` ${error.message}`); console.log(` ${error.message}`);
Logger.error(`Unable to initialize workflow "${workflowData.name}" (startup)`, {workflowName: workflowData.name, workflowId: workflowData.id}); Logger.error(`Unable to initialize workflow "${workflowData.name}" (startup)`, { workflowName: workflowData.name, workflowId: workflowData.id });
} }
} }
Logger.verbose('Finished initializing active workflows (startup)'); Logger.verbose('Finished initializing active workflows (startup)');
@ -188,7 +188,7 @@ export class ActiveWorkflowRunner {
} }
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
const workflow = new Workflow({ id: webhook.workflowId.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings}); const workflow = new Workflow({ id: webhook.workflowId.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
const credentials = await WorkflowCredentials([workflow.getNode(webhook.node as string) as INode]); const credentials = await WorkflowCredentials([workflow.getNode(webhook.node as string) as INode]);
@ -225,8 +225,8 @@ export class ActiveWorkflowRunner {
* @returns {Promise<string[]>} * @returns {Promise<string[]>}
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
async getWebhookMethods(path: string) : Promise<string[]> { async getWebhookMethods(path: string): Promise<string[]> {
const webhooks = await Db.collections.Webhook?.find({ webhookPath: path}) as IWebhookDb[]; const webhooks = await Db.collections.Webhook?.find({ webhookPath: path }) as IWebhookDb[];
// Gather all request methods in string array // Gather all request methods in string array
const webhookMethods: string[] = webhooks.map(webhook => webhook.method); const webhookMethods: string[] = webhooks.map(webhook => webhook.method);
@ -463,7 +463,7 @@ export class ActiveWorkflowRunner {
* @returns {IGetExecuteTriggerFunctions} * @returns {IGetExecuteTriggerFunctions}
* @memberof ActiveWorkflowRunner * @memberof ActiveWorkflowRunner
*/ */
getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecuteTriggerFunctions{ getExecuteTriggerFunctions(workflowData: IWorkflowDb, additionalData: IWorkflowExecuteAdditionalDataWorkflow, mode: WorkflowExecuteMode, activation: WorkflowActivateMode): IGetExecuteTriggerFunctions {
return ((workflow: Workflow, node: INode) => { return ((workflow: Workflow, node: INode) => {
const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode, activation); const returnFunctions = NodeExecuteFunctions.getExecuteTriggerFunctions(workflow, node, additionalData, mode, activation);
returnFunctions.emit = (data: INodeExecutionData[][]): void => { returnFunctions.emit = (data: INodeExecutionData[][]): void => {
@ -517,8 +517,8 @@ export class ActiveWorkflowRunner {
if (workflowInstance.getTriggerNodes().length !== 0 if (workflowInstance.getTriggerNodes().length !== 0
|| workflowInstance.getPollNodes().length !== 0) { || workflowInstance.getPollNodes().length !== 0) {
await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, mode, activation, getTriggerFunctions, getPollFunctions); await this.activeWorkflows.add(workflowId, workflowInstance, additionalData, mode, activation, getTriggerFunctions, getPollFunctions);
Logger.info(`Successfully activated workflow "${workflowData.name}"`); Logger.verbose(`Successfully activated workflow "${workflowData.name}"`, { workflowId, workflowName: workflowData.name });
} }
if (this.activationErrors[workflowId] !== undefined) { if (this.activationErrors[workflowId] !== undefined) {
@ -569,7 +569,8 @@ export class ActiveWorkflowRunner {
// if it's active in memory then it's a trigger // if it's active in memory then it's a trigger
// so remove from list of actives workflows // so remove from list of actives workflows
if (this.activeWorkflows.isActive(workflowId)) { if (this.activeWorkflows.isActive(workflowId)) {
this.activeWorkflows.remove(workflowId); await this.activeWorkflows.remove(workflowId);
Logger.verbose(`Successfully deactivated workflow "${workflowId}"`, { workflowId });
} }
return; return;

View file

@ -11,22 +11,6 @@ import { IPackageVersions } from './';
let versionCache: IPackageVersions | undefined; let versionCache: IPackageVersions | undefined;
/**
* Displays a message to the user
*
* @export
* @param {string} message The message to display
* @param {string} [level='log']
*/
export function logOutput(message: string, level = 'log'): void {
if (level === 'log') {
console.log(message);
} else if (level === 'error') {
console.error(message);
}
}
/** /**
* Returns the base URL n8n is reachable from * Returns the base URL n8n is reachable from
* *

View file

@ -4,11 +4,18 @@ import {
} from 'n8n-core'; } from 'n8n-core';
import { import {
ICredentialType, ICredentialType,
ILogger,
INodeType, INodeType,
INodeTypeData, INodeTypeData,
LoggerProxy,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as config from '../config'; import * as config from '../config';
import {
getLogger,
} from '../src/Logger';
import { import {
access as fsAccess, access as fsAccess,
readdir as fsReaddir, readdir as fsReaddir,
@ -31,7 +38,12 @@ class LoadNodesAndCredentialsClass {
nodeModulesPath = ''; nodeModulesPath = '';
logger: ILogger;
async init() { async init() {
this.logger = getLogger();
LoggerProxy.init(this.logger);
// Get the path to the node-modules folder to be later able // Get the path to the node-modules folder to be later able
// to load the credentials and nodes // to load the credentials and nodes
const checkPaths = [ const checkPaths = [
@ -171,6 +183,10 @@ class LoadNodesAndCredentialsClass {
tempNode.description.icon = 'file:' + path.join(path.dirname(filePath), tempNode.description.icon.substr(5)); tempNode.description.icon = 'file:' + path.join(path.dirname(filePath), tempNode.description.icon.substr(5));
} }
if (tempNode.executeSingle) {
this.logger.warn(`"executeSingle" will get deprecated soon. Please update the code of node "${packageName}.${nodeName}" to use "execute" instead!`, { filePath });
}
if (this.includeNodes !== undefined && !this.includeNodes.includes(fullNodeName)) { if (this.includeNodes !== undefined && !this.includeNodes.includes(fullNodeName)) {
return; return;
} }

View file

@ -8,7 +8,6 @@ import {
resolve as pathResolve, resolve as pathResolve,
} from 'path'; } from 'path';
import { import {
getConnection,
getConnectionManager, getConnectionManager,
In, In,
} from 'typeorm'; } from 'typeorm';
@ -22,7 +21,9 @@ import { RequestOptions } from 'oauth-1.0a';
import * as csrf from 'csrf'; import * as csrf from 'csrf';
import * as requestPromise from 'request-promise-native'; import * as requestPromise from 'request-promise-native';
import { createHmac } from 'crypto'; import { createHmac } from 'crypto';
import { compare } from '@node-rs/bcrypt'; // IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
import { compare } from 'bcryptjs';
import * as promClient from 'prom-client'; import * as promClient from 'prom-client';
import { import {
@ -572,6 +573,7 @@ class App {
const newWorkflowData = req.body as IWorkflowBase; const newWorkflowData = req.body as IWorkflowBase;
const id = req.params.id; const id = req.params.id;
newWorkflowData.id = id;
await this.externalHooks.run('workflow.update', [newWorkflowData]); await this.externalHooks.run('workflow.update', [newWorkflowData]);
@ -716,6 +718,7 @@ class App {
// get generated dynamically // get generated dynamically
this.app.get(`/${this.restEndpoint}/node-parameter-options`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<INodePropertyOptions[]> => { this.app.get(`/${this.restEndpoint}/node-parameter-options`, ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<INodePropertyOptions[]> => {
const nodeType = req.query.nodeType as string; const nodeType = req.query.nodeType as string;
const path = req.query.path as string;
let credentials: INodeCredentials | undefined = undefined; let credentials: INodeCredentials | undefined = undefined;
const currentNodeParameters = JSON.parse('' + req.query.currentNodeParameters) as INodeParameters; const currentNodeParameters = JSON.parse('' + req.query.currentNodeParameters) as INodeParameters;
if (req.query.credentials !== undefined) { if (req.query.credentials !== undefined) {
@ -725,7 +728,7 @@ class App {
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();
const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, JSON.parse('' + req.query.currentNodeParameters), credentials!); const loadDataInstance = new LoadNodeParameterOptions(nodeType, nodeTypes, path, JSON.parse('' + req.query.currentNodeParameters), credentials!);
const workflowData = loadDataInstance.getWorkflowData() as IWorkflowBase; const workflowData = loadDataInstance.getWorkflowData() as IWorkflowBase;
const workflowCredentials = await WorkflowCredentials(workflowData.nodes); const workflowCredentials = await WorkflowCredentials(workflowData.nodes);
@ -1732,6 +1735,7 @@ class App {
} }
); );
} }
returnData.sort((a, b) => parseInt(b.id, 10) - parseInt(a.id, 10));
return returnData; return returnData;
} }

View file

@ -194,9 +194,9 @@ export class WorkflowRunnerProcess {
* @param {any[]} parameters * @param {any[]} parameters
* @memberof WorkflowRunnerProcess * @memberof WorkflowRunnerProcess
*/ */
sendHookToParentProcess(hook: string, parameters: any[]) { // tslint:disable-line:no-any async sendHookToParentProcess(hook: string, parameters: any[]) { // tslint:disable-line:no-any
try { try {
sendToParentProcess('processHook', { await sendToParentProcess('processHook', {
hook, hook,
parameters, parameters,
}); });
@ -217,22 +217,22 @@ export class WorkflowRunnerProcess {
const hookFunctions: IWorkflowExecuteHooks = { const hookFunctions: IWorkflowExecuteHooks = {
nodeExecuteBefore: [ nodeExecuteBefore: [
async (nodeName: string): Promise<void> => { async (nodeName: string): Promise<void> => {
this.sendHookToParentProcess('nodeExecuteBefore', [nodeName]); await this.sendHookToParentProcess('nodeExecuteBefore', [nodeName]);
}, },
], ],
nodeExecuteAfter: [ nodeExecuteAfter: [
async (nodeName: string, data: ITaskData): Promise<void> => { async (nodeName: string, data: ITaskData): Promise<void> => {
this.sendHookToParentProcess('nodeExecuteAfter', [nodeName, data]); await this.sendHookToParentProcess('nodeExecuteAfter', [nodeName, data]);
}, },
], ],
workflowExecuteBefore: [ workflowExecuteBefore: [
async (): Promise<void> => { async (): Promise<void> => {
this.sendHookToParentProcess('workflowExecuteBefore', []); await this.sendHookToParentProcess('workflowExecuteBefore', []);
}, },
], ],
workflowExecuteAfter: [ workflowExecuteAfter: [
async (fullRunData: IRun, newStaticData?: IDataObject): Promise<void> => { async (fullRunData: IRun, newStaticData?: IDataObject): Promise<void> => {
this.sendHookToParentProcess('workflowExecuteAfter', [fullRunData, newStaticData]); await this.sendHookToParentProcess('workflowExecuteAfter', [fullRunData, newStaticData]);
}, },
], ],
}; };

View file

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import * as config from '../../../../config';
export class ChangeCredentialDataSize1620729500000 implements MigrationInterface {
name = 'ChangeCredentialDataSize1620729500000';
async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'credentials_entity` MODIFY COLUMN `type` varchar(128) NOT NULL');
}
async down(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
await queryRunner.query('ALTER TABLE `' + tablePrefix + 'credentials_entity` MODIFY COLUMN `type` varchar(32) NOT NULL');
}
}

View file

@ -4,6 +4,7 @@ import { CreateIndexStoppedAt1594902918301 } from './1594902918301-CreateIndexSt
import { AddWebhookId1611149998770 } from './1611149998770-AddWebhookId'; import { AddWebhookId1611149998770 } from './1611149998770-AddWebhookId';
import { MakeStoppedAtNullable1607431743767 } from './1607431743767-MakeStoppedAtNullable'; import { MakeStoppedAtNullable1607431743767 } from './1607431743767-MakeStoppedAtNullable';
import { ChangeDataSize1615306975123 } from './1615306975123-ChangeDataSize'; import { ChangeDataSize1615306975123 } from './1615306975123-ChangeDataSize';
import { ChangeCredentialDataSize1620729500000 } from './1620729500000-ChangeCredentialDataSize';
export const mysqlMigrations = [ export const mysqlMigrations = [
InitialMigration1588157391238, InitialMigration1588157391238,
@ -12,4 +13,5 @@ export const mysqlMigrations = [
AddWebhookId1611149998770, AddWebhookId1611149998770,
MakeStoppedAtNullable1607431743767, MakeStoppedAtNullable1607431743767,
ChangeDataSize1615306975123, ChangeDataSize1615306975123,
ChangeCredentialDataSize1620729500000,
]; ];

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-core", "name": "n8n-core",
"version": "0.69.0", "version": "0.72.0",
"description": "Core functionality of n8n", "description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -47,7 +47,7 @@
"file-type": "^14.6.2", "file-type": "^14.6.2",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"n8n-workflow": "~0.57.0", "n8n-workflow": "~0.59.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0", "p-cancelable": "^2.0.0",
"request": "^2.88.2", "request": "^2.88.2",

View file

@ -18,10 +18,12 @@ const TEMP_WORKFLOW_NAME = 'Temp-Workflow';
export class LoadNodeParameterOptions { export class LoadNodeParameterOptions {
path: string;
workflow: Workflow; workflow: Workflow;
constructor(nodeTypeName: string, nodeTypes: INodeTypes, currentNodeParameters: INodeParameters, credentials?: INodeCredentials) { constructor(nodeTypeName: string, nodeTypes: INodeTypes, path: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials) {
this.path = path;
const nodeType = nodeTypes.getByName(nodeTypeName); const nodeType = nodeTypes.getByName(nodeTypeName);
if (nodeType === undefined) { if (nodeType === undefined) {
@ -89,7 +91,7 @@ export class LoadNodeParameterOptions {
throw new Error(`The node-type "${node!.type}" does not have the method "${methodName}" defined!`); throw new Error(`The node-type "${node!.type}" does not have the method "${methodName}" defined!`);
} }
const thisArgs = NodeExecuteFunctions.getLoadOptionsFunctions(this.workflow, node!, additionalData); const thisArgs = NodeExecuteFunctions.getLoadOptionsFunctions(this.workflow, node!, this.path, additionalData);
return nodeType!.methods.loadOptions[methodName].call(thisArgs); return nodeType!.methods.loadOptions[methodName].call(thisArgs);
} }

View file

@ -691,7 +691,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
return continueOnFail(node); return continueOnFail(node);
}, },
evaluateExpression: (expression: string, itemIndex: number) => { evaluateExpression: (expression: string, itemIndex: number) => {
return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode); return workflow.expression.resolveSimpleParameterValue('=' + expression, {}, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode);
}, },
async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise<any> { // tslint:disable-line:no-any async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise<any> { // tslint:disable-line:no-any
return additionalData.executeWorkflow(workflowInfo, additionalData, inputData); return additionalData.executeWorkflow(workflowInfo, additionalData, inputData);
@ -742,7 +742,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
return getWorkflowMetadata(workflow); return getWorkflowMetadata(workflow);
}, },
getWorkflowDataProxy: (itemIndex: number): IWorkflowDataProxyData => { getWorkflowDataProxy: (itemIndex: number): IWorkflowDataProxyData => {
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode); const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, {}, mode);
return dataProxy.getDataProxy(); return dataProxy.getDataProxy();
}, },
getWorkflowStaticData(type: string): IDataObject { getWorkflowStaticData(type: string): IDataObject {
@ -789,7 +789,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
}, },
evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => { evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => {
evaluateItemIndex = evaluateItemIndex === undefined ? itemIndex : evaluateItemIndex; evaluateItemIndex = evaluateItemIndex === undefined ? itemIndex : evaluateItemIndex;
return workflow.expression.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData, mode); return workflow.expression.resolveSimpleParameterValue('=' + expression, {}, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData, mode);
}, },
getContext(type: string): IContextObject { getContext(type: string): IContextObject {
return NodeHelpers.getContext(runExecutionData, type, node); return NodeHelpers.getContext(runExecutionData, type, node);
@ -841,7 +841,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
return getWorkflowMetadata(workflow); return getWorkflowMetadata(workflow);
}, },
getWorkflowDataProxy: (): IWorkflowDataProxyData => { getWorkflowDataProxy: (): IWorkflowDataProxyData => {
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, mode); const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, node.name, connectionInputData, {}, mode);
return dataProxy.getDataProxy(); return dataProxy.getDataProxy();
}, },
getWorkflowStaticData(type: string): IDataObject { getWorkflowStaticData(type: string): IDataObject {
@ -871,18 +871,20 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
* @param {IWorkflowExecuteAdditionalData} additionalData * @param {IWorkflowExecuteAdditionalData} additionalData
* @returns {ILoadOptionsFunctions} * @returns {ILoadOptionsFunctions}
*/ */
export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData): ILoadOptionsFunctions { export function getLoadOptionsFunctions(workflow: Workflow, node: INode, path: string, additionalData: IWorkflowExecuteAdditionalData): ILoadOptionsFunctions {
return ((workflow: Workflow, node: INode) => { return ((workflow: Workflow, node: INode, path: string) => {
const that = { const that = {
getCredentials(type: string): ICredentialDataDecryptedObject | undefined { getCredentials(type: string): ICredentialDataDecryptedObject | undefined {
return getCredentials(workflow, node, type, additionalData, 'internal'); return getCredentials(workflow, node, type, additionalData, 'internal');
}, },
getCurrentNodeParameter: (parameterName: string): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object | undefined => { getCurrentNodeParameter: (parameterPath: string): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object | undefined => {
const nodeParameters = additionalData.currentNodeParameters; const nodeParameters = additionalData.currentNodeParameters;
if (nodeParameters && nodeParameters[parameterName]) {
return nodeParameters[parameterName]; if (parameterPath.charAt(0) === '&') {
parameterPath = `${path.split('.').slice(1, -1).join('.')}.${parameterPath.slice(1)}`;
} }
return undefined;
return get(nodeParameters, parameterPath);
}, },
getCurrentNodeParameters: (): INodeParameters | undefined => { getCurrentNodeParameters: (): INodeParameters | undefined => {
return additionalData.currentNodeParameters; return additionalData.currentNodeParameters;
@ -915,7 +917,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio
}, },
}; };
return that; return that;
})(workflow, node); })(workflow, node, path);
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-editor-ui", "name": "n8n-editor-ui",
"version": "0.88.0", "version": "0.91.0",
"description": "Workflow Editor UI for n8n", "description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -65,7 +65,7 @@
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.set": "^4.3.2", "lodash.set": "^4.3.2",
"n8n-workflow": "~0.57.0", "n8n-workflow": "~0.59.0",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"normalize-wheel": "^1.0.1", "normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1", "prismjs": "^1.17.1",

View file

@ -131,7 +131,7 @@ export interface IRestApi {
getSettings(): Promise<IN8nUISettings>; getSettings(): Promise<IN8nUISettings>;
getNodeTypes(): Promise<INodeTypeDescription[]>; getNodeTypes(): Promise<INodeTypeDescription[]>;
getNodesInformation(nodeList: string[]): Promise<INodeTypeDescription[]>; getNodesInformation(nodeList: string[]): Promise<INodeTypeDescription[]>;
getNodeParameterOptions(nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>; getNodeParameterOptions(nodeType: string, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
removeTestWebhook(workflowId: string): Promise<boolean>; removeTestWebhook(workflowId: string): Promise<boolean>;
runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>; runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>;
createNewWorkflow(sendData: IWorkflowData): Promise<IWorkflowDb>; createNewWorkflow(sendData: IWorkflowData): Promise<IWorkflowDb>;

View file

@ -438,7 +438,8 @@ export default mixins(
this.$store.commit('setActiveExecutions', results[1]); this.$store.commit('setActiveExecutions', results[1]);
const alreadyPresentExecutionIds = this.finishedExecutions.map(exec => exec.id); // execution IDs are typed as string, int conversion is necessary so we can order.
const alreadyPresentExecutionIds = this.finishedExecutions.map(exec => parseInt(exec.id, 10));
let lastId = 0; let lastId = 0;
const gaps = [] as number[]; const gaps = [] as number[];
for(let i = results[0].results.length - 1; i >= 0; i--) { for(let i = results[0].results.length - 1; i >= 0; i--) {
@ -459,7 +460,7 @@ export default mixins(
// Check new results from end to start // Check new results from end to start
// Add new items accordingly. // Add new items accordingly.
const executionIndex = alreadyPresentExecutionIds.indexOf(currentItem.id); const executionIndex = alreadyPresentExecutionIds.indexOf(currentId);
if (executionIndex !== -1) { if (executionIndex !== -1) {
// Execution that we received is already present. // Execution that we received is already present.
@ -477,7 +478,7 @@ export default mixins(
// Find the correct position to place this newcomer // Find the correct position to place this newcomer
let j; let j;
for (j = this.finishedExecutions.length - 1; j >= 0; j--) { for (j = this.finishedExecutions.length - 1; j >= 0; j--) {
if (currentItem.id < this.finishedExecutions[j].id) { if (currentId < parseInt(this.finishedExecutions[j].id, 10)) {
this.finishedExecutions.splice(j + 1, 0, currentItem); this.finishedExecutions.splice(j + 1, 0, currentItem);
break; break;
} }

View file

@ -30,7 +30,7 @@
<div class="editor-description"> <div class="editor-description">
Result Result
</div> </div>
<expression-input :parameter="parameter" resolvedValue="true" rows="8" :value="value" :path="path"></expression-input> <expression-input :parameter="parameter" resolvedValue="true" ref="expressionResult" rows="8" :value="value" :path="path"></expression-input>
</div> </div>
</el-col> </el-col>
@ -52,7 +52,11 @@ import {
Workflow, Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
export default Vue.extend({ import { externalHooks } from '@/components/mixins/externalHooks';
import mixins from 'vue-typed-mixins';
export default mixins(externalHooks).extend({
name: 'ExpressionEdit', name: 'ExpressionEdit',
props: [ props: [
'dialogVisible', 'dialogVisible',
@ -81,7 +85,16 @@ export default Vue.extend({
}, },
itemSelected (eventData: IVariableItemSelected) { itemSelected (eventData: IVariableItemSelected) {
// User inserted item from Expression Editor variable selector
(this.$refs.inputFieldExpression as any).itemSelected(eventData); // tslint:disable-line:no-any (this.$refs.inputFieldExpression as any).itemSelected(eventData); // tslint:disable-line:no-any
this.$externalHooks().run('expressionEdit.itemSelected', { parameter: this.parameter, value: this.value, selectedItem: eventData });
},
},
watch: {
dialogVisible (newValue) {
const resolvedExpressionValue = this.$refs.expressionResult && (this.$refs.expressionResult as any).getValue() || undefined; // tslint:disable-line:no-any
this.$externalHooks().run('expressionEdit.dialogVisibleChanged', { dialogVisible: newValue, parameter: this.parameter, value: this.value, resolvedExpressionValue });
}, },
}, },
}); });

View file

@ -177,7 +177,11 @@ export default mixins(genericHelpers)
} else if (optionParameter.typeOptions !== undefined && optionParameter.typeOptions.multipleValues === true) { } else if (optionParameter.typeOptions !== undefined && optionParameter.typeOptions.multipleValues === true) {
// Multiple values are allowed so append option to array // Multiple values are allowed so append option to array
newParameterValue[optionParameter.name] = get(this.nodeValues, `${this.path}.${optionParameter.name}`, []); newParameterValue[optionParameter.name] = get(this.nodeValues, `${this.path}.${optionParameter.name}`, []);
(newParameterValue[optionParameter.name] as INodeParameters[]).push(JSON.parse(JSON.stringify(optionParameter.default))); if (Array.isArray(optionParameter.default)) {
(newParameterValue[optionParameter.name] as INodeParameters[]).push(...JSON.parse(JSON.stringify(optionParameter.default)));
} else if (optionParameter.default !== '' && typeof optionParameter.default !== 'object') {
(newParameterValue[optionParameter.name] as INodeParameters[]).push(JSON.parse(JSON.stringify(optionParameter.default)));
}
} else { } else {
// Add a new option // Add a new option
newParameterValue[optionParameter.name] = JSON.parse(JSON.stringify(optionParameter.default)); newParameterValue[optionParameter.name] = JSON.parse(JSON.stringify(optionParameter.default));

View file

@ -37,9 +37,6 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import { import {
INodeIssues,
INodeIssueData,
INodeIssueObjectProperty,
INodeTypeDescription, INodeTypeDescription,
INodeParameters, INodeParameters,
INodeProperties, INodeProperties,
@ -59,12 +56,14 @@ import NodeCredentials from '@/components/NodeCredentials.vue';
import NodeWebhooks from '@/components/NodeWebhooks.vue'; import NodeWebhooks from '@/components/NodeWebhooks.vue';
import { get, set, unset } from 'lodash'; import { get, set, unset } from 'lodash';
import { externalHooks } from '@/components/mixins/externalHooks';
import { genericHelpers } from '@/components/mixins/genericHelpers'; import { genericHelpers } from '@/components/mixins/genericHelpers';
import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
export default mixins( export default mixins(
externalHooks,
genericHelpers, genericHelpers,
nodeHelpers, nodeHelpers,
) )
@ -323,6 +322,8 @@ export default mixins(
// Update the issues // Update the issues
this.updateNodeCredentialIssues(node); this.updateNodeCredentialIssues(node);
this.$externalHooks().run('nodeSettings.credentialSelected', { updateInformation });
}, },
valueChanged (parameterData: IUpdateInformation) { valueChanged (parameterData: IUpdateInformation) {
let newValue: NodeParameterValue; let newValue: NodeParameterValue;
@ -357,6 +358,7 @@ export default mixins(
// Get only the parameters which are different to the defaults // Get only the parameters which are different to the defaults
let nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, false, false); let nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, false, false);
const oldNodeParameters = Object.assign({}, nodeParameters);
// Copy the data because it is the data of vuex so make sure that // Copy the data because it is the data of vuex so make sure that
// we do not edit it directly // we do not edit it directly
@ -404,8 +406,11 @@ export default mixins(
name: node.name, name: node.name,
value: nodeParameters, value: nodeParameters,
}; };
this.$store.commit('setNodeParameters', updateInformation); this.$store.commit('setNodeParameters', updateInformation);
this.$externalHooks().run('nodeSettings.valueChanged', { parameterPath, newValue, parameters: this.parameters, oldNodeParameters });
this.updateNodeParameterIssues(node, nodeType); this.updateNodeParameterIssues(node, nodeType);
this.updateNodeCredentialIssues(node); this.updateNodeCredentialIssues(node);
} else { } else {

View file

@ -230,7 +230,7 @@ export default mixins(
// Get the resolved parameter values of the current node // Get the resolved parameter values of the current node
const currentNodeParameters = this.$store.getters.activeNode.parameters; const currentNodeParameters = this.$store.getters.activeNode.parameters;
const resolvedNodeParameters = this.getResolveNodeParameters(currentNodeParameters); const resolvedNodeParameters = this.resolveParameter(currentNodeParameters);
const returnValues: string[] = []; const returnValues: string[] = [];
for (const parameterPath of loadOptionsDependsOn) { for (const parameterPath of loadOptionsDependsOn) {
@ -456,21 +456,6 @@ export default mixins(
}, },
}, },
methods: { methods: {
getResolveNodeParameters (nodeParameters: INodeParameters): INodeParameters {
const returnData: INodeParameters = {};
for (const key of Object.keys(nodeParameters)) {
if (Array.isArray(nodeParameters[key])) {
returnData[key] = (nodeParameters[key] as string[]).map(value => {
return this.resolveExpression(value as string) as string;
});
} else if (typeof nodeParameters[key] === 'object') {
returnData[key] = this.getResolveNodeParameters(nodeParameters[key] as INodeParameters);
} else {
returnData[key] = this.resolveExpression(nodeParameters[key] as string);
}
}
return returnData;
},
async loadRemoteParameterOptions () { async loadRemoteParameterOptions () {
if (this.node === null || this.remoteMethod === undefined || this.remoteParameterOptionsLoading) { if (this.node === null || this.remoteMethod === undefined || this.remoteParameterOptionsLoading) {
return; return;
@ -481,10 +466,10 @@ export default mixins(
// Get the resolved parameter values of the current node // Get the resolved parameter values of the current node
const currentNodeParameters = this.$store.getters.activeNode.parameters; const currentNodeParameters = this.$store.getters.activeNode.parameters;
const resolvedNodeParameters = this.getResolveNodeParameters(currentNodeParameters); const resolvedNodeParameters = this.resolveParameter(currentNodeParameters) as INodeParameters;
try { try {
const options = await this.restApi().getNodeParameterOptions(this.node.type, this.remoteMethod, resolvedNodeParameters, this.node.credentials); const options = await this.restApi().getNodeParameterOptions(this.node.type, this.path, this.remoteMethod, resolvedNodeParameters, this.node.credentials);
this.remoteParameterOptions.push.apply(this.remoteParameterOptions, options); this.remoteParameterOptions.push.apply(this.remoteParameterOptions, options);
} catch (error) { } catch (error) {
this.remoteParameterOptionsLoadingIssues = error.message; this.remoteParameterOptionsLoadingIssues = error.message;

View file

@ -76,26 +76,27 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue';
import { import {
INodeParameters,
INodeProperties, INodeProperties,
NodeParameterValue,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { IUpdateInformation } from '@/Interface'; import { IUpdateInformation } from '@/Interface';
import MultipleParameter from '@/components/MultipleParameter.vue'; import MultipleParameter from '@/components/MultipleParameter.vue';
import { genericHelpers } from '@/components/mixins/genericHelpers'; import { genericHelpers } from '@/components/mixins/genericHelpers';
import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import ParameterInputFull from '@/components/ParameterInputFull.vue'; import ParameterInputFull from '@/components/ParameterInputFull.vue';
import { get } from 'lodash'; import { get, set } from 'lodash';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
export default mixins( export default mixins(
genericHelpers, genericHelpers,
nodeHelpers, workflowHelpers,
) )
.extend({ .extend({
name: 'ParameterInputList', name: 'ParameterInputList',
@ -110,9 +111,12 @@ export default mixins(
'hideDelete', // boolean 'hideDelete', // boolean
], ],
computed: { computed: {
filteredParameters (): INodeProperties { filteredParameters (): INodeProperties[] {
return this.parameters.filter((parameter: INodeProperties) => this.displayNodeParameter(parameter)); return this.parameters.filter((parameter: INodeProperties) => this.displayNodeParameter(parameter));
}, },
filteredParameterNames (): string[] {
return this.filteredParameters.map(parameter => parameter.name);
},
}, },
methods: { methods: {
multipleValues (parameter: INodeProperties): boolean { multipleValues (parameter: INodeProperties): boolean {
@ -157,12 +161,75 @@ export default mixins(
// If it is not defined no need to do a proper check // If it is not defined no need to do a proper check
return true; return true;
} }
const nodeValues: INodeParameters = {};
let rawValues = this.nodeValues;
if (this.path) {
rawValues = get(this.nodeValues, this.path);
}
// Resolve expressions
const resolveKeys = Object.keys(rawValues);
let key: string;
let i = 0;
let parameterGotResolved = false;
do {
key = resolveKeys.shift() as string;
if (typeof rawValues[key] === 'string' && rawValues[key].charAt(0) === '=') {
// Contains an expression that
if (rawValues[key].includes('$parameter') && resolveKeys.some(parameterName => rawValues[key].includes(parameterName))) {
// Contains probably an expression of a missing parameter so skip
resolveKeys.push(key);
continue;
} else {
// Contains probably no expression with a missing parameter so resolve
nodeValues[key] = this.resolveExpression(rawValues[key], nodeValues) as NodeParameterValue;
parameterGotResolved = true;
}
} else {
// Does not contain an expression, add directly
nodeValues[key] = rawValues[key];
}
// TODO: Think about how to calculate this best
if (i++ > 50) {
// Make sure we do not get caught
break;
}
} while(resolveKeys.length !== 0);
if (parameterGotResolved === true) {
if (this.path) {
rawValues = JSON.parse(JSON.stringify(this.nodeValues));
set(rawValues, this.path, nodeValues);
return this.displayParameter(rawValues, parameter, this.path);
} else {
return this.displayParameter(nodeValues, parameter, '');
}
}
return this.displayParameter(this.nodeValues, parameter, this.path); return this.displayParameter(this.nodeValues, parameter, this.path);
}, },
valueChanged (parameterData: IUpdateInformation): void { valueChanged (parameterData: IUpdateInformation): void {
this.$emit('valueChanged', parameterData); this.$emit('valueChanged', parameterData);
}, },
}, },
watch: {
filteredParameterNames(newValue, oldValue) {
// After a parameter does not get displayed anymore make sure that its value gets removed
// Is only needed for the edge-case when a parameter gets displayed depending on another field
// which contains an expression.
for (const parameter of oldValue) {
if (!newValue.includes(parameter)) {
const parameterData = {
name: `${this.path}.${parameter}`,
node: this.$store.getters.activeNode.name,
value: undefined,
};
this.$emit('valueChanged', parameterData);
}
}
},
},
beforeCreate: function () { // tslint:disable-line beforeCreate: function () { // tslint:disable-line
// Because we have a circular dependency on CollectionParameter import it here // Because we have a circular dependency on CollectionParameter import it here
// to not break Vue. // to not break Vue.

View file

@ -379,7 +379,7 @@ export default mixins(
return returnData; return returnData;
} }
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, nodeName, connectionInputData, 'manual'); const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, nodeName, connectionInputData, {}, 'manual');
const proxy = dataProxy.getDataProxy(); const proxy = dataProxy.getDataProxy();
// @ts-ignore // @ts-ignore

View file

@ -157,9 +157,10 @@ export const restApi = Vue.extend({
}, },
// Returns all the parameter options from the server // Returns all the parameter options from the server
getNodeParameterOptions: (nodeType: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]> => { getNodeParameterOptions: (nodeType: string, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]> => {
const sendData = { const sendData = {
nodeType, nodeType,
path,
methodName, methodName,
credentials, credentials,
currentNodeParameters, currentNodeParameters,

View file

@ -2,9 +2,12 @@ import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import { import {
IConnections, IConnections,
IDataObject,
INode, INode,
INodeExecutionData, INodeExecutionData,
INodeIssues, INodeIssues,
INodeParameters,
NodeParameterValue,
INodeType, INodeType,
INodeTypes, INodeTypes,
INodeTypeData, INodeTypeData,
@ -335,8 +338,8 @@ export const workflowHelpers = mixins(
return nodeData; return nodeData;
}, },
// Executes the given expression and returns its value
resolveExpression (expression: string) { resolveParameter(parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) {
const inputIndex = 0; const inputIndex = 0;
const itemIndex = 0; const itemIndex = 0;
const runIndex = 0; const runIndex = 0;
@ -362,7 +365,22 @@ export const workflowHelpers = mixins(
connectionInputData = []; connectionInputData = [];
} }
return workflow.expression.getParameterValue(expression, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, 'manual', true); return workflow.expression.getParameterValue(parameter, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, 'manual', false) as IDataObject;
},
resolveExpression(expression: string, siblingParameters: INodeParameters = {}) {
const parameters = {
'__xxxxxxx__': expression,
...siblingParameters,
};
const returnData = this.resolveParameter(parameters) as IDataObject;
if (typeof returnData['__xxxxxxx__'] === 'object') {
const workflow = this.getWorkflow();
return workflow.expression.convertObjectValueToString(returnData['__xxxxxxx__'] as object);
}
return returnData['__xxxxxxx__'];
}, },
// Saves the currently loaded workflow to the database. // Saves the currently loaded workflow to the database.

View file

@ -93,6 +93,7 @@ import {
faTrash, faTrash,
faUndo, faUndo,
faUsers, faUsers,
faClock,
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
@ -174,6 +175,7 @@ library.add(faTimes);
library.add(faTrash); library.add(faTrash);
library.add(faUndo); library.add(faUndo);
library.add(faUsers); library.add(faUsers);
library.add(faClock);
Vue.component('font-awesome-icon', FontAwesomeIcon); Vue.component('font-awesome-icon', FontAwesomeIcon);

View file

@ -1851,21 +1851,19 @@ export default mixins(
for (type of Object.keys(currentConnections[sourceNode])) { for (type of Object.keys(currentConnections[sourceNode])) {
connection[type] = []; connection[type] = [];
for (sourceIndex = 0; sourceIndex < currentConnections[sourceNode][type].length; sourceIndex++) { for (sourceIndex = 0; sourceIndex < currentConnections[sourceNode][type].length; sourceIndex++) {
if (!currentConnections[sourceNode][type][sourceIndex]) {
// There is so something wrong with the data so ignore
continue;
}
const nodeSourceConnections = []; const nodeSourceConnections = [];
for (connectionIndex = 0; connectionIndex < currentConnections[sourceNode][type][sourceIndex].length; connectionIndex++) { if (currentConnections[sourceNode][type][sourceIndex]) {
const nodeConnection: NodeInputConnections = []; for (connectionIndex = 0; connectionIndex < currentConnections[sourceNode][type][sourceIndex].length; connectionIndex++) {
connectionData = currentConnections[sourceNode][type][sourceIndex][connectionIndex]; const nodeConnection: NodeInputConnections = [];
if (!createNodeNames.includes(connectionData.node)) { connectionData = currentConnections[sourceNode][type][sourceIndex][connectionIndex];
// Node does not get created so skip input connection if (!createNodeNames.includes(connectionData.node)) {
continue; // Node does not get created so skip input connection
} continue;
}
nodeSourceConnections.push(connectionData); nodeSourceConnections.push(connectionData);
// Add connection // Add connection
}
} }
connection[type].push(nodeSourceConnections); connection[type].push(nodeSourceConnections);
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "n8n-node-dev", "name": "n8n-node-dev",
"version": "0.11.0", "version": "0.13.0",
"description": "CLI to simplify n8n credentials/node development", "description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md", "license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io", "homepage": "https://n8n.io",
@ -59,8 +59,8 @@
"change-case": "^4.1.1", "change-case": "^4.1.1",
"copyfiles": "^2.1.1", "copyfiles": "^2.1.1",
"inquirer": "^7.0.1", "inquirer": "^7.0.1",
"n8n-core": "^0.67.0", "n8n-core": "~0.71.0",
"n8n-workflow": "^0.55.0", "n8n-workflow": "~0.58.0",
"oauth-1.0a": "^2.2.6", "oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0", "replace-in-file": "^6.0.0",
"request": "^2.88.2", "request": "^2.88.2",

View file

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

View file

@ -0,0 +1,47 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class NotionOAuth2Api implements ICredentialType {
name = 'notionOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Notion OAuth2 API';
documentationUrl = 'notion';
properties = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://api.notion.com/v1/oauth/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://api.notion.com/v1/oauth/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'header',
},
];
}

View file

@ -20,5 +20,11 @@ export class PaddleApi implements ICredentialType {
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: '', default: '',
}, },
{
displayName: 'Use Sandbox environment API',
name: 'sandbox',
type: 'boolean' as NodePropertyTypes,
default: false,
},
]; ];
} }

View file

@ -9,6 +9,22 @@ export class TwilioApi implements ICredentialType {
displayName = 'Twilio API'; displayName = 'Twilio API';
documentationUrl = 'twilio'; documentationUrl = 'twilio';
properties = [ properties = [
{
displayName: 'Auth Type',
name: 'authType',
type: 'options' as NodePropertyTypes,
default: 'authToken',
options: [
{
name: 'Auth Token',
value: 'authToken',
},
{
name: 'API Key',
value: 'apiKey',
},
],
},
{ {
displayName: 'Account SID', displayName: 'Account SID',
name: 'accountSid', name: 'accountSid',
@ -20,6 +36,42 @@ export class TwilioApi implements ICredentialType {
name: 'authToken', name: 'authToken',
type: 'string' as NodePropertyTypes, type: 'string' as NodePropertyTypes,
default: '', default: '',
displayOptions: {
show: {
authType: [
'authToken',
],
},
},
},
{
displayName: 'API Key SID',
name: 'apiKeySid',
type: 'string' as NodePropertyTypes,
default: '',
displayOptions: {
show: {
authType: [
'apiKey',
],
},
},
},
{
displayName: 'API Key Secret',
name: 'apiKeySecret',
type: 'string' as NodePropertyTypes,
typeOptions: {
password: true,
},
default: '',
displayOptions: {
show: {
authType: [
'apiKey',
],
},
},
}, },
]; ];
} }

View file

@ -27,5 +27,14 @@ export class WooCommerceApi implements ICredentialType {
default: '', default: '',
placeholder: 'https://example.com', placeholder: 'https://example.com',
}, },
{
displayName: 'Include Credentials in Query',
name: 'includeCredentialsInQuery',
type: 'boolean' as NodePropertyTypes,
default: false,
description: `Occasionally, some servers may not parse the Authorization header correctly</br>
(if you see a Consumer key is missing error when authenticating over SSL, you have a server issue).</br>
In this case, you may provide the consumer key/secret as query string parameters instead.`,
},
]; ];
} }

View file

@ -27,6 +27,11 @@
"icon": "☀️", "icon": "☀️",
"url": "https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/" "url": "https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/"
}, },
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
},
{ {
"label": "Building an expense tracking app in 10 minutes", "label": "Building an expense tracking app in 10 minutes",
"icon": "📱", "icon": "📱",

View file

@ -24,7 +24,7 @@ export class ClockifyTrigger implements INodeType {
displayName: 'Clockify Trigger', displayName: 'Clockify Trigger',
icon: 'file:clockify.png', icon: 'file:clockify.png',
name: 'clockifyTrigger', name: 'clockifyTrigger',
group: ['trigger'], group: [ 'trigger' ],
version: 1, version: 1,
description: 'Watches Clockify For Events', description: 'Watches Clockify For Events',
defaults: { defaults: {
@ -32,7 +32,7 @@ export class ClockifyTrigger implements INodeType {
color: '#000000', color: '#000000',
}, },
inputs: [], inputs: [],
outputs: ['main'], outputs: [ 'main' ],
credentials: [ credentials: [
{ {
name: 'clockifyApi', name: 'clockifyApi',
@ -109,7 +109,7 @@ export class ClockifyTrigger implements INodeType {
qs.start = webhookData.lastTimeChecked; qs.start = webhookData.lastTimeChecked;
qs.end = moment().tz(workflowTimezone).format('YYYY-MM-DDTHH:mm:ss') + 'Z'; qs.end = moment().tz(workflowTimezone).format('YYYY-MM-DDTHH:mm:ss') + 'Z';
qs.hydrated = true; qs.hydrated = true;
qs['in-progress'] = false; qs[ 'in-progress' ] = false;
break; break;
} }
@ -117,8 +117,8 @@ export class ClockifyTrigger implements INodeType {
webhookData.lastTimeChecked = qs.end; webhookData.lastTimeChecked = qs.end;
if (Array.isArray(result) && result.length !== 0) { if (Array.isArray(result) && result.length !== 0) {
result = [this.helpers.returnJsonArray(result)]; return [ this.helpers.returnJsonArray(result) ];
} }
return result; return null;
} }
} }

View file

@ -18,5 +18,10 @@
"Zip", "Zip",
"Gzip", "Gzip",
"uncompress" "uncompress"
] ],
"subcategories": {
"Core Nodes": [
"Files"
]
}
} }

View file

@ -53,11 +53,21 @@
"icon": "📡", "icon": "📡",
"url": "https://n8n.io/blog/database-monitoring-and-alerting-with-n8n/" "url": "https://n8n.io/blog/database-monitoring-and-alerting-with-n8n/"
}, },
{
"label": "Automate your data processing pipeline in 9 steps",
"icon": "⚙️",
"url": "https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/"
},
{ {
"label": "Celebrating World Poetry Day with a daily poem in Telegram", "label": "Celebrating World Poetry Day with a daily poem in Telegram",
"icon": "📜", "icon": "📜",
"url": "https://n8n.io/blog/world-poetry-day-workflow/" "url": "https://n8n.io/blog/world-poetry-day-workflow/"
}, },
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
},
{ {
"label": "Automate Designs with Bannerbear and n8n", "label": "Automate Designs with Bannerbear and n8n",
"icon": "🎨", "icon": "🎨",
@ -114,5 +124,10 @@
"Time", "Time",
"Scheduler", "Scheduler",
"Poll" "Poll"
] ],
"subcategories": {
"Core Nodes": [
"Flow"
]
}
} }

View file

@ -18,5 +18,10 @@
"Encrypt", "Encrypt",
"SHA", "SHA",
"Hash" "Hash"
] ],
"subcategories": {
"Core Nodes": [
"Helpers"
]
}
} }

View file

@ -11,5 +11,10 @@
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.dateTime/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.dateTime/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Helpers"
]
} }
} }

View file

@ -23,7 +23,7 @@ export class DateTime implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Date & Time', displayName: 'Date & Time',
name: 'dateTime', name: 'dateTime',
icon: 'fa:calendar', icon: 'fa:clock',
group: ['transform'], group: ['transform'],
version: 1, version: 1,
description: 'Allows you to manipulate date and time values', description: 'Allows you to manipulate date and time values',

View file

@ -13,5 +13,10 @@
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.editImage/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.editImage/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Files"
]
} }
} }

View file

@ -24,5 +24,10 @@
"url": "https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/" "url": "https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Helpers"
]
} }
} }

View file

@ -24,6 +24,10 @@ import {
import * as lodash from 'lodash'; import * as lodash from 'lodash';
import {
LoggerProxy as Logger
} from 'n8n-workflow';
export class EmailReadImap implements INodeType { export class EmailReadImap implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'EmailReadImap', displayName: 'EmailReadImap',
@ -158,6 +162,13 @@ export class EmailReadImap implements INodeType {
default: false, default: false,
description: 'Do connect even if SSL certificate validation is not possible.', description: 'Do connect even if SSL certificate validation is not possible.',
}, },
{
displayName: 'Force reconnect',
name: 'forceReconnect',
type: 'number',
default: 60,
description: 'Sets an interval (in minutes) to force a reconnection.',
},
], ],
}, },
], ],
@ -176,16 +187,8 @@ export class EmailReadImap implements INodeType {
const postProcessAction = this.getNodeParameter('postProcessAction') as string; const postProcessAction = this.getNodeParameter('postProcessAction') as string;
const options = this.getNodeParameter('options', {}) as IDataObject; const options = this.getNodeParameter('options', {}) as IDataObject;
let searchCriteria = [ const staticData = this.getWorkflowStaticData('node');
'UNSEEN', Logger.debug('Loaded static data for node "EmailReadImap"', {staticData});
];
if (options.customEmailConfig !== undefined) {
try {
searchCriteria = JSON.parse(options.customEmailConfig as string);
} catch (error) {
throw new NodeOperationError(this.getNode(), `Custom email config is not valid JSON.`);
}
}
// Returns the email text // Returns the email text
const getText = async (parts: any[], message: Message, subtype: string) => { // tslint:disable-line:no-any const getText = async (parts: any[], message: Message, subtype: string) => { // tslint:disable-line:no-any
@ -237,7 +240,7 @@ export class EmailReadImap implements INodeType {
// Returns all the new unseen messages // Returns all the new unseen messages
const getNewEmails = async (connection: ImapSimple, searchCriteria: string[]): Promise<INodeExecutionData[]> => { const getNewEmails = async (connection: ImapSimple, searchCriteria: Array<string | string[]>): Promise<INodeExecutionData[]> => {
const format = this.getNodeParameter('format', 0) as string; const format = this.getNodeParameter('format', 0) as string;
let fetchOptions = {}; let fetchOptions = {};
@ -277,6 +280,12 @@ export class EmailReadImap implements INodeType {
const dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string; const dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string;
for (const message of results) { for (const message of results) {
if (staticData.lastMessageUid !== undefined && message.attributes.uid <= (staticData.lastMessageUid as number)) {
continue;
}
if (staticData.lastMessageUid === undefined || staticData.lastMessageUid as number < message.attributes.uid) {
staticData.lastMessageUid = message.attributes.uid;
}
const part = lodash.find(message.parts, { which: '' }); const part = lodash.find(message.parts, { which: '' });
if (part === undefined) { if (part === undefined) {
@ -295,6 +304,12 @@ export class EmailReadImap implements INodeType {
} }
for (const message of results) { for (const message of results) {
if (staticData.lastMessageUid !== undefined && message.attributes.uid <= (staticData.lastMessageUid as number)) {
continue;
}
if (staticData.lastMessageUid === undefined || staticData.lastMessageUid as number < message.attributes.uid) {
staticData.lastMessageUid = message.attributes.uid;
}
const parts = getParts(message.attributes.struct!); const parts = getParts(message.attributes.struct!);
newEmail = { newEmail = {
@ -335,6 +350,12 @@ export class EmailReadImap implements INodeType {
} }
} else if (format === 'raw') { } else if (format === 'raw') {
for (const message of results) { for (const message of results) {
if (staticData.lastMessageUid !== undefined && message.attributes.uid <= (staticData.lastMessageUid as number)) {
continue;
}
if (staticData.lastMessageUid === undefined || staticData.lastMessageUid as number < message.attributes.uid) {
staticData.lastMessageUid = message.attributes.uid;
}
const part = lodash.find(message.parts, { which: 'TEXT' }); const part = lodash.find(message.parts, { which: 'TEXT' });
if (part === undefined) { if (part === undefined) {
@ -366,6 +387,33 @@ export class EmailReadImap implements INodeType {
}, },
onmail: async () => { onmail: async () => {
if (connection) { if (connection) {
let searchCriteria = [
'UNSEEN',
] as Array<string | string[]>;
if (options.customEmailConfig !== undefined) {
try {
searchCriteria = JSON.parse(options.customEmailConfig as string);
} catch (error) {
throw new NodeOperationError(this.getNode(), `Custom email config is not valid JSON.`);
}
}
if (staticData.lastMessageUid !== undefined) {
searchCriteria.push(['UID', `${staticData.lastMessageUid as number}:*`]);
/**
* A short explanation about UIDs and how they work
* can be found here: https://dev.to/kehers/imap-new-messages-since-last-check-44gm
* TL;DR:
* - You cannot filter using ['UID', 'CURRENT ID + 1:*'] because IMAP
* won't return correct results if current id + 1 does not yet exist.
* - UIDs can change but this is not being treated here.
* If the mailbox is recreated (lets say you remove all emails, remove
* the mail box and create another with same name, UIDs will change)
* - You can check if UIDs changed in the above example
* by checking UIDValidity.
*/
Logger.debug('Querying for new messages on node "EmailReadImap"', {searchCriteria});
}
const returnData = await getNewEmails(connection, searchCriteria); const returnData = await getNewEmails(connection, searchCriteria);
if (returnData.length) { if (returnData.length) {
@ -386,7 +434,9 @@ export class EmailReadImap implements INodeType {
return imapConnect(config).then(async conn => { return imapConnect(config).then(async conn => {
conn.on('error', async err => { conn.on('error', async err => {
if (err.code.toUpperCase() === 'ECONNRESET') { if (err.code.toUpperCase() === 'ECONNRESET') {
Logger.verbose('IMAP connection was reset - reconnecting.');
connection = await establishConnection(); connection = await establishConnection();
await connection.openBox(mailbox);
} }
throw err; throw err;
}); });
@ -398,8 +448,22 @@ export class EmailReadImap implements INodeType {
await connection.openBox(mailbox); await connection.openBox(mailbox);
let reconnectionInterval: NodeJS.Timeout | undefined;
if (options.forceReconnect !== undefined) {
reconnectionInterval = setInterval(async () => {
Logger.verbose('Forcing reconnection of IMAP node.');
await connection.end();
connection = await establishConnection();
await connection.openBox(mailbox);
}, options.forceReconnect as number * 1000 * 60);
}
// When workflow and so node gets set to inactive close the connectoin // When workflow and so node gets set to inactive close the connectoin
async function closeFunction() { async function closeFunction() {
if (reconnectionInterval) {
clearInterval(reconnectionInterval);
}
await connection.end(); await connection.end();
} }

View file

@ -29,5 +29,10 @@
"url": "https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/" "url": "https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Helpers"
]
} }
} }

View file

@ -210,7 +210,7 @@ export class EmailSend implements INodeType {
// Send the email // Send the email
const info = await transporter.sendMail(mailOptions); const info = await transporter.sendMail(mailOptions);
returnData.push({ json: info }); returnData.push({ json: info as unknown as IDataObject });
} }
return this.prepareOutputData(returnData); return this.prepareOutputData(returnData);

View file

@ -20,5 +20,10 @@
"url": "https://n8n.io/blog/creating-error-workflows-in-n8n/" "url": "https://n8n.io/blog/creating-error-workflows-in-n8n/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Helpers"
]
} }
} }

View file

@ -20,5 +20,10 @@
"url": "https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/" "url": "https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Helpers"
]
} }
} }

View file

@ -12,5 +12,10 @@
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.executeWorkflow/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.executeWorkflow/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Helpers"
]
} }
} }

View file

@ -100,5 +100,10 @@
"url": "https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/" "url": "https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Data Transformation"
]
} }
} }

View file

@ -12,5 +12,10 @@
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.functionItem/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.functionItem/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Data Transformation"
]
} }
} }

View file

@ -15,6 +15,13 @@
{ {
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleAnalytics/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleAnalytics/"
} }
],
"generic": [
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
}
] ]
} }
} }

View file

@ -27,6 +27,11 @@
"icon": "🎫", "icon": "🎫",
"url": "https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/" "url": "https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/"
}, },
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
},
{ {
"label": "Hey founders! Your business doesn't need you to operate", "label": "Hey founders! Your business doesn't need you to operate",
"icon": " 🖥️", "icon": " 🖥️",

View file

@ -15,6 +15,18 @@
{ {
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleCloudNaturalLanguage/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleCloudNaturalLanguage/"
} }
],
"generic": [
{
"label": "Automate your data processing pipeline in 9 steps",
"icon": "⚙️",
"url": "https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/"
},
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
}
] ]
} }
} }

View file

@ -1970,7 +1970,6 @@ export class GoogleDrive implements INodeType {
// ---------------------------------- // ----------------------------------
// list // list
// ---------------------------------- // ----------------------------------
const returnAll = this.getNodeParameter('returnAll', i) as boolean; const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const qs: IDataObject = {}; const qs: IDataObject = {};
@ -1986,6 +1985,7 @@ export class GoogleDrive implements INodeType {
const data = await googleApiRequest.call(this, 'GET', `/drive/v3/drives`, {}, qs); const data = await googleApiRequest.call(this, 'GET', `/drive/v3/drives`, {}, qs);
response = data.drives as IDataObject[]; response = data.drives as IDataObject[];
} }
returnData.push.apply(returnData, response); returnData.push.apply(returnData, response);
} }
if (operation === 'update') { if (operation === 'update') {
@ -2004,7 +2004,8 @@ export class GoogleDrive implements INodeType {
returnData.push(response as IDataObject); returnData.push(response as IDataObject);
} }
} else if (resource === 'file') { }
if (resource === 'file') {
if (operation === 'copy') { if (operation === 'copy') {
// ---------------------------------- // ----------------------------------
// copy // copy
@ -2264,7 +2265,8 @@ export class GoogleDrive implements INodeType {
returnData.push(responseData as IDataObject); returnData.push(responseData as IDataObject);
} }
} else if (resource === 'folder') { }
if (resource === 'folder') {
if (operation === 'create') { if (operation === 'create') {
// ---------------------------------- // ----------------------------------
// folder:create // folder:create
@ -2326,11 +2328,8 @@ export class GoogleDrive implements INodeType {
returnData.push(response as IDataObject); returnData.push(response as IDataObject);
} }
} else {
throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known!`);
} }
} }
if (resource === 'file' && operation === 'download') { if (resource === 'file' && operation === 'download') {
// For file downloads the files get attached to the existing items // For file downloads the files get attached to the existing items
return this.prepareOutputData(items); return this.prepareOutputData(items);

View file

@ -12,6 +12,8 @@ import {
IDataObject, NodeApiError, IDataObject, NodeApiError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import * as moment from 'moment-timezone';
export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri: string | null = null): Promise<any> { // tslint:disable-line:no-any export async function googleApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri: string | null = null): Promise<any> { // tslint:disable-line:no-any
const options: OptionsWithUri = { const options: OptionsWithUri = {
@ -58,6 +60,7 @@ export async function googleApiRequestAllItems(this: IExecuteFunctions | ILoadOp
return returnData; return returnData;
} }
const isValidDate = (str: string) => moment(str, ['YYYY-MM-DD HH:mm:ss Z', moment.ISO_8601], true).isValid();
// Both functions below were taken from Stack Overflow jsonToDocument was fixed as it was unable to handle null values correctly // Both functions below were taken from Stack Overflow jsonToDocument was fixed as it was unable to handle null values correctly
// https://stackoverflow.com/questions/62246410/how-to-convert-a-firestore-document-to-plain-json-and-vice-versa // https://stackoverflow.com/questions/62246410/how-to-convert-a-firestore-document-to-plain-json-and-vice-versa
@ -73,7 +76,7 @@ export function jsonToDocument(value: string | number | IDataObject | IDataObjec
} else { } else {
return { 'integerValue': value }; return { 'integerValue': value };
} }
} else if (Date.parse(value as string)) { } else if (isValidDate(value as string)) {
const date = new Date(Date.parse(value as string)); const date = new Date(Date.parse(value as string));
return { 'timestampValue': date.toISOString() }; return { 'timestampValue': date.toISOString() };
} else if (typeof value === 'string') { } else if (typeof value === 'string') {
@ -108,13 +111,14 @@ export function fullDocumentToJson(data: IDataObject): IDataObject {
export function documentToJson(fields: IDataObject): IDataObject { export function documentToJson(fields: IDataObject): IDataObject {
if (fields === undefined) return {};
const result = {}; const result = {};
for (const f of Object.keys(fields)) { for (const f of Object.keys(fields)) {
const key = f, value = fields[f], const key = f, value = fields[f],
isDocumentType = ['stringValue', 'booleanValue', 'doubleValue', isDocumentType = ['stringValue', 'booleanValue', 'doubleValue',
'integerValue', 'timestampValue', 'mapValue', 'arrayValue', 'nullValue'].find(t => t === key); 'integerValue', 'timestampValue', 'mapValue', 'arrayValue', 'nullValue', 'geoPointValue'].find(t => t === key);
if (isDocumentType) { if (isDocumentType) {
const item = ['stringValue', 'booleanValue', 'doubleValue', 'integerValue', 'timestampValue', 'nullValue'] const item = ['stringValue', 'booleanValue', 'doubleValue', 'integerValue', 'timestampValue', 'nullValue', 'geoPointValue']
.find(t => t === key); .find(t => t === key);
if (item) { if (item) {
return value as IDataObject; return value as IDataObject;

View file

@ -15,6 +15,13 @@
{ {
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleFirebaseRealtimeDatabase/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleFirebaseRealtimeDatabase/"
} }
],
"generic": [
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
}
] ]
} }
} }

View file

@ -15,6 +15,13 @@
{ {
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.gSuiteAdmin/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.gSuiteAdmin/"
} }
],
"generic": [
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
}
] ]
} }
} }

View file

@ -27,6 +27,11 @@
"icon": "🎫", "icon": "🎫",
"url": "https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/" "url": "https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/"
}, },
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
},
{ {
"label": "Hey founders! Your business doesn't need you to operate", "label": "Hey founders! Your business doesn't need you to operate",
"icon": " 🖥️", "icon": " 🖥️",

View file

@ -286,7 +286,7 @@ export class GoogleSheet {
throw new NodeOperationError(this.executeFunctions.getNode(), `The range "${range}" is not valid.`); throw new NodeOperationError(this.executeFunctions.getNode(), `The range "${range}" is not valid.`);
} }
const keyRowRange = `${sheet ? sheet + '!' : ''}${rangeStartSplit[1]}${dataStartRowIndex}:${rangeEndSplit[1]}${dataStartRowIndex}`; const keyRowRange = `${sheet ? sheet + '!' : ''}${rangeStartSplit[1]}${keyRowIndex + 1}:${rangeEndSplit[1]}${keyRowIndex + 1}`;
const sheetDatakeyRow = await this.getData(this.encodeRange(keyRowRange), valueRenderMode); const sheetDatakeyRow = await this.getData(this.encodeRange(keyRowRange), valueRenderMode);
@ -302,7 +302,7 @@ export class GoogleSheet {
throw new NodeOperationError(this.executeFunctions.getNode(), `Could not find column for key "${indexKey}"!`); throw new NodeOperationError(this.executeFunctions.getNode(), `Could not find column for key "${indexKey}"!`);
} }
const startRowIndex = rangeStartSplit[2] || ''; const startRowIndex = rangeStartSplit[2] || dataStartRowIndex;
const endRowIndex = rangeEndSplit[2] || ''; const endRowIndex = rangeEndSplit[2] || '';
const keyColumn = this.getColumnWithOffset(rangeStartSplit[1], keyIndex); const keyColumn = this.getColumnWithOffset(rangeStartSplit[1], keyIndex);

View file

@ -48,6 +48,11 @@
"icon": "📈", "icon": "📈",
"url": "https://n8n.io/blog/migrating-community-metrics-to-orbit-using-n8n/" "url": "https://n8n.io/blog/migrating-community-metrics-to-orbit-using-n8n/"
}, },
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
},
{ {
"label": "Hey founders! Your business doesn't need you to operate", "label": "Hey founders! Your business doesn't need you to operate",
"icon": " 🖥️", "icon": " 🖥️",

View file

@ -15,6 +15,13 @@
{ {
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleSlides/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.googleSlides/"
} }
],
"generic": [
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
}
] ]
} }
} }

View file

@ -25,7 +25,37 @@ export class GraphQL implements INodeType {
}, },
inputs: ['main'], inputs: ['main'],
outputs: ['main'], outputs: ['main'],
credentials: [
{
name: 'httpHeaderAuth',
required: true,
displayOptions: {
show: {
authentication: [
'headerAuth',
],
},
},
},
],
properties: [ properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Header Auth',
value: 'headerAuth',
},
{
name: 'None',
value: 'none',
},
],
default: 'none',
description: 'The way to authenticate.',
},
{ {
displayName: 'HTTP Request Method', displayName: 'HTTP Request Method',
name: 'requestMethod', name: 'requestMethod',
@ -200,6 +230,7 @@ export class GraphQL implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData(); const items = this.getInputData();
const httpHeaderAuth = this.getCredentials('httpHeaderAuth');
let requestOptions: OptionsWithUri & RequestPromiseOptions; let requestOptions: OptionsWithUri & RequestPromiseOptions;
@ -228,6 +259,11 @@ export class GraphQL implements INodeType {
rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean, rejectUnauthorized: !this.getNodeParameter('allowUnauthorizedCerts', itemIndex, false) as boolean,
}; };
// Add credentials if any are set
if (httpHeaderAuth !== undefined) {
requestOptions.headers![httpHeaderAuth.name as string] = httpHeaderAuth.value;
}
const gqlQuery = this.getNodeParameter('query', itemIndex, '') as string; const gqlQuery = this.getNodeParameter('query', itemIndex, '') as string;
if (requestMethod === 'GET') { if (requestMethod === 'GET') {
requestOptions.qs = { requestOptions.qs = {

View file

@ -23,5 +23,10 @@
"url": "https://n8n.io/blog/how-to-use-the-http-request-node-the-swiss-army-knife-for-workflow-automation/" "url": "https://n8n.io/blog/how-to-use-the-http-request-node-the-swiss-army-knife-for-workflow-automation/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Helpers"
]
} }
} }

View file

@ -48,6 +48,11 @@
"icon": "📜", "icon": "📜",
"url": "https://n8n.io/blog/world-poetry-day-workflow/" "url": "https://n8n.io/blog/world-poetry-day-workflow/"
}, },
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
},
{ {
"label": "Automate Designs with Bannerbear and n8n", "label": "Automate Designs with Bannerbear and n8n",
"icon": "🎨", "icon": "🎨",
@ -89,5 +94,10 @@
"url": "https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/" "url": "https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Helpers"
]
} }
} }

View file

@ -76,9 +76,7 @@ export async function hubspotApiRequestAllItems(this: IHookFunctions | IExecuteF
return returnData; return returnData;
} }
} while ( } while (
responseData['has-more'] !== undefined && responseData['hasMore'] || responseData['has-more']
responseData['has-more'] !== null &&
responseData['has-more'] !== false
); );
return returnData; return returnData;
} }

View file

@ -2106,7 +2106,7 @@ export class Hubspot implements INodeType {
responseData = await hubspotApiRequestAllItems.call(this, 'results', 'POST', endpoint, body, qs); responseData = await hubspotApiRequestAllItems.call(this, 'results', 'POST', endpoint, body, qs);
} else { } else {
qs.count = this.getNodeParameter('limit', 0) as number; body.limit = this.getNodeParameter('limit', 0) as number;
responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body, qs); responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body, qs);
responseData = responseData.results; responseData = responseData.results;
} }

View file

@ -0,0 +1,21 @@
{
"node": "n8n-nodes-base.iCal",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Core Nodes",
"Productivity"
],
"resources": {
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.iCal/"
}
]
},
"subcategories": {
"Core Nodes": [
"Files"
]
}
}

View file

@ -0,0 +1,358 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
promisify,
} from 'util';
import * as moment from 'moment-timezone';
import * as ics from 'ics';
const createEvent = promisify(ics.createEvent);
export class ICalendar implements INodeType {
description: INodeTypeDescription = {
displayName: 'iCalendar',
name: 'iCal',
icon: 'fa:calendar',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"]}}',
description: 'Create iCalendar file',
defaults: {
name: 'iCalendar',
color: '#408000',
},
inputs: ['main'],
outputs: ['main'],
credentials: [],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Create Event File',
value: 'createEventFile',
},
],
default: 'createEventFile',
},
{
displayName: 'Event Title',
name: 'title',
type: 'string',
default: '',
},
{
displayName: 'Start',
name: 'start',
type: 'dateTime',
default: '',
required: true,
description: 'Date and time at which the event begins. (For all-day events, the time will be ignored.)',
},
{
displayName: 'End',
name: 'end',
type: 'dateTime',
default: '',
required: true,
description: 'Date and time at which the event ends. (For all-day events, the time will be ignored.)',
},
{
displayName: 'All Day',
name: 'allDay',
type: 'boolean',
default: false,
description: 'Whether the event lasts all day or not.',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
description: 'The field that your iCalendar file will be<br />available under in the output.',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
operation: [
'createEventFile',
],
},
},
options: [
{
displayName: 'Attendees',
name: 'attendeesUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
placeholder: 'Add Attendee',
default: {},
options: [
{
displayName: 'Attendees',
name: 'attendeeValues',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
required: true,
default: '',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
required: true,
default: '',
},
{
displayName: 'RSVP',
name: 'rsvp',
type: 'boolean',
default: false,
description: `Whether the attendee has to confirm attendance or not.`,
},
],
},
],
},
{
displayName: 'Busy Status',
name: 'busyStatus',
type: 'options',
options: [
{
name: 'Busy',
value: 'BUSY',
},
{
name: 'Tentative',
value: 'TENTATIVE',
},
],
default: '',
description: 'Used to specify busy status for Microsoft applications, like Outlook.',
},
{
displayName: 'Calendar Name',
name: 'calName',
type: 'string',
default: '',
description: 'Specifies the calendar (not event) name. Used by Apple iCal and Microsoft Outlook (<a href="https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcical/1da58449-b97e-46bd-b018-a1ce576f3e6d" target="_blank">spec</a>).',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
},
{
displayName: 'File Name',
name: 'fileName',
type: 'string',
default: '',
description: 'The name of the file to be generated. Default value is event.ics.',
},
{
displayName: 'Geolocation',
name: 'geolocationUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
placeholder: 'Add Geolocation',
default: {},
options: [
{
displayName: 'Geolocation',
name: 'geolocationValues',
values: [
{
displayName: 'Latitude',
name: 'lat',
type: 'string',
default: '',
},
{
displayName: 'Longitude',
name: 'lon',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Location',
name: 'location',
type: 'string',
default: '',
description: 'The intended venue.',
},
{
displayName: 'Recurrence Rule',
name: 'recurrenceRule',
type: 'string',
default: '',
description: `A rule to define the repeat pattern of the event (RRULE). (<a href="https://icalendar.org/rrule-tool.html" target="_blank">Rule generator</a>)`,
},
{
displayName: 'Organizer',
name: 'organizerUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
placeholder: 'Add Organizer',
default: {},
options: [
{
displayName: 'Organizer',
name: 'organizerValues',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
required: true,
},
{
displayName: 'Email',
name: 'email',
type: 'string',
default: '',
required: true,
},
],
},
],
},
{
displayName: 'Sequence',
name: 'sequence',
type: 'number',
default: 0,
description: 'When sending an update for an event (with the same uid), defines the revision sequence number.',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Confirmed',
value: 'CONFIRMED',
},
{
name: 'Cancelled',
value: 'CANCELLED',
},
{
name: 'Tentative',
value: 'TENTATIVE',
},
],
default: 'CONFIRMED',
},
{
displayName: 'UID',
name: 'uid',
type: 'string',
default: '',
description: `Universally unique id for the event (will be auto-generated if not specified here). Should be globally unique.`,
},
{
displayName: 'URL',
name: 'url',
type: 'string',
default: '',
description: 'URL associated with event.',
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const length = (items.length as unknown) as number;
const returnData: INodeExecutionData[] = [];
const operation = this.getNodeParameter('operation', 0) as string;
if (operation === 'createEventFile') {
for (let i = 0; i < length; i++) {
const title = this.getNodeParameter('title', i) as string;
const allDay = this.getNodeParameter('allDay', i) as boolean;
const start = this.getNodeParameter('start', i) as string;
let end = this.getNodeParameter('end', i) as string;
end = (allDay) ? moment(end).utc().add(1, 'day').format() as string : end;
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
let fileName = 'event.ics';
if (additionalFields.fileName) {
fileName = additionalFields.fileName as string;
}
const data: ics.EventAttributes = {
title,
start: (moment(start).toArray().splice(0, (allDay) ? 3 : 6) as ics.DateArray),
end: (moment(end).toArray().splice(0, (allDay) ? 3 : 6) as ics.DateArray),
startInputType: 'utc',
endInputType: 'utc',
};
if (additionalFields.geolocationUi) {
data.geo = (additionalFields.geolocationUi as IDataObject).geolocationValues as ics.GeoCoordinates;
delete additionalFields.geolocationUi;
}
if (additionalFields.organizerUi) {
data.organizer = (additionalFields.organizerUi as IDataObject).organizerValues as ics.Person;
delete additionalFields.organizerUi;
}
if (additionalFields.attendeesUi) {
data.attendees = (additionalFields.attendeesUi as IDataObject).attendeeValues as ics.Attendee[];
delete additionalFields.attendeesUi;
}
Object.assign(data, additionalFields);
const buffer = Buffer.from(await createEvent(data) as string);
const binaryData = await this.helpers.prepareBinaryData(buffer, fileName, 'text/calendar');
returnData.push(
{
json: {},
binary: {
[binaryPropertyName]: binaryData,
},
},
);
}
}
return [returnData];
}
}

View file

@ -28,6 +28,16 @@
"icon": "🧬", "icon": "🧬",
"url": "https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/" "url": "https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/"
}, },
{
"label": "Automate your data processing pipeline in 9 steps",
"icon": "⚙️",
"url": "https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/"
},
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
},
{ {
"label": "5 workflow automations for Mattermost that we love at n8n", "label": "5 workflow automations for Mattermost that we love at n8n",
"icon": "🤖", "icon": "🤖",
@ -49,5 +59,10 @@
"url": "https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/" "url": "https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Flow"
]
} }
} }

View file

@ -23,5 +23,10 @@
"url": "https://n8n.io/blog/creating-triggers-for-n8n-workflows-using-polling/" "url": "https://n8n.io/blog/creating-triggers-for-n8n-workflows-using-polling/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Flow"
]
} }
} }

View file

@ -83,7 +83,7 @@ export const ecommerceOrderFields = [
{ {
displayName: 'Order Title', displayName: 'Order Title',
name: 'orderTitle', name: 'orderTitle',
type: 'dateTime', type: 'string',
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {

View file

@ -3,8 +3,8 @@
"nodeVersion": "1.0", "nodeVersion": "1.0",
"codexVersion": "1.0", "codexVersion": "1.0",
"categories": [ "categories": [
"Communication", "Development",
"Development" "Communication"
], ],
"resources": { "resources": {
"credentialDocumentation": [ "credentialDocumentation": [

View file

@ -32,6 +32,11 @@
"icon": "⏲", "icon": "⏲",
"url": "https://n8n.io/blog/creating-triggers-for-n8n-workflows-using-polling/" "url": "https://n8n.io/blog/creating-triggers-for-n8n-workflows-using-polling/"
}, },
{
"label": "15 Google apps you can combine and automate to increase productivity",
"icon": "💡",
"url": "https://n8n.io/blog/automate-google-apps-for-productivity/"
},
{ {
"label": "The ultimate guide to automate your video collaboration with Whereby, Mattermost, and n8n", "label": "The ultimate guide to automate your video collaboration with Whereby, Mattermost, and n8n",
"icon": "📹", "icon": "📹",

View file

@ -33,5 +33,10 @@
"url": "https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/" "url": "https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Flow"
]
} }
} }

View file

@ -27,6 +27,11 @@
"label": "Running n8n on ships: An interview with Maranics", "label": "Running n8n on ships: An interview with Maranics",
"icon": "🛳", "icon": "🛳",
"url": "https://n8n.io/blog/running-n8n-on-ships-an-interview-with-maranics/" "url": "https://n8n.io/blog/running-n8n-on-ships-an-interview-with-maranics/"
},
{
"label": "Automate your data processing pipeline in 9 steps",
"icon": "⚙️",
"url": "https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/"
} }
] ]
} }

View file

@ -11,5 +11,10 @@
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.moveBinaryData/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.moveBinaryData/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Files"
]
} }
} }

View file

@ -11,5 +11,10 @@
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.n8nTrigger/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.n8nTrigger/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Flow"
]
} }
} }

View file

@ -22,6 +22,11 @@
"icon": "☀️", "icon": "☀️",
"url": "https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/" "url": "https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/"
}, },
{
"label": "Automate your data processing pipeline in 9 steps",
"icon": "⚙️",
"url": "https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/"
},
{ {
"label": "5 workflow automations for Mattermost that we love at n8n", "label": "5 workflow automations for Mattermost that we love at n8n",
"icon": "🤖", "icon": "🤖",
@ -38,5 +43,10 @@
"url": "https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/" "url": "https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Helpers"
]
} }
} }

View file

@ -0,0 +1,123 @@
import {
INodeProperties,
} from 'n8n-workflow';
import {
blocks,
} from './Blocks';
export const blockOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'block',
],
},
},
options: [
{
name: 'Append',
value: 'append',
description: 'Append a block',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all children blocks',
},
],
default: 'append',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const blockFields = [
/* -------------------------------------------------------------------------- */
/* block:append */
/* -------------------------------------------------------------------------- */
{
displayName: 'Block ID',
name: 'blockId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'block',
],
operation: [
'append',
],
},
},
description: `The ID of block. A page it is also considered a block. Hence, a Page ID can be used as well.`,
},
...blocks('block', 'append'),
/* -------------------------------------------------------------------------- */
/* block:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Block ID',
name: 'blockId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'block',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'block',
],
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: [
'block',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -0,0 +1,571 @@
import {
IDisplayOptions,
INodeProperties,
} from 'n8n-workflow';
const colors = [
{
name: 'Default',
value: 'default',
},
{
name: 'Gray',
value: 'gray',
},
{
name: 'Brown',
value: 'brown',
},
{
name: 'Orange',
value: 'orange',
},
{
name: 'Yellow',
value: 'yellow',
},
{
name: 'Green',
value: 'green',
},
{
name: 'Blue',
value: 'blue',
},
{
name: 'Purple',
value: 'purple',
},
{
name: 'Pink',
value: 'pink',
},
{
name: 'Red',
value: 'red',
},
{
name: 'Gray Background',
value: 'gray_background',
},
{
name: 'Brown Background',
value: 'brown_background',
},
{
name: 'Orange Background',
value: 'orange_background',
},
{
name: 'Yellow Background',
value: 'yellow_background',
},
{
name: 'Green Background',
value: 'green_background',
},
{
name: 'Blue Background',
value: 'blue_background',
},
{
name: 'Purple Background',
value: 'purple_background',
},
{
name: 'Pink Background',
value: 'pink_background',
},
{
name: 'Red Background',
value: 'red_background',
},
];
const annotation = [
{
displayName: 'Annotations',
name: 'annotationUi',
type: 'collection',
placeholder: 'Add Annotation',
default: {},
options: [
{
displayName: 'Bold',
name: 'bold',
type: 'boolean',
default: false,
description: 'Whether the text is bolded.',
},
{
displayName: 'Italic',
name: 'italic',
type: 'boolean',
default: false,
description: 'Whether the text is italicized.',
},
{
displayName: 'Strikethrough',
name: 'strikethrough',
type: 'boolean',
default: false,
description: 'Whether the text is struck through.',
},
{
displayName: 'Underline',
name: 'underline',
type: 'boolean',
default: false,
description: 'Whether the text is underlined.',
},
{
displayName: 'Code',
name: 'code',
type: 'boolean',
default: false,
description: 'Whether the text is code style.',
},
{
displayName: 'Color',
name: 'color',
type: 'options',
options: colors,
default: '',
description: 'Color of the text.',
},
],
description: 'All annotations that apply to this rich text.',
},
] as INodeProperties[];
const typeMention = [
{
displayName: 'Type',
name: 'mentionType',
type: 'options',
displayOptions: {
show: {
textType: [
'mention',
],
},
},
options: [
{
name: 'Database',
value: 'database',
},
{
name: 'Date',
value: 'date',
},
{
name: 'Page',
value: 'page',
},
{
name: 'User',
value: 'user',
},
],
default: '',
description: `An inline mention of a user, page, database, or date. In the app these are</br>
created by typing @ followed by the name of a user, page, database, or a date.`,
},
{
displayName: 'User ID',
name: 'user',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
displayOptions: {
show: {
mentionType: [
'user',
],
},
},
default: '',
description: 'The id of the user being mentioned.',
},
{
displayName: 'Page ID',
name: 'page',
type: 'string',
displayOptions: {
show: {
mentionType: [
'page',
],
},
},
default: '',
description: 'The id of the page being mentioned.',
},
{
displayName: 'Database ID',
name: 'database',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDatabases',
},
displayOptions: {
show: {
mentionType: [
'database',
],
},
},
default: '',
description: 'The id of the database being mentioned.',
},
{
displayName: 'Range',
name: 'range',
displayOptions: {
show: {
mentionType: [
'date',
],
},
},
type: 'boolean',
default: false,
description: 'Weather or not you want to define a date range.',
},
{
displayName: 'Date',
name: 'date',
displayOptions: {
show: {
mentionType: [
'date',
],
range: [
false,
],
},
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
},
{
displayName: 'Date Start',
name: 'dateStart',
displayOptions: {
show: {
mentionType: [
'date',
],
range: [
true,
],
},
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
},
{
displayName: 'Date End',
name: 'dateEnd',
displayOptions: {
show: {
range: [
true,
],
mentionType: [
'date',
],
},
},
type: 'dateTime',
default: '',
description: `An ISO 8601 formatted date, with optional time. Represents the end of a date range.`,
},
] as INodeProperties[];
const typeEquation = [
{
displayName: 'Expression',
name: 'expression',
type: 'string',
displayOptions: {
show: {
textType: [
'equation',
],
},
},
default: '',
description: '',
},
] as INodeProperties[];
const typeText = [
{
displayName: 'Text',
name: 'text',
displayOptions: {
show: {
textType: [
'text',
],
},
},
type: 'string',
default: '',
description: `Text content. This field contains the actual content</br>
of your text and is probably the field you'll use most often.`,
},
{
displayName: 'Is Link',
name: 'isLink',
displayOptions: {
show: {
textType: [
'text',
],
},
},
type: 'boolean',
default: false,
},
{
displayName: 'Text Link',
name: 'textLink',
displayOptions: {
show: {
textType: [
'text',
],
isLink: [
true,
],
},
},
type: 'string',
default: '',
description: 'The URL that this link points to.',
},
] as INodeProperties[];
export const text = (displayOptions: IDisplayOptions) => [
{
displayName: 'Text',
name: 'text',
placeholder: 'Add Text',
type: 'fixedCollection',
default: '',
typeOptions: {
multipleValues: true,
},
displayOptions,
options: [
{
name: 'text',
displayName: 'Text',
values: [
{
displayName: 'Type',
name: 'textType',
type: 'options',
options: [
{
name: 'Equation',
value: 'equation',
},
{
name: 'Mention',
value: 'mention',
},
{
name: 'Text',
value: 'text',
},
],
default: 'text',
description: '',
},
...typeText,
...typeMention,
...typeEquation,
...annotation,
],
},
],
description: 'Rich text in the block.',
}] as INodeProperties[];
const todo = (type: string) => [{
displayName: 'Checked',
name: 'checked',
type: 'boolean',
default: false,
displayOptions: {
show: {
type: [
type,
],
},
},
description: 'Whether the to_do is checked or not.',
}] as INodeProperties[];
const title = (type: string) => [{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
displayOptions: {
show: {
type: [
type,
],
},
},
description: 'Plain text of page title.',
}] as INodeProperties[];
const richText = (displayOptions: IDisplayOptions) => [
{
displayName: 'Rich Text',
name: 'richText',
type: 'boolean',
displayOptions,
default: false,
},
] as INodeProperties[];
const textContent = (displayOptions: IDisplayOptions) => [
{
displayName: 'Text',
name: 'textContent',
type: 'string',
displayOptions,
default: '',
},
] as INodeProperties[];
const block = (blockType: string) => {
const data: INodeProperties[] = [];
switch (blockType) {
case 'to_do':
data.push(...todo(blockType));
data.push(...richText({
show: {
type: [
blockType,
],
},
}));
data.push(...textContent({
show: {
type: [
blockType,
],
richText: [
false,
],
},
}));
data.push(...text({
show: {
type: [
blockType,
],
richText: [
true,
],
},
}));
break;
case 'child_page':
data.push(...title(blockType));
break;
default:
data.push(...richText({
show: {
type: [
blockType,
],
},
}));
data.push(...textContent({
show: {
type: [
blockType,
],
richText: [
false,
],
},
}));
data.push(...text({
show: {
type: [
blockType,
],
richText: [
true,
],
},
}));
break;
}
return data;
};
export const blocks = (resource: string, operation: string) => [{
displayName: 'Blocks',
name: 'blockUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: '',
displayOptions: {
show: {
resource: [
resource,
],
operation: [
operation,
],
},
},
placeholder: 'Add Block',
options: [
{
name: 'blockValues',
displayName: 'Block',
values: [
{
displayName: 'Type',
name: 'type',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getBlockTypes',
},
description: 'Type of block',
default: 'paragraph',
},
...block('paragraph'),
...block('heading_1'),
...block('heading_2'),
...block('heading_3'),
...block('toggle'),
...block('to_do'),
...block('child_page'),
...block('bulleted_list_item'),
...block('numbered_list_item'),
],
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,100 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const databaseOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'database',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get a database',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all databases',
},
],
default: 'get',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const databaseFields = [
/* -------------------------------------------------------------------------- */
/* database:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Database ID',
name: 'databaseId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'database',
],
operation: [
'get',
],
},
},
},
/* -------------------------------------------------------------------------- */
/* database:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'database',
],
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: [
'database',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -0,0 +1,994 @@
import {
INodeProperties,
} from 'n8n-workflow';
import {
blocks,
text,
} from './Blocks';
import {
filters,
} from './Filters';
export const databasePageOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'databasePage',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a pages in a database',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all pages in a database',
},
{
name: 'Update',
value: 'update',
description: 'Update pages in a database',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const databasePageFields = [
/* -------------------------------------------------------------------------- */
/* databasePage:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Database ID',
name: 'databaseId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getDatabases',
},
required: true,
displayOptions: {
show: {
resource: [
'databasePage',
],
operation: [
'create',
],
},
},
description: 'The ID of the database that this databasePage belongs to.',
},
{
displayName: 'Simple',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: [
'databasePage',
],
operation: [
'create',
],
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
},
{
displayName: 'Properties',
name: 'propertiesUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: [
'databasePage',
],
operation: [
'create',
],
},
},
default: '',
placeholder: 'Add Property',
options: [
{
name: 'propertyValues',
displayName: 'Property',
values: [
{
displayName: 'Key',
name: 'key',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDatabaseProperties',
loadOptionsDependsOn: [
'databaseId',
],
},
default: '',
},
{
displayName: 'Type',
name: 'type',
type: 'hidden',
default: '={{$parameter["&key"].split("|")[1]}}',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
displayOptions: {
show: {
type: [
'title',
],
},
},
default: '',
},
{
displayName: 'Rich Text',
name: 'richText',
type: 'boolean',
displayOptions: {
show: {
type: [
'rich_text',
],
},
},
default: false,
},
{
displayName: 'Text',
name: 'textContent',
type: 'string',
displayOptions: {
show: {
type: [
'rich_text',
],
richText: [
false,
],
},
},
default: '',
},
...text({
show: {
type: [
'rich_text',
],
richText: [
true,
],
},
}),
{
displayName: 'Phone Number',
name: 'phoneValue',
type: 'string',
displayOptions: {
show: {
type: [
'phone_number',
],
},
},
default: '',
description: `Phone number. No structure is enforced.`,
},
{
displayName: 'Options',
name: 'multiSelectValue',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getPropertySelectValues',
},
displayOptions: {
show: {
type: [
'multi_select',
],
},
},
default: [],
description: `Name of the options you want to set.
Multiples can be defined separated by comma.`,
},
{
displayName: 'Option',
name: 'selectValue',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getPropertySelectValues',
},
displayOptions: {
show: {
type: [
'select',
],
},
},
default: '',
description: `Name of the option you want to set.`,
},
{
displayName: 'Email',
name: 'emailValue',
type: 'string',
displayOptions: {
show: {
type: [
'email',
],
},
},
default: '',
description: 'Email address.',
},
{
displayName: 'URL',
name: 'urlValue',
type: 'string',
displayOptions: {
show: {
type: [
'url',
],
},
},
default: '',
description: 'Web address.',
},
{
displayName: 'User IDs',
name: 'peopleValue',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
displayOptions: {
show: {
type: [
'people',
],
},
},
default: [],
description: 'List of users. Multiples can be defined separated by comma.',
},
{
displayName: 'Relation IDs',
name: 'relationValue',
type: 'string',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
type: [
'relation',
],
},
},
default: [],
description: 'List of databases that belong to another database. Multiples can be defined separated by comma.',
},
{
displayName: 'Checked',
name: 'checkboxValue',
displayOptions: {
show: {
type: [
'checkbox',
],
},
},
type: 'boolean',
default: false,
description: `
Whether or not the checkbox is checked.</br>
true represents checked.</br>
false represents unchecked.
`,
},
{
displayName: 'Number',
name: 'numberValue',
displayOptions: {
show: {
type: [
'number',
],
},
},
type: 'number',
default: 0,
description: 'Number value.',
},
{
displayName: 'Range',
name: 'range',
displayOptions: {
show: {
type: [
'date',
],
},
},
type: 'boolean',
default: false,
description: 'Weather or not you want to define a date range.',
},
{
displayName: 'Date',
name: 'date',
displayOptions: {
show: {
range: [
false,
],
type: [
'date',
],
},
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
},
{
displayName: 'Date Start',
name: 'dateStart',
displayOptions: {
show: {
range: [
true,
],
type: [
'date',
],
},
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
},
{
displayName: 'Date End',
name: 'dateEnd',
displayOptions: {
show: {
range: [
true,
],
type: [
'date',
],
},
},
type: 'dateTime',
default: '',
description: `
An ISO 8601 formatted date, with optional time. Represents the end of a date range.`,
},
],
},
],
},
...blocks('databasePage', 'create'),
/* -------------------------------------------------------------------------- */
/* databasePage:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Page ID',
name: 'pageId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'databasePage',
],
operation: [
'update',
],
},
},
description: 'The ID of the databasePage to update.',
},
{
displayName: 'Simple',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: [
'databasePage',
],
operation: [
'update',
],
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
},
{
displayName: 'Properties',
name: 'propertiesUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: [
'databasePage',
],
operation: [
'update',
],
},
},
default: '',
placeholder: 'Add Property',
options: [
{
name: 'propertyValues',
displayName: 'Property',
values: [
{
displayName: 'Key',
name: 'key',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDatabaseIdFromPage',
loadOptionsDependsOn: [
'pageId',
],
},
default: '',
},
{
displayName: 'Type',
name: 'type',
type: 'hidden',
default: '={{$parameter["&key"].split("|")[1]}}',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
displayOptions: {
show: {
type: [
'title',
],
},
},
default: '',
},
{
displayName: 'Rich Text',
name: 'richText',
type: 'boolean',
displayOptions: {
show: {
type: [
'rich_text',
],
},
},
default: false,
},
{
displayName: 'Text',
name: 'textContent',
type: 'string',
displayOptions: {
show: {
type: [
'rich_text',
],
richText: [
false,
],
},
},
default: '',
},
...text({
show: {
type: [
'rich_text',
],
richText: [
true,
],
},
}),
{
displayName: 'Phone Number',
name: 'phoneValue',
type: 'string',
displayOptions: {
show: {
type: [
'phone_number',
],
},
},
default: '',
description: `Phone number. No structure is enforced.`,
},
{
displayName: 'Options',
name: 'multiSelectValue',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getDatabaseOptionsFromPage',
},
displayOptions: {
show: {
type: [
'multi_select',
],
},
},
default: [],
description: `Name of the options you want to set.
Multiples can be defined separated by comma.`,
},
{
displayName: 'Option',
name: 'selectValue',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDatabaseOptionsFromPage',
},
displayOptions: {
show: {
type: [
'select',
],
},
},
default: '',
description: `Name of the option you want to set.`,
},
{
displayName: 'Email',
name: 'emailValue',
type: 'string',
displayOptions: {
show: {
type: [
'email',
],
},
},
default: '',
description: 'Email address.',
},
{
displayName: 'URL',
name: 'urlValue',
type: 'string',
displayOptions: {
show: {
type: [
'url',
],
},
},
default: '',
description: 'Web address.',
},
{
displayName: 'User IDs',
name: 'peopleValue',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
displayOptions: {
show: {
type: [
'people',
],
},
},
default: [],
description: 'List of users. Multiples can be defined separated by comma.',
},
{
displayName: 'Relation IDs',
name: 'relationValue',
type: 'string',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
type: [
'relation',
],
},
},
default: [],
description: 'List of databases that belong to another database. Multiples can be defined separated by comma.',
},
{
displayName: 'Checked',
name: 'checkboxValue',
displayOptions: {
show: {
type: [
'checkbox',
],
},
},
type: 'boolean',
default: false,
description: `
Whether or not the checkbox is checked.</br>
true represents checked.</br>
false represents unchecked.
`,
},
{
displayName: 'Number',
name: 'numberValue',
displayOptions: {
show: {
type: [
'number',
],
},
},
type: 'number',
default: 0,
description: 'Number value.',
},
{
displayName: 'Range',
name: 'range',
displayOptions: {
show: {
type: [
'date',
],
},
},
type: 'boolean',
default: false,
description: 'Weather or not you want to define a date range.',
},
{
displayName: 'Date',
name: 'date',
displayOptions: {
show: {
range: [
false,
],
type: [
'date',
],
},
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
},
{
displayName: 'Date Start',
name: 'dateStart',
displayOptions: {
show: {
range: [
true,
],
type: [
'date',
],
},
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
},
{
displayName: 'Date End',
name: 'dateEnd',
displayOptions: {
show: {
range: [
true,
],
type: [
'date',
],
},
},
type: 'dateTime',
default: '',
description: `
An ISO 8601 formatted date, with optional time. Represents the end of a date range.`,
},
],
},
],
},
/* -------------------------------------------------------------------------- */
/* databasePage:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Database ID',
name: 'databaseId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDatabases',
},
default: '',
required: true,
displayOptions: {
show: {
resource: [
'databasePage',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'databasePage',
],
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: [
'databasePage',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
{
displayName: 'Simple',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: [
'databasePage',
],
operation: [
'getAll',
],
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
operation: [
'getAll',
],
resource: [
'databasePage',
],
},
},
default: {},
placeholder: 'Add Field',
options: [
{
displayName: 'Filters',
name: 'filter',
placeholder: 'Add Filter',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
displayName: 'Single Condition',
name: 'singleCondition',
values: [
...filters,
],
},
{
displayName: 'Multiple Condition',
name: 'multipleCondition',
values: [
{
displayName: 'Condition',
name: 'condition',
placeholder: 'Add Condition',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'OR',
name: 'or',
values: [
...filters,
],
},
{
displayName: 'AND',
name: 'and',
values: [
...filters,
],
},
],
},
],
},
],
},
{
displayName: 'Sort',
name: 'sort',
placeholder: 'Add Sort',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Sort',
name: 'sortValue',
values: [
{
displayName: 'Timestamp',
name: 'timestamp',
type: 'boolean',
default: false,
description: `Whether or not to use the record's timestamp to sort the response.`,
},
{
displayName: 'Property Name',
name: 'key',
type: 'options',
displayOptions: {
show: {
timestamp: [
false,
],
},
},
typeOptions: {
loadOptionsMethod: 'getFilterProperties',
loadOptionsDependsOn: [
'datatabaseId',
],
},
default: '',
description: 'The name of the property to filter by.',
},
{
displayName: 'Property Name',
name: 'key',
type: 'options',
options: [
{
name: 'Created Time',
value: 'created_time',
},
{
name: 'Last Edited Time',
value: 'last_edited_time',
},
],
displayOptions: {
show: {
timestamp: [
true,
],
},
},
default: '',
description: 'The name of the property to filter by.',
},
{
displayName: 'Type',
name: 'type',
type: 'hidden',
displayOptions: {
show: {
timestamp: [
true,
],
},
},
default: '={{$parameter["&key"].split("|")[1]}}',
},
{
displayName: 'Direction',
name: 'direction',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ascending',
},
{
name: 'Descending',
value: 'descending',
},
],
default: '',
description: 'The direction to sort.',
},
],
},
],
},
],
},
] as INodeProperties[];

View file

@ -0,0 +1,371 @@
import {
getConditions
} from './GenericFunctions';
export const filters = [{
displayName: 'Property Name',
name: 'key',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getFilterProperties',
loadOptionsDependsOn: [
'datatabaseId',
],
},
default: '',
description: 'The name of the property to filter by.',
},
{
displayName: 'Type',
name: 'type',
type: 'hidden',
default: '={{$parameter["&key"].split("|")[1]}}',
},
...getConditions(),
{
displayName: 'Title',
name: 'titleValue',
type: 'string',
displayOptions: {
show: {
type: [
'title',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
default: '',
},
{
displayName: 'Text',
name: 'richTextValue',
type: 'string',
displayOptions: {
show: {
type: [
'rich_text',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
default: '',
},
{
displayName: 'Phone Number',
name: 'phoneNumberValue',
type: 'string',
displayOptions: {
show: {
type: [
'phone_number',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
default: '',
description: `Phone number. No structure is enforced.`,
},
{
displayName: 'Option',
name: 'multiSelectValue',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getPropertySelectValues',
},
displayOptions: {
show: {
type: [
'multi_select',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
default: [],
description: `Name of the options you want to set.
Multiples can be defined separated by comma.`,
},
{
displayName: 'Option',
name: 'selectValue',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getPropertySelectValues',
},
displayOptions: {
show: {
type: [
'select',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
default: '',
description: `Name of the option you want to set.`,
},
{
displayName: 'Email',
name: 'emailValue',
type: 'string',
displayOptions: {
show: {
type: [
'email',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
default: '',
description: 'Email address.',
},
{
displayName: 'URL',
name: 'urlValue',
type: 'string',
displayOptions: {
show: {
type: [
'url',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
default: '',
description: 'Web address.',
},
{
displayName: 'User ID',
name: 'peopleValue',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
displayOptions: {
show: {
type: [
'people',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
default: '',
description: 'List of users. Multiples can be defined separated by comma.',
},
{
displayName: 'User ID',
name: 'createdByValue',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
displayOptions: {
show: {
type: [
'created_by',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
default: '',
description: 'List of users. Multiples can be defined separated by comma.',
},
{
displayName: 'User ID',
name: 'lastEditedByValue',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
displayOptions: {
show: {
type: [
'last_edited_by',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
default: '',
description: 'List of users. Multiples can be defined separated by comma.',
},
{
displayName: 'Relation ID',
name: 'relationValue',
type: 'string',
displayOptions: {
show: {
type: [
'relation',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
default: '',
},
{
displayName: 'Checked',
name: 'checkboxValue',
displayOptions: {
show: {
type: [
'checkbox',
],
},
},
type: 'boolean',
default: false,
description: `Whether or not the checkbox is checked.</br>
true represents checked.</br>
false represents unchecked.`,
},
{
displayName: 'Number',
name: 'numberValue',
displayOptions: {
show: {
type: [
'number',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
type: 'number',
default: 0,
description: 'Number value.',
},
{
displayName: 'Date',
name: 'date',
displayOptions: {
show: {
type: [
'date',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
'past_week',
'past_month',
'past_year',
'next_week',
'next_month',
'next_year',
],
},
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
},
{
displayName: 'Created Time',
name: 'createdTimeValue',
displayOptions: {
show: {
type: [
'created_time',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
'past_week',
'past_month',
'past_year',
'next_week',
'next_month',
'next_year',
],
},
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
},
{
displayName: 'Last Edited Time',
name: 'lastEditedTime',
displayOptions: {
show: {
type: [
'last_edited_time',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
'past_week',
'past_month',
'past_year',
'next_week',
'next_month',
'next_year',
],
},
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
}];

View file

@ -0,0 +1,544 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
IExecuteSingleFunctions,
IHookFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
IDisplayOptions,
INodeProperties,
IPollFunctions,
NodeApiError,
} from 'n8n-workflow';
import {
camelCase,
capitalCase,
} from 'change-case';
import * as moment from 'moment-timezone';
export async function notionApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
try {
let options: OptionsWithUri = {
headers: {
'Notion-Version': '2021-05-13',
},
method,
qs,
body,
uri: uri || `https://api.notion.com/v1${resource}`,
json: true,
};
options = Object.assign({}, options, option);
const credentials = this.getCredentials('notionApi') as IDataObject;
options!.headers!['Authorization'] = `Bearer ${credentials.apiKey}`;
return this.helpers.request!(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
export async function notionApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
do {
responseData = await notionApiRequest.call(this, method, endpoint, body, query);
const { next_cursor } = responseData;
query['start_cursor'] = next_cursor;
body['start_cursor'] = next_cursor;
returnData.push.apply(returnData, responseData[propertyName]);
if (query.limit && query.limit <= returnData.length) {
return returnData;
}
} while (
responseData.has_more !== false
);
return returnData;
}
export function getBlockTypes() {
return [
{
name: 'Paragraph',
value: 'paragraph',
},
{
name: 'Heading 1',
value: 'heading_1',
},
{
name: 'Heading 2',
value: 'heading_2',
},
{
name: 'Heading 3',
value: 'heading_3',
},
{
name: 'Toggle',
value: 'toggle',
},
{
name: 'To-Do',
value: 'to_do',
},
// {
// name: 'Child Page',
// value: 'child_page',
// },
{
name: 'Bulleted List Item',
value: 'bulleted_list_item',
},
{
name: 'Numbered List Item',
value: 'numbered_list_item',
},
];
}
function textContent(content: string) {
return {
text: {
content,
},
};
}
export function formatTitle(content: string) {
return {
title: [
textContent(content),
],
};
}
export function formatText(content: string) {
return {
text: [
textContent(content),
],
};
}
function getLink(text: { textLink: string, isLink: boolean }) {
if (text.isLink === true && text.textLink !== '') {
return {
link: {
url: text.textLink,
},
};
}
return {};
}
function getTexts(texts: [{ textType: string, text: string, isLink: boolean, range: boolean, textLink: string, mentionType: string, dateStart: string, dateEnd: string, date: string, annotationUi: IDataObject, expression: string }]) {
const results = [];
for (const text of texts) {
if (text.textType === 'text') {
results.push({
type: 'text',
text: {
content: text.text,
...getLink(text),
},
annotations: text.annotationUi,
});
} else if (text.textType === 'mention') {
if (text.mentionType === 'date') {
results.push({
type: 'mention',
mention: {
type: text.mentionType,
[text.mentionType]: (text.range === true)
? { start: text.dateStart, end: text.dateEnd }
: { start: text.date, end: null },
},
annotations: text.annotationUi,
});
} else {
//@ts-ignore
results.push({
type: 'mention',
mention: {
type: text.mentionType,
//@ts-ignore
[text.mentionType]: { id: text[text.mentionType] as string },
},
annotations: text.annotationUi,
});
}
} else if (text.textType === 'equation') {
results.push({
type: 'equation',
equation: {
expression: text.expression,
},
annotations: text.annotationUi,
});
}
}
return results;
}
export function formatBlocks(blocks: IDataObject[]) {
const results = [];
for (const block of blocks) {
results.push({
object: 'block',
type: block.type,
[block.type as string]: {
...(block.type === 'to_do') ? { checked: block.checked } : { checked: false },
//@ts-expect-error
// tslint:disable-next-line: no-any
text: (block.richText === false) ? formatText(block.textContent).text : getTexts(block.text.text as any || []),
},
});
}
return results;
}
// tslint:disable-next-line: no-any
function getPropertyKeyValue(value: any, type: string, timezone: string) {
let result = {};
switch (type) {
case 'rich_text':
if (value.richText === false) {
result = { rich_text: [{ text: { content: value.textContent } }] };
} else {
result = { rich_text: getTexts(value.text.text) };
}
break;
case 'title':
result = { title: [{ text: { content: value.title } }] };
break;
case 'number':
result = { type: 'number', number: value.numberValue };
break;
case 'url':
result = { type: 'url', url: value.urlValue };
break;
case 'checkbox':
result = { type: 'checkbox', checkbox: value.checkboxValue };
break;
case 'relation':
result = {
// tslint:disable-next-line: no-any
type: 'relation', relation: (value.relationValue).reduce((acc: [], cur: any) => {
return acc.concat(cur.split(',').map((relation: string) => ({ id: relation })));
}, []),
};
break;
case 'multi_select':
result = {
// tslint:disable-next-line: no-any
type: 'multi_select', multi_select: value.multiSelectValue.filter((id: any) => id !== null).map((option: string) => ({ id: option })),
};
break;
case 'email':
result = {
type: 'email', email: value.emailValue,
};
break;
case 'people':
result = {
type: 'people', people: value.peopleValue.map((option: string) => ({ id: option })),
};
break;
case 'phone_number':
result = {
type: 'phone_number', phone_number: value.phoneValue,
};
break;
case 'select':
result = {
type: 'select', select: { id: value.selectValue },
};
break;
case 'date':
//&& value.dateStart !== 'Invalid date' && value.dateEnd !== 'Invalid date'
if (value.range === true) {
result = {
type: 'date', date: { start: moment.tz(value.dateStart, timezone).utc().format(), end: moment.tz(value.dateEnd, timezone).utc().format() },
};
//if (value.date !== 'Invalid date')
} else {
result = {
type: 'date', date: { start: moment.tz(value.date, timezone).utc().format(), end: null },
};
}
break;
default:
}
return result;
}
function getNameAndType(key: string) {
const [name, type] = key.split('|');
return {
name,
type,
};
}
export function mapProperties(properties: IDataObject[], timezone: string) {
return properties.reduce((obj, value) => Object.assign(obj, {
[`${(value.key as string).split('|')[0]}`]: getPropertyKeyValue(value, (value.key as string).split('|')[1], timezone),
}), {});
}
export function mapSorting(data: [{ key: string, type: string, direction: string, timestamp: boolean }]) {
return data.map((sort) => {
return {
direction: sort.direction,
[(sort.timestamp) ? 'timestamp' : 'property']: sort.key.split('|')[0],
};
});
}
export function mapFilters(filters: IDataObject[], timezone: string) {
// tslint:disable-next-line: no-any
return filters.reduce((obj, value: { [key: string]: any }) => {
let key = getNameAndType(value.key).type;
let valuePropertyName = value[`${camelCase(key)}Value`];
if (['is_empty', 'is_not_empty'].includes(value.condition as string)) {
valuePropertyName = true;
} else if (['past_week', 'past_month', 'past_year', 'next_week', 'next_month', 'next_year'].includes(value.condition as string)) {
valuePropertyName = {};
}
if (key === 'rich_text') {
key = 'text';
} else if (key === 'phone_number') {
key = 'phone';
} else if (key === 'date') {
valuePropertyName = (valuePropertyName !== undefined && !Object.keys(valuePropertyName).length) ? {} : moment.tz(value.date, timezone).utc().format();
}
return Object.assign(obj, {
['property']: getNameAndType(value.key).name,
[key]: { [`${value.condition}`]: valuePropertyName },
});
}, {});
}
// tslint:disable-next-line: no-any
export function simplifyProperties(properties: any) {
// tslint:disable-next-line: no-any
const results: any = {};
for (const key of Object.keys(properties)) {
const type = (properties[key] as IDataObject).type as string;
if (['text'].includes(properties[key].type)) {
const texts = properties[key].text.map((e: { plain_text: string }) => e.plain_text || {}).join('');
results[`${key}`] = texts;
} else if (['url', 'created_time', 'checkbox', 'number', 'last_edited_time', 'email', 'phone_number', 'date'].includes(properties[key].type)) {
// tslint:disable-next-line: no-any
results[`${key}`] = properties[key][type] as any;
} else if (['title'].includes(properties[key].type)) {
if (Array.isArray(properties[key][type]) && properties[key][type].length !== 0) {
results[`${key}`] = properties[key][type][0].plain_text;
} else {
results[`${key}`] = '';
}
} else if (['created_by', 'last_edited_by', 'select'].includes(properties[key].type)) {
results[`${key}`] = properties[key][type].name;
} else if (['people'].includes(properties[key].type)) {
if (Array.isArray(properties[key][type])) {
// tslint:disable-next-line: no-any
results[`${key}`] = properties[key][type].map((person: any) => person.person.email || {});
} else {
results[`${key}`] = properties[key][type];
}
} else if (['multi_select'].includes(properties[key].type)) {
if (Array.isArray(properties[key][type])) {
results[`${key}`] = properties[key][type].map((e: IDataObject) => e.name || {});
} else {
results[`${key}`] = properties[key][type].options.map((e: IDataObject) => e.name || {});
}
} else if (['relation'].includes(properties[key].type)) {
if (Array.isArray(properties[key][type])) {
results[`${key}`] = properties[key][type].map((e: IDataObject) => e.id || {});
} else {
results[`${key}`] = properties[key][type].database_id;
}
} else if (['formula'].includes(properties[key].type)) {
results[`${key}`] = properties[key][type][properties[key][type].type];
} else if (['rollup'].includes(properties[key].type)) {
//TODO figure how to resolve rollup field type
// results[`${key}`] = properties[key][type][properties[key][type].type];
}
}
return results;
}
// tslint:disable-next-line: no-any
export function simplifyObjects(objects: any) {
if (!Array.isArray(objects)) {
objects = [objects];
}
const results: IDataObject[] = [];
for (const { object, id, properties, parent, title } of objects) {
if (object === 'page' && (parent.type === 'page_id' || parent.type === 'workspace')) {
results.push({
id,
title: properties.title.title[0].plain_text,
});
} else if (object === 'page' && parent.type === 'database_id') {
results.push({
id,
...simplifyProperties(properties),
});
} else if (object === 'database') {
results.push({
id,
title: title[0].plain_text,
});
}
}
return results;
}
export function getFormattedChildren(children: IDataObject[]) {
const results: IDataObject[] = [];
for (const child of children) {
const type = child.type;
results.push({ [`${type}`]: child, object: 'block', type });
}
return results;
}
export function getConditions() {
const elements: INodeProperties[] = [];
const types: { [key: string]: string } = {
title: 'rich_text',
rich_text: 'rich_text',
number: 'number',
checkbox: 'checkbox',
select: 'select',
multi_select: 'multi_select',
date: 'date',
people: 'people',
files: 'files',
url: 'rich_text',
email: 'rich_text',
phone_number: 'rich_text',
relation: 'relation',
//formula: 'formula',
created_by: 'people',
created_time: 'date',
last_edited_by: 'people',
last_edited_time: 'date',
};
const typeConditions: { [key: string]: string[] } = {
rich_text: [
'equals',
'does_not_equal',
'contains',
'does_not_contain',
'starts_with',
'ends_with',
'is_empty',
'is_not_empty',
],
number: [
'equals',
'does_not_equal',
'grater_than',
'less_than',
'greater_than_or_equal_to',
'less_than_or_equal_to',
'is_empty',
'is_not_empty',
],
checkbox: [
'equals',
'does_not_equal',
],
select: [
'equals',
'does_not_equal',
'is_empty',
'is_not_empty',
],
multi_select: [
'contains',
'does_not_equal',
'is_empty',
'is_not_empty',
],
date: [
'equals',
'before',
'after',
'on_or_before',
'is_empty',
'is_not_empty',
'on_or_after',
'past_week',
'past_month',
'past_year',
'next_week',
'next_month',
'next_year',
],
people: [
'contains',
'does_not_contain',
'is_empty',
'is_not_empty',
],
files: [
'is_empty',
'is_not_empty',
],
relation: [
'contains',
'does_not_contain',
'is_empty',
'is_not_empty',
],
formula: [
'contains',
'does_not_contain',
'is_empty',
'is_not_empty',
],
};
for (const type of Object.keys(types)) {
elements.push(
{
displayName: 'Condition',
name: 'condition',
type: 'options',
displayOptions: {
show: {
type: [
type,
],
},
} as IDisplayOptions,
options: (typeConditions[types[type]] as string[]).map((type: string) => ({ name: capitalCase(type), value: type })),
default: '',
description: 'The value of the property to filter by.',
} as INodeProperties,
);
}
return elements;
}

View file

@ -0,0 +1,20 @@
{
"node": "n8n-nodes-base.notion",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Productivity"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/notion"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.notion/"
}
]
}
}

View file

@ -0,0 +1,517 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
formatBlocks,
formatTitle,
getBlockTypes,
mapFilters,
mapProperties,
mapSorting,
notionApiRequest,
notionApiRequestAllItems,
simplifyObjects,
} from './GenericFunctions';
import {
databaseFields,
databaseOperations,
} from './DatabaseDescription';
import {
userFields,
userOperations,
} from './UserDescription';
import {
pageFields,
pageOperations,
} from './PageDescription';
import {
blockFields,
blockOperations,
} from './BlockDescription';
import {
databasePageFields,
databasePageOperations,
} from './DatabasePageDescription';
export class Notion implements INodeType {
description: INodeTypeDescription = {
displayName: 'Notion (Beta)',
name: 'notion',
icon: 'file:notion.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Notion API (Beta)',
defaults: {
name: 'Notion',
color: '#000000',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'notionApi',
required: true,
// displayOptions: {
// show: {
// authentication: [
// 'apiKey',
// ],
// },
// },
},
// {
// name: 'notionOAuth2Api',
// required: true,
// displayOptions: {
// show: {
// authentication: [
// 'oAuth2',
// ],
// },
// },
// },
],
properties: [
// {
// displayName: 'Authentication',
// name: 'authentication',
// type: 'options',
// options: [
// {
// name: 'API Key',
// value: 'apiKey',
// },
// {
// name: 'OAuth2',
// value: 'oAuth2',
// },
// ],
// default: 'apiKey',
// description: 'The resource to operate on.',
// },
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Block',
value: 'block',
},
{
name: 'Database',
value: 'database',
},
{
name: 'Database Page',
value: 'databasePage',
},
{
name: 'Page',
value: 'page',
},
{
name: 'User',
value: 'user',
},
],
default: 'page',
description: 'Resource to consume.',
},
...blockOperations,
...blockFields,
...databaseOperations,
...databaseFields,
...databasePageOperations,
...databasePageFields,
...pageOperations,
...pageFields,
...userOperations,
...userFields,
],
};
methods = {
loadOptions: {
async getDatabases(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const body: IDataObject = {
page_size: 100,
filter: { property: 'object', value: 'database' },
};
const databases = await notionApiRequestAllItems.call(this, 'results', 'POST', `/search`, body);
for (const database of databases) {
returnData.push({
name: database.title[0].plain_text,
value: database.id,
});
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getDatabaseProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const databaseId = this.getCurrentNodeParameter('databaseId') as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
for (const key of Object.keys(properties)) {
//remove parameters that cannot be set from the API.
if (!['created_time', 'last_edited_time', 'created_by', 'last_edited_by', 'formula', 'files'].includes(properties[key].type)) {
returnData.push({
name: `${key} - (${properties[key].type})`,
value: `${key}|${properties[key].type}`,
});
}
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getFilterProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const databaseId = this.getCurrentNodeParameter('databaseId') as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
for (const key of Object.keys(properties)) {
returnData.push({
name: `${key} - (${properties[key].type})`,
value: `${key}|${properties[key].type}`,
});
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getBlockTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
return getBlockTypes();
},
async getPropertySelectValues(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const [name, type] = (this.getCurrentNodeParameter('&key') as string).split('|');
const databaseId = this.getCurrentNodeParameter('databaseId') as string;
const resource = this.getCurrentNodeParameter('resource') as string;
const operation = this.getCurrentNodeParameter('operation') as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
const useNames = (resource === 'databasePage' && operation === 'getAll');
return (properties[name][type].options).map((option: IDataObject) => ({ name: option.name, value: (['select', 'multi_select'].includes(type) && useNames) ? option.name : option.id }));
},
async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const users = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users');
for (const user of users) {
returnData.push({
name: user.name,
value: user.id,
});
}
return returnData;
},
async getDatabaseIdFromPage(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const pageId = this.getCurrentNodeParameter('pageId') as string;
const { parent: { database_id: databaseId } } = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
for (const key of Object.keys(properties)) {
//remove parameters that cannot be set from the API.
if (!['created_time', 'last_edited_time', 'created_by', 'last_edited_by', 'formula', 'files'].includes(properties[key].type)) {
returnData.push({
name: `${key} - (${properties[key].type})`,
value: `${key}|${properties[key].type}`,
});
}
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getDatabaseOptionsFromPage(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const pageId = this.getCurrentNodeParameter('pageId') as string;
const [name, type] = (this.getCurrentNodeParameter('&key') as string).split('|');
const { parent: { database_id: databaseId } } = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
return (properties[name][type].options).map((option: IDataObject) => ({ name: option.name, value: option.id }));
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
let responseData;
const qs: IDataObject = {};
const timezone = this.getTimezone();
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
if (resource === 'block') {
if (operation === 'append') {
for (let i = 0; i < length; i++) {
const blockId = this.getNodeParameter('blockId', i) as string;
const body: IDataObject = {
children: formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]),
};
const block = await notionApiRequest.call(this, 'PATCH', `/blocks/${blockId}/children`, body);
returnData.push(block);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const blockId = this.getNodeParameter('blockId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', `/blocks/${blockId}/children`, {});
} else {
qs.page_size = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequest.call(this, 'GET', `/blocks/${blockId}/children`, {});
responseData = responseData.results;
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'database') {
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const databaseId = this.getNodeParameter('databaseId', i) as string;
responseData = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
returnData.push(responseData);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const body: IDataObject = {
page_size: 100,
filter: { property: 'object', value: 'database' },
};
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', `/search`, body);
} else {
body['page_size'] = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequest.call(this, 'POST', `/search`, body);
responseData = responseData.results;
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'databasePage') {
if (operation === 'create') {
for (let i = 0; i < length; i++) {
const simple = this.getNodeParameter('simple', i) as boolean;
// tslint:disable-next-line: no-any
const body: { [key: string]: any } = {
parent: {},
properties: {},
};
body.parent['database_id'] = this.getNodeParameter('databaseId', i) as string;
const properties = this.getNodeParameter('propertiesUi.propertyValues', i, []) as IDataObject[];
if (properties.length !== 0) {
body.properties = mapProperties(properties, timezone) as IDataObject;
}
body.children = formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]);
responseData = await notionApiRequest.call(this, 'POST', '/pages', body);
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const simple = this.getNodeParameter('simple', 0) as boolean;
const databaseId = this.getNodeParameter('databaseId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const filters = this.getNodeParameter('options.filter', i, {}) as IDataObject;
const sort = this.getNodeParameter('options.sort.sortValue', i, []) as IDataObject[];
const body: IDataObject = {
filter: {},
};
if (filters.singleCondition) {
body['filter'] = mapFilters([filters.singleCondition] as IDataObject[], timezone);
}
if (filters.multipleCondition) {
const { or, and } = (filters.multipleCondition as IDataObject).condition as IDataObject;
if (Array.isArray(or) && or.length !== 0) {
Object.assign(body.filter, { or: (or as IDataObject[]).map((data) => mapFilters([data], timezone)) });
}
if (Array.isArray(and) && and.length !== 0) {
Object.assign(body.filter, { and: (and as IDataObject[]).map((data) => mapFilters([data], timezone)) });
}
}
if (!Object.keys(body.filter as IDataObject).length) {
delete body.filter;
}
if (sort) {
//@ts-expect-error
body['sorts'] = mapSorting(sort);
}
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', `/databases/${databaseId}/query`, body, {});
} else {
body.page_size = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequest.call(this, 'POST', `/databases/${databaseId}/query`, body, qs);
responseData = responseData.results;
}
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, responseData);
}
}
if (operation === 'update') {
for (let i = 0; i < length; i++) {
const pageId = this.getNodeParameter('pageId', i) as string;
const simple = this.getNodeParameter('simple', i) as boolean;
const properties = this.getNodeParameter('propertiesUi.propertyValues', i, []) as IDataObject[];
// tslint:disable-next-line: no-any
const body: { [key: string]: any } = {
properties: {},
};
if (properties.length !== 0) {
body.properties = mapProperties(properties, timezone) as IDataObject;
}
responseData = await notionApiRequest.call(this, 'PATCH', `/pages/${pageId}`, body);
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
}
if (resource === 'user') {
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const userId = this.getNodeParameter('userId', i) as string;
responseData = await notionApiRequest.call(this, 'GET', `/users/${userId}`);
returnData.push(responseData);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users');
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users');
responseData = responseData.splice(0, qs.limit);
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'page') {
if (operation === 'create') {
for (let i = 0; i < length; i++) {
const simple = this.getNodeParameter('simple', i) as boolean;
// tslint:disable-next-line: no-any
const body: { [key: string]: any } = {
parent: {},
properties: {},
};
body.parent['page_id'] = this.getNodeParameter('pageId', i) as string;
body.properties = formatTitle(this.getNodeParameter('title', i) as string);
body.children = formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]);
responseData = await notionApiRequest.call(this, 'POST', '/pages', body);
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const pageId = this.getNodeParameter('pageId', i) as string;
const simple = this.getNodeParameter('simple', i) as boolean;
responseData = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'search') {
for (let i = 0; i < length; i++) {
const text = this.getNodeParameter('text', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const simple = this.getNodeParameter('simple', i) as boolean;
const body: IDataObject = {};
if (text) {
body['query'] = text;
}
if (options.filter) {
const filter = (options.filter as IDataObject || {}).filters as IDataObject[] || [];
body['filter'] = filter;
}
if (options.sort) {
const sort = (options.sort as IDataObject || {}).sortValue as IDataObject || {};
body['sort'] = sort;
}
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', '/search', body);
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', '/search', body);
responseData = responseData.splice(0, qs.limit);
}
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, responseData);
}
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View file

@ -0,0 +1,20 @@
{
"node": "n8n-nodes-base.notionTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": [
"Productivity"
],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/notion"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.notionTrigger/"
}
]
}
}

View file

@ -0,0 +1,188 @@
import {
IPollFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
notionApiRequest,
simplifyObjects,
} from './GenericFunctions';
import * as moment from 'moment';
export class NotionTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Notion Trigger (Beta)',
name: 'notionTrigger',
icon: 'file:notion.svg',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when Notion events occur',
subtitle: '={{$parameter["event"]}}',
defaults: {
name: 'Notion Trigger',
color: '#000000',
},
credentials: [
{
name: 'notionApi',
required: true,
},
],
polling: true,
inputs: [],
outputs: ['main'],
properties: [
{
displayName: 'Event',
name: 'event',
type: 'options',
options: [
{
name: 'Page Added to Database',
value: 'pageAddedToDatabase',
},
// {
// name: 'Record Updated',
// value: 'recordUpdated',
// },
],
required: true,
default: '',
},
{
displayName: 'Database',
name: 'databaseId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getDatabases',
},
displayOptions: {
show: {
event: [
'pageAddedToDatabase',
],
},
},
default: '',
required: true,
description: 'The ID of this database.',
},
{
displayName: 'Simple',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
event: [
'pageAddedToDatabase',
],
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
},
],
};
methods = {
loadOptions: {
async getDatabases(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const { results: databases } = await notionApiRequest.call(this, 'POST', `/search`, { page_size: 100, filter: { property: 'object', value: 'database' } });
for (const database of databases) {
returnData.push({
name: database.title[0].plain_text,
value: database.id,
});
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
},
};
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const webhookData = this.getWorkflowStaticData('node');
const databaseId = this.getNodeParameter('databaseId') as string;
const event = this.getNodeParameter('event') as string;
const simple = this.getNodeParameter('simple') as boolean;
const now = moment().utc().format();
const startDate = webhookData.lastTimeChecked as string || now;
const endDate = now;
webhookData.lastTimeChecked = endDate;
const sortProperty = (event === 'pageAddedToDatabase') ? 'created_time' : 'last_edited_time';
const body: IDataObject = {
page_size: 1,
sorts: [
{
timestamp: sortProperty,
direction: 'descending',
},
],
};
let records: IDataObject[] = [];
let hasMore = true;
//get last record
let { results: data } = await notionApiRequest.call(this, 'POST', `/databases/${databaseId}/query`, body);
if (this.getMode() === 'manual') {
if (simple === true) {
data = simplifyObjects(data);
}
if (Array.isArray(data) && data.length) {
return [this.helpers.returnJsonArray(data)];
}
}
// if something changed after the last check
if (Object.keys(data[0]).length !== 0 && webhookData.lastRecordProccesed !== data[0].id) {
do {
body.page_size = 10;
const { results, has_more, next_cursor } = await notionApiRequest.call(this, 'POST', `/databases/${databaseId}/query`, body);
records.push.apply(records, results);
hasMore = has_more;
if (next_cursor !== null) {
body['start_cursor'] = next_cursor;
}
} while (!moment(records[records.length - 1][sortProperty] as string).isSameOrBefore(startDate) && hasMore === true);
if (this.getMode() !== 'manual') {
records = records.filter((record: IDataObject) => moment(record[sortProperty] as string).isBetween(startDate, endDate));
}
if (simple === true) {
records = simplifyObjects(records);
}
webhookData.lastRecordProccesed = data[0].id;
if (Array.isArray(records) && records.length) {
return [this.helpers.returnJsonArray(records)];
}
}
return null;
}
}

View file

@ -0,0 +1,332 @@
import {
INodeProperties,
} from 'n8n-workflow';
import {
blocks,
} from './Blocks';
export const pageOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'page',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a page',
},
{
name: 'Get',
value: 'get',
description: 'Get a page',
},
{
name: 'Search',
value: 'search',
description: 'Text search of pages',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const pageFields = [
/* -------------------------------------------------------------------------- */
/* page:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Parent Page ID',
name: 'pageId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'page',
],
operation: [
'create',
],
},
},
description: 'The ID of the parent page that this child page belongs to.',
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'page',
],
operation: [
'create',
],
},
},
description: 'Page title. Appears at the top of the page and can be found via Quick Find.',
},
{
displayName: 'Simple',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: [
'page',
],
operation: [
'create',
],
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
},
...blocks('page', 'create'),
/* -------------------------------------------------------------------------- */
/* page:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Page ID',
name: 'pageId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'page',
],
operation: [
'get',
],
},
},
},
{
displayName: 'Simple',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: [
'page',
],
operation: [
'get',
],
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
},
/* -------------------------------------------------------------------------- */
/* page:search */
/* -------------------------------------------------------------------------- */
{
displayName: 'Search Text',
name: 'text',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'page',
],
operation: [
'search',
],
},
},
description: 'The text to search for.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'page',
],
operation: [
'search',
],
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'page',
],
operation: [
'search',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
{
displayName: 'Simple',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: [
'page',
],
operation: [
'search',
],
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
resource: [
'page',
],
operation: [
'search',
],
},
},
default: {},
placeholder: 'Add Field',
options: [
{
displayName: 'Filters',
name: 'filter',
placeholder: 'Add Filter',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
displayName: 'Filter',
name: 'filters',
values: [
{
displayName: 'Property',
name: 'property',
type: 'options',
options: [
{
name: 'Object',
value: 'object',
},
],
default: 'object',
description: 'The name of the property to filter by.',
},
{
displayName: 'Value',
name: 'value',
type: 'options',
options: [
{
name: 'Database',
value: 'database',
},
{
name: 'Page',
value: 'page',
},
],
default: '',
description: 'The value of the property to filter by.',
},
],
},
],
},
{
displayName: 'Sort',
name: 'sort',
placeholder: 'Add Sort',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
displayName: 'Sort',
name: 'sortValue',
values: [
{
displayName: 'Direction',
name: 'direction',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ascending',
},
{
name: 'Descending',
value: 'descending',
},
],
default: '',
description: 'The direction to sort.',
},
{
displayName: 'Timestamp',
name: 'timestamp',
type: 'options',
options: [
{
name: 'Last Edited Time',
value: 'last_edited_time',
},
],
default: 'last_edited_time',
description: `The name of the timestamp to sort against.`,
},
],
},
],
},
],
},
] as INodeProperties[];

View file

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

View file

@ -0,0 +1 @@
<svg height="2500" width="2500" xmlns="http://www.w3.org/2000/svg" viewBox="12 0.18999999999999906 487.619 510.941"><path d="M96.085 91.118c15.81 12.845 21.741 11.865 51.43 9.884l279.888-16.806c5.936 0 1-5.922-.98-6.906L379.94 43.686c-8.907-6.915-20.773-14.834-43.516-12.853L65.408 50.6c-9.884.98-11.858 5.922-7.922 9.883zm16.804 65.228v294.491c0 15.827 7.909 21.748 25.71 20.769l307.597-17.799c17.81-.979 19.794-11.865 19.794-24.722V136.57c0-12.836-4.938-19.758-15.84-18.77l-321.442 18.77c-11.863.997-15.82 6.931-15.82 19.776zm303.659 15.797c1.972 8.903 0 17.798-8.92 18.799l-14.82 2.953v217.412c-12.868 6.916-24.734 10.87-34.622 10.87-15.831 0-19.796-4.945-31.654-19.76l-96.944-152.19v147.248l30.677 6.922s0 17.78-24.75 17.78l-68.23 3.958c-1.982-3.958 0-13.832 6.921-15.81l17.805-4.935V210.7l-24.721-1.981c-1.983-8.903 2.955-21.74 16.812-22.736l73.195-4.934 100.889 154.171V198.836l-25.723-2.952c-1.974-10.884 5.927-18.787 15.819-19.767zM42.653 23.919l281.9-20.76c34.618-2.969 43.525-.98 65.283 14.825l89.986 63.247c14.848 10.876 19.797 13.837 19.797 25.693v346.883c0 21.74-7.92 34.597-35.608 36.564L136.64 510.14c-20.785.991-30.677-1.971-41.562-15.815l-66.267-85.978C16.938 392.52 12 380.68 12 366.828V58.495c0-17.778 7.922-32.608 30.653-34.576z" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -414,10 +414,12 @@ export class Orbit implements INodeType {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = { const body: IDataObject = {
type: 'post', type: 'post',
activity_type: 'post',
url, url,
}; };
if (additionalFields.publishedAt) { if (additionalFields.publishedAt) {
body.occurred_at = additionalFields.publishedAt as string; body.occurred_at = additionalFields.publishedAt as string;
delete body.publishedAt;
} }
responseData = await orbitApiRequest.call(this, 'POST', `/${workspaceId}/members/${memberId}/activities/`, body); responseData = await orbitApiRequest.call(this, 'POST', `/${workspaceId}/members/${memberId}/activities/`, body);

View file

@ -16,17 +16,21 @@ import {
export async function paddleApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise<any> { // tslint:disable-line:no-any export async function paddleApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IWebhookFunctions, endpoint: string, method: string, body: any = {}, query?: IDataObject, uri?: string): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('paddleApi'); const credentials = this.getCredentials('paddleApi');
const productionUrl = 'https://vendors.paddle.com/api';
const sandboxUrl = 'https://sandbox-vendors.paddle.com/api';
if (credentials === undefined) { if (credentials === undefined) {
throw new NodeOperationError(this.getNode(), 'Could not retrieve credentials!'); throw new NodeOperationError(this.getNode(), 'Could not retrieve credentials!');
} }
const isSandbox = credentials.sandbox;
const options: OptionsWithUri = { const options: OptionsWithUri = {
method, method,
headers: { headers: {
'content-type': 'application/json', 'content-type': 'application/json',
}, },
uri: `https://vendors.paddle.com/api${endpoint}`, uri: `${isSandbox === true ? sandboxUrl : productionUrl}${endpoint}`,
body, body,
json: true, json: true,
}; };

View file

@ -38,6 +38,11 @@
"icon": "🛳", "icon": "🛳",
"url": "https://n8n.io/blog/running-n8n-on-ships-an-interview-with-maranics/" "url": "https://n8n.io/blog/running-n8n-on-ships-an-interview-with-maranics/"
}, },
{
"label": "Automate your data processing pipeline in 9 steps",
"icon": "⚙️",
"url": "https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/"
},
{ {
"label": "How Honest Burgers Use Automation to Save $100k per year", "label": "How Honest Burgers Use Automation to Save $100k per year",
"icon": "🍔", "icon": "🍔",

View file

@ -23,5 +23,10 @@
"url": "https://n8n.io/blog/how-to-use-the-http-request-node-the-swiss-army-knife-for-workflow-automation/" "url": "https://n8n.io/blog/how-to-use-the-http-request-node-the-swiss-army-knife-for-workflow-automation/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Files"
]
} }
} }

View file

@ -11,5 +11,10 @@
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.readBinaryFiles/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.readBinaryFiles/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Files"
]
} }
} }

View file

@ -11,5 +11,10 @@
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.readPDF/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.readPDF/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Files"
]
} }
} }

View file

@ -11,5 +11,10 @@
"url": "https://docs.n8n.io/nodes/n8n-nodes-base.renameKeys/" "url": "https://docs.n8n.io/nodes/n8n-nodes-base.renameKeys/"
} }
] ]
},
"subcategories": {
"Core Nodes": [
"Data Transformation"
]
} }
} }

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