mirror of https://github.com/Budibase/budibase.git
17 changed files with 302 additions and 233 deletions
@ -1,24 +0,0 @@ |
|||
const userController = require("../../user") |
|||
|
|||
module.exports = async function createUser({ args, instanceId }) { |
|||
const ctx = { |
|||
params: { |
|||
instanceId, |
|||
}, |
|||
request: { |
|||
body: args.user, |
|||
}, |
|||
} |
|||
|
|||
try { |
|||
const response = await userController.create(ctx) |
|||
return { |
|||
user: response, |
|||
} |
|||
} catch (err) { |
|||
console.error(err) |
|||
return { |
|||
user: null, |
|||
} |
|||
} |
|||
} |
|||
@ -1,5 +0,0 @@ |
|||
const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) |
|||
|
|||
module.exports = async function delay({ args }) { |
|||
await wait(args.time) |
|||
} |
|||
@ -1,10 +0,0 @@ |
|||
module.exports = async function filter({ args }) { |
|||
const { field, condition, value } = args |
|||
switch (condition) { |
|||
case "equals": |
|||
if (field !== value) return |
|||
break |
|||
default: |
|||
return |
|||
} |
|||
} |
|||
@ -1,29 +0,0 @@ |
|||
const recordController = require("../../record") |
|||
|
|||
module.exports = async function saveRecord({ args, context }) { |
|||
const { model, ...record } = args.record |
|||
|
|||
const ctx = { |
|||
params: { |
|||
instanceId: context.instanceId, |
|||
modelId: model._id, |
|||
}, |
|||
request: { |
|||
body: record, |
|||
}, |
|||
user: { instanceId: context.instanceId }, |
|||
} |
|||
|
|||
try { |
|||
await recordController.save(ctx) |
|||
return { |
|||
record: ctx.body, |
|||
} |
|||
} catch (err) { |
|||
console.error(err) |
|||
return { |
|||
record: null, |
|||
error: err.message, |
|||
} |
|||
} |
|||
} |
|||
@ -1,26 +0,0 @@ |
|||
const sgMail = require("@sendgrid/mail") |
|||
|
|||
sgMail.setApiKey(process.env.SENDGRID_API_KEY) |
|||
|
|||
module.exports = async function sendEmail({ args }) { |
|||
const msg = { |
|||
to: args.to, |
|||
from: args.from, |
|||
subject: args.subject, |
|||
text: args.text, |
|||
} |
|||
|
|||
try { |
|||
await sgMail.send(msg) |
|||
return { |
|||
success: true, |
|||
...args, |
|||
} |
|||
} catch (err) { |
|||
console.error(err) |
|||
return { |
|||
success: false, |
|||
error: err.message, |
|||
} |
|||
} |
|||
} |
|||
@ -1,33 +1,11 @@ |
|||
const EventEmitter = require("events").EventEmitter |
|||
const CouchDB = require("../db") |
|||
const { Orchestrator, serverStrategy } = require("./workflow") |
|||
|
|||
const emitter = new EventEmitter() |
|||
|
|||
async function executeRelevantWorkflows(event, eventType) { |
|||
const db = new CouchDB(event.instanceId) |
|||
const workflowsToTrigger = await db.query("database/by_workflow_trigger", { |
|||
key: [eventType], |
|||
include_docs: true, |
|||
}) |
|||
|
|||
const workflows = workflowsToTrigger.rows.map(wf => wf.doc) |
|||
|
|||
// Create orchestrator
|
|||
const workflowOrchestrator = new Orchestrator() |
|||
workflowOrchestrator.strategy = serverStrategy |
|||
/** |
|||
* keeping event emitter in one central location as it might be used for things other than |
|||
* workflows (what it was for originally) - having a central emitter will be useful in the |
|||
* future. |
|||
*/ |
|||
|
|||
for (let workflow of workflows) { |
|||
workflowOrchestrator.execute(workflow, event) |
|||
} |
|||
} |
|||
|
|||
emitter.on("record:save", async function(event) { |
|||
await executeRelevantWorkflows(event, "record:save") |
|||
}) |
|||
|
|||
emitter.on("record:delete", async function(event) { |
|||
await executeRelevantWorkflows(event, "record:delete") |
|||
}) |
|||
const emitter = new EventEmitter() |
|||
|
|||
module.exports = emitter |
|||
|
|||
@ -1,52 +0,0 @@ |
|||
const mustache = require("mustache") |
|||
|
|||
/** |
|||
* The workflow orchestrator is a class responsible for executing workflows. |
|||
* It relies on the strategy pattern, which allows composable behaviour to be |
|||
* passed into its execute() function. This allows custom execution behaviour based |
|||
* on where the orchestrator is run. |
|||
* |
|||
*/ |
|||
exports.Orchestrator = class Orchestrator { |
|||
set strategy(strategy) { |
|||
this._strategy = strategy() |
|||
} |
|||
|
|||
async execute(workflow, context) { |
|||
if (workflow.live) { |
|||
this._strategy.run(workflow.definition, context) |
|||
} |
|||
} |
|||
} |
|||
|
|||
exports.serverStrategy = () => ({ |
|||
context: {}, |
|||
bindContextArgs: function(args) { |
|||
const mappedArgs = { ...args } |
|||
|
|||
// bind the workflow action args to the workflow context, if required
|
|||
for (let arg in args) { |
|||
const argValue = args[arg] |
|||
// We don't want to render mustache templates on non-strings
|
|||
if (typeof argValue !== "string") continue |
|||
|
|||
mappedArgs[arg] = mustache.render(argValue, { context: this.context }) |
|||
} |
|||
|
|||
return mappedArgs |
|||
}, |
|||
run: async function(workflow, context) { |
|||
for (let block of workflow.steps) { |
|||
const action = require(`../api/controllers/workflow/actions/${block.actionId}`) |
|||
const response = await action({ |
|||
args: this.bindContextArgs(block.args), |
|||
context, |
|||
}) |
|||
|
|||
this.context = { |
|||
...this.context, |
|||
[block.id]: response, |
|||
} |
|||
} |
|||
}, |
|||
}) |
|||
@ -0,0 +1,86 @@ |
|||
const userController = require("../api/controllers/user") |
|||
const recordController = require("../api/controllers/record") |
|||
const sgMail = require("@sendgrid/mail") |
|||
|
|||
sgMail.setApiKey(process.env.SENDGRID_API_KEY) |
|||
|
|||
let BUILTIN_ACTIONS = { |
|||
CREATE_USER: async function({ args, instanceId }) { |
|||
const ctx = { |
|||
params: { |
|||
instanceId, |
|||
}, |
|||
request: { |
|||
body: args.user, |
|||
}, |
|||
} |
|||
|
|||
try { |
|||
const response = await userController.create(ctx) |
|||
return { |
|||
user: response, |
|||
} |
|||
} catch (err) { |
|||
console.error(err) |
|||
return { |
|||
user: null, |
|||
} |
|||
} |
|||
}, |
|||
SAVE_RECORD: async function({ args, context }) { |
|||
const { model, ...record } = args.record |
|||
|
|||
const ctx = { |
|||
params: { |
|||
instanceId: context.instanceId, |
|||
modelId: model._id, |
|||
}, |
|||
request: { |
|||
body: record, |
|||
}, |
|||
user: { instanceId: context.instanceId }, |
|||
} |
|||
|
|||
try { |
|||
await recordController.save(ctx) |
|||
return { |
|||
record: ctx.body, |
|||
} |
|||
} catch (err) { |
|||
console.error(err) |
|||
return { |
|||
record: null, |
|||
error: err.message, |
|||
} |
|||
} |
|||
}, |
|||
SEND_EMAIL: async function({ args }) { |
|||
const msg = { |
|||
to: args.to, |
|||
from: args.from, |
|||
subject: args.subject, |
|||
text: args.text, |
|||
} |
|||
|
|||
try { |
|||
await sgMail.send(msg) |
|||
return { |
|||
success: true, |
|||
...args, |
|||
} |
|||
} catch (err) { |
|||
console.error(err) |
|||
return { |
|||
success: false, |
|||
error: err.message, |
|||
} |
|||
} |
|||
}, |
|||
} |
|||
|
|||
module.exports.getAction = async function(actionName) { |
|||
if (BUILTIN_ACTIONS[actionName] != null) { |
|||
return BUILTIN_ACTIONS[actionName] |
|||
} |
|||
// TODO: load async actions here
|
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
const mustache = require("mustache") |
|||
const actions = require("./actions") |
|||
const logic = require("./logic") |
|||
const triggers = require("./triggers") |
|||
|
|||
/** |
|||
* The workflow orchestrator is a class responsible for executing workflows. |
|||
* It relies on the strategy pattern, which allows composable behaviour to be |
|||
* passed into its execute() function. This allows custom execution behaviour based |
|||
* on where the orchestrator is run. |
|||
* |
|||
*/ |
|||
class Orchestrator { |
|||
constructor(workflow) { |
|||
this._context = {} |
|||
this._workflow = workflow |
|||
} |
|||
|
|||
async getStep(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 workflow step by name ${stepId}` |
|||
} |
|||
return step |
|||
} |
|||
|
|||
async execute(context) { |
|||
let workflow = this._workflow |
|||
if (!workflow.live) { |
|||
return |
|||
} |
|||
for (let block of workflow.steps) { |
|||
let step = this.getStep(block.type, block.stepId) |
|||
let args = { ...block.args } |
|||
// bind the workflow action args to the workflow context, if required
|
|||
for (let arg of Object.keys(args)) { |
|||
const argValue = args[arg] |
|||
// We don't want to render mustache templates on non-strings
|
|||
if (typeof argValue !== "string") continue |
|||
|
|||
args[arg] = mustache.render(argValue, { context: this._context }) |
|||
} |
|||
const response = await step({ |
|||
args, |
|||
context, |
|||
}) |
|||
|
|||
this._context = { |
|||
...this._context, |
|||
[block.id]: response, |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports.init = function() { |
|||
triggers.workflowQueue.process(async job => { |
|||
// Create orchestrator for each individual workflow (their own context)
|
|||
const workflowOrchestrator = new Orchestrator(job.data.workflow) |
|||
await workflowOrchestrator.execute(job.data.event) |
|||
}) |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
const wait = ms => new Promise(resolve => setTimeout(resolve, ms)) |
|||
|
|||
let LOGIC = { |
|||
DELAY: async function delay({ args }) { |
|||
await wait(args.time) |
|||
}, |
|||
|
|||
FILTER: async function filter({ args }) { |
|||
const { field, condition, value } = args |
|||
switch (condition) { |
|||
case "equals": |
|||
if (field !== value) return |
|||
break |
|||
default: |
|||
return |
|||
} |
|||
}, |
|||
} |
|||
|
|||
module.exports.getLogic = function(logicName) { |
|||
if (LOGIC[logicName] != null) { |
|||
return LOGIC[logicName] |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
let events = require("events") |
|||
|
|||
// Bull works with a Job wrapper around all messages that contains a lot more information about
|
|||
// the state of the message, implement this for the sake of maintaining API consistency
|
|||
function newJob(queue, message) { |
|||
return { |
|||
timestamp: Date.now(), |
|||
queue: queue, |
|||
data: message, |
|||
} |
|||
} |
|||
|
|||
// designed to replicate Bull (https://github.com/OptimalBits/bull) in memory as a sort of mock
|
|||
class InMemoryQueue { |
|||
// opts is not used by this as there is no real use case when in memory, but is the same API as Bull
|
|||
constructor(name, opts) { |
|||
this._name = name |
|||
this._opts = opts |
|||
this._messages = [] |
|||
this._emitter = new events.EventEmitter() |
|||
} |
|||
|
|||
// same API as bull, provide a callback and it will respond when messages are available
|
|||
process(func) { |
|||
this._emitter.on("message", async () => { |
|||
if (this._messages.length <= 0) { |
|||
return |
|||
} |
|||
let msg = this._messages.shift() |
|||
let resp = func(msg) |
|||
if (resp.then != null) { |
|||
await resp |
|||
} |
|||
}) |
|||
} |
|||
|
|||
// simply puts a message to the queue and emits to the queue for processing
|
|||
add(msg) { |
|||
this._messages.push(newJob(this._name, msg)) |
|||
this._emitter.emit("message") |
|||
} |
|||
} |
|||
|
|||
module.exports = InMemoryQueue |
|||
@ -0,0 +1,32 @@ |
|||
const CouchDB = require("../db") |
|||
const emitter = require("../events/index") |
|||
const InMemoryQueue = require("./queue/inMemoryQueue") |
|||
|
|||
let workflowQueue = new InMemoryQueue() |
|||
|
|||
async function queueRelevantWorkflows(event, eventType) { |
|||
const db = new CouchDB(event.instanceId) |
|||
const workflowsToTrigger = await db.query("database/by_workflow_trigger", { |
|||
key: [eventType], |
|||
include_docs: true, |
|||
}) |
|||
|
|||
const workflows = workflowsToTrigger.rows.map(wf => wf.doc) |
|||
for (let workflow of workflows) { |
|||
workflowQueue.add({ workflow, event }) |
|||
} |
|||
} |
|||
|
|||
emitter.on("record:save", async function(event) { |
|||
await queueRelevantWorkflows(event, "record:save") |
|||
}) |
|||
|
|||
emitter.on("record:delete", async function(event) { |
|||
await queueRelevantWorkflows(event, "record:delete") |
|||
}) |
|||
|
|||
module.exports.externalTrigger = async function(workflow, params) { |
|||
workflowQueue.add({ workflow, event: params }) |
|||
} |
|||
|
|||
module.exports.workflowQueue = workflowQueue |
|||
Loading…
Reference in new issue