diff --git a/packages/server/src/automations/steps/createRow.js b/packages/server/src/automations/steps/createRow.js index 0b7897809a..aeb75958f6 100644 --- a/packages/server/src/automations/steps/createRow.js +++ b/packages/server/src/automations/steps/createRow.js @@ -58,7 +58,7 @@ module.exports.definition = { }, } -module.exports.run = async function({ inputs, appId, apiKey }) { +module.exports.run = async function({ inputs, appId, apiKey, emitter }) { // TODO: better logging of when actions are missed due to missing parameters if (inputs.row == null || inputs.row.tableId == null) { return @@ -77,6 +77,7 @@ module.exports.run = async function({ inputs, appId, apiKey }) { body: inputs.row, }, user: { appId }, + eventEmitter: emitter, } try { diff --git a/packages/server/src/automations/steps/createUser.js b/packages/server/src/automations/steps/createUser.js index b24c2cbe56..5eb96e6791 100644 --- a/packages/server/src/automations/steps/createUser.js +++ b/packages/server/src/automations/steps/createUser.js @@ -58,7 +58,7 @@ module.exports.definition = { }, } -module.exports.run = async function({ inputs, appId, apiKey }) { +module.exports.run = async function({ inputs, appId, apiKey, emitter }) { const { username, password, accessLevelId } = inputs const ctx = { user: { @@ -67,6 +67,7 @@ module.exports.run = async function({ inputs, appId, apiKey }) { request: { body: { username, password, accessLevelId }, }, + eventEmitter: emitter, } try { diff --git a/packages/server/src/automations/steps/deleteRow.js b/packages/server/src/automations/steps/deleteRow.js index f4884caad3..8edee38dee 100644 --- a/packages/server/src/automations/steps/deleteRow.js +++ b/packages/server/src/automations/steps/deleteRow.js @@ -50,7 +50,7 @@ module.exports.definition = { }, } -module.exports.run = async function({ inputs, appId, apiKey }) { +module.exports.run = async function({ inputs, appId, apiKey, emitter }) { // TODO: better logging of when actions are missed due to missing parameters if (inputs.id == null || inputs.revision == null) { return @@ -62,6 +62,7 @@ module.exports.run = async function({ inputs, appId, apiKey }) { revId: inputs.revision, }, user: { appId }, + eventEmitter: emitter, } try { diff --git a/packages/server/src/automations/steps/updateRow.js b/packages/server/src/automations/steps/updateRow.js index b134ebfa9c..3b83f961f5 100644 --- a/packages/server/src/automations/steps/updateRow.js +++ b/packages/server/src/automations/steps/updateRow.js @@ -53,7 +53,7 @@ module.exports.definition = { }, } -module.exports.run = async function({ inputs, appId }) { +module.exports.run = async function({ inputs, appId, emitter }) { if (inputs.rowId == null || inputs.row == null) { return } @@ -79,6 +79,7 @@ module.exports.run = async function({ inputs, appId }) { body: inputs.row, }, user: { appId }, + eventEmitter: emitter, } try { diff --git a/packages/server/src/automations/thread.js b/packages/server/src/automations/thread.js index 26ca8f04a3..6c02875a03 100644 --- a/packages/server/src/automations/thread.js +++ b/packages/server/src/automations/thread.js @@ -2,6 +2,7 @@ const handlebars = require("handlebars") const actions = require("./actions") const logic = require("./logic") const automationUtils = require("./automationUtils") +const AutomationEmitter = require("../events/AutomationEmitter") handlebars.registerHelper("object", value => { return new handlebars.SafeString(JSON.stringify(value)) @@ -32,12 +33,18 @@ function recurseMustache(inputs, context) { */ class Orchestrator { constructor(automation, triggerOutput) { + this._metadata = triggerOutput.metadata + this._chainCount = this._metadata ? this._metadata.automationChainCount : 0 this._appId = triggerOutput.appId // remove from context delete triggerOutput.appId + delete triggerOutput.metadata // step zero is never used as the mustache is zero indexed for customer facing this._context = { steps: [{}], trigger: triggerOutput } this._automation = automation + // create an emitter which has the chain count for this automation run in it, so it can block + // excessive chaining if required + this._emitter = new AutomationEmitter(this._chainCount + 1) } async getStepFunctionality(type, stepId) { @@ -68,6 +75,7 @@ class Orchestrator { inputs: step.inputs, appId: this._appId, apiKey: automation.apiKey, + emitter: this._emitter, }) if (step.stepId === FILTER_STEP_ID && !outputs.success) { break diff --git a/packages/server/src/events/AutomationEmitter.js b/packages/server/src/events/AutomationEmitter.js new file mode 100644 index 0000000000..43cea664a9 --- /dev/null +++ b/packages/server/src/events/AutomationEmitter.js @@ -0,0 +1,51 @@ +const { rowEmission, tableEmission } = require("./utils") +const mainEmitter = require("./index") + +// max number of automations that can chain on top of each other +const MAX_AUTOMATION_CHAIN = 5 + +/** + * Special emitter which takes the count of automation runs which have occurred and blocks an + * automation from running if it has reached the maximum number of chained automations runs. + * This essentially "fakes" the normal emitter to add some functionality in-between to stop automations + * from getting stuck endlessly chaining. + */ +class AutomationEmitter { + constructor(chainCount) { + this.chainCount = chainCount + this.metadata = { + automationChainCount: chainCount, + } + } + + emitRow(eventName, appId, row, table = null) { + // don't emit even if we've reached max automation chain + if (this.chainCount > MAX_AUTOMATION_CHAIN) { + return + } + rowEmission({ + emitter: mainEmitter, + eventName, + appId, + row, + table, + metadata: this.metadata, + }) + } + + emitTable(eventName, appId, table = null) { + // don't emit even if we've reached max automation chain + if (this.chainCount > MAX_AUTOMATION_CHAIN) { + return + } + tableEmission({ + emitter: mainEmitter, + eventName, + appId, + table, + metadata: this.metadata, + }) + } +} + +module.exports = AutomationEmitter diff --git a/packages/server/src/events/index.js b/packages/server/src/events/index.js index 237e212293..6fd97487d6 100644 --- a/packages/server/src/events/index.js +++ b/packages/server/src/events/index.js @@ -1,4 +1,5 @@ const EventEmitter = require("events").EventEmitter +const { rowEmission, tableEmission } = require("./utils") /** * keeping event emitter in one central location as it might be used for things other than @@ -12,36 +13,11 @@ const EventEmitter = require("events").EventEmitter */ class BudibaseEmitter extends EventEmitter { emitRow(eventName, appId, row, table = null) { - let event = { - row, - appId, - tableId: row.tableId, - } - if (table) { - event.table = table - } - event.id = row._id - if (row._rev) { - event.revision = row._rev - } - this.emit(eventName, event) + rowEmission({ emitter: this, eventName, appId, row, table }) } emitTable(eventName, appId, table = null) { - const tableId = table._id - let event = { - table: { - ...table, - tableId: tableId, - }, - appId, - tableId: tableId, - } - event.id = tableId - if (table._rev) { - event.revision = table._rev - } - this.emit(eventName, event) + tableEmission({ emitter: this, eventName, appId, table }) } } diff --git a/packages/server/src/events/utils.js b/packages/server/src/events/utils.js new file mode 100644 index 0000000000..2d43139d27 --- /dev/null +++ b/packages/server/src/events/utils.js @@ -0,0 +1,38 @@ +exports.rowEmission = ({ emitter, eventName, appId, row, table, metadata }) => { + let event = { + row, + appId, + tableId: row.tableId, + } + if (table) { + event.table = table + } + event.id = row._id + if (row._rev) { + event.revision = row._rev + } + if (metadata) { + event.metadata = metadata + } + emitter.emit(eventName, event) +} + +exports.tableEmission = ({ emitter, eventName, appId, table, metadata }) => { + const tableId = table._id + let event = { + table: { + ...table, + tableId: tableId, + }, + appId, + tableId: tableId, + } + event.id = tableId + if (table._rev) { + event.revision = table._rev + } + if (metadata) { + event.metadata = metadata + } + emitter.emit(eventName, event) +}