Merge branch 'master' into fix-options-with-commas-and-types

This commit is contained in:
Sam Rose 2024-12-03 12:23:25 +00:00 committed by GitHub
commit ed49f81639
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 786 additions and 220 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.18", "version": "3.2.19",
"npmClient": "yarn", "npmClient": "yarn",
"concurrency": 20, "concurrency": 20,
"command": { "command": {

View File

@ -1,16 +1,22 @@
import { events, context } from "@budibase/backend-core" import { events, context } from "@budibase/backend-core"
import { AnalyticsPingRequest, App, PingSource } from "@budibase/types" import {
AnalyticsPingRequest,
App,
PingSource,
Ctx,
AnalyticsEnabledResponse,
} from "@budibase/types"
import { DocumentType, isDevAppID } from "../../db/utils" import { DocumentType, isDevAppID } from "../../db/utils"
export const isEnabled = async (ctx: any) => { export const isEnabled = async (ctx: Ctx<void, AnalyticsEnabledResponse>) => {
const enabled = await events.analytics.enabled() const enabled = await events.analytics.enabled()
ctx.body = { ctx.body = {
enabled, enabled,
} }
} }
export const ping = async (ctx: any) => { export const ping = async (ctx: Ctx<AnalyticsPingRequest, void>) => {
const body = ctx.request.body as AnalyticsPingRequest const body = ctx.request.body
switch (body.source) { switch (body.source) {
case PingSource.APP: { case PingSource.APP: {

View File

@ -1,18 +1,25 @@
import { db as dbCore, tenancy } from "@budibase/backend-core" import { db as dbCore, tenancy } from "@budibase/backend-core"
import { BBContext, Document } from "@budibase/types" import {
Document,
UserCtx,
ApiKeyDoc,
ApiKeyFetchResponse,
UpdateApiKeyRequest,
UpdateApiKeyResponse,
} from "@budibase/types"
const KEYS_DOC = dbCore.StaticDatabases.GLOBAL.docs.apiKeys const KEYS_DOC = dbCore.StaticDatabases.GLOBAL.docs.apiKeys
async function getBuilderMainDoc() { async function getBuilderMainDoc() {
const db = tenancy.getGlobalDB() const db = tenancy.getGlobalDB()
try { const doc = await db.tryGet<ApiKeyDoc>(KEYS_DOC)
return await db.get<any>(KEYS_DOC) if (!doc) {
} catch (err) {
// doesn't exist yet, nothing to get
return { return {
_id: KEYS_DOC, _id: KEYS_DOC,
apiKeys: {},
} }
} }
return doc
} }
async function setBuilderMainDoc(doc: Document) { async function setBuilderMainDoc(doc: Document) {
@ -22,7 +29,7 @@ async function setBuilderMainDoc(doc: Document) {
return db.put(doc) return db.put(doc)
} }
export async function fetch(ctx: BBContext) { export async function fetch(ctx: UserCtx<void, ApiKeyFetchResponse>) {
try { try {
const mainDoc = await getBuilderMainDoc() const mainDoc = await getBuilderMainDoc()
ctx.body = mainDoc.apiKeys ? mainDoc.apiKeys : {} ctx.body = mainDoc.apiKeys ? mainDoc.apiKeys : {}
@ -32,7 +39,9 @@ export async function fetch(ctx: BBContext) {
} }
} }
export async function update(ctx: BBContext) { export async function update(
ctx: UserCtx<UpdateApiKeyRequest, UpdateApiKeyResponse>
) {
const key = ctx.params.key const key = ctx.params.key
const value = ctx.request.body.value const value = ctx.request.body.value

View File

@ -59,6 +59,15 @@ import {
BBReferenceFieldSubType, BBReferenceFieldSubType,
Row, Row,
BBRequest, BBRequest,
SyncAppResponse,
CreateAppResponse,
FetchAppsResponse,
UpdateAppClientResponse,
RevertAppClientResponse,
DeleteAppResponse,
ImportToUpdateAppRequest,
ImportToUpdateAppResponse,
SetRevertableAppVersionRequest,
} from "@budibase/types" } from "@budibase/types"
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
import sdk from "../../sdk" import sdk from "../../sdk"
@ -166,7 +175,7 @@ async function createInstance(appId: string, template: AppTemplate) {
return { _id: appId } return { _id: appId }
} }
export const addSampleData = async (ctx: UserCtx) => { export const addSampleData = async (ctx: UserCtx<void, void>) => {
const db = context.getAppDB() const db = context.getAppDB()
try { try {
@ -182,7 +191,7 @@ export const addSampleData = async (ctx: UserCtx) => {
ctx.status = 200 ctx.status = 200
} }
export async function fetch(ctx: UserCtx<void, App[]>) { export async function fetch(ctx: UserCtx<void, FetchAppsResponse>) {
ctx.body = await sdk.applications.fetch( ctx.body = await sdk.applications.fetch(
ctx.query.status as AppStatus, ctx.query.status as AppStatus,
ctx.user ctx.user
@ -242,7 +251,9 @@ export async function fetchAppPackage(
} }
} }
async function performAppCreate(ctx: UserCtx<CreateAppRequest, App>) { async function performAppCreate(
ctx: UserCtx<CreateAppRequest, CreateAppResponse>
) {
const apps = (await dbCore.getAllApps({ dev: true })) as App[] const apps = (await dbCore.getAllApps({ dev: true })) as App[]
const { body } = ctx.request const { body } = ctx.request
const { name, url, encryptionPassword, templateKey } = body const { name, url, encryptionPassword, templateKey } = body
@ -510,7 +521,9 @@ async function appPostCreate(ctx: UserCtx<CreateAppRequest, App>, app: App) {
} }
} }
export async function create(ctx: UserCtx<CreateAppRequest, App>) { export async function create(
ctx: UserCtx<CreateAppRequest, CreateAppResponse>
) {
const newApplication = await quotas.addApp(() => performAppCreate(ctx)) const newApplication = await quotas.addApp(() => performAppCreate(ctx))
await appPostCreate(ctx, newApplication) await appPostCreate(ctx, newApplication)
await cache.bustCache(cache.CacheKey.CHECKLIST) await cache.bustCache(cache.CacheKey.CHECKLIST)
@ -553,7 +566,9 @@ export async function update(
}) })
} }
export async function updateClient(ctx: UserCtx) { export async function updateClient(
ctx: UserCtx<void, UpdateAppClientResponse>
) {
// Get current app version // Get current app version
const application = await sdk.applications.metadata.get() const application = await sdk.applications.metadata.get()
const currentVersion = application.version const currentVersion = application.version
@ -581,7 +596,9 @@ export async function updateClient(ctx: UserCtx) {
ctx.body = app ctx.body = app
} }
export async function revertClient(ctx: UserCtx) { export async function revertClient(
ctx: UserCtx<void, RevertAppClientResponse>
) {
// Check app can be reverted // Check app can be reverted
const application = await sdk.applications.metadata.get() const application = await sdk.applications.metadata.get()
if (!application.revertableVersion) { if (!application.revertableVersion) {
@ -668,7 +685,7 @@ async function postDestroyApp(ctx: UserCtx) {
} }
} }
export async function destroy(ctx: UserCtx) { export async function destroy(ctx: UserCtx<void, DeleteAppResponse>) {
await preDestroyApp(ctx) await preDestroyApp(ctx)
const result = await destroyApp(ctx) const result = await destroyApp(ctx)
await postDestroyApp(ctx) await postDestroyApp(ctx)
@ -676,7 +693,7 @@ export async function destroy(ctx: UserCtx) {
ctx.body = result ctx.body = result
} }
export async function unpublish(ctx: UserCtx) { export async function unpublish(ctx: UserCtx<void, void>) {
const prodAppId = dbCore.getProdAppID(ctx.params.appId) const prodAppId = dbCore.getProdAppID(ctx.params.appId)
const dbExists = await dbCore.dbExists(prodAppId) const dbExists = await dbCore.dbExists(prodAppId)
@ -692,7 +709,7 @@ export async function unpublish(ctx: UserCtx) {
builderSocket?.emitAppUnpublish(ctx) builderSocket?.emitAppUnpublish(ctx)
} }
export async function sync(ctx: UserCtx) { export async function sync(ctx: UserCtx<void, SyncAppResponse>) {
const appId = ctx.params.appId const appId = ctx.params.appId
try { try {
ctx.body = await sdk.applications.syncApp(appId) ctx.body = await sdk.applications.syncApp(appId)
@ -701,10 +718,12 @@ export async function sync(ctx: UserCtx) {
} }
} }
export async function importToApp(ctx: UserCtx) { export async function importToApp(
ctx: UserCtx<ImportToUpdateAppRequest, ImportToUpdateAppResponse>
) {
const { appId } = ctx.params const { appId } = ctx.params
const appExport = ctx.request.files?.appExport const appExport = ctx.request.files?.appExport
const password = ctx.request.body.encryptionPassword as string const password = ctx.request.body.encryptionPassword
if (!appExport) { if (!appExport) {
ctx.throw(400, "Must supply app export to import") ctx.throw(400, "Must supply app export to import")
} }
@ -811,7 +830,7 @@ export async function updateAppPackage(
} }
export async function setRevertableVersion( export async function setRevertableVersion(
ctx: UserCtx<{ revertableVersion: string }, App> ctx: UserCtx<SetRevertableAppVersionRequest, void>
) { ) {
if (!env.isDev()) { if (!env.isDev()) {
ctx.status = 403 ctx.status = 403

View File

@ -2,7 +2,7 @@ import { outputProcessing } from "../../utilities/rowProcessor"
import { InternalTables } from "../../db/utils" import { InternalTables } from "../../db/utils"
import { getFullUser } from "../../utilities/users" import { getFullUser } from "../../utilities/users"
import { roles, context, db as dbCore } from "@budibase/backend-core" import { roles, context, db as dbCore } from "@budibase/backend-core"
import { ContextUser, Row, UserCtx } from "@budibase/types" import { AppSelfResponse, ContextUser, UserCtx } from "@budibase/types"
import sdk from "../../sdk" import sdk from "../../sdk"
import { processUser } from "../../utilities/global" import { processUser } from "../../utilities/global"
@ -17,7 +17,7 @@ const addSessionAttributesToUser = (ctx: any) => {
} }
} }
export async function fetchSelf(ctx: UserCtx) { export async function fetchSelf(ctx: UserCtx<void, AppSelfResponse>) {
let userId = ctx.user.userId || ctx.user._id let userId = ctx.user.userId || ctx.user._id
/* istanbul ignore next */ /* istanbul ignore next */
if (!userId || !ctx.isAuthenticated) { if (!userId || !ctx.isAuthenticated) {
@ -45,9 +45,9 @@ export async function fetchSelf(ctx: UserCtx) {
try { try {
const userTable = await sdk.tables.getTable(InternalTables.USER_METADATA) const userTable = await sdk.tables.getTable(InternalTables.USER_METADATA)
// specifically needs to make sure is enriched // specifically needs to make sure is enriched
ctx.body = await outputProcessing(userTable, user as Row) ctx.body = await outputProcessing(userTable, user)
} catch (err: any) { } catch (err: any) {
let response let response: ContextUser | {}
// user didn't exist in app, don't pretend they do // user didn't exist in app, don't pretend they do
if (user.roleId === PUBLIC_ROLE) { if (user.roleId === PUBLIC_ROLE) {
response = {} response = {}

View File

@ -9,10 +9,25 @@ import {
App, App,
Automation, Automation,
AutomationActionStepId, AutomationActionStepId,
AutomationResults,
UserCtx, UserCtx,
DeleteAutomationResponse, DeleteAutomationResponse,
FetchAutomationResponse, FetchAutomationResponse,
GetAutomationTriggerDefinitionsResponse,
GetAutomationStepDefinitionsResponse,
GetAutomationActionDefinitionsResponse,
FindAutomationResponse,
UpdateAutomationRequest,
UpdateAutomationResponse,
CreateAutomationRequest,
CreateAutomationResponse,
SearchAutomationLogsRequest,
SearchAutomationLogsResponse,
ClearAutomationLogRequest,
ClearAutomationLogResponse,
TriggerAutomationRequest,
TriggerAutomationResponse,
TestAutomationRequest,
TestAutomationResponse,
} from "@budibase/types" } from "@budibase/types"
import { getActionDefinitions as actionDefs } from "../../automations/actions" import { getActionDefinitions as actionDefs } from "../../automations/actions"
import sdk from "../../sdk" import sdk from "../../sdk"
@ -34,7 +49,7 @@ function getTriggerDefinitions() {
*************************/ *************************/
export async function create( export async function create(
ctx: UserCtx<Automation, { message: string; automation: Automation }> ctx: UserCtx<CreateAutomationRequest, CreateAutomationResponse>
) { ) {
let automation = ctx.request.body let automation = ctx.request.body
automation.appId = ctx.appId automation.appId = ctx.appId
@ -55,7 +70,9 @@ export async function create(
builderSocket?.emitAutomationUpdate(ctx, automation) builderSocket?.emitAutomationUpdate(ctx, automation)
} }
export async function update(ctx: UserCtx) { export async function update(
ctx: UserCtx<UpdateAutomationRequest, UpdateAutomationResponse>
) {
let automation = ctx.request.body let automation = ctx.request.body
automation.appId = ctx.appId automation.appId = ctx.appId
@ -80,7 +97,7 @@ export async function fetch(ctx: UserCtx<void, FetchAutomationResponse>) {
ctx.body = { automations } ctx.body = { automations }
} }
export async function find(ctx: UserCtx) { export async function find(ctx: UserCtx<void, FindAutomationResponse>) {
ctx.body = await sdk.automations.get(ctx.params.id) ctx.body = await sdk.automations.get(ctx.params.id)
} }
@ -96,11 +113,15 @@ export async function destroy(ctx: UserCtx<void, DeleteAutomationResponse>) {
builderSocket?.emitAutomationDeletion(ctx, automationId) builderSocket?.emitAutomationDeletion(ctx, automationId)
} }
export async function logSearch(ctx: UserCtx) { export async function logSearch(
ctx: UserCtx<SearchAutomationLogsRequest, SearchAutomationLogsResponse>
) {
ctx.body = await automations.logs.logSearch(ctx.request.body) ctx.body = await automations.logs.logSearch(ctx.request.body)
} }
export async function clearLogError(ctx: UserCtx) { export async function clearLogError(
ctx: UserCtx<ClearAutomationLogRequest, ClearAutomationLogResponse>
) {
const { automationId, appId } = ctx.request.body const { automationId, appId } = ctx.request.body
await context.doInAppContext(appId, async () => { await context.doInAppContext(appId, async () => {
const db = context.getProdAppDB() const db = context.getProdAppDB()
@ -119,15 +140,21 @@ export async function clearLogError(ctx: UserCtx) {
}) })
} }
export async function getActionList(ctx: UserCtx) { export async function getActionList(
ctx: UserCtx<void, GetAutomationActionDefinitionsResponse>
) {
ctx.body = await getActionDefinitions() ctx.body = await getActionDefinitions()
} }
export async function getTriggerList(ctx: UserCtx) { export async function getTriggerList(
ctx: UserCtx<void, GetAutomationTriggerDefinitionsResponse>
) {
ctx.body = getTriggerDefinitions() ctx.body = getTriggerDefinitions()
} }
export async function getDefinitionList(ctx: UserCtx) { export async function getDefinitionList(
ctx: UserCtx<void, GetAutomationStepDefinitionsResponse>
) {
ctx.body = { ctx.body = {
trigger: getTriggerDefinitions(), trigger: getTriggerDefinitions(),
action: await getActionDefinitions(), action: await getActionDefinitions(),
@ -140,14 +167,16 @@ export async function getDefinitionList(ctx: UserCtx) {
* * * *
*********************/ *********************/
export async function trigger(ctx: UserCtx) { export async function trigger(
ctx: UserCtx<TriggerAutomationRequest, TriggerAutomationResponse>
) {
const db = context.getAppDB() const db = context.getAppDB()
let automation = await db.get<Automation>(ctx.params.id) let automation = await db.get<Automation>(ctx.params.id)
let hasCollectStep = sdk.automations.utils.checkForCollectStep(automation) let hasCollectStep = sdk.automations.utils.checkForCollectStep(automation)
if (hasCollectStep && (await features.isSyncAutomationsEnabled())) { if (hasCollectStep && (await features.isSyncAutomationsEnabled())) {
try { try {
const response: AutomationResults = await triggers.externalTrigger( const response = await triggers.externalTrigger(
automation, automation,
{ {
fields: ctx.request.body.fields, fields: ctx.request.body.fields,
@ -158,6 +187,10 @@ export async function trigger(ctx: UserCtx) {
{ getResponses: true } { getResponses: true }
) )
if (!("steps" in response)) {
ctx.throw(400, "Unable to collect response")
}
let collectedValue = response.steps.find( let collectedValue = response.steps.find(
step => step.stepId === AutomationActionStepId.COLLECT step => step.stepId === AutomationActionStepId.COLLECT
) )
@ -185,7 +218,7 @@ export async function trigger(ctx: UserCtx) {
} }
} }
function prepareTestInput(input: any) { function prepareTestInput(input: TestAutomationRequest) {
// prepare the test parameters // prepare the test parameters
if (input.id && input.row) { if (input.id && input.row) {
input.row._id = input.id input.row._id = input.id
@ -196,7 +229,9 @@ function prepareTestInput(input: any) {
return input return input
} }
export async function test(ctx: UserCtx) { export async function test(
ctx: UserCtx<TestAutomationRequest, TestAutomationResponse>
) {
const db = context.getAppDB() const db = context.getAppDB()
let automation = await db.get<Automation>(ctx.params.id) let automation = await db.get<Automation>(ctx.params.id)
await setTestFlag(automation._id!) await setTestFlag(automation._id!)

View File

@ -1,14 +1,16 @@
import sdk from "../../sdk" import sdk from "../../sdk"
import { events, context, db } from "@budibase/backend-core" import { events, context, db } from "@budibase/backend-core"
import { DocumentType } from "../../db/utils" import { DocumentType } from "../../db/utils"
import { App, Ctx } from "@budibase/types" import {
App,
Ctx,
ExportAppDumpRequest,
ExportAppDumpResponse,
} from "@budibase/types"
interface ExportAppDumpRequest { export async function exportAppDump(
excludeRows: boolean ctx: Ctx<ExportAppDumpRequest, ExportAppDumpResponse>
encryptPassword?: string ) {
}
export async function exportAppDump(ctx: Ctx<ExportAppDumpRequest>) {
const { appId } = ctx.query as any const { appId } = ctx.query as any
const { excludeRows, encryptPassword } = ctx.request.body const { excludeRows, encryptPassword } = ctx.request.body

View File

@ -1,9 +1,16 @@
import { DocumentType } from "../../db/utils" import { DocumentType } from "../../db/utils"
import { App, Plugin, UserCtx } from "@budibase/types" import {
App,
FetchComponentDefinitionResponse,
Plugin,
UserCtx,
} from "@budibase/types"
import { db as dbCore, context, tenancy } from "@budibase/backend-core" import { db as dbCore, context, tenancy } from "@budibase/backend-core"
import { getComponentLibraryManifest } from "../../utilities/fileSystem" import { getComponentLibraryManifest } from "../../utilities/fileSystem"
export async function fetchAppComponentDefinitions(ctx: UserCtx) { export async function fetchAppComponentDefinitions(
ctx: UserCtx<void, FetchComponentDefinitionResponse>
) {
try { try {
const db = context.getAppDB() const db = context.getAppDB()
const app = await db.get<App>(DocumentType.APP_METADATA) const app = await db.get<App>(DocumentType.APP_METADATA)

View File

@ -23,13 +23,17 @@ import {
Table, Table,
RowValue, RowValue,
DynamicVariable, DynamicVariable,
FetchDatasourcesResponse,
FindDatasourcesResponse,
DeleteDatasourceResponse,
FetchExternalSchemaResponse,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../../sdk" import sdk from "../../sdk"
import { builderSocket } from "../../websockets" import { builderSocket } from "../../websockets"
import { isEqual } from "lodash" import { isEqual } from "lodash"
import { processTable } from "../../sdk/app/tables/getters" import { processTable } from "../../sdk/app/tables/getters"
export async function fetch(ctx: UserCtx) { export async function fetch(ctx: UserCtx<void, FetchDatasourcesResponse>) {
ctx.body = await sdk.datasources.fetch() ctx.body = await sdk.datasources.fetch()
} }
@ -260,7 +264,7 @@ async function destroyInternalTablesBySourceId(datasourceId: string) {
} }
} }
export async function destroy(ctx: UserCtx) { export async function destroy(ctx: UserCtx<void, DeleteDatasourceResponse>) {
const db = context.getAppDB() const db = context.getAppDB()
const datasourceId = ctx.params.datasourceId const datasourceId = ctx.params.datasourceId
@ -291,12 +295,14 @@ export async function destroy(ctx: UserCtx) {
builderSocket?.emitDatasourceDeletion(ctx, datasourceId) builderSocket?.emitDatasourceDeletion(ctx, datasourceId)
} }
export async function find(ctx: UserCtx) { export async function find(ctx: UserCtx<void, FindDatasourcesResponse>) {
const datasource = await sdk.datasources.get(ctx.params.datasourceId) const datasource = await sdk.datasources.get(ctx.params.datasourceId)
ctx.body = await sdk.datasources.removeSecretSingle(datasource) ctx.body = await sdk.datasources.removeSecretSingle(datasource)
} }
export async function getExternalSchema(ctx: UserCtx) { export async function getExternalSchema(
ctx: UserCtx<void, FetchExternalSchemaResponse>
) {
const datasource = await sdk.datasources.get(ctx.params.datasourceId) const datasource = await sdk.datasources.get(ctx.params.datasourceId)
const enrichedDatasource = await sdk.datasources.getAndMergeDatasource( const enrichedDatasource = await sdk.datasources.getAndMergeDatasource(
datasource datasource

View File

@ -1,4 +1,5 @@
import { context, utils } from "@budibase/backend-core" import { context, utils } from "@budibase/backend-core"
import { DeploymentStatus } from "@budibase/types"
/** /**
* This is used to pass around information about the deployment that is occurring * This is used to pass around information about the deployment that is occurring
@ -6,7 +7,7 @@ import { context, utils } from "@budibase/backend-core"
export default class Deployment { export default class Deployment {
_id: string _id: string
verification: any verification: any
status?: string status?: DeploymentStatus
err?: any err?: any
appUrl?: string appUrl?: string
@ -25,7 +26,7 @@ export default class Deployment {
return this.verification return this.verification
} }
setStatus(status: string, err?: any) { setStatus(status: DeploymentStatus, err?: any) {
this.status = status this.status = status
if (err) { if (err) {
this.err = err this.err = err

View File

@ -7,20 +7,26 @@ import {
enableCronTrigger, enableCronTrigger,
} from "../../../automations/utils" } from "../../../automations/utils"
import { backups } from "@budibase/pro" import { backups } from "@budibase/pro"
import { App, AppBackupTrigger } from "@budibase/types" import {
App,
AppBackupTrigger,
DeploymentDoc,
FetchDeploymentResponse,
PublishAppResponse,
UserCtx,
DeploymentStatus,
DeploymentProgressResponse,
} from "@budibase/types"
import sdk from "../../../sdk" import sdk from "../../../sdk"
import { builderSocket } from "../../../websockets" import { builderSocket } from "../../../websockets"
// the max time we can wait for an invalidation to complete before considering it failed // the max time we can wait for an invalidation to complete before considering it failed
const MAX_PENDING_TIME_MS = 30 * 60000 const MAX_PENDING_TIME_MS = 30 * 60000
const DeploymentStatus = {
SUCCESS: "SUCCESS",
PENDING: "PENDING",
FAILURE: "FAILURE",
}
// checks that deployments are in a good state, any pending will be updated // checks that deployments are in a good state, any pending will be updated
async function checkAllDeployments(deployments: any) { async function checkAllDeployments(
deployments: any
): Promise<{ updated: boolean; deployments: DeploymentDoc }> {
let updated = false let updated = false
let deployment: any let deployment: any
for (deployment of Object.values(deployments.history)) { for (deployment of Object.values(deployments.history)) {
@ -96,7 +102,9 @@ async function initDeployedApp(prodAppId: any) {
}) })
} }
export async function fetchDeployments(ctx: any) { export async function fetchDeployments(
ctx: UserCtx<void, FetchDeploymentResponse>
) {
try { try {
const db = context.getAppDB() const db = context.getAppDB()
const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS) const deploymentDoc = await db.get(DocumentType.DEPLOYMENTS)
@ -104,17 +112,24 @@ export async function fetchDeployments(ctx: any) {
if (updated) { if (updated) {
await db.put(deployments) await db.put(deployments)
} }
ctx.body = Object.values(deployments.history).reverse() ctx.body = deployments.history
? Object.values(deployments.history).reverse()
: []
} catch (err) { } catch (err) {
ctx.body = [] ctx.body = []
} }
} }
export async function deploymentProgress(ctx: any) { export async function deploymentProgress(
ctx: UserCtx<void, DeploymentProgressResponse>
) {
try { try {
const db = context.getAppDB() const db = context.getAppDB()
const deploymentDoc = await db.get<any>(DocumentType.DEPLOYMENTS) const deploymentDoc = await db.get<DeploymentDoc>(DocumentType.DEPLOYMENTS)
ctx.body = deploymentDoc[ctx.params.deploymentId] if (!deploymentDoc.history?.[ctx.params.deploymentId]) {
ctx.throw(404, "No deployment found")
}
ctx.body = deploymentDoc.history?.[ctx.params.deploymentId]
} catch (err) { } catch (err) {
ctx.throw( ctx.throw(
500, 500,
@ -123,7 +138,9 @@ export async function deploymentProgress(ctx: any) {
} }
} }
export const publishApp = async function (ctx: any) { export const publishApp = async function (
ctx: UserCtx<void, PublishAppResponse>
) {
let deployment = new Deployment() let deployment = new Deployment()
console.log("Deployment object created") console.log("Deployment object created")
deployment.setStatus(DeploymentStatus.PENDING) deployment.setStatus(DeploymentStatus.PENDING)

View File

@ -11,7 +11,13 @@ import {
db as dbCore, db as dbCore,
cache, cache,
} from "@budibase/backend-core" } from "@budibase/backend-core"
import { App } from "@budibase/types" import {
App,
ClearDevLockResponse,
Ctx,
GetVersionResponse,
RevertAppResponse,
} from "@budibase/types"
async function redirect( async function redirect(
ctx: any, ctx: any,
@ -69,7 +75,7 @@ export function buildRedirectDelete(path: string) {
} }
} }
export async function clearLock(ctx: any) { export async function clearLock(ctx: Ctx<void, ClearDevLockResponse>) {
const { appId } = ctx.params const { appId } = ctx.params
try { try {
await redisClearLock(appId, ctx.user) await redisClearLock(appId, ctx.user)
@ -81,7 +87,7 @@ export async function clearLock(ctx: any) {
} }
} }
export async function revert(ctx: any) { export async function revert(ctx: Ctx<void, RevertAppResponse>) {
const { appId } = ctx.params const { appId } = ctx.params
const productionAppId = dbCore.getProdAppID(appId) const productionAppId = dbCore.getProdAppID(appId)
@ -131,7 +137,7 @@ export async function revert(ctx: any) {
} }
} }
export async function getBudibaseVersion(ctx: any) { export async function getBudibaseVersion(ctx: Ctx<void, GetVersionResponse>) {
const version = envCore.VERSION const version = envCore.VERSION
ctx.body = { ctx.body = {
version, version,

View File

@ -94,12 +94,16 @@ export async function trigger(ctx: BBContext) {
{ getResponses: true } { getResponses: true }
) )
let collectedValue = response.steps.find( if (triggers.isAutomationResults(response)) {
(step: any) => step.stepId === AutomationActionStepId.COLLECT let collectedValue = response.steps.find(
) (step: any) => step.stepId === AutomationActionStepId.COLLECT
)
ctx.status = 200 ctx.status = 200
ctx.body = collectedValue.outputs ctx.body = collectedValue?.outputs
} else {
ctx.throw(400, "Automation did not have a collect block.")
}
} else { } else {
await triggers.externalTrigger(target, { await triggers.externalTrigger(target, {
body: ctx.request.body, body: ctx.request.body,

View File

@ -96,9 +96,15 @@ if (env.SELF_HOSTED) {
ACTION_IMPLS["EXECUTE_BASH"] = bash.run ACTION_IMPLS["EXECUTE_BASH"] = bash.run
// @ts-ignore // @ts-ignore
BUILTIN_ACTION_DEFINITIONS["EXECUTE_BASH"] = bash.definition BUILTIN_ACTION_DEFINITIONS["EXECUTE_BASH"] = bash.definition
if (env.isTest()) {
BUILTIN_ACTION_DEFINITIONS["OPENAI"] = openai.definition
}
} }
export async function getActionDefinitions() { export async function getActionDefinitions(): Promise<
Record<keyof typeof AutomationActionStepId, AutomationStepDefinition>
> {
if (await features.flags.isEnabled(FeatureFlag.AUTOMATION_BRANCHING)) { if (await features.flags.isEnabled(FeatureFlag.AUTOMATION_BRANCHING)) {
BUILTIN_ACTION_DEFINITIONS["BRANCH"] = branch.definition BUILTIN_ACTION_DEFINITIONS["BRANCH"] = branch.definition
} }

View File

@ -3,7 +3,6 @@ import {
AutomationStepDefinition, AutomationStepDefinition,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
AutomationResults,
Automation, Automation,
AutomationCustomIOType, AutomationCustomIOType,
TriggerAutomationStepInputs, TriggerAutomationStepInputs,
@ -78,7 +77,7 @@ export async function run({
const db = context.getAppDB() const db = context.getAppDB()
let automation = await db.get<Automation>(inputs.automation.automationId) let automation = await db.get<Automation>(inputs.automation.automationId)
const response: AutomationResults = await triggers.externalTrigger( const response = await triggers.externalTrigger(
automation, automation,
{ {
fields: { ...fieldParams }, fields: { ...fieldParams },
@ -88,9 +87,13 @@ export async function run({
{ getResponses: true } { getResponses: true }
) )
return { if (triggers.isAutomationResults(response)) {
success: true, return {
value: response.steps, success: true,
value: response.steps,
}
} else {
throw new Error("Automation did not have a collect block")
} }
} }
} else { } else {

View File

@ -1,26 +1,148 @@
import { getConfig, afterAll as _afterAll, runStep } from "./utilities" import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
import * as automation from "../index"
import * as setup from "./utilities"
import { Table } from "@budibase/types"
describe("test the bash action", () => { describe("Execute Bash Automations", () => {
let config = getConfig() let config = setup.getConfig(),
table: Table
beforeAll(async () => { beforeAll(async () => {
await automation.init()
await config.init() await config.init()
}) table = await config.createTable()
afterAll(_afterAll) await config.createRow({
name: "test row",
it("should be able to execute a script", async () => { description: "test description",
let res = await runStep(config, "EXECUTE_BASH", { tableId: table._id!,
code: "echo 'test'",
}) })
expect(res.stdout).toEqual("test\n")
expect(res.success).toEqual(true)
}) })
it("should handle a null value", async () => { afterAll(setup.afterAll)
let res = await runStep(config, "EXECUTE_BASH", {
code: null, it("should use trigger data in bash command and pass output to subsequent steps", async () => {
const result = await createAutomationBuilder({
name: "Bash with Trigger Data",
config,
}) })
expect(res.stdout).toEqual( .appAction({ fields: { command: "hello world" } })
.bash(
{ code: "echo '{{ trigger.fields.command }}'" },
{ stepName: "Echo Command" }
)
.serverLog(
{ text: "Bash output was: {{ steps.[Echo Command].stdout }}" },
{ stepName: "Log Output" }
)
.run()
expect(result.steps[0].outputs.stdout).toEqual("hello world\n")
expect(result.steps[1].outputs.message).toContain(
"Bash output was: hello world"
)
})
it("should chain multiple bash commands using previous outputs", async () => {
const result = await createAutomationBuilder({
name: "Chained Bash Commands",
config,
})
.appAction({ fields: { filename: "testfile.txt" } })
.bash(
{ code: "echo 'initial content' > {{ trigger.fields.filename }}" },
{ stepName: "Create File" }
)
.bash(
{ code: "cat {{ trigger.fields.filename }} | tr '[a-z]' '[A-Z]'" },
{ stepName: "Transform Content" }
)
.bash(
{ code: "rm {{ trigger.fields.filename }}" },
{ stepName: "Cleanup" }
)
.run()
expect(result.steps[1].outputs.stdout).toEqual("INITIAL CONTENT\n")
expect(result.steps[1].outputs.success).toEqual(true)
})
it("should integrate bash output with row operations", async () => {
const result = await createAutomationBuilder({
name: "Bash with Row Operations",
config,
})
.appAction({ fields: {} })
.queryRows(
{
tableId: table._id!,
filters: {},
},
{ stepName: "Get Row" }
)
.bash(
{
code: "echo Row data: {{ steps.[Get Row].rows.[0].name }} - {{ steps.[Get Row].rows.[0].description }}",
},
{ stepName: "Process Row Data" }
)
.serverLog(
{ text: "{{ steps.[Process Row Data].stdout }}" },
{ stepName: "Log Result" }
)
.run()
expect(result.steps[1].outputs.stdout).toContain(
"Row data: test row - test description"
)
expect(result.steps[2].outputs.message).toContain(
"Row data: test row - test description"
)
})
it("should handle bash output in conditional logic", async () => {
const result = await createAutomationBuilder({
name: "Bash with Conditional",
config,
})
.appAction({ fields: { threshold: "5" } })
.bash(
{ code: "echo $(( {{ trigger.fields.threshold }} + 5 ))" },
{ stepName: "Calculate Value" }
)
.executeScript(
{
code: `
const value = parseInt(steps["Calculate Value"].stdout);
return value > 8 ? "high" : "low";
`,
},
{ stepName: "Check Value" }
)
.serverLog(
{ text: "Value was {{ steps.[Check Value].value }}" },
{ stepName: "Log Result" }
)
.run()
expect(result.steps[0].outputs.stdout).toEqual("10\n")
expect(result.steps[1].outputs.value).toEqual("high")
expect(result.steps[2].outputs.message).toContain("Value was high")
})
it("should handle null values gracefully", async () => {
const result = await createAutomationBuilder({
name: "Null Bash Input",
config,
})
.appAction({ fields: {} })
.bash(
//@ts-ignore
{ code: null },
{ stepName: "Null Command" }
)
.run()
expect(result.steps[0].outputs.stdout).toBe(
"Budibase bash automation failed: Invalid inputs" "Budibase bash automation failed: Invalid inputs"
) )
}) })

View File

@ -3,7 +3,7 @@ import * as triggers from "../triggers"
import { loopAutomation } from "../../tests/utilities/structures" import { loopAutomation } from "../../tests/utilities/structures"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import * as setup from "./utilities" import * as setup from "./utilities"
import { Table, LoopStepType } from "@budibase/types" import { Table, LoopStepType, AutomationResults } from "@budibase/types"
import * as loopUtils from "../loopUtils" import * as loopUtils from "../loopUtils"
import { LoopInput } from "../../definitions/automations" import { LoopInput } from "../../definitions/automations"
@ -20,15 +20,19 @@ describe("Attempt to run a basic loop automation", () => {
afterAll(setup.afterAll) afterAll(setup.afterAll)
async function runLoop(loopOpts?: LoopInput) { async function runLoop(loopOpts?: LoopInput): Promise<AutomationResults> {
const appId = config.getAppId() const appId = config.getAppId()
return await context.doInAppContext(appId, async () => { return await context.doInAppContext(appId, async () => {
const params = { fields: { appId } } const params = { fields: { appId } }
return await triggers.externalTrigger( const result = await triggers.externalTrigger(
loopAutomation(table._id!, loopOpts), loopAutomation(table._id!, loopOpts),
params, params,
{ getResponses: true } { getResponses: true }
) )
if ("outputs" in result && !result.outputs.success) {
throw new Error("Unable to proceed - failed to return anything.")
}
return result as AutomationResults
}) })
} }

View File

@ -1,7 +1,9 @@
import { getConfig, runStep, afterAll as _afterAll } from "./utilities" import { getConfig, afterAll as _afterAll } from "./utilities"
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
import { OpenAI } from "openai" import { OpenAI } from "openai"
import { setEnv as setCoreEnv } from "@budibase/backend-core" import { setEnv as setCoreEnv } from "@budibase/backend-core"
import * as pro from "@budibase/pro" import * as pro from "@budibase/pro"
import { Model } from "@budibase/types"
jest.mock("openai", () => ({ jest.mock("openai", () => ({
OpenAI: jest.fn().mockImplementation(() => ({ OpenAI: jest.fn().mockImplementation(() => ({
@ -47,6 +49,7 @@ describe("test the openai action", () => {
let resetEnv: () => void | undefined let resetEnv: () => void | undefined
beforeAll(async () => { beforeAll(async () => {
setCoreEnv({ SELF_HOSTED: true })
await config.init() await config.init()
}) })
@ -62,17 +65,39 @@ describe("test the openai action", () => {
afterAll(_afterAll) afterAll(_afterAll)
it("should be able to receive a response from ChatGPT given a prompt", async () => { it("should be able to receive a response from ChatGPT given a prompt", async () => {
const res = await runStep(config, "OPENAI", { prompt: OPENAI_PROMPT }) setCoreEnv({ SELF_HOSTED: true })
expect(res.response).toEqual("This is a test")
expect(res.success).toBeTruthy() const result = await createAutomationBuilder({
name: "Test OpenAI Response",
config,
})
.appAction({ fields: {} })
.openai(
{ prompt: OPENAI_PROMPT, model: Model.GPT_4O_MINI },
{ stepName: "Basic OpenAI Query" }
)
.run()
expect(result.steps[0].outputs.response).toEqual("This is a test")
expect(result.steps[0].outputs.success).toBeTruthy()
}) })
it("should present the correct error message when a prompt is not provided", async () => { it("should present the correct error message when a prompt is not provided", async () => {
const res = await runStep(config, "OPENAI", { prompt: null }) const result = await createAutomationBuilder({
expect(res.response).toEqual( name: "Test OpenAI No Prompt",
config,
})
.appAction({ fields: {} })
.openai(
{ prompt: "", model: Model.GPT_4O_MINI },
{ stepName: "Empty Prompt Query" }
)
.run()
expect(result.steps[0].outputs.response).toEqual(
"Budibase OpenAI Automation Failed: No prompt supplied" "Budibase OpenAI Automation Failed: No prompt supplied"
) )
expect(res.success).toBeFalsy() expect(result.steps[0].outputs.success).toBeFalsy()
}) })
it("should present the correct error message when an error is thrown from the createChatCompletion call", async () => { it("should present the correct error message when an error is thrown from the createChatCompletion call", async () => {
@ -91,14 +116,21 @@ describe("test the openai action", () => {
} as any) } as any)
) )
const res = await runStep(config, "OPENAI", { const result = await createAutomationBuilder({
prompt: OPENAI_PROMPT, name: "Test OpenAI Error",
config,
}) })
.appAction({ fields: {} })
.openai(
{ prompt: OPENAI_PROMPT, model: Model.GPT_4O_MINI },
{ stepName: "Error Producing Query" }
)
.run()
expect(res.response).toEqual( expect(result.steps[0].outputs.response).toEqual(
"Error: An error occurred while calling createChatCompletion" "Error: An error occurred while calling createChatCompletion"
) )
expect(res.success).toBeFalsy() expect(result.steps[0].outputs.success).toBeFalsy()
}) })
it("should ensure that the pro AI module is called when the budibase AI features are enabled", async () => { it("should ensure that the pro AI module is called when the budibase AI features are enabled", async () => {
@ -106,10 +138,19 @@ describe("test the openai action", () => {
jest.spyOn(pro.features, "isAICustomConfigsEnabled").mockResolvedValue(true) jest.spyOn(pro.features, "isAICustomConfigsEnabled").mockResolvedValue(true)
const prompt = "What is the meaning of life?" const prompt = "What is the meaning of life?"
await runStep(config, "OPENAI", { await createAutomationBuilder({
model: "gpt-4o-mini", name: "Test OpenAI Pro Features",
prompt, config,
}) })
.appAction({ fields: {} })
.openai(
{
model: Model.GPT_4O_MINI,
prompt,
},
{ stepName: "Pro Features Query" }
)
.run()
expect(pro.ai.LargeLanguageModel.forCurrentTenant).toHaveBeenCalledWith( expect(pro.ai.LargeLanguageModel.forCurrentTenant).toHaveBeenCalledWith(
"gpt-4o-mini" "gpt-4o-mini"

View File

@ -1,5 +1,7 @@
import { Table } from "@budibase/types" import { EmptyFilterOption, SortOrder, Table } from "@budibase/types"
import * as setup from "./utilities" import * as setup from "./utilities"
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
import * as automation from "../index"
const NAME = "Test" const NAME = "Test"
@ -8,6 +10,7 @@ describe("Test a query step automation", () => {
let config = setup.getConfig() let config = setup.getConfig()
beforeAll(async () => { beforeAll(async () => {
await automation.init()
await config.init() await config.init()
table = await config.createTable() table = await config.createTable()
const row = { const row = {
@ -22,107 +25,132 @@ describe("Test a query step automation", () => {
afterAll(setup.afterAll) afterAll(setup.afterAll)
it("should be able to run the query step", async () => { it("should be able to run the query step", async () => {
const inputs = { const result = await createAutomationBuilder({
tableId: table._id, name: "Basic Query Test",
filters: {
equal: {
name: NAME,
},
},
sortColumn: "name",
sortOrder: "ascending",
limit: 10,
}
const res = await setup.runStep(
config, config,
setup.actions.QUERY_ROWS.stepId, })
inputs .appAction({ fields: {} })
) .queryRows(
expect(res.success).toBe(true) {
expect(res.rows).toBeDefined() tableId: table._id!,
expect(res.rows.length).toBe(2) filters: {
expect(res.rows[0].name).toBe(NAME) equal: {
name: NAME,
},
},
sortColumn: "name",
sortOrder: SortOrder.ASCENDING,
limit: 10,
},
{ stepName: "Query All Rows" }
)
.run()
expect(result.steps[0].outputs.success).toBe(true)
expect(result.steps[0].outputs.rows).toBeDefined()
expect(result.steps[0].outputs.rows.length).toBe(2)
expect(result.steps[0].outputs.rows[0].name).toBe(NAME)
}) })
it("Returns all rows when onEmptyFilter has no value and no filters are passed", async () => { it("Returns all rows when onEmptyFilter has no value and no filters are passed", async () => {
const inputs = { const result = await createAutomationBuilder({
tableId: table._id, name: "Empty Filter Test",
filters: {},
sortColumn: "name",
sortOrder: "ascending",
limit: 10,
}
const res = await setup.runStep(
config, config,
setup.actions.QUERY_ROWS.stepId, })
inputs .appAction({ fields: {} })
) .queryRows(
expect(res.success).toBe(true) {
expect(res.rows).toBeDefined() tableId: table._id!,
expect(res.rows.length).toBe(2) filters: {},
expect(res.rows[0].name).toBe(NAME) sortColumn: "name",
sortOrder: SortOrder.ASCENDING,
limit: 10,
},
{ stepName: "Query With Empty Filter" }
)
.run()
expect(result.steps[0].outputs.success).toBe(true)
expect(result.steps[0].outputs.rows).toBeDefined()
expect(result.steps[0].outputs.rows.length).toBe(2)
expect(result.steps[0].outputs.rows[0].name).toBe(NAME)
}) })
it("Returns no rows when onEmptyFilter is RETURN_NONE and theres no filters", async () => { it("Returns no rows when onEmptyFilter is RETURN_NONE and theres no filters", async () => {
const inputs = { const result = await createAutomationBuilder({
tableId: table._id, name: "Return None Test",
filters: {},
"filters-def": [],
sortColumn: "name",
sortOrder: "ascending",
limit: 10,
onEmptyFilter: "none",
}
const res = await setup.runStep(
config, config,
setup.actions.QUERY_ROWS.stepId, })
inputs .appAction({ fields: {} })
) .queryRows(
expect(res.success).toBe(false) {
expect(res.rows).toBeDefined() tableId: table._id!,
expect(res.rows.length).toBe(0) filters: {},
"filters-def": [],
sortColumn: "name",
sortOrder: SortOrder.ASCENDING,
limit: 10,
onEmptyFilter: EmptyFilterOption.RETURN_NONE,
},
{ stepName: "Query With Return None" }
)
.run()
expect(result.steps[0].outputs.success).toBe(false)
expect(result.steps[0].outputs.rows).toBeDefined()
expect(result.steps[0].outputs.rows.length).toBe(0)
}) })
it("Returns no rows when onEmptyFilters RETURN_NONE and a filter is passed with a null value", async () => { it("Returns no rows when onEmptyFilters RETURN_NONE and a filter is passed with a null value", async () => {
const inputs = { const result = await createAutomationBuilder({
tableId: table._id, name: "Null Filter Test",
onEmptyFilter: "none",
filters: {},
"filters-def": [
{
value: null,
},
],
sortColumn: "name",
sortOrder: "ascending",
limit: 10,
}
const res = await setup.runStep(
config, config,
setup.actions.QUERY_ROWS.stepId, })
inputs .appAction({ fields: {} })
) .queryRows(
expect(res.success).toBe(false) {
expect(res.rows).toBeDefined() tableId: table._id!,
expect(res.rows.length).toBe(0) onEmptyFilter: EmptyFilterOption.RETURN_NONE,
filters: {},
"filters-def": [
{
value: null,
},
],
sortColumn: "name",
sortOrder: SortOrder.ASCENDING,
limit: 10,
},
{ stepName: "Query With Null Filter" }
)
.run()
expect(result.steps[0].outputs.success).toBe(false)
expect(result.steps[0].outputs.rows).toBeDefined()
expect(result.steps[0].outputs.rows.length).toBe(0)
}) })
it("Returns rows when onEmptyFilter is RETURN_ALL and no filter is passed", async () => { it("Returns rows when onEmptyFilter is RETURN_ALL and no filter is passed", async () => {
const inputs = { const result = await createAutomationBuilder({
tableId: table._id, name: "Return All Test",
onEmptyFilter: "all",
filters: {},
sortColumn: "name",
sortOrder: "ascending",
limit: 10,
}
const res = await setup.runStep(
config, config,
setup.actions.QUERY_ROWS.stepId, })
inputs .appAction({ fields: {} })
) .queryRows(
expect(res.success).toBe(true) {
expect(res.rows).toBeDefined() tableId: table._id!,
expect(res.rows.length).toBe(2) onEmptyFilter: EmptyFilterOption.RETURN_ALL,
filters: {},
sortColumn: "name",
sortOrder: SortOrder.ASCENDING,
limit: 10,
},
{ stepName: "Query With Return All" }
)
.run()
expect(result.steps[0].outputs.success).toBe(true)
expect(result.steps[0].outputs.rows).toBeDefined()
expect(result.steps[0].outputs.rows.length).toBe(2)
}) })
}) })

View File

@ -35,6 +35,8 @@ import {
Branch, Branch,
FilterStepInputs, FilterStepInputs,
ExecuteScriptStepInputs, ExecuteScriptStepInputs,
OpenAIStepInputs,
BashStepInputs,
} from "@budibase/types" } from "@budibase/types"
import TestConfiguration from "../../../tests/utilities/TestConfiguration" import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import * as setup from "../utilities" import * as setup from "../utilities"
@ -221,6 +223,30 @@ class BaseStepBuilder {
input input
) )
} }
bash(
input: BashStepInputs,
opts?: { stepName?: string; stepId?: string }
): this {
return this.step(
AutomationActionStepId.EXECUTE_BASH,
BUILTIN_ACTION_DEFINITIONS.EXECUTE_BASH,
input,
opts
)
}
openai(
input: OpenAIStepInputs,
opts?: { stepName?: string; stepId?: string }
): this {
return this.step(
AutomationActionStepId.OPENAI,
BUILTIN_ACTION_DEFINITIONS.OPENAI,
input,
opts
)
}
} }
class StepBuilder extends BaseStepBuilder { class StepBuilder extends BaseStepBuilder {
build(): AutomationStep[] { build(): AutomationStep[] {

View File

@ -20,6 +20,7 @@ import {
AutomationStatus, AutomationStatus,
AutomationRowEvent, AutomationRowEvent,
UserBindings, UserBindings,
AutomationResults,
} 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"
@ -32,6 +33,14 @@ 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>(
@ -139,6 +148,14 @@ function rowPassesFilters(row: Row, filters: SearchFilters) {
return filteredRows.length > 0 return filteredRows.length > 0
} }
export function isAutomationResults(
response: AutomationResults | DidNotTriggerResponse | AutomationJob
): response is AutomationResults {
return (
response !== null && "steps" in response && Array.isArray(response.steps)
)
}
export async function externalTrigger( export async function externalTrigger(
automation: Automation, automation: Automation,
params: { params: {
@ -148,7 +165,7 @@ export async function externalTrigger(
user?: UserBindings user?: UserBindings
}, },
{ getResponses }: { getResponses?: boolean } = {} { getResponses }: { getResponses?: boolean } = {}
): Promise<any> { ): Promise<AutomationResults | DidNotTriggerResponse | AutomationJob> {
if (automation.disabled) { if (automation.disabled) {
throw new Error("Automation is disabled") throw new Error("Automation is disabled")
} }

View File

@ -9,9 +9,11 @@ import { cloneDeep } from "lodash/fp"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import { import {
Automation, Automation,
AutomationActionStepId,
AutomationJob, AutomationJob,
AutomationStepDefinition, AutomationStepDefinition,
AutomationTriggerDefinition, AutomationTriggerDefinition,
AutomationTriggerStepId,
} from "@budibase/types" } from "@budibase/types"
import { automationsEnabled } from "../features" import { automationsEnabled } from "../features"
import { helpers, REBOOT_CRON } from "@budibase/shared-core" import { helpers, REBOOT_CRON } from "@budibase/shared-core"
@ -120,19 +122,21 @@ export async function updateTestHistory(
) )
} }
export function removeDeprecated( export function removeDeprecated<
definitions: Record< T extends
| Record<keyof typeof AutomationTriggerStepId, AutomationTriggerDefinition>
| Record<keyof typeof AutomationActionStepId, AutomationStepDefinition>
>(definitions: T): T {
const base: Record<
string, string,
AutomationStepDefinition | AutomationTriggerDefinition AutomationTriggerDefinition | AutomationStepDefinition
> > = cloneDeep(definitions)
) {
const base = cloneDeep(definitions)
for (let key of Object.keys(base)) { for (let key of Object.keys(base)) {
if (base[key].deprecated) { if (base[key].deprecated) {
delete base[key] delete base[key]
} }
} }
return base return base as T
} }
// end the repetition and the job itself // end the repetition and the job itself

View File

@ -26,3 +26,6 @@ export interface AutomationContext extends AutomationResults {
company?: string company?: string
} }
} }
export interface AutomationResponse
extends Omit<AutomationContext, "stepsByName" | "stepsById"> {}

View File

@ -113,7 +113,7 @@ export async function syncUsersToAllApps(userIds: string[]) {
export async function syncApp( export async function syncApp(
appId: string, appId: string,
opts?: { automationOnly?: boolean } opts?: { automationOnly?: boolean }
) { ): Promise<{ message: string }> {
if (env.DISABLE_AUTO_PROD_APP_SYNC) { if (env.DISABLE_AUTO_PROD_APP_SYNC) {
return { return {
message: message:

View File

@ -621,7 +621,7 @@ export default class TestConfiguration {
} }
async unpublish() { async unpublish() {
const response = await this._req(appController.unpublish, { const response = await this._req(appController.unpublish, undefined, {
appId: this.appId, appId: this.appId,
}) })
this.prodAppId = undefined this.prodAppId = undefined

View File

@ -30,7 +30,11 @@ import {
UserBindings, UserBindings,
isBasicSearchOperator, isBasicSearchOperator,
} from "@budibase/types" } from "@budibase/types"
import { AutomationContext, TriggerOutput } from "../definitions/automations" import {
AutomationContext,
AutomationResponse,
TriggerOutput,
} from "../definitions/automations"
import { WorkerCallback } from "./definitions" import { WorkerCallback } from "./definitions"
import { context, logging, configs } from "@budibase/backend-core" import { context, logging, configs } from "@budibase/backend-core"
import { import {
@ -81,7 +85,7 @@ class Orchestrator {
private job: Job private job: Job
private loopStepOutputs: LoopStep[] private loopStepOutputs: LoopStep[]
private stopped: boolean private stopped: boolean
private executionOutput: Omit<AutomationContext, "stepsByName" | "stepsById"> private executionOutput: AutomationResponse
private currentUser: UserBindings | undefined private currentUser: UserBindings | undefined
constructor(job: AutomationJob) { constructor(job: AutomationJob) {
@ -257,7 +261,7 @@ class Orchestrator {
}) })
} }
async execute(): Promise<any> { async execute(): Promise<AutomationResponse | undefined> {
return tracer.trace( return tracer.trace(
"Orchestrator.execute", "Orchestrator.execute",
{ resource: "automation" }, { resource: "automation" },
@ -723,7 +727,9 @@ export function execute(job: Job<AutomationData>, callback: WorkerCallback) {
}) })
} }
export async function executeInThread(job: Job<AutomationData>) { export async function executeInThread(
job: Job<AutomationData>
): Promise<AutomationResponse> {
const appId = job.data.event.appId const appId = job.data.event.appId
if (!appId) { if (!appId) {
throw new Error("Unable to execute, event doesn't contain app ID.") throw new Error("Unable to execute, event doesn't contain app ID.")
@ -735,7 +741,7 @@ export async function executeInThread(job: Job<AutomationData>) {
}, job.data.event.timeout || env.AUTOMATION_THREAD_TIMEOUT) }, job.data.event.timeout || env.AUTOMATION_THREAD_TIMEOUT)
}) })
return await context.doInAppContext(appId, async () => { return (await context.doInAppContext(appId, async () => {
await context.ensureSnippetContext() await context.ensureSnippetContext()
const envVars = await sdkUtils.getEnvironmentVariables() const envVars = await sdkUtils.getEnvironmentVariables()
// put into automation thread for whole context // put into automation thread for whole context
@ -746,7 +752,7 @@ export async function executeInThread(job: Job<AutomationData>) {
timeoutPromise, timeoutPromise,
]) ])
}) })
}) })) as AutomationResponse
} }
export const removeStalled = async (job: Job) => { export const removeStalled = async (job: Job) => {

View File

@ -3,6 +3,10 @@ export enum PingSource {
APP = "app", APP = "app",
} }
export interface AnalyticsEnabledResponse {
enabled: boolean
}
export interface AnalyticsPingRequest { export interface AnalyticsPingRequest {
source: PingSource source: PingSource
timezone: string timezone: string

View File

@ -0,0 +1,10 @@
export type ApiKeyFetchResponse = Record<string, string>
export interface UpdateApiKeyRequest {
value: string
}
export interface UpdateApiKeyResponse {
_id: string
_rev: string
}

View File

@ -0,0 +1,21 @@
import {
AutomationActionStepId,
AutomationStepDefinition,
AutomationTriggerDefinition,
AutomationTriggerStepId,
} from "../../../documents"
export type GetAutomationTriggerDefinitionsResponse = Record<
keyof typeof AutomationTriggerStepId,
AutomationTriggerDefinition
>
export type GetAutomationActionDefinitionsResponse = Record<
keyof typeof AutomationActionStepId,
AutomationStepDefinition
>
export interface GetAutomationStepDefinitionsResponse {
trigger: GetAutomationTriggerDefinitionsResponse
action: GetAutomationActionDefinitionsResponse
}

View File

@ -0,0 +1,4 @@
export type FetchComponentDefinitionResponse = Record<
string,
Record<string, any>
>

View File

@ -42,3 +42,14 @@ export interface BuildSchemaFromSourceResponse {
datasource: Datasource datasource: Datasource
errors: Record<string, string> errors: Record<string, string>
} }
export type FetchDatasourcesResponse = Datasource[]
export type FindDatasourcesResponse = Datasource
export interface DeleteDatasourceResponse {
message: string
}
export interface FetchExternalSchemaResponse {
schema: string
}

View File

@ -8,3 +8,5 @@ export * from "./permission"
export * from "./attachment" export * from "./attachment"
export * from "./user" export * from "./user"
export * from "./rowAction" export * from "./rowAction"
export * from "./automation"
export * from "./component"

View File

@ -7,3 +7,5 @@ export interface SetFlagRequest {
flag: string flag: string
value: any value: any
} }
export type AppSelfResponse = ContextUserMetadata | {}

View File

@ -1,5 +1,10 @@
import type { PlanType } from "../../sdk" import type { PlanType } from "../../sdk"
import type { Layout, App, Screen } from "../../documents" import type { Layout, App, Screen } from "../../documents"
import { ReadStream } from "fs"
export interface SyncAppResponse {
message: string
}
export interface CreateAppRequest { export interface CreateAppRequest {
name: string name: string
@ -12,6 +17,8 @@ export interface CreateAppRequest {
file?: { path: string } file?: { path: string }
} }
export interface CreateAppResponse extends App {}
export interface DuplicateAppRequest { export interface DuplicateAppRequest {
name: string name: string
url?: string url?: string
@ -37,6 +44,8 @@ export interface FetchAppPackageResponse {
hasLock: boolean hasLock: boolean
} }
export type FetchAppsResponse = App[]
export interface PublishResponse { export interface PublishResponse {
_id: string _id: string
status: string status: string
@ -45,3 +54,27 @@ export interface PublishResponse {
export interface UpdateAppRequest extends Partial<App> {} export interface UpdateAppRequest extends Partial<App> {}
export interface UpdateAppResponse extends App {} export interface UpdateAppResponse extends App {}
export interface UpdateAppClientResponse extends App {}
export interface RevertAppClientResponse extends App {}
export interface DeleteAppResponse {
ok: boolean
}
export interface ImportToUpdateAppRequest {
encryptionPassword?: string
}
export interface ImportToUpdateAppResponse {
message: string
}
export interface SetRevertableAppVersionRequest {
revertableVersion: string
}
export interface ExportAppDumpRequest {
excludeRows: boolean
encryptPassword?: string
}
export type ExportAppDumpResponse = ReadStream

View File

@ -1,8 +1,58 @@
import { DocumentDestroyResponse } from "@budibase/nano" import { DocumentDestroyResponse } from "@budibase/nano"
import { Automation } from "../../documents" import {
Automation,
AutomationLogPage,
AutomationStatus,
Row,
} from "../../documents"
export interface DeleteAutomationResponse extends DocumentDestroyResponse {} export interface DeleteAutomationResponse extends DocumentDestroyResponse {}
export interface FetchAutomationResponse { export interface FetchAutomationResponse {
automations: Automation[] automations: Automation[]
} }
export interface FindAutomationResponse extends Automation {}
export interface UpdateAutomationRequest extends Automation {}
export interface UpdateAutomationResponse {
message: string
automation: Automation
}
export interface CreateAutomationRequest extends Automation {}
export interface CreateAutomationResponse {
message: string
automation: Automation
}
export interface SearchAutomationLogsRequest {
startDate?: string
status?: AutomationStatus
automationId?: string
page?: string
}
export interface SearchAutomationLogsResponse extends AutomationLogPage {}
export interface ClearAutomationLogRequest {
automationId: string
appId: string
}
export interface ClearAutomationLogResponse {
message: string
}
export interface TriggerAutomationRequest {
fields: Record<string, any>
// time in seconds
timeout: number
}
export type TriggerAutomationResponse = Record<string, any> | undefined
export interface TestAutomationRequest {
id?: string
revision?: string
fields: Record<string, any>
row?: Row
}
export interface TestAutomationResponse {}

View File

@ -0,0 +1,12 @@
import { DeploymentDoc, DeploymentStatus } from "../../documents"
export interface PublishAppResponse extends DeploymentDoc {}
export interface DeploymentProgressResponse {
_id: string
appId: string
status?: DeploymentStatus
updatedAt: number
}
export type FetchDeploymentResponse = DeploymentProgressResponse[]

View File

@ -0,0 +1,11 @@
export interface GetVersionResponse {
version: string
}
export interface ClearDevLockResponse {
message: string
}
export interface RevertAppResponse {
message: string
}

View File

@ -16,3 +16,6 @@ export * from "./layout"
export * from "./query" export * from "./query"
export * from "./role" export * from "./role"
export * from "./plugins" export * from "./plugins"
export * from "./apikeys"
export * from "./deployment"
export * from "./dev"

View File

@ -150,7 +150,7 @@ export type OpenAIStepInputs = {
prompt: string prompt: string
model: Model model: Model
} }
enum Model { export enum Model {
GPT_35_TURBO = "gpt-3.5-turbo", GPT_35_TURBO = "gpt-3.5-turbo",
// will only work with api keys that have access to the GPT4 API // will only work with api keys that have access to the GPT4 API
GPT_4 = "gpt-4", GPT_4 = "gpt-4",

View File

@ -311,6 +311,7 @@ export type AutomationStep =
type EmptyInputs = {} type EmptyInputs = {}
export type AutomationStepDefinition = Omit<AutomationStep, "id" | "inputs"> & { export type AutomationStepDefinition = Omit<AutomationStep, "id" | "inputs"> & {
inputs: EmptyInputs inputs: EmptyInputs
deprecated?: boolean
} }
export type AutomationTriggerDefinition = Omit< export type AutomationTriggerDefinition = Omit<
@ -318,6 +319,7 @@ export type AutomationTriggerDefinition = Omit<
"id" | "inputs" "id" | "inputs"
> & { > & {
inputs: EmptyInputs inputs: EmptyInputs
deprecated?: boolean
} }
export type AutomationTriggerInputs<T extends AutomationTriggerStepId> = export type AutomationTriggerInputs<T extends AutomationTriggerStepId> =

View File

@ -0,0 +1,22 @@
export enum DeploymentStatus {
SUCCESS = "SUCCESS",
PENDING = "PENDING",
FAILURE = "FAILURE",
}
export interface DeploymentDoc {
_id: string
verification: any
status?: DeploymentStatus
history?: Record<
string,
{
_id: string
appId: string
status?: DeploymentStatus
updatedAt: number
}
>
err?: any
appUrl?: string
}

View File

@ -18,3 +18,4 @@ export * from "./sqlite"
export * from "./snippet" export * from "./snippet"
export * from "./rowAction" export * from "./rowAction"
export * from "./theme" export * from "./theme"
export * from "./deployment"

View File

@ -0,0 +1,5 @@
import { Document } from "../../"
export interface ApiKeyDoc extends Document {
apiKeys: Record<string, string>
}

View File

@ -7,3 +7,4 @@ export * from "./schedule"
export * from "./templates" export * from "./templates"
export * from "./environmentVariables" export * from "./environmentVariables"
export * from "./auditLogs" export * from "./auditLogs"
export * from "./apikeys"