server side event emitter

This commit is contained in:
Martin McKeaveney 2020-05-31 17:12:52 +01:00
parent 65d0161007
commit dc90e141f5
22 changed files with 214 additions and 127 deletions

View File

@ -43,6 +43,7 @@
"@nx-js/compiler-util": "^2.0.0", "@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0", "codemirror": "^5.51.0",
"date-fns": "^1.29.0", "date-fns": "^1.29.0",
"deepmerge": "^4.2.2",
"feather-icons": "^4.21.0", "feather-icons": "^4.21.0",
"flatpickr": "^4.5.7", "flatpickr": "^4.5.7",
"lodash": "^4.17.13", "lodash": "^4.17.13",

View File

@ -1,6 +1,6 @@
import mustache from "mustache" import mustache from "mustache"
// TODO: tidy up import // TODO: tidy up import
import blockDefinitions from "../../../components/workflow/WorkflowPanel/blockDefinitions" import blockDefinitions from "components/workflow/WorkflowPanel/blockDefinitions"
import { generate } from "shortid" import { generate } from "shortid"
/** /**
@ -12,6 +12,10 @@ export default class Workflow {
this.workflow = workflow this.workflow = workflow
} }
isEmpty() {
return !this.workflow.definition.next
}
addBlock(block) { addBlock(block) {
let node = this.workflow.definition let node = this.workflow.definition
while (node.next) node = node.next while (node.next) node = node.next
@ -74,6 +78,7 @@ export default class Workflow {
const tagline = definition.tagline || "" const tagline = definition.tagline || ""
const args = block.args || {} const args = block.args || {}
// all the fields the workflow block needs to render in the UI
tree.push({ tree.push({
id: block.id, id: block.id,
type: block.type, type: block.type,
@ -81,6 +86,7 @@ export default class Workflow {
args, args,
heading: block.actionId, heading: block.actionId,
body: mustache.render(tagline, args), body: mustache.render(tagline, args),
name: definition.name
}) })
return this.buildUiTree(block.next, tree) return this.buildUiTree(block.next, tree)

View File

@ -0,0 +1,45 @@
<script>
import { store } from "builderStore"
import deepmerge from "deepmerge";
export let value
let pages = []
let components = []
let pageName
let selectedPage
let selectedScreen
$: pages = $store.pages
$: selectedPage = pages[pageName]
$: screens = selectedPage ? selectedPage._screens : []
$: if (selectedPage) {
let result = selectedPage
for (screen of screens) {
result = deepmerge(result, screen)
}
components = result.props._children
}
</script>
<div class="uk-margin block-field">
<label class="uk-form-label">Page</label>
<div class="uk-form-controls">
<select class="budibase__input" bind:value={pageName}>
{#each Object.keys(pages) as page}
<option value={page}>{page}</option>
{/each}
</select>
</div>
{#if components.length > 0}
<label class="uk-form-label">Component</label>
<div class="uk-form-controls">
<select class="budibase__input" bind:value>
{#each components as component}
<option value={component._id}>{component._id}</option>
{/each}
</select>
</div>
{/if}
</div>

View File

@ -0,0 +1,17 @@
<script>
import { backendUiStore } from "builderStore"
export let value
</script>
<div class="uk-margin block-field">
<label class="uk-form-label">Page</label>
<div class="uk-form-controls">
<select class="budibase__input" bind:value>
{#each $backendUiStore.models as model}
<option value={model}>{model.name}</option>
{/each}
</select>
</div>
</div>

View File

@ -79,10 +79,11 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 20px;
} }
span:not(.selected) { header > span {
color: var(--dark-grey); color: var(--font);
} }
label { label {

View File

@ -1,21 +1,19 @@
<script> <script>
import { backendUiStore, store } from "builderStore" import { backendUiStore, store } from "builderStore"
import ComponentSelector from "./ParamInputs/ComponentSelector.svelte";
import ModelSelector from "./ParamInputs/ModelSelector.svelte";
export let workflowBlock export let workflowBlock
let params let params
console.log("wfblock", workflowBlock)
$: workflowParams = workflowBlock.params $: workflowParams = workflowBlock.params
? Object.entries(workflowBlock.params) ? Object.entries(workflowBlock.params)
: [] : []
$: components = Object.values($store.components).filter(comp => comp.name)
// $: workflowArgs = workflowBlock.args ? Object.keys(workflowBlock.args) : []
</script> </script>
<label class="uk-form-label"> <label class="uk-form-label">
{workflowBlock.type}: {workflowBlock.actionId} {workflowBlock.type}: {workflowBlock.name}
</label> </label>
{#each workflowParams as [parameter, type]} {#each workflowParams as [parameter, type]}
<div class="uk-margin block-field"> <div class="uk-margin block-field">
@ -29,6 +27,8 @@
<option value={option}>{option}</option> <option value={option}>{option}</option>
{/each} {/each}
</select> </select>
{:else if type === 'component'}
<ComponentSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'accessLevel'} {:else if type === 'accessLevel'}
<select <select
class="budibase__input" class="budibase__input"
@ -52,19 +52,7 @@
class="budibase__input" class="budibase__input"
bind:value={workflowBlock.args[parameter]} /> bind:value={workflowBlock.args[parameter]} />
{:else if type === 'model'} {:else if type === 'model'}
<select <ModelSelector bind:value={workflowBlock.args[parameter]} />
class="budibase__input"
bind:value={workflowBlock.args[parameter]}>
{#each $backendUiStore.models as model}
<option value={model._id}>{model.name}</option>
{/each}
</select>
{:else if type === 'component'}
<select class="budibase__input">
{#each components as component}
<option value={component.id}>{component.name}</option>
{/each}
</select>
{:else if type === 'string'} {:else if type === 'string'}
<input <input
type="text" type="text"
@ -87,4 +75,8 @@
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
} }
textarea {
min-height: 150px;
}
</style> </style>

View File

@ -1,42 +1,45 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { backendUiStore } from "builderStore" import { backendUiStore, workflowStore } from "builderStore"
import { WorkflowList } from "../" import { WorkflowList } from "../"
import WorkflowBlock from "./WorkflowBlock.svelte" import WorkflowBlock from "./WorkflowBlock.svelte"
import api from "builderStore/api" import api from "builderStore/api"
import blockDefinitions from "../blockDefinitions" import blockDefinitions from "../blockDefinitions"
const SUB_TABS = [
{
name: "Triggers",
key: "TRIGGER",
},
{
name: "Actions",
key: "ACTION",
},
{
name: "Logic",
key: "LOGIC",
},
]
let selectedTab = "TRIGGER" let selectedTab = "TRIGGER"
let definitions = [] let definitions = []
$: definitions = Object.entries(blockDefinitions[selectedTab]) $: definitions = Object.entries(blockDefinitions[selectedTab])
$: {
if (!$workflowStore.currentWorkflow.isEmpty() && selectedTab === "TRIGGER") {
selectedTab = "ACTION"
}
}
</script> </script>
<section> <section>
<div class="subtabs"> <div class="subtabs">
{#each SUB_TABS as tab} {#if $workflowStore.currentWorkflow.isEmpty()}
<span <span
class="hoverable" class="hoverable"
class:selected={tab.key === selectedTab} class:selected={'TRIGGER' === selectedTab}
on:click={() => (selectedTab = tab.key)}> on:click={() => (selectedTab = 'TRIGGER')}>
{tab.name} Triggers
</span> </span>
{/each} {/if}
<span
class="hoverable"
class:selected={'ACTION' === selectedTab}
on:click={() => (selectedTab = 'ACTION')}>
Actions
</span>
<span
class="hoverable"
class:selected={'LOGIC' === selectedTab}
on:click={() => (selectedTab = 'LOGIC')}>
Logic
</span>
</div> </div>
<div id="blocklist"> <div id="blocklist">
{#each definitions as [actionId, blockDefinition]} {#each definitions as [actionId, blockDefinition]}

View File

@ -6,7 +6,6 @@
export let actionId export let actionId
function addBlockToWorkflow() { function addBlockToWorkflow() {
// TODO: store the block type in the DB as well
workflowStore.actions.addBlockToWorkflow({ workflowStore.actions.addBlockToWorkflow({
...blockDefinition, ...blockDefinition,
args: {}, args: {},

View File

@ -11,7 +11,7 @@
<header> <header>
<span <span
class="hoverable" class="hoverable workflow-header"
class:selected={selectedTab === 'WORKFLOWS'} class:selected={selectedTab === 'WORKFLOWS'}
on:click={() => (selectedTab = 'WORKFLOWS')}> on:click={() => (selectedTab = 'WORKFLOWS')}>
Workflows Workflows
@ -37,10 +37,13 @@
font-weight: bold; font-weight: bold;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
margin-bottom: 20px; margin-bottom: 20px;
} }
.workflow-header {
margin-right: 20px;
}
span:not(.selected) { span:not(.selected) {
color: var(--dark-grey); color: var(--dark-grey);
} }

View File

@ -7,7 +7,7 @@ const ACTION = {
environment: "CLIENT", environment: "CLIENT",
params: { params: {
path: "string", path: "string",
value: "string", value: "longText",
}, },
}, },
NAVIGATE: { NAVIGATE: {
@ -77,8 +77,9 @@ const ACTION = {
const TRIGGER = { const TRIGGER = {
RECORD_SAVED: { RECORD_SAVED: {
name: "Record Saved", name: "Record Saved",
event: "record:save",
icon: "ri-save-line", icon: "ri-save-line",
tagline: "Record is added to {{model}}", tagline: "Record is added to <b>{{model.name}}</b>",
description: "Save a record to your database.", description: "Save a record to your database.",
environment: "SERVER", environment: "SERVER",
params: { params: {
@ -87,44 +88,45 @@ const TRIGGER = {
}, },
RECORD_DELETED: { RECORD_DELETED: {
name: "Record Deleted", name: "Record Deleted",
event: "record:delete",
icon: "ri-delete-bin-line", icon: "ri-delete-bin-line",
tagline: "Record is deleted from <b>{{model}}</b>", tagline: "Record is deleted from <b>{{model.name}}</b>",
description: "Fired when a record is deleted from your database.", description: "Fired when a record is deleted from your database.",
environment: "SERVER", environment: "SERVER",
params: { params: {
model: "model" model: "model"
}, },
}, },
CLICK: { // CLICK: {
name: "Click", // name: "Click",
icon: "ri-cursor-line", // icon: "ri-cursor-line",
tagline: "{{component}} is clicked", // tagline: "{{component}} is clicked",
description: "Trigger when you click on an element in the UI.", // description: "Trigger when you click on an element in the UI.",
environment: "CLIENT", // environment: "CLIENT",
params: { // params: {
component: "component" // component: "component"
} // }
}, // },
LOAD: { // LOAD: {
name: "Load", // name: "Load",
icon: "ri-loader-line", // icon: "ri-loader-line",
tagline: "{{component}} is loaded", // tagline: "{{component}} is loaded",
description: "Trigger an element has finished loading.", // description: "Trigger an element has finished loading.",
environment: "CLIENT", // environment: "CLIENT",
params: { // params: {
component: "component" // component: "component"
} // }
}, // },
INPUT: { // INPUT: {
name: "Input", // name: "Input",
icon: "ri-text", // icon: "ri-text",
tagline: "Text entered into {{component}", // tagline: "Text entered into {{component}",
description: "Trigger when you type into an input box.", // description: "Trigger when you type into an input box.",
environment: "CLIENT", // environment: "CLIENT",
params: { // params: {
component: "component" // component: "component"
} // }
}, // },
} }
const LOGIC = { const LOGIC = {
@ -135,9 +137,9 @@ const LOGIC = {
description: "Filter any workflows which do not meet certain conditions.", description: "Filter any workflows which do not meet certain conditions.",
environment: "CLIENT", environment: "CLIENT",
params: { params: {
field: "string", filter: "string",
condition: [ condition: [
"equals" "equals",
], ],
value: "string" value: "string"
}, },

View File

@ -8,6 +8,4 @@ export const triggerWorkflow = api => ({ workflow }) => {
workflowOrchestrator.strategy = clientStrategy workflowOrchestrator.strategy = clientStrategy
workflowOrchestrator.execute(workflow) workflowOrchestrator.execute(workflow)
// hit the API and get the workflow data back
} }

View File

@ -23,7 +23,10 @@ export default class Orchestrator {
async execute(workflowId) { async execute(workflowId) {
const EXECUTE_WORKFLOW_URL = `/api/${this.instanceId}/workflows/${workflowId}` const EXECUTE_WORKFLOW_URL = `/api/${this.instanceId}/workflows/${workflowId}`
const workflow = await this.api.get({ url: EXECUTE_WORKFLOW_URL }) const workflow = await this.api.get({ url: EXECUTE_WORKFLOW_URL })
this._strategy.run(workflow.definition)
if (workflow.live) {
this._strategy.run(workflow.definition)
}
} }
} }
@ -80,17 +83,9 @@ export const clientStrategy = ({ api, instanceId }) => ({
if (block.actionId === "FILTER") { if (block.actionId === "FILTER") {
const { field, condition, value } = block.args; const { field, condition, value } = block.args;
switch (condition) { switch (condition) {
case "=": case "equals":
if (field !== value) return; if (field !== value) return;
break; break;
case "!=":
if (field === value) return;
break;
case "gt":
if (field < value) return;
break;
case "lt":
if (field > value) return;
default: default:
return; return;
} }

View File

@ -29,6 +29,16 @@ exports.create = async function(ctx) {
emit([doc.type], doc._id) emit([doc.type], doc._id)
}.toString(), }.toString(),
}, },
by_workflow_trigger: {
map: function(doc) {
if (doc.type === "workflow") {
const trigger = doc.definition.next
if (trigger) {
emit([trigger.event], trigger)
}
}
}.toString()
}
}, },
}) })

View File

@ -5,7 +5,6 @@ const newid = require("../../db/newid")
const ajv = new Ajv() const ajv = new Ajv()
exports.save = async function(ctx) { exports.save = async function(ctx) {
console.log("THIS INSTANCE", ctx.params.instanceId)
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.params.instanceId)
const record = ctx.request.body const record = ctx.request.body
record.modelId = ctx.params.modelId record.modelId = ctx.params.modelId
@ -45,7 +44,11 @@ exports.save = async function(ctx) {
record.type = "record" record.type = "record"
const response = await db.post(record) const response = await db.post(record)
record._rev = response.rev record._rev = response.rev
// ctx.eventPublisher.emit("RECORD_CREATED", record)
ctx.eventEmitter.emit(`record:save`, {
record,
instanceId: ctx.params.instanceId
})
ctx.body = record ctx.body = record
ctx.status = 200 ctx.status = 200
ctx.message = `${model.name} created successfully` ctx.message = `${model.name} created successfully`
@ -85,4 +88,5 @@ exports.destroy = async function(ctx) {
return return
} }
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId) ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
ctx.eventEmitter.emit(`record:delete`, record)
} }

View File

@ -10,24 +10,6 @@ exports.create = async function(ctx) {
workflow._id = newid() workflow._id = newid()
// TODO: Possibly validate the workflow against a schema
// // validation with ajv
// const model = await db.get(record.modelId)
// const validate = ajv.compile({
// properties: model.schema,
// })
// const valid = validate(record)
// if (!valid) {
// ctx.status = 400
// ctx.body = {
// status: 400,
// errors: validate.errors,
// }
// return
// }
workflow.type = "workflow" workflow.type = "workflow"
const response = await db.post(workflow) const response = await db.post(workflow)
workflow._rev = response.rev workflow._rev = response.rev

View File

@ -4,7 +4,7 @@ const logger = require("koa-pino-logger")
const http = require("http") const http = require("http")
const api = require("./api") const api = require("./api")
const env = require("./environment") const env = require("./environment")
const eventPublisher = require("./events") const eventEmitter = require("./events")
const app = new Koa() const app = new Koa()
@ -20,7 +20,7 @@ app.use(
}) })
) )
app.context.publisher = eventPublisher app.context.eventEmitter = eventEmitter
// api routes // api routes
app.use(api.routes()) app.use(api.routes())

View File

@ -1,3 +1,31 @@
const EventEmitter = require("events").EventEmitter const EventEmitter = require("events").EventEmitter
const CouchDB = require("../db");
module.exports = new EventEmitter() const emitter = new EventEmitter()
function determineWorkflowsToTrigger(instanceId, event) {
const db = new CouchDB(instanceId);
const workflowsToTrigger = await db.query("database/by_workflow_trigger", {
key: [event]
})
return workflowsToTrigger.rows;
}
emitter.on("record:save", async function(event) {
const workflowsToTrigger = await determineWorkflowsToTrigger(instanceId, "record:save")
for (let workflow of workflowsToTrigger) {
// SERVER SIDE STUFF!!
}
})
emitter.on("record:delete", function(event) {
const workflowsToTrigger = await determineWorkflowsToTrigger(instanceId, "record:delete")
for (let workflow of workflowsToTrigger) {
// SERVER SIDE STUFF!!
}
})
module.exports = emitter

View File

@ -17,18 +17,19 @@ const WORKFLOW_SCHEMA = {
type: "object", type: "object",
properties: { properties: {
triggers: { type: "array" }, triggers: { type: "array" },
next: { steps: { type: "array" }
type: "object", // next: {
properties: { // type: "object",
environment: { environment: "string" }, // properties: {
type: { type: "string" }, // environment: { environment: "string" },
actionId: { type: "string" }, // type: { type: "string" },
args: { type: "object" }, // actionId: { type: "string" },
conditions: { type: "array" }, // args: { type: "object" },
errorHandling: { type: "object" }, // conditions: { type: "array" },
next: { type: "object" }, // errorHandling: { type: "object" },
}, // next: { type: "object" },
}, // },
// },
}, },
}, },
}, },