import * as automation from "../../index" import { basicTable } from "../../../tests/utilities/structures" import { Table, LoopStepType, ServerLogStepOutputs, CreateRowStepOutputs, FieldType, FilterCondition, AutomationStepStatus, } from "@budibase/types" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" import TestConfiguration from "../../../tests/utilities/TestConfiguration" describe("Attempt to run a basic loop automation", () => { const config = new TestConfiguration() let table: Table beforeAll(async () => { await config.init() await automation.init() }) beforeEach(async () => { await config.api.automation.deleteAll() table = await config.api.table.save(basicTable()) await config.api.row.save(table._id!, {}) }) afterAll(async () => { await automation.shutdown() config.end() }) it("attempt to run a basic loop", async () => { const result = await createAutomationBuilder(config) .onAppAction() .queryRows({ tableId: table._id!, }) .loop({ option: LoopStepType.ARRAY, binding: "{{ steps.1.rows }}", }) .serverLog({ text: "log statement" }) .test({ fields: {} }) expect(result.steps[1].outputs.iterations).toBe(1) }) it("test a loop with a string", async () => { const result = await createAutomationBuilder(config) .onAppAction() .loop({ option: LoopStepType.STRING, binding: "a,b,c", }) .serverLog({ text: "log statement" }) .test({ fields: {} }) expect(result.steps[0].outputs.iterations).toBe(3) }) it("test a loop with a binding that returns an integer", async () => { const result = await createAutomationBuilder(config) .onAppAction() .loop({ option: LoopStepType.ARRAY, binding: "{{ 1 }}", }) .serverLog({ text: "log statement" }) .test({ fields: {} }) expect(result.steps[0].outputs.iterations).toBe(1) }) it("should run an automation with a trigger, loop, and create row step", async () => { const results = await createAutomationBuilder(config) .onRowSaved({ tableId: table._id! }) .loop({ option: LoopStepType.ARRAY, binding: [1, 2, 3], }) .createRow({ row: { name: "Item {{ loop.currentItem }}", description: "Created from loop", tableId: table._id, }, }) .test({ row: { name: "Trigger Row", description: "This row triggers the automation", }, id: "1234", revision: "1", }) expect(results.trigger).toBeDefined() expect(results.steps).toHaveLength(1) expect(results.steps[0].outputs.iterations).toBe(3) expect(results.steps[0].outputs.items).toHaveLength(3) results.steps[0].outputs.items.forEach((output: any, index: number) => { expect(output).toMatchObject({ success: true, row: { name: `Item ${index + 1}`, description: "Created from loop", }, }) }) }) it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => { const results = await createAutomationBuilder(config) .onRowSaved({ tableId: table._id! }) .queryRows({ tableId: table._id!, }) .loop({ option: LoopStepType.ARRAY, binding: [1, 2, 3], }) .serverLog({ text: "Message {{loop.currentItem}}" }) .serverLog({ text: "{{steps.1.rows.0._id}}" }) .test({ row: { name: "Trigger Row", description: "This row triggers the automation", }, id: "1234", revision: "1", }) results.steps[1].outputs.items.forEach( (output: ServerLogStepOutputs, index: number) => { expect(output).toMatchObject({ success: true, }) expect(output.message).toContain(`Message ${index + 1}`) } ) expect(results.steps[2].outputs.message).toContain("ro_ta") }) it("if an incorrect type is passed to the loop it should return an error", async () => { const results = await createAutomationBuilder(config) .onAppAction() .loop({ option: LoopStepType.ARRAY, binding: "1, 2, 3", }) .serverLog({ text: "Message {{loop.currentItem}}" }) .test({ fields: {} }) expect(results.steps[0].outputs).toEqual({ success: false, status: "INCORRECT_TYPE", }) }) it("ensure the loop stops if the failure condition is reached", async () => { const results = await createAutomationBuilder(config) .onAppAction() .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], failure: "test2", }) .serverLog({ text: "Message {{loop.currentItem}}" }) .test({ fields: {} }) expect(results.steps[0].outputs).toEqual( expect.objectContaining({ status: "FAILURE_CONDITION_MET", success: false, }) ) }) it("ensure the loop stops if the max iterations are reached", async () => { const results = await createAutomationBuilder(config) .onAppAction() .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], iterations: 2, }) .serverLog({ text: "{{loop.currentItem}}" }) .serverLog({ text: "{{steps.1.iterations}}" }) .test({ fields: {} }) expect(results.steps[0].outputs.status).toBe( AutomationStepStatus.MAX_ITERATIONS ) expect(results.steps[0].outputs.iterations).toBe(2) expect(results.steps[0].outputs.items).toHaveLength(2) expect(results.steps[0].outputs.items[0].message).toEndWith("test") expect(results.steps[0].outputs.items[1].message).toEndWith("test2") }) it("should stop when a failure condition is hit", async () => { const results = await createAutomationBuilder(config) .onAppAction() .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], failure: "test3", }) .serverLog({ text: "{{loop.currentItem}}" }) .serverLog({ text: "{{steps.1.iterations}}" }) .test({ fields: {} }) expect(results.steps[0].outputs.status).toBe( AutomationStepStatus.FAILURE_CONDITION ) expect(results.steps[0].outputs.iterations).toBe(2) expect(results.steps[0].outputs.items).toHaveLength(2) expect(results.steps[0].outputs.items[0].message).toEndWith("test") expect(results.steps[0].outputs.items[1].message).toEndWith("test2") }) it("should run an automation with loop and max iterations to ensure context correctness further down the tree", async () => { const results = await createAutomationBuilder(config) .onAppAction() .loop({ option: LoopStepType.ARRAY, binding: ["test", "test2", "test3"], iterations: 2, }) .serverLog({ text: "{{loop.currentItem}}" }) .serverLog({ text: "{{steps.1.iterations}}" }) .test({ fields: {} }) expect(results.steps[1].outputs.message).toContain("- 2") }) it("should run an automation where a loop is successfully run twice", async () => { const results = await createAutomationBuilder(config) .onRowSaved({ tableId: table._id! }) .loop({ option: LoopStepType.ARRAY, binding: [1, 2, 3], }) .createRow({ row: { name: "Item {{ loop.currentItem }}", description: "Created from loop", tableId: table._id, }, }) .loop({ option: LoopStepType.STRING, binding: "Message 1,Message 2,Message 3", }) .serverLog({ text: "{{loop.currentItem}}" }) .test({ row: { name: "Trigger Row", description: "This row triggers the automation", }, id: "1234", revision: "1", }) expect(results.trigger).toBeDefined() expect(results.steps).toHaveLength(2) expect(results.steps[0].outputs.iterations).toBe(3) expect(results.steps[0].outputs.items).toHaveLength(3) results.steps[0].outputs.items.forEach( (output: CreateRowStepOutputs, index: number) => { expect(output).toMatchObject({ success: true, row: { name: `Item ${index + 1}`, description: "Created from loop", }, }) } ) expect(results.steps[1].outputs.iterations).toBe(3) expect(results.steps[1].outputs.items).toHaveLength(3) results.steps[1].outputs.items.forEach( (output: ServerLogStepOutputs, index: number) => { expect(output).toMatchObject({ success: true, }) expect(output.message).toContain(`Message ${index + 1}`) } ) }) it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => { const results = await createAutomationBuilder(config) .onAppAction() .loop({ option: LoopStepType.ARRAY, binding: [1, 2, 3], }) .serverLog({ text: "Message {{loop.currentItem}}" }) .serverLog({ text: "{{steps.1.iterations}}" }) .loop({ option: LoopStepType.ARRAY, binding: [1, 2, 3], }) .serverLog({ text: "{{loop.currentItem}}" }) .serverLog({ text: "{{steps.3.iterations}}" }) .test({ fields: {} }) // We want to ensure that bindings are corr expect(results.steps[1].outputs.message).toContain("- 3") expect(results.steps[3].outputs.message).toContain("- 3") }) it("should use automation names to loop with", async () => { const results = await createAutomationBuilder(config) .onAppAction() .loop( { option: LoopStepType.ARRAY, binding: [1, 2, 3], }, { stepName: "FirstLoopStep" } ) .serverLog( { text: "Message {{loop.currentItem}}" }, { stepName: "FirstLoopLog" } ) .serverLog( { text: "{{steps.FirstLoopLog.iterations}}" }, { stepName: "FirstLoopIterationLog" } ) .test({ fields: {} }) expect(results.steps[1].outputs.message).toContain("- 3") }) it("should run an automation with a loop and update row step", async () => { const table = await config.createTable({ name: "TestTable", type: "table", schema: { name: { name: "name", type: FieldType.STRING, constraints: { presence: true, }, }, value: { name: "value", type: FieldType.NUMBER, constraints: { presence: true, }, }, }, }) const rows = [ { name: "Row 1", value: 1, tableId: table._id }, { name: "Row 2", value: 2, tableId: table._id }, { name: "Row 3", value: 3, tableId: table._id }, ] await config.api.row.bulkImport(table._id!, { rows }) const results = await createAutomationBuilder(config) .onAppAction() .queryRows({ tableId: table._id!, }) .loop({ option: LoopStepType.ARRAY, binding: "{{ steps.1.rows }}", }) .updateRow({ rowId: "{{ loop.currentItem._id }}", row: { name: "Updated {{ loop.currentItem.name }}", value: "{{ loop.currentItem.value }}", tableId: table._id, }, meta: {}, }) .queryRows({ tableId: table._id!, }) .test({ fields: {} }) const expectedRows = [ { name: "Updated Row 1", value: 1 }, { name: "Updated Row 2", value: 2 }, { name: "Updated Row 3", value: 3 }, ] expect(results.steps[1].outputs.items).toEqual( expect.arrayContaining( expectedRows.map(row => expect.objectContaining({ success: true, row: expect.objectContaining(row), }) ) ) ) expect(results.steps[2].outputs.rows).toEqual( expect.arrayContaining( expectedRows.map(row => expect.objectContaining(row)) ) ) expect(results.steps[1].outputs.items).toHaveLength(expectedRows.length) expect(results.steps[2].outputs.rows).toHaveLength(expectedRows.length) }) it("should run an automation with a loop and update row step using stepIds", async () => { const table = await config.createTable({ name: "TestTable", type: "table", schema: { name: { name: "name", type: FieldType.STRING, constraints: { presence: true, }, }, value: { name: "value", type: FieldType.NUMBER, constraints: { presence: true, }, }, }, }) const rows = [ { name: "Row 1", value: 1, tableId: table._id }, { name: "Row 2", value: 2, tableId: table._id }, { name: "Row 3", value: 3, tableId: table._id }, ] await config.api.row.bulkImport(table._id!, { rows }) const results = await createAutomationBuilder(config) .onAppAction() .queryRows( { tableId: table._id!, }, { stepId: "abc123" } ) .loop({ option: LoopStepType.ARRAY, binding: "{{ steps.abc123.rows }}", }) .updateRow({ rowId: "{{ loop.currentItem._id }}", row: { name: "Updated {{ loop.currentItem.name }}", value: "{{ loop.currentItem.value }}", tableId: table._id, }, meta: {}, }) .queryRows({ tableId: table._id!, }) .test({ fields: {} }) const expectedRows = [ { name: "Updated Row 1", value: 1 }, { name: "Updated Row 2", value: 2 }, { name: "Updated Row 3", value: 3 }, ] expect(results.steps[1].outputs.items).toEqual( expect.arrayContaining( expectedRows.map(row => expect.objectContaining({ success: true, row: expect.objectContaining(row), }) ) ) ) expect(results.steps[2].outputs.rows).toEqual( expect.arrayContaining( expectedRows.map(row => expect.objectContaining(row)) ) ) expect(results.steps[1].outputs.items).toHaveLength(expectedRows.length) expect(results.steps[2].outputs.rows).toHaveLength(expectedRows.length) }) it("should run an automation with a loop and delete row step", async () => { const table = await config.createTable({ name: "TestTable", type: "table", schema: { name: { name: "name", type: FieldType.STRING, constraints: { presence: true, }, }, value: { name: "value", type: FieldType.NUMBER, constraints: { presence: true, }, }, }, }) const rows = [ { name: "Row 1", value: 1, tableId: table._id }, { name: "Row 2", value: 2, tableId: table._id }, { name: "Row 3", value: 3, tableId: table._id }, ] await config.api.row.bulkImport(table._id!, { rows }) const results = await createAutomationBuilder(config) .onAppAction() .queryRows({ tableId: table._id!, }) .loop({ option: LoopStepType.ARRAY, binding: "{{ steps.1.rows }}", }) .deleteRow({ tableId: table._id!, id: "{{ loop.currentItem._id }}", }) .queryRows({ tableId: table._id!, }) .test({ fields: {} }) expect(results.steps).toHaveLength(3) expect(results.steps[2].outputs.rows).toHaveLength(0) }) describe("loop output", () => { it("should not output anything if a filter stops the automation", async () => { const results = await createAutomationBuilder(config) .onAppAction() .filter({ condition: FilterCondition.EQUAL, field: "1", value: "2", }) .loop({ option: LoopStepType.ARRAY, binding: [1, 2, 3], }) .serverLog({ text: "Message {{loop.currentItem}}" }) .test({ fields: {} }) expect(results.steps.length).toBe(1) expect(results.steps[0].outputs).toEqual({ comparisonValue: 2, refValue: 1, result: false, success: true, status: "stopped", }) }) it("should not fail if queryRows returns nothing", async () => { const table = await config.api.table.save(basicTable()) const results = await createAutomationBuilder(config) .onAppAction() .queryRows({ tableId: table._id!, }) .loop({ option: LoopStepType.ARRAY, binding: "{{ steps.1.rows }}", }) .serverLog({ text: "Message {{loop.currentItem}}" }) .test({ fields: {} }) expect(results.steps[1].outputs.success).toBe(true) expect(results.steps[1].outputs.status).toBe( AutomationStepStatus.NO_ITERATIONS ) }) }) })