diff --git a/packages/server/src/automations/tests/branching.spec.ts b/packages/server/src/automations/tests/branching.spec.ts index 8c8797b416a..9b3bd78fab1 100644 --- a/packages/server/src/automations/tests/branching.spec.ts +++ b/packages/server/src/automations/tests/branching.spec.ts @@ -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" @@ -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") }) }) diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index edfd3d6a142..ad27a1db9e2 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -128,8 +128,54 @@ async function branchMatches( ctx: AutomationContext, branch: Readonly ): Promise { + 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 = {} + // 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 diff --git a/yarn.lock b/yarn.lock index 94f7b71bce7..783317cf2f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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== @@ -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== @@ -22224,11 +22264,6 @@ timekeeper@2.2.0: 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" @@ -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== @@ -23516,7 +23551,7 @@ worker-farm@1.7.0: 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== @@ -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"