First iteration of single-step automation test endpoint.
This commit is contained in:
parent
b28e4724b1
commit
5bc316916f
|
@ -2,7 +2,7 @@ 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 { setTestFlag, clearTestFlag } from "../../utilities/redis"
|
||||
import { withTestFlag } from "../../utilities/redis"
|
||||
import { context, cache, events, db as dbCore } from "@budibase/backend-core"
|
||||
import { automations, features } from "@budibase/pro"
|
||||
import {
|
||||
|
@ -28,11 +28,18 @@ import {
|
|||
TriggerAutomationResponse,
|
||||
TestAutomationRequest,
|
||||
TestAutomationResponse,
|
||||
TestAutomationStepRequest,
|
||||
TestAutomationStepResponse,
|
||||
} from "@budibase/types"
|
||||
import { getActionDefinitions as actionDefs } from "../../automations/actions"
|
||||
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())
|
||||
|
@ -231,24 +238,68 @@ export async function test(
|
|||
ctx: UserCtx<TestAutomationRequest, TestAutomationResponse>
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
let automation = await db.get<Automation>(ctx.params.id)
|
||||
await setTestFlag(automation._id!)
|
||||
const testInput = prepareTestInput(ctx.request.body)
|
||||
const response = await triggers.externalTrigger(
|
||||
automation,
|
||||
{
|
||||
...testInput,
|
||||
appId: ctx.appId,
|
||||
user: sdk.users.getUserContextBindings(ctx.user),
|
||||
},
|
||||
{ getResponses: true }
|
||||
)
|
||||
// save a test history run
|
||||
await updateTestHistory(ctx.appId, automation, {
|
||||
...ctx.request.body,
|
||||
occurredAt: new Date().getTime(),
|
||||
const automation = await db.tryGet<Automation>(ctx.params.id)
|
||||
if (!automation) {
|
||||
ctx.throw(404, `Automation ${ctx.params.id} not found`)
|
||||
}
|
||||
|
||||
const { request, appId } = ctx
|
||||
const { body } = request
|
||||
|
||||
ctx.body = await withTestFlag(automation._id!, async () => {
|
||||
const occurredAt = new Date().getTime()
|
||||
await updateTestHistory(appId, automation, { ...body, occurredAt })
|
||||
|
||||
const user = sdk.users.getUserContextBindings(ctx.user)
|
||||
return await triggers.externalTrigger(
|
||||
automation,
|
||||
{ ...prepareTestInput(body), appId, user },
|
||||
{ getResponses: true }
|
||||
)
|
||||
})
|
||||
await clearTestFlag(automation._id!)
|
||||
ctx.body = response
|
||||
|
||||
await events.automation.tested(automation)
|
||||
}
|
||||
|
||||
export async function testStep(
|
||||
ctx: UserCtx<TestAutomationStepRequest, TestAutomationStepResponse>
|
||||
) {
|
||||
const { id, stepId } = ctx.params
|
||||
const db = context.getAppDB()
|
||||
const automation = await db.tryGet<Automation>(id)
|
||||
if (!automation) {
|
||||
ctx.throw(404, `Automation ${ctx.params.id} not found`)
|
||||
}
|
||||
|
||||
const step = automation.definition.steps.find(s => s.stepId === 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`)
|
||||
}
|
||||
|
||||
const output = await withTestFlag(
|
||||
automation._id!,
|
||||
async () =>
|
||||
await fn({
|
||||
inputs: body.inputs,
|
||||
context: await enrichBaseContext(body.context),
|
||||
appId: ctx.appId,
|
||||
emitter: new NoopEmitter(),
|
||||
})
|
||||
)
|
||||
|
||||
ctx.body = output
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Router from "@koa/router"
|
||||
import * as controller from "../controllers/automation"
|
||||
import authorized from "../../middleware/authorized"
|
||||
import authorized, { authorizedResource } from "../../middleware/authorized"
|
||||
import { permissions } from "@budibase/backend-core"
|
||||
import { bodyResource, paramResource } from "../../middleware/resourceId"
|
||||
import {
|
||||
|
@ -82,5 +82,15 @@ router
|
|||
),
|
||||
controller.test
|
||||
)
|
||||
.post(
|
||||
"/api/automations/:id/step/:stepId/test",
|
||||
appInfoMiddleware({ appType: AppType.DEV }),
|
||||
authorizedResource(
|
||||
permissions.PermissionType.AUTOMATION,
|
||||
permissions.PermissionLevel.EXECUTE,
|
||||
"id"
|
||||
),
|
||||
controller.testStep
|
||||
)
|
||||
|
||||
export default router
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { EventEmitter } from "events"
|
||||
import {
|
||||
Table,
|
||||
Row,
|
||||
ContextEmitter,
|
||||
EventType,
|
||||
UserBindings,
|
||||
} from "@budibase/types"
|
||||
|
||||
export class NoopEmitter extends EventEmitter implements ContextEmitter {
|
||||
emitRow(values: {
|
||||
eventName: EventType.ROW_SAVE
|
||||
appId: string
|
||||
row: Row
|
||||
table: Table
|
||||
user: UserBindings
|
||||
}): void
|
||||
emitRow(values: {
|
||||
eventName: EventType.ROW_UPDATE
|
||||
appId: string
|
||||
row: Row
|
||||
table: Table
|
||||
oldRow: Row
|
||||
user: UserBindings
|
||||
}): void
|
||||
emitRow(values: {
|
||||
eventName: EventType.ROW_DELETE
|
||||
appId: string
|
||||
row: Row
|
||||
user: UserBindings
|
||||
}): void
|
||||
emitRow(_values: unknown): void {
|
||||
return
|
||||
}
|
||||
|
||||
emitTable(_eventName: string, _appId: string, _table?: Table) {
|
||||
return
|
||||
}
|
||||
}
|
|
@ -2,5 +2,6 @@ import BudibaseEmitter from "./BudibaseEmitter"
|
|||
|
||||
const emitter = new BudibaseEmitter()
|
||||
|
||||
export { NoopEmitter } from "./NoopEmitter"
|
||||
export { init } from "./docUpdates"
|
||||
export default emitter
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
LoopStep,
|
||||
UserBindings,
|
||||
isBasicSearchOperator,
|
||||
ContextEmitter,
|
||||
} from "@budibase/types"
|
||||
import {
|
||||
AutomationContext,
|
||||
|
@ -71,6 +72,24 @@ function getLoopIterations(loopStep: LoopStep) {
|
|||
return 0
|
||||
}
|
||||
|
||||
export async function enrichBaseContext(context: Record<string, any>) {
|
||||
context.env = await sdkUtils.getEnvironmentVariables()
|
||||
|
||||
try {
|
||||
const { config } = await configs.getSettingsConfigDoc()
|
||||
context.settings = {
|
||||
url: config.platformUrl,
|
||||
logo: config.logoUrl,
|
||||
company: config.company,
|
||||
}
|
||||
} catch (e) {
|
||||
// if settings doc doesn't exist, make the settings blank
|
||||
context.settings = {}
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
* The automation orchestrator is a class responsible for executing automations.
|
||||
* It handles the context of the automation and makes sure each step gets the correct
|
||||
|
@ -80,7 +99,7 @@ class Orchestrator {
|
|||
private chainCount: number
|
||||
private appId: string
|
||||
private automation: Automation
|
||||
private emitter: any
|
||||
private emitter: ContextEmitter
|
||||
private context: AutomationContext
|
||||
private job: Job
|
||||
private loopStepOutputs: LoopStep[]
|
||||
|
@ -270,20 +289,9 @@ class Orchestrator {
|
|||
appId: this.appId,
|
||||
automationId: this.automation._id,
|
||||
})
|
||||
this.context.env = await sdkUtils.getEnvironmentVariables()
|
||||
this.context.user = this.currentUser
|
||||
|
||||
try {
|
||||
const { config } = await configs.getSettingsConfigDoc()
|
||||
this.context.settings = {
|
||||
url: config.platformUrl,
|
||||
logo: config.logoUrl,
|
||||
company: config.company,
|
||||
}
|
||||
} catch (e) {
|
||||
// if settings doc doesn't exist, make the settings blank
|
||||
this.context.settings = {}
|
||||
}
|
||||
await enrichBaseContext(this.context)
|
||||
this.context.user = this.currentUser
|
||||
|
||||
let metadata
|
||||
|
||||
|
|
|
@ -58,30 +58,14 @@ export function checkSlashesInUrl(url: string) {
|
|||
export async function updateEntityMetadata(
|
||||
type: string,
|
||||
entityId: string,
|
||||
updateFn: any
|
||||
updateFn: (metadata: Document) => Document
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
const id = generateMetadataID(type, entityId)
|
||||
// read it to see if it exists, we'll overwrite it no matter what
|
||||
let rev, metadata: Document
|
||||
try {
|
||||
const oldMetadata = await db.get<any>(id)
|
||||
rev = oldMetadata._rev
|
||||
metadata = updateFn(oldMetadata)
|
||||
} catch (err) {
|
||||
rev = null
|
||||
metadata = updateFn({})
|
||||
}
|
||||
const metadata = updateFn((await db.tryGet(id)) || {})
|
||||
metadata._id = id
|
||||
if (rev) {
|
||||
metadata._rev = rev
|
||||
}
|
||||
const response = await db.put(metadata)
|
||||
return {
|
||||
...metadata,
|
||||
_id: id,
|
||||
_rev: response.rev,
|
||||
}
|
||||
return { ...metadata, _id: id, _rev: response.rev }
|
||||
}
|
||||
|
||||
export async function saveEntityMetadata(
|
||||
|
@ -89,26 +73,17 @@ export async function saveEntityMetadata(
|
|||
entityId: string,
|
||||
metadata: Document
|
||||
): Promise<Document> {
|
||||
return updateEntityMetadata(type, entityId, () => {
|
||||
return metadata
|
||||
})
|
||||
return updateEntityMetadata(type, entityId, () => metadata)
|
||||
}
|
||||
|
||||
export async function deleteEntityMetadata(type: string, entityId: string) {
|
||||
const db = context.getAppDB()
|
||||
const id = generateMetadataID(type, entityId)
|
||||
let rev
|
||||
try {
|
||||
const metadata = await db.get<any>(id)
|
||||
if (metadata) {
|
||||
rev = metadata._rev
|
||||
}
|
||||
} catch (err) {
|
||||
// don't need to error if it doesn't exist
|
||||
}
|
||||
if (id && rev) {
|
||||
await db.remove(id, rev)
|
||||
const metadata = await db.tryGet(id)
|
||||
if (!metadata) {
|
||||
return
|
||||
}
|
||||
await db.remove(metadata)
|
||||
}
|
||||
|
||||
export function escapeDangerousCharacters(string: string) {
|
||||
|
|
|
@ -89,17 +89,22 @@ export async function setDebounce(id: string, seconds: number) {
|
|||
await debounceClient.store(id, "debouncing", seconds)
|
||||
}
|
||||
|
||||
export async function setTestFlag(id: string) {
|
||||
await flagClient.store(id, { testing: true }, AUTOMATION_TEST_FLAG_SECONDS)
|
||||
}
|
||||
|
||||
export async function checkTestFlag(id: string) {
|
||||
const flag = await flagClient?.get(id)
|
||||
return !!(flag && flag.testing)
|
||||
}
|
||||
|
||||
export async function clearTestFlag(id: string) {
|
||||
await devAppClient.delete(id)
|
||||
export async function withTestFlag<R>(id: string, fn: () => Promise<R>) {
|
||||
// TODO(samwho): this has a bit of a problem where if 2 automations are tested
|
||||
// at the same time, the second one will overwrite the first one's flag. We
|
||||
// should instead use an atomic counter and only clear the flag when the
|
||||
// counter reaches 0.
|
||||
await flagClient.store(id, { testing: true }, AUTOMATION_TEST_FLAG_SECONDS)
|
||||
try {
|
||||
return await fn()
|
||||
} finally {
|
||||
await devAppClient.delete(id)
|
||||
}
|
||||
}
|
||||
|
||||
export function getSocketPubSubClients() {
|
||||
|
|
|
@ -75,3 +75,10 @@ export interface TestAutomationRequest {
|
|||
row?: Row
|
||||
}
|
||||
export interface TestAutomationResponse {}
|
||||
|
||||
export interface TestAutomationStepRequest {
|
||||
inputs: Record<string, any>
|
||||
context: Record<string, any>
|
||||
}
|
||||
|
||||
export type TestAutomationStepResponse = any
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Document } from "../../document"
|
||||
import { EventEmitter } from "events"
|
||||
import { User } from "../../global"
|
||||
import { ReadStream } from "fs"
|
||||
import { Row } from "../row"
|
||||
import { Table } from "../table"
|
||||
import { AutomationStep, AutomationTrigger } from "./schema"
|
||||
import { ContextEmitter } from "../../../sdk"
|
||||
|
||||
export enum AutomationIOType {
|
||||
OBJECT = "object",
|
||||
|
@ -218,7 +218,7 @@ export interface AutomationLogPage {
|
|||
|
||||
export interface AutomationStepInputBase {
|
||||
context: Record<string, any>
|
||||
emitter: EventEmitter
|
||||
emitter: ContextEmitter
|
||||
appId: string
|
||||
apiKey?: string
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue