import * as triggers from "../../automations/triggers" import { sdk as coreSdk } from "@budibase/shared-core" import { DocumentType } from "../../db/utils" import { updateTestHistory, removeDeprecated } from "../../automations/utils" import { context, cache, events, db as dbCore } from "@budibase/backend-core" import { automations, features } from "@budibase/pro" import { App, Automation, AutomationActionStepId, UserCtx, DeleteAutomationResponse, FetchAutomationResponse, GetAutomationTriggerDefinitionsResponse, GetAutomationStepDefinitionsResponse, GetAutomationActionDefinitionsResponse, FindAutomationResponse, UpdateAutomationRequest, UpdateAutomationResponse, CreateAutomationRequest, CreateAutomationResponse, SearchAutomationLogsRequest, SearchAutomationLogsResponse, ClearAutomationLogRequest, ClearAutomationLogResponse, TriggerAutomationRequest, TriggerAutomationResponse, TestAutomationRequest, TestAutomationResponse, TestAutomationStepRequest, TestAutomationStepResponse, } from "@budibase/types" import { getActionDefinitions as actionDefs, getAction, } from "../../automations/actions" import sdk from "../../sdk" import { builderSocket } from "../../websockets" import env from "../../environment" import { NoopEmitter } from "../../events" import { enrichBaseContext } from "../../threads/automation" async function getActionDefinitions() { return removeDeprecated(await actionDefs()) } function getTriggerDefinitions() { return removeDeprecated(triggers.TRIGGER_DEFINITIONS) } /************************* * * * BUILDER FUNCTIONS * * * *************************/ export async function create( ctx: UserCtx ) { let automation = ctx.request.body automation.appId = ctx.appId // call through to update if already exists if (automation._id && automation._rev) { await update(ctx) return } const createdAutomation = await sdk.automations.create(automation) ctx.body = { message: "Automation created successfully", automation: createdAutomation, } builderSocket?.emitAutomationUpdate(ctx, automation) } export async function update( ctx: UserCtx ) { let automation = ctx.request.body automation.appId = ctx.appId // Call through to create if it doesn't exist if (!automation._id || !automation._rev) { await create(ctx) return } const updatedAutomation = await sdk.automations.update(automation) ctx.body = { message: `Automation ${automation._id} updated successfully.`, automation: updatedAutomation, } builderSocket?.emitAutomationUpdate(ctx, automation) } export async function fetch(ctx: UserCtx) { const automations = await sdk.automations.fetch() ctx.body = { automations } } export async function find(ctx: UserCtx) { ctx.body = await sdk.automations.get(ctx.params.id) } export async function destroy(ctx: UserCtx) { const automationId = ctx.params.id const automation = await sdk.automations.get(ctx.params.id) if (coreSdk.automations.isRowAction(automation)) { ctx.throw("Row actions automations cannot be deleted", 422) } ctx.body = await sdk.automations.remove(automationId, ctx.params.rev) builderSocket?.emitAutomationDeletion(ctx, automationId) } export async function logSearch( ctx: UserCtx ) { ctx.body = await automations.logs.logSearch(ctx.request.body) } export async function clearLogError( ctx: UserCtx ) { const { automationId, appId } = ctx.request.body await context.doInAppContext(appId, async () => { const db = context.getProdAppDB() const metadata = await db.get(DocumentType.APP_METADATA) if (!automationId) { delete metadata.automationErrors } else if ( metadata.automationErrors && metadata.automationErrors[automationId] ) { delete metadata.automationErrors[automationId] } await db.put(metadata) await cache.app.invalidateAppMetadata(metadata.appId, metadata) ctx.body = { message: `Error logs cleared.` } }) } export async function getActionList( ctx: UserCtx ) { ctx.body = await getActionDefinitions() } export async function getTriggerList( ctx: UserCtx ) { ctx.body = getTriggerDefinitions() } export async function getDefinitionList( ctx: UserCtx ) { ctx.body = { trigger: getTriggerDefinitions(), action: await getActionDefinitions(), } } /********************* * * * API FUNCTIONS * * * *********************/ export async function trigger( ctx: UserCtx ) { const db = context.getAppDB() let automation = await db.get(ctx.params.id) let hasCollectStep = sdk.automations.utils.checkForCollectStep(automation) if (hasCollectStep && (await features.isSyncAutomationsEnabled())) { try { const response = await triggers.externalTrigger( automation, { fields: ctx.request.body.fields, user: sdk.users.getUserContextBindings(ctx.user), timeout: ctx.request.body.timeout * 1000 || env.AUTOMATION_THREAD_TIMEOUT, }, { getResponses: true } ) if (!("steps" in response)) { ctx.throw(400, "Unable to collect response") } let collectedValue = response.steps.find( step => step.stepId === AutomationActionStepId.COLLECT ) ctx.body = collectedValue?.outputs } catch (err: any) { if (err.message) { ctx.throw(400, err.message) } else { throw err } } } else { if (ctx.appId && !dbCore.isProdAppID(ctx.appId)) { ctx.throw(400, "Only apps in production support this endpoint") } await triggers.externalTrigger(automation, { ...ctx.request.body, appId: ctx.appId, user: sdk.users.getUserContextBindings(ctx.user), }) ctx.body = { message: `Automation ${automation._id} has been triggered.`, automation, } } } function prepareTestInput(input: TestAutomationRequest) { // prepare the test parameters if (input.id && input.row) { input.row._id = input.id } if (input.revision && input.row) { input.row._rev = input.revision } return input } export async function test( ctx: UserCtx ) { const db = context.getAppDB() const automation = await db.tryGet(ctx.params.id) if (!automation) { ctx.throw(404, `Automation ${ctx.params.id} not found`) } const { request, appId } = ctx const { body } = request const occurredAt = new Date().getTime() await updateTestHistory(appId, automation, { ...body, occurredAt }) const user = sdk.users.getUserContextBindings(ctx.user) ctx.body = await triggers.externalTrigger( automation, { ...prepareTestInput(body), appId, user }, { getResponses: true } ) await events.automation.tested(automation) } export async function testStep( ctx: UserCtx ) { const { id, stepId } = ctx.params const db = context.getAppDB() const automation = await db.tryGet(id) if (!automation) { ctx.throw(404, `Automation ${ctx.params.id} not found`) } const step = automation.definition.steps.find(s => s.id === stepId) if (!step) { ctx.throw(404, `Step ${stepId} not found on automation ${id}`) } if (step.stepId === AutomationActionStepId.BRANCH) { ctx.throw(400, "Branch steps cannot be tested directly") } if (step.stepId === AutomationActionStepId.LOOP) { ctx.throw(400, "Loop steps cannot be tested directly") } const { body } = ctx.request const fn = await getAction(step.stepId) if (!fn) { ctx.throw(400, `Step ${stepId} is not a valid step`) } ctx.body = await fn({ inputs: body.inputs, context: await enrichBaseContext(body.context), appId: ctx.appId, emitter: new NoopEmitter(), }) }