diff --git a/packages/server/src/automations/tests/triggers/createRow.spec.ts b/packages/server/src/automations/tests/triggers/createRow.spec.ts new file mode 100644 index 0000000000..84f136c8a6 --- /dev/null +++ b/packages/server/src/automations/tests/triggers/createRow.spec.ts @@ -0,0 +1,67 @@ +import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" +import TestConfiguration from "../../../tests/utilities/TestConfiguration" +import { getQueue } from "../.." +import { Job } from "bull" +import { basicTable } from "../../../tests/utilities/structures" +import { Table } from "@budibase/types" + +describe("cron trigger", () => { + const config = new TestConfiguration() + let table: Table + + beforeAll(async () => { + await config.init() + table = await config.api.table.save(basicTable()) + }) + + afterAll(() => { + config.end() + }) + + it("should successfully fire", async () => { + const queue = getQueue() + expect(await queue.getCompletedCount()).toEqual(0) + + const jobPromise = new Promise(resolve => { + queue.on("completed", async job => { + resolve(job) + }) + }) + + await createAutomationBuilder({ config }) + .rowSaved({ tableId: table._id! }) + .cron({ cron: "* * * * *" }) + .serverLog({ + text: "Hello, world!", + }) + .save() + + await config.api.application.publish(config.getAppId()) + + expect(await queue.getCompletedCount()).toEqual(1) + + const job = await jobPromise + const repeat = job.opts?.repeat + if (!repeat || !("cron" in repeat)) { + throw new Error("Expected cron repeat") + } + expect(repeat.cron).toEqual("* * * * *") + }) + + it("should fail if the cron expression is invalid", async () => { + await createAutomationBuilder({ config }) + .cron({ cron: "* * * * * *" }) + .serverLog({ + text: "Hello, world!", + }) + .save() + + await config.api.application.publish(config.getAppId(), { + status: 500, + body: { + message: + 'Deployment Failed: Invalid automation CRON "* * * * * *" - Expected 5 values, but got 6.', + }, + }) + }) +}) diff --git a/packages/server/src/automations/tests/triggers/cron.spec.ts b/packages/server/src/automations/tests/triggers/cron.spec.ts index 956f054ca4..baddb1dd51 100644 --- a/packages/server/src/automations/tests/triggers/cron.spec.ts +++ b/packages/server/src/automations/tests/triggers/cron.spec.ts @@ -6,7 +6,7 @@ import { Job } from "bull" describe("cron trigger", () => { const config = new TestConfiguration() - beforeEach(async () => { + beforeAll(async () => { await config.init() }) diff --git a/packages/server/src/automations/tests/triggers/webhook.spec.ts b/packages/server/src/automations/tests/triggers/webhook.spec.ts index 2e0df5de49..a0b5e7f195 100644 --- a/packages/server/src/automations/tests/triggers/webhook.spec.ts +++ b/packages/server/src/automations/tests/triggers/webhook.spec.ts @@ -1,4 +1,3 @@ -import * as automation from "../../index" import { Table, Webhook, WebhookActionType } from "@budibase/types" import { createAutomationBuilder } from "../utilities/AutomationTestBuilder" import { mocks } from "@budibase/backend-core/tests" @@ -37,7 +36,6 @@ describe("Branching automations", () => { } beforeEach(async () => { - await automation.init() await config.init() table = await config.createTable() }) diff --git a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts index 74e0ae87fb..9484f5cf21 100644 --- a/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts @@ -2,51 +2,24 @@ import { v4 as uuidv4 } from "uuid" import { BUILTIN_ACTION_DEFINITIONS } from "../../actions" import { TRIGGER_DEFINITIONS } from "../../triggers" import { - AppActionTriggerInputs, AppActionTriggerOutputs, Automation, AutomationActionStepId, AutomationStep, AutomationStepInputs, AutomationTrigger, - AutomationTriggerDefinition, AutomationTriggerInputs, + AutomationTriggerOutputs, AutomationTriggerStepId, - BashStepInputs, - Branch, BranchStepInputs, - CollectStepInputs, - CreateRowStepInputs, - CronTriggerInputs, CronTriggerOutputs, - DelayStepInputs, - DeleteRowStepInputs, - DiscordStepInputs, - ExecuteQueryStepInputs, - ExecuteScriptStepInputs, - FilterStepInputs, isDidNotTriggerResponse, - LoopStepInputs, - MakeIntegrationInputs, - n8nStepInputs, - OpenAIStepInputs, - OutgoingWebhookStepInputs, - QueryRowsStepInputs, - RowCreatedTriggerInputs, RowCreatedTriggerOutputs, - RowDeletedTriggerInputs, RowDeletedTriggerOutputs, - RowUpdatedTriggerInputs, RowUpdatedTriggerOutputs, SearchFilters, - ServerLogStepInputs, - SmtpEmailStepInputs, TestAutomationRequest, - TriggerAutomationStepInputs, - UpdateRowStepInputs, - WebhookTriggerInputs, WebhookTriggerOutputs, - ZapierStepInputs, } from "@budibase/types" import TestConfiguration from "../../../tests/utilities/TestConfiguration" import * as setup from "../utilities" @@ -74,28 +47,53 @@ class BaseStepBuilder { protected steps: AutomationStep[] = [] protected stepNames: { [key: string]: string } = {} - protected step( - stepId: TStep, - stepSchema: Omit, - inputs: AutomationStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - const id = opts?.stepId || uuidv4() - this.steps.push({ - ...stepSchema, - inputs: inputs as any, - id, - stepId, - name: opts?.stepName || stepSchema.name, - }) - if (opts?.stepName) { - this.stepNames[id] = opts.stepName + protected createStepFn(stepId: TStep) { + return ( + inputs: AutomationStepInputs, + opts?: { stepName?: string; stepId?: string } + ) => { + const schema = BUILTIN_ACTION_DEFINITIONS[stepId] + const id = opts?.stepId || uuidv4() + this.steps.push({ + ...schema, + inputs: inputs as any, + id, + stepId, + name: opts?.stepName || schema.name, + }) + if (opts?.stepName) { + this.stepNames[id] = opts.stepName + } + return this } - return this } + + createRow = this.createStepFn(AutomationActionStepId.CREATE_ROW) + updateRow = this.createStepFn(AutomationActionStepId.UPDATE_ROW) + deleteRow = this.createStepFn(AutomationActionStepId.DELETE_ROW) + sendSmtpEmail = this.createStepFn(AutomationActionStepId.SEND_EMAIL_SMTP) + executeQuery = this.createStepFn(AutomationActionStepId.EXECUTE_QUERY) + queryRows = this.createStepFn(AutomationActionStepId.QUERY_ROWS) + loop = this.createStepFn(AutomationActionStepId.LOOP) + serverLog = this.createStepFn(AutomationActionStepId.SERVER_LOG) + executeScript = this.createStepFn(AutomationActionStepId.EXECUTE_SCRIPT) + filter = this.createStepFn(AutomationActionStepId.FILTER) + bash = this.createStepFn(AutomationActionStepId.EXECUTE_BASH) + openai = this.createStepFn(AutomationActionStepId.OPENAI) + collect = this.createStepFn(AutomationActionStepId.COLLECT) + zapier = this.createStepFn(AutomationActionStepId.zapier) + triggerAutomationRun = this.createStepFn( + AutomationActionStepId.TRIGGER_AUTOMATION_RUN + ) + outgoingWebhook = this.createStepFn(AutomationActionStepId.OUTGOING_WEBHOOK) + n8n = this.createStepFn(AutomationActionStepId.n8n) + make = this.createStepFn(AutomationActionStepId.integromat) + discord = this.createStepFn(AutomationActionStepId.discord) + delay = this.createStepFn(AutomationActionStepId.DELAY) + protected addBranchStep(branchConfig: BranchConfig): void { const branchStepInputs: BranchStepInputs = { - branches: [] as Branch[], + branches: [], children: {}, } @@ -118,243 +116,6 @@ class BaseStepBuilder { } this.steps.push(branchStep) } - - // STEPS - createRow( - inputs: CreateRowStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.CREATE_ROW, - BUILTIN_ACTION_DEFINITIONS.CREATE_ROW, - inputs, - opts - ) - } - - updateRow( - inputs: UpdateRowStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.UPDATE_ROW, - BUILTIN_ACTION_DEFINITIONS.UPDATE_ROW, - inputs, - opts - ) - } - - deleteRow( - inputs: DeleteRowStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.DELETE_ROW, - BUILTIN_ACTION_DEFINITIONS.DELETE_ROW, - inputs, - opts - ) - } - - sendSmtpEmail( - inputs: SmtpEmailStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.SEND_EMAIL_SMTP, - BUILTIN_ACTION_DEFINITIONS.SEND_EMAIL_SMTP, - inputs, - opts - ) - } - - executeQuery( - inputs: ExecuteQueryStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.EXECUTE_QUERY, - BUILTIN_ACTION_DEFINITIONS.EXECUTE_QUERY, - inputs, - opts - ) - } - - queryRows( - inputs: QueryRowsStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.QUERY_ROWS, - BUILTIN_ACTION_DEFINITIONS.QUERY_ROWS, - inputs, - opts - ) - } - - loop( - inputs: LoopStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.LOOP, - BUILTIN_ACTION_DEFINITIONS.LOOP, - inputs, - opts - ) - } - - serverLog( - input: ServerLogStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.SERVER_LOG, - BUILTIN_ACTION_DEFINITIONS.SERVER_LOG, - input, - opts - ) - } - - executeScript( - input: ExecuteScriptStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.EXECUTE_SCRIPT, - BUILTIN_ACTION_DEFINITIONS.EXECUTE_SCRIPT, - input, - opts - ) - } - - filter(input: FilterStepInputs): this { - return this.step( - AutomationActionStepId.FILTER, - BUILTIN_ACTION_DEFINITIONS.FILTER, - input - ) - } - - bash( - input: BashStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.EXECUTE_BASH, - BUILTIN_ACTION_DEFINITIONS.EXECUTE_BASH, - input, - opts - ) - } - - openai( - input: OpenAIStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.OPENAI, - BUILTIN_ACTION_DEFINITIONS.OPENAI, - input, - opts - ) - } - - collect( - input: CollectStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.COLLECT, - BUILTIN_ACTION_DEFINITIONS.COLLECT, - input, - opts - ) - } - - zapier( - input: ZapierStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.zapier, - BUILTIN_ACTION_DEFINITIONS.zapier, - input, - opts - ) - } - - triggerAutomationRun( - input: TriggerAutomationStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.TRIGGER_AUTOMATION_RUN, - BUILTIN_ACTION_DEFINITIONS.TRIGGER_AUTOMATION_RUN, - input, - opts - ) - } - - outgoingWebhook( - input: OutgoingWebhookStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.OUTGOING_WEBHOOK, - BUILTIN_ACTION_DEFINITIONS.OUTGOING_WEBHOOK, - input, - opts - ) - } - - n8n( - input: n8nStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.n8n, - BUILTIN_ACTION_DEFINITIONS.n8n, - input, - opts - ) - } - - make( - input: MakeIntegrationInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.integromat, - BUILTIN_ACTION_DEFINITIONS.integromat, - input, - opts - ) - } - - discord( - input: DiscordStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.discord, - BUILTIN_ACTION_DEFINITIONS.discord, - input, - opts - ) - } - - delay( - input: DelayStepInputs, - opts?: { stepName?: string; stepId?: string } - ): this { - return this.step( - AutomationActionStepId.DELAY, - BUILTIN_ACTION_DEFINITIONS.DELAY, - input, - opts - ) - } } class StepBuilder extends BaseStepBuilder { @@ -379,11 +140,17 @@ class AutomationBuilder extends BaseStepBuilder { ) { super() this.config = options.config || setup.getConfig() + this.triggerOutputs = { fields: {} } this.automationConfig = { name: options.name || `Test Automation ${uuidv4()}`, definition: { steps: [], - trigger: {} as AutomationTrigger, + trigger: { + ...TRIGGER_DEFINITIONS[AutomationTriggerStepId.APP], + stepId: AutomationTriggerStepId.APP, + inputs: this.triggerOutputs, + id: uuidv4(), + }, stepNames: {}, }, type: "automation", @@ -391,94 +158,34 @@ class AutomationBuilder extends BaseStepBuilder { } } - // TRIGGERS - rowSaved(inputs: RowCreatedTriggerInputs, outputs: RowCreatedTriggerOutputs) { - this.triggerOutputs = outputs - return this.trigger( - TRIGGER_DEFINITIONS.ROW_SAVED, - AutomationTriggerStepId.ROW_SAVED, - inputs, - outputs - ) - } - - rowUpdated( - inputs: RowUpdatedTriggerInputs, - outputs: RowUpdatedTriggerOutputs - ) { - this.triggerOutputs = outputs - return this.trigger( - TRIGGER_DEFINITIONS.ROW_UPDATED, - AutomationTriggerStepId.ROW_UPDATED, - inputs, - outputs - ) - } - - rowDeleted( - inputs: RowDeletedTriggerInputs, - outputs: RowDeletedTriggerOutputs - ) { - this.triggerOutputs = outputs - return this.trigger( - TRIGGER_DEFINITIONS.ROW_DELETED, - AutomationTriggerStepId.ROW_DELETED, - inputs, - outputs - ) - } - - appAction(outputs: AppActionTriggerOutputs, inputs?: AppActionTriggerInputs) { - this.triggerOutputs = outputs - return this.trigger( - TRIGGER_DEFINITIONS.APP, - AutomationTriggerStepId.APP, - inputs, - outputs - ) - } - - webhook(outputs: WebhookTriggerOutputs, inputs?: WebhookTriggerInputs) { - this.triggerOutputs = outputs - return this.trigger( - TRIGGER_DEFINITIONS.WEBHOOK, - AutomationTriggerStepId.WEBHOOK, - inputs, - outputs - ) - } - - cron(inputs: CronTriggerInputs, outputs?: CronTriggerOutputs) { - this.triggerOutputs = outputs - return this.trigger( - TRIGGER_DEFINITIONS.CRON, - AutomationTriggerStepId.CRON, - inputs, - outputs - ) - } - - private trigger( - triggerSchema: AutomationTriggerDefinition, - stepId: TStep, - inputs?: AutomationTriggerInputs, - outputs?: TriggerOutputs - ): this { - if (this.triggerSet) { - throw new Error("Only one trigger can be set for an automation.") + protected createTriggerFn< + TStep extends AutomationTriggerStepId, + TInput = AutomationTriggerInputs, + TOutput = AutomationTriggerOutputs + >(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 } - this.automationConfig.definition.trigger = { - ...triggerSchema, - stepId, - inputs: inputs || ({} as any), - id: uuidv4(), - } - this.triggerOutputs = outputs - this.triggerSet = true - - return this } + rowSaved = this.createTriggerFn(AutomationTriggerStepId.ROW_SAVED) + rowUpdated = this.createTriggerFn(AutomationTriggerStepId.ROW_UPDATED) + rowDeleted = this.createTriggerFn(AutomationTriggerStepId.ROW_DELETED) + appAction = this.createTriggerFn(AutomationTriggerStepId.APP) + webhook = this.createTriggerFn(AutomationTriggerStepId.WEBHOOK) + cron = this.createTriggerFn(AutomationTriggerStepId.CRON) + branch(branchConfig: BranchConfig): this { this.addBranchStep(branchConfig) return this diff --git a/packages/types/src/documents/app/automation/schema.ts b/packages/types/src/documents/app/automation/schema.ts index 84bfebf6bf..952397b511 100644 --- a/packages/types/src/documents/app/automation/schema.ts +++ b/packages/types/src/documents/app/automation/schema.ts @@ -52,6 +52,12 @@ import { RowDeletedTriggerInputs, BranchStepInputs, BaseAutomationOutputs, + AppActionTriggerOutputs, + CronTriggerOutputs, + RowDeletedTriggerOutputs, + RowCreatedTriggerOutputs, + RowUpdatedTriggerOutputs, + WebhookTriggerOutputs, } from "./StepInputsOutputs" export type ActionImplementations = { @@ -341,6 +347,23 @@ export type AutomationTriggerInputs = ? Record : never +export type AutomationTriggerOutputs = + T extends AutomationTriggerStepId.APP + ? AppActionTriggerOutputs + : T extends AutomationTriggerStepId.CRON + ? CronTriggerOutputs + : T extends AutomationTriggerStepId.ROW_ACTION + ? Record + : T extends AutomationTriggerStepId.ROW_DELETED + ? RowDeletedTriggerOutputs + : T extends AutomationTriggerStepId.ROW_SAVED + ? RowCreatedTriggerOutputs + : T extends AutomationTriggerStepId.ROW_UPDATED + ? RowUpdatedTriggerOutputs + : T extends AutomationTriggerStepId.WEBHOOK + ? WebhookTriggerOutputs + : never + export interface AutomationTriggerSchema< TTrigger extends AutomationTriggerStepId > extends AutomationStepSchemaBase {