diff --git a/packages/server/src/api/routes/tests/automation.spec.js b/packages/server/src/api/routes/tests/automation.spec.js index 4c9bdb67cb..128d3530e9 100644 --- a/packages/server/src/api/routes/tests/automation.spec.js +++ b/packages/server/src/api/routes/tests/automation.spec.js @@ -126,8 +126,8 @@ describe("/automations", () => { trigger.id = "wadiawdo34" let createAction = ACTION_DEFINITIONS["CREATE_ROW"] createAction.inputs.row = { - name: "{{trigger.name}}", - description: "{{trigger.description}}" + name: "{{trigger.row.name}}", + description: "{{trigger.row.description}}" } createAction.id = "awde444wk" @@ -166,19 +166,20 @@ describe("/automations", () => { TEST_AUTOMATION.definition.trigger.inputs.tableId = table._id TEST_AUTOMATION.definition.steps[0].inputs.row.tableId = table._id await createAutomation() + await delay(500) + const res = await triggerWorkflow(automation._id) // this looks a bit mad but we don't actually have a way to wait for a response from the automation to // know that it has finished all of its actions - this is currently the best way // also when this runs in CI it is very temper-mental so for now trying to make run stable by repeating until it works // TODO: update when workflow logs are a thing for (let tries = 0; tries < MAX_RETRIES; tries++) { - const res = await triggerWorkflow(automation._id) expect(res.body.message).toEqual(`Automation ${automation._id} has been triggered.`) expect(res.body.automation.name).toEqual(TEST_AUTOMATION.name) await delay(500) let elements = await getAllFromTable(request, appId, table._id) // don't test it unless there are values to test - if (elements.length === 1) { - expect(elements.length).toEqual(1) + if (elements.length > 1) { + expect(elements.length).toEqual(5) expect(elements[0].name).toEqual("Test") expect(elements[0].description).toEqual("TEST") return 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 a9dce4b66f..8496967105 100644 --- a/packages/server/src/automations/steps/createUser.js +++ b/packages/server/src/automations/steps/createUser.js @@ -59,7 +59,7 @@ module.exports.definition = { }, } -module.exports.run = async function({ inputs, appId, apiKey }) { +module.exports.run = async function({ inputs, appId, apiKey, emitter }) { const { email, password, roleId } = inputs const ctx = { user: { @@ -68,6 +68,7 @@ module.exports.run = async function({ inputs, appId, apiKey }) { request: { body: { email, password, roleId }, }, + 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 cc01ec2d4b..e331ee43b6 100644 --- a/packages/server/src/automations/thread.js +++ b/packages/server/src/automations/thread.js @@ -1,10 +1,31 @@ +const handlebars = require("handlebars") const actions = require("./actions") const logic = require("./logic") const automationUtils = require("./automationUtils") -const { recurseMustache } = require("../utilities/mustache") +const AutomationEmitter = require("../events/AutomationEmitter") + +handlebars.registerHelper("object", value => { + return new handlebars.SafeString(JSON.stringify(value)) +}) const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId +function recurseMustache(inputs, context) { + for (let key of Object.keys(inputs)) { + let val = inputs[key] + if (typeof val === "string") { + val = automationUtils.cleanMustache(inputs[key]) + const template = handlebars.compile(val) + inputs[key] = template(context) + } + // this covers objects and arrays + else if (typeof val === "object") { + inputs[key] = recurseMustache(inputs[key], context) + } + } + return inputs +} + /** * The automation orchestrator is a class responsible for executing automations. * It handles the context of the automation and makes sure each step gets the correct @@ -12,12 +33,18 @@ const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId */ 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) { @@ -48,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 @@ -76,4 +104,4 @@ module.exports = async (job, cb = null) => { cb(err) } } -} +} \ No newline at end of file diff --git a/packages/server/src/automations/triggers.js b/packages/server/src/automations/triggers.js index 5d2121df7b..6fbd8e4c25 100644 --- a/packages/server/src/automations/triggers.js +++ b/packages/server/src/automations/triggers.js @@ -174,22 +174,20 @@ async function fillRowOutput(automation, params) { let table = await db.get(tableId) let row = {} for (let schemaKey of Object.keys(table.schema)) { - if (params[schemaKey] != null) { - continue - } + const paramValue = params[schemaKey] let propSchema = table.schema[schemaKey] switch (propSchema.constraints.type) { case "string": - row[schemaKey] = FAKE_STRING + row[schemaKey] = paramValue || FAKE_STRING break case "boolean": - row[schemaKey] = FAKE_BOOL + row[schemaKey] = paramValue || FAKE_BOOL break case "number": - row[schemaKey] = FAKE_NUMBER + row[schemaKey] = paramValue || FAKE_NUMBER break case "datetime": - row[schemaKey] = FAKE_DATETIME + row[schemaKey] = paramValue || FAKE_DATETIME break } } diff --git a/packages/server/src/events/AutomationEmitter.js b/packages/server/src/events/AutomationEmitter.js new file mode 100644 index 0000000000..fbfc445e2c --- /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) +}