diff --git a/docker/images/n8n-ubuntu/Dockerfile b/docker/images/n8n-ubuntu/Dockerfile index a76c2501a7..200506f058 100644 --- a/docker/images/n8n-ubuntu/Dockerfile +++ b/docker/images/n8n-ubuntu/Dockerfile @@ -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 diff --git a/docker/images/n8n/Dockerfile b/docker/images/n8n/Dockerfile index d609d00363..c0997dcabd 100644 --- a/docker/images/n8n/Dockerfile +++ b/docker/images/n8n/Dockerfile @@ -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 diff --git a/docker/images/n8n/README.md b/docker/images/n8n/README.md index 0007ba018a..8088150821 100644 --- a/docker/images/n8n/README.md +++ b/docker/images/n8n/README.md @@ -156,6 +156,7 @@ Replace the following placeholders with the actual data: - - - + - ``` docker run -it --rm \ @@ -166,6 +167,7 @@ docker run -it --rm \ -e DB_POSTGRESDB_HOST= \ -e DB_POSTGRESDB_PORT= \ -e DB_POSTGRESDB_USER= \ + -e DB_POSTGRESDB_SCHEMA= \ -e DB_POSTGRESDB_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 diff --git a/docs/configuration.md b/docs/configuration.md index 503e90b075..25bd371ac6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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) diff --git a/docs/database.md b/docs/database.md index f9a7d5802f..8fa4325394 100644 --- a/docs/database.md +++ b/docs/database.md @@ -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 ``` diff --git a/docs/node-basics.md b/docs/node-basics.md index ff319fd4d7..dae607fb82 100644 --- a/docs/node-basics.md +++ b/docs/node-basics.md @@ -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 diff --git a/docs/nodes.md b/docs/nodes.md index 03620ab01e..a3b0fc7b2e 100644 --- a/docs/nodes.md +++ b/docs/nodes.md @@ -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 diff --git a/docs/sensitive-data.md b/docs/sensitive-data.md index 027fd5c63a..7faa6007d6 100644 --- a/docs/sensitive-data.md +++ b/docs/sensitive-data.md @@ -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 diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index f1951f5002..4e2efaacfd 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -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; diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index b54fc33f65..2452bdd67f 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -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', }); diff --git a/packages/cli/package.json b/packages/cli/package.json index 58affb3a25..572a98000b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index 3864304748..a97cbaa837 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -42,6 +42,7 @@ export async function init(synchronize?: boolean): Promise 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 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 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 connectionOptions = { type: 'sqlite', database: path.join(n8nFolder, 'database.sqlite'), + entityPrefix: await GenericHelpers.getConfigValue('database.tablePrefix') as string, }; break; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 0f99ee8500..42bed2942e 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -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 => { 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 { 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}`); diff --git a/packages/cli/src/TestWebhooks.ts b/packages/cli/src/TestWebhooks.ts index 8aac38c9c6..aa6dd1d163 100644 --- a/packages/cli/src/TestWebhooks.ts +++ b/packages/cli/src/TestWebhooks.ts @@ -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); diff --git a/packages/core/package.json b/packages/core/package.json index c1d35696af..4b3d4937ea 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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" }, diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 212f8140b1..5b31f7bed8 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -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 { // 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); }, diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index e702a62223..587e41d35d 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -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", diff --git a/packages/nodes-base/credentials/JotFormApi.credentials.ts b/packages/nodes-base/credentials/JotFormApi.credentials.ts index 73d4cd267a..d1d9ab73c8 100644 --- a/packages/nodes-base/credentials/JotFormApi.credentials.ts +++ b/packages/nodes-base/credentials/JotFormApi.credentials.ts @@ -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.', + }, ]; } diff --git a/packages/nodes-base/credentials/MondayComApi.credentials.ts b/packages/nodes-base/credentials/MondayComApi.credentials.ts new file mode 100644 index 0000000000..3fa3ef5b6b --- /dev/null +++ b/packages/nodes-base/credentials/MondayComApi.credentials.ts @@ -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: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Affinity/AffinityTrigger.node.ts b/packages/nodes-base/nodes/Affinity/AffinityTrigger.node.ts index 828b3ce1da..6b57d46804 100644 --- a/packages/nodes-base/nodes/Affinity/AffinityTrigger.node.ts +++ b/packages/nodes-base/nodes/Affinity/AffinityTrigger.node.ts @@ -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 { - 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[]; diff --git a/packages/nodes-base/nodes/Function.node.ts b/packages/nodes-base/nodes/Function.node.ts index 7f5ea4c1ea..253a216650 100644 --- a/packages/nodes-base/nodes/Function.node.ts +++ b/packages/nodes-base/nodes/Function.node.ts @@ -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, diff --git a/packages/nodes-base/nodes/FunctionItem.node.ts b/packages/nodes-base/nodes/FunctionItem.node.ts index 6352fc3f98..356e419477 100644 --- a/packages/nodes-base/nodes/FunctionItem.node.ts +++ b/packages/nodes-base/nodes/FunctionItem.node.ts @@ -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; }, diff --git a/packages/nodes-base/nodes/Google/GoogleSheet.ts b/packages/nodes-base/nodes/Google/GoogleSheet.ts index 7b1f918ae4..2d31c884d2 100644 --- a/packages/nodes-base/nodes/Google/GoogleSheet.ts +++ b/packages/nodes-base/nodes/Google/GoogleSheet.ts @@ -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) { diff --git a/packages/nodes-base/nodes/Google/GoogleSheets.node.ts b/packages/nodes-base/nodes/Google/GoogleSheets.node.ts index 36f59a2675..71e76698de 100644 --- a/packages/nodes-base/nodes/Google/GoogleSheets.node.ts +++ b/packages/nodes-base/nodes/Google/GoogleSheets.node.ts @@ -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') { // ---------------------------------- diff --git a/packages/nodes-base/nodes/Hubspot/FormDescription.ts b/packages/nodes-base/nodes/Hubspot/FormDescription.ts new file mode 100644 index 0000000000..ab77db9585 --- /dev/null +++ b/packages/nodes-base/nodes/Hubspot/FormDescription.ts @@ -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,
+ 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,
+ 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
+ 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[]; diff --git a/packages/nodes-base/nodes/Hubspot/FormInterface.ts b/packages/nodes-base/nodes/Hubspot/FormInterface.ts new file mode 100644 index 0000000000..8b3b424f7a --- /dev/null +++ b/packages/nodes-base/nodes/Hubspot/FormInterface.ts @@ -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; +} diff --git a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts index 427446ac6d..454b604438 100644 --- a/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Hubspot/GenericFunctions.ts @@ -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 diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index 645751e499..9b88c91162 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -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 { 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 { 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 { 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 { 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 { + 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 { + 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)) { diff --git a/packages/nodes-base/nodes/JotForm/GenericFunctions.ts b/packages/nodes-base/nodes/JotForm/GenericFunctions.ts index 343ca0acb2..555e06d69a 100644 --- a/packages/nodes-base/nodes/JotForm/GenericFunctions.ts +++ b/packages/nodes-base/nodes/JotForm/GenericFunctions.ts @@ -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) { diff --git a/packages/nodes-base/nodes/Mailjet/EmailDescription.ts b/packages/nodes-base/nodes/Mailjet/EmailDescription.ts index 117e5e1ed2..34102fd934 100644 --- a/packages/nodes-base/nodes/Mailjet/EmailDescription.ts +++ b/packages/nodes-base/nodes/Mailjet/EmailDescription.ts @@ -299,7 +299,7 @@ export const emailFields = [ }, }, { - displayName: 'Template', + displayName: 'Template ID', name: 'templateId', type: 'string', required: true, diff --git a/packages/nodes-base/nodes/Mailjet/Mailjet.node.ts b/packages/nodes-base/nodes/Mailjet/Mailjet.node.ts index 5a6bf3b05d..789b1c41c8 100644 --- a/packages/nodes-base/nodes/Mailjet/Mailjet.node.ts +++ b/packages/nodes-base/nodes/Mailjet/Mailjet.node.ts @@ -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; diff --git a/packages/nodes-base/nodes/MondayCom/BoardColumnDescription.ts b/packages/nodes-base/nodes/MondayCom/BoardColumnDescription.ts new file mode 100644 index 0000000000..f1c2fff181 --- /dev/null +++ b/packages/nodes-base/nodes/MondayCom/BoardColumnDescription.ts @@ -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[]; diff --git a/packages/nodes-base/nodes/MondayCom/BoardDescription.ts b/packages/nodes-base/nodes/MondayCom/BoardDescription.ts new file mode 100644 index 0000000000..a6a549588e --- /dev/null +++ b/packages/nodes-base/nodes/MondayCom/BoardDescription.ts @@ -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[]; diff --git a/packages/nodes-base/nodes/MondayCom/BoardGroupDescription.ts b/packages/nodes-base/nodes/MondayCom/BoardGroupDescription.ts new file mode 100644 index 0000000000..d98ec67912 --- /dev/null +++ b/packages/nodes-base/nodes/MondayCom/BoardGroupDescription.ts @@ -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[]; diff --git a/packages/nodes-base/nodes/MondayCom/BoardItemDescription.ts b/packages/nodes-base/nodes/MondayCom/BoardItemDescription.ts new file mode 100644 index 0000000000..c6725279ed --- /dev/null +++ b/packages/nodes-base/nodes/MondayCom/BoardItemDescription.ts @@ -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[]; diff --git a/packages/nodes-base/nodes/MondayCom/GenericFunctions.ts b/packages/nodes-base/nodes/MondayCom/GenericFunctions.ts new file mode 100644 index 0000000000..f05ae3938e --- /dev/null +++ b/packages/nodes-base/nodes/MondayCom/GenericFunctions.ts @@ -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 { // 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 { // 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; +} diff --git a/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts b/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts new file mode 100644 index 0000000000..f8e538e5cb --- /dev/null +++ b/packages/nodes-base/nodes/MondayCom/MondayCom.node.ts @@ -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 { + 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 { + 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 { + 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 { + 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)]; + } +} diff --git a/packages/nodes-base/nodes/MondayCom/mondayCom.png b/packages/nodes-base/nodes/MondayCom/mondayCom.png new file mode 100644 index 0000000000..83a0a2c004 Binary files /dev/null and b/packages/nodes-base/nodes/MondayCom/mondayCom.png differ diff --git a/packages/nodes-base/nodes/Postgres/Postgres.node.ts b/packages/nodes-base/nodes/Postgres/Postgres.node.ts index c1da2fba35..f9db1a01e1 100644 --- a/packages/nodes-base/nodes/Postgres/Postgres.node.ts +++ b/packages/nodes-base/nodes/Postgres/Postgres.node.ts @@ -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++) { diff --git a/packages/nodes-base/nodes/Set.node.ts b/packages/nodes-base/nodes/Set.node.ts index 67501c5858..24ff044e60 100644 --- a/packages/nodes-base/nodes/Set.node.ts +++ b/packages/nodes-base/nodes/Set.node.ts @@ -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..
+ This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.
+ 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); diff --git a/packages/nodes-base/nodes/SpreadsheetFile.node.ts b/packages/nodes-base/nodes/SpreadsheetFile.node.ts index 37fb564e09..ecac7db2cd 100644 --- a/packages/nodes-base/nodes/SpreadsheetFile.node.ts +++ b/packages/nodes-base/nodes/SpreadsheetFile.node.ts @@ -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
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!'); diff --git a/packages/nodes-base/nodes/Webhook.node.ts b/packages/nodes-base/nodes/Webhook.node.ts index a05bf1cccc..f549e02754 100644 --- a/packages/nodes-base/nodes/Webhook.node.ts +++ b/packages/nodes-base/nodes/Webhook.node.ts @@ -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
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 { - 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, } }; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index b3467fc31e..8633eab42a 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -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", diff --git a/packages/workflow/package.json b/packages/workflow/package.json index 72cda7ba9a..b0dc6ca43d 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -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", diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 8e7fe082e9..44e4373e1d 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -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; // 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 { diff --git a/packages/workflow/src/Workflow.ts b/packages/workflow/src/Workflow.ts index 19749c7920..a63060913c 100644 --- a/packages/workflow/src/Workflow.ts +++ b/packages/workflow/src/Workflow.ts @@ -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. diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index 6a0ac34787..527b186196 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -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();