diff --git a/packages/backend-core/src/events/constants.js b/packages/backend-core/src/events/constants.js index d74004acd0..712e41b17f 100644 --- a/packages/backend-core/src/events/constants.js +++ b/packages/backend-core/src/events/constants.js @@ -125,9 +125,10 @@ exports.Events = { AUTOMATION_CREATED: "automation:created", AUTOMATION_DELETED: "automation:deleted", AUTOMATION_TESTED: "automation:tested", - AUTOMATION_RUN: "automation:run", + // AUTOMATION_RUN: "automation:run", AUTOMATION_STEP_CREATED: "automation:step:created", AUTOMATION_STEP_DELETED: "automation:step:deleted", + AUTOMATION_TRIGGER_UPDATED: "automation:trigger:updated", // LICENSING LICENSING_QUOTA_EXCEEDED: "licensing:quota:exceeded", diff --git a/packages/backend-core/src/events/handlers/automation.js b/packages/backend-core/src/events/handlers/automation.js index 72c2d73d56..2a978b4645 100644 --- a/packages/backend-core/src/events/handlers/automation.js +++ b/packages/backend-core/src/events/handlers/automation.js @@ -6,32 +6,33 @@ exports.created = () => { events.processEvent(Events.AUTOMATION_CREATED, properties) } -// TODO exports.deleted = () => { const properties = {} events.processEvent(Events.AUTOMATION_DELETED, properties) } -// TODO exports.tested = () => { const properties = {} events.processEvent(Events.AUTOMATION_TESTED, properties) } // TODO -exports.run = () => { - const properties = {} - events.processEvent(Events.AUTOMATION_RUN, properties) -} +// exports.run = () => { +// const properties = {} +// events.processEvent(Events.AUTOMATION_RUN, properties) +// } -// TODO exports.stepCreated = () => { const properties = {} events.processEvent(Events.AUTOMATION_STEP_CREATED, properties) } -// TODO exports.stepDeleted = () => { const properties = {} events.processEvent(Events.AUTOMATION_STEP_DELETED, properties) } + +exports.triggerUpdated = () => { + const properties = {} + events.processEvent(Events.AUTOMATION_TRIGGER_UPDATED, properties) +} diff --git a/packages/backend-core/src/tests/utilities/mocks/events.js b/packages/backend-core/src/tests/utilities/mocks/events.js index f6eb2eddcc..74e62ef73c 100644 --- a/packages/backend-core/src/tests/utilities/mocks/events.js +++ b/packages/backend-core/src/tests/utilities/mocks/events.js @@ -26,6 +26,15 @@ jest.mock("../../../events", () => { SSOActivated: jest.fn(), SSODeactivated: jest.fn(), }, + automation: { + created: jest.fn(), + deleted: jest.fn(), + tested: jest.fn(), + // run: jest.fn(), + stepCreated: jest.fn(), + stepDeleted: jest.fn(), + triggerUpdated: jest.fn(), + }, datasource: { created: jest.fn(), updated: jest.fn(), diff --git a/packages/server/src/api/controllers/automation.js b/packages/server/src/api/controllers/automation.js index 74942dad40..2c4c669392 100644 --- a/packages/server/src/api/controllers/automation.js +++ b/packages/server/src/api/controllers/automation.js @@ -10,6 +10,7 @@ const { deleteEntityMetadata } = require("../../utilities") const { MetadataTypes } = require("../../constants") const { setTestFlag, clearTestFlag } = require("../../utilities/redis") const { getAppDB } = require("@budibase/backend-core/context") +const { events } = require("@budibase/backend-core") const ACTION_DEFS = removeDeprecated(actions.ACTION_DEFINITIONS) const TRIGGER_DEFS = removeDeprecated(triggers.TRIGGER_DEFINITIONS) @@ -70,6 +71,10 @@ exports.create = async function (ctx) { newAuto: automation, }) const response = await db.put(automation) + events.automation.created() + for (let step of automation.definition.steps) { + events.automation.stepCreated(step) + } automation._rev = response.rev ctx.status = 200 @@ -82,6 +87,29 @@ exports.create = async function (ctx) { } } +const getNewSteps = (oldAutomation, automation) => { + const oldStepIds = oldAutomation.definition.steps.map(s => s.id) + return automation.definition.steps.filter(s => !oldStepIds.includes(s.id)) +} + +const getDeletedSteps = (oldAutomation, automation) => { + const stepIds = automation.definition.steps.map(s => s.id) + return oldAutomation.definition.steps.filter(s => !stepIds.includes(s.id)) +} + +const handleStepEvents = (oldAutomation, automation) => { + // new steps + const newSteps = getNewSteps(oldAutomation, automation) + for (let step of newSteps) { + events.automation.stepCreated(step) + } + + // old steps + const deletedSteps = getDeletedSteps(oldAutomation, automation) + for (let step of deletedSteps) { + events.automation.stepDeleted(step) + } +} exports.update = async function (ctx) { const db = getAppDB() let automation = ctx.request.body @@ -98,13 +126,14 @@ exports.update = async function (ctx) { const oldAutoTrigger = oldAutomation && oldAutomation.definition.trigger ? oldAutomation.definition.trigger - : {} + : undefined const newAutoTrigger = automation && automation.definition.trigger ? automation.definition.trigger : {} // trigger has been updated, remove the test inputs - if (oldAutoTrigger.id !== newAutoTrigger.id) { + if (oldAutoTrigger && oldAutoTrigger.id !== newAutoTrigger.id) { + events.automation.triggerUpdated() await deleteEntityMetadata( ctx.appId, MetadataTypes.AUTOMATION_TEST_INPUT, @@ -112,6 +141,8 @@ exports.update = async function (ctx) { ) } + handleStepEvents(oldAutomation, automation) + ctx.status = 200 ctx.body = { message: `Automation ${automation._id} updated successfully.`, @@ -148,6 +179,7 @@ exports.destroy = async function (ctx) { // delete metadata first await cleanupAutomationMetadata(automationId) ctx.body = await db.remove(automationId, ctx.params.rev) + events.automation.deleted() } exports.getActionList = async function (ctx) { @@ -215,4 +247,5 @@ exports.test = async function (ctx) { }) await clearTestFlag(automation._id) ctx.body = response + events.automation.tested() } diff --git a/packages/server/src/api/routes/tests/automation.spec.js b/packages/server/src/api/routes/tests/automation.spec.js index cce51fc94b..aef2927889 100644 --- a/packages/server/src/api/routes/tests/automation.spec.js +++ b/packages/server/src/api/routes/tests/automation.spec.js @@ -10,6 +10,7 @@ const { mocks } = require("@budibase/backend-core/testUtils") mocks.date.mock() const MAX_RETRIES = 4 const { TRIGGER_DEFINITIONS, ACTION_DEFINITIONS } = require("../../../automations") +const { events } = require("@budibase/backend-core") describe("/automations", () => { let request = setup.getRequest() @@ -58,8 +59,9 @@ describe("/automations", () => { }) describe("create", () => { - it("returns a success message when the automation is successfully created", async () => { + it("creates an automation with no steps", async () => { const automation = newAutomation() + automation.definition.steps = [] const res = await request .post(`/api/automations`) @@ -71,6 +73,27 @@ describe("/automations", () => { expect(res.body.message).toEqual("Automation created successfully") expect(res.body.automation.name).toEqual("My Automation") expect(res.body.automation._id).not.toEqual(null) + expect(events.automation.created).toBeCalledTimes(1) + expect(events.automation.stepCreated).not.toBeCalled() + }) + + it("creates an automation with steps", async () => { + const automation = newAutomation() + automation.definition.steps.push(automationStep()) + jest.clearAllMocks() + + const res = await request + .post(`/api/automations`) + .set(config.defaultHeaders()) + .send(automation) + .expect('Content-Type', /json/) + .expect(200) + + expect(res.body.message).toEqual("Automation created successfully") + expect(res.body.automation.name).toEqual("My Automation") + expect(res.body.automation._id).not.toEqual(null) + expect(events.automation.created).toBeCalledTimes(1) + expect(events.automation.stepCreated).toBeCalledTimes(2) }) it("should apply authorization to endpoint", async () => { @@ -97,8 +120,8 @@ describe("/automations", () => { }) }) - describe("trigger", () => { - it("trigger the automation successfully", async () => { + describe("test", () => { + it("tests the automation successfully", async () => { let table = await config.createTable() let automation = newAutomation() automation.definition.trigger.inputs.tableId = table._id @@ -113,6 +136,7 @@ describe("/automations", () => { automation = await config.createAutomation(automation) await setup.delay(500) const res = await testAutomation(config, automation) + expect(events.automation.tested).toBeCalledTimes(1) // this looks a bit mad but we don't actually have a way to wait for a response from the automation to // know that it has finished all of its actions - this is currently the best way // also when this runs in CI it is very temper-mental so for now trying to make run stable by repeating until it works @@ -134,53 +158,141 @@ describe("/automations", () => { }) describe("update", () => { - it("updates a automations data", async () => { - let automation = newAutomation() - await config.createAutomation(automation) - automation.name = "Updated Name" - const res = await request + const update = async (automation) => { + return request .put(`/api/automations`) .set(config.defaultHeaders()) .send(automation) .expect('Content-Type', /json/) .expect(200) + } - expect(res.body.message).toEqual(`Automation ${automation._id} updated successfully.`) - expect(res.body.automation.name).toEqual("Updated Name") - }) - - it("should be able to update an automation trigger", async () => { - // create webhook automation - const webhookTrigger = automationTrigger(TRIGGER_DEFINITIONS.WEBHOOK) - let automation = newAutomation({ trigger: webhookTrigger }) - - let res = await request + const updateWithPost = async (automation) => { + return request .post(`/api/automations`) .set(config.defaultHeaders()) .send(automation) .expect('Content-Type', /json/) .expect(200) + } - automation = res.body.automation - expect(automation._id).toBeDefined() - expect(automation._rev).toBeDefined() + it("updates a automations name", async () => { + let automation = newAutomation() + await config.createAutomation(automation) + automation.name = "Updated Name" + jest.clearAllMocks() - // change the trigger - automation.trigger = automationTrigger(TRIGGER_DEFINITIONS.ROW_SAVED) - - // check the post request honours updates with same id - res = await request - .post(`/api/automations`) - .set(config.defaultHeaders()) - .send(automation) - .expect('Content-Type', /json/) - .expect(200) + const res = await update(automation) const automationRes = res.body.automation + const message = res.body.message + + // doc attributes expect(automationRes._id).toEqual(automation._id) expect(automationRes._rev).toBeDefined() expect(automationRes._rev).not.toEqual(automation._rev) + // content updates + expect(automationRes.name).toEqual("Updated Name") + expect(message).toEqual(`Automation ${automation._id} updated successfully.`) + // events + expect(events.automation.created).not.toBeCalled() + expect(events.automation.stepCreated).not.toBeCalled() + expect(events.automation.stepDeleted).not.toBeCalled() + expect(events.automation.triggerUpdated).not.toBeCalled() + }) + + + it("updates a automations name using POST request", async () => { + let automation = newAutomation() + await config.createAutomation(automation) + automation.name = "Updated Name" + jest.clearAllMocks() + + // the POST request will defer to the update + // when an id has been supplied. + const res = await updateWithPost(automation) + + const automationRes = res.body.automation + const message = res.body.message + // doc attributes + expect(automationRes._id).toEqual(automation._id) + expect(automationRes._rev).toBeDefined() + expect(automationRes._rev).not.toEqual(automation._rev) + // content updates + expect(automationRes.name).toEqual("Updated Name") + expect(message).toEqual(`Automation ${automation._id} updated successfully.`) + // events + expect(events.automation.created).not.toBeCalled() + expect(events.automation.stepCreated).not.toBeCalled() + expect(events.automation.stepDeleted).not.toBeCalled() + expect(events.automation.triggerUpdated).not.toBeCalled() + }) + + it("updates an automation trigger", async () => { + let automation = newAutomation() + automation = await config.createAutomation(automation) + automation.definition.trigger = automationTrigger(TRIGGER_DEFINITIONS.WEBHOOK) + jest.clearAllMocks() + + await update(automation) + + // events + expect(events.automation.created).not.toBeCalled() + expect(events.automation.stepCreated).not.toBeCalled() + expect(events.automation.stepDeleted).not.toBeCalled() + expect(events.automation.triggerUpdated).toBeCalledTimes(1) + }) + + it("adds automation steps", async () => { + let automation = newAutomation() + automation = await config.createAutomation(automation) + automation.definition.steps.push(automationStep()) + automation.definition.steps.push(automationStep()) + jest.clearAllMocks() + + // check the post request honours updates with same id + await update(automation) + + // events + expect(events.automation.stepCreated).toBeCalledTimes(2) + expect(events.automation.created).not.toBeCalled() + expect(events.automation.stepDeleted).not.toBeCalled() + expect(events.automation.triggerUpdated).not.toBeCalled() + }) + + + it("removes automation steps", async () => { + let automation = newAutomation() + automation.definition.steps.push(automationStep()) + automation = await config.createAutomation(automation) + automation.definition.steps = [] + jest.clearAllMocks() + + // check the post request honours updates with same id + await update(automation) + + // events + expect(events.automation.stepDeleted).toBeCalledTimes(2) + expect(events.automation.stepCreated).not.toBeCalled() + expect(events.automation.created).not.toBeCalled() + expect(events.automation.triggerUpdated).not.toBeCalled() + }) + + it("adds and removes automation steps", async () => { + let automation = newAutomation() + automation = await config.createAutomation(automation) + automation.definition.steps = [automationStep(), automationStep()] + jest.clearAllMocks() + + // check the post request honours updates with same id + await update(automation) + + // events + expect(events.automation.stepCreated).toBeCalledTimes(2) + expect(events.automation.stepDeleted).toBeCalledTimes(1) + expect(events.automation.created).not.toBeCalled() + expect(events.automation.triggerUpdated).not.toBeCalled() }) }) @@ -216,7 +328,8 @@ describe("/automations", () => { .expect('Content-Type', /json/) .expect(200) - expect(res.body.id).toEqual(automation._id) + expect(res.body.id).toEqual(automation._id) + expect(events.automation.deleted).toBeCalledTimes(1) }) it("should apply authorization to endpoint", async () => {