add validators and tests for automation branching

This commit is contained in:
Peter Clement 2024-09-03 16:16:52 +01:00
parent 56641e06c3
commit 839292b84d
3 changed files with 245 additions and 4 deletions

View File

@ -25,6 +25,8 @@ let {
collectAutomation, collectAutomation,
filterAutomation, filterAutomation,
updateRowAutomationWithFilters, updateRowAutomationWithFilters,
branchAutomationIncorrectPosition,
branchAutomation,
} = setup.structures } = setup.structures
describe("/automations", () => { describe("/automations", () => {
@ -121,6 +123,78 @@ describe("/automations", () => {
expect(events.automation.stepCreated).toHaveBeenCalledTimes(2) 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 () => { it("should apply authorization to endpoint", async () => {
const automation = newAutomation() const automation = newAutomation()
await checkBuilderEndpoint({ await checkBuilderEndpoint({

View File

@ -1,6 +1,9 @@
import { auth, permissions } from "@budibase/backend-core" import { auth, permissions } from "@budibase/backend-core"
import { DataSourceOperation } from "../../../constants" import { DataSourceOperation } from "../../../constants"
import { import {
AutomationActionStepId,
AutomationStep,
AutomationStepType,
EmptyFilterOption, EmptyFilterOption,
SearchFilters, SearchFilters,
Table, Table,
@ -88,7 +91,7 @@ export function datasourceValidator() {
) )
} }
function filterObject() { function filterObject(unknown = true) {
const conditionalFilteringObject = () => const conditionalFilteringObject = () =>
Joi.object({ Joi.object({
conditions: Joi.array().items(Joi.link("#schema")).required(), conditions: Joi.array().items(Joi.link("#schema")).required(),
@ -115,7 +118,7 @@ function filterObject() {
fuzzyOr: Joi.forbidden(), fuzzyOr: Joi.forbidden(),
documentType: Joi.forbidden(), documentType: Joi.forbidden(),
} }
return Joi.object(filtersValidators).unknown(true).id("schema") return Joi.object(filtersValidators).unknown(unknown).id("schema")
} }
export function internalSearchValidator() { export function internalSearchValidator() {
@ -259,6 +262,11 @@ export function screenValidator() {
} }
function generateStepSchema(allowStepTypes: string[]) { function generateStepSchema(allowStepTypes: string[]) {
const branchSchema = Joi.object({
name: Joi.string().required(),
condition: filterObject(false).required().min(1),
})
return Joi.object({ return Joi.object({
stepId: Joi.string().required(), stepId: Joi.string().required(),
id: Joi.string().required(), id: Joi.string().required(),
@ -267,6 +275,17 @@ function generateStepSchema(allowStepTypes: string[]) {
tagline: Joi.string().required(), tagline: Joi.string().required(),
icon: Joi.string().required(), icon: Joi.string().required(),
params: Joi.object(), 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(), args: Joi.object(),
type: Joi.string() type: Joi.string()
.required() .required()
@ -274,6 +293,17 @@ function generateStepSchema(allowStepTypes: string[]) {
}).unknown(true) }).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) { export function automationValidator(existing = false) {
return auth.joiValidator.body( return auth.joiValidator.body(
Joi.object({ Joi.object({
@ -284,9 +314,20 @@ export function automationValidator(existing = false) {
definition: Joi.object({ definition: Joi.object({
steps: Joi.array() steps: Joi.array()
.required() .required()
.items(generateStepSchema(["ACTION", "LOGIC"])), .items(
trigger: generateStepSchema(["TRIGGER"]).allow(null), 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() .required()
.unknown(true), .unknown(true),
}).unknown(true) }).unknown(true)

View File

@ -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( export function loopAutomation(
tableId: string, tableId: string,
loopOpts?: LoopInput loopOpts?: LoopInput