diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 1e603ca1cb..83b2514ea4 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,21 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.93.0 + +### What changed? + +Change in naming of the Authentication field for the Pipedrive Trigger node. + +### When is action necessary? + +If you had set "Basic Auth" for the "Authentication" field in the node. + +### How to upgrade: + +The "Authentication" field has been renamed to "Incoming Authentication". Please set the parameter “Incoming Authentication” to “Basic Auth” to activate it again. + + ## 0.90.0 ### What changed? diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index 8e14b98e2c..d6e16b7cb3 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -15,7 +15,6 @@ import { Db, ExternalHooks, GenericHelpers, - IExecutionsCurrentSummary, LoadNodesAndCredentials, NodeTypes, Server, diff --git a/packages/cli/package.json b/packages/cli/package.json index ea41423761..14968d585f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "n8n", - "version": "0.92.0", + "version": "0.93.0", "description": "n8n Workflow Automation Tool", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -103,10 +103,10 @@ "lodash.get": "^4.4.2", "mongodb": "^3.5.5", "mysql2": "~2.1.0", - "n8n-core": "~0.50.0", - "n8n-editor-ui": "~0.62.0", - "n8n-nodes-base": "~0.87.0", - "n8n-workflow": "~0.43.0", + "n8n-core": "~0.51.0", + "n8n-editor-ui": "~0.63.0", + "n8n-nodes-base": "~0.88.0", + "n8n-workflow": "~0.44.0", "oauth-1.0a": "^2.2.6", "open": "^7.0.0", "pg": "^8.3.0", diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 4d3a9e1aec..4b38988b71 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -1534,17 +1534,21 @@ class App { // Loads the currently saved workflow to execute instead of the // one saved at the time of the execution. const workflowId = fullExecutionData.workflowData.id; - data.workflowData = await Db.collections.Workflow!.findOne(workflowId) as IWorkflowBase; + const workflowData = await Db.collections.Workflow!.findOne(workflowId) as IWorkflowBase; - if (data.workflowData === undefined) { + if (workflowData === undefined) { throw new Error(`The workflow with the ID "${workflowId}" could not be found and so the data not be loaded for the retry.`); } + data.workflowData = workflowData; + const nodeTypes = NodeTypes(); + const workflowInstance = new Workflow({ id: workflowData.id as string, name: workflowData.name, nodes: workflowData.nodes, connections: workflowData.connections, active: false, nodeTypes, staticData: undefined, settings: workflowData.settings }); + // Replace all of the nodes in the execution stack with the ones of the new workflow for (const stack of data!.executionData!.executionData!.nodeExecutionStack) { // Find the data of the last executed node in the new workflow - const node = data.workflowData.nodes.find(node => node.name === stack.node.name); - if (node === undefined) { + const node = workflowInstance.getNode(stack.node.name); + if (node === null) { throw new Error(`Could not find the node "${stack.node.name}" in workflow. It probably got deleted or renamed. Without it the workflow can sadly not be retried.`); } diff --git a/packages/cli/src/WebhookHelpers.ts b/packages/cli/src/WebhookHelpers.ts index 114111a821..6d3504963a 100644 --- a/packages/cli/src/WebhookHelpers.ts +++ b/packages/cli/src/WebhookHelpers.ts @@ -222,7 +222,7 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { return; } - // Now that we know that the workflow should run we can return the default respons + // Now that we know that the workflow should run we can return the default response // directly if responseMode it set to "onReceived" and a respone should be sent if (responseMode === 'onReceived' && didSendResponse === false) { // Return response directly and do not wait for the workflow to finish @@ -302,6 +302,19 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { } const returnData = WorkflowHelpers.getDataLastExecutedNodeData(data); + if(data.data.resultData.error || returnData?.error !== undefined) { + if (didSendResponse === false) { + responseCallback(null, { + data: { + message: 'Workflow did error.', + }, + responseCode: 500, + }); + } + didSendResponse = true; + return data; + } + if (returnData === undefined) { if (didSendResponse === false) { responseCallback(null, { @@ -313,17 +326,6 @@ export function getWorkflowWebhooksBasic(workflow: Workflow): IWebhookData[] { } didSendResponse = true; return data; - } else if (returnData.error !== undefined) { - if (didSendResponse === false) { - responseCallback(null, { - data: { - message: 'Workflow did error.', - }, - responseCode: 500, - }); - } - didSendResponse = true; - return data; } const responseData = workflow.expression.getSimpleParameterValue(workflowStartNode, webhookData.webhookDescription['responseData'], 'firstEntryJson'); diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 11c15aee8e..cf755a6eaa 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -202,6 +202,18 @@ function hookFunctionsPush(): IWorkflowExecuteHooks { } +export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowExecuteHooks { + const externalHooks = ExternalHooks(); + + return { + workflowExecuteBefore: [ + async function (this: WorkflowHooks, workflow: Workflow): Promise { + await externalHooks.run('workflow.preExecute', [workflow, this.mode]); + }, + ], + }; +} + /** * Returns hook functions to save workflow execution and call error workflow * @@ -337,7 +349,6 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi const externalHooks = ExternalHooks(); await externalHooks.init(); - await externalHooks.run('workflow.execute', [workflowData, mode]); const nodeTypes = NodeTypes(); @@ -462,6 +473,10 @@ export async function getBase(credentials: IWorkflowCredentials, currentNodePara export function getWorkflowHooksIntegrated(mode: WorkflowExecuteMode, executionId: string, workflowData: IWorkflowBase, optionalParameters?: IWorkflowHooksOptionalParameters): WorkflowHooks { optionalParameters = optionalParameters || {}; const hookFunctions = hookFunctionsSave(optionalParameters.parentProcessMode); + const preExecuteFunctions = hookFunctionsPreExecute(optionalParameters.parentProcessMode); + for (const key of Object.keys(preExecuteFunctions)) { + hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]); + } return new WorkflowHooks(hookFunctions, mode, executionId, workflowData, optionalParameters); } @@ -474,12 +489,19 @@ export function getWorkflowHooksIntegrated(mode: WorkflowExecuteMode, executionI * @param {string} executionId * @returns {WorkflowHooks} */ -export function getWorkflowHooksMain(data: IWorkflowExecutionDataProcess, executionId: string): WorkflowHooks { +export function getWorkflowHooksMain(data: IWorkflowExecutionDataProcess, executionId: string, isMainProcess = false): WorkflowHooks { const hookFunctions = hookFunctionsSave(); const pushFunctions = hookFunctionsPush(); for (const key of Object.keys(pushFunctions)) { hookFunctions[key]!.push.apply(hookFunctions[key], pushFunctions[key]); } + if (isMainProcess) { + const preExecuteFunctions = hookFunctionsPreExecute(); + for (const key of Object.keys(preExecuteFunctions)) { + hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]); + } + } + return new WorkflowHooks(hookFunctions, data.executionMode, executionId, data.workflowData, { sessionId: data.sessionId, retryOf: data.retryOf as string}); } diff --git a/packages/cli/src/WorkflowRunner.ts b/packages/cli/src/WorkflowRunner.ts index 3a5e197f1a..3306282a4e 100644 --- a/packages/cli/src/WorkflowRunner.ts +++ b/packages/cli/src/WorkflowRunner.ts @@ -100,9 +100,6 @@ export class WorkflowRunner { * @memberof WorkflowRunner */ async run(data: IWorkflowExecutionDataProcess, loadStaticData?: boolean): Promise { - const externalHooks = ExternalHooks(); - await externalHooks.run('workflow.execute', [data.workflowData, data.executionMode]); - const executionsProcess = config.get('executions.process') as string; let executionId: string; @@ -112,6 +109,7 @@ export class WorkflowRunner { executionId = await this.runSubprocess(data, loadStaticData); } + const externalHooks = ExternalHooks(); if (externalHooks.exists('workflow.postExecute')) { this.activeExecutions.getPostExecutePromise(executionId) .then(async (executionData) => { @@ -148,7 +146,7 @@ export class WorkflowRunner { // Register the active execution const executionId = this.activeExecutions.add(data, undefined); - additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId); + additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksMain(data, executionId, true); let workflowExecution: PCancelable; if (data.executionData !== undefined) { diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index 894e110297..8fc749b4b8 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -2,6 +2,7 @@ import { CredentialsOverwrites, CredentialTypes, + ExternalHooks, IWorkflowExecutionDataProcessWithExecution, NodeTypes, WorkflowExecuteAdditionalData, @@ -19,6 +20,7 @@ import { INodeTypeData, IRun, ITaskData, + IWorkflowExecuteHooks, Workflow, WorkflowHooks, } from 'n8n-workflow'; @@ -68,6 +70,10 @@ export class WorkflowRunnerProcess { const credentialsOverwrites = CredentialsOverwrites(); await credentialsOverwrites.init(inputData.credentialsOverwrite); + // Load all external hooks + const externalHooks = ExternalHooks(); + await externalHooks.init(); + this.workflow = new Workflow({ id: this.data.workflowData.id as string | undefined, name: this.data.workflowData.name, nodes: this.data.workflowData!.nodes, connections: this.data.workflowData!.connections, active: this.data.workflowData!.active, nodeTypes, staticData: this.data.workflowData!.staticData, settings: this.data.workflowData!.settings}); const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials); additionalData.hooks = this.getProcessForwardHooks(); @@ -121,7 +127,7 @@ export class WorkflowRunnerProcess { * @returns */ getProcessForwardHooks(): WorkflowHooks { - const hookFunctions = { + const hookFunctions: IWorkflowExecuteHooks = { nodeExecuteBefore: [ async (nodeName: string): Promise => { this.sendHookToParentProcess('nodeExecuteBefore', [nodeName]); @@ -144,6 +150,11 @@ export class WorkflowRunnerProcess { ], }; + const preExecuteFunctions = WorkflowExecuteAdditionalData.hookFunctionsPreExecute(); + for (const key of Object.keys(preExecuteFunctions)) { + hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]); + } + return new WorkflowHooks(hookFunctions, this.data!.executionMode, this.data!.executionId, this.data!.workflowData, { sessionId: this.data!.sessionId, retryOf: this.data!.retryOf as string }); } diff --git a/packages/core/package.json b/packages/core/package.json index dad78b0bcd..bb7dd473aa 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "n8n-core", - "version": "0.50.0", + "version": "0.51.0", "description": "Core functionality of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -47,7 +47,7 @@ "file-type": "^14.6.2", "lodash.get": "^4.4.2", "mime-types": "^2.1.27", - "n8n-workflow": "~0.43.0", + "n8n-workflow": "~0.44.0", "oauth-1.0a": "^2.2.6", "p-cancelable": "^2.0.0", "request": "^2.88.2", diff --git a/packages/core/src/ActiveWorkflows.ts b/packages/core/src/ActiveWorkflows.ts index 2e3b6d7c12..86a88c617b 100644 --- a/packages/core/src/ActiveWorkflows.ts +++ b/packages/core/src/ActiveWorkflows.ts @@ -127,7 +127,7 @@ export class ActiveWorkflows { for (const item of pollTimes.item) { cronTime = []; if (item.mode === 'custom') { - cronTimes.push(item.cronExpression as string); + cronTimes.push((item.cronExpression as string).trim()); continue; } if (item.mode === 'everyMinute') { @@ -178,6 +178,11 @@ export class ActiveWorkflows { // Start the cron-jobs const cronJobs: CronJob[] = []; for (const cronTime of cronTimes) { + const cronTimeParts = cronTime.split(' '); + if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*')) { + throw new Error('The polling interval is too short. It has to be at least a minute!'); + } + cronJobs.push(new CronJob(cronTime, executeTrigger, undefined, true, timezone)); } diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 4133db0d9f..4ed49770a3 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -468,7 +468,6 @@ export class WorkflowExecute { this.runExecutionData.startData = {}; } - this.executeHook('workflowExecuteBefore', []); let currentExecutionTry = ''; let lastExecutionTry = ''; @@ -482,6 +481,35 @@ export class WorkflowExecute { }); const returnPromise = (async () => { + try { + await this.executeHook('workflowExecuteBefore', [workflow]); + } catch (error) { + // Set the error that it can be saved correctly + executionError = { + message: error.message, + stack: error.stack, + }; + + // Set the incoming data of the node that it can be saved correctly + executionData = this.runExecutionData.executionData!.nodeExecutionStack[0] as IExecuteData; + this.runExecutionData.resultData = { + runData: { + [executionData.node.name]: [ + { + startTime, + executionTime: (new Date().getTime()) - startTime, + data: ({ + 'main': executionData.data.main, + } as ITaskDataConnections), + }, + ], + }, + lastNodeExecuted: executionData.node.name, + error: executionError, + }; + + throw error; + } executionLoop: while (this.runExecutionData.executionData!.nodeExecutionStack.length !== 0) { diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 805de89a55..4e5f0d91d0 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -1,6 +1,6 @@ { "name": "n8n-editor-ui", - "version": "0.62.0", + "version": "0.63.0", "description": "Workflow Editor UI for n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -65,7 +65,7 @@ "lodash.debounce": "^4.0.8", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", - "n8n-workflow": "~0.43.0", + "n8n-workflow": "~0.44.0", "node-sass": "^4.12.0", "normalize-wheel": "^1.0.1", "prismjs": "^1.17.1", diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue index 49e3a12acd..6d01db51ac 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -20,7 +20,7 @@ - Need help? Open credential docs + Need help? Open credential docs @@ -119,7 +119,11 @@ export default mixins( const credentialType = this.$store.getters.credentialType(credentialTypeName); if (credentialType.documentationUrl !== undefined) { - return `${credentialType.documentationUrl}`; + if (credentialType.documentationUrl.startsWith('http')) { + return credentialType.documentationUrl; + } else { + return 'https://docs.n8n.io/credentials/' + credentialType.documentationUrl + '/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal'; + } } return undefined; }, diff --git a/packages/editor-ui/src/components/DataDisplay.vue b/packages/editor-ui/src/components/DataDisplay.vue index 6eb2b90ea3..c694173540 100644 --- a/packages/editor-ui/src/components/DataDisplay.vue +++ b/packages/editor-ui/src/components/DataDisplay.vue @@ -9,7 +9,7 @@
-
@@ -65,6 +65,17 @@ export default Vue.extend({ }; }, computed: { + documentationUrl (): string { + if (!this.nodeType) { + return ''; + } + + if (this.nodeType.documentationUrl && this.nodeType.documentationUrl.startsWith('http')) { + return this.nodeType.documentationUrl; + } + + return 'https://docs.n8n.io/nodes/' + (this.nodeType.documentationUrl || this.nodeType.name) + '?utm_source=n8n_app&utm_medium=node_settings_modal-credential_link&utm_campaign=' + this.nodeType.name; + }, node (): INodeUi { return this.$store.getters.activeNode; }, diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 893c61aa33..336e27041a 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -82,8 +82,8 @@
- - + +
@@ -213,6 +213,10 @@ export default mixins( this.loadRemoteParameterOptions(); }, value () { + if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true) { + // Do not set for color with alpha else wrong value gets displayed in field + return; + } this.tempValue = this.displayValue as string; }, }, @@ -274,6 +278,18 @@ export default mixins( returnValue = this.expressionValueComputed; } + if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && returnValue.charAt(0) === '#') { + // Convert the value to rgba that el-color-picker can display it correctly + const bigint = parseInt(returnValue.slice(1), 16); + const h = []; + h.push((bigint >> 24) & 255); + h.push((bigint >> 16) & 255); + h.push((bigint >> 8) & 255); + h.push((255 - bigint & 255) / 255); + + returnValue = 'rgba('+h.join()+')'; + } + if (returnValue !== undefined && returnValue !== null && this.parameter.type === 'string') { const rows = this.getArgument('rows'); if (rows === undefined || rows === 1) { @@ -537,14 +553,35 @@ export default mixins( // Set focus on field setTimeout(() => { // @ts-ignore - (this.$refs.inputField.$el.querySelector('input') as HTMLInputElement).focus(); + if (this.$refs.inputField.$el) { + // @ts-ignore + (this.$refs.inputField.$el.querySelector('input') as HTMLInputElement).focus(); + } }); }, + rgbaToHex (value: string): string | null { + // Convert rgba to hex from: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb + const valueMatch = (value as string).match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)$/); + if (valueMatch === null) { + // TODO: Display something if value is not valid + return null; + } + const [r, g, b, a] = valueMatch.splice(1, 4).map(v => Number(v)); + return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) + ((1 << 8) + Math.floor((1-a)*255)).toString(16).slice(1); + }, valueChanged (value: string | number | boolean | Date | null) { if (value instanceof Date) { value = value.toISOString(); } + if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && value !== null && value.toString().charAt(0) !== '#') { + const newValue = this.rgbaToHex(value as string); + if (newValue !== null) { + this.tempValue = newValue; + value = newValue; + } + } + const parameterData = { node: this.node !== null ? this.node.name : this.nodeName, name: this.path, @@ -570,6 +607,13 @@ export default mixins( this.nodeName = this.node.name; } + if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && this.displayValue !== null && this.displayValue.toString().charAt(0) !== '#') { + const newValue = this.rgbaToHex(this.displayValue as string); + if (newValue !== null) { + this.tempValue = newValue; + } + } + if (this.remoteMethod !== undefined && this.node !== null) { // Make sure to load the parameter options // directly and whenever the credentials change diff --git a/packages/nodes-base/credentials/GetResponseApi.credentials.ts b/packages/nodes-base/credentials/GetResponseApi.credentials.ts new file mode 100644 index 0000000000..1494a01930 --- /dev/null +++ b/packages/nodes-base/credentials/GetResponseApi.credentials.ts @@ -0,0 +1,17 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class GetResponseApi implements ICredentialType { + name = 'getResponseApi'; + displayName = 'GetResponse API'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts b/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts new file mode 100644 index 0000000000..76ce3acb0f --- /dev/null +++ b/packages/nodes-base/credentials/GetResponseOAuth2Api.credentials.ts @@ -0,0 +1,47 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class GetResponseOAuth2Api implements ICredentialType { + name = 'getResponseOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'GetResponse OAuth2 API'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://app.getresponse.com/oauth2_authorize.html', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.getresponse.com/v3/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + description: 'Resource to consume.', + }, + ]; +} diff --git a/packages/nodes-base/credentials/GotifyApi.credentials.ts b/packages/nodes-base/credentials/GotifyApi.credentials.ts new file mode 100644 index 0000000000..e417f60592 --- /dev/null +++ b/packages/nodes-base/credentials/GotifyApi.credentials.ts @@ -0,0 +1,33 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class GotifyApi implements ICredentialType { + name = 'gotifyApi'; + displayName = 'Gotify API'; + documentationUrl = 'gotify'; + properties = [ + { + displayName: 'App API Token', + name: 'appApiToken', + type: 'string' as NodePropertyTypes, + default: '', + description: '(Optional) Needed for message creation.', + }, + { + displayName: 'Client API Token', + name: 'clientApiToken', + type: 'string' as NodePropertyTypes, + default: '', + description: '(Optional) Needed for everything (delete, getAll) but message creation.', + }, + { + displayName: 'URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + description: 'The URL of the Gotify host.', + }, + ]; +} diff --git a/packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts b/packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts new file mode 100644 index 0000000000..b5bc61fb8f --- /dev/null +++ b/packages/nodes-base/credentials/LineNotifyOAuth2Api.credentials.ts @@ -0,0 +1,48 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class LineNotifyOAuth2Api implements ICredentialType { + name = 'lineNotifyOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Line Notify OAuth2 API'; + documentationUrl = 'line'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://notify-bot.line.me/oauth/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://notify-bot.line.me/oauth/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'notify', + required: true, + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'body', + }, + ]; +} diff --git a/packages/nodes-base/credentials/MicrosoftSql.credentials.ts b/packages/nodes-base/credentials/MicrosoftSql.credentials.ts index 598c9ded21..1b14ec5778 100644 --- a/packages/nodes-base/credentials/MicrosoftSql.credentials.ts +++ b/packages/nodes-base/credentials/MicrosoftSql.credentials.ts @@ -44,5 +44,11 @@ export class MicrosoftSql implements ICredentialType { type: 'string' as NodePropertyTypes, default: '', }, + { + displayName: 'TLS', + name: 'tls', + type: 'boolean' as NodePropertyTypes, + default: true, + }, ]; } diff --git a/packages/nodes-base/credentials/Sftp.credentials.ts b/packages/nodes-base/credentials/Sftp.credentials.ts index 5c7dd2122c..d9eb0f2747 100644 --- a/packages/nodes-base/credentials/Sftp.credentials.ts +++ b/packages/nodes-base/credentials/Sftp.credentials.ts @@ -38,5 +38,25 @@ export class Sftp implements ICredentialType { }, default: '', }, + { + displayName: 'Private Key', + name: 'privateKey', + type: 'string' as NodePropertyTypes, + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'String that contains a private key for either key-based or hostbased user authentication (OpenSSH format).', + }, + { + displayName: 'Passphrase', + name: 'passphrase', + typeOptions: { + password: true, + }, + type: 'string' as NodePropertyTypes, + default: '', + description: 'For an encrypted private key, this is the passphrase used to decrypt it.', + }, ]; } diff --git a/packages/nodes-base/credentials/StrapiApi.credentials.ts b/packages/nodes-base/credentials/StrapiApi.credentials.ts new file mode 100644 index 0000000000..801503193c --- /dev/null +++ b/packages/nodes-base/credentials/StrapiApi.credentials.ts @@ -0,0 +1,33 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class StrapiApi implements ICredentialType { + name = 'strapiApi'; + displayName = 'Strapi API'; + properties = [ + { + displayName: 'Email', + name: 'email', + type: 'string' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Password', + name: 'password', + type: 'string' as NodePropertyTypes, + typeOptions: { + password: true, + }, + default: '', + }, + { + displayName: 'URL', + name: 'url', + type: 'string' as NodePropertyTypes, + default: '', + placeholder: 'https://api.example.com', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Amqp/Amqp.node.ts b/packages/nodes-base/nodes/Amqp/Amqp.node.ts index 6cbdcf4b4d..3fd8133711 100644 --- a/packages/nodes-base/nodes/Amqp/Amqp.node.ts +++ b/packages/nodes-base/nodes/Amqp/Amqp.node.ts @@ -1,6 +1,6 @@ import { ContainerOptions, Delivery } from 'rhea'; -import { IExecuteSingleFunctions } from 'n8n-core'; +import { IExecuteFunctions } from 'n8n-core'; import { IDataObject, INodeExecutionData, @@ -69,17 +69,15 @@ export class Amqp implements INodeType { ], }; - async executeSingle(this: IExecuteSingleFunctions): Promise { - const item = this.getInputData(); - + async execute(this: IExecuteFunctions): Promise < INodeExecutionData[][] > { const credentials = this.getCredentials('amqp'); if (!credentials) { throw new Error('Credentials are mandatory!'); } - const sink = this.getNodeParameter('sink', '') as string; - const applicationProperties = this.getNodeParameter('headerParametersJson', {}) as string | object; - const options = this.getNodeParameter('options', {}) as IDataObject; + const sink = this.getNodeParameter('sink', 0, '') as string; + const applicationProperties = this.getNodeParameter('headerParametersJson', 0, {}) as string | object; + const options = this.getNodeParameter('options', 0, {}) as IDataObject; let headerProperties = applicationProperties; if (typeof applicationProperties === 'string' && applicationProperties !== '') { @@ -109,35 +107,43 @@ export class Amqp implements INodeType { connectOptions.transport = credentials.transportType; } - const allSent = new Promise(( resolve ) => { - container.on('sendable', (context: any) => { // tslint:disable-line:no-any + const conn = container.connect(connectOptions); + const sender = conn.open_sender(sink); - let body: IDataObject | string = item.json; - const sendOnlyProperty = options.sendOnlyProperty as string; + const responseData: IDataObject[] = await new Promise((resolve) => { + container.once('sendable', (context: any) => { // tslint:disable-line:no-any + const returnData = []; - if (sendOnlyProperty) { - body = body[sendOnlyProperty] as string; + const items = this.getInputData(); + for (let i = 0; i < items.length; i++) { + const item = items[i]; + + let body: IDataObject | string = item.json; + const sendOnlyProperty = options.sendOnlyProperty as string; + + if (sendOnlyProperty) { + body = body[sendOnlyProperty] as string; + } + + if (options.dataAsObject !== true) { + body = JSON.stringify(body); + } + + const result = context.sender.send({ + application_properties: headerProperties, + body, + }); + + returnData.push({ id: result.id }); } - if (options.dataAsObject !== true) { - body = JSON.stringify(body); - } - - const message = { - application_properties: headerProperties, - body, - }; - - const sendResult = context.sender.send(message); - - resolve(sendResult); + resolve(returnData); }); }); - container.connect(connectOptions).open_sender(sink); + sender.close(); + conn.close(); - const sendResult: Delivery = await allSent as Delivery; // sendResult has a a property that causes circular reference if returned - - return { json: { id: sendResult.id } } as INodeExecutionData; + return [this.helpers.returnJsonArray(responseData)]; } } diff --git a/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts b/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts index 1b1e679f13..40af50c4f2 100644 --- a/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts +++ b/packages/nodes-base/nodes/Amqp/AmqpTrigger.node.ts @@ -82,6 +82,20 @@ export class AmqpTrigger implements INodeType { default: false, description: 'Returns only the body property.', }, + { + displayName: 'Messages per Cicle', + name: 'pullMessagesNumber', + type: 'number', + default: 100, + description: 'Number of messages to pull from the bus for every cicle', + }, + { + displayName: 'Sleep Time', + name: 'sleepTime', + type: 'number', + default: 10, + description: 'Milliseconds to sleep after every cicle.', + }, ], }, ], @@ -99,6 +113,7 @@ export class AmqpTrigger implements INodeType { const clientname = this.getNodeParameter('clientname', '') as string; const subscription = this.getNodeParameter('subscription', '') as string; const options = this.getNodeParameter('options', {}) as IDataObject; + const pullMessagesNumber = options.pullMessagesNumber || 100; if (sink === '') { throw new Error('Queue or Topic required!'); @@ -130,10 +145,13 @@ export class AmqpTrigger implements INodeType { connectOptions.transport = credentials.transportType; } - let lastMsgId: number | undefined = undefined; const self = this; + container.on('receiver_open', (context: any) => { // tslint:disable-line:no-any + context.receiver.add_credit(pullMessagesNumber); + }); + container.on('message', (context: any) => { // tslint:disable-line:no-any // ignore duplicate message check, don't think it's necessary, but it was in the rhea-lib example code if (context.message.message_id && context.message.message_id === lastMsgId) { @@ -143,6 +161,12 @@ export class AmqpTrigger implements INodeType { let data = context.message; + if (options.jsonConvertByteArrayToString === true && data.body.content !== undefined) { + // The buffer is not ready... Stringify and parse back to load it. + const cont = JSON.stringify(data.body.content); + data.body = String.fromCharCode.apply(null, JSON.parse(cont).data); + } + if (options.jsonConvertByteArrayToString === true && data.body.content !== undefined) { // The buffer is not ready... Stringify and parse back to load it. const content = JSON.stringify(data.body.content); @@ -158,6 +182,12 @@ export class AmqpTrigger implements INodeType { self.emit([self.helpers.returnJsonArray([data])]); + + if (context.receiver.credit === 0) { + setTimeout(() => { + context.receiver.add_credit(pullMessagesNumber); + }, options.sleepTime as number || 10); + } }); const connection = container.connect(connectOptions); @@ -170,14 +200,14 @@ export class AmqpTrigger implements INodeType { durable: 2, expiry_policy: 'never', }, - credit_window: 1, // prefetch 1 + credit_window: 0, // prefetch 1 }; } else { clientOptions = { source: { address: sink, }, - credit_window: 1, // prefetch 1 + credit_window: 0, // prefetch 1 }; } connection.open_receiver(clientOptions); @@ -186,6 +216,8 @@ export class AmqpTrigger implements INodeType { // The "closeFunction" function gets called by n8n whenever // the workflow gets deactivated and can so clean up. async function closeFunction() { + container.removeAllListeners('receiver_open'); + container.removeAllListeners('message'); connection.close(); } diff --git a/packages/nodes-base/nodes/EditImage.node.ts b/packages/nodes-base/nodes/EditImage.node.ts index 9e10000ffd..5c3b9c53aa 100644 --- a/packages/nodes-base/nodes/EditImage.node.ts +++ b/packages/nodes-base/nodes/EditImage.node.ts @@ -9,6 +9,12 @@ import { INodeTypeDescription, } from 'n8n-workflow'; import * as gm from 'gm'; +import { file } from 'tmp-promise'; +import { + writeFile as fsWriteFile, +} from 'fs'; +import { promisify } from 'util'; +const fsWriteFileAsync = promisify(fsWriteFile); export class EditImage implements INodeType { @@ -61,6 +67,11 @@ export class EditImage implements INodeType { value: 'resize', description: 'Change the size of image', }, + { + name: 'Shear', + value: 'shear', + description: 'Shear image along the X or Y axis', + }, { name: 'Text', value: 'text', @@ -385,6 +396,11 @@ export class EditImage implements INodeType { value: 'onlyIfSmaller', description: 'Resize only if image is smaller than width or height', }, + { + name: 'Percent', + value: 'percent', + description: 'Width and height are specified in percents.', + }, ], default: 'maximumArea', displayOptions: { @@ -422,7 +438,10 @@ export class EditImage implements INodeType { displayName: 'Background Color', name: 'backgroundColor', type: 'color', - default: '#ffffff', + default: '#ffffffff', + typeOptions: { + showAlpha: true, + }, displayOptions: { show: { operation: [ @@ -433,6 +452,39 @@ export class EditImage implements INodeType { description: 'The color to use for the background when image gets rotated by anything which is not a multiple of 90..', }, + + // ---------------------------------- + // shear + // ---------------------------------- + { + displayName: 'Degrees X', + name: 'degreesX', + type: 'number', + default: 0, + displayOptions: { + show: { + operation: [ + 'shear', + ], + }, + }, + description: 'X (horizontal) shear degrees.', + }, + { + displayName: 'Degrees Y', + name: 'degreesY', + type: 'number', + default: 0, + displayOptions: { + show: { + operation: [ + 'shear', + ], + }, + }, + description: 'Y (vertical) shear degrees.', + }, + { displayName: 'Options', name: 'options', @@ -503,7 +555,6 @@ export class EditImage implements INodeType { }, description: 'Sets the jpeg|png|tiff compression level from 0 to 100 (best).', }, - ], }, ], @@ -529,6 +580,8 @@ export class EditImage implements INodeType { let gmInstance = gm(Buffer.from(item.binary![dataPropertyName as string].data, BINARY_ENCODING)); + gmInstance = gmInstance.background('transparent'); + if (operation === 'blur') { const blur = this.getNodeParameter('blur') as number; const sigma = this.getNodeParameter('sigma') as number; @@ -574,6 +627,8 @@ export class EditImage implements INodeType { option = '<'; } else if (resizeOption === 'onlyIfLarger') { option = '>'; + } else if (resizeOption === 'percent') { + option = '%'; } gmInstance = gmInstance.resize(width, height, option); @@ -581,6 +636,10 @@ export class EditImage implements INodeType { const rotate = this.getNodeParameter('rotate') as number; const backgroundColor = this.getNodeParameter('backgroundColor') as string; gmInstance = gmInstance.rotate(backgroundColor, rotate); + } else if (operation === 'shear') { + const xDegrees = this.getNodeParameter('degreesX') as number; + const yDegress = this.getNodeParameter('degreesY') as number; + gmInstance = gmInstance.shear(xDegrees, yDegress); } else if (operation === 'text') { const fontColor = this.getNodeParameter('fontColor') as string; const fontSize = this.getNodeParameter('fontSize') as number; @@ -624,6 +683,8 @@ export class EditImage implements INodeType { // data references which do not get changed still stay behind // but the incoming data does not get changed. Object.assign(newItem.binary, item.binary); + // Make a deep copy of the binary data we change + newItem.binary![dataPropertyName as string] = JSON.parse(JSON.stringify(newItem.binary![dataPropertyName as string])); } if (options.quality !== undefined) { diff --git a/packages/nodes-base/nodes/Ftp.node.ts b/packages/nodes-base/nodes/Ftp.node.ts index c71b096bf7..dacbd40a5d 100644 --- a/packages/nodes-base/nodes/Ftp.node.ts +++ b/packages/nodes-base/nodes/Ftp.node.ts @@ -288,6 +288,8 @@ export class Ftp implements INodeType { port: credentials.port as number, username: credentials.username as string, password: credentials.password as string, + privateKey: credentials.privateKey as string | undefined, + passphrase: credentials.passphrase as string | undefined, }); } else { diff --git a/packages/nodes-base/nodes/GetResponse/ContactDescription.ts b/packages/nodes-base/nodes/GetResponse/ContactDescription.ts new file mode 100644 index 0000000000..0a8e60cdfe --- /dev/null +++ b/packages/nodes-base/nodes/GetResponse/ContactDescription.ts @@ -0,0 +1,646 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const contactOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'contact', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new contact', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a contact', + }, + { + name: 'Get', + value: 'get', + description: 'Get a contact', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all contacts', + }, + { + name: 'Update', + value: 'update', + description: 'Update contact properties', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const contactFields = [ + /* -------------------------------------------------------------------------- */ + /* contact:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Email', + name: 'email', + type: 'string', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'create', + ], + }, + }, + options: [ + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Custom Field', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customFieldValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'customFieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + description: 'The end user specified key of the user defined data.', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + description: 'The end user specified value of the user defined data.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Day Of Cycle', + name: 'dayOfCycle', + type: 'string', + description: `The day on which the contact is in the Autoresponder cycle. null indicates the contacts is not in the cycle.`, + default: '', + }, + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + description: `The contact's IP address. IPv4 and IPv6 formats are accepted.`, + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + default: '', + }, + { + displayName: 'Scoring', + name: 'scoring', + type: 'number', + default: '', + description: 'Contact scoring, pass null to remove the score from a contact', + typeOptions: { + minValue: 0, + }, + }, + { + displayName: 'Tag IDs', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* contact:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + description: 'Id of contact to delete.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'delete', + ], + }, + }, + options: [ + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + description: `This makes it possible to pass the IP from which the contact unsubscribed. Used only if the messageId was send.`, + default: '', + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + description: `The ID of a message (such as a newsletter, an autoresponder, or an RSS-newsletter). When passed, this method will simulate the unsubscribe process, as if the contact clicked the unsubscribe link in a given message.`, + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* contact:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'get', + ], + }, + }, + default: '', + description: 'Unique identifier for a particular contact', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'get', + ], + }, + }, + options: [ + { + displayName: 'Fields', + name: 'fields', + type: 'string', + description: `List of fields that should be returned. Id is always returned. Fields should be separated by comma`, + default: '', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* contact:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 20, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'string', + description: `Search contacts by campaign ID`, + default: '', + }, + { + displayName: 'Change On From', + name: 'changeOnFrom', + type: 'dateTime', + default: '', + description: `Search contacts edited from this date`, + }, + { + displayName: 'Change On To', + name: 'changeOnTo', + type: 'dateTime', + default: '', + description: `Search contacts edited to this date`, + }, + { + displayName: 'Created On From', + name: 'createdOnFrom', + type: 'dateTime', + default: '', + description: `Count data from this date`, + }, + { + displayName: 'Created On To', + name: 'createdOnTo', + type: 'dateTime', + default: '', + description: `Count data from this date`, + }, + { + displayName: 'Exact Match', + name: 'exactMatch', + type: 'boolean', + default: false, + description: `When set to true it will search for contacts with the exact value
+ of the email and name provided in the query string. Without this flag, matching is done via a standard 'like' comparison,
+ which may sometimes be slow.`, + }, + { + displayName: 'Fields', + name: 'fields', + type: 'string', + description: `List of fields that should be returned. Id is always returned. Fields should be separated by comma`, + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + description: `Search contacts by name`, + default: '', + }, + { + displayName: 'Origin', + name: 'origin', + type: 'options', + options: [ + { + name: 'API', + value: 'api', + }, + { + name: 'Copy', + value: 'copy', + }, + { + name: 'Email', + value: 'email', + }, + { + name: 'Forward', + value: 'forward', + }, + { + name: 'import', + value: 'import', + }, + { + name: 'Iphone', + value: 'iphone', + }, + { + name: 'Landing Page', + value: 'landing_page', + }, + { + name: 'Leads', + value: 'leads', + }, + { + name: 'Panel', + value: 'panel', + }, + { + name: 'Sale', + value: 'sale', + }, + { + name: 'Survey', + value: 'survey', + }, + { + name: 'Webinar', + value: 'webinar', + }, + { + name: 'WWW', + value: 'www', + }, + ], + description: `Search contacts by origin`, + default: '', + }, + { + displayName: 'Sort By', + name: 'sortBy', + type: 'options', + options: [ + { + name: 'Campaign ID', + value: 'campaignId', + }, + { + name: 'Changed On', + value: 'changedOn', + }, + { + name: 'Created On', + value: 'createdOn', + }, + { + name: 'Email', + value: 'email', + }, + ], + default: '', + }, + { + displayName: 'Sort Order', + name: 'sortOrder', + type: 'options', + options: [ + { + name: 'ASC', + value: 'ASC', + }, + { + name: 'DESC', + value: 'DESC', + }, + ], + default: '', + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* contact:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Contact ID', + name: 'contactId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + description: 'Unique identifier for a particular contact', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'contact', + ], + operation: [ + 'update', + ], + }, + }, + options: [ + { + displayName: 'Campaign ID', + name: 'campaignId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCampaigns', + }, + default: '', + }, + { + displayName: 'Custom Fields', + name: 'customFieldsUi', + type: 'fixedCollection', + default: '', + placeholder: 'Add Custom Field', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customFieldValues', + displayName: 'Custom Field', + values: [ + { + displayName: 'Field ID', + name: 'customFieldId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getCustomFields', + }, + description: 'The end user specified key of the user defined data.', + default: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + description: 'The end user specified value of the user defined data.', + default: '', + }, + ], + }, + ], + }, + { + displayName: 'Day Of Cycle', + name: 'dayOfCycle', + type: 'string', + description: `The day on which the contact is in the Autoresponder cycle. null indicates the contacts is not in the cycle.`, + default: '', + }, + { + displayName: 'Email', + name: 'email', + type: 'string', + default: '', + }, + { + displayName: 'IP Address', + name: 'ipAddress', + type: 'string', + description: `The contact's IP address. IPv4 and IPv6 formats are accepted.`, + default: '', + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + default: '', + }, + { + displayName: 'Note', + name: 'note', + type: 'string', + default: '', + }, + { + displayName: 'Scoring', + name: 'scoring', + type: 'number', + default: '', + description: 'Contact scoring, pass null to remove the score from a contact', + typeOptions: { + minValue: 0, + }, + }, + { + displayName: 'Tag IDs', + name: 'tags', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getTags', + }, + default: '', + }, + ], + }, + +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts b/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts new file mode 100644 index 0000000000..f07d130da4 --- /dev/null +++ b/packages/nodes-base/nodes/GetResponse/GenericFunctions.ts @@ -0,0 +1,70 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject +} from 'n8n-workflow'; + +export async function getresponseApiRequest(this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const authentication = this.getNodeParameter('authentication', 0, 'apiKey') as string; + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || `https://api.getresponse.com/v3${resource}`, + json: true, + }; + try { + options = Object.assign({}, options, option); + if (Object.keys(body).length === 0) { + delete options.body; + } + + if (authentication === 'apiKey') { + const credentials = this.getCredentials('getResponseApi') as IDataObject; + options!.headers!['X-Auth-Token'] = `api-key ${credentials.apiKey}`; + //@ts-ignore + return await this.helpers.request.call(this, options); + } else { + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'getResponseOAuth2Api', options); + } + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`GetResponse error response [${error.statusCode}]: ${error.response.body.message}`); + } + throw error; + } +} + +export async function getResponseApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + query.page = 1; + + do { + responseData = await getresponseApiRequest.call(this, method, endpoint, body, query, undefined, { resolveWithFullResponse: true }); + query.page++; + returnData.push.apply(returnData, responseData.body); + } while ( + responseData.headers.TotalPages !== responseData.headers.CurrentPage + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts b/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts new file mode 100644 index 0000000000..09e772e1ac --- /dev/null +++ b/packages/nodes-base/nodes/GetResponse/GetResponse.node.ts @@ -0,0 +1,320 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + getresponseApiRequest, + getResponseApiRequestAllItems, +} from './GenericFunctions'; + +import { + contactFields, + contactOperations, +} from './ContactDescription'; + +import * as moment from 'moment-timezone'; + +export class GetResponse implements INodeType { + description: INodeTypeDescription = { + displayName: 'GetResponse', + name: 'getResponse', + icon: 'file:getResponse.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume GetResponse API.', + defaults: { + name: 'GetResponse', + color: '#00afec', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'getResponseApi', + required: true, + displayOptions: { + show: { + authentication: [ + 'apiKey', + ], + }, + }, + }, + { + name: 'getResponseOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, + }, + ], + properties: [ + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'API Key', + value: 'apiKey', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'apiKey', + description: 'The resource to operate on.', + }, + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Contact', + value: 'contact', + }, + ], + default: 'contact', + description: 'The resource to operate on.', + }, + ...contactOperations, + ...contactFields, + ], + }; + + methods = { + loadOptions: { + // Get all the campaigns to display them to user so that he can + // select them easily + async getCampaigns( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const campaigns = await getresponseApiRequest.call( + this, + 'GET', + `/campaigns`, + ); + for (const campaign of campaigns) { + returnData.push({ + name: campaign.name as string, + value: campaign.campaignId, + }); + } + return returnData; + }, + // Get all the tagd to display them to user so that he can + // select them easily + async getTags( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const tags = await getresponseApiRequest.call( + this, + 'GET', + `/tags`, + ); + for (const tag of tags) { + returnData.push({ + name: tag.name as string, + value: tag.tagId, + }); + } + return returnData; + }, + // Get all the custom fields to display them to user so that he can + // select them easily + async getCustomFields( + this: ILoadOptionsFunctions, + ): Promise { + const returnData: INodePropertyOptions[] = []; + const customFields = await getresponseApiRequest.call( + this, + 'GET', + `/custom-fields`, + ); + for (const customField of customFields) { + returnData.push({ + name: customField.name as string, + value: customField.customFieldId, + }); + } + return returnData; + }, + }, + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + + if (resource === 'contact') { + //https://apireference.getresponse.com/#operation/createContact + if (operation === 'create') { + const email = this.getNodeParameter('email', i) as string; + + const campaignId = this.getNodeParameter('campaignId', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + email, + campaign: { + campaignId, + }, + }; + + Object.assign(body, additionalFields); + + if (additionalFields.customFieldsUi) { + const customFieldValues = (additionalFields.customFieldsUi as IDataObject).customFieldValues as IDataObject[]; + if (customFieldValues) { + body.customFieldValues = customFieldValues; + for (let i = 0; i < customFieldValues.length; i++) { + if (!Array.isArray(customFieldValues[i].value)) { + customFieldValues[i].value = [customFieldValues[i].value]; + } + } + delete body.customFieldsUi; + } + } + + responseData = await getresponseApiRequest.call(this, 'POST', '/contacts', body); + + responseData = { success: true }; + } + //https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/deleteContact + if (operation === 'delete') { + const contactId = this.getNodeParameter('contactId', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + Object.assign(qs, options); + + responseData = await getresponseApiRequest.call(this, 'DELETE', `/contacts/${contactId}`, {}, qs); + + responseData = { success: true }; + } + //https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/getContactById + if (operation === 'get') { + const contactId = this.getNodeParameter('contactId', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + Object.assign(qs, options); + + responseData = await getresponseApiRequest.call(this, 'GET', `/contacts/${contactId}`, {}, qs); + } + //https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/getContactList + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const options = this.getNodeParameter('options', i) as IDataObject; + + const timezone = this.getTimezone(); + + Object.assign(qs, options); + + const isNotQuery = [ + 'sortBy', + 'sortOrder', + 'additionalFlags', + 'fields', + 'exactMatch', + ]; + + const isDate = [ + 'createdOnFrom', + 'createdOnTo', + 'changeOnFrom', + 'changeOnTo', + ]; + + const dateMapToKey: { [key: string]: string; } = { + 'createdOnFrom': '[createdOn][from]', + 'createdOnTo': '[createdOn][to]', + 'changeOnFrom': '[changeOn][from]', + 'changeOnTo': '[changeOn][to]', + }; + + for (const key of Object.keys(qs)) { + if (!isNotQuery.includes(key)) { + if (isDate.includes(key)) { + qs[`query${dateMapToKey[key]}`] = moment.tz(qs[key], timezone).format('YYYY-MM-DDTHH:mm:ssZZ'); + } else { + qs[`query[${key}]`] = qs[key]; + } + delete qs[key]; + } + } + + if (qs.sortBy) { + qs[`sort[${qs.sortBy}]`] = qs.sortOrder || 'ASC'; + } + + if (qs.exactMatch === true) { + qs['additionalFlags'] = 'exactMatch'; + delete qs.exactMatch; + } + + if (returnAll) { + responseData = await getResponseApiRequestAllItems.call(this, 'GET', `/contacts`, {}, qs); + } else { + qs.perPage = this.getNodeParameter('limit', i) as number; + responseData = await getresponseApiRequest.call(this, 'GET', `/contacts`, {}, qs); + } + } + //https://apireference.getresponse.com/?_ga=2.160836350.2102802044.1604719933-1897033509.1604598019#operation/updateContact + if (operation === 'update') { + + const contactId = this.getNodeParameter('contactId', i) as string; + + const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; + + const body: IDataObject = {}; + + Object.assign(body, updateFields); + + if (updateFields.customFieldsUi) { + const customFieldValues = (updateFields.customFieldsUi as IDataObject).customFieldValues as IDataObject[]; + if (customFieldValues) { + body.customFieldValues = customFieldValues; + delete body.customFieldsUi; + } + } + + responseData = await getresponseApiRequest.call(this, 'POST', `/contacts/${contactId}`, body); + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/GetResponse/getResponse.png b/packages/nodes-base/nodes/GetResponse/getResponse.png new file mode 100644 index 0000000000..f533a87a64 Binary files /dev/null and b/packages/nodes-base/nodes/GetResponse/getResponse.png differ diff --git a/packages/nodes-base/nodes/Gotify/GenericFunctions.ts b/packages/nodes-base/nodes/Gotify/GenericFunctions.ts new file mode 100644 index 0000000000..ab694fe577 --- /dev/null +++ b/packages/nodes-base/nodes/Gotify/GenericFunctions.ts @@ -0,0 +1,68 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function gotifyApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, uri?: string | undefined, option = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('gotifyApi') as IDataObject; + + const options: OptionsWithUri = { + method, + headers: { + 'X-Gotify-Key': (method === 'POST') ? credentials.appApiToken : credentials.clientApiToken, + accept: 'application/json', + }, + body, + qs, + uri: uri || `${credentials.url}${path}`, + json: true, + }; + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + + //@ts-ignore + return await this.helpers.request.call(this, options); + } catch (error) { + + if (error.response && error.response.body && error.response.body.errorDescription) { + const message = error.response.body.errorDescription; + // Try to return the error prettier + throw new Error( + `Gotify error response [${error.statusCode}]: ${message}`, + ); + } + throw error; + } +} + +export async function gotifyApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + let uri: string | undefined; + query.limit = 100; + do { + responseData = await gotifyApiRequest.call(this, method, endpoint, body, query, uri); + if (responseData.paging.next) { + uri = responseData.paging.next; + } + returnData.push.apply(returnData, responseData[propertyName]); + } while ( + responseData.paging.next + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Gotify/Gotify.node.ts b/packages/nodes-base/nodes/Gotify/Gotify.node.ts new file mode 100644 index 0000000000..1c6179efc6 --- /dev/null +++ b/packages/nodes-base/nodes/Gotify/Gotify.node.ts @@ -0,0 +1,262 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + gotifyApiRequest, + gotifyApiRequestAllItems, +} from './GenericFunctions'; + +export class Gotify implements INodeType { + description: INodeTypeDescription = { + displayName: 'Gotify', + name: 'gotify', + icon: 'file:gotify.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Gotify API.', + defaults: { + name: 'Gotify', + color: '#71c8ec', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'gotifyApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Message', + value: 'message', + }, + ], + default: 'message', + description: 'The resource to operate on.', + }, + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'message', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + }, + { + name: 'Delete', + value: 'delete', + }, + { + name: 'Get All', + value: 'getAll', + }, + ], + default: 'create', + description: 'The resource to operate on.', + }, + { + displayName: 'Message', + name: 'message', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + description: `The message. Markdown (excluding html) is allowed.`, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'create', + ], + }, + }, + default: {}, + options: [ + { + displayName: 'Priority', + name: 'priority', + type: 'number', + default: 1, + description: 'The priority of the message.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + description: `The title of the message.`, + }, + ], + }, + { + displayName: 'Message ID', + name: 'messageId', + type: 'string', + required: true, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'delete', + ], + }, + }, + default: '', + description: `The message id.`, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 20, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + if (resource === 'message') { + if (operation === 'create') { + + const message = this.getNodeParameter('message', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + message, + }; + + Object.assign(body, additionalFields); + + responseData = await gotifyApiRequest.call( + this, + 'POST', + `/message`, + body, + ); + } + if (operation === 'delete') { + const messageId = this.getNodeParameter('messageId', i) as string; + + responseData = await gotifyApiRequest.call( + this, + 'DELETE', + `/message/${messageId}`, + ); + responseData = { success: true }; + } + + if (operation === 'getAll') { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + if (returnAll) { + responseData = await gotifyApiRequestAllItems.call( + this, + 'messages', + 'GET', + '/message', + {}, + qs, + ); + + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await gotifyApiRequest.call( + this, + 'GET', + `/message`, + {}, + qs, + ); + responseData = responseData.messages; + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Gotify/gotify.png b/packages/nodes-base/nodes/Gotify/gotify.png new file mode 100644 index 0000000000..f38a25e88a Binary files /dev/null and b/packages/nodes-base/nodes/Gotify/gotify.png differ diff --git a/packages/nodes-base/nodes/Hubspot/DealDescription.ts b/packages/nodes-base/nodes/Hubspot/DealDescription.ts index 837c37a238..28e2261fed 100644 --- a/packages/nodes-base/nodes/Hubspot/DealDescription.ts +++ b/packages/nodes-base/nodes/Hubspot/DealDescription.ts @@ -100,39 +100,12 @@ export const dealFields = [ }, }, options: [ - { - displayName: 'Deal Name', - name: 'dealName', - type: 'string', - default: '', - }, - { - displayName: 'Pipeline', - name: 'pipeline', - type: 'string', - default: '', - }, - { - displayName: 'Close Date', - name: 'closeDate', - type: 'dateTime', - default: '', - }, { displayName: 'Amount', name: 'amount', type: 'string', default: '', }, - { - displayName: 'Deal Type', - name: 'dealType', - type: 'options', - typeOptions: { - loadOptionsMethod: 'getDealTypes', - }, - default: '', - }, { displayName: 'Associated Company', name: 'associatedCompany', @@ -151,6 +124,68 @@ export const dealFields = [ }, default: [], }, + { + displayName: 'Close Date', + name: 'closeDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Custom Properties', + name: 'customPropertiesUi', + placeholder: 'Add Custom Property', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'customPropertiesValues', + displayName: 'Custom Property', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDealCustomProperties', + }, + default: '', + description: 'Name of the property.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the property', + }, + ], + }, + ], + }, + { + displayName: 'Deal Name', + name: 'dealName', + type: 'string', + default: '', + }, + { + displayName: 'Deal Type', + name: 'dealType', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDealTypes', + }, + default: '', + }, + { + displayName: 'Pipeline', + name: 'pipeline', + type: 'string', + default: '', + }, ], }, /* -------------------------------------------------------------------------- */ @@ -191,6 +226,53 @@ export const dealFields = [ }, }, options: [ + { + displayName: 'Amount', + name: 'amount', + type: 'string', + default: '', + }, + { + displayName: 'Close Date', + name: 'closeDate', + type: 'dateTime', + default: '', + }, + { + displayName: 'Custom Properties', + name: 'customPropertiesUi', + placeholder: 'Add Custom Property', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + name: 'customPropertiesValues', + displayName: 'Custom Property', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDealCustomProperties', + }, + default: '', + description: 'Name of the property.', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: 'Value of the property', + }, + ], + }, + ], + }, { displayName: 'Deal Name', name: 'dealName', @@ -208,24 +290,6 @@ export const dealFields = [ default: '', description: 'The dealstage is required when creating a deal. See the CRM Pipelines API for details on managing pipelines and stages.', }, - { - displayName: 'Pipeline', - name: 'pipeline', - type: 'string', - default: '', - }, - { - displayName: 'Close Date', - name: 'closeDate', - type: 'dateTime', - default: '', - }, - { - displayName: 'Amount', - name: 'amount', - type: 'string', - default: '', - }, { displayName: 'Deal Type', name: 'dealType', @@ -235,6 +299,12 @@ export const dealFields = [ }, default: '', }, + { + displayName: 'Pipeline', + name: 'pipeline', + type: 'string', + default: '', + }, ], }, /* -------------------------------------------------------------------------- */ diff --git a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts index aaae8c5895..e04a829d93 100644 --- a/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts +++ b/packages/nodes-base/nodes/Hubspot/Hubspot.node.ts @@ -552,6 +552,25 @@ export class Hubspot implements INodeType { } return returnData; }, + + // Get all the deal properties to display them to user so that he can + // select them easily + async getDealCustomProperties(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const endpoint = '/properties/v2/deals/properties'; + const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {}); + for (const property of properties) { + if (property.hubspotDefined === null) { + const propertyName = property.label; + const propertyId = property.name; + returnData.push({ + name: propertyName, + value: propertyId, + }); + } + } + return returnData; + }, /* -------------------------------------------------------------------------- */ /* FORM */ @@ -1801,6 +1820,17 @@ export class Hubspot implements INodeType { value: additionalFields.pipeline as string, }); } + if (additionalFields.customPropertiesUi) { + const customProperties = (additionalFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[]; + if (customProperties) { + for (const customProperty of customProperties) { + body.properties.push({ + name: customProperty.property, + value: customProperty.value, + }); + } + } + } body.associations = association; const endpoint = '/deals/v1/deal'; responseData = await hubspotApiRequest.call(this, 'POST', endpoint, body); @@ -1846,6 +1876,17 @@ export class Hubspot implements INodeType { value: updateFields.pipeline as string, }); } + if (updateFields.customPropertiesUi) { + const customProperties = (updateFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[]; + if (customProperties) { + for (const customProperty of customProperties) { + body.properties.push({ + name: customProperty.property, + value: customProperty.value, + }); + } + } + } const endpoint = `/deals/v1/deal/${dealId}`; responseData = await hubspotApiRequest.call(this, 'PUT', endpoint, body); } diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts index 93cbdd5cc1..3d2198d34f 100644 --- a/packages/nodes-base/nodes/Jira/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Jira/GenericFunctions.ts @@ -73,7 +73,7 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut } } -export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any +export async function jiraSoftwareCloudApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, endpoint: string, method: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any const returnData: IDataObject[] = []; diff --git a/packages/nodes-base/nodes/Jira/Jira.node.ts b/packages/nodes-base/nodes/Jira/Jira.node.ts index 0418f0b42c..98ecc8757f 100644 --- a/packages/nodes-base/nodes/Jira/Jira.node.ts +++ b/packages/nodes-base/nodes/Jira/Jira.node.ts @@ -112,12 +112,16 @@ export class Jira implements INodeType { async getProjects(this: ILoadOptionsFunctions): Promise { const returnData: INodePropertyOptions[] = []; const jiraVersion = this.getCurrentNodeParameter('jiraVersion') as string; + let endpoint = ''; + let projects; - let endpoint = '/api/2/project/search'; if (jiraVersion === 'server') { endpoint = '/api/2/project'; + projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET'); + } else { + endpoint = '/api/2/project/search'; + projects = await jiraSoftwareCloudApiRequestAllItems.call(this, 'values', endpoint, 'GET'); } - let projects = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET'); if (projects.values && Array.isArray(projects.values)) { projects = projects.values; @@ -130,6 +134,13 @@ export class Jira implements INodeType { value: projectId, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, @@ -165,6 +176,12 @@ export class Jira implements INodeType { } } + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, @@ -184,6 +201,13 @@ export class Jira implements INodeType { value: labelId, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, @@ -203,6 +227,13 @@ export class Jira implements INodeType { value: priorityId, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, @@ -241,6 +272,12 @@ export class Jira implements INodeType { } } + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, @@ -260,6 +297,13 @@ export class Jira implements INodeType { value: groupId, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, @@ -277,6 +321,13 @@ export class Jira implements INodeType { value: transition.id, }); } + + returnData.sort((a, b) => { + if (a.name < b.name) { return -1; } + if (a.name > b.name) { return 1; } + return 0; + }); + return returnData; }, }, diff --git a/packages/nodes-base/nodes/Line/GenericFunctions.ts b/packages/nodes-base/nodes/Line/GenericFunctions.ts new file mode 100644 index 0000000000..cc95fadd37 --- /dev/null +++ b/packages/nodes-base/nodes/Line/GenericFunctions.ts @@ -0,0 +1,50 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function lineApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise { // tslint:disable-line:no-any + + let options: OptionsWithUri = { + headers: { + 'Content-Type': 'application/json', + }, + method, + body, + qs, + uri: uri || ``, + json: true, + }; + options = Object.assign({}, options, option); + + try { + if (Object.keys(body).length === 0) { + delete options.body; + } + + //@ts-ignore + return await this.helpers.requestOAuth2.call(this, 'lineNotifyOAuth2Api', options, { tokenType: 'Bearer' }); + + } catch (error) { + + let errorMessage; + + if (error.response && error.response.body && error.response.body.message) { + + errorMessage = error.response.body.message; + + throw new Error(`Line error response [${error.statusCode}]: ${errorMessage}`); + } + throw error; + } +} diff --git a/packages/nodes-base/nodes/Line/Line.node.ts b/packages/nodes-base/nodes/Line/Line.node.ts new file mode 100644 index 0000000000..8c50baa354 --- /dev/null +++ b/packages/nodes-base/nodes/Line/Line.node.ts @@ -0,0 +1,144 @@ +import { + BINARY_ENCODING, + IExecuteFunctions, +} from 'n8n-core'; + +import { + IBinaryKeyData, + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + lineApiRequest, +} from './GenericFunctions'; + +import { + notificationFields, + notificationOperations, +} from './NotificationDescription'; + +export class Line implements INodeType { + description: INodeTypeDescription = { + displayName: 'Line', + name: 'line', + icon: 'file:line.png', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Line API.', + defaults: { + name: 'Line', + color: '#00b900', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'lineNotifyOAuth2Api', + required: true, + displayOptions: { + show: { + resource: [ + 'notification', + ], + }, + }, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Notification', + value: 'notification', + }, + ], + default: 'notification', + description: 'The resource to operate on.', + }, + ...notificationOperations, + ...notificationFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + for (let i = 0; i < length; i++) { + + if (resource === 'notification') { + //https://notify-bot.line.me/doc/en/ + if (operation === 'send') { + const message = this.getNodeParameter('message', i) as string; + + const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; + + const body: IDataObject = { + message, + }; + + Object.assign(body, additionalFields); + + if (body.hasOwnProperty('notificationDisabled')) { + body.notificationDisabled = (body.notificationDisabled) ? 'true' : 'false'; + } + + if (body.stickerUi) { + const sticker = (body.stickerUi as IDataObject).stickerValue as IDataObject; + if (sticker) { + body.stickerId = sticker.stickerId; + body.stickerPackageId = sticker.stickerPackageId; + } + delete body.stickerUi; + } + + if (body.imageUi) { + const image = (body.imageUi as IDataObject).imageValue as IDataObject; + + if (image && image.binaryData === true) { + if (items[i].binary === undefined) { + throw new Error('No binary data exists on item!'); + } + //@ts-ignore + if (items[i].binary[image.binaryProperty] === undefined) { + throw new Error(`No binary data property "${image.binaryProperty}" does not exists on item!`); + } + + const binaryData = (items[i].binary as IBinaryKeyData)[image.binaryProperty as string]; + + body.imageFile = { + value: Buffer.from(binaryData.data, BINARY_ENCODING), + options: { + filename: binaryData.fileName, + }, + }; + } else { + body.imageFullsize = image.imageFullsize; + body.imageThumbnail = image.imageThumbnail; + } + delete body.imageUi; + } + responseData = await lineApiRequest.call(this, 'POST', '', {}, {}, 'https://notify-api.line.me/api/notify', { formData: body }); + } + } + } + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + + } else if (responseData !== undefined) { + returnData.push(responseData as IDataObject); + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Line/NotificationDescription.ts b/packages/nodes-base/nodes/Line/NotificationDescription.ts new file mode 100644 index 0000000000..f26dd68068 --- /dev/null +++ b/packages/nodes-base/nodes/Line/NotificationDescription.ts @@ -0,0 +1,176 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const notificationOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'notification', + ], + }, + }, + options: [ + { + name: 'Send', + value: 'send', + description: 'Sends notifications to users or groups', + }, + ], + default: 'send', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const notificationFields = [ + + /* -------------------------------------------------------------------------- */ + /* notification:send */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Message', + name: 'message', + required: true, + type: 'string', + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'notification', + ], + }, + }, + default: '', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + operation: [ + 'send', + ], + resource: [ + 'notification', + ], + }, + }, + options: [ + { + displayName: 'Image', + name: 'imageUi', + placeholder: 'Add Image', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'imageValue', + displayName: 'image', + values: [ + { + displayName: 'Binary Data', + name: 'binaryData', + type: 'boolean', + default: false, + }, + { + displayName: 'Image Full Size', + name: 'imageFullsize', + type: 'string', + default: '', + displayOptions: { + show: { + binaryData: [ + false, + ], + }, + }, + description: 'HTTP/HTTPS URL. Maximum size of 2048×2048px JPEG', + }, + { + displayName: 'Image Thumbnail', + name: 'imageThumbnail', + type: 'string', + displayOptions: { + show: { + binaryData: [ + false, + ], + }, + }, + default: '', + description: 'HTTP/HTTPS URL. Maximum size of 240×240px JPEG', + }, + { + displayName: 'Binary Property', + name: 'binaryProperty', + type: 'string', + displayOptions: { + show: { + binaryData: [ + true, + ], + }, + }, + default: 'data', + description: `Name of the property that holds the binary data.
`, + }, + ], + }, + ], + }, + { + displayName: 'Notification Disabled', + name: 'notificationDisabled', + type: 'boolean', + default: false, + description: `true: The user doesn't receive a push notification when the message is sent.
+ false: The user receives a push notification when the message is sent`, + }, + { + displayName: 'Sticker', + name: 'stickerUi', + placeholder: 'Add Sticker', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + name: 'stickerValue', + displayName: 'Sticker', + values: [ + { + displayName: 'Sticker ID', + name: 'stickerId', + type: 'number', + default: '', + description: 'Sticker ID', + }, + { + displayName: 'Sticker Package ID', + name: 'stickerPackageId', + type: 'number', + default: '', + description: 'Package ID', + }, + ], + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Line/line.png b/packages/nodes-base/nodes/Line/line.png new file mode 100644 index 0000000000..da40b65f5a Binary files /dev/null and b/packages/nodes-base/nodes/Line/line.png differ diff --git a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts index 46128fa895..39516f94f1 100644 --- a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts @@ -217,6 +217,9 @@ export class MicrosoftSql implements INodeType { user: credentials.user as string, password: credentials.password as string, domain: credentials.domain ? (credentials.domain as string) : undefined, + options: { + encrypt: credentials.tls as boolean, + }, }; const pool = new mssql.ConnectionPool(config); diff --git a/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts b/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts index e212fb0fed..eb1b10f747 100644 --- a/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts +++ b/packages/nodes-base/nodes/Pipedrive/PipedriveTrigger.node.ts @@ -54,13 +54,31 @@ export class PipedriveTrigger implements INodeType { { name: 'pipedriveApi', required: true, + displayOptions: { + show: { + authentication: [ + 'apiToken', + ], + }, + }, + }, + { + name: 'pipedriveOAuth2Api', + required: true, + displayOptions: { + show: { + authentication: [ + 'oAuth2', + ], + }, + }, }, { name: 'httpBasicAuth', required: true, displayOptions: { show: { - authentication: [ + incomingAuthentication: [ 'basicAuth', ], }, @@ -80,6 +98,23 @@ export class PipedriveTrigger implements INodeType { displayName: 'Authentication', name: 'authentication', type: 'options', + options: [ + { + name: 'API Token', + value: 'apiToken', + }, + { + name: 'OAuth2', + value: 'oAuth2', + }, + ], + default: 'apiToken', + description: 'Method of authentication.', + }, + { + displayName: 'Incoming Authentication', + name: 'incomingAuthentication', + type: 'options', options: [ { name: 'Basic Auth', @@ -91,7 +126,7 @@ export class PipedriveTrigger implements INodeType { }, ], default: 'none', - description: 'If authentication should be activated for the webhook (makes it more scure).', + description: 'If authentication should be activated for the webhook (makes it more secure).', }, { displayName: 'Action', @@ -218,7 +253,7 @@ export class PipedriveTrigger implements INodeType { }, async create(this: IHookFunctions): Promise { const webhookUrl = this.getNodeWebhookUrl('default'); - const authentication = this.getNodeParameter('authentication', 0) as string; + const incomingAuthentication = this.getNodeParameter('incomingAuthentication', 0) as string; const eventAction = this.getNodeParameter('action') as string; const eventObject = this.getNodeParameter('object') as string; @@ -232,7 +267,7 @@ export class PipedriveTrigger implements INodeType { http_auth_password: undefined as string | undefined, }; - if (authentication === 'basicAuth') { + if (incomingAuthentication === 'basicAuth') { const httpBasicAuth = this.getCredentials('httpBasicAuth'); if (httpBasicAuth === undefined || !httpBasicAuth.user || !httpBasicAuth.password) { @@ -285,9 +320,9 @@ export class PipedriveTrigger implements INodeType { const resp = this.getResponseObject(); const realm = 'Webhook'; - const authentication = this.getNodeParameter('authentication', 0) as string; + const incomingAuthentication = this.getNodeParameter('incomingAuthentication', 0) as string; - if (authentication === 'basicAuth') { + if (incomingAuthentication === 'basicAuth') { // Basic authorization is needed to call webhook const httpBasicAuth = this.getCredentials('httpBasicAuth'); diff --git a/packages/nodes-base/nodes/Segment/IdentifyDescription.ts b/packages/nodes-base/nodes/Segment/IdentifyDescription.ts index 44b2c60b48..72b78fef35 100644 --- a/packages/nodes-base/nodes/Segment/IdentifyDescription.ts +++ b/packages/nodes-base/nodes/Segment/IdentifyDescription.ts @@ -262,6 +262,38 @@ export const identifyFields = [ }, ], }, + { + displayName: 'Custom Traits', + name: 'customTraitsUi', + placeholder: 'Add Custom Trait', + type: 'fixedCollection', + default: '', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customTraitValues', + displayName: 'Custom Traits', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: '', + }, + ], + }, + ], + }, ], }, ], diff --git a/packages/nodes-base/nodes/Segment/Segment.node.ts b/packages/nodes-base/nodes/Segment/Segment.node.ts index b4ad23e9c1..954193969e 100644 --- a/packages/nodes-base/nodes/Segment/Segment.node.ts +++ b/packages/nodes-base/nodes/Segment/Segment.node.ts @@ -38,6 +38,7 @@ import { } from './TrackInterface'; import * as uuid from 'uuid/v4'; +import { customerFields } from '../CustomerIo/CustomerDescription'; export class Segment implements INodeType { description: INodeTypeDescription = { @@ -170,6 +171,7 @@ export class Segment implements INodeType { if (traits.id) { body.traits!.id = traits.id as string; } + if (traits.company) { const company = (traits.company as IDataObject).companyUi as IDataObject; if (company) { @@ -384,6 +386,14 @@ export class Segment implements INodeType { if (traits.id) { body.traits!.id = traits.id as string; } + if (traits.customTraitsUi) { + const customTraits = (traits.customTraitsUi as IDataObject).customTraitValues as IDataObject[]; + if (customTraits && customTraits.length !== 0) { + for (const customTrait of customTraits) { + body.traits![customTrait.key as string] = customTrait.value; + } + } + } if (traits.company) { const company = (traits.company as IDataObject).companyUi as IDataObject; if (company) { @@ -531,6 +541,17 @@ export class Segment implements INodeType { body.integrations!.salesforce = integrations.salesforce as boolean; } } + + if (Object.keys(traits.company as IDataObject).length === 0) { + //@ts-ignore + delete body.traits.company; + } + + if (Object.keys(traits.address as IDataObject).length === 0) { + //@ts-ignore + delete body.traits.address; + } + responseData = await segmentApiRequest.call(this, 'POST', '/identify', body); } } @@ -602,6 +623,14 @@ export class Segment implements INodeType { if (traits.id) { body.traits!.id = traits.id as string; } + if (traits.customTraitsUi) { + const customTraits = (traits.customTraitsUi as IDataObject).customTraitValues as IDataObject[]; + if (customTraits && customTraits.length !== 0) { + for (const customTrait of customTraits) { + body.traits![customTrait.key as string] = customTrait.value; + } + } + } if (traits.company) { const company = (traits.company as IDataObject).companyUi as IDataObject; if (company) { @@ -760,6 +789,17 @@ export class Segment implements INodeType { body.properties!.value = properties.value as string; } } + + if (Object.keys(traits.company as IDataObject).length === 0) { + //@ts-ignore + delete body.traits.company; + } + + if (Object.keys(traits.address as IDataObject).length === 0) { + //@ts-ignore + delete body.traits.address; + } + responseData = await segmentApiRequest.call(this, 'POST', '/track', body); } //https://segment.com/docs/connections/sources/catalog/libraries/server/http-api/#page diff --git a/packages/nodes-base/nodes/Segment/TrackDescription.ts b/packages/nodes-base/nodes/Segment/TrackDescription.ts index f32b03278c..1e49c5e579 100644 --- a/packages/nodes-base/nodes/Segment/TrackDescription.ts +++ b/packages/nodes-base/nodes/Segment/TrackDescription.ts @@ -285,6 +285,38 @@ export const trackFields = [ }, ], }, + { + displayName: 'Custom Traits', + name: 'customTraitsUi', + placeholder: 'Add Custom Trait', + type: 'fixedCollection', + default: '', + typeOptions: { + multipleValues: true, + }, + options: [ + { + name: 'customTraitValues', + displayName: 'Custom Traits', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'string', + default: '', + description: '', + }, + { + displayName: 'Value', + name: 'value', + type: 'string', + default: '', + description: '', + }, + ], + }, + ], + }, ], }, ], diff --git a/packages/nodes-base/nodes/Shopify/GenericFunctions.ts b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts index e808846a25..6a7205e860 100644 --- a/packages/nodes-base/nodes/Shopify/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Shopify/GenericFunctions.ts @@ -24,7 +24,7 @@ export async function shopifyApiRequest(this: IHookFunctions | IExecuteFunctions throw new Error('No credentials got returned!'); } const headerWithAuthentication = Object.assign({}, - { Authorization: ` Basic ${Buffer.from(`${credentials.apiKey}:${credentials.password}`).toString(BINARY_ENCODING)}` }); + { Authorization: `Basic ${Buffer.from(`${credentials.apiKey}:${credentials.password}`).toString(BINARY_ENCODING)}` }); const options: OptionsWithUri = { headers: headerWithAuthentication, @@ -47,6 +47,7 @@ export async function shopifyApiRequest(this: IHookFunctions | IExecuteFunctions try { return await this.helpers.request!(options); } catch (error) { + console.log(error.response.body); if (error.response.body && error.response.body.errors) { let message = ''; if (typeof error.response.body.errors === 'object') { diff --git a/packages/nodes-base/nodes/Shopify/ShopifyTrigger.node.ts b/packages/nodes-base/nodes/Shopify/ShopifyTrigger.node.ts index f0d627d500..e6c49d4cf0 100644 --- a/packages/nodes-base/nodes/Shopify/ShopifyTrigger.node.ts +++ b/packages/nodes-base/nodes/Shopify/ShopifyTrigger.node.ts @@ -25,6 +25,7 @@ export class ShopifyTrigger implements INodeType { icon: 'file:shopify.png', group: ['trigger'], version: 1, + subtitle: '={{$parameter["event"]}}', description: 'Handle Shopify events via webhooks', defaults: { name: 'Shopify Trigger', @@ -55,271 +56,268 @@ export class ShopifyTrigger implements INodeType { options: [ { - name: 'App uninstalled', + name: 'App Uninstalled', value: 'app/uninstalled', }, { - name: 'Carts create', + name: 'Cart Created', value: 'carts/create', }, { - name: 'Carts update', + name: 'Cart Updated', value: 'carts/update', }, { - name: 'Checkouts create', + name: 'Checkout Created', value: 'checkouts/create', }, { - name: 'Checkouts delete', + name: 'Checkout Delete', value: 'checkouts/delete', }, { - name: 'Checkouts update', + name: 'Checkout Update', value: 'checkouts/update', }, { - name: 'Collection listings add', + name: 'Collection Listings Added', value: 'collection_listings/add', }, { - name: 'Collection listings remove', + name: 'Collection Listings Removed', value: 'collection_listings/remove', }, { - name: 'Collection listings update', + name: 'Collection Listings Updated', value: 'collection_listings/update', }, { - name: 'Collections create', + name: 'Collection Created', value: 'collections/create', }, { - name: 'Collections delete', + name: 'Collection Deleted', value: 'collections/delete', }, { - name: 'Collections update', + name: 'Collection Updated', value: 'collections/update', }, { - name: 'Customer groups create', + name: 'Customer Groups Created', value: 'customer_groups/create', }, { - name: 'Customer groups delete', + name: 'Customer Groups Deleted', value: 'customer_groups/delete', }, { - name: 'Customer groups update', + name: 'Customer Groups Updated', value: 'customer_groups/update', }, { - name: 'Customers create', + name: 'Customer Created', value: 'customers/create', }, { - name: 'Customers delete', + name: 'Customer Deleted', value: 'customers/delete', }, { - name: 'Customers disable', + name: 'Customer disabled', value: 'customers/disable', }, { - name: 'Customers enable', + name: 'Customer Enabled', value: 'customers/enable', }, { - name: 'Customers update', + name: 'Customer Updated', value: 'customers/update', }, { - name: 'Draft orders create', + name: 'Draft Orders Created', value: 'draft_orders/create', }, { - name: 'Draft orders delete', + name: 'Draft Orders Deleted', value: 'draft_orders/delete', }, { - name: 'Draft orders update', + name: 'Draft orders Updated', value: 'draft_orders/update', }, { - name: 'Fulfillment events create', + name: 'Fulfillment Events Created', value: 'fulfillment_events/create', }, { - name: 'Fulfillment events delete', + name: 'Fulfillment Events Deleted', value: 'fulfillment_events/delete', }, { - name: 'Fulfillments create', + name: 'Fulfillment created', value: 'fulfillments/create', }, { - name: 'Fulfillments update', + name: 'Fulfillment Updated', value: 'fulfillments/update', }, { - name: 'Inventory_items create', + name: 'Inventory Items Created', value: 'inventory_items/create', }, { - name: 'Inventory_items delete', + name: 'Inventory Items Deleted', value: 'inventory_items/delete', }, { - name: 'Inventory_items update', + name: 'Inventory Items Updated', value: 'inventory_items/update', }, { - name: 'Inventory_levels connect', + name: 'Inventory Levels Connected', value: 'inventory_levels/connect', }, { - name: 'Inventory_levels disconnect', + name: 'Inventory Levels Disconnected', value: 'inventory_levels/disconnect', }, { - name: 'Inventory_levels update', + name: 'Inventory Levels Updated', value: 'inventory_levels/update', }, { - name: 'Locales create', + name: 'Locale Created', value: 'locales/create', }, { - name: 'Locales update', + name: 'Locale Updated', value: 'locales/update', }, { - name: 'Locations create', + name: 'Location Created', value: 'locations/create', }, { - name: 'Locations delete', + name: 'Location Deleted', value: 'locations/delete', }, { - name: 'Locations update', + name: 'Location Updated', value: 'locations/update', }, { - name: 'Order transactions create', + name: 'Order transactions Created', value: 'order_transactions/create', }, { - name: 'Orders cancelled', + name: 'Order cancelled', value: 'orders/cancelled', }, { - name: 'Orders create', + name: 'Order Created', value: 'orders/create', }, { - name: 'Orders delete', + name: 'Orders Deleted', value: 'orders/delete', }, { - name: 'Orders fulfilled', + name: 'Order Fulfilled', value: 'orders/fulfilled', }, { - name: 'Orders paid', + name: 'Order Paid', value: 'orders/paid', }, { - name: 'Orders partially fulfilled', + name: 'Order Partially Fulfilled', value: 'orders/partially_fulfilled', }, { - name: 'Orders updated', + name: 'Order Updated', value: 'orders/updated', }, { - name: 'Product listings add', + name: 'Product Listings Added', value: 'product_listings/add', }, { - name: 'Product listings remove', + name: 'Product Listings Removed', value: 'product_listings/remove', }, { - name: 'Product listings update', + name: 'Product Listings Updated', value: 'product_listings/update', }, { - name: 'Products create', + name: 'Product Created', value: 'products/create', }, { - name: 'Products delete', + name: 'Product Deleted', value: 'products/delete', }, { - name: 'Products update', + name: 'Product Updated', value: 'products/update', }, { - name: 'Refunds create', + name: 'Refund Created', value: 'refunds/create', }, { - name: 'Shop update', + name: 'Shop Updated', value: 'shop/update', }, { - name: 'Tender transactions create', + name: 'Tender Transactions Created', value: 'tender_transactions/create', }, { - name: 'Themes create', + name: 'Theme Created', value: 'themes/create', }, { - name: 'Themes delete', + name: 'Theme Deleted', value: 'themes/delete', }, { - name: 'Themes publish', + name: 'Theme Published', value: 'themes/publish', }, { - name: 'Themes update', + name: 'Theme Updated', value: 'themes/update', }, ], description: 'Event that triggers the webhook', }, ], - }; // @ts-ignore (because of request) webhookMethods = { default: { async checkExists(this: IHookFunctions): Promise { + const topic = this.getNodeParameter('topic') as string; const webhookData = this.getWorkflowStaticData('node'); - if (webhookData.webhookId === undefined) { - return false; - } - const endpoint = `/webhooks/${webhookData.webhookId}.json`; - try { - await shopifyApiRequest.call(this, 'GET', endpoint, {}); - } catch (e) { - if (e.statusCode === 404) { - delete webhookData.webhookId; - return false; + const webhookUrl = this.getNodeWebhookUrl('default'); + const endpoint = `/webhooks`; + + const { webhooks } = await shopifyApiRequest.call(this, 'GET', endpoint, {}, { topic }); + for (const webhook of webhooks) { + if (webhook.address === webhookUrl) { + webhookData.webhookId = webhook.id; + return true; } - throw e; } - return true; + return false; }, async create(this: IHookFunctions): Promise { - const credentials = this.getCredentials('shopifyApi'); const webhookUrl = this.getNodeWebhookUrl('default'); const topic = this.getNodeParameter('topic') as string; + const webhookData = this.getWorkflowStaticData('node'); const endpoint = `/webhooks.json`; const body = { webhook: { @@ -330,21 +328,15 @@ export class ShopifyTrigger implements INodeType { }; let responseData; - try { - responseData = await shopifyApiRequest.call(this, 'POST', endpoint, body); - } catch (error) { - return false; - } + + responseData = await shopifyApiRequest.call(this, 'POST', endpoint, body); if (responseData.webhook === undefined || responseData.webhook.id === undefined) { // Required data is missing so was not successful return false; } - const webhookData = this.getWorkflowStaticData('node'); webhookData.webhookId = responseData.webhook.id as string; - webhookData.sharedSecret = credentials!.sharedSecret as string; - webhookData.topic = topic as string; return true; }, async delete(this: IHookFunctions): Promise { @@ -357,8 +349,6 @@ export class ShopifyTrigger implements INodeType { return false; } delete webhookData.webhookId; - delete webhookData.sharedSecret; - delete webhookData.topic; } return true; }, @@ -368,17 +358,18 @@ export class ShopifyTrigger implements INodeType { async webhook(this: IWebhookFunctions): Promise { const headerData = this.getHeaderData() as IDataObject; const req = this.getRequestObject(); - const webhookData = this.getWorkflowStaticData('node') as IDataObject; + const credentials = this.getCredentials('shopifyApi') as IDataObject; + const topic = this.getNodeParameter('topic') as string; if (headerData['x-shopify-topic'] !== undefined && headerData['x-shopify-hmac-sha256'] !== undefined && headerData['x-shopify-shop-domain'] !== undefined && headerData['x-shopify-api-version'] !== undefined) { // @ts-ignore - const computedSignature = createHmac('sha256', webhookData.sharedSecret as string).update(req.rawBody).digest('base64'); + const computedSignature = createHmac('sha256', credentials.sharedSecret as string).update(req.rawBody).digest('base64'); if (headerData['x-shopify-hmac-sha256'] !== computedSignature) { return {}; } - if (webhookData.topic !== headerData['x-shopify-topic']) { + if (topic !== headerData['x-shopify-topic']) { return {}; } } else { diff --git a/packages/nodes-base/nodes/Strapi/EntryDescription.ts b/packages/nodes-base/nodes/Strapi/EntryDescription.ts new file mode 100644 index 0000000000..7305aa6ce7 --- /dev/null +++ b/packages/nodes-base/nodes/Strapi/EntryDescription.ts @@ -0,0 +1,350 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const entryOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'entry', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create an entry', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an entry', + }, + { + name: 'Get', + value: 'get', + description: 'Get an entry', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all entries', + }, + { + name: 'Update', + value: 'update', + description: 'Update an entry', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const entryFields = [ + /* -------------------------------------------------------------------------- */ + /* entry:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Content Type', + name: 'contentType', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Name of the content type.', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + placeholder: 'id,name,description', + description: 'Comma separated list of the properties which should used as columns for the new rows.', + }, + + /* -------------------------------------------------------------------------- */ + /* entry:delete */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Content Type', + name: 'contentType', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'Name of the content type.', + }, + { + displayName: 'Entry ID', + name: 'entryId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'delete', + ], + }, + }, + description: 'The ID of the entry to delete.', + }, + + /* -------------------------------------------------------------------------- */ + /* entry:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Content Type', + name: 'contentType', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'get', + ], + }, + }, + description: 'Name of the content type.', + }, + { + displayName: 'Entry ID', + name: 'entryId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'get', + ], + }, + }, + description: 'The ID of the entry to get.', + }, + + /* -------------------------------------------------------------------------- */ + /* entry:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Content Type', + name: 'contentType', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'getAll', + ], + }, + }, + description: 'Name of the content type', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'Returns a list of your user contacts.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'getAll', + ], + }, + }, + options: [ + { + displayName: 'Publication State', + name: 'publicationState', + type: 'options', + options: [ + { + name: 'Live', + value: 'live', + }, + { + name: 'Preview', + value: 'preview', + }, + ], + default: '', + description: 'Only select entries matching the publication state provided.', + }, + { + displayName: 'Sort Fields', + name: 'sort', + type: 'string', + typeOptions: { + multipleValues: true, + multipleValueButtonText: 'Add Sort Field', + }, + default: '', + placeholder: 'name:asc', + description: `Name of the fields to sort the data by. By default will be sorted ascendingly.
+ To modify that behavior, you have to add the sort direction after the name of sort field preceded by a colon. + For example: name:asc`, + }, + { + displayName: 'Where (JSON)', + name: 'where', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + default: '', + description: 'JSON query to filter the data. Info', + }, + ], + }, + + /* -------------------------------------------------------------------------- */ + /* entry:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Content Type', + name: 'contentType', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'update', + ], + }, + }, + description: 'Name of the content type.', + }, + { + displayName: 'Update Key', + name: 'updateKey', + type: 'string', + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'update', + ], + }, + }, + default: 'id', + required: true, + description: 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".', + }, + { + displayName: 'Columns', + name: 'columns', + type: 'string', + displayOptions: { + show: { + resource: [ + 'entry', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + placeholder: 'id,name,description', + description: 'Comma separated list of the properties which should used as columns for the new rows.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Strapi/GenericFunctions.ts b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts new file mode 100644 index 0000000000..fbd7c57ff0 --- /dev/null +++ b/packages/nodes-base/nodes/Strapi/GenericFunctions.ts @@ -0,0 +1,103 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, + ILoadOptionsFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +export async function strapiApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const credentials = this.getCredentials('strapiApi') as IDataObject; + + try { + const options: OptionsWithUri = { + headers: {}, + method, + body, + qs, + uri: uri || `${credentials.url}${resource}`, + json: true, + }; + if (Object.keys(headers).length !== 0) { + options.headers = Object.assign({}, options.headers, headers); + } + if (Object.keys(body).length === 0) { + delete options.body; + } + + //@ts-ignore + return await this.helpers?.request(options); + } catch (error) { + if (error.response && error.response.body && error.response.body.message) { + + let messages = error.response.body.message; + + if (Array.isArray(error.response.body.message)) { + messages = messages[0].messages.map((e: IDataObject) => e.message).join('|'); + } + // Try to return the error prettier + throw new Error( + `Strapi error response [${error.statusCode}]: ${messages}`, + ); + } + throw error; + } +} + +export async function getToken(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions | IWebhookFunctions): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('strapiApi') as IDataObject; + + const options: OptionsWithUri = { + headers: { + 'content-type': `application/json`, + }, + method: 'POST', + uri: `${credentials.url}/auth/local`, + body: { + identifier: credentials.email, + password: credentials.password, + }, + json: true, + }; + + return this.helpers.request!(options); +} + +export async function strapiApiRequestAllItems(this: IHookFunctions | ILoadOptionsFunctions | IExecuteFunctions, method: string, resource: string, body: any = {}, query: IDataObject = {}, headers: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + query._limit = 20; + + query._start = 0; + + do { + responseData = await strapiApiRequest.call(this, method, resource, body, query, undefined, headers); + query._start += query._limit; + returnData.push.apply(returnData, responseData); + } while ( + responseData.length !== 0 + ); + + return returnData; +} + +export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any + let result; + try { + result = JSON.parse(json!); + } catch (exception) { + result = undefined; + } + return result; +} diff --git a/packages/nodes-base/nodes/Strapi/Strapi.node.ts b/packages/nodes-base/nodes/Strapi/Strapi.node.ts new file mode 100644 index 0000000000..74849e6187 --- /dev/null +++ b/packages/nodes-base/nodes/Strapi/Strapi.node.ts @@ -0,0 +1,192 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + getToken, + strapiApiRequest, + strapiApiRequestAllItems, + validateJSON, +} from './GenericFunctions'; + +import { + entryFields, + entryOperations, +} from './EntryDescription'; + +export class Strapi implements INodeType { + description: INodeTypeDescription = { + displayName: 'Strapi', + name: 'strapi', + icon: 'file:strapi.svg', + group: ['input'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Consume Strapi API.', + defaults: { + name: 'Strapi', + color: '#725ed8', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'strapiApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Entry', + value: 'entry', + }, + ], + default: 'entry', + description: 'The resource to operate on.', + }, + ...entryOperations, + ...entryFields, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const length = (items.length as unknown) as number; + const qs: IDataObject = {}; + const headers: IDataObject = {}; + let responseData; + const resource = this.getNodeParameter('resource', 0) as string; + const operation = this.getNodeParameter('operation', 0) as string; + + const { jwt } = await getToken.call(this); + + headers.Authorization = `Bearer ${jwt}`; + + if (resource === 'entry') { + if (operation === 'create') { + for (let i = 0; i < length; i++) { + + const body: IDataObject = {}; + + const contentType = this.getNodeParameter('contentType', i) as string; + + const columns = this.getNodeParameter('columns', i) as string; + + const columnList = columns.split(',').map(column => column.trim()); + + for (const key of Object.keys(items[i].json)) { + if (columnList.includes(key)) { + body[key] = items[i].json[key]; + } + } + responseData = await strapiApiRequest.call(this, 'POST', `/${contentType}`, body, qs, undefined, headers); + + returnData.push(responseData); + } + } + + if (operation === 'delete') { + for (let i = 0; i < length; i++) { + const contentType = this.getNodeParameter('contentType', i) as string; + + const entryId = this.getNodeParameter('entryId', i) as string; + + responseData = await strapiApiRequest.call(this, 'DELETE', `/${contentType}/${entryId}`, {}, qs, undefined, headers); + + returnData.push(responseData); + } + } + + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { + + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + + const contentType = this.getNodeParameter('contentType', i) as string; + + const options = this.getNodeParameter('options', i) as IDataObject; + + if (options.sort && (options.sort as string[]).length !== 0) { + const sortFields = options.sort as string[]; + qs._sort = sortFields.join(','); + } + + if (options.where) { + const query = validateJSON(options.where as string); + if (query !== undefined) { + qs._where = query; + } else { + throw new Error('Query must be a valid JSON'); + } + } + + if (options.publicationState) { + qs._publicationState = options.publicationState as string; + } + + if (returnAll) { + responseData = await strapiApiRequestAllItems.call(this, 'GET', `/${contentType}`, {}, qs, headers); + } else { + qs._limit = this.getNodeParameter('limit', i) as number; + + responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}`, {}, qs, undefined, headers); + } + returnData.push.apply(returnData, responseData); + } + } + + if (operation === 'get') { + for (let i = 0; i < length; i++) { + + const contentType = this.getNodeParameter('contentType', i) as string; + + const entryId = this.getNodeParameter('entryId', i) as string; + + responseData = await strapiApiRequest.call(this, 'GET', `/${contentType}/${entryId}`, {}, qs, undefined, headers); + + returnData.push(responseData); + } + } + + if (operation === 'update') { + for (let i = 0; i < length; i++) { + + const body: IDataObject = {}; + + const contentType = this.getNodeParameter('contentType', i) as string; + + const columns = this.getNodeParameter('columns', i) as string; + + const updateKey = this.getNodeParameter('updateKey', i) as string; + + const columnList = columns.split(',').map(column => column.trim()); + + const entryId = items[i].json[updateKey]; + + for (const key of Object.keys(items[i].json)) { + if (columnList.includes(key)) { + body[key] = items[i].json[key]; + } + } + responseData = await strapiApiRequest.call(this, 'PUT', `/${contentType}/${entryId}`, body, qs, undefined, headers); + + returnData.push(responseData); + } + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Strapi/strapi.svg b/packages/nodes-base/nodes/Strapi/strapi.svg new file mode 100644 index 0000000000..bf9f95847a --- /dev/null +++ b/packages/nodes-base/nodes/Strapi/strapi.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 5e7fd2b583..8a001362d9 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -1,6 +1,6 @@ { "name": "n8n-nodes-base", - "version": "0.87.0", + "version": "0.88.0", "description": "Base nodes of n8n", "license": "SEE LICENSE IN LICENSE.md", "homepage": "https://n8n.io", @@ -71,6 +71,8 @@ "dist/credentials/FileMaker.credentials.js", "dist/credentials/FlowApi.credentials.js", "dist/credentials/Ftp.credentials.js", + "dist/credentials/GetResponseApi.credentials.js", + "dist/credentials/GetResponseOAuth2Api.credentials.js", "dist/credentials/GithubApi.credentials.js", "dist/credentials/GithubOAuth2Api.credentials.js", "dist/credentials/GitlabApi.credentials.js", @@ -86,6 +88,7 @@ "dist/credentials/GSuiteAdminOAuth2Api.credentials.js", "dist/credentials/GoogleTasksOAuth2Api.credentials.js", "dist/credentials/GoogleTranslateOAuth2Api.credentials.js", + "dist/credentials/GotifyApi.credentials.js", "dist/credentials/YouTubeOAuth2Api.credentials.js", "dist/credentials/GumroadApi.credentials.js", "dist/credentials/HarvestApi.credentials.js", @@ -105,6 +108,7 @@ "dist/credentials/JotFormApi.credentials.js", "dist/credentials/Kafka.credentials.js", "dist/credentials/KeapOAuth2Api.credentials.js", + "dist/credentials/LineNotifyOAuth2Api.credentials.js", "dist/credentials/LinkedInOAuth2Api.credentials.js", "dist/credentials/MailerLiteApi.credentials.js", "dist/credentials/MailchimpApi.credentials.js", @@ -176,6 +180,7 @@ "dist/credentials/SpotifyOAuth2Api.credentials.js", "dist/credentials/StoryblokContentApi.credentials.js", "dist/credentials/StoryblokManagementApi.credentials.js", + "dist/credentials/StrapiApi.credentials.js", "dist/credentials/SurveyMonkeyApi.credentials.js", "dist/credentials/SurveyMonkeyOAuth2Api.credentials.js", "dist/credentials/TaigaCloudApi.credentials.js", @@ -277,6 +282,7 @@ "dist/nodes/Flow/FlowTrigger.node.js", "dist/nodes/Function.node.js", "dist/nodes/FunctionItem.node.js", + "dist/nodes/GetResponse/GetResponse.node.js", "dist/nodes/Github/Github.node.js", "dist/nodes/Github/GithubTrigger.node.js", "dist/nodes/Gitlab/Gitlab.node.js", @@ -291,6 +297,7 @@ "dist/nodes/Google/Task/GoogleTasks.node.js", "dist/nodes/Google/Translate/GoogleTranslate.node.js", "dist/nodes/Google/YouTube/YouTube.node.js", + "dist/nodes/Gotify/Gotify.node.js", "dist/nodes/GraphQL/GraphQL.node.js", "dist/nodes/Gumroad/GumroadTrigger.node.js", "dist/nodes/HackerNews/HackerNews.node.js", @@ -313,6 +320,7 @@ "dist/nodes/Kafka/Kafka.node.js", "dist/nodes/Keap/Keap.node.js", "dist/nodes/Keap/KeapTrigger.node.js", + "dist/nodes/Line/Line.node.js", "dist/nodes/LinkedIn/LinkedIn.node.js", "dist/nodes/MailerLite/MailerLite.node.js", "dist/nodes/MailerLite/MailerLiteTrigger.node.js", @@ -381,6 +389,7 @@ "dist/nodes/SseTrigger.node.js", "dist/nodes/Start.node.js", "dist/nodes/Storyblok/Storyblok.node.js", + "dist/nodes/Strapi/Strapi.node.js", "dist/nodes/Strava/Strava.node.js", "dist/nodes/Strava/StravaTrigger.node.js", "dist/nodes/Stripe/StripeTrigger.node.js", @@ -449,7 +458,7 @@ "@types/xml2js": "^0.4.3", "gulp": "^4.0.0", "jest": "^26.4.2", - "n8n-workflow": "~0.43.0", + "n8n-workflow": "~0.44.0", "ts-jest": "^26.3.0", "tslint": "^6.1.2", "typescript": "~3.9.7" @@ -479,7 +488,7 @@ "mqtt": "^4.2.0", "mssql": "^6.2.0", "mysql2": "~2.1.0", - "n8n-core": "~0.50.0", + "n8n-core": "~0.51.0", "nodemailer": "^6.4.6", "pdf-parse": "^1.1.1", "pg": "^8.3.0", diff --git a/packages/workflow/package.json b/packages/workflow/package.json index bcaed2e5d4..cfe35eac2a 100644 --- a/packages/workflow/package.json +++ b/packages/workflow/package.json @@ -1,6 +1,6 @@ { "name": "n8n-workflow", - "version": "0.43.0", + "version": "0.44.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 bf0fc63e13..33524176bb 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -408,6 +408,7 @@ export interface INodePropertyTypeOptions { numberStepSize?: number; // Supported by: number password?: boolean; // Supported by: string rows?: number; // Supported by: string + showAlpha?: boolean; // Supported by: color [key: string]: boolean | number | string | EditorTypes | undefined | string[]; } @@ -535,6 +536,7 @@ export interface INodeTypeDescription { version: number; description: string; defaults: INodeParameters; + documentationUrl?: string; inputs: string[]; inputNames?: string[]; outputs: string[]; @@ -714,7 +716,7 @@ export interface IWorkflowExecuteHooks { nodeExecuteAfter?: Array<((nodeName: string, data: ITaskData) => Promise)>; nodeExecuteBefore?: Array<((nodeName: string) => Promise)>; workflowExecuteAfter?: Array<((data: IRun, newStaticData: IDataObject) => Promise)>; - workflowExecuteBefore?: Array<(() => Promise)>; + workflowExecuteBefore?: Array<((workflow: Workflow, data: IRunExecutionData) => Promise)>; } export interface IWorkflowExecuteAdditionalData { diff --git a/packages/workflow/src/WorkflowHooks.ts b/packages/workflow/src/WorkflowHooks.ts index 94a02abddc..92efb468a0 100644 --- a/packages/workflow/src/WorkflowHooks.ts +++ b/packages/workflow/src/WorkflowHooks.ts @@ -28,19 +28,9 @@ export class WorkflowHooks { async executeHookFunctions(hookName: string, parameters: any[]) { // tslint:disable-line:no-any if (this.hookFunctions[hookName] !== undefined && Array.isArray(this.hookFunctions[hookName])) { for (const hookFunction of this.hookFunctions[hookName]!) { - await hookFunction.apply(this, parameters) - .catch((error: Error) => { - // Catch all errors here because when "executeHook" gets called - // we have the most time no "await" and so the errors would so - // not be uncaught by anything. - - // TODO: Add proper logging - console.error(`There was a problem executing hook: "${hookName}"`); - console.error('Parameters:'); - console.error(parameters); - console.error('Error:'); - console.error(error); - }); + // TODO: As catch got removed we should make sure that we catch errors + // where hooks get called + await hookFunction.apply(this, parameters); } } }