budibase/packages/server/src/automations/tests/utilities/AutomationTestBuilder.ts

259 lines
7.6 KiB
TypeScript

import { v4 as uuidv4 } from "uuid"
import { BUILTIN_ACTION_DEFINITIONS } from "../../actions"
import { TRIGGER_DEFINITIONS } from "../../triggers"
import {
Automation,
AutomationActionStepId,
AutomationStep,
AutomationStepInputs,
AutomationTrigger,
AutomationTriggerDefinition,
AutomationTriggerInputs,
AutomationTriggerOutputs,
AutomationTriggerStepId,
BranchStepInputs,
isDidNotTriggerResponse,
SearchFilters,
TestAutomationRequest,
TriggerAutomationRequest,
TriggerAutomationResponse,
} from "@budibase/types"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import { automations } from "@budibase/shared-core"
type StepBuilderFunction = <TStep extends AutomationTriggerStepId>(
stepBuilder: BranchStepBuilder<TStep>
) => void
type BranchConfig = {
[key: string]: {
steps: StepBuilderFunction
condition: SearchFilters
}
}
class TriggerBuilder {
private readonly config: TestConfiguration
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)
onRowAction = this.trigger(AutomationTriggerStepId.ROW_ACTION)
}
class BranchStepBuilder<TStep extends AutomationTriggerStepId> {
protected readonly steps: AutomationStep[] = []
protected readonly stepNames: { [key: string]: string } = {}
protected step<TStep extends AutomationActionStepId>(stepId: TStep) {
return (
inputs: AutomationStepInputs<TStep>,
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
}
}
createRow = this.step(AutomationActionStepId.CREATE_ROW)
updateRow = this.step(AutomationActionStepId.UPDATE_ROW)
deleteRow = this.step(AutomationActionStepId.DELETE_ROW)
sendSmtpEmail = this.step(AutomationActionStepId.SEND_EMAIL_SMTP)
executeQuery = this.step(AutomationActionStepId.EXECUTE_QUERY)
queryRows = this.step(AutomationActionStepId.QUERY_ROWS)
loop = this.step(AutomationActionStepId.LOOP)
serverLog = this.step(AutomationActionStepId.SERVER_LOG)
executeScript = this.step(AutomationActionStepId.EXECUTE_SCRIPT)
executeScriptV2 = this.step(AutomationActionStepId.EXECUTE_SCRIPT_V2)
filter = this.step(AutomationActionStepId.FILTER)
bash = this.step(AutomationActionStepId.EXECUTE_BASH)
openai = this.step(AutomationActionStepId.OPENAI)
collect = this.step(AutomationActionStepId.COLLECT)
zapier = this.step(AutomationActionStepId.zapier)
triggerAutomationRun = this.step(
AutomationActionStepId.TRIGGER_AUTOMATION_RUN
)
outgoingWebhook = this.step(AutomationActionStepId.OUTGOING_WEBHOOK)
n8n = this.step(AutomationActionStepId.n8n)
make = this.step(AutomationActionStepId.integromat)
discord = this.step(AutomationActionStepId.discord)
delay = this.step(AutomationActionStepId.DELAY)
protected addBranchStep(branchConfig: BranchConfig): void {
const inputs: BranchStepInputs = {
branches: [],
children: {},
}
for (const [name, branch] of Object.entries(branchConfig)) {
const builder = new BranchStepBuilder<TStep>()
branch.steps(builder)
let id = uuidv4()
inputs.branches.push({ name, condition: branch.condition, id })
inputs.children![id] = builder.steps
}
this.steps.push({
...automations.steps.branch.definition,
id: uuidv4(),
stepId: AutomationActionStepId.BRANCH,
inputs,
})
}
branch(branchConfig: BranchConfig): this {
this.addBranchStep(branchConfig)
return this
}
}
class StepBuilder<
TStep extends AutomationTriggerStepId
> extends BranchStepBuilder<TStep> {
private readonly config: TestConfiguration
private readonly _trigger: AutomationTrigger
private _name: string | undefined = undefined
constructor(config: TestConfiguration, trigger: AutomationTrigger) {
super()
this.config = config
this._trigger = trigger
}
name(n: string): this {
this._name = n
return this
}
build(): Automation {
const name = this._name || `Test Automation ${uuidv4()}`
return {
name,
definition: {
steps: this.steps,
trigger: this._trigger,
stepNames: this.stepNames,
},
type: "automation",
appId: this.config.getAppId(),
}
}
async save() {
const { automation } = await this.config.api.automation.post(this.build())
return new AutomationRunner<TStep>(this.config, automation)
}
async test(triggerOutput: AutomationTriggerOutputs<TStep>) {
const runner = await this.save()
return await runner.test(triggerOutput)
}
async trigger(
request: TriggerAutomationRequest
): Promise<TriggerAutomationResponse> {
const runner = await this.save()
return await runner.trigger(request)
}
}
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(
this.automation._id!,
// TODO: figure out why this cast is needed.
triggerOutput as TestAutomationRequest
)
if (isDidNotTriggerResponse(response)) {
throw new Error(response.message)
}
// Remove the trigger step from the response.
response.steps.shift()
return response
}
async trigger(
request: TriggerAutomationRequest
): Promise<TriggerAutomationResponse> {
if (!this.config.prodAppId) {
throw new Error(
"Automations can only be triggered in a production app context, call config.api.application.publish()"
)
}
// Because you can only trigger automations in a production app context, we
// wrap the trigger call to make tests a bit cleaner. If you really want to
// test triggering an automation in a dev app context, you can use the
// automation API directly.
return await this.config.withProdApp(async () => {
try {
return await this.config.api.automation.trigger(
this.automation._id!,
request
)
} catch (e: any) {
if (e.cause.status === 404) {
throw new Error(
`Automation with ID ${
this.automation._id
} not found in app ${this.config.getAppId()}. You may have forgotten to call config.api.application.publish().`,
{ cause: e }
)
} else {
throw e
}
}
})
}
}
export function createAutomationBuilder(config: TestConfiguration) {
return new TriggerBuilder(config)
}