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",
+}