Skip to content
55 changes: 45 additions & 10 deletions packages/server/src/automations/tests/branching.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AutomationStatus, EmptyFilterOption, Table } from "@budibase/types"
import { AutomationStatus, Table } from "@budibase/types"
import TestConfiguration from "../../tests/utilities/TestConfiguration"
import * as automation from "../index"
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
Expand Down Expand Up @@ -284,22 +284,57 @@ describe("Branching automations", () => {
expect(results.steps[2].outputs.message).toContain("Special user")
})

it("should not fail with empty conditions", async () => {
it("should execute ELSE branch when no other conditions match", async () => {
const results = await createAutomationBuilder(config)
.onAppAction()
.branch({
specialBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Hello!" }),
branch1: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Branch 1" }),
condition: {
equal: { "{{trigger.fields.input}}": "1" },
},
},
branch2: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Branch 2" }),
condition: {
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
equal: { "{{trigger.fields.input}}": "2" },
},
},
elseBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "ELSE Branch" }),
condition: {}, // Empty condition acts as default/ELSE branch
},
})
.test({ fields: { test_trigger: true } })
.test({ fields: { input: "3" } })

expect(results.steps[0].outputs.success).toEqual(false)
expect(results.steps[0].outputs.status).toEqual(
AutomationStatus.NO_CONDITION_MET
)
expect(results.steps[0].outputs.status).toContain("elseBranch branch taken")
expect(results.steps[1].outputs.message).toContain("ELSE Branch")
})

it("should execute first matching branch and skip ELSE", async () => {
const results = await createAutomationBuilder(config)
.onAppAction()
.branch({
branch1: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Branch 1" }),
condition: {
equal: { "{{trigger.fields.input}}": "1" },
},
},
branch2: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Branch 2" }),
condition: {
equal: { "{{trigger.fields.input}}": "2" },
},
},
elseBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "ELSE Branch" }),
condition: {}, // Empty condition acts as default/ELSE branch
},
})
.test({ fields: { input: "2" } })

expect(results.steps[0].outputs.status).toContain("branch2 branch taken")
expect(results.steps[1].outputs.message).toContain("Branch 2")
})
})
46 changes: 46 additions & 0 deletions packages/server/src/threads/automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,54 @@ async function branchMatches(
ctx: AutomationContext,
branch: Readonly<Branch>
): Promise<boolean> {
const requiresEvaluation = (condition: any): boolean => {
if (!condition || typeof condition !== "object") {
return false
}

const keys = Object.keys(condition)
if (keys.length === 0) {
return false
}

if (keys.length === 1 && keys[0] === "onEmptyFilter") {
return false
}

for (const key of keys) {
if (key === "onEmptyFilter") {
continue
}

const value = condition[key]
if (value && typeof value === "object") {
if (key === "$and" || key === "$or") {
if (
value.conditions &&
Array.isArray(value.conditions) &&
value.conditions.length > 0
) {
return true
}
} else if (Object.keys(value).length > 0) {
return true
}
} else if (value !== undefined && value !== null) {
return true
}
}

return false
}

if (!requiresEvaluation(branch.condition)) {
return true
}

const toFilter: Record<string, any> = {}

// If the condition requires evaluation (has actual filtering logic), evaluate it.
// If not (empty condition), it's a default/ELSE branch that should always match.
// Because we allow bindings on both the left and right of each condition in
// automation branches, we can't pass the BranchSearchFilters directly to
// dataFilters.runQuery as-is. We first need to walk the filter tree and
Expand Down
62 changes: 53 additions & 9 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2811,6 +2811,30 @@
pouchdb-promise "^6.0.4"
through2 "^2.0.0"

"@budibase/pro@*":
version "3.21.3"
resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-3.21.3.tgz#7fcfea1a79fc06ce1ed27ca7fa969b6d373510ee"
integrity sha512-uv3WB94aDtpvMf1CDzqwl8GmamvrsyQys8VpxqdojED+C0IfhwMmIMdiad/tRwNj9U3fk7qbCxxkh3am9wMTGg==
dependencies:
"@anthropic-ai/sdk" "^0.27.3"
"@budibase/backend-core" "*"
"@budibase/shared-core" "*"
"@budibase/string-templates" "*"
"@budibase/types" "*"
"@koa/router" "13.1.0"
bull "4.10.1"
dd-trace "5.56.0"
joi "17.6.0"
jsonwebtoken "9.0.2"
lru-cache "^7.14.1"
memorystream "^0.3.1"
node-fetch "2.6.7"
openai "5.12.1"
scim-patch "^0.8.1"
scim2-parse-filter "^0.2.8"
undici "^6.0.0"
zod "^3.23.8"

"@budibase/vm-browserify@^1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@budibase/vm-browserify/-/vm-browserify-1.1.4.tgz#eecb001bd9521cb7647e26fb4d2d29d0a4dce262"
Expand Down Expand Up @@ -21505,7 +21529,16 @@ string-length@^4.0.2:
char-regex "^1.0.2"
strip-ansi "^6.0.0"

"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"

"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
Expand Down Expand Up @@ -21592,7 +21625,14 @@ stringify-object@^3.2.1:
is-obj "^1.0.1"
is-regexp "^1.0.0"

"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"

strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
Expand Down Expand Up @@ -22224,11 +22264,6 @@ [email protected]:
resolved "https://registry.yarnpkg.com/timekeeper/-/timekeeper-2.2.0.tgz#9645731fce9e3280a18614a57a9d1b72af3ca368"
integrity sha512-W3AmPTJWZkRwu+iSNxPIsLZ2ByADsOLbbLxe46UJyWj3mlYLlwucKiq+/dPm0l9wTzqoF3/2PH0AGFCebjq23A==

timekeeper@^2.2.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/timekeeper/-/timekeeper-2.3.1.tgz#2deb6e0b95d93625fda84c18d47f84a99e4eba01"
integrity sha512-LeQRS7/4JcC0PgdSFnfUiStQEdiuySlCj/5SJ18D+T1n9BoY7PxKFfCwLulpHXoLUFr67HxBddQdEX47lDGx1g==

tiny-glob@^0.2.9:
version "0.2.9"
resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2"
Expand Down Expand Up @@ -22461,7 +22496,7 @@ tsconfig-paths@^3.10.1, tsconfig-paths@^3.15.0:
minimist "^1.2.6"
strip-bom "^3.0.0"

tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0:
tsconfig-paths@^4.1.2:
version "4.2.0"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c"
integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==
Expand Down Expand Up @@ -23516,7 +23551,7 @@ [email protected]:
dependencies:
errno "~0.1.7"

"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
Expand All @@ -23534,6 +23569,15 @@ wrap-ansi@^6.0.1:
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
Expand Down
Loading