Merge branch 'master' into state-and-bindings-panels

This commit is contained in:
Andrew Kingston 2025-01-23 09:08:40 +00:00 committed by GitHub
commit 6eeee862e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 158 additions and 122 deletions

View File

@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.2.47",
"version": "3.3.1",
"npmClient": "yarn",
"concurrency": 20,
"command": {

View File

@ -11,7 +11,7 @@ export const datasourceSelect = {
},
viewV2: (view, datasources) => {
const datasource = datasources
.filter(f => f.entities)
?.filter(f => f.entities)
.flatMap(d => d.entities)
.find(ds => ds._id === view.tableId)
return {

View File

@ -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 {
@ -231,24 +231,25 @@ 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)
}

View File

@ -5,8 +5,11 @@ import {
sendAutomationAttachmentsToStorage,
} from "../automationUtils"
import { buildCtx } from "./utils"
import { CreateRowStepInputs, CreateRowStepOutputs } from "@budibase/types"
import { EventEmitter } from "events"
import {
ContextEmitter,
CreateRowStepInputs,
CreateRowStepOutputs,
} from "@budibase/types"
export async function run({
inputs,
@ -15,7 +18,7 @@ export async function run({
}: {
inputs: CreateRowStepInputs
appId: string
emitter: EventEmitter
emitter: ContextEmitter
}): Promise<CreateRowStepOutputs> {
if (inputs.row == null || inputs.row.tableId == null) {
return {

View File

@ -1,8 +1,11 @@
import { EventEmitter } from "events"
import { destroy } from "../../api/controllers/row"
import { buildCtx } from "./utils"
import { getError } from "../automationUtils"
import { DeleteRowStepInputs, DeleteRowStepOutputs } from "@budibase/types"
import {
ContextEmitter,
DeleteRowStepInputs,
DeleteRowStepOutputs,
} from "@budibase/types"
export async function run({
inputs,
@ -11,7 +14,7 @@ export async function run({
}: {
inputs: DeleteRowStepInputs
appId: string
emitter: EventEmitter
emitter: ContextEmitter
}): Promise<DeleteRowStepOutputs> {
if (inputs.id == null) {
return {

View File

@ -1,8 +1,8 @@
import { EventEmitter } from "events"
import * as queryController from "../../api/controllers/query"
import { buildCtx } from "./utils"
import * as automationUtils from "../automationUtils"
import {
ContextEmitter,
ExecuteQueryStepInputs,
ExecuteQueryStepOutputs,
} from "@budibase/types"
@ -14,7 +14,7 @@ export async function run({
}: {
inputs: ExecuteQueryStepInputs
appId: string
emitter: EventEmitter
emitter: ContextEmitter
}): Promise<ExecuteQueryStepOutputs> {
if (inputs.query == null) {
return {

View File

@ -2,10 +2,10 @@ import * as scriptController from "../../api/controllers/script"
import { buildCtx } from "./utils"
import * as automationUtils from "../automationUtils"
import {
ContextEmitter,
ExecuteScriptStepInputs,
ExecuteScriptStepOutputs,
} from "@budibase/types"
import { EventEmitter } from "events"
export async function run({
inputs,
@ -16,7 +16,7 @@ export async function run({
inputs: ExecuteScriptStepInputs
appId: string
context: object
emitter: EventEmitter
emitter: ContextEmitter
}): Promise<ExecuteScriptStepOutputs> {
if (inputs.code == null) {
return {

View File

@ -1,8 +1,11 @@
import { EventEmitter } from "events"
import * as rowController from "../../api/controllers/row"
import * as automationUtils from "../automationUtils"
import { buildCtx } from "./utils"
import { UpdateRowStepInputs, UpdateRowStepOutputs } from "@budibase/types"
import {
ContextEmitter,
UpdateRowStepInputs,
UpdateRowStepOutputs,
} from "@budibase/types"
export async function run({
inputs,
@ -11,7 +14,7 @@ export async function run({
}: {
inputs: UpdateRowStepInputs
appId: string
emitter: EventEmitter
emitter: ContextEmitter
}): Promise<UpdateRowStepOutputs> {
if (inputs.rowId == null || inputs.row == null) {
return {

View File

@ -1,4 +1,4 @@
import { EventEmitter } from "events"
import { ContextEmitter } from "@budibase/types"
export async function getFetchResponse(fetched: any) {
let status = fetched.status,
@ -22,7 +22,7 @@ export async function getFetchResponse(fetched: any) {
// opts can contain, body, params and version
export function buildCtx(
appId: string,
emitter?: EventEmitter | null,
emitter?: ContextEmitter | null,
opts: any = {}
) {
const ctx: any = {

View File

@ -1,5 +1,4 @@
import { v4 as uuidv4 } from "uuid"
import { testAutomation } from "../../../api/routes/tests/utilities/TestFunctions"
import { BUILTIN_ACTION_DEFINITIONS } from "../../actions"
import { TRIGGER_DEFINITIONS } from "../../triggers"
import {
@ -7,7 +6,6 @@ import {
AppActionTriggerOutputs,
Automation,
AutomationActionStepId,
AutomationResults,
AutomationStep,
AutomationStepInputs,
AutomationTrigger,
@ -24,6 +22,7 @@ import {
ExecuteQueryStepInputs,
ExecuteScriptStepInputs,
FilterStepInputs,
isDidNotTriggerResponse,
LoopStepInputs,
OpenAIStepInputs,
QueryRowsStepInputs,
@ -36,6 +35,7 @@ import {
SearchFilters,
ServerLogStepInputs,
SmtpEmailStepInputs,
TestAutomationRequest,
UpdateRowStepInputs,
WebhookTriggerInputs,
WebhookTriggerOutputs,
@ -279,7 +279,7 @@ class StepBuilder extends BaseStepBuilder {
class AutomationBuilder extends BaseStepBuilder {
private automationConfig: Automation
private config: TestConfiguration
private triggerOutputs: any
private triggerOutputs: TriggerOutputs
private triggerSet = false
constructor(
@ -398,21 +398,19 @@ class AutomationBuilder extends BaseStepBuilder {
async run() {
const automation = await this.save()
const results = await testAutomation(
this.config,
automation,
this.triggerOutputs
const response = await this.config.api.automation.test(
automation._id!,
this.triggerOutputs as TestAutomationRequest
)
return this.processResults(results)
}
private processResults(results: {
body: AutomationResults
}): AutomationResults {
results.body.steps.shift()
if (isDidNotTriggerResponse(response)) {
throw new Error(response.message)
}
response.steps.shift()
return {
trigger: results.body.trigger,
steps: results.body.steps,
trigger: response.trigger,
steps: response.steps,
}
}
}

View File

@ -21,6 +21,7 @@ import {
AutomationRowEvent,
UserBindings,
AutomationResults,
DidNotTriggerResponse,
} from "@budibase/types"
import { executeInThread } from "../threads/automation"
import { dataFilters, sdk } from "@budibase/shared-core"
@ -33,14 +34,6 @@ const JOB_OPTS = {
import * as automationUtils from "../automations/automationUtils"
import { doesTableExist } from "../sdk/app/tables/getters"
type DidNotTriggerResponse = {
outputs: {
success: false
status: AutomationStatus.STOPPED
}
message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET
}
async function getAllAutomations() {
const db = context.getAppDB()
let automations = await db.allDocs<Automation>(
@ -156,14 +149,26 @@ export function isAutomationResults(
)
}
interface AutomationTriggerParams {
fields: Record<string, any>
timeout?: number
appId?: string
user?: UserBindings
}
export async function externalTrigger(
automation: Automation,
params: {
fields: Record<string, any>
timeout?: number
appId?: string
user?: UserBindings
},
params: AutomationTriggerParams,
options: { getResponses: true }
): Promise<AutomationResults | DidNotTriggerResponse>
export async function externalTrigger(
automation: Automation,
params: AutomationTriggerParams,
options?: { getResponses: false }
): Promise<AutomationJob | DidNotTriggerResponse>
export async function externalTrigger(
automation: Automation,
params: AutomationTriggerParams,
{ getResponses }: { getResponses?: boolean } = {}
): Promise<AutomationResults | DidNotTriggerResponse | AutomationJob> {
if (automation.disabled) {

View File

@ -1,4 +1,9 @@
import { Automation, FetchAutomationResponse } from "@budibase/types"
import {
Automation,
FetchAutomationResponse,
TestAutomationRequest,
TestAutomationResponse,
} from "@budibase/types"
import { Expectations, TestAPI } from "./base"
export class AutomationAPI extends TestAPI {
@ -33,4 +38,18 @@ export class AutomationAPI extends TestAPI {
})
return result
}
test = async (
id: string,
body: TestAutomationRequest,
expectations?: Expectations
): Promise<TestAutomationResponse> => {
return await this._post<TestAutomationResponse>(
`/api/automations/${id}/test`,
{
body,
expectations,
}
)
}
}

View File

@ -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

View File

@ -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) {

View File

@ -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() {

View File

@ -2,10 +2,12 @@ import {
Automation,
AutomationActionStepId,
AutomationLogPage,
AutomationResults,
AutomationStatus,
AutomationStepDefinition,
AutomationTriggerDefinition,
AutomationTriggerStepId,
DidNotTriggerResponse,
Row,
} from "../../../documents"
import { DocumentDestroyResponse } from "@budibase/nano"
@ -74,4 +76,10 @@ export interface TestAutomationRequest {
fields: Record<string, any>
row?: Row
}
export interface TestAutomationResponse {}
export type TestAutomationResponse = AutomationResults | DidNotTriggerResponse
export function isDidNotTriggerResponse(
response: TestAutomationResponse
): response is DidNotTriggerResponse {
return !!("message" in response && response.message)
}

View File

@ -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",
@ -205,6 +205,14 @@ export interface AutomationResults {
}[]
}
export interface DidNotTriggerResponse {
outputs: {
success: false
status: AutomationStatus.STOPPED
}
message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET
}
export interface AutomationLog extends AutomationResults, Document {
automationName: string
_rev?: string
@ -218,7 +226,7 @@ export interface AutomationLogPage {
export interface AutomationStepInputBase {
context: Record<string, any>
emitter: EventEmitter
emitter: ContextEmitter
appId: string
apiKey?: string
}