From 649dafba47c8cc248b76bd2049ceb5c426c58e37 Mon Sep 17 00:00:00 2001 From: melohagan <101575380+melohagan@users.noreply.github.com> Date: Thu, 15 Feb 2024 13:05:03 +0000 Subject: [PATCH] n8n automation action integration (#12992) * Add n8n automation action * Add authorization header support * add unit tests * Replace test.com with example.com * Add HttpMethod enum to types * fix unit test * Add required field label asterisk --- packages/builder/assets/n8n_square.png | Bin 0 -> 12155 bytes .../FlowChart/ActionModal.svelte | 6 +- .../FlowChart/ExternalActions.js | 2 + .../SetupPanel/AutomationBlockSetup.svelte | 8 +- .../src/constants/backend/automations.js | 1 + packages/server/src/automations/actions.ts | 3 + packages/server/src/automations/steps/make.ts | 22 +-- packages/server/src/automations/steps/n8n.ts | 125 ++++++++++++++++++ .../server/src/automations/steps/zapier.ts | 20 --- .../server/src/automations/tests/n8n.spec.ts | 68 ++++++++++ packages/server/src/integrations/rest.ts | 23 ++-- .../types/src/documents/app/automation.ts | 1 + packages/types/src/documents/app/query.ts | 9 ++ 13 files changed, 232 insertions(+), 56 deletions(-) create mode 100644 packages/builder/assets/n8n_square.png create mode 100644 packages/server/src/automations/steps/n8n.ts create mode 100644 packages/server/src/automations/tests/n8n.spec.ts diff --git a/packages/builder/assets/n8n_square.png b/packages/builder/assets/n8n_square.png new file mode 100644 index 0000000000000000000000000000000000000000..23b75ee68842219fd2e11989be8067ac4aeee507 GIT binary patch literal 12155 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 literal 0 HcmV?d00001 diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte index a8711d220b..72cedb2b21 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte @@ -128,10 +128,10 @@ >
zapier diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ExternalActions.js b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ExternalActions.js index c6f8d25640..d5d382485c 100644 --- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ExternalActions.js +++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ExternalActions.js @@ -1,5 +1,6 @@ import DiscordLogo from "assets/discord.svg" import ZapierLogo from "assets/zapier.png" +import n8nLogo from "assets/n8n_square.png" import MakeLogo from "assets/make.svg" import SlackLogo from "assets/slack.svg" @@ -8,4 +9,5 @@ export const externalActions = { discord: { name: "discord", icon: DiscordLogo }, slack: { name: "slack", icon: SlackLogo }, integromat: { name: "integromat", icon: MakeLogo }, + n8n: { name: "n8n", icon: n8nLogo }, } diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 7ba1c8a4b1..707317f9e6 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -79,6 +79,7 @@ disableWrapping: true, }) $: editingJs = codeMode === EditorModes.JS + $: requiredProperties = block.schema.inputs.required || [] $: stepCompletions = codeMode === EditorModes.Handlebars @@ -359,6 +360,11 @@ ) } + function getFieldLabel(key, value) { + const requiredSuffix = requiredProperties.includes(key) ? "*" : "" + return `${value.title || (key === "row" ? "Table" : key)} ${requiredSuffix}` + } + onMount(async () => { try { await environment.loadVariables() @@ -376,7 +382,7 @@ {getFieldLabel(key, value)} {/if}
diff --git a/packages/builder/src/constants/backend/automations.js b/packages/builder/src/constants/backend/automations.js index 6981418fa7..7c3e17e225 100644 --- a/packages/builder/src/constants/backend/automations.js +++ b/packages/builder/src/constants/backend/automations.js @@ -27,6 +27,7 @@ export const ActionStepID = { slack: "slack", zapier: "zapier", integromat: "integromat", + n8n: "n8n", } export const Features = { diff --git a/packages/server/src/automations/actions.ts b/packages/server/src/automations/actions.ts index ac8a340e82..eee8ab4a7b 100644 --- a/packages/server/src/automations/actions.ts +++ b/packages/server/src/automations/actions.ts @@ -9,6 +9,7 @@ import * as serverLog from "./steps/serverLog" import * as discord from "./steps/discord" import * as slack from "./steps/slack" import * as zapier from "./steps/zapier" +import * as n8n from "./steps/n8n" import * as make from "./steps/make" import * as filter from "./steps/filter" import * as delay from "./steps/delay" @@ -48,6 +49,7 @@ const ACTION_IMPLS: Record< slack: slack.run, zapier: zapier.run, integromat: make.run, + n8n: n8n.run, } export const BUILTIN_ACTION_DEFINITIONS: Record = { @@ -70,6 +72,7 @@ export const BUILTIN_ACTION_DEFINITIONS: Record = slack: slack.definition, zapier: zapier.definition, integromat: make.definition, + n8n: n8n.definition, } // don't add the bash script/definitions unless in self host diff --git a/packages/server/src/automations/steps/make.ts b/packages/server/src/automations/steps/make.ts index 06e96907d9..555df8308a 100644 --- a/packages/server/src/automations/steps/make.ts +++ b/packages/server/src/automations/steps/make.ts @@ -34,28 +34,8 @@ export const definition: AutomationStepSchema = { type: AutomationIOType.JSON, title: "Payload", }, - value1: { - type: AutomationIOType.STRING, - title: "Input Value 1", - }, - value2: { - type: AutomationIOType.STRING, - title: "Input Value 2", - }, - value3: { - type: AutomationIOType.STRING, - title: "Input Value 3", - }, - value4: { - type: AutomationIOType.STRING, - title: "Input Value 4", - }, - value5: { - type: AutomationIOType.STRING, - title: "Input Value 5", - }, }, - required: ["url", "value1", "value2", "value3", "value4", "value5"], + required: ["url", "body"], }, outputs: { properties: { diff --git a/packages/server/src/automations/steps/n8n.ts b/packages/server/src/automations/steps/n8n.ts new file mode 100644 index 0000000000..c400c7037a --- /dev/null +++ b/packages/server/src/automations/steps/n8n.ts @@ -0,0 +1,125 @@ +import fetch, { HeadersInit } from "node-fetch" +import { getFetchResponse } from "./utils" +import { + AutomationActionStepId, + AutomationStepSchema, + AutomationStepInput, + AutomationStepType, + AutomationIOType, + AutomationFeature, + HttpMethod, +} from "@budibase/types" + +export const definition: AutomationStepSchema = { + name: "n8n Integration", + stepTitle: "n8n", + tagline: "Trigger an n8n workflow", + description: + "Performs a webhook call to n8n and gets the response (if configured)", + icon: "ri-shut-down-line", + stepId: AutomationActionStepId.n8n, + type: AutomationStepType.ACTION, + internal: false, + features: { + [AutomationFeature.LOOPING]: true, + }, + inputs: {}, + schema: { + inputs: { + properties: { + url: { + type: AutomationIOType.STRING, + title: "Webhook URL", + }, + method: { + type: AutomationIOType.STRING, + title: "Method", + enum: Object.values(HttpMethod), + }, + authorization: { + type: AutomationIOType.STRING, + title: "Authorization", + }, + body: { + type: AutomationIOType.JSON, + title: "Payload", + }, + }, + required: ["url", "method"], + }, + outputs: { + properties: { + success: { + type: AutomationIOType.BOOLEAN, + description: "Whether call was successful", + }, + httpStatus: { + type: AutomationIOType.NUMBER, + description: "The HTTP status code returned", + }, + response: { + type: AutomationIOType.OBJECT, + description: "The webhook response - this can have properties", + }, + }, + required: ["success", "response"], + }, + }, +} + +export async function run({ inputs }: AutomationStepInput) { + const { url, body, method, authorization } = inputs + + let payload = {} + try { + payload = body?.value ? JSON.parse(body?.value) : {} + } catch (err) { + return { + httpStatus: 400, + response: "Invalid payload JSON", + success: false, + } + } + + if (!url?.trim()?.length) { + return { + httpStatus: 400, + response: "Missing Webhook URL", + success: false, + } + } + let response + let request: { + method: string + headers: HeadersInit + body?: string + } = { + method: method || HttpMethod.GET, + headers: { + "Content-Type": "application/json", + Authorization: authorization, + }, + } + if (!["GET", "HEAD"].includes(request.method)) { + request.body = JSON.stringify({ + ...payload, + }) + } + + try { + response = await fetch(url, request) + } catch (err: any) { + return { + httpStatus: 400, + response: err.message, + success: false, + } + } + + const { status, message } = await getFetchResponse(response) + return { + httpStatus: status, + success: status === 200, + response: message, + } +} diff --git a/packages/server/src/automations/steps/zapier.ts b/packages/server/src/automations/steps/zapier.ts index eeff0c2c7d..e48d677228 100644 --- a/packages/server/src/automations/steps/zapier.ts +++ b/packages/server/src/automations/steps/zapier.ts @@ -32,26 +32,6 @@ export const definition: AutomationStepSchema = { type: AutomationIOType.JSON, title: "Payload", }, - value1: { - type: AutomationIOType.STRING, - title: "Payload Value 1", - }, - value2: { - type: AutomationIOType.STRING, - title: "Payload Value 2", - }, - value3: { - type: AutomationIOType.STRING, - title: "Payload Value 3", - }, - value4: { - type: AutomationIOType.STRING, - title: "Payload Value 4", - }, - value5: { - type: AutomationIOType.STRING, - title: "Payload Value 5", - }, }, required: ["url"], }, diff --git a/packages/server/src/automations/tests/n8n.spec.ts b/packages/server/src/automations/tests/n8n.spec.ts new file mode 100644 index 0000000000..d60a08b53b --- /dev/null +++ b/packages/server/src/automations/tests/n8n.spec.ts @@ -0,0 +1,68 @@ +import { getConfig, afterAll, runStep, actions } from "./utilities" + +describe("test the outgoing webhook action", () => { + let config = getConfig() + + beforeAll(async () => { + await config.init() + }) + + afterAll() + + it("should be able to run the action and default to 'get'", async () => { + const res = await runStep(actions.n8n.stepId, { + url: "http://www.example.com", + body: { + test: "IGNORE_ME", + }, + }) + expect(res.response.url).toEqual("http://www.example.com") + expect(res.response.method).toEqual("GET") + expect(res.response.body).toBeUndefined() + expect(res.success).toEqual(true) + }) + + it("should add the payload props when a JSON string is provided", async () => { + const payload = `{ "name": "Adam", "age": 9 }` + const res = await runStep(actions.n8n.stepId, { + body: { + value: payload, + }, + method: "POST", + url: "http://www.example.com", + }) + expect(res.response.url).toEqual("http://www.example.com") + expect(res.response.method).toEqual("POST") + expect(res.response.body).toEqual(`{"name":"Adam","age":9}`) + expect(res.success).toEqual(true) + }) + + it("should return a 400 if the JSON payload string is malformed", async () => { + const payload = `{ value1 1 }` + const res = await runStep(actions.n8n.stepId, { + value1: "ONE", + body: { + value: payload, + }, + method: "POST", + url: "http://www.example.com", + }) + expect(res.httpStatus).toEqual(400) + expect(res.response).toEqual("Invalid payload JSON") + expect(res.success).toEqual(false) + }) + + it("should not append the body if the method is HEAD", async () => { + const res = await runStep(actions.n8n.stepId, { + url: "http://www.example.com", + method: "HEAD", + body: { + test: "IGNORE_ME", + }, + }) + expect(res.response.url).toEqual("http://www.example.com") + expect(res.response.method).toEqual("HEAD") + expect(res.response.body).toBeUndefined() + expect(res.success).toEqual(true) + }) +}) diff --git a/packages/server/src/integrations/rest.ts b/packages/server/src/integrations/rest.ts index 9cb8f8e2c1..44c62f60b7 100644 --- a/packages/server/src/integrations/rest.ts +++ b/packages/server/src/integrations/rest.ts @@ -10,6 +10,7 @@ import { RestAuthType, RestBasicAuthConfig, RestBearerAuthConfig, + HttpMethod, } from "@budibase/types" import get from "lodash/get" import * as https from "https" @@ -86,30 +87,30 @@ const SCHEMA: Integration = { query: { create: { readable: true, - displayName: "POST", + displayName: HttpMethod.POST, type: QueryType.FIELDS, fields: coreFields, }, read: { - displayName: "GET", + displayName: HttpMethod.GET, readable: true, type: QueryType.FIELDS, fields: coreFields, }, update: { - displayName: "PUT", + displayName: HttpMethod.PUT, readable: true, type: QueryType.FIELDS, fields: coreFields, }, patch: { - displayName: "PATCH", + displayName: HttpMethod.PATCH, readable: true, type: QueryType.FIELDS, fields: coreFields, }, delete: { - displayName: "DELETE", + displayName: HttpMethod.DELETE, type: QueryType.FIELDS, fields: coreFields, }, @@ -358,7 +359,7 @@ class RestIntegration implements IntegrationBase { path = "", queryString = "", headers = {}, - method = "GET", + method = HttpMethod.GET, disabledHeaders, bodyType, requestBody, @@ -413,23 +414,23 @@ class RestIntegration implements IntegrationBase { } async create(opts: RestQuery) { - return this._req({ ...opts, method: "POST" }) + return this._req({ ...opts, method: HttpMethod.POST }) } async read(opts: RestQuery) { - return this._req({ ...opts, method: "GET" }) + return this._req({ ...opts, method: HttpMethod.GET }) } async update(opts: RestQuery) { - return this._req({ ...opts, method: "PUT" }) + return this._req({ ...opts, method: HttpMethod.PUT }) } async patch(opts: RestQuery) { - return this._req({ ...opts, method: "PATCH" }) + return this._req({ ...opts, method: HttpMethod.PATCH }) } async delete(opts: RestQuery) { - return this._req({ ...opts, method: "DELETE" }) + return this._req({ ...opts, method: HttpMethod.DELETE }) } } diff --git a/packages/types/src/documents/app/automation.ts b/packages/types/src/documents/app/automation.ts index 91a1a2ab68..fef72b78a9 100644 --- a/packages/types/src/documents/app/automation.ts +++ b/packages/types/src/documents/app/automation.ts @@ -69,6 +69,7 @@ export enum AutomationActionStepId { slack = "slack", zapier = "zapier", integromat = "integromat", + n8n = "n8n", } export interface EmailInvite { diff --git a/packages/types/src/documents/app/query.ts b/packages/types/src/documents/app/query.ts index 790c297813..81aa90b807 100644 --- a/packages/types/src/documents/app/query.ts +++ b/packages/types/src/documents/app/query.ts @@ -64,3 +64,12 @@ export interface ExecuteQueryRequest { export interface ExecuteQueryResponse { data: Row[] } + +export enum HttpMethod { + GET = "GET", + POST = "POST", + PATCH = "PATCH", + PUT = "PUT", + HEAD = "HEAD", + DELETE = "DELETE", +}