From 63006e36d4b1203f826a66f3c9a3aff341e357e5 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 9 Aug 2023 16:24:57 +0100 Subject: [PATCH 01/10] Adding the ability to disable an automation, as well making sure that automations have steps before running. --- .../backend-core/src/db/couch/DatabaseImpl.ts | 14 ++++++ .../server/src/api/controllers/application.ts | 4 +- .../server/src/api/controllers/automation.ts | 29 ++++++------ packages/server/src/automations/triggers.ts | 4 +- packages/server/src/automations/utils.ts | 25 +++------- .../src/sdk/app/automations/automations.ts | 38 +++++++++++++++ .../server/src/sdk/app/automations/index.ts | 2 + packages/server/src/threads/automation.ts | 47 ++++++++++++++----- .../types/src/documents/app/automation.ts | 5 ++ packages/types/src/sdk/db.ts | 6 +++ 10 files changed, 124 insertions(+), 50 deletions(-) create mode 100644 packages/server/src/sdk/app/automations/automations.ts diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 29ca4123f5..89f76769b3 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -8,6 +8,7 @@ import { DatabasePutOpts, DatabaseCreateIndexOpts, DatabaseDeleteIndexOpts, + DocExistsResponse, Document, isDocument, } from "@budibase/types" @@ -120,6 +121,19 @@ export class DatabaseImpl implements Database { return this.updateOutput(() => db.get(id)) } + async docExists(docId: string): Promise { + const db = await this.checkSetup() + let _rev, exists + try { + const { etag } = await db.head(docId) + _rev = etag + exists = true + } catch (err) { + exists = false + } + return { _rev, exists } + } + async remove(idOrDoc: string | Document, rev?: string) { const db = await this.checkSetup() let _id: string diff --git a/packages/server/src/api/controllers/application.ts b/packages/server/src/api/controllers/application.ts index c5556479fe..f8b6bd6d16 100644 --- a/packages/server/src/api/controllers/application.ts +++ b/packages/server/src/api/controllers/application.ts @@ -39,9 +39,8 @@ import { } from "../../db/defaultData/datasource_bb_default" import { removeAppFromUserRoles } from "../../utilities/workerRequests" import { stringToReadStream } from "../../utilities" -import { doesUserHaveLock, getLocksById } from "../../utilities/redis" +import { doesUserHaveLock } from "../../utilities/redis" import { cleanupAutomations } from "../../automations/utils" -import { checkAppMetadata } from "../../automations/logging" import { getUniqueRows } from "../../utilities/usageQuota/rows" import { groups, licensing, quotas } from "@budibase/pro" import { @@ -51,7 +50,6 @@ import { PlanType, Screen, UserCtx, - ContextUser, } from "@budibase/types" import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts" import sdk from "../../sdk" diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts index a4697f99f2..c861782a02 100644 --- a/packages/server/src/api/controllers/automation.ts +++ b/packages/server/src/api/controllers/automation.ts @@ -20,7 +20,7 @@ import { Automation, AutomationActionStepId, AutomationResults, - BBContext, + Ctx, } from "@budibase/types" import { getActionDefinitions as actionDefs } from "../../automations/actions" import sdk from "../../sdk" @@ -73,7 +73,7 @@ function cleanAutomationInputs(automation: Automation) { return automation } -export async function create(ctx: BBContext) { +export async function create(ctx: Ctx) { const db = context.getAppDB() let automation = ctx.request.body automation.appId = ctx.appId @@ -142,7 +142,7 @@ export async function handleStepEvents( } } -export async function update(ctx: BBContext) { +export async function update(ctx: Ctx) { const db = context.getAppDB() let automation = ctx.request.body automation.appId = ctx.appId @@ -193,7 +193,7 @@ export async function update(ctx: BBContext) { builderSocket?.emitAutomationUpdate(ctx, automation) } -export async function fetch(ctx: BBContext) { +export async function fetch(ctx: Ctx) { const db = context.getAppDB() const response = await db.allDocs( getAutomationParams(null, { @@ -203,12 +203,11 @@ export async function fetch(ctx: BBContext) { ctx.body = response.rows.map(row => row.doc) } -export async function find(ctx: BBContext) { - const db = context.getAppDB() - ctx.body = await db.get(ctx.params.id) +export async function find(ctx: Ctx) { + ctx.body = await sdk.automations.get(ctx.params.id) } -export async function destroy(ctx: BBContext) { +export async function destroy(ctx: Ctx) { const db = context.getAppDB() const automationId = ctx.params.id const oldAutomation = await db.get(automationId) @@ -222,11 +221,11 @@ export async function destroy(ctx: BBContext) { builderSocket?.emitAutomationDeletion(ctx, automationId) } -export async function logSearch(ctx: BBContext) { +export async function logSearch(ctx: Ctx) { ctx.body = await automations.logs.logSearch(ctx.request.body) } -export async function clearLogError(ctx: BBContext) { +export async function clearLogError(ctx: Ctx) { const { automationId, appId } = ctx.request.body await context.doInAppContext(appId, async () => { const db = context.getProdAppDB() @@ -245,15 +244,15 @@ export async function clearLogError(ctx: BBContext) { }) } -export async function getActionList(ctx: BBContext) { +export async function getActionList(ctx: Ctx) { ctx.body = await getActionDefinitions() } -export async function getTriggerList(ctx: BBContext) { +export async function getTriggerList(ctx: Ctx) { ctx.body = getTriggerDefinitions() } -export async function getDefinitionList(ctx: BBContext) { +export async function getDefinitionList(ctx: Ctx) { ctx.body = { trigger: getTriggerDefinitions(), action: await getActionDefinitions(), @@ -266,7 +265,7 @@ export async function getDefinitionList(ctx: BBContext) { * * *********************/ -export async function trigger(ctx: BBContext) { +export async function trigger(ctx: Ctx) { const db = context.getAppDB() let automation = await db.get(ctx.params.id) @@ -311,7 +310,7 @@ function prepareTestInput(input: any) { return input } -export async function test(ctx: BBContext) { +export async function test(ctx: Ctx) { const db = context.getAppDB() let automation = await db.get(ctx.params.id) await setTestFlag(automation._id!) diff --git a/packages/server/src/automations/triggers.ts b/packages/server/src/automations/triggers.ts index 922bc10343..9cc8c6b077 100644 --- a/packages/server/src/automations/triggers.ts +++ b/packages/server/src/automations/triggers.ts @@ -6,11 +6,11 @@ import { isDevAppID } from "../db/utils" // need this to call directly, so we can get a response import { automationQueue } from "./bullboard" import { checkTestFlag } from "../utilities/redis" -import * as utils from "./utils" import env from "../environment" import { context, db as dbCore } from "@budibase/backend-core" import { Automation, Row, AutomationData, AutomationJob } from "@budibase/types" import { executeSynchronously } from "../threads/automation" +import sdk from "../sdk" export const TRIGGER_DEFINITIONS = definitions const JOB_OPTS = { @@ -142,7 +142,7 @@ export async function rebootTrigger() { let automations = await getAllAutomations() let rebootEvents = [] for (let automation of automations) { - if (utils.isRebootTrigger(automation)) { + if (sdk.automations.isReboot(automation)) { const job = { automation, event: { diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts index 194eff5b32..add5637bfd 100644 --- a/packages/server/src/automations/utils.ts +++ b/packages/server/src/automations/utils.ts @@ -128,19 +128,6 @@ export async function clearMetadata() { await db.bulkDocs(automationMetadata) } -export function isCronTrigger(auto: Automation) { - return ( - auto && - auto.definition.trigger && - auto.definition.trigger.stepId === CRON_STEP_ID - ) -} - -export function isRebootTrigger(auto: Automation) { - const trigger = auto ? auto.definition.trigger : null - return isCronTrigger(auto) && trigger?.inputs.cron === REBOOT_CRON -} - /** * This function handles checking of any cron jobs that need to be enabled/updated. * @param {string} appId The ID of the app in which we are checking for webhooks @@ -148,13 +135,15 @@ export function isRebootTrigger(auto: Automation) { */ export async function enableCronTrigger(appId: any, automation: Automation) { const trigger = automation ? automation.definition.trigger : null + const validCron = + sdk.automations.isCron(automation) && + trigger?.inputs.cron + const needsCreated = + !sdk.automations.isReboot(automation) && + !sdk.automations.disabled(automation) // need to create cron job - if ( - isCronTrigger(automation) && - !isRebootTrigger(automation) && - trigger?.inputs.cron - ) { + if (validCron && needsCreated) { // make a job id rather than letting Bull decide, makes it easier to handle on way out const jobId = `${appId}_cron_${newid()}` const job: any = await automationQueue.add( diff --git a/packages/server/src/sdk/app/automations/automations.ts b/packages/server/src/sdk/app/automations/automations.ts new file mode 100644 index 0000000000..80f84c90e2 --- /dev/null +++ b/packages/server/src/sdk/app/automations/automations.ts @@ -0,0 +1,38 @@ +import { context } from "@budibase/backend-core" +import { Automation, AutomationState, DocumentType } from "@budibase/types" +import { definitions } from "../../../automations/triggerInfo" + +const REBOOT_CRON = "@reboot" + +export async function exists(automationId: string) { + if (!automationId?.startsWith(DocumentType.AUTOMATION)) { + throw new Error("Invalid automation ID.") + } + const db = context.getAppDB() + return db.docExists(automationId) +} + +export async function get(automationId: string) { + const db = context.getAppDB() + return (await db.get(automationId)) as Automation +} + +export function disabled(automation: Automation) { + return automation.state === AutomationState.DISABLED || !hasSteps(automation) +} + +export function isCron(automation: Automation) { + return ( + automation?.definition.trigger && + automation?.definition.trigger.stepId === definitions.CRON.stepId + ) +} + +export function isReboot(automation: Automation) { + const trigger = automation?.definition.trigger + return isCron(automation) && trigger?.inputs.cron === REBOOT_CRON +} + +export function hasSteps(automation: Automation) { + return automation?.definition?.steps?.length > 0 +} diff --git a/packages/server/src/sdk/app/automations/index.ts b/packages/server/src/sdk/app/automations/index.ts index 16530cf085..540d5545fc 100644 --- a/packages/server/src/sdk/app/automations/index.ts +++ b/packages/server/src/sdk/app/automations/index.ts @@ -1,7 +1,9 @@ import * as webhook from "./webhook" import * as utils from "./utils" +import * as automations from "./automations" export default { webhook, utils, + ...automations, } diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index bc0629a939..5a79444531 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -1,10 +1,9 @@ import { default as threadUtils } from "./utils" import { Job } from "bull" -threadUtils.threadSetup() import { - isRecurring, disableCronById, isErrorInOutput, + isRecurring, } from "../automations/utils" import * as actions from "../automations/actions" import * as automationUtils from "../automations/automationUtils" @@ -15,17 +14,17 @@ import { AutomationErrors, MAX_AUTOMATION_RECURRING_ERRORS } from "../constants" import { storeLog } from "../automations/logging" import { Automation, - AutomationStep, - AutomationStatus, - AutomationMetadata, - AutomationJob, AutomationData, + AutomationJob, + AutomationMetadata, + AutomationStatus, + AutomationStep, } from "@budibase/types" import { - LoopStep, - LoopInput, - TriggerOutput, AutomationContext, + LoopInput, + LoopStep, + TriggerOutput, } from "../definitions/automations" import { WorkerCallback } from "./definitions" import { context, logging } from "@budibase/backend-core" @@ -34,6 +33,10 @@ import { cloneDeep } from "lodash/fp" import { performance } from "perf_hooks" import * as sdkUtils from "../sdk/utils" import env from "../environment" +import sdk from "../sdk" + +threadUtils.threadSetup() + const FILTER_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.FILTER.stepId const LOOP_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.LOOP.stepId const CRON_STEP_ID = triggerDefs.CRON.stepId @@ -514,7 +517,8 @@ class Orchestrator { export function execute(job: Job, callback: WorkerCallback) { const appId = job.data.event.appId - const automationId = job.data.automation._id + const automation = job.data.automation + const automationId = automation._id if (!appId) { throw new Error("Unable to execute, event doesn't contain app ID.") } @@ -525,10 +529,30 @@ export function execute(job: Job, callback: WorkerCallback) { appId, automationId, task: async () => { + let automation = job.data.automation, + isCron = sdk.automations.isCron(job.data.automation), + notFound = false + try { + automation = await sdk.automations.get(automationId) + } catch (err: any) { + // automation no longer exists + notFound = err + } + const disabled = sdk.automations.disabled(automation) + const stopAutomation = disabled || notFound const envVars = await sdkUtils.getEnvironmentVariables() // put into automation thread for whole context await context.doInEnvironmentContext(envVars, async () => { const automationOrchestrator = new Orchestrator(job) + // hard stop on automations + if (isCron && stopAutomation) { + await automationOrchestrator.stopCron( + disabled ? "disabled" : "not_found" + ) + } + if (stopAutomation) { + return + } try { const response = await automationOrchestrator.execute() callback(null, response) @@ -557,11 +581,10 @@ export function executeSynchronously(job: Job) { // put into automation thread for whole context return context.doInEnvironmentContext(envVars, async () => { const automationOrchestrator = new Orchestrator(job) - const response = await Promise.race([ + return await Promise.race([ automationOrchestrator.execute(), timeoutPromise, ]) - return response }) }) } diff --git a/packages/types/src/documents/app/automation.ts b/packages/types/src/documents/app/automation.ts index d1dbbec21b..05ac76d5e9 100644 --- a/packages/types/src/documents/app/automation.ts +++ b/packages/types/src/documents/app/automation.ts @@ -100,6 +100,10 @@ export const AutomationStepIdArray = [ ...Object.values(AutomationTriggerStepId), ] +export enum AutomationState { + DISABLED = "disabled", +} + export interface Automation extends Document { definition: { steps: AutomationStep[] @@ -112,6 +116,7 @@ export interface Automation extends Document { name: string internal?: boolean type?: string + state?: AutomationState } interface BaseIOStructure { diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts index e07cc82fd2..9ac8ff26d4 100644 --- a/packages/types/src/sdk/db.ts +++ b/packages/types/src/sdk/db.ts @@ -40,6 +40,11 @@ export type DatabasePutOpts = { force?: boolean } +export type DocExistsResponse = { + _rev?: string + exists: boolean +} + export type DatabaseCreateIndexOpts = { index: { fields: string[] @@ -90,6 +95,7 @@ export interface Database { exists(): Promise checkSetup(): Promise> get(id?: string): Promise + docExists(id: string): Promise remove( id: string | Document, rev?: string From 2622fecd93389144b2f6bb1620352a12f05aadad Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 9 Aug 2023 16:33:37 +0100 Subject: [PATCH 02/10] Quick fix. --- packages/server/src/threads/automation.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/server/src/threads/automation.ts b/packages/server/src/threads/automation.ts index 5a79444531..8bf0b506fe 100644 --- a/packages/server/src/threads/automation.ts +++ b/packages/server/src/threads/automation.ts @@ -1,5 +1,6 @@ import { default as threadUtils } from "./utils" import { Job } from "bull" +threadUtils.threadSetup() import { disableCronById, isErrorInOutput, @@ -35,8 +36,6 @@ import * as sdkUtils from "../sdk/utils" import env from "../environment" import sdk from "../sdk" -threadUtils.threadSetup() - const FILTER_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.FILTER.stepId const LOOP_STEP_ID = actions.BUILTIN_ACTION_DEFINITIONS.LOOP.stepId const CRON_STEP_ID = triggerDefs.CRON.stepId From a3a8d35b8d12ef83044989f62a65e703e8166f59 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 9 Aug 2023 16:58:46 +0100 Subject: [PATCH 03/10] Adding logging of start, end and duration from automation logs. --- packages/server/src/automations/utils.ts | 48 +++++++++++++++++++----- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts index add5637bfd..7b0ab4f2fd 100644 --- a/packages/server/src/automations/utils.ts +++ b/packages/server/src/automations/utils.ts @@ -16,13 +16,14 @@ import { } from "@budibase/types" import sdk from "../sdk" -const REBOOT_CRON = "@reboot" const WH_STEP_ID = definitions.WEBHOOK.stepId -const CRON_STEP_ID = definitions.CRON.stepId const Runner = new Thread(ThreadType.AUTOMATION) -function loggingArgs(job: AutomationJob) { - return [ +function loggingArgs( + job: AutomationJob, + timing?: { start: number; complete?: boolean } +) { + const logs: any[] = [ { _logKey: "automation", trigger: job.data.automation.definition.trigger.event, @@ -32,24 +33,53 @@ function loggingArgs(job: AutomationJob) { jobId: job.id, }, ] + if (timing?.start) { + logs.push({ + _logKey: "startTime", + start: timing.start, + }) + } + if (timing?.start && timing?.complete) { + const end = new Date().getTime() + const duration = end - timing.start + logs.push({ + _logKey: "endTime", + end, + }) + logs.push({ + _logKey: "duration", + duration, + }) + } + return logs } export async function processEvent(job: AutomationJob) { const appId = job.data.event.appId! const automationId = job.data.automation._id! + const start = new Date().getTime() const task = async () => { try { // need to actually await these so that an error can be captured properly - console.log("automation running", ...loggingArgs(job)) + console.log("automation running", ...loggingArgs(job, { start })) const runFn = () => Runner.run(job) const result = await quotas.addAutomation(runFn, { automationId, }) - console.log("automation completed", ...loggingArgs(job)) + const end = new Date().getTime() + const duration = end - start + console.log( + "automation completed", + ...loggingArgs(job, { start, complete: true }) + ) return result } catch (err) { - console.error(`automation was unable to run`, err, ...loggingArgs(job)) + console.error( + `automation was unable to run`, + err, + ...loggingArgs(job, { start, complete: true }) + ) return { err } } } @@ -135,9 +165,7 @@ export async function clearMetadata() { */ export async function enableCronTrigger(appId: any, automation: Automation) { const trigger = automation ? automation.definition.trigger : null - const validCron = - sdk.automations.isCron(automation) && - trigger?.inputs.cron + const validCron = sdk.automations.isCron(automation) && trigger?.inputs.cron const needsCreated = !sdk.automations.isReboot(automation) && !sdk.automations.disabled(automation) From 91f9338741af8e821c06358f53266c2cd96e9361 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Wed, 9 Aug 2023 16:43:47 +0000 Subject: [PATCH 04/10] Bump version to 2.9.21 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 9d89818cbd..05fb4a3b9d 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.9.20", + "version": "2.9.21", "npmClient": "yarn", "packages": [ "packages/*" From f226d7d093fc42e186e7386447aabd47981591a8 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 10 Aug 2023 10:07:05 +0100 Subject: [PATCH 05/10] Update package.json --- packages/backend-core/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 04efe7f5c6..1d6aa5b9fd 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -2,11 +2,11 @@ "name": "@budibase/backend-core", "version": "0.0.0", "description": "Budibase backend core libraries used in server and worker", - "main": "dist/src/index.js", + "main": "dist/index.js", "types": "dist/src/index.d.ts", "exports": { - ".": "./dist/src/index.js", - "./tests": "./dist/tests/index.js", + ".": "./dist/index.js", + "./tests": "./dist/tests.js", "./*": "./dist/*.js" }, "author": "Budibase", From a7be5b9ff7d7e8be88d653575a6f50ca3469d881 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Thu, 10 Aug 2023 09:35:57 +0000 Subject: [PATCH 06/10] Bump version to 2.9.22 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index 05fb4a3b9d..c90d8fb9cf 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.9.21", + "version": "2.9.22", "npmClient": "yarn", "packages": [ "packages/*" From 90e9263dc73921df8e1a38099bbee85d9451c5da Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 10 Aug 2023 13:00:02 +0300 Subject: [PATCH 07/10] Update pro ref --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 3495feff36..6dcfd9833c 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 3495feff36b73ab9abf2cc45df39900e79d38e27 +Subproject commit 6dcfd9833c4c4fbd87d2e9a064d5af09eebc22b2 From 5c66a10f36f56e6a419b17a006ae033bc5be18e9 Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 10 Aug 2023 14:19:01 +0300 Subject: [PATCH 08/10] Update pro ref --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index 6dcfd9833c..de3bd2eda7 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit 6dcfd9833c4c4fbd87d2e9a064d5af09eebc22b2 +Subproject commit de3bd2eda75cdd6993c8208998dda5e0c3bccc8a From b4615af979c05d9fdf858a045478f634808d748e Mon Sep 17 00:00:00 2001 From: Adria Navarro Date: Thu, 10 Aug 2023 14:26:33 +0300 Subject: [PATCH 09/10] Update pro ref --- packages/pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pro b/packages/pro index de3bd2eda7..02626390cd 160000 --- a/packages/pro +++ b/packages/pro @@ -1 +1 @@ -Subproject commit de3bd2eda75cdd6993c8208998dda5e0c3bccc8a +Subproject commit 02626390cde905a248cb60729968667c9e49fae9 From ad98588f306abc6ff8a5d3a1f694ac030204b43d Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Thu, 10 Aug 2023 11:39:18 +0000 Subject: [PATCH 10/10] Bump version to 2.9.23 --- lerna.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lerna.json b/lerna.json index c90d8fb9cf..0df96afd31 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.9.22", + "version": "2.9.23", "npmClient": "yarn", "packages": [ "packages/*"