Merge branch 'state-and-bindings-panels' of github.com:Budibase/budibase into bindings-panel
This commit is contained in:
commit
bdd1f765cc
|
@ -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": {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
const processModals = () => {
|
||||
const defaultCacheFn = key => {
|
||||
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
||||
temporalStore.setExpiring(key, {}, oneDayInSeconds)
|
||||
}
|
||||
|
||||
const dismissableModals = [
|
||||
|
@ -50,7 +50,7 @@
|
|||
},
|
||||
]
|
||||
return dismissableModals.filter(modal => {
|
||||
return !temporalStore.actions.getExpiring(modal.key) && modal.criteria()
|
||||
return !temporalStore.getExpiring(modal.key) && modal.criteria()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { BANNER_TYPES } from "@budibase/bbui"
|
|||
const oneDayInSeconds = 86400
|
||||
|
||||
const defaultCacheFn = key => {
|
||||
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds)
|
||||
temporalStore.setExpiring(key, {}, oneDayInSeconds)
|
||||
}
|
||||
|
||||
const upgradeAction = key => {
|
||||
|
@ -148,7 +148,7 @@ export const getBanners = () => {
|
|||
buildUsersAboveLimitBanner(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER),
|
||||
].filter(licensingBanner => {
|
||||
return (
|
||||
!temporalStore.actions.getExpiring(licensingBanner.key) &&
|
||||
!temporalStore.getExpiring(licensingBanner.key) &&
|
||||
licensingBanner.criteria()
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
$: useAccountPortal = cloud && !$admin.disableAccountPortal
|
||||
|
||||
navigation.actions.init($redirect)
|
||||
navigation.init($redirect)
|
||||
|
||||
const validateTenantId = async () => {
|
||||
const host = window.location.host
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { it, expect, describe, beforeEach, vi } from "vitest"
|
||||
import { createAdminStore } from "./admin"
|
||||
import { AdminStore } from "./admin"
|
||||
import { writable, get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { auth } from "@/stores/portal"
|
||||
|
@ -46,16 +46,7 @@ describe("admin store", () => {
|
|||
ctx.writableReturn = { update: vi.fn(), subscribe: vi.fn() }
|
||||
writable.mockReturnValue(ctx.writableReturn)
|
||||
|
||||
ctx.returnedStore = createAdminStore()
|
||||
})
|
||||
|
||||
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(),
|
||||
})
|
||||
ctx.returnedStore = new AdminStore()
|
||||
})
|
||||
|
||||
describe("init method", () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { get } from "svelte/store"
|
||||
import { API } from "@/api"
|
||||
import { auth } from "@/stores/portal"
|
||||
import { banner } from "@budibase/bbui"
|
||||
|
@ -7,15 +7,17 @@ import {
|
|||
GetEnvironmentResponse,
|
||||
SystemStatusResponse,
|
||||
} from "@budibase/types"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
interface PortalAdminStore extends GetEnvironmentResponse {
|
||||
interface AdminState extends GetEnvironmentResponse {
|
||||
loaded: boolean
|
||||
checklist?: ConfigChecklistResponse
|
||||
status?: SystemStatusResponse
|
||||
}
|
||||
|
||||
export function createAdminStore() {
|
||||
const admin = writable<PortalAdminStore>({
|
||||
export class AdminStore extends BudiStore<AdminState> {
|
||||
constructor() {
|
||||
super({
|
||||
loaded: false,
|
||||
multiTenancy: false,
|
||||
cloud: false,
|
||||
|
@ -24,25 +26,25 @@ export function createAdminStore() {
|
|||
offlineMode: false,
|
||||
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
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
async function getEnvironment() {
|
||||
async getEnvironment() {
|
||||
const environment = await API.getEnvironment()
|
||||
admin.update(store => {
|
||||
this.update(store => {
|
||||
store.multiTenancy = environment.multiTenancy
|
||||
store.cloud = environment.cloud
|
||||
store.disableAccountPortal = environment.disableAccountPortal
|
||||
|
@ -56,43 +58,36 @@ export function createAdminStore() {
|
|||
})
|
||||
}
|
||||
|
||||
const checkStatus = async () => {
|
||||
const health = get(admin)?.status?.health
|
||||
async checkStatus() {
|
||||
const health = get(this.store).status?.health
|
||||
if (!health?.passing) {
|
||||
await banner.showStatus()
|
||||
}
|
||||
}
|
||||
|
||||
async function getSystemStatus() {
|
||||
async getSystemStatus() {
|
||||
const status = await API.getSystemStatus()
|
||||
admin.update(store => {
|
||||
this.update(store => {
|
||||
store.status = status
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
async function getChecklist() {
|
||||
async getChecklist() {
|
||||
const tenantId = get(auth).tenantId
|
||||
const checklist = await API.getChecklist(tenantId)
|
||||
admin.update(store => {
|
||||
this.update(store => {
|
||||
store.checklist = checklist
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
function unload() {
|
||||
admin.update(store => {
|
||||
unload() {
|
||||
this.update(store => {
|
||||
store.loaded = false
|
||||
return store
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: admin.subscribe,
|
||||
init,
|
||||
unload,
|
||||
getChecklist,
|
||||
}
|
||||
}
|
||||
|
||||
export const admin = createAdminStore()
|
||||
export const admin = new AdminStore()
|
||||
|
|
|
@ -13,7 +13,7 @@ interface PortalAuditLogsStore {
|
|||
logs?: SearchAuditLogsResponse
|
||||
}
|
||||
|
||||
export class AuditLogsStore extends BudiStore<PortalAuditLogsStore> {
|
||||
class AuditLogsStore extends BudiStore<PortalAuditLogsStore> {
|
||||
constructor() {
|
||||
super({})
|
||||
}
|
||||
|
|
|
@ -1,38 +1,31 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { BudiStore } from "../BudiStore"
|
||||
|
||||
type GotoFuncType = (path: string) => void
|
||||
|
||||
interface PortalNavigationStore {
|
||||
interface NavigationState {
|
||||
initialisated: boolean
|
||||
goto: GotoFuncType
|
||||
}
|
||||
|
||||
export function createNavigationStore() {
|
||||
const store = writable<PortalNavigationStore>({
|
||||
class NavigationStore extends BudiStore<NavigationState> {
|
||||
constructor() {
|
||||
super({
|
||||
initialisated: false,
|
||||
goto: undefined as any,
|
||||
})
|
||||
const { set, subscribe } = store
|
||||
}
|
||||
|
||||
const init = (gotoFunc: GotoFuncType) => {
|
||||
init(gotoFunc: GotoFuncType) {
|
||||
if (typeof gotoFunc !== "function") {
|
||||
throw new Error(
|
||||
`gotoFunc must be a function, found a "${typeof gotoFunc}" instead`
|
||||
)
|
||||
}
|
||||
|
||||
set({
|
||||
this.set({
|
||||
initialisated: true,
|
||||
goto: gotoFunc,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
actions: {
|
||||
init,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const navigation = createNavigationStore()
|
||||
export const navigation = new NavigationStore()
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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,18 +89,23 @@ 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() {
|
||||
return {
|
||||
|
|
|
@ -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