Merge branch 'master' into elasticsearch-node

This commit is contained in:
Iván Ovejero 2021-05-27 09:49:15 +02:00
commit 561f6e453e
57 changed files with 5037 additions and 161 deletions

View file

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

View file

@ -11,22 +11,6 @@ import { IPackageVersions } from './';
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
*

View file

@ -4,11 +4,18 @@ import {
} from 'n8n-core';
import {
ICredentialType,
ILogger,
INodeType,
INodeTypeData,
LoggerProxy,
} from 'n8n-workflow';
import * as config from '../config';
import {
getLogger,
} from '../src/Logger';
import {
access as fsAccess,
readdir as fsReaddir,
@ -31,7 +38,12 @@ class LoadNodesAndCredentialsClass {
nodeModulesPath = '';
logger: ILogger;
async init() {
this.logger = getLogger();
LoggerProxy.init(this.logger);
// Get the path to the node-modules folder to be later able
// to load the credentials and nodes
const checkPaths = [
@ -171,6 +183,10 @@ class LoadNodesAndCredentialsClass {
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)) {
return;
}

View file

@ -8,7 +8,6 @@ import {
resolve as pathResolve,
} from 'path';
import {
getConnection,
getConnectionManager,
In,
} from 'typeorm';
@ -22,7 +21,9 @@ import { RequestOptions } from 'oauth-1.0a';
import * as csrf from 'csrf';
import * as requestPromise from 'request-promise-native';
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 {
@ -572,6 +573,7 @@ class App {
const newWorkflowData = req.body as IWorkflowBase;
const id = req.params.id;
newWorkflowData.id = id;
await this.externalHooks.run('workflow.update', [newWorkflowData]);
@ -716,6 +718,7 @@ class App {
// get generated dynamically
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 path = req.query.path as string;
let credentials: INodeCredentials | undefined = undefined;
const currentNodeParameters = JSON.parse('' + req.query.currentNodeParameters) as INodeParameters;
if (req.query.credentials !== undefined) {
@ -725,7 +728,7 @@ class App {
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 workflowCredentials = await WorkflowCredentials(workflowData.nodes);

View file

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

View file

@ -18,10 +18,12 @@ const TEMP_WORKFLOW_NAME = 'Temp-Workflow';
export class LoadNodeParameterOptions {
path: string;
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);
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!`);
}
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);
}

View file

@ -691,7 +691,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
return continueOnFail(node);
},
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
return additionalData.executeWorkflow(workflowInfo, additionalData, inputData);
@ -742,7 +742,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
return getWorkflowMetadata(workflow);
},
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();
},
getWorkflowStaticData(type: string): IDataObject {
@ -789,7 +789,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
},
evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => {
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 {
return NodeHelpers.getContext(runExecutionData, type, node);
@ -841,7 +841,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
return getWorkflowMetadata(workflow);
},
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();
},
getWorkflowStaticData(type: string): IDataObject {
@ -871,18 +871,20 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
* @param {IWorkflowExecuteAdditionalData} additionalData
* @returns {ILoadOptionsFunctions}
*/
export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additionalData: IWorkflowExecuteAdditionalData): ILoadOptionsFunctions {
return ((workflow: Workflow, node: INode) => {
export function getLoadOptionsFunctions(workflow: Workflow, node: INode, path: string, additionalData: IWorkflowExecuteAdditionalData): ILoadOptionsFunctions {
return ((workflow: Workflow, node: INode, path: string) => {
const that = {
getCredentials(type: string): ICredentialDataDecryptedObject | undefined {
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;
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 => {
return additionalData.currentNodeParameters;
@ -915,7 +917,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio
},
};
return that;
})(workflow, node);
})(workflow, node, path);
}

View file

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

View file

@ -131,7 +131,7 @@ export interface IRestApi {
getSettings(): Promise<IN8nUISettings>;
getNodeTypes(): 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>;
runWorkflow(runData: IStartRunData): Promise<IExecutionPushResponse>;
createNewWorkflow(sendData: IWorkflowData): Promise<IWorkflowDb>;
@ -444,4 +444,4 @@ export interface ILinkMenuItemProperties {
icon: string;
href: string;
newWindow?: boolean;
}
}

View file

@ -177,7 +177,11 @@ export default mixins(genericHelpers)
} else if (optionParameter.typeOptions !== undefined && optionParameter.typeOptions.multipleValues === true) {
// Multiple values are allowed so append option to array
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 {
// Add a new option
newParameterValue[optionParameter.name] = JSON.parse(JSON.stringify(optionParameter.default));

View file

@ -37,9 +37,6 @@
<script lang="ts">
import Vue from 'vue';
import {
INodeIssues,
INodeIssueData,
INodeIssueObjectProperty,
INodeTypeDescription,
INodeParameters,
INodeProperties,
@ -409,9 +406,9 @@ export default mixins(
name: node.name,
value: nodeParameters,
};
this.$store.commit('setNodeParameters', updateInformation);
this.$externalHooks().run('nodeSettings.valueChanged', { parameterPath, newValue, parameters: this.parameters, oldNodeParameters });
this.updateNodeParameterIssues(node, nodeType);

View file

@ -230,7 +230,7 @@ export default mixins(
// Get the resolved parameter values of the current node
const currentNodeParameters = this.$store.getters.activeNode.parameters;
const resolvedNodeParameters = this.getResolveNodeParameters(currentNodeParameters);
const resolvedNodeParameters = this.resolveParameter(currentNodeParameters);
const returnValues: string[] = [];
for (const parameterPath of loadOptionsDependsOn) {
@ -456,21 +456,6 @@ export default mixins(
},
},
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 () {
if (this.node === null || this.remoteMethod === undefined || this.remoteParameterOptionsLoading) {
return;
@ -481,10 +466,10 @@ export default mixins(
// Get the resolved parameter values of the current node
const currentNodeParameters = this.$store.getters.activeNode.parameters;
const resolvedNodeParameters = this.getResolveNodeParameters(currentNodeParameters);
const resolvedNodeParameters = this.resolveParameter(currentNodeParameters) as INodeParameters;
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);
} catch (error) {
this.remoteParameterOptionsLoadingIssues = error.message;

View file

@ -76,26 +76,27 @@
</template>
<script lang="ts">
import Vue from 'vue';
import {
INodeParameters,
INodeProperties,
NodeParameterValue,
} from 'n8n-workflow';
import { IUpdateInformation } from '@/Interface';
import MultipleParameter from '@/components/MultipleParameter.vue';
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 { get } from 'lodash';
import { get, set } from 'lodash';
import mixins from 'vue-typed-mixins';
export default mixins(
genericHelpers,
nodeHelpers,
workflowHelpers,
)
.extend({
name: 'ParameterInputList',
@ -110,9 +111,12 @@ export default mixins(
'hideDelete', // boolean
],
computed: {
filteredParameters (): INodeProperties {
filteredParameters (): INodeProperties[] {
return this.parameters.filter((parameter: INodeProperties) => this.displayNodeParameter(parameter));
},
filteredParameterNames (): string[] {
return this.filteredParameters.map(parameter => parameter.name);
},
},
methods: {
multipleValues (parameter: INodeProperties): boolean {
@ -157,12 +161,75 @@ export default mixins(
// If it is not defined no need to do a proper check
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);
},
valueChanged (parameterData: IUpdateInformation): void {
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
// Because we have a circular dependency on CollectionParameter import it here
// to not break Vue.

View file

@ -379,7 +379,7 @@ export default mixins(
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();
// @ts-ignore

View file

@ -157,9 +157,10 @@ export const restApi = Vue.extend({
},
// 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 = {
nodeType,
path,
methodName,
credentials,
currentNodeParameters,

View file

@ -2,9 +2,12 @@ import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import {
IConnections,
IDataObject,
INode,
INodeExecutionData,
INodeIssues,
INodeParameters,
NodeParameterValue,
INodeType,
INodeTypes,
INodeTypeData,
@ -335,8 +338,8 @@ export const workflowHelpers = mixins(
return nodeData;
},
// Executes the given expression and returns its value
resolveExpression (expression: string) {
resolveParameter(parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) {
const inputIndex = 0;
const itemIndex = 0;
const runIndex = 0;
@ -362,7 +365,22 @@ export const workflowHelpers = mixins(
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.

View file

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

View file

@ -1851,21 +1851,19 @@ export default mixins(
for (type of Object.keys(currentConnections[sourceNode])) {
connection[type] = [];
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 = [];
for (connectionIndex = 0; connectionIndex < currentConnections[sourceNode][type][sourceIndex].length; connectionIndex++) {
const nodeConnection: NodeInputConnections = [];
connectionData = currentConnections[sourceNode][type][sourceIndex][connectionIndex];
if (!createNodeNames.includes(connectionData.node)) {
// Node does not get created so skip input connection
continue;
}
if (currentConnections[sourceNode][type][sourceIndex]) {
for (connectionIndex = 0; connectionIndex < currentConnections[sourceNode][type][sourceIndex].length; connectionIndex++) {
const nodeConnection: NodeInputConnections = [];
connectionData = currentConnections[sourceNode][type][sourceIndex][connectionIndex];
if (!createNodeNames.includes(connectionData.node)) {
// Node does not get created so skip input connection
continue;
}
nodeSourceConnections.push(connectionData);
// Add connection
nodeSourceConnections.push(connectionData);
// Add connection
}
}
connection[type].push(nodeSourceConnections);
}

View file

@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "0.12.0",
"version": "0.13.0",
"description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -59,8 +59,8 @@
"change-case": "^4.1.1",
"copyfiles": "^2.1.1",
"inquirer": "^7.0.1",
"n8n-core": "~0.70.0",
"n8n-workflow": "^0.57.0",
"n8n-core": "~0.71.0",
"n8n-workflow": "~0.58.0",
"oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0",
"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

@ -9,6 +9,22 @@ export class TwilioApi implements ICredentialType {
displayName = 'Twilio API';
documentationUrl = 'twilio';
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',
name: 'accountSid',
@ -20,6 +36,42 @@ export class TwilioApi implements ICredentialType {
name: 'authToken',
type: 'string' as NodePropertyTypes,
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

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

View file

@ -24,6 +24,10 @@ import {
import * as lodash from 'lodash';
import {
LoggerProxy as Logger
} from 'n8n-workflow';
export class EmailReadImap implements INodeType {
description: INodeTypeDescription = {
displayName: 'EmailReadImap',
@ -158,6 +162,13 @@ export class EmailReadImap implements INodeType {
default: false,
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 options = this.getNodeParameter('options', {}) as IDataObject;
let searchCriteria = [
'UNSEEN',
];
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.`);
}
}
const staticData = this.getWorkflowStaticData('node');
Logger.debug('Loaded static data for node "EmailReadImap"', {staticData});
// Returns the email text
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
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;
let fetchOptions = {};
@ -277,6 +280,12 @@ export class EmailReadImap implements INodeType {
const dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string;
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: '' });
if (part === undefined) {
@ -295,6 +304,12 @@ export class EmailReadImap implements INodeType {
}
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!);
newEmail = {
@ -335,6 +350,12 @@ export class EmailReadImap implements INodeType {
}
} else if (format === 'raw') {
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' });
if (part === undefined) {
@ -366,6 +387,33 @@ export class EmailReadImap implements INodeType {
},
onmail: async () => {
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);
if (returnData.length) {
@ -386,7 +434,9 @@ export class EmailReadImap implements INodeType {
return imapConnect(config).then(async conn => {
conn.on('error', async err => {
if (err.code.toUpperCase() === 'ECONNRESET') {
Logger.verbose('IMAP connection was reset - reconnecting.');
connection = await establishConnection();
await connection.openBox(mailbox);
}
throw err;
});
@ -398,8 +448,22 @@ export class EmailReadImap implements INodeType {
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
async function closeFunction() {
if (reconnectionInterval) {
clearInterval(reconnectionInterval);
}
await connection.end();
}

View file

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

View file

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

View file

@ -25,7 +25,37 @@ export class GraphQL implements INodeType {
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'httpHeaderAuth',
required: true,
displayOptions: {
show: {
authentication: [
'headerAuth',
],
},
},
},
],
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',
name: 'requestMethod',
@ -200,6 +230,7 @@ export class GraphQL implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const httpHeaderAuth = this.getCredentials('httpHeaderAuth');
let requestOptions: OptionsWithUri & RequestPromiseOptions;
@ -228,6 +259,11 @@ export class GraphQL implements INodeType {
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;
if (requestMethod === 'GET') {
requestOptions.qs = {

View file

@ -2106,7 +2106,7 @@ export class Hubspot implements INodeType {
responseData = await hubspotApiRequestAllItems.call(this, 'results', 'POST', endpoint, body, qs);
} 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 = responseData.results;
}

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

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

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,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,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 body: IDataObject = {
type: 'post',
activity_type: 'post',
url,
};
if (additionalFields.publishedAt) {
body.occurred_at = additionalFields.publishedAt as string;
delete body.publishedAt;
}
responseData = await orbitApiRequest.call(this, 'POST', `/${workspaceId}/members/${memberId}/activities/`, body);

View file

@ -1035,11 +1035,11 @@ export class Slack implements INodeType {
if (operation === 'get') {
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IDataObject = {};
const qs: IDataObject = {};
Object.assign(body, additionalFields);
Object.assign(qs, additionalFields);
responseData = await slackApiRequest.call(this, 'POST', '/users.profile.get', body);
responseData = await slackApiRequest.call(this, 'POST', '/users.profile.get', undefined, qs);
responseData = responseData.profile;
}

View file

@ -445,6 +445,37 @@ export class Spotify implements INodeType {
placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU',
description: `The track's Spotify URI or its ID. The track to add/delete from the playlist.`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'playlist',
],
operation: [
'add',
],
},
},
options: [
{
displayName: 'Position',
name: 'position',
type: 'number',
typeOptions: {
minValue: 0,
},
default: 0,
placeholder: '0',
description: `The new track's position in the playlist.`,
},
],
},
// -----------------------------------------------------
// Track Operations
// Get a Track, Get a Track's Audio Features
@ -918,15 +949,19 @@ export class Spotify implements INodeType {
requestMethod = 'POST';
const trackId = this.getNodeParameter('trackID', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
qs = {
uris: trackId,
};
if (additionalFields.position !== undefined) {
qs.position = additionalFields.position;
}
endpoint = `/playlists/${id}/tracks`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
}
} else if (operation === 'getUserPlaylists') {
requestMethod = 'GET';

View file

@ -285,7 +285,7 @@ export const entryFields = [
alwaysOpenEditWindow: true,
},
default: '',
description: 'JSON query to filter the data.<a href="https://strapi.io/documentation/v3.x/content-api/parameters.html#filters" target="_blank"> Info</a>',
description: 'JSON query to filter the data. <a href="https://strapi.io/documentation/developer-docs/latest/developer-resources/content-api/content-api.html#filters" target="_blank">More info</a>.',
},
],
},

View file

@ -137,7 +137,7 @@ export class Strava implements INodeType {
responseData = await stravaApiRequestAllItems.call(this, 'GET', `/activities`, {}, qs);
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
qs.per_page = this.getNodeParameter('limit', i) as number;
responseData = await stravaApiRequest.call(this, 'GET', `/activities`, {}, qs);
}

View file

@ -18,7 +18,7 @@ export class TogglTrigger implements INodeType {
icon: 'file:toggl.png',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when Toggl events occure',
description: 'Starts the workflow when Toggl events occur',
defaults: {
name: 'Toggl',
color: '#00FF00',

View file

@ -7,6 +7,10 @@ import {
IDataObject, NodeApiError, NodeOperationError,
} from 'n8n-workflow';
import {
OptionsWithUri,
} from 'request';
/**
* Make an API request to Twilio
*
@ -17,7 +21,14 @@ import {
* @returns {Promise<any>}
*/
export async function twilioApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: IDataObject, query?: IDataObject): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('twilioApi');
const credentials = this.getCredentials('twilioApi') as {
accountSid: string;
authType: 'authToken' | 'apiKey';
authToken: string;
apiKeySid: string;
apiKeySecret: string;
};
if (credentials === undefined) {
throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
}
@ -26,18 +37,26 @@ export async function twilioApiRequest(this: IHookFunctions | IExecuteFunctions,
query = {};
}
const options = {
const options: OptionsWithUri = {
method,
form: body,
qs: query,
uri: `https://api.twilio.com/2010-04-01/Accounts/${credentials.accountSid}${endpoint}`,
auth: {
user: credentials.accountSid as string,
pass: credentials.authToken as string,
},
json: true,
};
if (credentials.authType === 'apiKey') {
options.auth = {
user: credentials.apiKeySid,
password: credentials.apiKeySecret,
};
} else if (credentials.authType === 'authToken') {
options.auth = {
user: credentials.accountSid,
pass: credentials.authToken,
};
}
try {
return await this.helpers.request(options);
} catch (error) {

View file

@ -41,7 +41,7 @@ export class Zulip implements INodeType {
description: INodeTypeDescription = {
displayName: 'Zulip',
name: 'zulip',
icon: 'file:zulip.png',
icon: 'file:zulip.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1 @@
<svg width="256" height="256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="a"><stop stop-color="#24ADFF" offset="0%"/><stop stop-color="#7B71FF" offset="100%"/></linearGradient></defs><path d="M128 0c70.692 0 128 57.308 128 128 0 70.692-57.308 128-128 128C57.308 256 0 198.692 0 128 0 57.308 57.308 0 128 0zm-6.32 118.222l-45.892 40.979c-4.728 3.72-7.83 9.86-7.83 16.766 0 11.279 8.274 20.508 18.386 20.508h86.247c10.112 0 18.386-9.23 18.386-20.508 0-11.28-8.274-20.507-18.386-20.507H107.3c-.968 0-1.58-1.16-1.108-2.104l16.833-33.703c.615-.983-.493-2.161-1.345-1.43zm50.91-58.86H86.345c-10.112 0-18.386 9.227-18.386 20.508 0 11.279 8.274 20.508 18.386 20.508h65.292c.968 0 1.58 1.16 1.108 2.103l-16.834 33.704c-.615.983.494 2.161 1.346 1.43l45.892-40.984c4.727-3.723 7.829-9.86 7.829-16.767 0-11.278-8.274-20.507-18.386-20.501z" fill="url(#a)"/></svg>

After

Width:  |  Height:  |  Size: 934 B

View file

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-base",
"version": "0.116.0",
"version": "0.118.0",
"description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -172,6 +172,8 @@
"dist/credentials/NasaApi.credentials.js",
"dist/credentials/NextCloudApi.credentials.js",
"dist/credentials/NextCloudOAuth2Api.credentials.js",
"dist/credentials/NotionApi.credentials.js",
"dist/credentials/NotionOAuth2Api.credentials.js",
"dist/credentials/OAuth1Api.credentials.js",
"dist/credentials/OAuth2Api.credentials.js",
"dist/credentials/OpenWeatherMapApi.credentials.js",
@ -396,6 +398,7 @@
"dist/nodes/Hubspot/HubspotTrigger.node.js",
"dist/nodes/HumanticAI/HumanticAi.node.js",
"dist/nodes/Hunter/Hunter.node.js",
"dist/nodes/ICalendar.node.js",
"dist/nodes/If.node.js",
"dist/nodes/Iterable/Iterable.node.js",
"dist/nodes/Intercom/Intercom.node.js",
@ -450,6 +453,8 @@
"dist/nodes/Nasa/Nasa.node.js",
"dist/nodes/NextCloud/NextCloud.node.js",
"dist/nodes/NoOp.node.js",
"dist/nodes/Notion/Notion.node.js",
"dist/nodes/Notion/NotionTrigger.node.js",
"dist/nodes/OpenThesaurus/OpenThesaurus.node.js",
"dist/nodes/OpenWeatherMap.node.js",
"dist/nodes/Orbit/Orbit.node.js",
@ -588,7 +593,7 @@
"@types/xml2js": "^0.4.3",
"gulp": "^4.0.0",
"jest": "^26.4.2",
"n8n-workflow": "~0.57.0",
"n8n-workflow": "~0.59.0",
"ts-jest": "^26.3.0",
"tslint": "^6.1.2",
"typescript": "~3.9.7"
@ -610,6 +615,7 @@
"glob-promise": "^3.4.0",
"gm": "^1.23.1",
"iconv-lite": "^0.6.2",
"ics": "^2.27.0",
"imap-simple": "^4.3.0",
"iso-639-1": "^2.1.3",
"jsonwebtoken": "^8.5.1",
@ -625,7 +631,7 @@
"mqtt": "4.2.6",
"mssql": "^6.2.0",
"mysql2": "~2.2.0",
"n8n-core": "~0.70.0",
"n8n-core": "~0.72.0",
"nodemailer": "^6.5.0",
"pdf-parse": "^1.1.1",
"pg": "^8.3.0",

View file

@ -1,6 +1,6 @@
{
"name": "n8n-workflow",
"version": "0.57.0",
"version": "0.59.0",
"description": "Workflow base code of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -38,6 +38,7 @@
},
"dependencies": {
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"riot-tmpl": "^3.0.8",
"xml2js": "^0.4.23"
},

View file

@ -59,7 +59,7 @@ export class Expression {
* @returns {(NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[])}
* @memberof Workflow
*/
resolveSimpleParameterValue(parameterValue: NodeParameterValue, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], mode: WorkflowExecuteMode, returnObjectAsString = false, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
resolveSimpleParameterValue(parameterValue: NodeParameterValue, siblingParameters: INodeParameters, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], mode: WorkflowExecuteMode, returnObjectAsString = false, selfData = {}): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
// Check if it is an expression
if (typeof parameterValue !== 'string' || parameterValue.charAt(0) !== '=') {
// Is no expression so return value
@ -72,7 +72,7 @@ export class Expression {
parameterValue = parameterValue.substr(1);
// Generate a data proxy which allows to query workflow data
const dataProxy = new WorkflowDataProxy(this.workflow, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, -1, selfData);
const dataProxy = new WorkflowDataProxy(this.workflow, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, siblingParameters, mode, -1, selfData);
const data = dataProxy.getDataProxy();
// Execute the expression
@ -179,17 +179,17 @@ export class Expression {
};
// Helper function which resolves a parameter value depending on if it is simply or not
const resolveParameterValue = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) => {
const resolveParameterValue = (value: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], siblingParameters: INodeParameters) => {
if (isComplexParameter(value)) {
return this.getParameterValue(value, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, returnObjectAsString, selfData);
} else {
return this.resolveSimpleParameterValue(value as NodeParameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, returnObjectAsString, selfData);
return this.resolveSimpleParameterValue(value as NodeParameterValue, siblingParameters, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, returnObjectAsString, selfData);
}
};
// Check if it value is a simple one that we can get it resolved directly
if (!isComplexParameter(parameterValue)) {
return this.resolveSimpleParameterValue(parameterValue as NodeParameterValue, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, returnObjectAsString, selfData);
return this.resolveSimpleParameterValue(parameterValue as NodeParameterValue, {}, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, mode, returnObjectAsString, selfData);
}
// The parameter value is complex so resolve depending on type
@ -198,7 +198,7 @@ export class Expression {
// Data is an array
const returnData = [];
for (const item of parameterValue) {
returnData.push(resolveParameterValue(item));
returnData.push(resolveParameterValue(item, {}));
}
if (returnObjectAsString === true && typeof returnData === 'object') {
@ -212,7 +212,7 @@ export class Expression {
// Data is an object
const returnData: INodeParameters = {};
for (const key of Object.keys(parameterValue)) {
returnData[key] = resolveParameterValue((parameterValue as INodeParameters)[key]);
returnData[key] = resolveParameterValue((parameterValue as INodeParameters)[key], parameterValue as INodeParameters);
}
if (returnObjectAsString === true && typeof returnData === 'object') {

View file

@ -21,7 +21,7 @@ import {
Workflow
} from './Workflow';
import { get } from 'lodash';
import { get, isEqual } from 'lodash';
@ -296,6 +296,10 @@ export function displayParameter(nodeValues: INodeParameters, parameter: INodePr
values.push.apply(values, value);
}
if (values.some(v => (typeof v) === 'string' && (v as string).charAt(0) === '=')) {
return true;
}
if (values.length === 0 || !parameter.displayOptions.show[propertyName].some(v => values.includes(v))) {
return false;
}
@ -581,7 +585,9 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name] || nodeProperties.default;
}
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
} else if (nodeValues[nodeProperties.name] !== nodeProperties.default || (nodeValues[nodeProperties.name] !== undefined && parentType === 'collection')) {
} else if ((nodeValues[nodeProperties.name] !== nodeProperties.default && typeof nodeValues[nodeProperties.name] !== 'object') ||
(typeof nodeValues[nodeProperties.name] === 'object' && !isEqual(nodeValues[nodeProperties.name], nodeProperties.default)) ||
(nodeValues[nodeProperties.name] !== undefined && parentType === 'collection')) {
// Set only if it is different to the default value
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name];
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
@ -606,9 +612,14 @@ export function getNodeParameters(nodePropertiesArray: INodeProperties[], nodeVa
if (nodeValues[nodeProperties.name] !== undefined) {
nodeParameters[nodeProperties.name] = nodeValues[nodeProperties.name];
} else if (returnDefaults === true) {
// Does not have values defined but defaults should be returned which is in the
// case of a collection with multipleValues always an empty array
nodeParameters[nodeProperties.name] = [];
// Does not have values defined but defaults should be returned
if (Array.isArray(nodeProperties.default)) {
nodeParameters[nodeProperties.name] = JSON.parse(JSON.stringify(nodeProperties.default));
} else {
// As it is probably wrong for many nodes, do we keep on returning an empty array if
// anything else than an array is set as default
nodeParameters[nodeProperties.name] = [];
}
}
nodeParametersFull[nodeProperties.name] = nodeParameters[nodeProperties.name];
} else {

View file

@ -1,9 +1,11 @@
import {
IDataObject,
INodeExecutionData,
INodeParameters,
IRunExecutionData,
IWorkflowDataProxyData,
NodeHelpers,
NodeParameterValue,
Workflow,
WorkflowExecuteMode,
} from './';
@ -18,12 +20,13 @@ export class WorkflowDataProxy {
private itemIndex: number;
private activeNodeName: string;
private connectionInputData: INodeExecutionData[];
private siblingParameters: INodeParameters;
private mode: WorkflowExecuteMode;
private selfData: IDataObject;
constructor(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], mode: WorkflowExecuteMode, defaultReturnRunIndex = -1, selfData = {}) {
constructor(workflow: Workflow, runExecutionData: IRunExecutionData | null, runIndex: number, itemIndex: number, activeNodeName: string, connectionInputData: INodeExecutionData[], siblingParameters: INodeParameters, mode: WorkflowExecuteMode, defaultReturnRunIndex = -1, selfData = {}) {
this.workflow = workflow;
this.runExecutionData = runExecutionData;
this.defaultReturnRunIndex = defaultReturnRunIndex;
@ -31,6 +34,7 @@ export class WorkflowDataProxy {
this.itemIndex = itemIndex;
this.activeNodeName = activeNodeName;
this.connectionInputData = connectionInputData;
this.siblingParameters = siblingParameters;
this.mode = mode;
this.selfData = selfData;
}
@ -108,12 +112,22 @@ export class WorkflowDataProxy {
get(target, name, receiver) {
name = name.toString();
if (!node.parameters.hasOwnProperty(name)) {
// Parameter does not exist on node
throw new Error(`Could not find parameter "${name}" on node "${nodeName}"`);
}
let returnValue: INodeParameters | NodeParameterValue | NodeParameterValue[] | INodeParameters[];
if (name[0] === '&') {
const key = name.slice(1);
if (!that.siblingParameters.hasOwnProperty(key)) {
throw new Error(`Could not find sibling parameter "${key}" on node "${nodeName}"`);
const returnValue = node.parameters[name];
}
returnValue = that.siblingParameters[key];
} else {
if (!node.parameters.hasOwnProperty(name)) {
// Parameter does not exist on node
throw new Error(`Could not find parameter "${name}" on node "${nodeName}"`);
}
returnValue = node.parameters[name];
}
if (typeof returnValue === 'string' && returnValue.charAt(0) === '=') {
// The found value is an expression so resolve it
@ -361,7 +375,7 @@ export class WorkflowDataProxy {
},
$item: (itemIndex: number, runIndex?: number) => {
const defaultReturnRunIndex = runIndex === undefined ? -1 : runIndex;
const dataProxy = new WorkflowDataProxy(this.workflow, this.runExecutionData, this.runIndex, itemIndex, this.activeNodeName, this.connectionInputData, that.mode, defaultReturnRunIndex);
const dataProxy = new WorkflowDataProxy(this.workflow, this.runExecutionData, this.runIndex, itemIndex, this.activeNodeName, this.connectionInputData, that.siblingParameters, that.mode, defaultReturnRunIndex);
return dataProxy.getDataProxy();
},
$items: (nodeName?: string, outputIndex?: number, runIndex?: number) => {

View file

@ -2174,7 +2174,7 @@ describe('Workflow', () => {
typeOptions: {
multipleValues: true,
},
default: {},
default: [],
options: [
{
displayName: 'string1',
@ -2195,17 +2195,13 @@ describe('Workflow', () => {
},
output: {
noneDisplayedFalse: {
defaultsFalse: {
// collection1: [],
},
defaultsFalse: {},
defaultsTrue: {
collection1: [],
},
},
noneDisplayedTrue: {
defaultsFalse: {
// collection1: [],
},
defaultsFalse: {},
defaultsTrue: {
collection1: [],
},
@ -2677,7 +2673,7 @@ describe('Workflow', () => {
},
},
{
description: 'complex type "fixedCollection" with "multipleValues: true". Which contains complex type "fixedCollection" with "multipleValues: true". One value set.',
description: 'complex type "fixedCollection" with "multipleValues: true". Which contains complex type "fixedCollection" with "multipleValues: true". One value set.',
input: {
nodePropertiesArray: [
{
@ -2814,6 +2810,302 @@ describe('Workflow', () => {
},
},
},
{
description: 'complex type "fixedCollection" with "multipleValues: true". Which contains parameters which get displayed on a parameter with a default expression with relative parameter references.',
input: {
nodePropertiesArray: [
{
displayName: 'Values1',
name: 'values1',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
description: 'The value to set.',
default: {},
options: [
{
displayName: 'Options1',
name: 'options1',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
},
{
displayName: 'Type',
name: 'type',
type: 'hidden',
default: '={{$parameter["&key"].split("|")[1]}}',
},
{
displayName: 'Title Value',
name: 'titleValue',
displayOptions: {
show: {
type: [
'title',
],
},
},
type: 'string',
default: 'defaultTitle',
},
{
displayName: 'Title Number',
name: 'numberValue',
displayOptions: {
show: {
type: [
'number',
],
},
},
type: 'number',
default: 1,
},
],
},
],
},
],
nodeValues: {
values1: {
options1: [
{
key: 'asdf|title',
titleValue: 'different',
},
],
},
},
},
output: {
noneDisplayedFalse: {
defaultsFalse: {
values1: {
options1: [
{
key: 'asdf|title',
titleValue: 'different',
},
],
},
},
defaultsTrue: {
values1: {
options1: [
{
key: 'asdf|title',
type: '={{$parameter["&key"].split("|")[1]}}',
// This is not great that it displays this theoretically hidden parameter
// but because we can not resolve the values for now
numberValue: 1,
titleValue: 'different',
},
],
},
},
},
noneDisplayedTrue: {
defaultsFalse: {
values1: {
options1: [
{
key: 'asdf|title',
titleValue: 'different',
},
],
},
},
defaultsTrue: {
values1: {
options1: [
{
key: 'asdf|title',
type: '={{$parameter["&key"].split("|")[1]}}',
titleValue: 'different',
numberValue: 1,
},
],
},
},
},
},
},
{
description: 'complex type "fixedCollection" with "multipleValues: true". Which contains parameter of type "multiOptions" and has so an array default value',
input: {
nodePropertiesArray: [
{
name: 'values',
displayName: 'Values',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'propertyValues',
displayName: 'Property',
values: [
{
displayName: 'Options',
name: 'multiSelectValue',
type: 'multiOptions',
options: [
{
name: 'Value1',
value: 'value1',
},
{
name: 'Value2',
value: 'value2',
},
],
default: [],
},
],
},
],
},
],
nodeValues: {
values: {
propertyValues: [
{
multiSelectValue: [],
},
],
},
},
},
output: {
noneDisplayedFalse: {
defaultsFalse: {
values: {
propertyValues: [
{
},
],
},
},
defaultsTrue: {
values: {
propertyValues: [
{
multiSelectValue: [],
},
],
},
},
},
noneDisplayedTrue: {
defaultsFalse: {
values: {
propertyValues: [
{
},
],
},
},
defaultsTrue: {
values: {
propertyValues: [
{
multiSelectValue: [],
},
],
},
},
},
},
},
{
description: 'complex type "fixedCollection" with "multipleValues: true". Which contains parameter of type "string" with "multipleValues: true" and a custom default value',
input: {
nodePropertiesArray: [
{
name: 'values',
displayName: 'Values',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'propertyValues',
displayName: 'Property',
values: [
{
displayName: 'MultiString',
name: 'multiString',
type: 'string',
typeOptions: {
multipleValues: true,
},
default: ['value1'],
},
],
},
],
},
],
nodeValues: {
values: {
propertyValues: [
{
multiString: ['value1'],
},
],
},
},
},
output: {
noneDisplayedFalse: {
defaultsFalse: {
values: {
propertyValues: [
{
},
],
},
},
defaultsTrue: {
values: {
propertyValues: [
{
multiString: ['value1'],
},
],
},
},
},
noneDisplayedTrue: {
defaultsFalse: {
values: {
propertyValues: [
{
},
],
},
},
defaultsTrue: {
values: {
propertyValues: [
{
multiString: ['value1'],
},
],
},
},
},
},
},
];