Loop tests passing again.

This commit is contained in:
Sam Rose 2025-02-13 12:52:03 +00:00
parent 3f104cb2ab
commit 57149b77e1
No known key found for this signature in database
5 changed files with 94 additions and 142 deletions

View File

@ -274,26 +274,3 @@ export function stringSplit(value: string | string[]) {
} }
return value.split(",") return value.split(",")
} }
export function typecastForLooping(input: LoopStepInputs) {
if (!input || !input.binding) {
return null
}
try {
switch (input.option) {
case LoopStepType.ARRAY:
if (typeof input.binding === "string") {
return JSON.parse(input.binding)
}
break
case LoopStepType.STRING:
if (Array.isArray(input.binding)) {
return input.binding.join(",")
}
break
}
} catch (err) {
throw new Error("Unable to cast to correct type")
}
return input.binding
}

View File

@ -1,9 +1,4 @@
import { import { cleanInputValues, substituteLoopStep } from "../automationUtils"
typecastForLooping,
cleanInputValues,
substituteLoopStep,
} from "../automationUtils"
import { LoopStepType } from "@budibase/types"
describe("automationUtils", () => { describe("automationUtils", () => {
describe("substituteLoopStep", () => { describe("substituteLoopStep", () => {
@ -30,29 +25,6 @@ describe("automationUtils", () => {
}) })
}) })
describe("typeCastForLooping", () => {
it("should parse to correct type", () => {
expect(
typecastForLooping({ option: LoopStepType.ARRAY, binding: [1, 2, 3] })
).toEqual([1, 2, 3])
expect(
typecastForLooping({ option: LoopStepType.ARRAY, binding: "[1,2,3]" })
).toEqual([1, 2, 3])
expect(
typecastForLooping({ option: LoopStepType.STRING, binding: [1, 2, 3] })
).toEqual("1,2,3")
})
it("should handle null values", () => {
// expect it to handle where the binding is null
expect(
typecastForLooping({ option: LoopStepType.ARRAY, binding: null })
).toEqual(null)
expect(() =>
typecastForLooping({ option: LoopStepType.ARRAY, binding: "test" })
).toThrow()
})
})
describe("cleanInputValues", () => { describe("cleanInputValues", () => {
it("should handle array relationship fields from read binding", () => { it("should handle array relationship fields from read binding", () => {
const schema = { const schema = {

View File

@ -130,11 +130,6 @@ export enum InvalidColumns {
TABLE_ID = "tableId", TABLE_ID = "tableId",
} }
export enum AutomationErrors {
INCORRECT_TYPE = "INCORRECT_TYPE",
FAILURE_CONDITION = "FAILURE_CONDITION_MET",
}
// pass through the list from the auth/core lib // pass through the list from the auth/core lib
export const ObjectStoreBuckets = objectStore.ObjectStoreBuckets export const ObjectStoreBuckets = objectStore.ObjectStoreBuckets
export const MAX_AUTOMATION_RECURRING_ERRORS = 5 export const MAX_AUTOMATION_RECURRING_ERRORS = 5

View File

@ -11,7 +11,7 @@ import { dataFilters, helpers, utils } from "@budibase/shared-core"
import { default as AutomationEmitter } from "../events/AutomationEmitter" import { default as AutomationEmitter } from "../events/AutomationEmitter"
import { generateAutomationMetadataID, isProdAppID } from "../db/utils" import { generateAutomationMetadataID, isProdAppID } from "../db/utils"
import { automations } from "@budibase/shared-core" import { automations } from "@budibase/shared-core"
import { AutomationErrors, MAX_AUTOMATION_RECURRING_ERRORS } from "../constants" import { MAX_AUTOMATION_RECURRING_ERRORS } from "../constants"
import { storeLog } from "../automations/logging" import { storeLog } from "../automations/logging"
import { import {
Automation, Automation,
@ -65,32 +65,23 @@ function matchesLoopFailureCondition(loopStep: LoopStep, currentItem: any) {
return currentItem === loopStep.inputs.failure return currentItem === loopStep.inputs.failure
} }
function getLoopIterations(loopStep: LoopStep) { function getLoopIterable(loopStep: LoopStep): any[] {
const binding = loopStep.inputs.binding const option = loopStep.inputs.option
if (!binding) { let input: any = loopStep.inputs.binding
return 0
} if (option === LoopStepType.ARRAY && typeof input === "string") {
try { input = JSON.parse(input)
const json = typeof binding === "string" ? JSON.parse(binding) : binding
if (Array.isArray(json)) {
return json.length
}
} catch (err) {
// ignore error - wasn't able to parse
}
if (typeof binding === "string") {
return automationUtils.stringSplit(binding).length
}
return 0
} }
function getLoopMaxIterations(loopStep: LoopStep) { if (option === LoopStepType.STRING && Array.isArray(input)) {
const value = loopStep.inputs.iterations input = input.join(",")
if (typeof value === "number") return value
if (typeof value === "string") {
return parseInt(value)
} }
return undefined
if (option === LoopStepType.STRING && typeof input === "string") {
input = automationUtils.stringSplit(input)
}
return Array.isArray(input) ? input : [input]
} }
function prepareContext(context: AutomationContext) { function prepareContext(context: AutomationContext) {
@ -333,24 +324,26 @@ class Orchestrator {
const step = steps[stepIndex] const step = steps[stepIndex]
if (step.stepId === AutomationActionStepId.BRANCH) { if (step.stepId === AutomationActionStepId.BRANCH) {
// stepIndex for current step context offset const [result, ...childResults] = await this.executeBranchStep(
// pathIdx relating to the full list of steps in the run ctx,
const [branchResult, ...branchStepResults] = step
await this.executeBranchStep(ctx, step) )
stepOutputs.push(branchResult) stepOutputs.push(result)
stepOutputs.push(...branchStepResults) stepOutputs.push(...childResults)
stepIndex++ stepIndex++
} else if (step.stepId === AutomationActionStepId.LOOP) { } else if (step.stepId === AutomationActionStepId.LOOP) {
const output = await this.executeLoopStep( const stepToLoop = steps[stepIndex + 1]
ctx, const result = await this.executeLoopStep(ctx, step, stepToLoop)
step,
steps[stepIndex + 1]
)
ctx.steps.push(result.outputs)
ctx.stepsById[stepToLoop.id] = result.outputs
ctx.stepsByName[stepToLoop.name || stepToLoop.id] =
result.outputs
stepOutputs.push(result)
stepIndex += 2 stepIndex += 2
stepOutputs.push(output)
} else { } else {
const result = await this.executeStep(ctx, step) const result = await this.executeStep(ctx, step)
@ -381,56 +374,81 @@ class Orchestrator {
stepToLoop: AutomationStep stepToLoop: AutomationStep
): Promise<AutomationStepResult> { ): Promise<AutomationStepResult> {
await processObject(loopStep.inputs, prepareContext(ctx)) await processObject(loopStep.inputs, prepareContext(ctx))
const maxIterations = getLoopMaxIterations(loopStep)
const items: AutomationStepResult[] = []
let status: AutomationStepStatus | undefined = undefined const result = {
let success = true id: loopStep.id,
stepId: loopStep.stepId,
inputs: loopStep.inputs,
}
let i = 0 const loopMaxIterations =
for (; i < getLoopIterations(loopStep); i++) { typeof loopStep.inputs.iterations === "string"
try { ? parseInt(loopStep.inputs.iterations)
loopStep.inputs.binding = automationUtils.typecastForLooping( : loopStep.inputs.iterations
loopStep.inputs const maxIterations = Math.min(
loopMaxIterations || env.AUTOMATION_MAX_ITERATIONS,
env.AUTOMATION_MAX_ITERATIONS
) )
const items: Record<string, any>[] = []
let iterations = 0
let iterable: any[] = []
try {
iterable = getLoopIterable(loopStep)
} catch (err) { } catch (err) {
break return {
...result,
outputs: {
success: false,
status: AutomationStepStatus.INCORRECT_TYPE,
},
}
} }
if ( for (; iterations < iterable.length; iterations++) {
i === env.AUTOMATION_MAX_ITERATIONS || const currentItem = iterable[iterations]
(loopStep.inputs.iterations && i === maxIterations)
) { if (iterations === maxIterations) {
status = AutomationStepStatus.MAX_ITERATIONS return {
break ...result,
outputs: {
success: false,
iterations,
items,
status: AutomationStepStatus.MAX_ITERATIONS,
},
}
} }
const currentItem = this.getCurrentLoopItem(loopStep, i)
if (matchesLoopFailureCondition(loopStep, currentItem)) { if (matchesLoopFailureCondition(loopStep, currentItem)) {
status = AutomationStepStatus.FAILURE_CONDITION return {
success = false ...result,
break outputs: {
success: false,
iterations,
items,
status: AutomationStepStatus.FAILURE_CONDITION,
},
}
} }
ctx.loop = { currentItem } ctx.loop = { currentItem }
items.push(await this.executeStep(ctx, stepToLoop)) const loopedStepResult = await this.executeStep(ctx, stepToLoop)
items.push(loopedStepResult.outputs)
ctx.loop = undefined ctx.loop = undefined
} }
if (i === 0) {
status = AutomationStepStatus.NO_ITERATIONS
}
return { return {
id: loopStep.id, id: stepToLoop.id,
stepId: loopStep.stepId, stepId: stepToLoop.stepId,
inputs: stepToLoop.inputs,
outputs: { outputs: {
success, success: true,
status, status:
iterations: i, iterations === 0 ? AutomationStepStatus.NO_ITERATIONS : undefined,
iterations,
items, items,
}, },
inputs: loopStep.inputs,
} }
} }
@ -573,6 +591,7 @@ class Orchestrator {
outputs.result === false outputs.result === false
) { ) {
this.stopped = true this.stopped = true
;(outputs as any).status = AutomationStatus.STOPPED
} }
return { return {
@ -584,18 +603,6 @@ class Orchestrator {
} }
) )
} }
private getCurrentLoopItem(loopStep: LoopStep, index: number): any {
if (
typeof loopStep.inputs.binding === "string" &&
loopStep.inputs.option === LoopStepType.STRING
) {
return automationUtils.stringSplit(loopStep.inputs.binding)[index]
} else if (Array.isArray(loopStep.inputs.binding)) {
return loopStep.inputs.binding[index]
}
return null
}
} }
export function execute(job: Job<AutomationData>, callback: WorkerCallback) { export function execute(job: Job<AutomationData>, callback: WorkerCallback) {

View File

@ -176,7 +176,8 @@ export enum AutomationFeature {
export enum AutomationStepStatus { export enum AutomationStepStatus {
NO_ITERATIONS = "no_iterations", NO_ITERATIONS = "no_iterations",
MAX_ITERATIONS = "max_iterations_reached", MAX_ITERATIONS = "max_iterations_reached",
FAILURE_CONDITION = "failure_condition", FAILURE_CONDITION = "FAILURE_CONDITION_MET",
INCORRECT_TYPE = "INCORRECT_TYPE",
} }
export enum AutomationStatus { export enum AutomationStatus {