Merge branch 'master' into state-and-bindings-panels
This commit is contained in:
commit
6eeee862e8
|
@ -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": {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
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,
|
||||
{
|
||||
...testInput,
|
||||
appId: ctx.appId,
|
||||
user: sdk.users.getUserContextBindings(ctx.user),
|
||||
},
|
||||
{ ...prepareTestInput(body), appId, user },
|
||||
{ getResponses: true }
|
||||
)
|
||||
// save a test history run
|
||||
await updateTestHistory(ctx.appId, automation, {
|
||||
...ctx.request.body,
|
||||
occurredAt: new Date().getTime(),
|
||||
})
|
||||
await clearTestFlag(automation._id!)
|
||||
ctx.body = response
|
||||
|
||||
await events.automation.tested(automation)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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)
|
||||
|
||||
if (isDidNotTriggerResponse(response)) {
|
||||
throw new Error(response.message)
|
||||
}
|
||||
|
||||
private processResults(results: {
|
||||
body: AutomationResults
|
||||
}): AutomationResults {
|
||||
results.body.steps.shift()
|
||||
response.steps.shift()
|
||||
return {
|
||||
trigger: results.body.trigger,
|
||||
steps: results.body.steps,
|
||||
trigger: response.trigger,
|
||||
steps: response.steps,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
|||
)
|
||||
}
|
||||
|
||||
export async function externalTrigger(
|
||||
automation: Automation,
|
||||
params: {
|
||||
interface AutomationTriggerParams {
|
||||
fields: Record<string, any>
|
||||
timeout?: number
|
||||
appId?: string
|
||||
user?: UserBindings
|
||||
},
|
||||
}
|
||||
|
||||
export async function externalTrigger(
|
||||
automation: Automation,
|
||||
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) {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue