diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml
index e2fa9f2515..01ff12491e 100644
--- a/.github/workflows/pr-labeler.yml
+++ b/.github/workflows/pr-labeler.yml
@@ -21,6 +21,7 @@ jobs:
l_max_size: "1000"
fail_if_xl: "false"
files_to_ignore: "yarn.lock"
+ message_if_xl: ""
team-labeler:
runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 64492b97e4..26ad9f80c2 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,7 @@ Budibase is open-source - licensed as GPL v3. This should fill you with confiden
### Load data or start from scratch
-Budibase pulls data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
+Budibase pulls data from multiple sources, including MongoDB, CouchDB, PostgreSQL, MariaDB, MySQL, Airtable, S3, DynamoDB, or a REST API. And unlike other platforms, with Budibase you can start from scratch and create business apps with no data sources. [Request new datasources](https://github.com/Budibase/budibase/discussions?discussions_q=category%3AIdeas).
diff --git a/lerna.json b/lerna.json
index bb0302353e..9b2404edfd 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
- "version": "2.31.5",
+ "version": "2.31.6",
"npmClient": "yarn",
"packages": [
"packages/*",
diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts
index 1ade332f0a..b6233b24ad 100644
--- a/packages/server/src/api/routes/tests/automation.spec.ts
+++ b/packages/server/src/api/routes/tests/automation.spec.ts
@@ -15,6 +15,7 @@ import { Automation, FieldType, Table } from "@budibase/types"
import { mocks } from "@budibase/backend-core/tests"
import { FilterConditions } from "../../../automations/steps/filter"
import { removeDeprecated } from "../../../automations/utils"
+import { createAutomationBuilder } from "../../../automations/tests/utilities/AutomationTestBuilder"
const MAX_RETRIES = 4
let {
@@ -121,6 +122,104 @@ describe("/automations", () => {
expect(events.automation.stepCreated).toHaveBeenCalledTimes(2)
})
+ it("Should ensure you can't have a branch as not a last step", async () => {
+ const automation = createAutomationBuilder({
+ name: "String Equality Branching",
+ appId: config.getAppId(),
+ })
+ .appAction({ fields: { status: "active" } })
+ .branch({
+ activeBranch: {
+ steps: stepBuilder =>
+ stepBuilder.serverLog({ text: "Active user" }),
+ condition: {
+ equal: { "trigger.fields.status": "active" },
+ },
+ },
+ })
+ .serverLog({ text: "Inactive user" })
+ .build()
+
+ await config.api.automation.post(automation, {
+ status: 400,
+ body: {
+ message:
+ "Invalid body - Branch steps are only allowed as the last step",
+ },
+ })
+ })
+
+ it("Should check validation on an automation that has a branch step with no children", async () => {
+ const automation = createAutomationBuilder({
+ name: "String Equality Branching",
+ appId: config.getAppId(),
+ })
+ .appAction({ fields: { status: "active" } })
+ .branch({})
+ .serverLog({ text: "Inactive user" })
+ .build()
+
+ await config.api.automation.post(automation, {
+ status: 400,
+ body: {
+ message:
+ 'Invalid body - "definition.steps[0].inputs.branches" must contain at least 1 items',
+ },
+ })
+ })
+
+ it("Should check validation on a branch step with empty conditions", async () => {
+ const automation = createAutomationBuilder({
+ name: "String Equality Branching",
+ appId: config.getAppId(),
+ })
+ .appAction({ fields: { status: "active" } })
+ .branch({
+ activeBranch: {
+ steps: stepBuilder =>
+ stepBuilder.serverLog({ text: "Active user" }),
+ condition: {},
+ },
+ })
+ .build()
+
+ await config.api.automation.post(automation, {
+ status: 400,
+ body: {
+ message:
+ 'Invalid body - "definition.steps[0].inputs.branches[0].condition" must have at least 1 key',
+ },
+ })
+ })
+
+ it("Should check validation on an branch that has a condition that is not valid", async () => {
+ const automation = createAutomationBuilder({
+ name: "String Equality Branching",
+ appId: config.getAppId(),
+ })
+ .appAction({ fields: { status: "active" } })
+ .branch({
+ activeBranch: {
+ steps: stepBuilder =>
+ stepBuilder.serverLog({ text: "Active user" }),
+ condition: {
+ //@ts-ignore
+ INCORRECT: { "trigger.fields.status": "active" },
+ },
+ },
+ })
+ .serverLog({ text: "Inactive user" })
+ .build()
+
+ await config.api.automation.post(automation, {
+ status: 400,
+ body: {
+ message:
+ 'Invalid body - "definition.steps[0].inputs.branches[0].condition.INCORRECT" is not allowed',
+ },
+ })
+ })
+
it("should apply authorization to endpoint", async () => {
const automation = newAutomation()
await checkBuilderEndpoint({
diff --git a/packages/server/src/api/routes/utils/validators.ts b/packages/server/src/api/routes/utils/validators.ts
index 9aa112cf4d..5e2a585b4a 100644
--- a/packages/server/src/api/routes/utils/validators.ts
+++ b/packages/server/src/api/routes/utils/validators.ts
@@ -1,6 +1,9 @@
import { auth, permissions } from "@budibase/backend-core"
import { DataSourceOperation } from "../../../constants"
import {
+ AutomationActionStepId,
+ AutomationStep,
+ AutomationStepType,
EmptyFilterOption,
SearchFilters,
Table,
@@ -88,7 +91,8 @@ export function datasourceValidator() {
)
}
-function filterObject() {
+function filterObject(opts?: { unknown: boolean }) {
+ const { unknown = true } = opts || {}
const conditionalFilteringObject = () =>
Joi.object({
conditions: Joi.array().items(Joi.link("#schema")).required(),
@@ -115,7 +119,7 @@ function filterObject() {
fuzzyOr: Joi.forbidden(),
documentType: Joi.forbidden(),
}
- return Joi.object(filtersValidators).unknown(true).id("schema")
+ return Joi.object(filtersValidators).unknown(unknown).id("schema")
}
export function internalSearchValidator() {
@@ -259,6 +263,11 @@ export function screenValidator() {
}
function generateStepSchema(allowStepTypes: string[]) {
+ const branchSchema = Joi.object({
+ name: Joi.string().required(),
+ condition: filterObject({ unknown: false }).required().min(1),
+ })
+
return Joi.object({
stepId: Joi.string().required(),
id: Joi.string().required(),
@@ -267,11 +276,35 @@ function generateStepSchema(allowStepTypes: string[]) {
tagline: Joi.string().required(),
icon: Joi.string().required(),
params: Joi.object(),
+ inputs: Joi.when("stepId", {
+ is: AutomationActionStepId.BRANCH,
+ then: Joi.object({
+ branches: Joi.array().items(branchSchema).min(1).required(),
+ children: Joi.object()
+ .pattern(Joi.string(), Joi.array().items(Joi.link("#step")))
+ .required(),
+ }).required(),
+ otherwise: Joi.object(),
+ }),
+
args: Joi.object(),
type: Joi.string()
.required()
.valid(...allowStepTypes),
- }).unknown(true)
+ })
+ .unknown(true)
+ .id("step")
+}
+
+const validateStepsArray = (
+ steps: AutomationStep[],
+ helpers: Joi.CustomHelpers
+) => {
+ for (const step of steps.slice(0, -1)) {
+ if (step.stepId === AutomationActionStepId.BRANCH) {
+ return helpers.error("branchStepPosition")
+ }
+ }
}
export function automationValidator(existing = false) {
@@ -284,9 +317,20 @@ export function automationValidator(existing = false) {
definition: Joi.object({
steps: Joi.array()
.required()
- .items(generateStepSchema(["ACTION", "LOGIC"])),
- trigger: generateStepSchema(["TRIGGER"]).allow(null),
+ .items(
+ generateStepSchema([
+ AutomationStepType.ACTION,
+ AutomationStepType.LOGIC,
+ ])
+ )
+ .custom(validateStepsArray)
+ .messages({
+ branchStepPosition:
+ "Branch steps are only allowed as the last step",
+ }),
+ trigger: generateStepSchema([AutomationStepType.TRIGGER]).allow(null),
})
+
.required()
.unknown(true),
}).unknown(true)
diff --git a/packages/server/src/automations/tests/scenarios/scenarios.spec.ts b/packages/server/src/automations/tests/scenarios/scenarios.spec.ts
index a0dab7f177..7fe4776d54 100644
--- a/packages/server/src/automations/tests/scenarios/scenarios.spec.ts
+++ b/packages/server/src/automations/tests/scenarios/scenarios.spec.ts
@@ -63,8 +63,8 @@ describe("Automation Scenarios", () => {
},
})
.run()
-
- expect(results.steps[2].outputs.message).toContain("Branch 1.1")
+ expect(results.steps[3].outputs.status).toContain("branch1 branch taken")
+ expect(results.steps[4].outputs.message).toContain("Branch 1.1")
})
it("should execute correct branch based on string equality", async () => {
@@ -91,8 +91,10 @@ describe("Automation Scenarios", () => {
},
})
.run()
-
- expect(results.steps[0].outputs.message).toContain("Active user")
+ expect(results.steps[0].outputs.status).toContain(
+ "activeBranch branch taken"
+ )
+ expect(results.steps[1].outputs.message).toContain("Active user")
})
it("should handle multiple conditions with AND operator", async () => {
@@ -124,7 +126,7 @@ describe("Automation Scenarios", () => {
})
.run()
- expect(results.steps[0].outputs.message).toContain("Active admin user")
+ expect(results.steps[1].outputs.message).toContain("Active admin user")
})
it("should handle multiple conditions with OR operator", async () => {
@@ -162,7 +164,7 @@ describe("Automation Scenarios", () => {
})
.run()
- expect(results.steps[0].outputs.message).toContain("Special user")
+ expect(results.steps[1].outputs.message).toContain("Special user")
})
})
@@ -362,6 +364,32 @@ describe("Automation Scenarios", () => {
}
)
})
+
+ it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => {
+ const builder = createAutomationBuilder({
+ name: "Test Trigger with Loop and Create Row",
+ })
+
+ const results = await builder
+ .appAction({ fields: {} })
+ .loop({
+ option: LoopStepType.ARRAY,
+ binding: [1, 2, 3],
+ })
+ .serverLog({ text: "Message {{loop.currentItem}}" })
+ .serverLog({ text: "{{steps.1.iterations}}" })
+ .loop({
+ option: LoopStepType.ARRAY,
+ binding: [1, 2, 3],
+ })
+ .serverLog({ text: "{{loop.currentItem}}" })
+ .serverLog({ text: "{{steps.3.iterations}}" })
+ .run()
+
+ // We want to ensure that bindings are corr
+ expect(results.steps[1].outputs.message).toContain("- 3")
+ expect(results.steps[3].outputs.message).toContain("- 3")
+ })
})
describe("Row Automations", () => {
diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts
index 16cab73b75..f477efabe4 100644
--- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts
+++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts
@@ -179,7 +179,7 @@ class AutomationBuilder extends BaseStepBuilder {
private triggerOutputs: any
private triggerSet: boolean = false
- constructor(options: { name?: string } = {}) {
+ constructor(options: { name?: string; appId?: string } = {}) {
super()
this.automationConfig = {
name: options.name || `Test Automation ${uuidv4()}`,
@@ -188,7 +188,7 @@ class AutomationBuilder extends BaseStepBuilder {
trigger: {} as AutomationTrigger,
},
type: "automation",
- appId: setup.getConfig().getAppId(),
+ appId: options.appId ?? setup.getConfig().getAppId(),
}
this.config = setup.getConfig()
}
@@ -261,13 +261,14 @@ class AutomationBuilder extends BaseStepBuilder {
return this
}
- branch(branchConfig: BranchConfig): {
- run: () => Promise