const handlebars = require("handlebars") const actions = require("./actions") const logic = require("./logic") const automationUtils = require("./automationUtils") 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 * inputs and handles any outputs. */ class Orchestrator { constructor(automation, triggerOutput) { this._instanceId = triggerOutput.instanceId // remove from context delete triggerOutput.instanceId // step zero is never used as the mustache is zero indexed for customer facing this._context = { steps: [{}], trigger: triggerOutput } this._automation = automation } async getStepFunctionality(type, stepId) { let step = null if (type === "ACTION") { step = await actions.getAction(stepId) } else if (type === "LOGIC") { step = logic.getLogic(stepId) } if (step == null) { throw `Cannot find automation step by name ${stepId}` } return step } async execute() { let automation = this._automation for (let step of automation.definition.steps) { let stepFn = await this.getStepFunctionality(step.type, step.stepId) step.inputs = recurseMustache(step.inputs, this._context) step.inputs = automationUtils.cleanInputValues( step.inputs, step.schema.inputs ) // instanceId is always passed try { const outputs = await stepFn({ inputs: step.inputs, instanceId: this._instanceId, apiKey: automation.apiKey, }) if (step.stepId === FILTER_STEP_ID && !outputs.success) { break } this._context.steps.push(outputs) } catch (err) { console.error(`Automation error - ${step.stepId} - ${err}`) } } } } // callback is required for worker-farm to state that the worker thread has completed module.exports = async (job, cb = null) => { try { const automationOrchestrator = new Orchestrator( job.data.automation, job.data.event ) await automationOrchestrator.execute() if (cb) { cb() } } catch (err) { if (cb) { cb(err) } } }