server side event emitter

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

View File

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

View File

@ -1,6 +1,6 @@
import mustache from "mustache"
// TODO: tidy up import
import blockDefinitions from "../../../components/workflow/WorkflowPanel/blockDefinitions"
import blockDefinitions from "components/workflow/WorkflowPanel/blockDefinitions"
import { generate } from "shortid"
/**
@ -12,6 +12,10 @@ export default class Workflow {
this.workflow = workflow
}
isEmpty() {
return !this.workflow.definition.next
}
addBlock(block) {
let node = this.workflow.definition
while (node.next) node = node.next
@ -74,6 +78,7 @@ export default class Workflow {
const tagline = definition.tagline || ""
const args = block.args || {}
// all the fields the workflow block needs to render in the UI
tree.push({
id: block.id,
type: block.type,
@ -81,6 +86,7 @@ export default class Workflow {
args,
heading: block.actionId,
body: mustache.render(tagline, args),
name: definition.name
})
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;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
span:not(.selected) {
color: var(--dark-grey);
header > span {
color: var(--font);
}
label {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,4 @@ export const triggerWorkflow = api => ({ workflow }) => {
workflowOrchestrator.strategy = clientStrategy
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) {
const EXECUTE_WORKFLOW_URL = `/api/${this.instanceId}/workflows/${workflowId}`
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") {
const { field, condition, value } = block.args;
switch (condition) {
case "=":
case "equals":
if (field !== value) return;
break;
case "!=":
if (field === value) return;
break;
case "gt":
if (field < value) return;
break;
case "lt":
if (field > value) return;
default:
return;
}

View File

@ -29,6 +29,16 @@ exports.create = async function(ctx) {
emit([doc.type], doc._id)
}.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()
exports.save = async function(ctx) {
console.log("THIS INSTANCE", ctx.params.instanceId)
const db = new CouchDB(ctx.params.instanceId)
const record = ctx.request.body
record.modelId = ctx.params.modelId
@ -45,7 +44,11 @@ exports.save = async function(ctx) {
record.type = "record"
const response = await db.post(record)
record._rev = response.rev
// ctx.eventPublisher.emit("RECORD_CREATED", record)
ctx.eventEmitter.emit(`record:save`, {
record,
instanceId: ctx.params.instanceId
})
ctx.body = record
ctx.status = 200
ctx.message = `${model.name} created successfully`
@ -85,4 +88,5 @@ exports.destroy = async function(ctx) {
return
}
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()
// 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"
const response = await db.post(workflow)
workflow._rev = response.rev

View File

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

View File

@ -1,3 +1,31 @@
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",
properties: {
triggers: { type: "array" },
next: {
type: "object",
properties: {
environment: { environment: "string" },
type: { type: "string" },
actionId: { type: "string" },
args: { type: "object" },
conditions: { type: "array" },
errorHandling: { type: "object" },
next: { type: "object" },
},
},
steps: { type: "array" }
// next: {
// type: "object",
// properties: {
// environment: { environment: "string" },
// type: { type: "string" },
// actionId: { type: "string" },
// args: { type: "object" },
// conditions: { type: "array" },
// errorHandling: { type: "object" },
// next: { type: "object" },
// },
// },
},
},
},