diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts index 1ade332f0a..3f35eb0c9a 100644 --- a/packages/server/src/api/routes/tests/automation.spec.ts +++ b/packages/server/src/api/routes/tests/automation.spec.ts @@ -25,6 +25,8 @@ let { collectAutomation, filterAutomation, updateRowAutomationWithFilters, + branchAutomationIncorrectPosition, + branchAutomation, } = setup.structures describe("/automations", () => { @@ -121,6 +123,78 @@ 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 = branchAutomationIncorrectPosition() + const res = await request + .post(`/api/automations`) + .set(config.defaultHeaders()) + .send(automation) + .expect("Content-Type", /json/) + .expect(400) + + expect(res.body.message).toContain("must contain at least 1 items") + }) + + it("Should check validation on an automation that has a branch step with no children", async () => { + const automation = branchAutomationIncorrectPosition() + automation.definition.steps[0].inputs.branches = [ + { name: "test", condition: { equal: { "steps.1.success": "true" } } }, + ] + automation.definition.steps[0].inputs.children = {} + + const res = await request + .post(`/api/automations`) + .set(config.defaultHeaders()) + .send(automation) + .expect("Content-Type", /json/) + .expect(400) + + expect(res.body.message).toContain( + "Branch steps are only allowed as the last step" + ) + }) + + it("Should check validation on a branch step with empty conditions", async () => { + const automation = branchAutomation() + + automation.definition.steps[1].inputs.branches = [ + { name: "test", condition: {} }, + ] + automation.definition.steps[1].inputs.children = {} + + const res = await request + .post(`/api/automations`) + .set(config.defaultHeaders()) + .send(automation) + .expect("Content-Type", /json/) + .expect(400) + + expect(res.body.message).toContain("must have at least 1 key") + }) + + it("Should check validation on an branch that has a condition that is not valid", async () => { + const automation = branchAutomation() + + automation.definition.steps[1].inputs.branches = [ + { + name: "test", + condition: { + INCORRECT: { "steps.1.success": true }, + }, + }, + ] + automation.definition.steps[1].inputs.children = {} + + const res = await request + .post(`/api/automations`) + .set(config.defaultHeaders()) + .send(automation) + .expect("Content-Type", /json/) + .expect(400) + + expect(res.body.message).toContain('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..e68d5b3795 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,7 @@ export function datasourceValidator() { ) } -function filterObject() { +function filterObject(unknown = true) { const conditionalFilteringObject = () => Joi.object({ conditions: Joi.array().items(Joi.link("#schema")).required(), @@ -115,7 +118,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 +262,11 @@ export function screenValidator() { } function generateStepSchema(allowStepTypes: string[]) { + const branchSchema = Joi.object({ + name: Joi.string().required(), + condition: filterObject(false).required().min(1), + }) + return Joi.object({ stepId: Joi.string().required(), id: Joi.string().required(), @@ -267,6 +275,17 @@ 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() @@ -274,6 +293,17 @@ function generateStepSchema(allowStepTypes: string[]) { }).unknown(true) } +const validateStepsArray = ( + steps: AutomationStep[], + helpers: Joi.CustomHelpers +) => { + for (let i = 0; i < steps.length - 1; i++) { + if (steps[i].stepId === AutomationActionStepId.BRANCH) { + return helpers.error("branchStepPosition") + } + } +} + export function automationValidator(existing = false) { return auth.joiValidator.body( Joi.object({ @@ -284,9 +314,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/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts index 2e501932b8..6655444a13 100644 --- a/packages/server/src/tests/utilities/structures.ts +++ b/packages/server/src/tests/utilities/structures.ts @@ -292,6 +292,132 @@ export function serverLogAutomation(appId?: string): Automation { } } +export function branchAutomationIncorrectPosition(appId?: string): Automation { + return { + name: "My Automation", + screenId: "kasdkfldsafkl", + live: true, + uiTree: {}, + definition: { + trigger: { + stepId: AutomationTriggerStepId.APP, + name: "test", + tagline: "test", + icon: "test", + description: "test", + type: AutomationStepType.TRIGGER, + id: "test", + inputs: { fields: {} }, + schema: { + inputs: { + properties: {}, + }, + outputs: { + properties: {}, + }, + }, + }, + steps: [ + { + stepId: AutomationActionStepId.BRANCH, + name: "Branch", + tagline: "Console log a value in the backend", + icon: "Monitoring", + description: "Logs the given text to the server (using console.log)", + inputs: { + branches: [], + }, + schema: { inputs: { properties: {} }, outputs: { properties: {} } }, + id: "y8lkZbeSe", + type: AutomationStepType.LOGIC, + }, + { + stepId: AutomationActionStepId.SERVER_LOG, + name: "Backend log", + tagline: "Console log a value in the backend", + icon: "Monitoring", + description: "Logs the given text to the server (using console.log)", + internal: true, + features: { + LOOPING: true, + }, + inputs: { + text: "log statement", + }, + schema: BUILTIN_ACTION_DEFINITIONS.SERVER_LOG.schema, + id: "y8lkZbeSe", + type: AutomationStepType.ACTION, + }, + ], + }, + type: "automation", + appId: appId!, + } +} + +export function branchAutomation(appId?: string): Automation { + return { + name: "My Automation", + screenId: "kasdkfldsafkl", + live: true, + uiTree: {}, + definition: { + trigger: { + stepId: AutomationTriggerStepId.APP, + name: "test", + tagline: "test", + icon: "test", + description: "test", + type: AutomationStepType.TRIGGER, + id: "test", + inputs: { fields: {} }, + schema: { + inputs: { + properties: {}, + }, + outputs: { + properties: {}, + }, + }, + }, + steps: [ + { + stepId: AutomationActionStepId.SERVER_LOG, + name: "Backend log", + tagline: "Console log a value in the backend", + icon: "Monitoring", + description: "Logs the given text to the server (using console.log)", + internal: true, + features: { + LOOPING: true, + }, + inputs: { + text: "log statement", + }, + schema: BUILTIN_ACTION_DEFINITIONS.SERVER_LOG.schema, + id: "y8lkZbeSe", + type: AutomationStepType.ACTION, + }, + { + stepId: AutomationActionStepId.BRANCH, + name: "Branch", + tagline: "Console log a value in the backend", + icon: "Monitoring", + description: "Logs the given text to the server (using console.log)", + inputs: { + branches: [], + }, + schema: { inputs: { properties: {} }, outputs: { properties: {} } }, + id: "y8lkZbeSe", + type: AutomationStepType.LOGIC, + }, + ], + }, + type: "automation", + appId: appId!, + } +} + export function loopAutomation( tableId: string, loopOpts?: LoopInput