Merge branch 'state-and-bindings-panels' of github.com:Budibase/budibase into bindings-panel

This commit is contained in:
Andrew Kingston 2025-01-23 11:19:38 +00:00
commit bdd1f765cc
No known key found for this signature in database
31 changed files with 325 additions and 294 deletions

View File

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

View File

@ -20,7 +20,7 @@
const processModals = () => { const processModals = () => {
const defaultCacheFn = key => { const defaultCacheFn = key => {
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds) temporalStore.setExpiring(key, {}, oneDayInSeconds)
} }
const dismissableModals = [ const dismissableModals = [
@ -50,7 +50,7 @@
}, },
] ]
return dismissableModals.filter(modal => { return dismissableModals.filter(modal => {
return !temporalStore.actions.getExpiring(modal.key) && modal.criteria() return !temporalStore.getExpiring(modal.key) && modal.criteria()
}) })
} }

View File

@ -6,7 +6,7 @@ import { BANNER_TYPES } from "@budibase/bbui"
const oneDayInSeconds = 86400 const oneDayInSeconds = 86400
const defaultCacheFn = key => { const defaultCacheFn = key => {
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds) temporalStore.setExpiring(key, {}, oneDayInSeconds)
} }
const upgradeAction = key => { const upgradeAction = key => {
@ -148,7 +148,7 @@ export const getBanners = () => {
buildUsersAboveLimitBanner(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER), buildUsersAboveLimitBanner(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER),
].filter(licensingBanner => { ].filter(licensingBanner => {
return ( return (
!temporalStore.actions.getExpiring(licensingBanner.key) && !temporalStore.getExpiring(licensingBanner.key) &&
licensingBanner.criteria() licensingBanner.criteria()
) )
}) })

View File

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

View File

@ -18,7 +18,7 @@
$: useAccountPortal = cloud && !$admin.disableAccountPortal $: useAccountPortal = cloud && !$admin.disableAccountPortal
navigation.actions.init($redirect) navigation.init($redirect)
const validateTenantId = async () => { const validateTenantId = async () => {
const host = window.location.host const host = window.location.host

View File

@ -1,5 +1,5 @@
import { it, expect, describe, beforeEach, vi } from "vitest" import { it, expect, describe, beforeEach, vi } from "vitest"
import { createAdminStore } from "./admin" import { AdminStore } from "./admin"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { auth } from "@/stores/portal" import { auth } from "@/stores/portal"
@ -46,16 +46,7 @@ describe("admin store", () => {
ctx.writableReturn = { update: vi.fn(), subscribe: vi.fn() } ctx.writableReturn = { update: vi.fn(), subscribe: vi.fn() }
writable.mockReturnValue(ctx.writableReturn) writable.mockReturnValue(ctx.writableReturn)
ctx.returnedStore = createAdminStore() ctx.returnedStore = new AdminStore()
})
it("returns the created store", ctx => {
expect(ctx.returnedStore).toEqual({
subscribe: expect.toBe(ctx.writableReturn.subscribe),
init: expect.toBeFunc(),
unload: expect.toBeFunc(),
getChecklist: expect.toBeFunc(),
})
}) })
describe("init method", () => { describe("init method", () => {

View File

@ -1,4 +1,4 @@
import { writable, get } from "svelte/store" import { get } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { auth } from "@/stores/portal" import { auth } from "@/stores/portal"
import { banner } from "@budibase/bbui" import { banner } from "@budibase/bbui"
@ -7,15 +7,17 @@ import {
GetEnvironmentResponse, GetEnvironmentResponse,
SystemStatusResponse, SystemStatusResponse,
} from "@budibase/types" } from "@budibase/types"
import { BudiStore } from "../BudiStore"
interface PortalAdminStore extends GetEnvironmentResponse { interface AdminState extends GetEnvironmentResponse {
loaded: boolean loaded: boolean
checklist?: ConfigChecklistResponse checklist?: ConfigChecklistResponse
status?: SystemStatusResponse status?: SystemStatusResponse
} }
export function createAdminStore() { export class AdminStore extends BudiStore<AdminState> {
const admin = writable<PortalAdminStore>({ constructor() {
super({
loaded: false, loaded: false,
multiTenancy: false, multiTenancy: false,
cloud: false, cloud: false,
@ -24,25 +26,25 @@ export function createAdminStore() {
offlineMode: false, offlineMode: false,
maintenance: [], maintenance: [],
}) })
async function init() {
await getChecklist()
await getEnvironment()
// enable system status checks in the cloud
if (get(admin).cloud) {
await getSystemStatus()
checkStatus()
} }
admin.update(store => { async init() {
await this.getChecklist()
await this.getEnvironment()
// enable system status checks in the cloud
if (get(this.store).cloud) {
await this.getSystemStatus()
this.checkStatus()
}
this.update(store => {
store.loaded = true store.loaded = true
return store return store
}) })
} }
async function getEnvironment() { async getEnvironment() {
const environment = await API.getEnvironment() const environment = await API.getEnvironment()
admin.update(store => { this.update(store => {
store.multiTenancy = environment.multiTenancy store.multiTenancy = environment.multiTenancy
store.cloud = environment.cloud store.cloud = environment.cloud
store.disableAccountPortal = environment.disableAccountPortal store.disableAccountPortal = environment.disableAccountPortal
@ -56,43 +58,36 @@ export function createAdminStore() {
}) })
} }
const checkStatus = async () => { async checkStatus() {
const health = get(admin)?.status?.health const health = get(this.store).status?.health
if (!health?.passing) { if (!health?.passing) {
await banner.showStatus() await banner.showStatus()
} }
} }
async function getSystemStatus() { async getSystemStatus() {
const status = await API.getSystemStatus() const status = await API.getSystemStatus()
admin.update(store => { this.update(store => {
store.status = status store.status = status
return store return store
}) })
} }
async function getChecklist() { async getChecklist() {
const tenantId = get(auth).tenantId const tenantId = get(auth).tenantId
const checklist = await API.getChecklist(tenantId) const checklist = await API.getChecklist(tenantId)
admin.update(store => { this.update(store => {
store.checklist = checklist store.checklist = checklist
return store return store
}) })
} }
function unload() { unload() {
admin.update(store => { this.update(store => {
store.loaded = false store.loaded = false
return store return store
}) })
} }
return {
subscribe: admin.subscribe,
init,
unload,
getChecklist,
}
} }
export const admin = createAdminStore() export const admin = new AdminStore()

View File

@ -13,7 +13,7 @@ interface PortalAuditLogsStore {
logs?: SearchAuditLogsResponse logs?: SearchAuditLogsResponse
} }
export class AuditLogsStore extends BudiStore<PortalAuditLogsStore> { class AuditLogsStore extends BudiStore<PortalAuditLogsStore> {
constructor() { constructor() {
super({}) super({})
} }

View File

@ -1,38 +1,31 @@
import { writable } from "svelte/store" import { BudiStore } from "../BudiStore"
type GotoFuncType = (path: string) => void type GotoFuncType = (path: string) => void
interface PortalNavigationStore { interface NavigationState {
initialisated: boolean initialisated: boolean
goto: GotoFuncType goto: GotoFuncType
} }
export function createNavigationStore() { class NavigationStore extends BudiStore<NavigationState> {
const store = writable<PortalNavigationStore>({ constructor() {
super({
initialisated: false, initialisated: false,
goto: undefined as any, goto: undefined as any,
}) })
const { set, subscribe } = store }
const init = (gotoFunc: GotoFuncType) => { init(gotoFunc: GotoFuncType) {
if (typeof gotoFunc !== "function") { if (typeof gotoFunc !== "function") {
throw new Error( throw new Error(
`gotoFunc must be a function, found a "${typeof gotoFunc}" instead` `gotoFunc must be a function, found a "${typeof gotoFunc}" instead`
) )
} }
this.set({
set({
initialisated: true, initialisated: true,
goto: gotoFunc, goto: gotoFunc,
}) })
} }
return {
subscribe,
actions: {
init,
},
}
} }
export const navigation = createNavigationStore() export const navigation = new NavigationStore()

View File

@ -1,16 +0,0 @@
import { writable } from "svelte/store"
import { API } from "@/api"
export function templatesStore() {
const { subscribe, set } = writable([])
return {
subscribe,
load: async () => {
const templates = await API.getAppTemplates()
set(templates)
},
}
}
export const templates = templatesStore()

View File

@ -0,0 +1,16 @@
import { API } from "@/api"
import { BudiStore } from "../BudiStore"
import { TemplateMetadata } from "@budibase/types"
class TemplateStore extends BudiStore<TemplateMetadata[]> {
constructor() {
super([])
}
async load() {
const templates = await API.getAppTemplates()
this.set(templates)
}
}
export const templates = new TemplateStore()

View File

@ -1,45 +0,0 @@
import { createLocalStorageStore } from "@budibase/frontend-core"
import { get } from "svelte/store"
export const createTemporalStore = () => {
const initialValue = {}
const localStorageKey = `bb-temporal`
const store = createLocalStorageStore(localStorageKey, initialValue)
const setExpiring = (key, data, duration) => {
const updated = {
...data,
expiry: Date.now() + duration * 1000,
}
store.update(state => ({
...state,
[key]: updated,
}))
}
const getExpiring = key => {
const entry = get(store)[key]
if (!entry) {
return
}
const currentExpiry = entry.expiry
if (currentExpiry < Date.now()) {
store.update(state => {
delete state[key]
return state
})
return null
} else {
return entry
}
}
return {
subscribe: store.subscribe,
actions: { setExpiring, getExpiring },
}
}
export const temporalStore = createTemporalStore()

View File

@ -0,0 +1,53 @@
import { get } from "svelte/store"
import { BudiStore, PersistenceType } from "../BudiStore"
type TemporalItem = Record<string, any> & { expiry: number }
type TemporalState = Record<string, TemporalItem>
class TemporalStore extends BudiStore<TemporalState> {
constructor() {
super(
{},
{
persistence: {
key: "bb-temporal",
type: PersistenceType.LOCAL,
},
}
)
}
setExpiring = (
key: string,
data: Record<string, any>,
durationSeconds: number
) => {
const updated: TemporalItem = {
...data,
expiry: Date.now() + durationSeconds * 1000,
}
this.update(state => ({
...state,
[key]: updated,
}))
}
getExpiring(key: string) {
const entry = get(this.store)[key]
if (!entry) {
return null
}
const currentExpiry = entry.expiry
if (currentExpiry < Date.now()) {
this.update(state => {
delete state[key]
return state
})
return null
} else {
return entry
}
}
}
export const temporalStore = new TemporalStore()

View File

@ -1,37 +0,0 @@
import { createLocalStorageStore } from "@budibase/frontend-core"
import { derived } from "svelte/store"
import {
DefaultBuilderTheme,
ensureValidTheme,
getThemeClassNames,
ThemeOptions,
ThemeClassPrefix,
} from "@budibase/shared-core"
export const getThemeStore = () => {
const themeElement = document.documentElement
const initialValue = {
theme: DefaultBuilderTheme,
}
const store = createLocalStorageStore("bb-theme", initialValue)
const derivedStore = derived(store, $store => ({
...$store,
theme: ensureValidTheme($store.theme, DefaultBuilderTheme),
}))
// Update theme class when store changes
derivedStore.subscribe(({ theme }) => {
const classNames = getThemeClassNames(theme).split(" ")
ThemeOptions.forEach(option => {
const className = `${ThemeClassPrefix}${option.id}`
themeElement.classList.toggle(className, classNames.includes(className))
})
})
return {
...store,
subscribe: derivedStore.subscribe,
}
}
export const themeStore = getThemeStore()

View File

@ -0,0 +1,45 @@
import { derived, Writable } from "svelte/store"
import {
DefaultBuilderTheme,
ensureValidTheme,
getThemeClassNames,
ThemeOptions,
ThemeClassPrefix,
} from "@budibase/shared-core"
import { Theme } from "@budibase/types"
import { DerivedBudiStore, PersistenceType } from "../BudiStore"
interface ThemeState {
theme: Theme
}
class ThemeStore extends DerivedBudiStore<ThemeState, ThemeState> {
constructor() {
const makeDerivedStore = (store: Writable<ThemeState>) => {
return derived(store, $store => ({
...$store,
theme: ensureValidTheme($store.theme, DefaultBuilderTheme),
}))
}
super({ theme: DefaultBuilderTheme }, makeDerivedStore, {
persistence: {
key: "bb-theme",
type: PersistenceType.LOCAL,
},
})
// Update theme class when store changes
this.subscribe(({ theme }) => {
const classNames = getThemeClassNames(theme).split(" ")
ThemeOptions.forEach(option => {
const className = `${ThemeClassPrefix}${option.id}`
document.documentElement.classList.toggle(
className,
classNames.includes(className)
)
})
})
}
}
export const themeStore = new ThemeStore()

View File

@ -2,7 +2,7 @@ import * as triggers from "../../automations/triggers"
import { sdk as coreSdk } from "@budibase/shared-core" import { sdk as coreSdk } from "@budibase/shared-core"
import { DocumentType } from "../../db/utils" import { DocumentType } from "../../db/utils"
import { updateTestHistory, removeDeprecated } from "../../automations/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 { context, cache, events, db as dbCore } from "@budibase/backend-core"
import { automations, features } from "@budibase/pro" import { automations, features } from "@budibase/pro"
import { import {
@ -231,24 +231,25 @@ export async function test(
ctx: UserCtx<TestAutomationRequest, TestAutomationResponse> ctx: UserCtx<TestAutomationRequest, TestAutomationResponse>
) { ) {
const db = context.getAppDB() const db = context.getAppDB()
let automation = await db.get<Automation>(ctx.params.id) const automation = await db.tryGet<Automation>(ctx.params.id)
await setTestFlag(automation._id!) if (!automation) {
const testInput = prepareTestInput(ctx.request.body) ctx.throw(404, `Automation ${ctx.params.id} not found`)
const response = await triggers.externalTrigger( }
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, automation,
{ { ...prepareTestInput(body), appId, user },
...testInput,
appId: ctx.appId,
user: sdk.users.getUserContextBindings(ctx.user),
},
{ getResponses: true } { 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) await events.automation.tested(automation)
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@ import {
AutomationRowEvent, AutomationRowEvent,
UserBindings, UserBindings,
AutomationResults, AutomationResults,
DidNotTriggerResponse,
} from "@budibase/types" } from "@budibase/types"
import { executeInThread } from "../threads/automation" import { executeInThread } from "../threads/automation"
import { dataFilters, sdk } from "@budibase/shared-core" import { dataFilters, sdk } from "@budibase/shared-core"
@ -33,14 +34,6 @@ const JOB_OPTS = {
import * as automationUtils from "../automations/automationUtils" import * as automationUtils from "../automations/automationUtils"
import { doesTableExist } from "../sdk/app/tables/getters" import { doesTableExist } from "../sdk/app/tables/getters"
type DidNotTriggerResponse = {
outputs: {
success: false
status: AutomationStatus.STOPPED
}
message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET
}
async function getAllAutomations() { async function getAllAutomations() {
const db = context.getAppDB() const db = context.getAppDB()
let automations = await db.allDocs<Automation>( let automations = await db.allDocs<Automation>(
@ -156,14 +149,26 @@ export function isAutomationResults(
) )
} }
export async function externalTrigger( interface AutomationTriggerParams {
automation: Automation,
params: {
fields: Record<string, any> fields: Record<string, any>
timeout?: number timeout?: number
appId?: string appId?: string
user?: UserBindings 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 } = {} { getResponses }: { getResponses?: boolean } = {}
): Promise<AutomationResults | DidNotTriggerResponse | AutomationJob> { ): Promise<AutomationResults | DidNotTriggerResponse | AutomationJob> {
if (automation.disabled) { 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" import { Expectations, TestAPI } from "./base"
export class AutomationAPI extends TestAPI { export class AutomationAPI extends TestAPI {
@ -33,4 +38,18 @@ export class AutomationAPI extends TestAPI {
}) })
return result 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, LoopStep,
UserBindings, UserBindings,
isBasicSearchOperator, isBasicSearchOperator,
ContextEmitter,
} from "@budibase/types" } from "@budibase/types"
import { import {
AutomationContext, AutomationContext,
@ -71,6 +72,24 @@ function getLoopIterations(loopStep: LoopStep) {
return 0 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. * 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 * 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 chainCount: number
private appId: string private appId: string
private automation: Automation private automation: Automation
private emitter: any private emitter: ContextEmitter
private context: AutomationContext private context: AutomationContext
private job: Job private job: Job
private loopStepOutputs: LoopStep[] private loopStepOutputs: LoopStep[]
@ -270,20 +289,9 @@ class Orchestrator {
appId: this.appId, appId: this.appId,
automationId: this.automation._id, automationId: this.automation._id,
}) })
this.context.env = await sdkUtils.getEnvironmentVariables()
this.context.user = this.currentUser
try { await enrichBaseContext(this.context)
const { config } = await configs.getSettingsConfigDoc() this.context.user = this.currentUser
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 = {}
}
let metadata let metadata

View File

@ -58,30 +58,14 @@ export function checkSlashesInUrl(url: string) {
export async function updateEntityMetadata( export async function updateEntityMetadata(
type: string, type: string,
entityId: string, entityId: string,
updateFn: any updateFn: (metadata: Document) => Document
) { ) {
const db = context.getAppDB() const db = context.getAppDB()
const id = generateMetadataID(type, entityId) const id = generateMetadataID(type, entityId)
// read it to see if it exists, we'll overwrite it no matter what const metadata = updateFn((await db.tryGet(id)) || {})
let rev, metadata: Document
try {
const oldMetadata = await db.get<any>(id)
rev = oldMetadata._rev
metadata = updateFn(oldMetadata)
} catch (err) {
rev = null
metadata = updateFn({})
}
metadata._id = id metadata._id = id
if (rev) {
metadata._rev = rev
}
const response = await db.put(metadata) const response = await db.put(metadata)
return { return { ...metadata, _id: id, _rev: response.rev }
...metadata,
_id: id,
_rev: response.rev,
}
} }
export async function saveEntityMetadata( export async function saveEntityMetadata(
@ -89,26 +73,17 @@ export async function saveEntityMetadata(
entityId: string, entityId: string,
metadata: Document metadata: Document
): Promise<Document> { ): Promise<Document> {
return updateEntityMetadata(type, entityId, () => { return updateEntityMetadata(type, entityId, () => metadata)
return metadata
})
} }
export async function deleteEntityMetadata(type: string, entityId: string) { export async function deleteEntityMetadata(type: string, entityId: string) {
const db = context.getAppDB() const db = context.getAppDB()
const id = generateMetadataID(type, entityId) const id = generateMetadataID(type, entityId)
let rev const metadata = await db.tryGet(id)
try { if (!metadata) {
const metadata = await db.get<any>(id) return
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)
} }
await db.remove(metadata)
} }
export function escapeDangerousCharacters(string: string) { 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) 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) { export async function checkTestFlag(id: string) {
const flag = await flagClient?.get(id) const flag = await flagClient?.get(id)
return !!(flag && flag.testing) 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) await devAppClient.delete(id)
}
} }
export function getSocketPubSubClients() { export function getSocketPubSubClients() {

View File

@ -2,10 +2,12 @@ import {
Automation, Automation,
AutomationActionStepId, AutomationActionStepId,
AutomationLogPage, AutomationLogPage,
AutomationResults,
AutomationStatus, AutomationStatus,
AutomationStepDefinition, AutomationStepDefinition,
AutomationTriggerDefinition, AutomationTriggerDefinition,
AutomationTriggerStepId, AutomationTriggerStepId,
DidNotTriggerResponse,
Row, Row,
} from "../../../documents" } from "../../../documents"
import { DocumentDestroyResponse } from "@budibase/nano" import { DocumentDestroyResponse } from "@budibase/nano"
@ -74,4 +76,10 @@ export interface TestAutomationRequest {
fields: Record<string, any> fields: Record<string, any>
row?: Row 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 { Document } from "../../document"
import { EventEmitter } from "events"
import { User } from "../../global" import { User } from "../../global"
import { ReadStream } from "fs" import { ReadStream } from "fs"
import { Row } from "../row" import { Row } from "../row"
import { Table } from "../table" import { Table } from "../table"
import { AutomationStep, AutomationTrigger } from "./schema" import { AutomationStep, AutomationTrigger } from "./schema"
import { ContextEmitter } from "../../../sdk"
export enum AutomationIOType { export enum AutomationIOType {
OBJECT = "object", 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 { export interface AutomationLog extends AutomationResults, Document {
automationName: string automationName: string
_rev?: string _rev?: string
@ -218,7 +226,7 @@ export interface AutomationLogPage {
export interface AutomationStepInputBase { export interface AutomationStepInputBase {
context: Record<string, any> context: Record<string, any>
emitter: EventEmitter emitter: ContextEmitter
appId: string appId: string
apiKey?: string apiKey?: string
} }