Merge pull request #15054 from Budibase/fix/automation-adjacent-bugs

Some automation bugs
This commit is contained in:
Peter Clement 2024-11-26 14:12:06 +00:00 committed by GitHub
commit ef4da634a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 153 additions and 45 deletions

View File

@ -114,7 +114,7 @@
$: schemaFields = search.getFields( $: schemaFields = search.getFields(
$tables.list, $tables.list,
Object.values(schema || {}), Object.values(schema || {}),
{ allowLinks: true } { allowLinks: false }
) )
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000" $: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
$: isTrigger = $memoBlock?.type === AutomationStepType.TRIGGER $: isTrigger = $memoBlock?.type === AutomationStepType.TRIGGER

View File

@ -16,6 +16,7 @@ import {
AutomationTriggerStepId, AutomationTriggerStepId,
AutomationEventType, AutomationEventType,
AutomationStepType, AutomationStepType,
AutomationActionStepId,
} from "@budibase/types" } from "@budibase/types"
import { ActionStepID } from "constants/backend/automations" import { ActionStepID } from "constants/backend/automations"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
@ -466,9 +467,13 @@ const automationActions = store => ({
.getPathSteps(block.pathTo, automation) .getPathSteps(block.pathTo, automation)
.slice(0, -1) .slice(0, -1)
// Current step will always be the last step of the path
const currentBlock = store.actions
.getPathSteps(block.pathTo, automation)
.at(-1)
// Extract all outputs from all previous steps as available bindingsx§x // Extract all outputs from all previous steps as available bindingsx§x
let bindings = [] let bindings = []
const addBinding = (name, value, icon, idx, isLoopBlock, bindingName) => { const addBinding = (name, value, icon, idx, isLoopBlock, bindingName) => {
if (!name) return if (!name) return
const runtimeBinding = determineRuntimeBinding( const runtimeBinding = determineRuntimeBinding(
@ -519,9 +524,24 @@ const automationActions = store => ({
runtimeName = `loop.${name}` runtimeName = `loop.${name}`
} else if (idx === 0) { } else if (idx === 0) {
runtimeName = `trigger.${name}` runtimeName = `trigger.${name}`
} else if (
currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT
) {
const stepId = pathSteps[idx].id
if (!stepId) {
notifications.error("Error generating binding: Step ID not found.")
return null
}
runtimeName = `steps["${stepId}"].${name}`
} else { } else {
runtimeName = `steps.${pathSteps[idx]?.id}.${name}` const stepId = pathSteps[idx].id
if (!stepId) {
notifications.error("Error generating binding: Step ID not found.")
return null
}
runtimeName = `steps.${stepId}.${name}`
} }
return runtimeName return runtimeName
} }
@ -637,7 +657,6 @@ const automationActions = store => ({
console.error("Loop block missing.") console.error("Loop block missing.")
} }
} }
Object.entries(schema).forEach(([name, value]) => { Object.entries(schema).forEach(([name, value]) => {
addBinding(name, value, icon, blockIdx, isLoopBlock, bindingName) addBinding(name, value, icon, blockIdx, isLoopBlock, bindingName)
}) })

View File

@ -61,6 +61,9 @@ export async function run({
inputs: ServerLogStepInputs inputs: ServerLogStepInputs
appId: string appId: string
}): Promise<ServerLogStepOutputs> { }): Promise<ServerLogStepOutputs> {
if (typeof inputs.text !== "string") {
inputs.text = JSON.stringify(inputs.text)
}
const message = `App ${appId} - ${inputs.text}` const message = `App ${appId} - ${inputs.text}`
console.log(message) console.log(message)
return { return {

View File

@ -1,50 +1,123 @@
import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities" import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
import * as automation from "../index"
import * as setup from "./utilities"
import { Table } from "@budibase/types"
describe("test the execute script action", () => { describe("Execute Script Automations", () => {
let config = getConfig() let config = setup.getConfig(),
table: Table
beforeAll(async () => { beforeEach(async () => {
await automation.init()
await config.init() await config.init()
table = await config.createTable()
await config.createRow()
}) })
afterAll(_afterAll)
it("should be able to execute a script", async () => { afterAll(setup.afterAll)
const res = await runStep(config, actions.EXECUTE_SCRIPT.stepId, {
code: "return 1 + 1", it("should execute a basic script and return the result", async () => {
const builder = createAutomationBuilder({
name: "Basic Script Execution",
}) })
expect(res.value).toEqual(2)
expect(res.success).toEqual(true) const results = await builder
.appAction({ fields: {} })
.executeScript({ code: "return 2 + 2" })
.run()
expect(results.steps[0].outputs.value).toEqual(4)
}) })
it("should handle a null value", async () => { it("should access bindings from previous steps", async () => {
const res = await runStep(config, actions.EXECUTE_SCRIPT.stepId, { const builder = createAutomationBuilder({
code: null, name: "Access Bindings",
}) })
expect(res.response.message).toEqual("Invalid inputs")
expect(res.success).toEqual(false) const results = await builder
.appAction({ fields: { data: [1, 2, 3] } })
.executeScript(
{
code: "return trigger.fields.data.map(x => x * 2)",
},
{ stepId: "binding-script-step" }
)
.run()
expect(results.steps[0].outputs.value).toEqual([2, 4, 6])
}) })
it("should be able to get a value from context", async () => { it("should handle script execution errors gracefully", async () => {
const res = await runStep( const builder = createAutomationBuilder({
config, name: "Handle Script Errors",
actions.EXECUTE_SCRIPT.stepId, })
{
code: "return steps.map(d => d.value)", const results = await builder
}, .appAction({ fields: {} })
{ .executeScript({ code: "return nonexistentVariable.map(x => x)" })
steps: [{ value: 0 }, { value: 1 }], .run()
}
expect(results.steps[0].outputs.response).toContain(
"ReferenceError: nonexistentVariable is not defined"
) )
expect(res.value).toEqual([0, 1]) expect(results.steps[0].outputs.success).toEqual(false)
expect(res.response).toBeUndefined()
expect(res.success).toEqual(true)
}) })
it("should be able to handle an error gracefully", async () => { it("should handle conditional logic in scripts", async () => {
const res = await runStep(config, actions.EXECUTE_SCRIPT.stepId, { const builder = createAutomationBuilder({
code: "return something.map(x => x.name)", name: "Conditional Script Logic",
}) })
expect(res.response).toEqual("ReferenceError: something is not defined")
expect(res.success).toEqual(false) const results = await builder
.appAction({ fields: { value: 10 } })
.executeScript({
code: `
if (trigger.fields.value > 5) {
return "Value is greater than 5";
} else {
return "Value is 5 or less";
}
`,
})
.run()
expect(results.steps[0].outputs.value).toEqual("Value is greater than 5")
})
it("should use multiple steps and validate script execution", async () => {
const builder = createAutomationBuilder({
name: "Multi-Step Script Execution",
})
const results = await builder
.appAction({ fields: {} })
.serverLog(
{ text: "Starting multi-step automation" },
{ stepId: "start-log-step" }
)
.createRow(
{ row: { name: "Test Row", value: 42, tableId: table._id } },
{ stepId: "abc123" }
)
.executeScript(
{
code: `
const createdRow = steps['abc123'];
return createdRow.row.value * 2;
`,
},
{ stepId: "ScriptingStep1" }
)
.serverLog({
text: `Final result is {{ steps.ScriptingStep1.value }}`,
})
.run()
expect(results.steps[0].outputs.message).toContain(
"Starting multi-step automation"
)
expect(results.steps[1].outputs.row.value).toEqual(42)
expect(results.steps[2].outputs.value).toEqual(84)
expect(results.steps[3].outputs.message).toContain("Final result is 84")
}) })
}) })

View File

@ -34,6 +34,7 @@ import {
SearchFilters, SearchFilters,
Branch, Branch,
FilterStepInputs, FilterStepInputs,
ExecuteScriptStepInputs,
} from "@budibase/types" } from "@budibase/types"
import TestConfiguration from "../../../tests/utilities/TestConfiguration" import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import * as setup from "../utilities" import * as setup from "../utilities"
@ -201,6 +202,18 @@ class BaseStepBuilder {
) )
} }
executeScript(
input: ExecuteScriptStepInputs,
opts?: { stepName?: string; stepId?: string }
): this {
return this.step(
AutomationActionStepId.EXECUTE_SCRIPT,
BUILTIN_ACTION_DEFINITIONS.EXECUTE_SCRIPT,
input,
opts
)
}
filter(input: FilterStepInputs): this { filter(input: FilterStepInputs): this {
return this.step( return this.step(
AutomationActionStepId.FILTER, AutomationActionStepId.FILTER,

View File

@ -385,7 +385,7 @@ class Orchestrator {
stepIdx: number, stepIdx: number,
pathIdx?: number pathIdx?: number
): Promise<number> { ): Promise<number> {
await processObject(loopStep.inputs, this.processContext(this.context)) await processObject(loopStep.inputs, this.mergeContexts(this.context))
const iterations = getLoopIterations(loopStep) const iterations = getLoopIterations(loopStep)
let stepToLoopIndex = stepIdx + 1 let stepToLoopIndex = stepIdx + 1
let pathStepIdx = (pathIdx || stepIdx) + 1 let pathStepIdx = (pathIdx || stepIdx) + 1
@ -573,14 +573,14 @@ class Orchestrator {
for (const [field, value] of Object.entries(filters[filterKey])) { for (const [field, value] of Object.entries(filters[filterKey])) {
const fromContext = processStringSync( const fromContext = processStringSync(
field, field,
this.processContext(this.context) this.mergeContexts(this.context)
) )
toFilter[field] = fromContext toFilter[field] = fromContext
if (typeof value === "string" && findHBSBlocks(value).length > 0) { if (typeof value === "string" && findHBSBlocks(value).length > 0) {
const processedVal = processStringSync( const processedVal = processStringSync(
value, value,
this.processContext(this.context) this.mergeContexts(this.context)
) )
filters[filterKey][field] = processedVal filters[filterKey][field] = processedVal
@ -637,7 +637,7 @@ class Orchestrator {
const stepFn = await this.getStepFunctionality(step.stepId) const stepFn = await this.getStepFunctionality(step.stepId)
let inputs = await processObject( let inputs = await processObject(
originalStepInput, originalStepInput,
this.processContext(this.context) this.mergeContexts(this.context)
) )
inputs = automationUtils.cleanInputValues(inputs, step.schema.inputs) inputs = automationUtils.cleanInputValues(inputs, step.schema.inputs)
@ -645,7 +645,7 @@ class Orchestrator {
inputs: inputs, inputs: inputs,
appId: this.appId, appId: this.appId,
emitter: this.emitter, emitter: this.emitter,
context: this.context, context: this.mergeContexts(this.context),
}) })
this.handleStepOutput(step, outputs, loopIteration) this.handleStepOutput(step, outputs, loopIteration)
} }
@ -665,8 +665,8 @@ class Orchestrator {
return null return null
} }
private processContext(context: AutomationContext) { private mergeContexts(context: AutomationContext) {
const processContext = { const mergeContexts = {
...context, ...context,
steps: { steps: {
...context.steps, ...context.steps,
@ -674,7 +674,7 @@ class Orchestrator {
...context.stepsByName, ...context.stepsByName,
}, },
} }
return processContext return mergeContexts
} }
private handleStepOutput( private handleStepOutput(