Merge pull request #1 from n8n-io/master

Sync upstream
This commit is contained in:
István Richter 2020-03-30 09:36:04 +02:00 committed by GitHub
commit 618ae1a82b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 2675 additions and 159 deletions

View file

@ -11,7 +11,9 @@ RUN \
# Set a custom user to not have n8n run as root
USER root
RUN npm_config_user=root npm install -g n8n@${N8N_VERSION}
RUN npm_config_user=root npm install -g full-icu n8n@${N8N_VERSION}
ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu
WORKDIR /data

View file

@ -1,4 +1,4 @@
FROM node:12.13.0-alpine
FROM node:12.16-alpine
ARG N8N_VERSION
@ -13,9 +13,11 @@ USER root
# Install n8n and the also temporary all the packages
# it needs to build it correctly.
RUN apk --update add --virtual build-dependencies python build-base ca-certificates && \
npm_config_user=root npm install -g n8n@${N8N_VERSION} && \
npm_config_user=root npm install -g full-icu n8n@${N8N_VERSION} && \
apk del build-dependencies
ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu
WORKDIR /data
COPY docker-entrypoint.sh /docker-entrypoint.sh

View file

@ -156,6 +156,7 @@ Replace the following placeholders with the actual data:
- <POSTGRES_PASSWORD>
- <POSTGRES_PORT>
- <POSTGRES_USER>
- <POSTGRES_SCHEMA>
```
docker run -it --rm \
@ -166,6 +167,7 @@ docker run -it --rm \
-e DB_POSTGRESDB_HOST=<POSTGRES_HOST> \
-e DB_POSTGRESDB_PORT=<POSTGRES_PORT> \
-e DB_POSTGRESDB_USER=<POSTGRES_USER> \
-e DB_POSTGRESDB_SCHEMA=<POSTGRES_SCHEMA> \
-e DB_POSTGRESDB_PASSWORD=<POSTGRES_PASSWORD> \
-v ~/.n8n:/root/.n8n \
n8nio/n8n \
@ -214,6 +216,7 @@ The following environment variables support file input:
- DB_POSTGRESDB_PASSWORD_FILE
- DB_POSTGRESDB_PORT_FILE
- DB_POSTGRESDB_USER_FILE
- DB_POSTGRESDB_SCHEMA_FILE
- N8N_BASIC_AUTH_PASSWORD_FILE
- N8N_BASIC_AUTH_USER_FILE

View file

@ -137,6 +137,19 @@ export NODE_FUNCTION_ALLOW_EXTERNAL=moment,lodash
```
## SSL
It is possible to start n8n with SSL enabled by supplying a certificate to use:
```bash
export N8N_PROTOCOL=https
export N8N_SSL_KEY=/data/certs/server.key
export N8N_SSL_CERT=/data/certs/server.pem
```
## Timezone
The timezone is set by default to "America/New_York". It gets for example used by the
@ -177,3 +190,52 @@ webhook URLs get registred with external services.
```bash
export WEBHOOK_TUNNEL_URL="https://n8n.example.com/"
```
## Configuration via file
It is also possible to configure n8n via a configuration file.
It is not necessary to define all values. Only the ones which should be
different from the defaults.
If needed also multiple files can be supplied to for example have generic
base settings and some specific ones depending on the environment.
The path to the JSON configuration file to use can be set via the environment
variable `N8N_CONFIG_FILES`.
```bash
# Single file
export N8N_CONFIG_FILES=/folder/my-config.json
# Multiple files can be comma-separated
export N8N_CONFIG_FILES=/folder/my-config.json,/folder/production.json
```
A possible configuration file could look like this:
```json
{
"executions": {
"process": "main",
"saveDataOnSuccess": "none"
},
"generic": {
"timezone": "Europe/Berlin"
},
"security": {
"basicAuth": {
"active": true,
"user": "frank",
"password": "some-secure-password"
}
},
"nodes": {
"exclude": "[\"n8n-nodes-base.executeCommand\",\"n8n-nodes-base.writeBinaryFile\"]"
}
}
```
All possible values which can be set and their defaults can be found here:
[https://github.com/n8n-io/n8n/blob/master/packages/cli/config/index.ts](https://github.com/n8n-io/n8n/blob/master/packages/cli/config/index.ts)

View file

@ -4,6 +4,13 @@ By default, n8n uses SQLite to save credentials, past executions, and workflows.
n8n however also supports MongoDB and PostgresDB.
## Shared Settings
The following environment variables get used by all databases:
- `DB_TABLE_PREFIX` (default: '') - Prefix for table names
## MongoDB
!> **WARNING**: Use Postgres if possible! Mongo has problems with saving large
@ -38,6 +45,7 @@ To use PostgresDB as database you can provide the following environment variable
- `DB_POSTGRESDB_PORT` (default: 5432)
- `DB_POSTGRESDB_USER` (default: 'root')
- `DB_POSTGRESDB_PASSWORD` (default: empty)
- `DB_POSTGRESDB_SCHEMA` (default: 'public')
```bash
@ -47,6 +55,7 @@ export DB_POSTGRESDB_HOST=postgresdb
export DB_POSTGRESDB_PORT=5432
export DB_POSTGRESDB_USER=n8n
export DB_POSTGRESDB_PASSWORD=n8n
export DB_POSTGRESDB_SCHEMA=n8n
n8n start
```

View file

@ -49,6 +49,7 @@ The following special variables are available:
- **$binary**: Incoming binary data of a node
- **$data**: Incoming JSON data of a node
- **$evaluateExpression**: Evaluates a string as expression
- **$env**: Environment variables
- **$node**: Data of other nodes (output-data, parameters)
- **$parameters**: Parameters of the current node

View file

@ -99,6 +99,22 @@ const channel = $node["Slack"].parameter["channel"];
```
#### Method: evaluateExpression(expression: string, itemIndex: number)
Evaluates a given string as expression.
If no `itemIndex` is provided it uses by default in the Function-Node the data of item 0 and
in the Function Item-Node the data of the current item.
Example:
```javascript
items[0].json.variable1 = evaluateExpression('{{1+2}}');
items[0].json.variable2 = evaluateExpression($node["Set"].json["myExpression"], 1);
return items;
```
#### Method: getWorkflowStaticData(type)
Gives access to the static workflow data.
@ -114,7 +130,7 @@ same in the whole workflow. And every node in the workflow can access it. The no
Example:
```typescript
```javascript
// Get the global workflow static data
const staticData = getWorkflowStaticData('global');
// Get the static data of the node

View file

@ -13,5 +13,6 @@ The following environment variables support file input:
- DB_POSTGRESDB_PASSWORD_FILE
- DB_POSTGRESDB_PORT_FILE
- DB_POSTGRESDB_USER_FILE
- DB_POSTGRESDB_SCHEMA_FILE
- N8N_BASIC_AUTH_PASSWORD_FILE
- N8N_BASIC_AUTH_USER_FILE

View file

@ -20,10 +20,6 @@ import {
} from "../src";
// // Add support for internationalization
// const fullIcuPath = require.resolve('full-icu');
// process.env.NODE_ICU_DATA = dirname(fullIcuPath);
let activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner | undefined;
let processExistCode = 0;

View file

@ -20,6 +20,12 @@ const config = convict({
env: 'DB_MONGODB_CONNECTION_URL'
}
},
tablePrefix: {
doc: 'Prefix for table names',
format: '*',
default: '',
env: 'DB_TABLE_PREFIX'
},
postgresdb: {
database: {
doc: 'PostgresDB Database',
@ -51,6 +57,12 @@ const config = convict({
default: 'root',
env: 'DB_POSTGRESDB_USER'
},
schema: {
doc: 'PostgresDB Schema',
format: String,
default: 'public',
env: 'DB_POSTGRESDB_SCHEMA'
},
},
mysqldb: {
database: {
@ -163,6 +175,18 @@ const config = convict({
env: 'N8N_PROTOCOL',
doc: 'HTTP Protocol via which n8n can be reached'
},
ssl_key: {
format: String,
default: 'server.key',
env: 'N8N_SSL_KEY',
doc: 'SSL Key for HTTPS Protocol'
},
ssl_cert: {
format: String,
default: 'server.pem',
env: 'N8N_SSL_CERT',
doc: 'SSL Cert for HTTPS Protocol'
},
security: {
basicAuth: {
@ -261,6 +285,15 @@ const config = convict({
});
// Overwrite default configuration with settings which got defined in
// optional configuration files
if (process.env.N8N_CONFIG_FILES !== undefined) {
const configFiles = process.env.N8N_CONFIG_FILES.split(',');
console.log(`\nLoading configuration overwrites from:\n - ${configFiles.join('\n - ')}\n`);
config.loadFile(configFiles);
}
config.validate({
allowed: 'strict',
});

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.55.1",
"version": "0.59.0",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -94,10 +94,10 @@
"lodash.get": "^4.4.2",
"mongodb": "^3.2.3",
"mysql2": "^2.0.1",
"n8n-core": "~0.26.0",
"n8n-editor-ui": "~0.37.0",
"n8n-nodes-base": "~0.50.1",
"n8n-workflow": "~0.23.0",
"n8n-core": "~0.28.0",
"n8n-editor-ui": "~0.39.0",
"n8n-nodes-base": "~0.54.0",
"n8n-workflow": "~0.25.0",
"open": "^7.0.0",
"pg": "^7.11.0",
"request-promise-native": "^1.0.7",

View file

@ -42,6 +42,7 @@ export async function init(synchronize?: boolean): Promise<IDatabaseCollections>
entities = MongoDb;
connectionOptions = {
type: 'mongodb',
entityPrefix: await GenericHelpers.getConfigValue('database.tablePrefix') as string,
url: await GenericHelpers.getConfigValue('database.mongodb.connectionUrl') as string,
useNewUrlParser: true,
};
@ -52,11 +53,13 @@ export async function init(synchronize?: boolean): Promise<IDatabaseCollections>
entities = PostgresDb;
connectionOptions = {
type: 'postgres',
entityPrefix: await GenericHelpers.getConfigValue('database.tablePrefix') as string,
database: await GenericHelpers.getConfigValue('database.postgresdb.database') as string,
host: await GenericHelpers.getConfigValue('database.postgresdb.host') as string,
password: await GenericHelpers.getConfigValue('database.postgresdb.password') as string,
port: await GenericHelpers.getConfigValue('database.postgresdb.port') as number,
username: await GenericHelpers.getConfigValue('database.postgresdb.user') as string,
schema: await GenericHelpers.getConfigValue('database.postgresdb.schema') as string,
};
break;
@ -66,10 +69,11 @@ export async function init(synchronize?: boolean): Promise<IDatabaseCollections>
connectionOptions = {
type: 'mysql',
database: await GenericHelpers.getConfigValue('database.mysqldb.database') as string,
entityPrefix: await GenericHelpers.getConfigValue('database.tablePrefix') as string,
host: await GenericHelpers.getConfigValue('database.mysqldb.host') as string,
password: await GenericHelpers.getConfigValue('database.mysqldb.password') as string,
port: await GenericHelpers.getConfigValue('database.mysqldb.port') as number,
username: await GenericHelpers.getConfigValue('database.mysqldb.user') as string
username: await GenericHelpers.getConfigValue('database.mysqldb.user') as string,
};
break;
@ -79,6 +83,7 @@ export async function init(synchronize?: boolean): Promise<IDatabaseCollections>
connectionOptions = {
type: 'sqlite',
database: path.join(n8nFolder, 'database.sqlite'),
entityPrefix: await GenericHelpers.getConfigValue('database.tablePrefix') as string,
};
break;

View file

@ -1,4 +1,7 @@
import * as express from 'express';
import {
readFileSync,
} from 'fs';
import {
dirname as pathDirname,
join as pathJoin,
@ -97,6 +100,10 @@ class App {
push: Push.Push;
versions: IPackageVersions | undefined;
protocol: string;
sslKey: string;
sslCert: string;
constructor() {
this.app = express();
@ -112,6 +119,10 @@ class App {
this.push = Push.getInstance();
this.activeExecutionsInstance = ActiveExecutions.getInstance();
this.protocol = config.get('protocol');
this.sslKey = config.get('ssl_key');
this.sslCert = config.get('ssl_cert');
}
@ -1099,16 +1110,7 @@ class App {
// Removes a test webhook
this.app.delete('/rest/test-webhook/:id', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<boolean> => {
const workflowId = req.params.id;
const workflowData = await Db.collections.Workflow!.findOne(workflowId);
if (workflowData === undefined) {
throw new ResponseHelper.ResponseError(`Could not find workflow with id "${workflowId}" so webhook could not be deleted!`);
}
const nodeTypes = NodeTypes();
const workflow = new Workflow({ id: workflowId.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
return this.testWebhooks.cancelTestWebhook(workflowId, workflow);
return this.testWebhooks.cancelTestWebhook(workflowId);
}));
@ -1264,7 +1266,20 @@ export async function start(): Promise<void> {
await app.config();
app.app.listen(PORT, async () => {
let server;
if(app.protocol === 'https'){
const https = require('https');
const privateKey = readFileSync(app.sslKey,'utf8');
const cert = readFileSync(app.sslCert,'utf8');
const credentials = { key: privateKey,cert };
server = https.createServer(credentials,app.app);
}else{
const http = require('http');
server = http.createServer(app.app);
}
server.listen(PORT, async () => {
const versions = await GenericHelpers.getVersions();
console.log(`n8n ready on port ${PORT}`);
console.log(`Version: ${versions.cli}`);

View file

@ -131,7 +131,7 @@ export class TestWebhooks {
// Remove test-webhooks automatically if they do not get called (after 120 seconds)
const timeout = setTimeout(() => {
this.cancelTestWebhook(workflowData.id.toString(), workflow);
this.cancelTestWebhook(workflowData.id.toString());
}, 120000);
let key: string;
@ -143,10 +143,10 @@ export class TestWebhooks {
workflowData,
};
await this.activeWebhooks!.add(workflow, webhookData, mode);
}
// Save static data!
await WorkflowHelpers.saveStaticData(workflow);
// Save static data!
this.testWebhookData[key].workflowData.staticData = workflow.staticData;
}
return true;
}
@ -159,7 +159,9 @@ export class TestWebhooks {
* @returns {boolean}
* @memberof TestWebhooks
*/
cancelTestWebhook(workflowId: string, workflow: Workflow): boolean {
cancelTestWebhook(workflowId: string): boolean {
const nodeTypes = NodeTypes();
let foundWebhook = false;
for (const webhookKey of Object.keys(this.testWebhookData)) {
const webhookData = this.testWebhookData[webhookKey];
@ -182,6 +184,9 @@ export class TestWebhooks {
}
}
const workflowData = webhookData.workflowData;
const workflow = new Workflow({ id: workflowData.id.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
// Remove the webhook
delete this.testWebhookData[webhookKey];
this.activeWebhooks!.removeWorkflow(workflow);
@ -207,7 +212,7 @@ export class TestWebhooks {
for (const webhookKey of Object.keys(this.testWebhookData)) {
workflowData = this.testWebhookData[webhookKey].workflowData;
workflow = new Workflow({ id: workflowData.id.toString(), name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: workflowData.active, nodeTypes, staticData: workflowData.staticData, settings: workflowData.settings });
workflows.push();
workflows.push(workflow);
}
return this.activeWebhooks.removeAll(workflows);

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.26.0",
"version": "0.28.0",
"description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -44,7 +44,7 @@
"crypto-js": "3.1.9-1",
"lodash.get": "^4.4.2",
"mmmagic": "^0.5.2",
"n8n-workflow": "~0.23.0",
"n8n-workflow": "~0.25.0",
"p-cancelable": "^2.0.0",
"request-promise-native": "^1.0.7"
},

View file

@ -490,6 +490,9 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
continueOnFail: () => {
return continueOnFail(node);
},
evaluateExpression: (expression: string, itemIndex: number) => {
return workflow.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, node.name, connectionInputData);
},
async executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise<any> { // tslint:disable-line:no-any
return additionalData.executeWorkflow(workflowInfo, additionalData, inputData);
},
@ -578,6 +581,10 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData:
continueOnFail: () => {
return continueOnFail(node);
},
evaluateExpression: (expression: string, evaluateItemIndex: number | undefined) => {
evaluateItemIndex = evaluateItemIndex === undefined ? itemIndex : evaluateItemIndex;
return workflow.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, evaluateItemIndex, node.name, connectionInputData);
},
getContext(type: string): IContextObject {
return NodeHelpers.getContext(runExecutionData, type, node);
},

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.37.0",
"version": "0.39.0",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -63,7 +63,7 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"n8n-workflow": "~0.23.0",
"n8n-workflow": "~0.25.0",
"node-sass": "^4.12.0",
"prismjs": "^1.17.1",
"quill": "^2.0.0-dev.3",

View file

@ -13,5 +13,22 @@ export class JotFormApi implements ICredentialType {
type: 'string' as NodePropertyTypes,
default: '',
},
{
displayName: 'API Domain',
name: 'apiDomain',
type: 'options' as NodePropertyTypes,
options: [
{
name: 'api.jotform.com',
value: 'api.jotform.com',
},
{
name: 'eu-api.jotform.com',
value: 'eu-api.jotform.com',
},
],
default: 'api.jotform.com',
description: 'The API domain to use. Use "eu-api.jotform.com" if your account is in based in Europe.',
},
];
}

View file

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class MondayComApi implements ICredentialType {
name = 'mondayComApi';
displayName = 'Monday.com API';
properties = [
{
displayName: 'Token V2',
name: 'apiToken',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -25,7 +25,7 @@ export class AffinityTrigger implements INodeType {
version: 1,
description: 'Handle Affinity events via webhooks',
defaults: {
name: 'Affinity Trigger',
name: 'Affinity-Trigger',
color: '#3343df',
},
inputs: [],
@ -184,7 +184,11 @@ export class AffinityTrigger implements INodeType {
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default');
const webhookUrl = this.getNodeWebhookUrl('default') as string;
if (webhookUrl.includes('%20')) {
throw new Error('The name of the Affinity Trigger Node is not allowed to contain any spaces!');
}
const events = this.getNodeParameter('events') as string[];

View file

@ -47,6 +47,9 @@ export class Function implements INodeType {
// Define the global objects for the custom function
const sandbox = {
evaluateExpression: (expression: string, itemIndex = 0) => {
return this.evaluateExpression(expression, itemIndex);
},
getNodeParameter: this.getNodeParameter,
getWorkflowStaticData: this.getWorkflowStaticData,
helpers: this.helpers,

View file

@ -48,6 +48,9 @@ export class FunctionItem implements INodeType {
// Define the global objects for the custom function
const sandbox = {
evaluateExpression: (expression: string, itemIndex: number | undefined) => {
return this.evaluateExpression(expression, itemIndex);
},
getBinaryData: (): IBinaryKeyData | undefined => {
return item.binary;
},

View file

@ -3,6 +3,10 @@ import { google, sheets_v4 } from 'googleapis';
import { JWT } from 'google-auth-library';
import { getAuthenticationClient } from './GoogleApi';
import {
utils as xlsxUtils,
} from 'xlsx';
const Sheets = google.sheets('v4'); // tslint:disable-line:variable-name
export interface ISheetOptions {
@ -271,6 +275,12 @@ export class GoogleSheet {
}
getColumnWithOffset (startColumn: string, offset: number): string {
const columnIndex = xlsxUtils.decode_col(startColumn) + offset;
return xlsxUtils.encode_col(columnIndex);
}
/**
* Updates data in a sheet
*
@ -291,7 +301,14 @@ export class GoogleSheet {
}
[rangeStart, rangeEnd] = range.split(':');
const keyRowRange = `${sheet ? sheet + '!' : ''}${rangeStart}${dataStartRowIndex}:${rangeEnd}${dataStartRowIndex}`;
const rangeStartSplit = rangeStart.match(/([a-zA-Z]{1,10})([0-9]{0,10})/);
const rangeEndSplit = rangeEnd.match(/([a-zA-Z]{1,10})([0-9]{0,10})/);
if (rangeStartSplit === null || rangeStartSplit.length !== 3 || rangeEndSplit === null || rangeEndSplit.length !== 3) {
throw new Error(`The range "${range}" is not valid.`);
}
const keyRowRange = `${sheet ? sheet + '!' : ''}${rangeStartSplit[1]}${dataStartRowIndex}:${rangeEndSplit[1]}${dataStartRowIndex}`;
const sheetDatakeyRow = await this.getData(keyRowRange, valueRenderMode);
@ -307,9 +324,11 @@ export class GoogleSheet {
throw new Error(`Could not find column for key "${indexKey}"!`);
}
const characterCode = rangeStart.toUpperCase().charCodeAt(0) + keyIndex;
let keyColumnRange = String.fromCharCode(characterCode);
keyColumnRange = `${sheet ? sheet + '!' : ''}${keyColumnRange}:${keyColumnRange}`;
const startRowIndex = rangeStartSplit[2] || '';
const endRowIndex = rangeEndSplit[2] || '';
const keyColumn = this.getColumnWithOffset(rangeStartSplit[1], keyIndex);
const keyColumnRange = `${sheet ? sheet + '!' : ''}${keyColumn}${startRowIndex}:${keyColumn}${endRowIndex}`;
const sheetDataKeyColumn = await this.getData(keyColumnRange, valueRenderMode);
@ -348,7 +367,6 @@ export class GoogleSheet {
}
// Get the row index in which the data should be updated
// TODO: Should probably change the indexes to be 1 based because Google Sheet is
updateRowIndex = keyColumnIndexLookup.indexOf(itemKey) + dataStartRowIndex + 1;
// Check all the properties in the sheet and check which ones exist on the
@ -367,7 +385,7 @@ export class GoogleSheet {
// Property exists so add it to the data to update
// Get the column name in which the property data can be found
updateColumnName = String.fromCharCode(rangeStart.toUpperCase().charCodeAt(0) + keyColumnOrder.indexOf(propertyName));
updateColumnName = this.getColumnWithOffset(rangeStartSplit[1], keyColumnOrder.indexOf(propertyName));
updateData.push({
range: `${sheet ? sheet + '!' : ''}${updateColumnName}${updateRowIndex}`,
@ -426,7 +444,7 @@ export class GoogleSheet {
// Loop over all the items and find the one with the matching value
for (rowIndex = dataStartRowIndex; rowIndex < inputData.length; rowIndex++) {
if (inputData[rowIndex][returnColumnIndex].toString() === lookupValue.lookupValue.toString()) {
if (inputData[rowIndex][returnColumnIndex]?.toString() === lookupValue.lookupValue.toString()) {
returnData.push(inputData[rowIndex]);
if (returnAllMatches !== true) {

View file

@ -20,7 +20,6 @@ import {
ValueRenderOption,
} from './GoogleSheet';
export class GoogleSheets implements INodeType {
description: INodeTypeDescription = {
displayName: 'Google Sheets ',
@ -358,7 +357,6 @@ export class GoogleSheets implements INodeType {
type: 'string',
default: '',
placeholder: 'frank@example.com',
required: true,
displayOptions: {
show: {
operation: [
@ -407,6 +405,21 @@ export class GoogleSheets implements INodeType {
},
},
options: [
{
displayName: 'Continue If Empty',
name: 'continue',
type: 'boolean',
default: false,
displayOptions: {
show: {
'/operation': [
'lookup',
'read',
],
},
},
description: 'By default, the workflow stops executing if the lookup/read does not return values.',
},
{
displayName: 'Return All Matches',
name: 'returnAllMatches',
@ -679,7 +692,13 @@ export class GoogleSheets implements INodeType {
});
}
const returnData = await sheet.lookupValues(sheetData, keyRow, dataStartRow, lookupValues, options.returnAllMatches as boolean | undefined);
let returnData = await sheet.lookupValues(sheetData, keyRow, dataStartRow, lookupValues, options.returnAllMatches as boolean | undefined);
if (returnData.length === 0 && options.continue && options.returnAllMatches) {
returnData = [{}];
} else if (returnData.length === 1 && Object.keys(returnData[0]).length === 0 && !options.continue && !options.returnAllMatches) {
returnData = [];
}
return [this.helpers.returnJsonArray(returnData)];
} else if (operation === 'read') {
@ -708,6 +727,10 @@ export class GoogleSheets implements INodeType {
returnData = sheet.structureArrayDataByColumn(sheetData, keyRow, dataStartRow);
}
if (returnData.length === 0 && options.continue) {
returnData = [{}];
}
return [this.helpers.returnJsonArray(returnData)];
} else if (operation === 'update') {
// ----------------------------------

View file

@ -0,0 +1,326 @@
import { INodeProperties } from "n8n-workflow";
export const formOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'form',
],
},
},
options: [
{
name: 'Get Fields',
value: 'getFields',
description: 'Get all fields from a form',
},
{
name: 'Submit',
value: 'submit',
description: 'Submit data to a form',
},
],
default: 'getFields',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const formFields = [
/* -------------------------------------------------------------------------- */
/* form:submit */
/* -------------------------------------------------------------------------- */
{
displayName: 'Form',
name: 'formId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getForms',
},
required: true,
displayOptions: {
show: {
resource: [
'form',
],
operation: [
'submit',
],
},
},
default: '',
description: `The ID of the form you're sending data to.`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: [
'form',
],
operation: [
'submit',
],
},
},
options: [
{
displayName: 'Skip Validation',
name: 'skipValidation',
type: 'boolean',
default: false,
description: `Whether or not to skip validation based on the form settings.`,
},
{
displayName: 'Submitted At',
name: 'submittedAt',
type: 'dateTime',
default: '',
description: 'Time of the form submission.',
},
],
},
{
displayName: 'Context',
name: 'contextUi',
placeholder: 'Add Context',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
displayOptions: {
show: {
resource: [
'form',
],
operation: [
'submit',
],
},
},
default: {},
options: [
{
displayName: 'Context',
name: 'contextValue',
values: [
{
displayName: 'HubSpot usertoken',
name: 'hutk',
type: 'string',
default: '',
description: 'Include this parameter and set it to the hubspotutk cookie value to enable cookie tracking on your submission',
},
{
displayName: 'IP Address',
name: 'ipAddress',
type: 'string',
default: '',
description: 'The IP address of the visitor filling out the form.',
},
{
displayName: 'Page URI',
name: 'pageUri',
type: 'string',
default: '',
description: 'The URI of the page the submission happened on.',
},
{
displayName: 'Page Name',
name: 'pageName',
type: 'string',
default: '',
description: 'The name or title of the page the submission happened on.',
},
{
displayName: 'Page ID',
name: 'pageId',
type: 'string',
default: '',
description: 'The ID of a page created on the HubSpot CMS.',
},
{
displayName: 'SFDC campaign ID',
name: 'sfdcCampaignId',
type: 'string',
default: '',
description: `If the form is for an account using the HubSpot Salesforce Integration,</br>
you can include the ID of a Salesforce campaign to add the contact to the specified campaign.`,
},
{
displayName: 'Go to Webinar Webinar ID',
name: 'goToWebinarWebinarKey',
type: 'string',
default: '',
description: `If the form is for an account using the HubSpot GoToWebinar Integration,</br>
you can include the ID of a webinar to enroll the contact in that webinar when they submit the form.`,
},
],
},
],
},
{
displayName: 'Legal Consent',
name: 'lengalConsentUi',
placeholder: 'Add Legal Consent',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
displayOptions: {
show: {
resource: [
'form',
],
operation: [
'submit',
],
},
},
default: {},
options: [
{
displayName: 'Consent',
name: 'lengalConsentValues',
values: [
{
displayName: 'Consent To Process',
name: 'consentToProcess',
type: 'boolean',
default: false,
description: 'Whether or not the visitor checked the Consent to process checkbox',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
default: '',
description: 'The text displayed to the visitor for the Consent to process checkbox',
},
{
displayName: 'Communications',
name: 'communicationsUi',
placeholder: 'Add Communication',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Communication',
name: 'communicationValues',
values: [
{
displayName: 'Subcription Type',
name: 'subscriptionTypeId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getSubscriptionTypes',
},
default: '',
description: 'The ID of the specific subscription type',
},
{
displayName: 'Value',
name: 'value',
type: 'boolean',
default: false,
description: ' Whether or not the visitor checked the checkbox for this subscription type.',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
default: '',
description: 'The text displayed to the visitor for this specific subscription checkbox',
},
],
},
],
},
],
},
{
displayName: 'Legitimate Interest',
name: 'legitimateInterestValues',
values: [
{
displayName: 'Subcription Type',
name: 'subscriptionTypeId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getSubscriptionTypes',
},
default: '',
description: 'The ID of the specific subscription type that this forms indicates interest to.',
},
{
displayName: 'Value',
name: 'value',
type: 'boolean',
default: false,
description: `This must be true when using the 'legitimateInterest' option, as it reflects</br>
the consent indicated by the visitor when submitting the form`,
},
{
displayName: 'Legal Basis',
name: 'legalBasis',
type: 'options',
options: [
{
name: 'Customer',
value: 'CUSTOMER',
},
{
name: 'Lead',
value: 'LEAD',
},
],
default: '',
description: 'The privacy text displayed to the visitor.',
},
{
displayName: 'Text',
name: 'text',
type: 'string',
default: '',
description: 'The privacy text displayed to the visitor.',
},
],
},
],
},
/* -------------------------------------------------------------------------- */
/* form:getFields */
/* -------------------------------------------------------------------------- */
{
displayName: 'Form',
name: 'formId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getForms',
},
required: true,
displayOptions: {
show: {
resource: [
'form',
],
operation: [
'getFields',
],
},
},
default: '',
description: 'The ID of the form',
},
] as INodeProperties[];

View file

@ -0,0 +1,21 @@
import { IDataObject } from "n8n-workflow";
export interface IContext {
goToWebinarWebinarKey?: string;
hutk?: string;
ipAddress?: string;
pageId?: string;
pageName?: string;
pageUri?: string;
sfdcCampaignId?: string;
}
export interface IForm {
portalId?: number;
formId?: string;
fields?: IDataObject[];
legalConsentOptions?: IDataObject;
context?: IContext[];
submittedAt?: number;
skipValidation?: boolean;
}

View file

@ -26,17 +26,20 @@ export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions
json: true,
useQuerystring: true,
};
try {
return await this.helpers.request!(options);
} catch (error) {
const errorMessage = error.response.body.message || error.response.body.Message || error.message;
throw new Error(`Hubspot error response [${error.statusCode}]: ${errorMessage}`);
if (error.response && error.response.body && error.response.body.errors) {
// Try to return the error prettier
const errorMessages = error.response.body.errors.map((e: IDataObject) => e.message);
throw new Error(`Hubspot error response [${error.statusCode}]: ${errorMessages.join(' | ')}`);
}
throw error;
}
}
/**
* Make an API request to paginated hubspot endpoint
* and return all results

View file

@ -1,6 +1,7 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeTypeDescription,
@ -9,15 +10,30 @@ import {
ILoadOptionsFunctions,
INodePropertyOptions,
} from 'n8n-workflow';
import {
hubspotApiRequest,
hubspotApiRequestAllItems,
} from './GenericFunctions';
import {
dealOperations,
dealFields,
} from '../Hubspot/DealDescription';
import { IDeal, IAssociation } from './DealInterface';
} from './DealDescription';
import {
IDeal,
IAssociation
} from './DealInterface';
import {
formOperations,
formFields,
} from './FormDescription';
import {
IForm
} from './FormInterface';
export class Hubspot implements INodeType {
description: INodeTypeDescription = {
@ -50,6 +66,10 @@ export class Hubspot implements INodeType {
name: 'Deal',
value: 'deal',
},
{
name: 'Form',
value: 'form',
},
],
default: 'deal',
description: 'Resource to consume.',
@ -58,23 +78,22 @@ export class Hubspot implements INodeType {
// Deal
...dealOperations,
...dealFields,
// Form
...formOperations,
...formFields,
],
};
methods = {
loadOptions: {
// Get all the groups to display them to user so that he can
// select them easily
async getDealStages(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let stages;
try {
const endpoint = '/crm-pipelines/v1/pipelines/deals';
stages = await hubspotApiRequest.call(this, 'GET', endpoint, {});
stages = stages.results[0].stages;
} catch (err) {
throw new Error(`Hubspot Error: ${err}`);
}
const endpoint = '/crm-pipelines/v1/pipelines/deals';
let stages = await hubspotApiRequest.call(this, 'GET', endpoint, {});
stages = stages.results[0].stages;
for (const stage of stages) {
const stageName = stage.label;
const stageId = stage.stageId;
@ -90,13 +109,8 @@ export class Hubspot implements INodeType {
// select them easily
async getCompanies(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let companies;
try {
const endpoint = '/companies/v2/companies/paged';
companies = await hubspotApiRequestAllItems.call(this, 'results', 'GET', endpoint);
} catch (err) {
throw new Error(`Hubspot Error: ${err}`);
}
const endpoint = '/companies/v2/companies/paged';
const companies = await hubspotApiRequestAllItems.call(this, 'results', 'GET', endpoint);
for (const company of companies) {
const companyName = company.properties.name.value;
const companyId = company.companyId;
@ -112,13 +126,8 @@ export class Hubspot implements INodeType {
// select them easily
async getContacts(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let contacts;
try {
const endpoint = '/contacts/v1/lists/all/contacts/all';
contacts = await hubspotApiRequestAllItems.call(this, 'contacts', 'GET', endpoint);
} catch (err) {
throw new Error(`Hubspot Error: ${err}`);
}
const endpoint = '/contacts/v1/lists/all/contacts/all';
const contacts = await hubspotApiRequestAllItems.call(this, 'contacts', 'GET', endpoint);
for (const contact of contacts) {
const contactName = `${contact.properties.firstname.value} ${contact.properties.lastname.value}` ;
const contactId = contact.vid;
@ -134,13 +143,8 @@ export class Hubspot implements INodeType {
// select them easily
async getDealTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
let dealTypes;
try {
const endpoint = '/properties/v1/deals/properties/named/dealtype';
dealTypes = await hubspotApiRequest.call(this, 'GET', endpoint);
} catch (err) {
throw new Error(`Hubspot Error: ${err}`);
}
const endpoint = '/properties/v1/deals/properties/named/dealtype';
const dealTypes = await hubspotApiRequest.call(this, 'GET', endpoint);
for (const dealType of dealTypes.options) {
const dealTypeName = dealType.label ;
const dealTypeId = dealType.value;
@ -151,6 +155,40 @@ export class Hubspot implements INodeType {
}
return returnData;
},
// Get all the forms to display them to user so that he can
// select them easily
async getForms(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const endpoint = '/forms/v2/forms';
const forms = await hubspotApiRequest.call(this, 'GET', endpoint, {}, { formTypes: 'ALL' });
for (const form of forms) {
const formName = form.name;
const formId = form.guid;
returnData.push({
name: formName,
value: formId,
});
}
return returnData;
},
// Get all the subscription types to display them to user so that he can
// select them easily
async getSubscriptionTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const endpoint = '/email/public/v1/subscriptions';
const subscriptions = await hubspotApiRequestAllItems.call(this, 'subscriptionDefinitions', 'GET', endpoint, {});
for (const subscription of subscriptions) {
const subscriptionName = subscription.name;
const subscriptionId = subscription.id;
returnData.push({
name: subscriptionName,
value: subscriptionId,
});
}
return returnData;
},
}
};
@ -220,12 +258,8 @@ export class Hubspot implements INodeType {
});
}
body.associations = association;
try {
const endpoint = '/deals/v1/deal';
responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body);
} catch (err) {
throw new Error(`Hubspot Error: ${JSON.stringify(err)}`);
}
const endpoint = '/deals/v1/deal';
responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body);
}
if (operation === 'update') {
const body: IDeal = {};
@ -273,12 +307,8 @@ export class Hubspot implements INodeType {
value: updateFields.pipeline as string
});
}
try {
const endpoint = `/deals/v1/deal/${dealId}`;
responseData = await hubspotApiRequest.call(this, 'PUT', endpoint, body);
} catch (err) {
throw new Error(`Hubspot Error: ${JSON.stringify(err)}`);
}
const endpoint = `/deals/v1/deal/${dealId}`;
responseData = await hubspotApiRequest.call(this, 'PUT', endpoint, body);
}
if (operation === 'get') {
const dealId = this.getNodeParameter('dealId', i) as string;
@ -286,12 +316,8 @@ export class Hubspot implements INodeType {
if (additionalFields.includePropertyVersions) {
qs.includePropertyVersions = additionalFields.includePropertyVersions as boolean;
}
try {
const endpoint = `/deals/v1/deal/${dealId}`;
responseData = await hubspotApiRequest.call(this, 'GET', endpoint);
} catch (err) {
throw new Error(`Hubspot Error: ${JSON.stringify(err)}`);
}
const endpoint = `/deals/v1/deal/${dealId}`;
responseData = await hubspotApiRequest.call(this, 'GET', endpoint);
}
if (operation === 'getAll') {
const filters = this.getNodeParameter('filters', i) as IDataObject;
@ -307,17 +333,13 @@ export class Hubspot implements INodeType {
// @ts-ignore
qs.propertiesWithHistory = filters.propertiesWithHistory.split(',');
}
try {
const endpoint = `/deals/v1/deal/paged`;
if (returnAll) {
responseData = await hubspotApiRequestAllItems.call(this, 'deals', 'GET', endpoint, {}, qs);
} else {
qs.limit = this.getNodeParameter('limit', 0) as number;
responseData = await hubspotApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.deals;
}
} catch (err) {
throw new Error(`Hubspot Error: ${JSON.stringify(err)}`);
const endpoint = `/deals/v1/deal/paged`;
if (returnAll) {
responseData = await hubspotApiRequestAllItems.call(this, 'deals', 'GET', endpoint, {}, qs);
} else {
qs.limit = this.getNodeParameter('limit', 0) as number;
responseData = await hubspotApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.deals;
}
}
if (operation === 'getRecentlyCreated' || operation === 'getRecentlyModified') {
@ -330,31 +352,77 @@ export class Hubspot implements INodeType {
if (filters.includePropertyVersions) {
qs.includePropertyVersions = filters.includePropertyVersions as boolean;
}
try {
if (operation === 'getRecentlyCreated') {
endpoint = `/deals/v1/deal/recent/created`;
} else {
endpoint = `/deals/v1/deal/recent/modified`;
}
if (returnAll) {
responseData = await hubspotApiRequestAllItems.call(this, 'results', 'GET', endpoint, {}, qs);
} else {
qs.count = this.getNodeParameter('limit', 0) as number;
responseData = await hubspotApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.results;
}
} catch (err) {
throw new Error(`Hubspot Error: ${JSON.stringify(err)}`);
if (operation === 'getRecentlyCreated') {
endpoint = `/deals/v1/deal/recent/created`;
} else {
endpoint = `/deals/v1/deal/recent/modified`;
}
if (returnAll) {
responseData = await hubspotApiRequestAllItems.call(this, 'results', 'GET', endpoint, {}, qs);
} else {
qs.count = this.getNodeParameter('limit', 0) as number;
responseData = await hubspotApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.results;
}
}
if (operation === 'delete') {
const dealId = this.getNodeParameter('dealId', i) as string;
try {
const endpoint = `/deals/v1/deal/${dealId}`;
responseData = await hubspotApiRequest.call(this, 'DELETE', endpoint);
} catch (err) {
throw new Error(`Hubspot Error: ${JSON.stringify(err)}`);
const endpoint = `/deals/v1/deal/${dealId}`;
responseData = await hubspotApiRequest.call(this, 'DELETE', endpoint);
}
}
//https://developers.hubspot.com/docs/methods/forms/forms_overview
if (resource === 'form') {
//https://developers.hubspot.com/docs/methods/forms/v2/get_fields
if (operation === 'getFields') {
const formId = this.getNodeParameter('formId', i) as string;
responseData = await hubspotApiRequest.call(this, 'GET', `/forms/v2/fields/${formId}`);
}
//https://developers.hubspot.com/docs/methods/forms/submit_form_v3
if (operation === 'submit') {
const formId = this.getNodeParameter('formId', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const context = (this.getNodeParameter('contextUi', i) as IDataObject).contextValue as IDataObject;
const legalConsent = (this.getNodeParameter('lengalConsentUi', i) as IDataObject).lengalConsentValues as IDataObject;
const legitimateInteres = (this.getNodeParameter('lengalConsentUi', i) as IDataObject).legitimateInterestValues as IDataObject;
const { portalId } = await hubspotApiRequest.call(this, 'GET', `/forms/v2/forms/${formId}`);
const body: IForm = {
formId,
portalId,
legalConsentOptions: {},
fields: [],
};
if (additionalFields.submittedAt) {
body.submittedAt = new Date(additionalFields.submittedAt as string).getTime();
}
if (additionalFields.skipValidation) {
body.skipValidation = additionalFields.skipValidation as boolean;
}
const consent: IDataObject = {};
if (legalConsent) {
if (legalConsent.consentToProcess) {
consent!.consentToProcess = legalConsent.consentToProcess as boolean;
}
if (legalConsent.text) {
consent!.text = legalConsent.text as string;
}
if (legalConsent.communicationsUi) {
consent.communications = (legalConsent.communicationsUi as IDataObject).communicationValues as IDataObject;
}
}
body.legalConsentOptions!.consent = consent;
const fields: IDataObject = items[i].json;
for (const key of Object.keys(fields)) {
body.fields?.push({ name: key, value: fields[key] });
}
if (body.legalConsentOptions!.legitimateInterest) {
Object.assign(body, { legalConsentOptions: { legitimateInterest: legitimateInteres } });
}
if (context) {
Object.assign(body, { context });
}
const uri = `https://api.hsforms.com/submissions/v3/integration/submit/${portalId}/${formId}`;
responseData = await hubspotApiRequest.call(this, 'POST', '', body, {}, uri);
}
}
if (Array.isArray(responseData)) {

View file

@ -21,7 +21,7 @@ export async function jotformApiRequest(this: IHookFunctions | IExecuteFunctions
method,
qs,
form: body,
uri: uri ||`http://api.jotform.com${resource}`,
uri: uri || `https://${credentials.apiDomain || 'api.jotform.com'}${resource}`,
json: true
};
if (!Object.keys(body).length) {

View file

@ -299,7 +299,7 @@ export const emailFields = [
},
},
{
displayName: 'Template',
displayName: 'Template ID',
name: 'templateId',
type: 'string',
required: true,

View file

@ -185,7 +185,7 @@ export class Mailjet implements INodeType {
//https://dev.mailjet.com/email/guides/send-api-v31/#use-a-template
if (operation === 'sendTemplate') {
const fromEmail = this.getNodeParameter('fromEmail', i) as string;
const templateId = this.getNodeParameter('templateId', i) as string;
const templateId = parseInt(this.getNodeParameter('templateId', i) as string, 10);
const subject = this.getNodeParameter('subject', i) as string;
const variables = (this.getNodeParameter('variablesUi', i) as IDataObject).variablesValues as IDataObject[];
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;

View file

@ -0,0 +1,230 @@
import {
INodeProperties
} from 'n8n-workflow';
export const boardColumnOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'boardColumn',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new column',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all columns',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const boardColumnFields = [
/* -------------------------------------------------------------------------- */
/* boardColumn:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Board ID',
name: 'boardId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getBoards',
},
required: true,
displayOptions: {
show: {
resource: [
'boardColumn',
],
operation: [
'create',
],
},
},
},
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardColumn',
],
operation: [
'create',
],
},
},
},
{
displayName: 'Column Type',
name: 'columnType',
type: 'options',
default: '',
options: [
{
name: 'Country',
value: 'country',
},
{
name: 'Checkbox',
value: 'checkbox',
},
{
name: 'Date',
value: 'date',
},
{
name: 'Dropdown',
value: 'dropdown',
},
{
name: 'Email',
value: 'email',
},
{
name: 'Hour',
value: 'hour',
},
{
name: 'Link',
value: 'Link',
},
{
name: 'Long Text',
value: 'longText',
},
{
name: 'Numbers',
value: 'numbers',
},
{
name: 'People',
value: 'people',
},
{
name: 'Person',
value: 'person',
},
{
name: 'Phone',
value: 'phone',
},
{
name: 'Rating',
value: 'rating',
},
{
name: 'Status',
value: 'status',
},
{
name: 'Tags',
value: 'tags',
},
{
name: 'Team',
value: 'team',
},
{
name: 'Text',
value: 'text',
},
{
name: 'Timeline',
value: 'timeline',
},
{
name: 'Timezone',
value: 'timezone',
},
{
name: 'Week',
value: 'week',
},
{
name: 'World Clock',
value: 'worldClock',
},
],
required: true,
displayOptions: {
show: {
resource: [
'boardColumn',
],
operation: [
'create',
],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
resource: [
'boardColumn',
],
operation: [
'create',
],
},
},
default: {},
options: [
{
displayName: 'Defauls',
name: 'defaults',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: `The new column's defaults.`,
},
],
},
/* -------------------------------------------------------------------------- */
/* boardColumn:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Board ID',
name: 'boardId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getBoards',
},
required: true,
displayOptions: {
show: {
resource: [
'boardColumn',
],
operation: [
'getAll',
],
},
},
},
] as INodeProperties[];

View file

@ -0,0 +1,220 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const boardOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'board',
],
},
},
options: [
{
name: 'Archive',
value: 'archive',
description: 'Archive a board',
},
{
name: 'Create',
value: 'create',
description: 'Create a new board',
},
{
name: 'Get',
value: 'get',
description: 'Get a board',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all boards',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const boardFields = [
/* -------------------------------------------------------------------------- */
/* board:archive */
/* -------------------------------------------------------------------------- */
{
displayName: 'Board ID',
name: 'boardId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getBoards',
},
required: true,
displayOptions: {
show: {
resource: [
'board',
],
operation: [
'archive',
],
},
},
description: 'Board unique identifiers.',
},
/* -------------------------------------------------------------------------- */
/* board:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'name',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'board',
],
},
},
default: '',
description: `The board's name`,
},
{
displayName: 'Kind',
name: 'kind',
type: 'options',
options: [
{
name: 'Share',
value: 'share',
},
{
name: 'Public',
value: 'public',
},
{
name: 'Private',
value: 'private',
},
],
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'board',
],
},
},
default: '',
description: `The board's kind (public / private / share)`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'board',
],
},
},
default: {},
options: [
{
displayName: 'Template ID',
name: 'templateId',
type: 'number',
typeOptions: {
minValue: 0,
},
default: 0,
description: 'Optional board template id',
},
],
},
/* -------------------------------------------------------------------------- */
/* board:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Board ID',
name: 'boardId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getBoards',
},
required: true,
displayOptions: {
show: {
resource: [
'board',
],
operation: [
'get',
],
},
},
description: 'Board unique identifiers.',
},
/* -------------------------------------------------------------------------- */
/* board:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'board',
],
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: [
'board',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -0,0 +1,151 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const boardGroupOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'boardGroup',
],
},
},
options: [
{
name: 'Delete',
value: 'delete',
description: 'Delete a group in a board',
},
{
name: 'Create',
value: 'create',
description: 'Create a group in a board',
},
{
name: 'Get All',
value: 'getAll',
description: `Get board's groups`,
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const boardGroupFields = [
/* -------------------------------------------------------------------------- */
/* boardGroup:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Board ID',
name: 'boardId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getBoards',
},
required: true,
displayOptions: {
show: {
resource: [
'boardGroup',
],
operation: [
'create',
],
},
},
},
{
displayName: 'Name',
name: 'name',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'boardGroup',
],
},
},
default: '',
description: `The group name`,
},
/* -------------------------------------------------------------------------- */
/* boardGroup:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Board ID',
name: 'boardId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getBoards',
},
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardGroup',
],
operation: [
'delete',
],
},
},
},
{
displayName: 'Group ID',
name: 'groupId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getGroups',
loadOptionsDependsOn: [
'boardId',
],
},
required: true,
displayOptions: {
show: {
resource: [
'boardGroup',
],
operation: [
'delete',
],
},
},
},
/* -------------------------------------------------------------------------- */
/* boardGroup:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Board ID',
name: 'boardId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getBoards',
},
required: true,
displayOptions: {
show: {
resource: [
'boardGroup',
],
operation: [
'getAll',
],
},
},
},
] as INodeProperties[];

View file

@ -0,0 +1,380 @@
import {
INodeProperties,
} from 'n8n-workflow';
export const boardItemOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
resource: [
'boardItem',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: `Create an item in a board's group`,
},
{
name: 'Delete',
value: 'delete',
description: `Delete an item`,
},
{
name: 'Get',
value: 'get',
description: 'Get an item',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all item',
},
{
name: 'Get By Column Value',
value: 'getByColumnValue',
description: 'Get items by column value',
},
],
default: 'create',
description: 'The operation to perform.',
},
] as INodeProperties[];
export const boardItemFields = [
/* -------------------------------------------------------------------------- */
/* boardItem:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Board ID',
name: 'boardId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getBoards',
},
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'create',
],
},
},
},
{
displayName: 'Group ID',
name: 'groupId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getGroups',
loadOptionsDependsOn: [
'boardId'
],
},
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'create',
],
},
},
},
{
displayName: 'Name',
name: 'name',
type: 'string',
required: true,
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'boardItem',
],
},
},
default: '',
description: `The new item's name.`,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
displayOptions: {
show: {
operation: [
'create',
],
resource: [
'boardItem',
],
},
},
default: {},
options: [
{
displayName: 'Column Values',
name: 'columnValues',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
description: 'The column values of the new item',
},
],
},
/* -------------------------------------------------------------------------- */
/* boardItem:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Item ID',
name: 'itemId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'delete',
],
},
},
description: `Item's ID`
},
/* -------------------------------------------------------------------------- */
/* boardItem:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Item ID',
name: 'itemId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'get',
],
},
},
description: `Item's ID (Multiple can be added separated by comma)`
},
/* -------------------------------------------------------------------------- */
/* boardItem:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Board ID',
name: 'boardId',
type: 'options',
default: '',
typeOptions: {
loadOptionsMethod: 'getBoards',
},
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Group ID',
name: 'groupId',
default: '',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getGroups',
loadOptionsDependsOn: [
'boardId',
],
},
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'getAll',
],
},
},
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'boardItem',
],
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: [
'boardItem',
],
operation: [
'getAll',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
/* -------------------------------------------------------------------------- */
/* boardItem:getByColumnValue */
/* -------------------------------------------------------------------------- */
{
displayName: 'Board ID',
name: 'boardId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getBoards',
},
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'getByColumnValue',
],
},
},
description: 'The unique identifier of the board.',
},
{
displayName: 'Column ID',
name: 'columnId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getColumns',
loadOptionsDependsOn: [
'boardId'
],
},
default: '',
required: true,
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'getByColumnValue',
],
},
},
description: `The column's unique identifier.`,
},
{
displayName: 'Column Value',
name: 'columnValue',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'getByColumnValue',
],
},
},
description: 'The column value to search items by.'
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'boardItem',
],
operation: [
'getByColumnValue',
],
},
},
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: [
'boardItem',
],
operation: [
'getByColumnValue',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
},
] as INodeProperties[];

View file

@ -0,0 +1,68 @@
import {
OptionsWithUri,
} from 'request';
import {
IExecuteFunctions,
ILoadOptionsFunctions,
} from 'n8n-core';
import {
IDataObject,
IHookFunctions,
IWebhookFunctions
} from 'n8n-workflow';
import {
get,
} from 'lodash';
export async function mondayComApiRequest(this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions, body: any = {}, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
const credentials = this.getCredentials('mondayComApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const endpoint = 'https://api.monday.com/v2/';
let options: OptionsWithUri = {
headers: {
'Content-Type': 'application/json',
'Authorization': credentials.apiToken,
},
method: 'POST',
body,
uri: endpoint,
json: true
};
options = Object.assign({}, options, option);
try {
return await this.helpers.request!(options);
} catch (error) {
if (error.response) {
const errorMessage = error.response.body.error_message;
throw new Error(`Monday error response [${error.statusCode}]: ${errorMessage}`);
}
throw error;
}
}
export async function mondayComApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, body: any = {}): Promise<any> { // tslint:disable-line:no-any
const returnData: IDataObject[] = [];
let responseData;
body.variables.limit = 50;
body.variables.page = 1;
do {
responseData = await mondayComApiRequest.call(this, body);
returnData.push.apply(returnData, get(responseData, propertyName));
body.variables.page++;
} while (
get(responseData, propertyName).length > 0
);
return returnData;
}

View file

@ -0,0 +1,629 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import {
mondayComApiRequest,
mondayComApiRequestAllItems,
} from './GenericFunctions';
import {
boardFields,
boardOperations,
} from './BoardDescription';
import {
boardColumnFields,
boardColumnOperations,
} from './BoardColumnDescription';
import {
boardGroupFields,
boardGroupOperations,
} from './BoardGroupDescription';
import {
boardItemFields,
boardItemOperations,
} from './BoardItemDescription';
import {
snakeCase,
} from 'change-case';
interface IGraphqlBody {
query: string;
variables: IDataObject;
}
export class MondayCom implements INodeType {
description: INodeTypeDescription = {
displayName: 'Monday.com',
name: 'mondayCom',
icon: 'file:mondayCom.png',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Monday.com API',
defaults: {
name: 'Monday.com',
color: '#4353ff',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'mondayComApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Board',
value: 'board',
},
{
name: 'Board Column',
value: 'boardColumn',
},
{
name: 'Board Group',
value: 'boardGroup',
},
{
name: 'Board Item',
value: 'boardItem',
},
],
default: 'board',
description: 'Resource to consume.',
},
//BOARD
...boardOperations,
...boardFields,
// BOARD COLUMN
...boardColumnOperations,
...boardColumnFields,
// BOARD GROUP
...boardGroupOperations,
...boardGroupFields,
// BOARD ITEM
...boardItemOperations,
...boardItemFields,
],
};
methods = {
loadOptions: {
// Get all the available boards to display them to user so that he can
// select them easily
async getBoards(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const body = {
query:
`query ($page: Int, $limit: Int) {
boards (page: $page, limit: $limit){
id
description
name
}
}`,
variables: {
page: 1,
},
};
const boards = await mondayComApiRequestAllItems.call(this, 'data.boards', body);
if (boards === undefined) {
return returnData;
}
for (const board of boards) {
const boardName = board.name;
const boardId = board.id;
const boardDescription = board.description;
returnData.push({
name: boardName,
value: boardId,
description: boardDescription,
});
}
return returnData;
},
// Get all the available columns to display them to user so that he can
// select them easily
async getColumns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const boardId = parseInt(this.getCurrentNodeParameter('boardId') as string, 10);
const body: IGraphqlBody = {
query:
`query ($boardId: [Int]) {
boards (ids: $boardId){
columns() {
id
title
}
}
}`,
variables: {
page: 1,
boardId,
},
};
const { data } = await mondayComApiRequest.call(this, body);
if (data === undefined) {
return returnData;
}
const columns = data.boards[0].columns;
for (const column of columns) {
const columnName = column.title;
const columnId = column.id;
returnData.push({
name: columnName,
value: columnId,
});
}
return returnData;
},
// Get all the available groups to display them to user so that he can
// select them easily
async getGroups(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const boardId = parseInt(this.getCurrentNodeParameter('boardId') as string, 10);
const body = {
query:
`query ($boardId: Int!) {
boards ( ids: [$boardId]){
groups () {
id
title
}
}
}`,
variables: {
boardId,
},
};
const { data } = await mondayComApiRequest.call(this, body);
if (data === undefined) {
return returnData;
}
const groups = data.boards[0].groups;
for (const group of groups) {
const groupName = group.title;
const groupId = group.id;
returnData.push({
name: groupName,
value: groupId,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
let responseData;
const qs: IDataObject = {};
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < length; i++) {
if (resource === 'board') {
if (operation === 'archive') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const body: IGraphqlBody = {
query:
`mutation ($id: Int!) {
archive_board (board_id: $id) {
id
}
}`,
variables: {
id: boardId,
},
};
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.archive_board;
}
if (operation === 'create') {
const name = this.getNodeParameter('name', i) as string;
const kind = this.getNodeParameter('kind', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IGraphqlBody = {
query:
`mutation ($name: String!, $kind: BoardKind!, $templateId: Int) {
create_board (board_name: $name, board_kind: $kind, template_id: $templateId) {
id
}
}`,
variables: {
name,
kind,
},
};
if (additionalFields.templateId) {
body.variables.templateId = additionalFields.templateId as number;
}
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.create_board;
}
if (operation === 'get') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const body: IGraphqlBody = {
query:
`query ($id: [Int]) {
boards (ids: $id){
id
name
description
state
board_folder_id
board_kind
owner() {
id
}
}
}`,
variables: {
id: boardId,
},
};
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.boards;
}
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const body: IGraphqlBody = {
query:
`query ($page: Int, $limit: Int) {
boards (page: $page, limit: $limit){
id
name
description
state
board_folder_id
board_kind
owner() {
id
}
}
}`,
variables: {
page: 1,
},
};
if (returnAll === true) {
responseData = await mondayComApiRequestAllItems.call(this, 'data.boards', body);
} else {
body.variables.limit = this.getNodeParameter('limit', i) as number,
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.boards;
}
}
}
if (resource === 'boardColumn') {
if (operation === 'create') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const title = this.getNodeParameter('title', i) as string;
const columnType = this.getNodeParameter('columnType', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IGraphqlBody = {
query:
`mutation ($boardId: Int!, $title: String!, $columnType: ColumnType, $defaults: JSON ) {
create_column (board_id: $boardId, title: $title, column_type: $columnType, defaults: $defaults) {
id
}
}`,
variables: {
boardId,
title,
columnType: snakeCase(columnType),
},
};
if (additionalFields.defaults) {
try {
JSON.parse(additionalFields.defaults as string);
} catch (e) {
throw new Error('Defauls must be a valid JSON');
}
body.variables.defaults = JSON.stringify(JSON.parse(additionalFields.defaults as string));
}
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.create_column;
}
if (operation === 'getAll') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const body: IGraphqlBody = {
query:
`query ($boardId: [Int]) {
boards (ids: $boardId){
columns() {
id
title
type
settings_str
archived
}
}
}`,
variables: {
page: 1,
boardId,
},
};
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.boards[0].columns;
}
}
if (resource === 'boardGroup') {
if (operation === 'create') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const name = this.getNodeParameter('name', i) as string;
const body: IGraphqlBody = {
query:
`mutation ($boardId: Int!, $groupName: String!) {
create_group (board_id: $boardId, group_name: $groupName) {
id
}
}`,
variables: {
boardId,
groupName: name,
},
};
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.create_group;
}
if (operation === 'delete') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const groupId = this.getNodeParameter('groupId', i) as string;
const body: IGraphqlBody = {
query:
`mutation ($boardId: Int!, $groupId: String!) {
delete_group (board_id: $boardId, group_id: $groupId) {
id
}
}`,
variables: {
boardId,
groupId,
},
};
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.delete_group;
}
if (operation === 'getAll') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const body: IGraphqlBody = {
query:
`query ($boardId: [Int]) {
boards (ids: $boardId, ){
id
groups() {
id
title
color
position
archived
}
}
}`,
variables: {
boardId,
},
};
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.boards[0].groups;
}
}
if (resource === 'boardItem') {
if (operation === 'create') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const groupId = this.getNodeParameter('groupId', i) as string;
const itemName = this.getNodeParameter('name', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
const body: IGraphqlBody = {
query:
`mutation ($boardId: Int!, $groupId: String!, $itemName: String!, $columnValues: JSON) {
create_item (board_id: $boardId, group_id: $groupId, item_name: $itemName, column_values: $columnValues) {
id
}
}`,
variables: {
boardId,
groupId,
itemName,
},
};
if (additionalFields.columnValues) {
try {
JSON.parse(additionalFields.columnValues as string);
} catch (e) {
throw new Error('Custom Values must be a valid JSON');
}
body.variables.columnValues = JSON.stringify(JSON.parse(additionalFields.columnValues as string));
}
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.create_item;
}
if (operation === 'delete') {
const itemId = parseInt((this.getNodeParameter('itemId', i) as string), 10);
const body: IGraphqlBody = {
query:
`mutation ($itemId: Int!) {
delete_item (item_id: $itemId) {
id
}
}`,
variables: {
itemId,
},
};
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.delete_item;
}
if (operation === 'get') {
const itemIds = ((this.getNodeParameter('itemId', i) as string).split(',') as string[]).map((n) => parseInt(n, 10));
const body: IGraphqlBody = {
query:
`query ($itemId: [Int!]){
items (ids: $itemId) {
id
name
created_at
state
column_values() {
id
text
title
type
value
additional_info
}
}
}`,
variables: {
itemId: itemIds,
},
};
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.items;
}
if (operation === 'getAll') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const groupId = this.getNodeParameter('groupId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const body: IGraphqlBody = {
query:
`query ($boardId: [Int], $groupId: [String], $page: Int, $limit: Int) {
boards (ids: $boardId) {
groups (ids: $groupId) {
id
items(limit: $limit, page: $page) {
id
name
created_at
state
column_values() {
id
text
title
type
value
additional_info
}
}
}
}
}`,
variables: {
boardId,
groupId,
},
};
if (returnAll) {
responseData = await mondayComApiRequestAllItems.call(this, 'data.boards[0].groups[0].items', body);
} else {
body.variables.limit = this.getNodeParameter('limit', i) as number;
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.boards[0].groups[0].items;
}
}
if (operation === 'getByColumnValue') {
const boardId = parseInt(this.getNodeParameter('boardId', i) as string, 10);
const columnId = this.getNodeParameter('columnId', i) as string;
const columnValue = this.getNodeParameter('columnValue', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const body: IGraphqlBody = {
query:
`query ($boardId: Int!, $columnId: String!, $columnValue: String!, $page: Int, $limit: Int ){
items_by_column_values (board_id: $boardId, column_id: $columnId, column_value: $columnValue, page: $page, limit: $limit) {
id
name
created_at
state
board {
id
}
column_values() {
id
text
title
type
value
additional_info
}
}
}`,
variables: {
boardId,
columnId,
columnValue,
},
};
if (returnAll) {
responseData = await mondayComApiRequestAllItems.call(this, 'data.items_by_column_values', body);
} else {
body.variables.limit = this.getNodeParameter('limit', i) as number;
responseData = await mondayComApiRequest.call(this, body);
responseData = responseData.data.items_by_column_values;
}
}
}
if (Array.isArray(responseData)) {
returnData.push.apply(returnData, responseData as IDataObject[]);
} else {
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -107,6 +107,21 @@ export class Postgres implements INodeType {
// ----------------------------------
// insert
// ----------------------------------
{
displayName: 'Schema',
name: 'schema',
type: 'string',
displayOptions: {
show: {
operation: [
'insert'
],
},
},
default: 'public',
required: true,
description: 'Name of the schema the table belongs to',
},
{
displayName: 'Table',
name: 'table',
@ -137,6 +152,20 @@ export class Postgres implements INodeType {
placeholder: 'id,name,description',
description: 'Comma separated list of the properties which should used as columns for the new rows.',
},
{
displayName: 'Return Fields',
name: 'returnFields',
type: 'string',
displayOptions: {
show: {
operation: [
'insert'
],
},
},
default: '*',
description: 'Comma separated list of the fields that the operation will return',
},
// ----------------------------------
@ -239,20 +268,24 @@ export class Postgres implements INodeType {
// ----------------------------------
const table = this.getNodeParameter('table', 0) as string;
const schema = this.getNodeParameter('schema', 0) as string;
let returnFields = (this.getNodeParameter('returnFields', 0) as string).split(',') as string[];
const columnString = this.getNodeParameter('columns', 0) as string;
const columns = columnString.split(',').map(column => column.trim());
const cs = new pgp.helpers.ColumnSet(columns, { table });
const cs = new pgp.helpers.ColumnSet(columns);
const te = new pgp.helpers.TableName({ table, schema });
// Prepare the data to insert and copy it to be returned
const insertItems = getItemCopy(items, columns);
// Generate the multi-row insert query and return the id of new row
const query = pgp.helpers.insert(insertItems, cs) + ' RETURNING id';
returnFields = returnFields.map(value => value.trim()).filter(value => !!value);
const query = pgp.helpers.insert(insertItems, cs, te) + (returnFields.length ? ` RETURNING ${returnFields.join(',')}` : '');
// Executing the query to insert the data
const insertData = await db.many(query);
const insertData = await db.manyOrNone(query);
// Add the id to the data
for (let i = 0; i < insertData.length; i++) {

View file

@ -1,5 +1,6 @@
import { IExecuteFunctions } from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeParameters,
INodeType,
@ -103,6 +104,26 @@ export class Set implements INodeType {
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Dot Notation',
name: 'dotNotation',
type: 'boolean',
default: true,
description: `By default does dot-notation get used in property names..<br />
This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.<br />
If that is not intended this can be deactivated, it will then set { "a.b": value } instead.
`,
},
],
},
]
};
@ -122,6 +143,7 @@ export class Set implements INodeType {
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
keepOnlySet = this.getNodeParameter('keepOnlySet', itemIndex, []) as boolean;
item = items[itemIndex];
const options = this.getNodeParameter('options', itemIndex, {}) as IDataObject;
const newItem: INodeExecutionData = {
json: {},
@ -141,17 +163,29 @@ export class Set implements INodeType {
// Add boolean values
(this.getNodeParameter('values.boolean', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
set(newItem.json, setItem.name as string, !!setItem.value);
if (options.dotNotation === false) {
newItem.json[setItem.name as string] = !!setItem.value;
} else {
set(newItem.json, setItem.name as string, !!setItem.value);
}
});
// Add number values
(this.getNodeParameter('values.number', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
set(newItem.json, setItem.name as string, setItem.value);
if (options.dotNotation === false) {
newItem.json[setItem.name as string] = setItem.value;
} else {
set(newItem.json, setItem.name as string, setItem.value);
}
});
// Add string values
(this.getNodeParameter('values.string', itemIndex, []) as INodeParameters[]).forEach((setItem) => {
set(newItem.json, setItem.name as string, setItem.value);
if (options.dotNotation === false) {
newItem.json[setItem.name as string] = setItem.value;
} else {
set(newItem.json, setItem.name as string, setItem.value);
}
});
returnData.push(newItem);

View file

@ -196,6 +196,20 @@ export class SpreadsheetFile implements INodeType {
default: false,
description: 'If the data should be returned RAW instead of parsed.',
},
{
displayName: 'Read As String',
name: 'readAsString',
type: 'boolean',
displayOptions: {
show: {
'/operation': [
'fromFile'
],
},
},
default: false,
description: 'In some cases and file formats, it is necessary to read<br />specifically as string else some special character get interpreted wrong.',
},
{
displayName: 'Sheet Name',
name: 'sheetName',
@ -259,7 +273,12 @@ export class SpreadsheetFile implements INodeType {
// Read the binary spreadsheet data
const binaryData = Buffer.from(item.binary[binaryPropertyName].data, BINARY_ENCODING);
const workbook = xlsxRead(binaryData, { raw: options.rawData as boolean });
let workbook;
if (options.readAsString === true) {
workbook = xlsxRead(binaryData.toString(), { type: 'string', raw: options.rawData as boolean });
} else {
workbook = xlsxRead(binaryData, { raw: options.rawData as boolean });
}
if (workbook.SheetNames.length === 0) {
throw new Error('Spreadsheet does not have any sheets!');

View file

@ -1,4 +1,5 @@
import {
BINARY_ENCODING,
IWebhookFunctions,
} from 'n8n-core';
@ -218,6 +219,35 @@ export class Webhook implements INodeType {
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Binary Data',
name: 'binaryData',
type: 'boolean',
displayOptions: {
show: {
'/httpMethod': [
'POST',
],
},
},
default: false,
description: 'Set to true if webhook will receive binary data.',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
binaryData: [
true,
],
},
},
description: 'Name of the binary property to which to<br />write the data of the received file.',
},
{
displayName: 'Response Content-Type',
name: 'responseContentType',
@ -257,6 +287,13 @@ export class Webhook implements INodeType {
displayName: 'Raw Body',
name: 'rawBody',
type: 'boolean',
displayOptions: {
hide: {
binaryData: [
true,
],
},
},
default: false,
description: 'Raw body (binary)',
},
@ -265,10 +302,9 @@ export class Webhook implements INodeType {
],
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const authentication = this.getNodeParameter('authentication', 0) as string;
const options = this.getNodeParameter('options', 0) as IDataObject;
const authentication = this.getNodeParameter('authentication') as string;
const options = this.getNodeParameter('options', {}) as IDataObject;
const req = this.getRequestObject();
const resp = this.getResponseObject();
const headers = this.getHeaderData();
@ -328,8 +364,9 @@ export class Webhook implements INodeType {
const fileJson = files[file].toJSON() as IDataObject;
const [fileName, fileExtension] = (fileJson.name as string).split('.');
const fileContent = await fs.promises.readFile(files[file].path);
const buffer = Buffer.from(fileContent);
set(returnData[0], `binary[${fileName}]`, {
data: fileContent,
data: buffer.toString(BINARY_ENCODING),
mimeType: fileJson.type,
fileName: fileJson.name,
fileExtension,
@ -345,6 +382,47 @@ export class Webhook implements INodeType {
});
}
if (options.binaryData === true) {
return new Promise((resolve, reject) => {
const binaryPropertyName = options.binaryPropertyName || 'data';
const data: Buffer[] = [];
req.on('data', (chunk) => {
data.push(chunk);
});
req.on('end', async () => {
const returnItem: INodeExecutionData = {
binary: {},
json: {
body: this.getBodyData(),
headers,
query: this.getQueryData(),
},
};
const returnData: IDataObject[] = [{ json: {} }];
set(returnData[0], `binary[${binaryPropertyName}]`, {
data: Buffer.concat(data).toString(BINARY_ENCODING),
mimeType: req.headers['content-type'],
});
returnItem.binary![binaryPropertyName as string] = await this.helpers.prepareBinaryData(Buffer.concat(data));
return resolve({
workflowData: [
[
returnItem
]
],
});
});
req.on('error', (err) => {
throw new Error(err.message);
});
});
}
const response: INodeExecutionData = {
json: {
body: this.getBodyData(),
@ -352,11 +430,12 @@ export class Webhook implements INodeType {
query: this.getQueryData(),
},
};
if (options.rawBody) {
response.binary = {
data: {
// @ts-ignore
data: req.rawBody.toString('base64'),
data: req.rawBody.toString(BINARY_ENCODING),
mimeType,
}
};

View file

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-base",
"version": "0.50.1",
"version": "0.54.0",
"description": "Base nodes of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -40,8 +40,8 @@
"dist/credentials/ClickUpApi.credentials.js",
"dist/credentials/ClockifyApi.credentials.js",
"dist/credentials/CodaApi.credentials.js",
"dist/credentials/CopperApi.credentials.js",
"dist/credentials/CalendlyApi.credentials.js",
"dist/credentials/CopperApi.credentials.js",
"dist/credentials/CalendlyApi.credentials.js",
"dist/credentials/DisqusApi.credentials.js",
"dist/credentials/DriftApi.credentials.js",
"dist/credentials/DropboxApi.credentials.js",
@ -74,7 +74,8 @@
"dist/credentials/MandrillApi.credentials.js",
"dist/credentials/MattermostApi.credentials.js",
"dist/credentials/MauticApi.credentials.js",
"dist/credentials/MoceanApi.credentials.js",
"dist/credentials/MoceanApi.credentials.js",
"dist/credentials/MondayComApi.credentials.js",
"dist/credentials/MongoDb.credentials.js",
"dist/credentials/Msg91Api.credentials.js",
"dist/credentials/MySql.credentials.js",
@ -122,8 +123,8 @@
"dist/nodes/Aws/AwsSns.node.js",
"dist/nodes/Aws/AwsSnsTrigger.node.js",
"dist/nodes/Bitbucket/BitbucketTrigger.node.js",
"dist/nodes/Bitly/Bitly.node.js",
"dist/nodes/Calendly/CalendlyTrigger.node.js",
"dist/nodes/Bitly/Bitly.node.js",
"dist/nodes/Calendly/CalendlyTrigger.node.js",
"dist/nodes/Chargebee/Chargebee.node.js",
"dist/nodes/Chargebee/ChargebeeTrigger.node.js",
"dist/nodes/Clearbit/Clearbit.node.js",
@ -185,6 +186,7 @@
"dist/nodes/Mautic/MauticTrigger.node.js",
"dist/nodes/Merge.node.js",
"dist/nodes/Mocean/Mocean.node.js",
"dist/nodes/MondayCom/MondayCom.node.js",
"dist/nodes/MongoDb/MongoDb.node.js",
"dist/nodes/MoveBinaryData.node.js",
"dist/nodes/Msg91/Msg91.node.js",
@ -253,14 +255,14 @@
"@types/moment-timezone": "^0.5.12",
"@types/mongodb": "^3.3.6",
"@types/node": "^10.10.1",
"@types/nodemailer": "^4.6.5",
"@types/nodemailer": "^4.6.5",
"@types/redis": "^2.8.11",
"@types/request-promise-native": "~1.0.15",
"@types/uuid": "^3.4.6",
"@types/xml2js": "^0.4.3",
"gulp": "^4.0.0",
"jest": "^24.9.0",
"n8n-workflow": "~0.23.0",
"n8n-workflow": "~0.25.0",
"ts-jest": "^24.0.2",
"tslint": "^5.17.0",
"typescript": "~3.7.4"
@ -282,7 +284,7 @@
"lodash.unset": "^4.5.2",
"mongodb": "^3.3.2",
"mysql2": "^2.0.1",
"n8n-core": "~0.26.0",
"n8n-core": "~0.28.0",
"nodemailer": "^5.1.1",
"pdf-parse": "^1.1.1",
"pg-promise": "^9.0.3",

View file

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

View file

@ -155,6 +155,7 @@ export interface IExecuteContextData {
export interface IExecuteFunctions {
continueOnFail(): boolean;
evaluateExpression(expression: string, itemIndex: number): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[];
executeWorkflow(workflowInfo: IExecuteWorkflowInfo, inputData?: INodeExecutionData[]): Promise<any>; // tslint:disable-line:no-any
getContext(type: string): IContextObject;
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
@ -176,6 +177,7 @@ export interface IExecuteFunctions {
export interface IExecuteSingleFunctions {
continueOnFail(): boolean;
evaluateExpression(expression: string, itemIndex: number | undefined): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[];
getContext(type: string): IContextObject;
getCredentials(type: string): ICredentialDataDecryptedObject | undefined;
getInputData(inputIndex?: number, inputName?: string): INodeExecutionData;
@ -530,8 +532,12 @@ export interface IWorkflowDataProxyData {
$binary: any; // tslint:disable-line:no-any
$data: any; // tslint:disable-line:no-any
$env: any; // tslint:disable-line:no-any
$evaluateExpression: any; // tslint:disable-line:no-any
$item: any; // tslint:disable-line:no-any
$json: any; // tslint:disable-line:no-any
$node: any; // tslint:disable-line:no-any
$parameter: any; // tslint:disable-line:no-any
$workflow: any; // tslint:disable-line:no-any
}
export interface IWorkflowMetadata {

View file

@ -897,10 +897,14 @@ export class Workflow {
// Generate a data proxy which allows to query workflow data
const dataProxy = new WorkflowDataProxy(this, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData);
const data = dataProxy.getDataProxy();
data.$evaluateExpression = (expression: string) => {
return this.resolveSimpleParameterValue('=' + expression, runExecutionData, runIndex, itemIndex, activeNodeName, connectionInputData, returnObjectAsString);
};
// Execute the expression
try {
const returnValue = tmpl.tmpl(parameterValue, dataProxy.getDataProxy());
const returnValue = tmpl.tmpl(parameterValue, data);
if (returnValue !== null && typeof returnValue === 'object') {
if (Object.keys(returnValue).length === 0) {
// When expression is incomplete it returns a Proxy which causes problems.

View file

@ -300,6 +300,7 @@ export class WorkflowDataProxy {
$binary: {}, // Placeholder
$data: {}, // Placeholder
$env: this.envGetter(),
$evaluateExpression: (expression: string) => { }, // Placeholder
$item: (itemIndex: number) => {
const dataProxy = new WorkflowDataProxy(this.workflow, this.runExecutionData, this.runIndex, itemIndex, this.activeNodeName, this.connectionInputData);
return dataProxy.getDataProxy();