Merge branch 'master' into feature/pre-empt-data-source-deletion

This commit is contained in:
Michael Drury 2025-02-06 17:46:58 +00:00 committed by GitHub
commit 5be6141dce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 330 additions and 326 deletions

View File

@ -108,7 +108,7 @@ describe("/automations", () => {
it("Should ensure you can't have a branch as not a last step", async () => { it("Should ensure you can't have a branch as not a last step", async () => {
const automation = createAutomationBuilder(config) const automation = createAutomationBuilder(config)
.appAction({ fields: { status: "active" } }) .onAppAction()
.branch({ .branch({
activeBranch: { activeBranch: {
steps: stepBuilder => steps: stepBuilder =>
@ -132,7 +132,7 @@ describe("/automations", () => {
it("Should check validation on an automation that has a branch step with no children", async () => { it("Should check validation on an automation that has a branch step with no children", async () => {
const automation = createAutomationBuilder(config) const automation = createAutomationBuilder(config)
.appAction({ fields: { status: "active" } }) .onAppAction()
.branch({}) .branch({})
.serverLog({ text: "Inactive user" }) .serverLog({ text: "Inactive user" })
.build() .build()
@ -148,7 +148,7 @@ describe("/automations", () => {
it("Should check validation on a branch step with empty conditions", async () => { it("Should check validation on a branch step with empty conditions", async () => {
const automation = createAutomationBuilder(config) const automation = createAutomationBuilder(config)
.appAction({ fields: { status: "active" } }) .onAppAction()
.branch({ .branch({
activeBranch: { activeBranch: {
steps: stepBuilder => steps: stepBuilder =>
@ -169,7 +169,7 @@ describe("/automations", () => {
it("Should check validation on an branch that has a condition that is not valid", async () => { it("Should check validation on an branch that has a condition that is not valid", async () => {
const automation = createAutomationBuilder(config) const automation = createAutomationBuilder(config)
.appAction({ fields: { status: "active" } }) .onAppAction()
.branch({ .branch({
activeBranch: { activeBranch: {
steps: stepBuilder => steps: stepBuilder =>
@ -241,6 +241,7 @@ describe("/automations", () => {
it("should be able to access platformUrl, logoUrl and company in the automation", async () => { it("should be able to access platformUrl, logoUrl and company in the automation", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.serverLog({ .serverLog({
text: "{{ settings.url }}", text: "{{ settings.url }}",
}) })
@ -250,7 +251,7 @@ describe("/automations", () => {
.serverLog({ .serverLog({
text: "{{ settings.company }}", text: "{{ settings.company }}",
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.message).toEndWith("https://example.com") expect(result.steps[0].outputs.message).toEndWith("https://example.com")
expect(result.steps[1].outputs.message).toEndWith( expect(result.steps[1].outputs.message).toEndWith(

View File

@ -25,6 +25,7 @@ describe("Branching automations", () => {
const branch2Id = "44444444-4444-4444-4444-444444444444" const branch2Id = "44444444-4444-4444-4444-444444444444"
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.serverLog( .serverLog(
{ text: "Starting automation" }, { text: "Starting automation" },
{ stepName: "FirstLog", stepId: firstLogId } { stepName: "FirstLog", stepId: firstLogId }
@ -75,7 +76,7 @@ describe("Branching automations", () => {
}, },
}, },
}) })
.run() .test({ fields: {} })
expect(results.steps[3].outputs.status).toContain("branch1 branch taken") expect(results.steps[3].outputs.status).toContain("branch1 branch taken")
expect(results.steps[4].outputs.message).toContain("Branch 1.1") expect(results.steps[4].outputs.message).toContain("Branch 1.1")
@ -83,7 +84,7 @@ describe("Branching automations", () => {
it("should execute correct branch based on string equality", async () => { it("should execute correct branch based on string equality", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.appAction({ fields: { status: "active" } }) .onAppAction()
.branch({ .branch({
activeBranch: { activeBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Active user" }), steps: stepBuilder => stepBuilder.serverLog({ text: "Active user" }),
@ -99,7 +100,7 @@ describe("Branching automations", () => {
}, },
}, },
}) })
.run() .test({ fields: { status: "active" } })
expect(results.steps[0].outputs.status).toContain( expect(results.steps[0].outputs.status).toContain(
"activeBranch branch taken" "activeBranch branch taken"
) )
@ -108,7 +109,7 @@ describe("Branching automations", () => {
it("should handle multiple conditions with AND operator", async () => { it("should handle multiple conditions with AND operator", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.appAction({ fields: { status: "active", role: "admin" } }) .onAppAction()
.branch({ .branch({
activeAdminBranch: { activeAdminBranch: {
steps: stepBuilder => steps: stepBuilder =>
@ -129,14 +130,14 @@ describe("Branching automations", () => {
}, },
}, },
}) })
.run() .test({ fields: { status: "active", role: "admin" } })
expect(results.steps[1].outputs.message).toContain("Active admin user") expect(results.steps[1].outputs.message).toContain("Active admin user")
}) })
it("should handle multiple conditions with OR operator", async () => { it("should handle multiple conditions with OR operator", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.appAction({ fields: { status: "test", role: "user" } }) .onAppAction()
.branch({ .branch({
specialBranch: { specialBranch: {
steps: stepBuilder => stepBuilder.serverLog({ text: "Special user" }), steps: stepBuilder => stepBuilder.serverLog({ text: "Special user" }),
@ -161,14 +162,14 @@ describe("Branching automations", () => {
}, },
}, },
}) })
.run() .test({ fields: { status: "test", role: "user" } })
expect(results.steps[1].outputs.message).toContain("Special user") expect(results.steps[1].outputs.message).toContain("Special user")
}) })
it("should stop the branch automation when no conditions are met", async () => { it("should stop the branch automation when no conditions are met", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.appAction({ fields: { status: "test", role: "user" } }) .onAppAction()
.createRow({ row: { name: "Test", tableId: table._id } }) .createRow({ row: { name: "Test", tableId: table._id } })
.branch({ .branch({
specialBranch: { specialBranch: {
@ -194,7 +195,7 @@ describe("Branching automations", () => {
}, },
}, },
}) })
.run() .test({ fields: { status: "test", role: "user" } })
expect(results.steps[1].outputs.status).toEqual( expect(results.steps[1].outputs.status).toEqual(
AutomationStatus.NO_CONDITION_MET AutomationStatus.NO_CONDITION_MET
@ -204,7 +205,7 @@ describe("Branching automations", () => {
it("evaluate multiple conditions", async () => { it("evaluate multiple conditions", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.appAction({ fields: { test_trigger: true } }) .onAppAction()
.serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" }) .serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" })
.branch({ .branch({
specialBranch: { specialBranch: {
@ -238,14 +239,14 @@ describe("Branching automations", () => {
}, },
}, },
}) })
.run() .test({ fields: { test_trigger: true } })
expect(results.steps[2].outputs.message).toContain("Special user") expect(results.steps[2].outputs.message).toContain("Special user")
}) })
it("evaluate multiple conditions with interpolated text", async () => { it("evaluate multiple conditions with interpolated text", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.appAction({ fields: { test_trigger: true } }) .onAppAction()
.serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" }) .serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" })
.branch({ .branch({
specialBranch: { specialBranch: {
@ -275,7 +276,7 @@ describe("Branching automations", () => {
}, },
}, },
}) })
.run() .test({ fields: { test_trigger: true } })
expect(results.steps[2].outputs.message).toContain("Special user") expect(results.steps[2].outputs.message).toContain("Special user")
}) })

View File

@ -30,13 +30,7 @@ describe("Automation Scenarios", () => {
const table = await config.api.table.save(basicTable()) const table = await config.api.table.save(basicTable())
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.rowUpdated( .onRowUpdated({ tableId: table._id! })
{ tableId: table._id! },
{
row: { name: "Test", description: "TEST" },
id: "1234",
}
)
.createRow({ .createRow({
row: { row: {
name: "{{trigger.row.name}}", name: "{{trigger.row.name}}",
@ -44,7 +38,10 @@ describe("Automation Scenarios", () => {
tableId: table._id, tableId: table._id,
}, },
}) })
.run() .test({
row: { name: "Test", description: "TEST" },
id: "1234",
})
expect(results.steps).toHaveLength(1) expect(results.steps).toHaveLength(1)
@ -66,10 +63,11 @@ describe("Automation Scenarios", () => {
await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row)
await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row)
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
}) })
.run() .test({ fields: {} })
expect(results.steps).toHaveLength(1) expect(results.steps).toHaveLength(1)
expect(results.steps[0].outputs.rows).toHaveLength(2) expect(results.steps[0].outputs.rows).toHaveLength(2)
@ -84,6 +82,7 @@ describe("Automation Scenarios", () => {
await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row)
await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row)
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
}) })
@ -94,7 +93,7 @@ describe("Automation Scenarios", () => {
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
}) })
.run() .test({ fields: {} })
expect(results.steps).toHaveLength(3) expect(results.steps).toHaveLength(3)
expect(results.steps[1].outputs.success).toBeTruthy() expect(results.steps[1].outputs.success).toBeTruthy()
@ -125,6 +124,7 @@ describe("Automation Scenarios", () => {
}) })
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.createRow( .createRow(
{ {
row: { row: {
@ -153,7 +153,7 @@ describe("Automation Scenarios", () => {
}, },
{ stepName: "QueryRowsStep" } { stepName: "QueryRowsStep" }
) )
.run() .test({ fields: {} })
expect(results.steps).toHaveLength(3) expect(results.steps).toHaveLength(3)
@ -193,6 +193,7 @@ describe("Automation Scenarios", () => {
await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row)
await config.api.row.save(table._id!, row) await config.api.row.save(table._id!, row)
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.queryRows( .queryRows(
{ {
tableId: table._id!, tableId: table._id!,
@ -206,7 +207,7 @@ describe("Automation Scenarios", () => {
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
}) })
.run() .test({ fields: {} })
expect(results.steps).toHaveLength(3) expect(results.steps).toHaveLength(3)
expect(results.steps[1].outputs.success).toBeTruthy() expect(results.steps[1].outputs.success).toBeTruthy()
@ -242,6 +243,7 @@ describe("Automation Scenarios", () => {
it("should stop an automation if the condition is not met", async () => { it("should stop an automation if the condition is not met", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.createRow({ .createRow({
row: { row: {
name: "Equal Test", name: "Equal Test",
@ -258,7 +260,7 @@ describe("Automation Scenarios", () => {
value: 20, value: 20,
}) })
.serverLog({ text: "Equal condition met" }) .serverLog({ text: "Equal condition met" })
.run() .test({ fields: {} })
expect(results.steps[2].outputs.success).toBeTrue() expect(results.steps[2].outputs.success).toBeTrue()
expect(results.steps[2].outputs.result).toBeFalse() expect(results.steps[2].outputs.result).toBeFalse()
@ -267,6 +269,7 @@ describe("Automation Scenarios", () => {
it("should continue the automation if the condition is met", async () => { it("should continue the automation if the condition is met", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.createRow({ .createRow({
row: { row: {
name: "Not Equal Test", name: "Not Equal Test",
@ -283,7 +286,7 @@ describe("Automation Scenarios", () => {
value: 20, value: 20,
}) })
.serverLog({ text: "Not Equal condition met" }) .serverLog({ text: "Not Equal condition met" })
.run() .test({ fields: {} })
expect(results.steps[2].outputs.success).toBeTrue() expect(results.steps[2].outputs.success).toBeTrue()
expect(results.steps[2].outputs.result).toBeTrue() expect(results.steps[2].outputs.result).toBeTrue()
@ -333,6 +336,7 @@ describe("Automation Scenarios", () => {
"should pass the filter when condition is $condition", "should pass the filter when condition is $condition",
async ({ condition, value, rowValue, expectPass }) => { async ({ condition, value, rowValue, expectPass }) => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.createRow({ .createRow({
row: { row: {
name: `${condition} Test`, name: `${condition} Test`,
@ -351,7 +355,7 @@ describe("Automation Scenarios", () => {
.serverLog({ .serverLog({
text: `${condition} condition ${expectPass ? "passed" : "failed"}`, text: `${condition} condition ${expectPass ? "passed" : "failed"}`,
}) })
.run() .test({ fields: {} })
expect(results.steps[2].outputs.result).toBe(expectPass) expect(results.steps[2].outputs.result).toBe(expectPass)
if (expectPass) { if (expectPass) {
@ -367,23 +371,21 @@ describe("Automation Scenarios", () => {
const table = await config.api.table.save(basicTable()) const table = await config.api.table.save(basicTable())
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.rowUpdated( .onRowUpdated({ tableId: table._id! })
{ tableId: table._id! },
{
row: { name: "Test", description: "TEST" },
id: "1234",
}
)
.serverLog({ text: "{{ [user].[email] }}" }) .serverLog({ text: "{{ [user].[email] }}" })
.run() .test({
row: { name: "Test", description: "TEST" },
id: "1234",
})
expect(results.steps[0].outputs.message).toContain("example.com") expect(results.steps[0].outputs.message).toContain("example.com")
}) })
it("Check user is passed through from app trigger", async () => { it("Check user is passed through from app trigger", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.serverLog({ text: "{{ [user].[email] }}" }) .serverLog({ text: "{{ [user].[email] }}" })
.run() .test({ fields: {} })
expect(results.steps[0].outputs.message).toContain("example.com") expect(results.steps[0].outputs.message).toContain("example.com")
}) })
@ -453,9 +455,7 @@ if (descriptions.length) {
}) })
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.appAction({ .onAppAction()
fields: {},
})
.executeQuery({ .executeQuery({
query: { query: {
queryId: query._id!, queryId: query._id!,
@ -475,7 +475,7 @@ if (descriptions.length) {
.queryRows({ .queryRows({
tableId: newTable._id!, tableId: newTable._id!,
}) })
.run() .test({ fields: {} })
expect(results.steps).toHaveLength(3) expect(results.steps).toHaveLength(3)

View File

@ -25,7 +25,7 @@ describe("Execute Bash Automations", () => {
it("should use trigger data in bash command and pass output to subsequent steps", async () => { it("should use trigger data in bash command and pass output to subsequent steps", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.appAction({ fields: { command: "hello world" } }) .onAppAction()
.bash( .bash(
{ code: "echo '{{ trigger.fields.command }}'" }, { code: "echo '{{ trigger.fields.command }}'" },
{ stepName: "Echo Command" } { stepName: "Echo Command" }
@ -34,7 +34,7 @@ describe("Execute Bash Automations", () => {
{ text: "Bash output was: {{ steps.[Echo Command].stdout }}" }, { text: "Bash output was: {{ steps.[Echo Command].stdout }}" },
{ stepName: "Log Output" } { stepName: "Log Output" }
) )
.run() .test({ fields: { command: "hello world" } })
expect(result.steps[0].outputs.stdout).toEqual("hello world\n") expect(result.steps[0].outputs.stdout).toEqual("hello world\n")
expect(result.steps[1].outputs.message).toContain( expect(result.steps[1].outputs.message).toContain(
@ -44,7 +44,7 @@ describe("Execute Bash Automations", () => {
it("should chain multiple bash commands using previous outputs", async () => { it("should chain multiple bash commands using previous outputs", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.appAction({ fields: { filename: "testfile.txt" } }) .onAppAction()
.bash( .bash(
{ code: "echo 'initial content' > {{ trigger.fields.filename }}" }, { code: "echo 'initial content' > {{ trigger.fields.filename }}" },
{ stepName: "Create File" } { stepName: "Create File" }
@ -57,7 +57,7 @@ describe("Execute Bash Automations", () => {
{ code: "rm {{ trigger.fields.filename }}" }, { code: "rm {{ trigger.fields.filename }}" },
{ stepName: "Cleanup" } { stepName: "Cleanup" }
) )
.run() .test({ fields: { filename: "testfile.txt" } })
expect(result.steps[1].outputs.stdout).toEqual("INITIAL CONTENT\n") expect(result.steps[1].outputs.stdout).toEqual("INITIAL CONTENT\n")
expect(result.steps[1].outputs.success).toEqual(true) expect(result.steps[1].outputs.success).toEqual(true)
@ -65,6 +65,7 @@ describe("Execute Bash Automations", () => {
it("should integrate bash output with row operations", async () => { it("should integrate bash output with row operations", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.queryRows( .queryRows(
{ {
tableId: table._id!, tableId: table._id!,
@ -82,7 +83,7 @@ describe("Execute Bash Automations", () => {
{ text: "{{ steps.[Process Row Data].stdout }}" }, { text: "{{ steps.[Process Row Data].stdout }}" },
{ stepName: "Log Result" } { stepName: "Log Result" }
) )
.run() .test({ fields: {} })
expect(result.steps[1].outputs.stdout).toContain( expect(result.steps[1].outputs.stdout).toContain(
"Row data: test row - test description" "Row data: test row - test description"
@ -94,7 +95,7 @@ describe("Execute Bash Automations", () => {
it("should handle bash output in conditional logic", async () => { it("should handle bash output in conditional logic", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.appAction({ fields: { threshold: "5" } }) .onAppAction()
.bash( .bash(
{ code: "echo $(( {{ trigger.fields.threshold }} + 5 ))" }, { code: "echo $(( {{ trigger.fields.threshold }} + 5 ))" },
{ stepName: "Calculate Value" } { stepName: "Calculate Value" }
@ -112,7 +113,7 @@ describe("Execute Bash Automations", () => {
{ text: "Value was {{ steps.[Check Value].value }}" }, { text: "Value was {{ steps.[Check Value].value }}" },
{ stepName: "Log Result" } { stepName: "Log Result" }
) )
.run() .test({ fields: { threshold: "5" } })
expect(result.steps[0].outputs.stdout).toEqual("10\n") expect(result.steps[0].outputs.stdout).toEqual("10\n")
expect(result.steps[1].outputs.value).toEqual("high") expect(result.steps[1].outputs.value).toEqual("high")
@ -121,12 +122,13 @@ describe("Execute Bash Automations", () => {
it("should handle null values gracefully", async () => { it("should handle null values gracefully", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.bash( .bash(
// @ts-expect-error - testing null input // @ts-expect-error - testing null input
{ code: null }, { code: null },
{ stepName: "Null Command" } { stepName: "Null Command" }
) )
.run() .test({ fields: {} })
expect(result.steps[0].outputs.stdout).toBe( expect(result.steps[0].outputs.stdout).toBe(
"Budibase bash automation failed: Invalid inputs" "Budibase bash automation failed: Invalid inputs"

View File

@ -41,14 +41,14 @@ describe("test the create row action", () => {
it("should be able to run the action", async () => { it("should be able to run the action", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.appAction({ fields: { status: "new" } }) .onAppAction()
.serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" }) .serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" })
.createRow({ row }, { stepName: "CreateRow" }) .createRow({ row }, { stepName: "CreateRow" })
.serverLog( .serverLog(
{ text: "Row created with ID: {{ stepsByName.CreateRow.row._id }}" }, { text: "Row created with ID: {{ stepsByName.CreateRow.row._id }}" },
{ stepName: "CreationLog" } { stepName: "CreationLog" }
) )
.run() .test({ fields: { status: "new" } })
expect(result.steps[1].outputs.success).toBeDefined() expect(result.steps[1].outputs.success).toBeDefined()
expect(result.steps[1].outputs.id).toBeDefined() expect(result.steps[1].outputs.id).toBeDefined()
@ -67,7 +67,7 @@ describe("test the create row action", () => {
it("should return an error (not throw) when bad info provided", async () => { it("should return an error (not throw) when bad info provided", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.appAction({ fields: { status: "error" } }) .onAppAction()
.serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" }) .serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" })
.createRow( .createRow(
{ {
@ -78,14 +78,14 @@ describe("test the create row action", () => {
}, },
{ stepName: "CreateRow" } { stepName: "CreateRow" }
) )
.run() .test({ fields: { status: "error" } })
expect(result.steps[1].outputs.success).toEqual(false) expect(result.steps[1].outputs.success).toEqual(false)
}) })
it("should check invalid inputs return an error", async () => { it("should check invalid inputs return an error", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.appAction({ fields: { status: "invalid" } }) .onAppAction()
.serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" }) .serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" })
.createRow({ row: {} }, { stepName: "CreateRow" }) .createRow({ row: {} }, { stepName: "CreateRow" })
.filter({ .filter({
@ -97,7 +97,7 @@ describe("test the create row action", () => {
{ text: "This log should not appear" }, { text: "This log should not appear" },
{ stepName: "SkippedLog" } { stepName: "SkippedLog" }
) )
.run() .test({ fields: { status: "invalid" } })
expect(result.steps[1].outputs.success).toEqual(false) expect(result.steps[1].outputs.success).toEqual(false)
expect(result.steps.length).toBeLessThan(4) expect(result.steps.length).toBeLessThan(4)
@ -123,7 +123,7 @@ describe("test the create row action", () => {
attachmentRow.file_attachment = attachmentObject attachmentRow.file_attachment = attachmentObject
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.appAction({ fields: { type: "attachment" } }) .onAppAction()
.serverLog( .serverLog(
{ text: "Processing attachment upload" }, { text: "Processing attachment upload" },
{ stepName: "StartLog" } { stepName: "StartLog" }
@ -140,7 +140,7 @@ describe("test the create row action", () => {
}, },
{ stepName: "UploadLog" } { stepName: "UploadLog" }
) )
.run() .test({ fields: { type: "attachment" } })
expect(result.steps[1].outputs.success).toEqual(true) expect(result.steps[1].outputs.success).toEqual(true)
expect(result.steps[1].outputs.row.file_attachment[0]).toHaveProperty("key") expect(result.steps[1].outputs.row.file_attachment[0]).toHaveProperty("key")
@ -174,7 +174,7 @@ describe("test the create row action", () => {
attachmentRow.single_file_attachment = attachmentObject attachmentRow.single_file_attachment = attachmentObject
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.appAction({ fields: { type: "single-attachment" } }) .onAppAction()
.serverLog( .serverLog(
{ text: "Processing single attachment" }, { text: "Processing single attachment" },
{ stepName: "StartLog" } { stepName: "StartLog" }
@ -209,7 +209,7 @@ describe("test the create row action", () => {
}, },
}, },
}) })
.run() .test({ fields: { type: "single-attachment" } })
expect(result.steps[1].outputs.success).toEqual(true) expect(result.steps[1].outputs.success).toEqual(true)
expect(result.steps[1].outputs.row.single_file_attachment).toHaveProperty( expect(result.steps[1].outputs.row.single_file_attachment).toHaveProperty(
@ -245,7 +245,7 @@ describe("test the create row action", () => {
attachmentRow.single_file_attachment = attachmentObject attachmentRow.single_file_attachment = attachmentObject
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.appAction({ fields: { type: "invalid-attachment" } }) .onAppAction()
.serverLog( .serverLog(
{ text: "Testing invalid attachment keys" }, { text: "Testing invalid attachment keys" },
{ stepName: "StartLog" } { stepName: "StartLog" }
@ -278,7 +278,7 @@ describe("test the create row action", () => {
}, },
}, },
}) })
.run() .test({ fields: { type: "invalid-attachment" } })
expect(result.steps[1].outputs.success).toEqual(false) expect(result.steps[1].outputs.success).toEqual(false)
expect(result.steps[1].outputs.response).toEqual( expect(result.steps[1].outputs.response).toEqual(

View File

@ -27,7 +27,7 @@ describe("cron automations", () => {
}) })
it("should initialise the automation timestamp", async () => { it("should initialise the automation timestamp", async () => {
await createAutomationBuilder(config).cron({ cron: "* * * * *" }).save() await createAutomationBuilder(config).onCron({ cron: "* * * * *" }).save()
tk.travel(Date.now() + oneMinuteInMs) tk.travel(Date.now() + oneMinuteInMs)
await config.publish() await config.publish()

View File

@ -16,7 +16,10 @@ describe("test the delay logic", () => {
const time = 100 const time = 100
const before = performance.now() const before = performance.now()
await createAutomationBuilder(config).delay({ time }).run() await createAutomationBuilder(config)
.onAppAction()
.delay({ time })
.test({ fields: {} })
const now = performance.now() const now = performance.now()

View File

@ -21,12 +21,13 @@ describe("test the delete row action", () => {
it("should be able to run the delete row action", async () => { it("should be able to run the delete row action", async () => {
await createAutomationBuilder(config) await createAutomationBuilder(config)
.onAppAction()
.deleteRow({ .deleteRow({
tableId: table._id!, tableId: table._id!,
id: row._id!, id: row._id!,
revision: row._rev, revision: row._rev,
}) })
.run() .test({ fields: {} })
await config.api.row.get(table._id!, row._id!, { await config.api.row.get(table._id!, row._id!, {
status: 404, status: 404,
@ -35,20 +36,22 @@ describe("test the delete row action", () => {
it("should check invalid inputs return an error", async () => { it("should check invalid inputs return an error", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.deleteRow({ tableId: "", id: "", revision: "" }) .deleteRow({ tableId: "", id: "", revision: "" })
.run() .test({ fields: {} })
expect(results.steps[0].outputs.success).toEqual(false) expect(results.steps[0].outputs.success).toEqual(false)
}) })
it("should return an error when table doesn't exist", async () => { it("should return an error when table doesn't exist", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.deleteRow({ .deleteRow({
tableId: "invalid", tableId: "invalid",
id: "invalid", id: "invalid",
revision: "invalid", revision: "invalid",
}) })
.run() .test({ fields: {} })
expect(results.steps[0].outputs.success).toEqual(false) expect(results.steps[0].outputs.success).toEqual(false)
}) })

View File

@ -20,12 +20,13 @@ describe("test the outgoing webhook action", () => {
it("should be able to run the action", async () => { it("should be able to run the action", async () => {
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.discord({ .discord({
url: "http://www.example.com", url: "http://www.example.com",
username: "joe_bloggs", username: "joe_bloggs",
content: "Hello, world", content: "Hello, world",
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.response.foo).toEqual("bar") expect(result.steps[0].outputs.response.foo).toEqual("bar")
expect(result.steps[0].outputs.success).toEqual(true) expect(result.steps[0].outputs.success).toEqual(true)
}) })

View File

@ -21,30 +21,32 @@ describe("Execute Script Automations", () => {
it("should execute a basic script and return the result", async () => { it("should execute a basic script and return the result", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.executeScript({ code: "return 2 + 2" }) .executeScript({ code: "return 2 + 2" })
.run() .test({ fields: {} })
expect(results.steps[0].outputs.value).toEqual(4) expect(results.steps[0].outputs.value).toEqual(4)
}) })
it("should access bindings from previous steps", async () => { it("should access bindings from previous steps", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.appAction({ fields: { data: [1, 2, 3] } }) .onAppAction()
.executeScript( .executeScript(
{ {
code: "return trigger.fields.data.map(x => x * 2)", code: "return trigger.fields.data.map(x => x * 2)",
}, },
{ stepId: "binding-script-step" } { stepId: "binding-script-step" }
) )
.run() .test({ fields: { data: [1, 2, 3] } })
expect(results.steps[0].outputs.value).toEqual([2, 4, 6]) expect(results.steps[0].outputs.value).toEqual([2, 4, 6])
}) })
it("should handle script execution errors gracefully", async () => { it("should handle script execution errors gracefully", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.executeScript({ code: "return nonexistentVariable.map(x => x)" }) .executeScript({ code: "return nonexistentVariable.map(x => x)" })
.run() .test({ fields: {} })
expect(results.steps[0].outputs.response).toContain( expect(results.steps[0].outputs.response).toContain(
"ReferenceError: nonexistentVariable is not defined" "ReferenceError: nonexistentVariable is not defined"
@ -54,7 +56,7 @@ describe("Execute Script Automations", () => {
it("should handle conditional logic in scripts", async () => { it("should handle conditional logic in scripts", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.appAction({ fields: { value: 10 } }) .onAppAction()
.executeScript({ .executeScript({
code: ` code: `
if (trigger.fields.value > 5) { if (trigger.fields.value > 5) {
@ -64,14 +66,14 @@ describe("Execute Script Automations", () => {
} }
`, `,
}) })
.run() .test({ fields: { value: 10 } })
expect(results.steps[0].outputs.value).toEqual("Value is greater than 5") expect(results.steps[0].outputs.value).toEqual("Value is greater than 5")
}) })
it("should use multiple steps and validate script execution", async () => { it("should use multiple steps and validate script execution", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.appAction({ fields: {} }) .onAppAction()
.serverLog( .serverLog(
{ text: "Starting multi-step automation" }, { text: "Starting multi-step automation" },
{ stepId: "start-log-step" } { stepId: "start-log-step" }
@ -92,7 +94,7 @@ describe("Execute Script Automations", () => {
.serverLog({ .serverLog({
text: `Final result is {{ steps.ScriptingStep1.value }}`, text: `Final result is {{ steps.ScriptingStep1.value }}`,
}) })
.run() .test({ fields: {} })
expect(results.steps[0].outputs.message).toContain( expect(results.steps[0].outputs.message).toContain(
"Starting multi-step automation" "Starting multi-step automation"

View File

@ -43,8 +43,9 @@ describe("test the filter logic", () => {
] ]
it.each(pass)("should pass %p %p %p", async (field, condition, value) => { it.each(pass)("should pass %p %p %p", async (field, condition, value) => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.filter({ field, condition: stringToFilterCondition(condition), value }) .filter({ field, condition: stringToFilterCondition(condition), value })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.result).toEqual(true) expect(result.steps[0].outputs.result).toEqual(true)
expect(result.steps[0].outputs.success).toEqual(true) expect(result.steps[0].outputs.success).toEqual(true)
@ -60,8 +61,9 @@ describe("test the filter logic", () => {
] ]
it.each(fail)("should fail %p %p %p", async (field, condition, value) => { it.each(fail)("should fail %p %p %p", async (field, condition, value) => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.filter({ field, condition: stringToFilterCondition(condition), value }) .filter({ field, condition: stringToFilterCondition(condition), value })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.result).toEqual(false) expect(result.steps[0].outputs.result).toEqual(false)
expect(result.steps[0].outputs.success).toEqual(true) expect(result.steps[0].outputs.success).toEqual(true)

View File

@ -73,17 +73,7 @@ describe("Attempt to run a basic loop automation", () => {
it("should run an automation with a trigger, loop, and create row step", async () => { it("should run an automation with a trigger, loop, and create row step", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.rowSaved( .onRowSaved({ tableId: table._id! })
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.loop({ .loop({
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
binding: [1, 2, 3], binding: [1, 2, 3],
@ -95,7 +85,14 @@ describe("Attempt to run a basic loop automation", () => {
tableId: table._id, tableId: table._id,
}, },
}) })
.run() .test({
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
})
expect(results.trigger).toBeDefined() expect(results.trigger).toBeDefined()
expect(results.steps).toHaveLength(1) expect(results.steps).toHaveLength(1)
@ -116,17 +113,7 @@ describe("Attempt to run a basic loop automation", () => {
it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => { it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.rowSaved( .onRowSaved({ tableId: table._id! })
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
}) })
@ -136,7 +123,14 @@ describe("Attempt to run a basic loop automation", () => {
}) })
.serverLog({ text: "Message {{loop.currentItem}}" }) .serverLog({ text: "Message {{loop.currentItem}}" })
.serverLog({ text: "{{steps.1.rows.0._id}}" }) .serverLog({ text: "{{steps.1.rows.0._id}}" })
.run() .test({
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
})
results.steps[1].outputs.items.forEach( results.steps[1].outputs.items.forEach(
(output: ServerLogStepOutputs, index: number) => { (output: ServerLogStepOutputs, index: number) => {
@ -152,12 +146,13 @@ describe("Attempt to run a basic loop automation", () => {
it("if an incorrect type is passed to the loop it should return an error", async () => { it("if an incorrect type is passed to the loop it should return an error", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.loop({ .loop({
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
binding: "1, 2, 3", binding: "1, 2, 3",
}) })
.serverLog({ text: "Message {{loop.currentItem}}" }) .serverLog({ text: "Message {{loop.currentItem}}" })
.run() .test({ fields: {} })
expect(results.steps[0].outputs).toEqual({ expect(results.steps[0].outputs).toEqual({
success: false, success: false,
@ -167,13 +162,14 @@ describe("Attempt to run a basic loop automation", () => {
it("ensure the loop stops if the failure condition is reached", async () => { it("ensure the loop stops if the failure condition is reached", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.loop({ .loop({
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
binding: ["test", "test2", "test3"], binding: ["test", "test2", "test3"],
failure: "test2", failure: "test2",
}) })
.serverLog({ text: "Message {{loop.currentItem}}" }) .serverLog({ text: "Message {{loop.currentItem}}" })
.run() .test({ fields: {} })
expect(results.steps[0].outputs).toEqual( expect(results.steps[0].outputs).toEqual(
expect.objectContaining({ expect.objectContaining({
@ -185,6 +181,7 @@ describe("Attempt to run a basic loop automation", () => {
it("ensure the loop stops if the max iterations are reached", async () => { it("ensure the loop stops if the max iterations are reached", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.loop({ .loop({
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
binding: ["test", "test2", "test3"], binding: ["test", "test2", "test3"],
@ -192,13 +189,14 @@ describe("Attempt to run a basic loop automation", () => {
}) })
.serverLog({ text: "{{loop.currentItem}}" }) .serverLog({ text: "{{loop.currentItem}}" })
.serverLog({ text: "{{steps.1.iterations}}" }) .serverLog({ text: "{{steps.1.iterations}}" })
.run() .test({ fields: {} })
expect(results.steps[0].outputs.iterations).toBe(2) expect(results.steps[0].outputs.iterations).toBe(2)
}) })
it("should run an automation with loop and max iterations to ensure context correctness further down the tree", async () => { it("should run an automation with loop and max iterations to ensure context correctness further down the tree", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.loop({ .loop({
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
binding: ["test", "test2", "test3"], binding: ["test", "test2", "test3"],
@ -206,24 +204,14 @@ describe("Attempt to run a basic loop automation", () => {
}) })
.serverLog({ text: "{{loop.currentItem}}" }) .serverLog({ text: "{{loop.currentItem}}" })
.serverLog({ text: "{{steps.1.iterations}}" }) .serverLog({ text: "{{steps.1.iterations}}" })
.run() .test({ fields: {} })
expect(results.steps[1].outputs.message).toContain("- 2") expect(results.steps[1].outputs.message).toContain("- 2")
}) })
it("should run an automation where a loop is successfully run twice", async () => { it("should run an automation where a loop is successfully run twice", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.rowSaved( .onRowSaved({ tableId: table._id! })
{ tableId: table._id! },
{
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
}
)
.loop({ .loop({
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
binding: [1, 2, 3], binding: [1, 2, 3],
@ -240,7 +228,14 @@ describe("Attempt to run a basic loop automation", () => {
binding: "Message 1,Message 2,Message 3", binding: "Message 1,Message 2,Message 3",
}) })
.serverLog({ text: "{{loop.currentItem}}" }) .serverLog({ text: "{{loop.currentItem}}" })
.run() .test({
row: {
name: "Trigger Row",
description: "This row triggers the automation",
},
id: "1234",
revision: "1",
})
expect(results.trigger).toBeDefined() expect(results.trigger).toBeDefined()
expect(results.steps).toHaveLength(2) expect(results.steps).toHaveLength(2)
@ -275,6 +270,7 @@ describe("Attempt to run a basic loop automation", () => {
it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => { 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) const results = await createAutomationBuilder(config)
.onAppAction()
.loop({ .loop({
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
binding: [1, 2, 3], binding: [1, 2, 3],
@ -287,7 +283,7 @@ describe("Attempt to run a basic loop automation", () => {
}) })
.serverLog({ text: "{{loop.currentItem}}" }) .serverLog({ text: "{{loop.currentItem}}" })
.serverLog({ text: "{{steps.3.iterations}}" }) .serverLog({ text: "{{steps.3.iterations}}" })
.run() .test({ fields: {} })
// We want to ensure that bindings are corr // We want to ensure that bindings are corr
expect(results.steps[1].outputs.message).toContain("- 3") expect(results.steps[1].outputs.message).toContain("- 3")
@ -296,6 +292,7 @@ describe("Attempt to run a basic loop automation", () => {
it("should use automation names to loop with", async () => { it("should use automation names to loop with", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.loop( .loop(
{ {
option: LoopStepType.ARRAY, option: LoopStepType.ARRAY,
@ -311,7 +308,7 @@ describe("Attempt to run a basic loop automation", () => {
{ text: "{{steps.FirstLoopLog.iterations}}" }, { text: "{{steps.FirstLoopLog.iterations}}" },
{ stepName: "FirstLoopIterationLog" } { stepName: "FirstLoopIterationLog" }
) )
.run() .test({ fields: {} })
expect(results.steps[1].outputs.message).toContain("- 3") expect(results.steps[1].outputs.message).toContain("- 3")
}) })
@ -347,6 +344,7 @@ describe("Attempt to run a basic loop automation", () => {
await config.api.row.bulkImport(table._id!, { rows }) await config.api.row.bulkImport(table._id!, { rows })
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
}) })
@ -366,7 +364,7 @@ describe("Attempt to run a basic loop automation", () => {
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
}) })
.run() .test({ fields: {} })
const expectedRows = [ const expectedRows = [
{ name: "Updated Row 1", value: 1 }, { name: "Updated Row 1", value: 1 },
@ -426,6 +424,7 @@ describe("Attempt to run a basic loop automation", () => {
await config.api.row.bulkImport(table._id!, { rows }) await config.api.row.bulkImport(table._id!, { rows })
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.queryRows( .queryRows(
{ {
tableId: table._id!, tableId: table._id!,
@ -448,7 +447,7 @@ describe("Attempt to run a basic loop automation", () => {
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
}) })
.run() .test({ fields: {} })
const expectedRows = [ const expectedRows = [
{ name: "Updated Row 1", value: 1 }, { name: "Updated Row 1", value: 1 },
@ -508,6 +507,7 @@ describe("Attempt to run a basic loop automation", () => {
await config.api.row.bulkImport(table._id!, { rows }) await config.api.row.bulkImport(table._id!, { rows })
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
}) })
@ -522,7 +522,7 @@ describe("Attempt to run a basic loop automation", () => {
.queryRows({ .queryRows({
tableId: table._id!, tableId: table._id!,
}) })
.run() .test({ fields: {} })
expect(results.steps).toHaveLength(3) expect(results.steps).toHaveLength(3)

View File

@ -20,11 +20,12 @@ describe("test the outgoing webhook action", () => {
it("should be able to run the action", async () => { it("should be able to run the action", async () => {
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.make({ .make({
url: "http://www.example.com", url: "http://www.example.com",
body: null, body: null,
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.response.foo).toEqual("bar") expect(result.steps[0].outputs.response.foo).toEqual("bar")
expect(result.steps[0].outputs.success).toEqual(true) expect(result.steps[0].outputs.success).toEqual(true)
@ -46,11 +47,12 @@ describe("test the outgoing webhook action", () => {
.reply(200, { foo: "bar" }) .reply(200, { foo: "bar" })
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.make({ .make({
body: { value: JSON.stringify(payload) }, body: { value: JSON.stringify(payload) },
url: "http://www.example.com", url: "http://www.example.com",
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.response.foo).toEqual("bar") expect(result.steps[0].outputs.response.foo).toEqual("bar")
expect(result.steps[0].outputs.success).toEqual(true) expect(result.steps[0].outputs.success).toEqual(true)
@ -58,11 +60,12 @@ describe("test the outgoing webhook action", () => {
it("should return a 400 if the JSON payload string is malformed", async () => { it("should return a 400 if the JSON payload string is malformed", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.make({ .make({
body: { value: "{ invalid json }" }, body: { value: "{ invalid json }" },
url: "http://www.example.com", url: "http://www.example.com",
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.httpStatus).toEqual(400) expect(result.steps[0].outputs.httpStatus).toEqual(400)
expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON") expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON")

View File

@ -21,12 +21,13 @@ describe("test the outgoing webhook action", () => {
it("should be able to run the action and default to 'get'", async () => { it("should be able to run the action and default to 'get'", async () => {
nock("http://www.example.com/").get("/").reply(200, { foo: "bar" }) nock("http://www.example.com/").get("/").reply(200, { foo: "bar" })
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.n8n({ .n8n({
url: "http://www.example.com", url: "http://www.example.com",
body: { test: "IGNORE_ME" }, body: { test: "IGNORE_ME" },
authorization: "", authorization: "",
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.response).toEqual({ foo: "bar" }) expect(result.steps[0].outputs.response).toEqual({ foo: "bar" })
expect(result.steps[0].outputs.httpStatus).toEqual(200) expect(result.steps[0].outputs.httpStatus).toEqual(200)
@ -39,26 +40,28 @@ describe("test the outgoing webhook action", () => {
.reply(200) .reply(200)
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.n8n({ .n8n({
url: "http://www.example.com", url: "http://www.example.com",
body: { value: JSON.stringify({ name: "Adam", age: 9 }) }, body: { value: JSON.stringify({ name: "Adam", age: 9 }) },
method: HttpMethod.POST, method: HttpMethod.POST,
authorization: "", authorization: "",
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toEqual(true) expect(result.steps[0].outputs.success).toEqual(true)
}) })
it("should return a 400 if the JSON payload string is malformed", async () => { it("should return a 400 if the JSON payload string is malformed", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.n8n({ .n8n({
url: "http://www.example.com", url: "http://www.example.com",
body: { value: "{ value1 1 }" }, body: { value: "{ value1 1 }" },
method: HttpMethod.POST, method: HttpMethod.POST,
authorization: "", authorization: "",
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.httpStatus).toEqual(400) expect(result.steps[0].outputs.httpStatus).toEqual(400)
expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON") expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON")
@ -71,13 +74,14 @@ describe("test the outgoing webhook action", () => {
.reply(200) .reply(200)
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.n8n({ .n8n({
url: "http://www.example.com", url: "http://www.example.com",
method: HttpMethod.HEAD, method: HttpMethod.HEAD,
body: { test: "IGNORE_ME" }, body: { test: "IGNORE_ME" },
authorization: "", authorization: "",
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toEqual(true) expect(result.steps[0].outputs.success).toEqual(true)
}) })

View File

@ -58,8 +58,9 @@ describe("test the openai action", () => {
// own API key. We don't count this against your quota. // own API key. We don't count this against your quota.
const result = await expectAIUsage(0, () => const result = await expectAIUsage(0, () =>
createAutomationBuilder(config) createAutomationBuilder(config)
.onAppAction()
.openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI }) .openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI })
.run() .test({ fields: {} })
) )
expect(result.steps[0].outputs.response).toEqual("This is a test") expect(result.steps[0].outputs.response).toEqual("This is a test")
@ -69,8 +70,9 @@ describe("test the openai action", () => {
it("should present the correct error message when a prompt is not provided", async () => { it("should present the correct error message when a prompt is not provided", async () => {
const result = await expectAIUsage(0, () => const result = await expectAIUsage(0, () =>
createAutomationBuilder(config) createAutomationBuilder(config)
.onAppAction()
.openai({ prompt: "", model: Model.GPT_4O_MINI }) .openai({ prompt: "", model: Model.GPT_4O_MINI })
.run() .test({ fields: {} })
) )
expect(result.steps[0].outputs.response).toEqual( expect(result.steps[0].outputs.response).toEqual(
@ -84,8 +86,9 @@ describe("test the openai action", () => {
const result = await expectAIUsage(0, () => const result = await expectAIUsage(0, () =>
createAutomationBuilder(config) createAutomationBuilder(config)
.onAppAction()
.openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI }) .openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI })
.run() .test({ fields: {} })
) )
expect(result.steps[0].outputs.response).toEqual( expect(result.steps[0].outputs.response).toEqual(
@ -106,8 +109,9 @@ describe("test the openai action", () => {
// key, so we charge users for it. // key, so we charge users for it.
const result = await expectAIUsage(14, () => const result = await expectAIUsage(14, () =>
createAutomationBuilder(config) createAutomationBuilder(config)
.onAppAction()
.openai({ model: Model.GPT_4O_MINI, prompt: "Hello, world" }) .openai({ model: Model.GPT_4O_MINI, prompt: "Hello, world" })
.run() .test({ fields: {} })
) )
expect(result.steps[0].outputs.response).toEqual("This is a test") expect(result.steps[0].outputs.response).toEqual("This is a test")

View File

@ -24,13 +24,14 @@ describe("test the outgoing webhook action", () => {
.reply(200, { foo: "bar" }) .reply(200, { foo: "bar" })
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.outgoingWebhook({ .outgoingWebhook({
requestMethod: RequestType.POST, requestMethod: RequestType.POST,
url: "http://www.example.com", url: "http://www.example.com",
requestBody: JSON.stringify({ a: 1 }), requestBody: JSON.stringify({ a: 1 }),
headers: {}, headers: {},
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toEqual(true) expect(result.steps[0].outputs.success).toEqual(true)
expect(result.steps[0].outputs.httpStatus).toEqual(200) expect(result.steps[0].outputs.httpStatus).toEqual(200)
@ -39,13 +40,14 @@ describe("test the outgoing webhook action", () => {
it("should return an error if something goes wrong in fetch", async () => { it("should return an error if something goes wrong in fetch", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.outgoingWebhook({ .outgoingWebhook({
requestMethod: RequestType.GET, requestMethod: RequestType.GET,
url: "www.invalid.com", url: "www.invalid.com",
requestBody: "", requestBody: "",
headers: {}, headers: {},
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toEqual(false) expect(result.steps[0].outputs.success).toEqual(false)
}) })
}) })

View File

@ -29,6 +29,7 @@ describe("Test a query step automation", () => {
it("should be able to run the query step", async () => { it("should be able to run the query step", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.queryRows( .queryRows(
{ {
tableId: table._id!, tableId: table._id!,
@ -43,7 +44,7 @@ describe("Test a query step automation", () => {
}, },
{ stepName: "Query All Rows" } { stepName: "Query All Rows" }
) )
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.success).toBe(true)
expect(result.steps[0].outputs.rows).toBeDefined() expect(result.steps[0].outputs.rows).toBeDefined()
@ -53,6 +54,7 @@ describe("Test a query step automation", () => {
it("Returns all rows when onEmptyFilter has no value and no filters are passed", async () => { it("Returns all rows when onEmptyFilter has no value and no filters are passed", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.queryRows( .queryRows(
{ {
tableId: table._id!, tableId: table._id!,
@ -63,7 +65,7 @@ describe("Test a query step automation", () => {
}, },
{ stepName: "Query With Empty Filter" } { stepName: "Query With Empty Filter" }
) )
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.success).toBe(true)
expect(result.steps[0].outputs.rows).toBeDefined() expect(result.steps[0].outputs.rows).toBeDefined()
@ -73,6 +75,7 @@ describe("Test a query step automation", () => {
it("Returns no rows when onEmptyFilter is RETURN_NONE and theres no filters", async () => { it("Returns no rows when onEmptyFilter is RETURN_NONE and theres no filters", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.queryRows( .queryRows(
{ {
tableId: table._id!, tableId: table._id!,
@ -85,7 +88,7 @@ describe("Test a query step automation", () => {
}, },
{ stepName: "Query With Return None" } { stepName: "Query With Return None" }
) )
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.success).toBe(true)
expect(result.steps[0].outputs.rows).toBeDefined() expect(result.steps[0].outputs.rows).toBeDefined()
@ -94,6 +97,7 @@ describe("Test a query step automation", () => {
it("Returns no rows when onEmptyFilters RETURN_NONE and a filter is passed with a null value", async () => { it("Returns no rows when onEmptyFilters RETURN_NONE and a filter is passed with a null value", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.queryRows( .queryRows(
{ {
tableId: table._id!, tableId: table._id!,
@ -110,7 +114,7 @@ describe("Test a query step automation", () => {
}, },
{ stepName: "Query With Null Filter" } { stepName: "Query With Null Filter" }
) )
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.success).toBe(true)
expect(result.steps[0].outputs.rows).toBeDefined() expect(result.steps[0].outputs.rows).toBeDefined()
@ -119,6 +123,7 @@ describe("Test a query step automation", () => {
it("Returns rows when onEmptyFilter is RETURN_ALL and no filter is passed", async () => { it("Returns rows when onEmptyFilter is RETURN_ALL and no filter is passed", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.queryRows( .queryRows(
{ {
tableId: table._id!, tableId: table._id!,
@ -130,7 +135,7 @@ describe("Test a query step automation", () => {
}, },
{ stepName: "Query With Return All" } { stepName: "Query With Return All" }
) )
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.success).toBe(true)
expect(result.steps[0].outputs.rows).toBeDefined() expect(result.steps[0].outputs.rows).toBeDefined()
@ -146,6 +151,7 @@ describe("Test a query step automation", () => {
name: NAME, name: NAME,
}) })
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.queryRows( .queryRows(
{ {
tableId: tableWithSpaces._id!, tableId: tableWithSpaces._id!,
@ -154,7 +160,7 @@ describe("Test a query step automation", () => {
}, },
{ stepName: "Query table with spaces" } { stepName: "Query table with spaces" }
) )
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.success).toBe(true)
expect(result.steps[0].outputs.rows).toBeDefined() expect(result.steps[0].outputs.rows).toBeDefined()
expect(result.steps[0].outputs.rows.length).toBe(1) expect(result.steps[0].outputs.rows.length).toBe(1)

View File

@ -14,8 +14,9 @@ describe("test the server log action", () => {
it("should be able to log the text", async () => { it("should be able to log the text", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.serverLog({ text: "Hello World" }) .serverLog({ text: "Hello World" })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.message).toEqual( expect(result.steps[0].outputs.message).toEqual(
`App ${config.getAppId()} - Hello World` `App ${config.getAppId()} - Hello World`
) )

View File

@ -17,24 +17,27 @@ describe("Test triggering an automation from another automation", () => {
}) })
it("should trigger an other server log automation", async () => { it("should trigger an other server log automation", async () => {
const automation = await createAutomationBuilder(config) const { automation } = await createAutomationBuilder(config)
.onAppAction()
.serverLog({ text: "Hello World" }) .serverLog({ text: "Hello World" })
.save() .save()
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.triggerAutomationRun({ .triggerAutomationRun({
automation: { automation: {
automationId: automation._id!, automationId: automation._id!,
}, },
timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT, timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT,
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toBe(true) expect(result.steps[0].outputs.success).toBe(true)
}) })
it("should fail gracefully if the automation id is incorrect", async () => { it("should fail gracefully if the automation id is incorrect", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.triggerAutomationRun({ .triggerAutomationRun({
automation: { automation: {
// @ts-expect-error - incorrect on purpose // @ts-expect-error - incorrect on purpose
@ -42,7 +45,7 @@ describe("Test triggering an automation from another automation", () => {
}, },
timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT, timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT,
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toBe(false) expect(result.steps[0].outputs.success).toBe(false)
}) })

View File

@ -31,6 +31,7 @@ describe("test the update row action", () => {
it("should be able to run the update row action", async () => { it("should be able to run the update row action", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.updateRow({ .updateRow({
rowId: row._id!, rowId: row._id!,
row: { row: {
@ -40,7 +41,7 @@ describe("test the update row action", () => {
}, },
meta: {}, meta: {},
}) })
.run() .test({ fields: {} })
expect(results.steps[0].outputs.success).toEqual(true) expect(results.steps[0].outputs.success).toEqual(true)
const updatedRow = await config.api.row.get( const updatedRow = await config.api.row.get(
@ -53,20 +54,22 @@ describe("test the update row action", () => {
it("should check invalid inputs return an error", async () => { it("should check invalid inputs return an error", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.updateRow({ meta: {}, row: {}, rowId: "" }) .updateRow({ meta: {}, row: {}, rowId: "" })
.run() .test({ fields: {} })
expect(results.steps[0].outputs.success).toEqual(false) expect(results.steps[0].outputs.success).toEqual(false)
}) })
it("should return an error when table doesn't exist", async () => { it("should return an error when table doesn't exist", async () => {
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.updateRow({ .updateRow({
row: { _id: "invalid" }, row: { _id: "invalid" },
rowId: "invalid", rowId: "invalid",
meta: {}, meta: {},
}) })
.run() .test({ fields: {} })
expect(results.steps[0].outputs.success).toEqual(false) expect(results.steps[0].outputs.success).toEqual(false)
}) })
@ -104,6 +107,7 @@ describe("test the update row action", () => {
}) })
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.updateRow({ .updateRow({
rowId: row._id!, rowId: row._id!,
row: { row: {
@ -115,7 +119,7 @@ describe("test the update row action", () => {
}, },
meta: {}, meta: {},
}) })
.run() .test({ fields: {} })
expect(results.steps[0].outputs.success).toEqual(true) expect(results.steps[0].outputs.success).toEqual(true)
@ -157,6 +161,7 @@ describe("test the update row action", () => {
}) })
const results = await createAutomationBuilder(config) const results = await createAutomationBuilder(config)
.onAppAction()
.updateRow({ .updateRow({
rowId: row._id!, rowId: row._id!,
row: { row: {
@ -174,7 +179,7 @@ describe("test the update row action", () => {
}, },
}, },
}) })
.run() .test({ fields: {} })
expect(results.steps[0].outputs.success).toEqual(true) expect(results.steps[0].outputs.success).toEqual(true)

View File

@ -21,8 +21,9 @@ describe("test the outgoing webhook action", () => {
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" }) nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.zapier({ url: "http://www.example.com", body: null }) .zapier({ url: "http://www.example.com", body: null })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.response.foo).toEqual("bar") expect(result.steps[0].outputs.response.foo).toEqual("bar")
expect(result.steps[0].outputs.success).toEqual(true) expect(result.steps[0].outputs.success).toEqual(true)
@ -44,11 +45,12 @@ describe("test the outgoing webhook action", () => {
.reply(200, { foo: "bar" }) .reply(200, { foo: "bar" })
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.zapier({ .zapier({
url: "http://www.example.com", url: "http://www.example.com",
body: { value: JSON.stringify(payload) }, body: { value: JSON.stringify(payload) },
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.response.foo).toEqual("bar") expect(result.steps[0].outputs.response.foo).toEqual("bar")
expect(result.steps[0].outputs.success).toEqual(true) expect(result.steps[0].outputs.success).toEqual(true)
@ -56,11 +58,12 @@ describe("test the outgoing webhook action", () => {
it("should return a 400 if the JSON payload string is malformed", async () => { it("should return a 400 if the JSON payload string is malformed", async () => {
const result = await createAutomationBuilder(config) const result = await createAutomationBuilder(config)
.onAppAction()
.zapier({ .zapier({
url: "http://www.example.com", url: "http://www.example.com",
body: { value: "{ invalid json }" }, body: { value: "{ invalid json }" },
}) })
.run() .test({ fields: {} })
expect(result.steps[0].outputs.success).toEqual(false) expect(result.steps[0].outputs.success).toEqual(false)
expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON") expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON")

View File

@ -25,7 +25,7 @@ describe("cron trigger", () => {
}) })
await createAutomationBuilder(config) await createAutomationBuilder(config)
.cron({ cron: "* * * * *" }) .onCron({ cron: "* * * * *" })
.serverLog({ .serverLog({
text: "Hello, world!", text: "Hello, world!",
}) })
@ -45,7 +45,7 @@ describe("cron trigger", () => {
it("should fail if the cron expression is invalid", async () => { it("should fail if the cron expression is invalid", async () => {
await createAutomationBuilder(config) await createAutomationBuilder(config)
.cron({ cron: "* * * * * *" }) .onCron({ cron: "* * * * * *" })
.serverLog({ .serverLog({
text: "Hello, world!", text: "Hello, world!",
}) })

View File

@ -11,8 +11,8 @@ describe("Branching automations", () => {
let webhook: Webhook let webhook: Webhook
async function createWebhookAutomation() { async function createWebhookAutomation() {
const automation = await createAutomationBuilder(config) const { automation } = await createAutomationBuilder(config)
.webhook({ fields: { parameter: "string" } }) .onWebhook({ fields: { parameter: "string" } })
.createRow({ .createRow({
row: { tableId: table._id!, name: "{{ trigger.parameter }}" }, row: { tableId: table._id!, name: "{{ trigger.parameter }}" },
}) })

View File

@ -2,39 +2,26 @@ import { v4 as uuidv4 } from "uuid"
import { BUILTIN_ACTION_DEFINITIONS } from "../../actions" import { BUILTIN_ACTION_DEFINITIONS } from "../../actions"
import { TRIGGER_DEFINITIONS } from "../../triggers" import { TRIGGER_DEFINITIONS } from "../../triggers"
import { import {
AppActionTriggerOutputs,
Automation, Automation,
AutomationActionStepId, AutomationActionStepId,
AutomationStep, AutomationStep,
AutomationStepInputs, AutomationStepInputs,
AutomationTrigger, AutomationTrigger,
AutomationTriggerDefinition,
AutomationTriggerInputs, AutomationTriggerInputs,
AutomationTriggerOutputs, AutomationTriggerOutputs,
AutomationTriggerStepId, AutomationTriggerStepId,
BranchStepInputs, BranchStepInputs,
CronTriggerOutputs,
isDidNotTriggerResponse, isDidNotTriggerResponse,
RowCreatedTriggerOutputs,
RowDeletedTriggerOutputs,
RowUpdatedTriggerOutputs,
SearchFilters, SearchFilters,
TestAutomationRequest, TestAutomationRequest,
WebhookTriggerOutputs,
} from "@budibase/types" } from "@budibase/types"
import TestConfiguration from "../../../tests/utilities/TestConfiguration" import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import * as setup from "../utilities"
import { automations } from "@budibase/shared-core" import { automations } from "@budibase/shared-core"
type TriggerOutputs = type StepBuilderFunction = <TStep extends AutomationTriggerStepId>(
| RowCreatedTriggerOutputs stepBuilder: BranchStepBuilder<TStep>
| RowUpdatedTriggerOutputs ) => void
| RowDeletedTriggerOutputs
| AppActionTriggerOutputs
| WebhookTriggerOutputs
| CronTriggerOutputs
| undefined
type StepBuilderFunction = (stepBuilder: StepBuilder) => void
type BranchConfig = { type BranchConfig = {
[key: string]: { [key: string]: {
@ -43,11 +30,44 @@ type BranchConfig = {
} }
} }
class BaseStepBuilder { class TriggerBuilder {
protected steps: AutomationStep[] = [] private readonly config: TestConfiguration
protected stepNames: { [key: string]: string } = {}
protected createStepFn<TStep extends AutomationActionStepId>(stepId: TStep) { constructor(config: TestConfiguration) {
this.config = config
}
protected trigger<
TStep extends AutomationTriggerStepId,
TInput = AutomationTriggerInputs<TStep>
>(stepId: TStep) {
return (inputs: TInput) => {
const definition: AutomationTriggerDefinition =
TRIGGER_DEFINITIONS[stepId]
const trigger: AutomationTrigger = {
...definition,
stepId,
inputs: (inputs || {}) as any,
id: uuidv4(),
}
return new StepBuilder<TStep>(this.config, trigger)
}
}
onAppAction = this.trigger(AutomationTriggerStepId.APP)
onRowSaved = this.trigger(AutomationTriggerStepId.ROW_SAVED)
onRowUpdated = this.trigger(AutomationTriggerStepId.ROW_UPDATED)
onRowDeleted = this.trigger(AutomationTriggerStepId.ROW_DELETED)
onWebhook = this.trigger(AutomationTriggerStepId.WEBHOOK)
onCron = this.trigger(AutomationTriggerStepId.CRON)
}
class BranchStepBuilder<TStep extends AutomationTriggerStepId> {
protected readonly steps: AutomationStep[] = []
protected readonly stepNames: { [key: string]: string } = {}
protected step<TStep extends AutomationActionStepId>(stepId: TStep) {
return ( return (
inputs: AutomationStepInputs<TStep>, inputs: AutomationStepInputs<TStep>,
opts?: { stepName?: string; stepId?: string } opts?: { stepName?: string; stepId?: string }
@ -68,59 +88,49 @@ class BaseStepBuilder {
} }
} }
createRow = this.createStepFn(AutomationActionStepId.CREATE_ROW) createRow = this.step(AutomationActionStepId.CREATE_ROW)
updateRow = this.createStepFn(AutomationActionStepId.UPDATE_ROW) updateRow = this.step(AutomationActionStepId.UPDATE_ROW)
deleteRow = this.createStepFn(AutomationActionStepId.DELETE_ROW) deleteRow = this.step(AutomationActionStepId.DELETE_ROW)
sendSmtpEmail = this.createStepFn(AutomationActionStepId.SEND_EMAIL_SMTP) sendSmtpEmail = this.step(AutomationActionStepId.SEND_EMAIL_SMTP)
executeQuery = this.createStepFn(AutomationActionStepId.EXECUTE_QUERY) executeQuery = this.step(AutomationActionStepId.EXECUTE_QUERY)
queryRows = this.createStepFn(AutomationActionStepId.QUERY_ROWS) queryRows = this.step(AutomationActionStepId.QUERY_ROWS)
loop = this.createStepFn(AutomationActionStepId.LOOP) loop = this.step(AutomationActionStepId.LOOP)
serverLog = this.createStepFn(AutomationActionStepId.SERVER_LOG) serverLog = this.step(AutomationActionStepId.SERVER_LOG)
executeScript = this.createStepFn(AutomationActionStepId.EXECUTE_SCRIPT) executeScript = this.step(AutomationActionStepId.EXECUTE_SCRIPT)
filter = this.createStepFn(AutomationActionStepId.FILTER) filter = this.step(AutomationActionStepId.FILTER)
bash = this.createStepFn(AutomationActionStepId.EXECUTE_BASH) bash = this.step(AutomationActionStepId.EXECUTE_BASH)
openai = this.createStepFn(AutomationActionStepId.OPENAI) openai = this.step(AutomationActionStepId.OPENAI)
collect = this.createStepFn(AutomationActionStepId.COLLECT) collect = this.step(AutomationActionStepId.COLLECT)
zapier = this.createStepFn(AutomationActionStepId.zapier) zapier = this.step(AutomationActionStepId.zapier)
triggerAutomationRun = this.createStepFn( triggerAutomationRun = this.step(
AutomationActionStepId.TRIGGER_AUTOMATION_RUN AutomationActionStepId.TRIGGER_AUTOMATION_RUN
) )
outgoingWebhook = this.createStepFn(AutomationActionStepId.OUTGOING_WEBHOOK) outgoingWebhook = this.step(AutomationActionStepId.OUTGOING_WEBHOOK)
n8n = this.createStepFn(AutomationActionStepId.n8n) n8n = this.step(AutomationActionStepId.n8n)
make = this.createStepFn(AutomationActionStepId.integromat) make = this.step(AutomationActionStepId.integromat)
discord = this.createStepFn(AutomationActionStepId.discord) discord = this.step(AutomationActionStepId.discord)
delay = this.createStepFn(AutomationActionStepId.DELAY) delay = this.step(AutomationActionStepId.DELAY)
protected addBranchStep(branchConfig: BranchConfig): void { protected addBranchStep(branchConfig: BranchConfig): void {
const branchStepInputs: BranchStepInputs = { const inputs: BranchStepInputs = {
branches: [], branches: [],
children: {}, children: {},
} }
Object.entries(branchConfig).forEach(([key, branch]) => { for (const [name, branch] of Object.entries(branchConfig)) {
const stepBuilder = new StepBuilder() const builder = new BranchStepBuilder<TStep>()
branch.steps(stepBuilder) branch.steps(builder)
let branchId = uuidv4() let id = uuidv4()
branchStepInputs.branches.push({ inputs.branches.push({ name, condition: branch.condition, id })
name: key, inputs.children![id] = builder.steps
condition: branch.condition, }
id: branchId,
}) this.steps.push({
branchStepInputs.children![branchId] = stepBuilder.build()
})
const branchStep: AutomationStep = {
...automations.steps.branch.definition, ...automations.steps.branch.definition,
id: uuidv4(), id: uuidv4(),
stepId: AutomationActionStepId.BRANCH, stepId: AutomationActionStepId.BRANCH,
inputs: branchStepInputs, inputs,
} })
this.steps.push(branchStep)
}
}
class StepBuilder extends BaseStepBuilder {
build(): AutomationStep[] {
return this.steps
} }
branch(branchConfig: BranchConfig): this { branch(branchConfig: BranchConfig): this {
@ -129,121 +139,76 @@ class StepBuilder extends BaseStepBuilder {
} }
} }
class AutomationBuilder extends BaseStepBuilder { class StepBuilder<
private automationConfig: Automation TStep extends AutomationTriggerStepId
private config: TestConfiguration > extends BranchStepBuilder<TStep> {
private triggerOutputs: TriggerOutputs private readonly config: TestConfiguration
private triggerSet = false private readonly trigger: AutomationTrigger
private _name: string | undefined = undefined
constructor(config?: TestConfiguration) { constructor(config: TestConfiguration, trigger: AutomationTrigger) {
super() super()
this.config = config || setup.getConfig() this.config = config
this.triggerOutputs = { fields: {} } this.trigger = trigger
this.automationConfig = { }
name: `Test Automation ${uuidv4()}`,
name(n: string): this {
this._name = n
return this
}
build(): Automation {
const name = this._name || `Test Automation ${uuidv4()}`
return {
name,
definition: { definition: {
steps: [], steps: this.steps,
trigger: { trigger: this.trigger,
...TRIGGER_DEFINITIONS[AutomationTriggerStepId.APP], stepNames: this.stepNames,
stepId: AutomationTriggerStepId.APP,
inputs: this.triggerOutputs,
id: uuidv4(),
},
stepNames: {},
}, },
type: "automation", type: "automation",
appId: this.config.getAppId(), appId: this.config.getAppId(),
} }
} }
name(n: string): this {
this.automationConfig.name = n
return this
}
protected triggerInputOutput<
TStep extends AutomationTriggerStepId,
TInput = AutomationTriggerInputs<TStep>,
TOutput = AutomationTriggerOutputs<TStep>
>(stepId: TStep) {
return (inputs: TInput, outputs?: TOutput) => {
if (this.triggerSet) {
throw new Error("Only one trigger can be set for an automation.")
}
this.triggerOutputs = outputs as TriggerOutputs | undefined
this.automationConfig.definition.trigger = {
...TRIGGER_DEFINITIONS[stepId],
stepId,
inputs,
id: uuidv4(),
} as AutomationTrigger
this.triggerSet = true
return this
}
}
protected triggerOutputOnly<
TStep extends AutomationTriggerStepId,
TOutput = AutomationTriggerOutputs<TStep>
>(stepId: TStep) {
return (outputs: TOutput) => {
this.triggerOutputs = outputs as TriggerOutputs
this.automationConfig.definition.trigger = {
...TRIGGER_DEFINITIONS[stepId],
stepId,
id: uuidv4(),
} as AutomationTrigger
this.triggerSet = true
return this
}
}
// The input and output for appAction is identical, and we only ever seem to
// set the output, so we're ignoring the input for now.
appAction = this.triggerOutputOnly(AutomationTriggerStepId.APP)
rowSaved = this.triggerInputOutput(AutomationTriggerStepId.ROW_SAVED)
rowUpdated = this.triggerInputOutput(AutomationTriggerStepId.ROW_UPDATED)
rowDeleted = this.triggerInputOutput(AutomationTriggerStepId.ROW_DELETED)
webhook = this.triggerInputOutput(AutomationTriggerStepId.WEBHOOK)
cron = this.triggerInputOutput(AutomationTriggerStepId.CRON)
branch(branchConfig: BranchConfig): this {
this.addBranchStep(branchConfig)
return this
}
build(): Automation {
this.automationConfig.definition.steps = this.steps
this.automationConfig.definition.stepNames = this.stepNames
return this.automationConfig
}
async save() { async save() {
this.automationConfig.definition.steps = this.steps
const { automation } = await this.config.api.automation.post(this.build()) const { automation } = await this.config.api.automation.post(this.build())
return automation return new AutomationRunner<TStep>(this.config, automation)
} }
async run() { async test(triggerOutput: AutomationTriggerOutputs<TStep>) {
const automation = await this.save() const runner = await this.save()
return await runner.test(triggerOutput)
}
}
class AutomationRunner<TStep extends AutomationTriggerStepId> {
private readonly config: TestConfiguration
readonly automation: Automation
constructor(config: TestConfiguration, automation: Automation) {
this.config = config
this.automation = automation
}
async test(triggerOutput: AutomationTriggerOutputs<TStep>) {
const response = await this.config.api.automation.test( const response = await this.config.api.automation.test(
automation._id!, this.automation._id!,
this.triggerOutputs as TestAutomationRequest // TODO: figure out why this cast is needed.
triggerOutput as TestAutomationRequest
) )
if (isDidNotTriggerResponse(response)) { if (isDidNotTriggerResponse(response)) {
throw new Error(response.message) throw new Error(response.message)
} }
// Remove the trigger step from the response.
response.steps.shift() response.steps.shift()
return {
trigger: response.trigger, return response
steps: response.steps,
}
} }
} }
export function createAutomationBuilder(config: TestConfiguration) { export function createAutomationBuilder(config: TestConfiguration) {
return new AutomationBuilder(config) return new TriggerBuilder(config)
} }

View File

@ -222,10 +222,8 @@ export function basicAutomation(opts?: DeepPartial<Automation>): Automation {
icon: "test", icon: "test",
description: "test", description: "test",
type: AutomationStepType.TRIGGER, type: AutomationStepType.TRIGGER,
inputs: {},
id: "test", id: "test",
inputs: {
fields: {},
},
schema: { schema: {
inputs: { inputs: {
properties: {}, properties: {},

View File

@ -253,10 +253,6 @@ export type OutgoingWebhookStepInputs = {
headers: string | Record<string, string> headers: string | Record<string, string>
} }
export type AppActionTriggerInputs = {
fields: object
}
export type AppActionTriggerOutputs = { export type AppActionTriggerOutputs = {
fields: object fields: object
} }

View File

@ -45,7 +45,6 @@ import {
OpenAIStepInputs, OpenAIStepInputs,
OpenAIStepOutputs, OpenAIStepOutputs,
LoopStepInputs, LoopStepInputs,
AppActionTriggerInputs,
CronTriggerInputs, CronTriggerInputs,
RowUpdatedTriggerInputs, RowUpdatedTriggerInputs,
RowCreatedTriggerInputs, RowCreatedTriggerInputs,
@ -332,7 +331,7 @@ export type AutomationTriggerDefinition = Omit<
export type AutomationTriggerInputs<T extends AutomationTriggerStepId> = export type AutomationTriggerInputs<T extends AutomationTriggerStepId> =
T extends AutomationTriggerStepId.APP T extends AutomationTriggerStepId.APP
? AppActionTriggerInputs ? void | Record<string, any>
: T extends AutomationTriggerStepId.CRON : T extends AutomationTriggerStepId.CRON
? CronTriggerInputs ? CronTriggerInputs
: T extends AutomationTriggerStepId.ROW_ACTION : T extends AutomationTriggerStepId.ROW_ACTION