forked from tsai/budibase
12 changed files with 237 additions and 37 deletions
@ -0,0 +1,16 @@ |
|||||
|
import Orchestrator, { clientStrategy } from "./orchestrator"; |
||||
|
|
||||
|
|
||||
|
export const triggerWorkflow = api => ({ workflow }) => { |
||||
|
console.log(workflow); |
||||
|
const workflowOrchestrator = new Orchestrator( |
||||
|
api, |
||||
|
"inst_60dd510_700f7dc06735403e81d5af91072d7241" |
||||
|
); |
||||
|
workflowOrchestrator.strategy = clientStrategy |
||||
|
|
||||
|
workflowOrchestrator.execute(workflow); |
||||
|
|
||||
|
// hit the API and get the workflow data back
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,103 @@ |
|||||
|
import get from "lodash/fp/get"; |
||||
|
|
||||
|
/** |
||||
|
* The workflow orhestrator 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. |
||||
|
* |
||||
|
*/ |
||||
|
export default class Orchestrator { |
||||
|
constructor(api, instanceId) { |
||||
|
this.api = api |
||||
|
this.instanceId = instanceId |
||||
|
} |
||||
|
|
||||
|
set strategy(strategy) { |
||||
|
this._strategy = strategy |
||||
|
} |
||||
|
|
||||
|
async execute(workflowId) { |
||||
|
const EXECUTE_WORKFLOW_URL = `/api/${this.instanceId}/workflows/${workflowId}`; |
||||
|
const workflow = await this.api.get({ url: EXECUTE_WORKFLOW_URL }) |
||||
|
this._strategy.run({ |
||||
|
workflow: workflow.definition, |
||||
|
api: this.api, |
||||
|
instanceId: this.instanceId |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Execute a workflow from a running budibase app
|
||||
|
export const clientStrategy = { |
||||
|
context: {}, |
||||
|
bindContextArgs: function(args) { |
||||
|
const mappedArgs = { ...args }; |
||||
|
|
||||
|
console.log("original args", args) |
||||
|
|
||||
|
// bind the workflow action args to the workflow context, if required
|
||||
|
for (let arg in args) { |
||||
|
const argValue = args[arg]; |
||||
|
// Means that it's bound to state or workflow context
|
||||
|
if (argValue.startsWith("$")) { |
||||
|
// if value is bound to workflow context.
|
||||
|
if (argValue.startsWith("$context")) { |
||||
|
const path = argValue.replace("$context.", ""); |
||||
|
// pass in the value from context
|
||||
|
mappedArgs[arg] = get(path, this.context); |
||||
|
} |
||||
|
|
||||
|
// if the value is bound to state
|
||||
|
if (argValue.startsWith("$state")) { |
||||
|
const path = argValue.match("$context.", ""); |
||||
|
// pass in the value from context
|
||||
|
mappedArgs[arg] = get(path, this.context); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return Object.values(mappedArgs); |
||||
|
}, |
||||
|
run: async function({ workflow, api, instanceId }) { |
||||
|
const block = workflow.next; |
||||
|
|
||||
|
console.log("Executing workflow block", block); |
||||
|
|
||||
|
if (!block) return; |
||||
|
|
||||
|
// This code gets run in the browser
|
||||
|
if (block.type === "CLIENT") { |
||||
|
if (block.actionId === "SET_STATE") { |
||||
|
// get props from the workflow context if required
|
||||
|
api.setState(...this.bindContextArgs(block.args)) |
||||
|
// update the context with the data
|
||||
|
this.context = { |
||||
|
...this.context, |
||||
|
SET_STATE: block.args |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// this workflow block gets executed on the server
|
||||
|
if (block.type === "SERVER") { |
||||
|
const EXECUTE_WORKFLOW_URL = `/api/${instanceId}/workflows/action` |
||||
|
const response = await api.post({ |
||||
|
url: EXECUTE_WORKFLOW_URL, |
||||
|
body: { |
||||
|
action: block.actionId, |
||||
|
args: block.args |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
this.context = { |
||||
|
...this.context, |
||||
|
[block.actionId]: response |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
console.log("workflowContext", this.context) |
||||
|
|
||||
|
// TODO: clean this up, don't pass all those args
|
||||
|
this.run({ workflow: workflow.next, instanceId, api }); |
||||
|
} |
||||
|
} |
||||
@ -1 +0,0 @@ |
|||||
export const ERROR = "##error_message" |
|
||||
@ -0,0 +1,43 @@ |
|||||
|
const TEST_WORKFLOW = { |
||||
|
"_id": "8ebe79daf1c744c7ab204c0b964e309e", |
||||
|
"_rev": "37-94ae573300721c98267cc1d18822c94d", |
||||
|
"name": "Workflow", |
||||
|
"type": "workflow", |
||||
|
"definition": { |
||||
|
"next": { |
||||
|
"type": "CLIENT", |
||||
|
"actionId": "SET_STATE", |
||||
|
"args": { |
||||
|
"path": "myPath", |
||||
|
"value": "foo" |
||||
|
}, |
||||
|
"next": { |
||||
|
"type": "SERVER", |
||||
|
"actionId": "SAVE_RECORD", |
||||
|
"args": { |
||||
|
"record": { |
||||
|
"modelId": "f452a2b9c3a94251b9ea7be1e20e3b19", |
||||
|
"name": "workflowRecord" |
||||
|
}, |
||||
|
"next": { |
||||
|
"type": "CLIENT", |
||||
|
"actionId": "SET_STATE", |
||||
|
"args": { |
||||
|
"path": "myPath", |
||||
|
"value": "$context.SAVE_RECORD.record.name" |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
describe("Workflow Orchestrator", () => { |
||||
|
it("executes a workflow", () => { |
||||
|
}); |
||||
|
|
||||
|
it("", () => { |
||||
|
|
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,9 @@ |
|||||
|
export default async function () { |
||||
|
const response = await fetch("www.google.com"); |
||||
|
console.log(response); |
||||
|
console.log("CUSTOM ACTION"); |
||||
|
return { |
||||
|
message: "CUSTOM_WORKFLOW_SCRIPT", |
||||
|
response |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
const recordController = require("../../record"); |
||||
|
|
||||
|
module.exports = async function saveRecord(args) { |
||||
|
console.log("SAVING this record", args.record); |
||||
|
|
||||
|
const ctx = { |
||||
|
params: { |
||||
|
instanceId: "inst_60dd510_700f7dc06735403e81d5af91072d7241", |
||||
|
}, |
||||
|
request: { |
||||
|
body: args.record |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
await recordController.save(ctx); |
||||
|
|
||||
|
return { |
||||
|
record: ctx.body |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue