server side event emitter
This commit is contained in:
parent
9a898a8d74
commit
5c1aa00fd0
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
export let actionId
|
||||
|
||||
function addBlockToWorkflow() {
|
||||
// TODO: store the block type in the DB as well
|
||||
workflowStore.actions.addBlockToWorkflow({
|
||||
...blockDefinition,
|
||||
args: {},
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -8,6 +8,4 @@ export const triggerWorkflow = api => ({ workflow }) => {
|
|||
workflowOrchestrator.strategy = clientStrategy
|
||||
|
||||
workflowOrchestrator.execute(workflow)
|
||||
|
||||
// hit the API and get the workflow data back
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" },
|
||||
// },
|
||||
// },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue