2022-03-20 02:13:54 +01:00
|
|
|
import { Thread, ThreadType } from "../threads"
|
|
|
|
import { definitions } from "./triggerInfo"
|
2022-10-13 18:39:26 +02:00
|
|
|
import { automationQueue } from "./bullboard"
|
2022-03-20 02:13:54 +01:00
|
|
|
import newid from "../db/newid"
|
|
|
|
import { updateEntityMetadata } from "../utilities"
|
2022-10-25 19:19:18 +02:00
|
|
|
import { MetadataTypes } from "../constants"
|
2022-11-11 12:57:50 +01:00
|
|
|
import { db as dbCore, context } from "@budibase/backend-core"
|
2022-09-28 09:56:45 +02:00
|
|
|
import { getAutomationMetadataParams } from "../db/utils"
|
2022-03-20 02:13:54 +01:00
|
|
|
import { cloneDeep } from "lodash/fp"
|
|
|
|
import { quotas } from "@budibase/pro"
|
2022-10-25 19:19:18 +02:00
|
|
|
import { Automation, WebhookActionType } from "@budibase/types"
|
|
|
|
import sdk from "../sdk"
|
2021-09-08 20:29:28 +02:00
|
|
|
|
2022-09-28 09:56:45 +02:00
|
|
|
const REBOOT_CRON = "@reboot"
|
2021-09-08 20:29:28 +02:00
|
|
|
const WH_STEP_ID = definitions.WEBHOOK.stepId
|
|
|
|
const CRON_STEP_ID = definitions.CRON.stepId
|
2021-11-11 13:11:09 +01:00
|
|
|
const Runner = new Thread(ThreadType.AUTOMATION)
|
2021-09-07 20:06:20 +02:00
|
|
|
|
2022-09-28 09:56:45 +02:00
|
|
|
const jobMessage = (job: any, message: string) => {
|
|
|
|
return `app=${job.data.event.appId} automation=${job.data.automation._id} jobId=${job.id} trigger=${job.data.automation.definition.trigger.event} : ${message}`
|
|
|
|
}
|
|
|
|
|
2022-03-20 02:13:54 +01:00
|
|
|
export async function processEvent(job: any) {
|
2021-09-07 20:06:20 +02:00
|
|
|
try {
|
2022-05-26 17:01:10 +02:00
|
|
|
const automationId = job.data.automation._id
|
2022-09-28 09:56:45 +02:00
|
|
|
console.log(jobMessage(job, "running"))
|
2022-04-12 17:18:41 +02:00
|
|
|
// need to actually await these so that an error can be captured properly
|
2022-05-30 15:06:42 +02:00
|
|
|
return await context.doInContext(job.data.event.appId, async () => {
|
2022-03-20 02:13:54 +01:00
|
|
|
const runFn = () => Runner.run(job)
|
2022-05-26 17:01:10 +02:00
|
|
|
return quotas.addAutomation(runFn, {
|
|
|
|
automationId,
|
|
|
|
})
|
2022-03-20 02:13:54 +01:00
|
|
|
})
|
2021-09-07 20:06:20 +02:00
|
|
|
} catch (err) {
|
2022-03-30 15:26:51 +02:00
|
|
|
const errJson = JSON.stringify(err)
|
2022-09-28 09:56:45 +02:00
|
|
|
console.error(jobMessage(job, `was unable to run - ${errJson}`))
|
2022-03-21 15:25:31 +01:00
|
|
|
console.trace(err)
|
2021-09-13 18:43:53 +02:00
|
|
|
return { err }
|
2021-09-07 20:06:20 +02:00
|
|
|
}
|
|
|
|
}
|
2021-09-08 20:29:28 +02:00
|
|
|
|
2022-03-20 02:13:54 +01:00
|
|
|
export async function updateTestHistory(
|
|
|
|
appId: any,
|
|
|
|
automation: any,
|
|
|
|
history: any
|
|
|
|
) {
|
2021-09-10 14:52:41 +02:00
|
|
|
return updateEntityMetadata(
|
|
|
|
MetadataTypes.AUTOMATION_TEST_HISTORY,
|
|
|
|
automation._id,
|
2022-03-20 02:13:54 +01:00
|
|
|
(metadata: any) => {
|
2021-09-10 14:52:41 +02:00
|
|
|
if (metadata && Array.isArray(metadata.history)) {
|
|
|
|
metadata.history.push(history)
|
|
|
|
} else {
|
|
|
|
metadata = {
|
|
|
|
history: [history],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return metadata
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-03-20 02:13:54 +01:00
|
|
|
export function removeDeprecated(definitions: any) {
|
2021-12-14 18:59:02 +01:00
|
|
|
const base = cloneDeep(definitions)
|
|
|
|
for (let key of Object.keys(base)) {
|
|
|
|
if (base[key].deprecated) {
|
|
|
|
delete base[key]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return base
|
|
|
|
}
|
|
|
|
|
2021-09-08 20:29:28 +02:00
|
|
|
// end the repetition and the job itself
|
2022-03-20 02:13:54 +01:00
|
|
|
export async function disableAllCrons(appId: any) {
|
2021-09-08 20:29:28 +02:00
|
|
|
const promises = []
|
2022-10-13 18:39:26 +02:00
|
|
|
const jobs = await automationQueue.getRepeatableJobs()
|
2021-09-08 20:29:28 +02:00
|
|
|
for (let job of jobs) {
|
|
|
|
if (job.key.includes(`${appId}_cron`)) {
|
2022-10-13 18:39:26 +02:00
|
|
|
promises.push(automationQueue.removeRepeatableByKey(job.key))
|
2022-03-20 02:13:54 +01:00
|
|
|
if (job.id) {
|
2022-10-13 18:39:26 +02:00
|
|
|
promises.push(automationQueue.removeJobs(job.id))
|
2022-03-20 02:13:54 +01:00
|
|
|
}
|
2021-09-08 20:29:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return Promise.all(promises)
|
|
|
|
}
|
|
|
|
|
2022-10-14 14:26:42 +02:00
|
|
|
export async function disableCronById(jobId: number | string) {
|
|
|
|
const repeatJobs = await automationQueue.getRepeatableJobs()
|
|
|
|
for (let repeatJob of repeatJobs) {
|
|
|
|
if (repeatJob.id === jobId) {
|
|
|
|
await automationQueue.removeRepeatableByKey(repeatJob.key)
|
|
|
|
}
|
|
|
|
}
|
2022-09-28 09:56:45 +02:00
|
|
|
console.log(`jobId=${jobId} disabled`)
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function clearMetadata() {
|
2022-11-11 12:57:50 +01:00
|
|
|
const db = context.getProdAppDB()
|
2022-09-28 09:56:45 +02:00
|
|
|
const automationMetadata = (
|
|
|
|
await db.allDocs(
|
|
|
|
getAutomationMetadataParams({
|
|
|
|
include_docs: true,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
).rows.map((row: any) => row.doc)
|
|
|
|
for (let metadata of automationMetadata) {
|
|
|
|
metadata._deleted = true
|
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-09-08 20:29:28 +02:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
* @param {object|undefined} automation The automation object to be updated.
|
|
|
|
*/
|
2022-09-28 09:56:45 +02:00
|
|
|
export async function enableCronTrigger(appId: any, automation: Automation) {
|
2021-09-08 20:29:28 +02:00
|
|
|
const trigger = automation ? automation.definition.trigger : null
|
2022-09-28 09:56:45 +02:00
|
|
|
|
2021-09-08 20:29:28 +02:00
|
|
|
// need to create cron job
|
2022-09-28 09:56:45 +02:00
|
|
|
if (
|
|
|
|
isCronTrigger(automation) &&
|
|
|
|
!isRebootTrigger(automation) &&
|
|
|
|
trigger?.inputs.cron
|
|
|
|
) {
|
2021-09-08 20:29:28 +02:00
|
|
|
// make a job id rather than letting Bull decide, makes it easier to handle on way out
|
|
|
|
const jobId = `${appId}_cron_${newid()}`
|
2022-10-13 18:39:26 +02:00
|
|
|
const job: any = await automationQueue.add(
|
2021-09-08 20:29:28 +02:00
|
|
|
{
|
|
|
|
automation,
|
|
|
|
event: { appId, timestamp: Date.now() },
|
|
|
|
},
|
|
|
|
{ repeat: { cron: trigger.inputs.cron }, jobId }
|
|
|
|
)
|
|
|
|
// Assign cron job ID from bull so we can remove it later if the cron trigger is removed
|
|
|
|
trigger.cronJobId = job.id
|
2022-01-31 18:27:47 +01:00
|
|
|
// can't use getAppDB here as this is likely to be called from dev app,
|
|
|
|
// but this call could be for dev app or prod app, need to just use what
|
|
|
|
// was passed in
|
2022-11-11 12:57:50 +01:00
|
|
|
await dbCore.doWithDB(appId, async (db: any) => {
|
2022-04-19 20:42:52 +02:00
|
|
|
const response = await db.put(automation)
|
|
|
|
automation._id = response.id
|
|
|
|
automation._rev = response.rev
|
|
|
|
})
|
2021-09-08 20:29:28 +02:00
|
|
|
}
|
|
|
|
return automation
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This function handles checking if any webhooks need to be created or deleted for automations.
|
|
|
|
* @param {string} appId The ID of the app in which we are checking for webhooks
|
|
|
|
* @param {object|undefined} oldAuto The old automation object if updating/deleting
|
|
|
|
* @param {object|undefined} newAuto The new automation object if creating/updating
|
|
|
|
* @returns {Promise<object|undefined>} After this is complete the new automation object may have been updated and should be
|
|
|
|
* written to DB (this does not write to DB as it would be wasteful to repeat).
|
|
|
|
*/
|
2022-03-20 02:13:54 +01:00
|
|
|
export async function checkForWebhooks({ oldAuto, newAuto }: any) {
|
2022-11-11 12:57:50 +01:00
|
|
|
const appId = context.getAppId()
|
|
|
|
if (!appId) {
|
|
|
|
throw new Error("Unable to check webhooks - no app ID in context.")
|
|
|
|
}
|
2021-09-08 20:29:28 +02:00
|
|
|
const oldTrigger = oldAuto ? oldAuto.definition.trigger : null
|
|
|
|
const newTrigger = newAuto ? newAuto.definition.trigger : null
|
|
|
|
const triggerChanged =
|
|
|
|
oldTrigger && newTrigger && oldTrigger.id !== newTrigger.id
|
2022-03-20 02:13:54 +01:00
|
|
|
function isWebhookTrigger(auto: any) {
|
2021-09-08 20:29:28 +02:00
|
|
|
return (
|
|
|
|
auto &&
|
|
|
|
auto.definition.trigger &&
|
|
|
|
auto.definition.trigger.stepId === WH_STEP_ID
|
|
|
|
)
|
|
|
|
}
|
|
|
|
// need to delete webhook
|
|
|
|
if (
|
|
|
|
isWebhookTrigger(oldAuto) &&
|
|
|
|
(!isWebhookTrigger(newAuto) || triggerChanged) &&
|
|
|
|
oldTrigger.webhookId
|
|
|
|
) {
|
|
|
|
try {
|
2022-11-11 12:57:50 +01:00
|
|
|
let db = context.getAppDB()
|
2021-09-08 20:29:28 +02:00
|
|
|
// need to get the webhook to get the rev
|
|
|
|
const webhook = await db.get(oldTrigger.webhookId)
|
|
|
|
// might be updating - reset the inputs to remove the URLs
|
|
|
|
if (newTrigger) {
|
|
|
|
delete newTrigger.webhookId
|
|
|
|
newTrigger.inputs = {}
|
|
|
|
}
|
2022-10-25 19:19:18 +02:00
|
|
|
await sdk.automations.webhook.destroy(webhook._id, webhook._rev)
|
2021-09-08 20:29:28 +02:00
|
|
|
} catch (err) {
|
|
|
|
// don't worry about not being able to delete, if it doesn't exist all good
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// need to create webhook
|
|
|
|
if (
|
|
|
|
(!isWebhookTrigger(oldAuto) || triggerChanged) &&
|
|
|
|
isWebhookTrigger(newAuto)
|
|
|
|
) {
|
2022-10-25 19:19:18 +02:00
|
|
|
const webhook = await sdk.automations.webhook.save(
|
|
|
|
sdk.automations.webhook.newDoc(
|
|
|
|
"Automation webhook",
|
|
|
|
WebhookActionType.AUTOMATION,
|
|
|
|
newAuto._id
|
|
|
|
)
|
|
|
|
)
|
|
|
|
const id = webhook._id
|
2021-09-08 20:29:28 +02:00
|
|
|
newTrigger.webhookId = id
|
2021-11-03 15:08:47 +01:00
|
|
|
// the app ID has to be development for this endpoint
|
|
|
|
// it can only be used when building the app
|
|
|
|
// but the trigger endpoint will always be used in production
|
2022-11-11 12:57:50 +01:00
|
|
|
const prodAppId = dbCore.getProdAppID(appId)
|
2021-09-08 20:29:28 +02:00
|
|
|
newTrigger.inputs = {
|
|
|
|
schemaUrl: `api/webhooks/schema/${appId}/${id}`,
|
2021-11-03 15:08:47 +01:00
|
|
|
triggerUrl: `api/webhooks/trigger/${prodAppId}/${id}`,
|
2021-09-08 20:29:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return newAuto
|
|
|
|
}
|
2021-11-17 17:28:52 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* When removing an app/unpublishing it need to make sure automations are cleaned up (cron).
|
|
|
|
* @param appId {string} the app that is being removed.
|
|
|
|
* @return {Promise<void>} clean is complete if this succeeds.
|
|
|
|
*/
|
2022-03-20 02:13:54 +01:00
|
|
|
export async function cleanupAutomations(appId: any) {
|
|
|
|
await disableAllCrons(appId)
|
2021-11-17 17:28:52 +01:00
|
|
|
}
|
2022-09-28 09:56:45 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if the supplied automation is of a recurring type.
|
|
|
|
* @param automation The automation to check.
|
|
|
|
* @return {boolean} if it is recurring (cron).
|
|
|
|
*/
|
|
|
|
export function isRecurring(automation: Automation) {
|
|
|
|
return automation.definition.trigger.stepId === definitions.CRON.stepId
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isErrorInOutput(output: {
|
|
|
|
steps: { outputs?: { success: boolean } }[]
|
|
|
|
}) {
|
|
|
|
let first = true,
|
|
|
|
error = false
|
|
|
|
for (let step of output.steps) {
|
|
|
|
// skip the trigger, its always successful if automation ran
|
|
|
|
if (first) {
|
|
|
|
first = false
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if (!step.outputs?.success) {
|
|
|
|
error = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return error
|
|
|
|
}
|