Merge pull request #621 from Budibase/contextual-workflows
Contextual workflows
This commit is contained in:
commit
f07ddef370
|
@ -16,7 +16,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x]
|
||||
node-version: [12.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
node-version: [10.x]
|
||||
node-version: [12.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
@ -21,17 +21,25 @@ context("Create a workflow", () => {
|
|||
// Add trigger
|
||||
cy.get("[data-cy=add-workflow-component]").click()
|
||||
cy.get("[data-cy=RECORD_SAVED]").click()
|
||||
cy.get(".budibase__input").select("dog")
|
||||
cy.get("[data-cy=workflow-block-setup]").within(() => {
|
||||
cy.get("select")
|
||||
.first()
|
||||
.select("dog")
|
||||
})
|
||||
|
||||
// Create action
|
||||
cy.get("[data-cy=SAVE_RECORD]").click()
|
||||
cy.get(".budibase__input").select("dog")
|
||||
cy.get(".container input")
|
||||
.first()
|
||||
.type("goodboy")
|
||||
cy.get(".container input")
|
||||
.eq(1)
|
||||
.type("11")
|
||||
cy.get("[data-cy=workflow-block-setup]").within(() => {
|
||||
cy.get("select")
|
||||
.first()
|
||||
.select("dog")
|
||||
cy.get("input")
|
||||
.first()
|
||||
.type("goodboy")
|
||||
cy.get("input")
|
||||
.eq(1)
|
||||
.type("11")
|
||||
})
|
||||
|
||||
// Save
|
||||
cy.contains("Save Workflow").click()
|
||||
|
@ -44,7 +52,6 @@ context("Create a workflow", () => {
|
|||
|
||||
it("should add record when a new record is added", () => {
|
||||
cy.contains("backend").click()
|
||||
|
||||
cy.addRecord(["Rover", 15])
|
||||
cy.reload()
|
||||
cy.contains("goodboy").should("have.text", "goodboy")
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.33.0",
|
||||
"@budibase/bbui": "^1.34.2",
|
||||
"@budibase/client": "^0.1.21",
|
||||
"@budibase/colorpicker": "^1.0.1",
|
||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||
|
@ -100,7 +100,7 @@
|
|||
"jest": "^24.8.0",
|
||||
"ncp": "^2.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rollup": "^1.12.0",
|
||||
"rollup": "^2.11.2",
|
||||
"rollup-plugin-alias": "^1.5.2",
|
||||
"rollup-plugin-commonjs": "^10.0.0",
|
||||
"rollup-plugin-copy": "^3.0.0",
|
||||
|
@ -110,10 +110,10 @@
|
|||
"rollup-plugin-node-globals": "^1.4.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-svelte": "^5.0.3",
|
||||
"rollup-plugin-terser": "^4.0.4",
|
||||
"rollup-plugin-terser": "^7.0.2",
|
||||
"rollup-plugin-url": "^2.2.2",
|
||||
"start-server-and-test": "^1.11.0",
|
||||
"svelte": "3.23.x",
|
||||
"svelte": "^3.24.1",
|
||||
"svelte-jester": "^1.0.6"
|
||||
},
|
||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||
|
|
|
@ -87,7 +87,7 @@ export const getBackendUiStore = () => {
|
|||
state.models = state.models.filter(
|
||||
existing => existing._id !== model._id
|
||||
)
|
||||
state.selectedModel = state.models[0] || {}
|
||||
state.selectedModel = {}
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
|
|
@ -23,6 +23,7 @@ const workflowActions = store => ({
|
|||
create: async ({ name }) => {
|
||||
const workflow = {
|
||||
name,
|
||||
type: "workflow",
|
||||
definition: {
|
||||
steps: [],
|
||||
},
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<script>
|
||||
import GenericBindingPopover from "./GenericBindingPopover.svelte"
|
||||
import { Input, Icon } from "@budibase/bbui"
|
||||
|
||||
export let bindings = []
|
||||
export let value
|
||||
let anchor
|
||||
let popover = undefined
|
||||
let enrichedValue
|
||||
let inputProps
|
||||
|
||||
// Extract all other props to pass to input component
|
||||
$: {
|
||||
let { bindings, ...otherProps } = $$props
|
||||
inputProps = otherProps
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container" bind:this={anchor}>
|
||||
<Input {...inputProps} bind:value />
|
||||
<button on:click={popover.show}>
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
</div>
|
||||
<GenericBindingPopover
|
||||
{anchor}
|
||||
{bindings}
|
||||
bind:value
|
||||
bind:popover
|
||||
align="right" />
|
||||
|
||||
<style>
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background: var(--grey-4);
|
||||
right: 7px;
|
||||
bottom: 7px;
|
||||
}
|
||||
button:hover {
|
||||
background: var(--grey-5);
|
||||
cursor: pointer;
|
||||
}
|
||||
button :global(svg) {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%) !important;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,129 @@
|
|||
<script>
|
||||
import groupBy from "lodash/fp/groupBy"
|
||||
import { TextArea, Label, Body, Button, Popover } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value = ""
|
||||
export let bindings = []
|
||||
export let anchor
|
||||
export let align
|
||||
export let popover = null
|
||||
|
||||
$: categories = Object.entries(groupBy("category", bindings))
|
||||
|
||||
function onClickBinding(binding) {
|
||||
value += `{{ ${binding.path} }}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<Popover {anchor} {align} bind:this={popover}>
|
||||
<div class="container">
|
||||
<div class="bindings">
|
||||
<Label large>Available bindings</Label>
|
||||
<div class="bindings__wrapper">
|
||||
<div class="bindings__list">
|
||||
{#each categories as [categoryName, bindings]}
|
||||
<Label small>{categoryName}</Label>
|
||||
{#each bindings as binding}
|
||||
<div class="binding" on:click={() => onClickBinding(binding)}>
|
||||
<span class="binding__label">{binding.label}</span>
|
||||
<span class="binding__type">{binding.type}</span>
|
||||
<br />
|
||||
<div class="binding__description">{binding.description}</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor">
|
||||
<Label large>Data binding</Label>
|
||||
<Body small>
|
||||
Binding connects one piece of data to another and makes it dynamic.
|
||||
Click the objects on the left to add them to the textbox.
|
||||
</Body>
|
||||
<TextArea thin bind:value placeholder="..." />
|
||||
<div class="controls">
|
||||
<a href="#">
|
||||
<Body small>Learn more about binding</Body>
|
||||
</a>
|
||||
<Button on:click={popover.hide} primary>Done</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
.bindings {
|
||||
border-right: 1px solid var(--grey-4);
|
||||
flex: 0 0 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
padding-right: var(--spacing-l);
|
||||
}
|
||||
.bindings :global(label) {
|
||||
margin: var(--spacing-m) 0;
|
||||
}
|
||||
.bindings :global(label:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
.bindings__wrapper {
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.bindings__list {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.binding {
|
||||
font-size: 12px;
|
||||
padding: var(--spacing-s);
|
||||
border-radius: var(--border-radius-m);
|
||||
}
|
||||
.binding:hover {
|
||||
background-color: var(--grey-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
.binding__label {
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.binding__description {
|
||||
color: var(--grey-8);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.binding__type {
|
||||
font-family: monospace;
|
||||
background-color: var(--grey-2);
|
||||
border-radius: var(--border-radius-m);
|
||||
padding: 2px;
|
||||
margin-left: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.editor {
|
||||
padding-left: var(--spacing-l);
|
||||
}
|
||||
.editor :global(textarea) {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-m);
|
||||
}
|
||||
</style>
|
|
@ -1,23 +1,15 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { Select } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
$: modelId = value ? value._id : ""
|
||||
|
||||
function onChange(e) {
|
||||
value = $backendUiStore.models.find(model => model._id === e.target.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="block-field">
|
||||
<select
|
||||
class="budibase__input"
|
||||
value={modelId}
|
||||
on:blur={onChange}
|
||||
on:change={onChange}>
|
||||
<Select bind:value secondary thin>
|
||||
<option value="">Choose an option</option>
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
|
@ -1,51 +1,69 @@
|
|||
<script>
|
||||
import { backendUiStore } from "builderStore"
|
||||
import { Input, Label } from "@budibase/bbui"
|
||||
import { Input, Select, Label } from "@budibase/bbui"
|
||||
import BindableInput from "../../../userInterface/BindableInput.svelte"
|
||||
|
||||
export let value
|
||||
$: modelId = value && value.model ? value.model._id : ""
|
||||
$: schemaFields = Object.keys(value && value.model ? value.model.schema : {})
|
||||
export let bindings
|
||||
|
||||
function onChangeModel(e) {
|
||||
value.model = $backendUiStore.models.find(
|
||||
model => model._id === e.target.value
|
||||
)
|
||||
}
|
||||
$: model = $backendUiStore.models.find(model => model._id === value?.modelId)
|
||||
$: schemaFields = Object.entries(model?.schema ?? {})
|
||||
|
||||
function setParsedValue(evt, field) {
|
||||
const fieldSchema = value.model.schema[field]
|
||||
if (fieldSchema.type === "number") {
|
||||
value[field] = parseInt(evt.target.value)
|
||||
return
|
||||
}
|
||||
value[field] = evt.target.value
|
||||
// Ensure any nullish modelId values get set to empty string so
|
||||
// that the select works
|
||||
$: if (value?.modelId == null) value = { modelId: "" }
|
||||
|
||||
function schemaHasOptions(schema) {
|
||||
return !!schema.constraints?.inclusion?.length
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="block-field">
|
||||
<select
|
||||
class="budibase__input"
|
||||
value={modelId}
|
||||
on:blur={onChangeModel}
|
||||
on:change={onChangeModel}>
|
||||
<Select bind:value={value.modelId} thin secondary>
|
||||
<option value="">Choose an option</option>
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{#if schemaFields.length}
|
||||
<div class="bb-margin-xl block-field">
|
||||
<Label small forAttr={'fields'}>Fields</Label>
|
||||
{#each schemaFields as field}
|
||||
<div class="bb-margin-xl">
|
||||
<Input
|
||||
thin
|
||||
value={value[field]}
|
||||
label={field}
|
||||
on:change={e => setParsedValue(e, field)} />
|
||||
{#each schemaFields as [field, schema]}
|
||||
<div class="bb-margin-xl capitalise">
|
||||
{#if schemaHasOptions(schema)}
|
||||
<div class="field-label">{field}</div>
|
||||
<Select thin secondary bind:value={value[field]}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each schema.constraints.inclusion as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if schema.type === 'string' || schema.type === 'number'}
|
||||
<BindableInput
|
||||
thin
|
||||
bind:value={value[field]}
|
||||
label={field}
|
||||
type={schema.type}
|
||||
{bindings} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.field-label {
|
||||
color: var(--ink);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.capitalise :global(label),
|
||||
.field-label {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,8 +10,10 @@
|
|||
|
||||
let selectedTab = "SETUP"
|
||||
|
||||
$: workflow =
|
||||
$workflowStore.selectedWorkflow && $workflowStore.selectedWorkflow.workflow
|
||||
$: workflow = $workflowStore.selectedWorkflow?.workflow
|
||||
$: allowDeleteBlock =
|
||||
$workflowStore.selectedBlock?.type !== "TRIGGER" ||
|
||||
!workflow?.definition?.steps?.length
|
||||
|
||||
function deleteWorkflow() {
|
||||
open(
|
||||
|
@ -60,7 +62,13 @@
|
|||
<Button green wide data-cy="save-workflow-setup" on:click={saveWorkflow}>
|
||||
Save Workflow
|
||||
</Button>
|
||||
<Button red wide on:click={deleteWorkflowBlock}>Delete Block</Button>
|
||||
<Button
|
||||
disabled={!allowDeleteBlock}
|
||||
red
|
||||
wide
|
||||
on:click={deleteWorkflowBlock}>
|
||||
Delete Block
|
||||
</Button>
|
||||
</div>
|
||||
{:else if $workflowStore.selectedWorkflow}
|
||||
<div class="panel">
|
||||
|
|
|
@ -1,41 +1,75 @@
|
|||
<script>
|
||||
import ModelSelector from "./ParamInputs/ModelSelector.svelte"
|
||||
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
|
||||
import { Input, TextArea, Select } from "@budibase/bbui"
|
||||
import { Input, TextArea, Select, Label } from "@budibase/bbui"
|
||||
import { workflowStore } from "builderStore"
|
||||
import BindableInput from "../../userInterface/BindableInput.svelte"
|
||||
|
||||
export let block
|
||||
$: params = block.params ? Object.entries(block.params) : []
|
||||
$: inputs = Object.entries(block.schema?.inputs?.properties || {})
|
||||
$: bindings = getAvailableBindings(
|
||||
block,
|
||||
$workflowStore.selectedWorkflow?.workflow?.definition
|
||||
)
|
||||
|
||||
function getAvailableBindings(block, workflow) {
|
||||
if (!block || !workflow) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Find previous steps to the selected one
|
||||
let allSteps = [...workflow.steps]
|
||||
if (workflow.trigger) {
|
||||
allSteps = [workflow.trigger, ...allSteps]
|
||||
}
|
||||
const blockIdx = allSteps.findIndex(step => step.id === block.id)
|
||||
|
||||
// Extract all outputs from all previous steps as available bindings
|
||||
let bindings = []
|
||||
for (let idx = 0; idx < blockIdx; idx++) {
|
||||
const outputs = Object.entries(
|
||||
allSteps[idx].schema?.outputs?.properties ?? {}
|
||||
)
|
||||
bindings = bindings.concat(
|
||||
outputs.map(([name, value]) => ({
|
||||
label: name,
|
||||
type: value.type,
|
||||
description: value.description,
|
||||
category: idx === 0 ? "Trigger outputs" : `Step ${idx} outputs`,
|
||||
path: idx === 0 ? `trigger.${name}` : `steps.${idx}.${name}`,
|
||||
}))
|
||||
)
|
||||
}
|
||||
return bindings
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="selected-label">{block.name}</div>
|
||||
{#each params as [parameter, type]}
|
||||
<div class="block-field">
|
||||
<label class="label">{parameter}</label>
|
||||
{#if Array.isArray(type)}
|
||||
<Select bind:value={block.args[parameter]} thin secondary>
|
||||
<div class="container" data-cy="workflow-block-setup">
|
||||
<div class="block-label">{block.name}</div>
|
||||
{#each inputs as [key, value]}
|
||||
<div class="bb-margin-xl block-field">
|
||||
<div class="field-label">{value.title}</div>
|
||||
{#if value.type === 'string' && value.enum}
|
||||
<Select bind:value={block.inputs[key]} thin secondary>
|
||||
<option value="">Choose an option</option>
|
||||
{#each type as option}
|
||||
<option value={option}>{option}</option>
|
||||
{#each value.enum as option, idx}
|
||||
<option value={option}>
|
||||
{value.pretty ? value.pretty[idx] : option}
|
||||
</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if type === 'accessLevel'}
|
||||
<Select bind:value={block.args[parameter]} thin secondary>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="POWER_USER">Power User</option>
|
||||
</Select>
|
||||
{:else if type === 'password'}
|
||||
<Input type="password" thin bind:value={block.args[parameter]} />
|
||||
{:else if type === 'number'}
|
||||
<Input type="number" thin bind:value={block.args[parameter]} />
|
||||
{:else if type === 'longText'}
|
||||
<TextArea type="text" thin bind:value={block.args[parameter]} />
|
||||
{:else if type === 'model'}
|
||||
<ModelSelector bind:value={block.args[parameter]} />
|
||||
{:else if type === 'record'}
|
||||
<RecordSelector bind:value={block.args[parameter]} />
|
||||
{:else if type === 'string'}
|
||||
<Input type="text" thin bind:value={block.args[parameter]} />
|
||||
{:else if value.customType === 'password'}
|
||||
<Input type="password" thin bind:value={block.inputs[key]} />
|
||||
{:else if value.customType === 'model'}
|
||||
<ModelSelector bind:value={block.inputs[key]} />
|
||||
{:else if value.customType === 'record'}
|
||||
<RecordSelector bind:value={block.inputs[key]} {bindings} />
|
||||
{:else if value.type === 'string' || value.type === 'number'}
|
||||
<BindableInput
|
||||
type={value.type}
|
||||
thin
|
||||
bind:value={block.inputs[key]}
|
||||
{bindings} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -46,17 +80,16 @@
|
|||
display: grid;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-family: sans-serif;
|
||||
font-weight: 500;
|
||||
.field-label {
|
||||
color: var(--ink);
|
||||
margin-bottom: 12px;
|
||||
text-transform: capitalize;
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.selected-label {
|
||||
.block-label {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--grey-7);
|
||||
|
|
|
@ -4,9 +4,8 @@
|
|||
import { notifier } from "builderStore/store/notifications"
|
||||
import Flowchart from "./flowchart/FlowChart.svelte"
|
||||
|
||||
$: workflow =
|
||||
$workflowStore.selectedWorkflow && $workflowStore.selectedWorkflow.workflow
|
||||
$: workflowLive = workflow && workflow.live
|
||||
$: workflow = $workflowStore.selectedWorkflow?.workflow
|
||||
$: workflowLive = workflow?.live
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
function onSelect(block) {
|
||||
|
|
|
@ -1,42 +1,44 @@
|
|||
<script>
|
||||
import mustache from "mustache"
|
||||
import { workflowStore } from "builderStore"
|
||||
import WorkflowBlockTagline from "./WorkflowBlockTagline.svelte"
|
||||
|
||||
export let onSelect
|
||||
export let block
|
||||
let selected
|
||||
|
||||
$: selected =
|
||||
$workflowStore.selectedBlock != null &&
|
||||
$workflowStore.selectedBlock.id === block.id
|
||||
|
||||
function selectBlock() {
|
||||
onSelect(block)
|
||||
}
|
||||
$: selected = $workflowStore.selectedBlock?.id === block.id
|
||||
$: steps = $workflowStore.selectedWorkflow?.workflow?.definition?.steps ?? []
|
||||
$: blockIdx = steps.findIndex(step => step.id === block.id)
|
||||
</script>
|
||||
|
||||
<div class={`${block.type} hoverable`} class:selected on:click={selectBlock}>
|
||||
<div
|
||||
class={`block ${block.type} hoverable`}
|
||||
class:selected
|
||||
on:click={() => onSelect(block)}>
|
||||
<header>
|
||||
{#if block.type === 'TRIGGER'}
|
||||
<i class="ri-lightbulb-fill" />
|
||||
When this happens...
|
||||
<span>When this happens...</span>
|
||||
{:else if block.type === 'ACTION'}
|
||||
<i class="ri-flashlight-fill" />
|
||||
Do this...
|
||||
<span>Do this...</span>
|
||||
{:else if block.type === 'LOGIC'}
|
||||
<i class="ri-pause-fill" />
|
||||
Only continue if...
|
||||
<i class="ri-git-branch-line" />
|
||||
<span>Only continue if...</span>
|
||||
{/if}
|
||||
<div class="label">
|
||||
{#if block.type === 'TRIGGER'}Trigger{:else}Step {blockIdx + 1}{/if}
|
||||
</div>
|
||||
</header>
|
||||
<hr />
|
||||
<p>
|
||||
{@html mustache.render(block.tagline, block.args)}
|
||||
<WorkflowBlockTagline {block} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
width: 320px;
|
||||
.block {
|
||||
width: 360px;
|
||||
padding: 20px;
|
||||
border-radius: var(--border-radius-m);
|
||||
transition: 0.3s all ease;
|
||||
|
@ -45,14 +47,30 @@
|
|||
font-size: 16px;
|
||||
color: var(--white);
|
||||
}
|
||||
.block.selected,
|
||||
.block:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.15);
|
||||
}
|
||||
|
||||
header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header span {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
header .label {
|
||||
font-size: 14px;
|
||||
padding: var(--spacing-s);
|
||||
color: var(--grey-8);
|
||||
border-radius: var(--border-radius-m);
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
header i {
|
||||
font-size: 20px;
|
||||
margin-right: 5px;
|
||||
|
@ -67,6 +85,10 @@
|
|||
background-color: var(--ink);
|
||||
color: var(--white);
|
||||
}
|
||||
.TRIGGER header .label {
|
||||
background-color: var(--grey-9);
|
||||
color: var(--grey-5);
|
||||
}
|
||||
|
||||
.LOGIC {
|
||||
background-color: var(--blue-light);
|
||||
|
@ -77,10 +99,4 @@
|
|||
color: inherit;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.selected,
|
||||
div:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.15);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
<script>
|
||||
import mustache from "mustache"
|
||||
import { get } from "lodash/fp"
|
||||
import { backendUiStore } from "builderStore"
|
||||
|
||||
export let block
|
||||
|
||||
$: inputs = enrichInputs(block.inputs)
|
||||
$: tagline = formatTagline(block.tagline, block.schema, inputs)
|
||||
$: html = (tagline || "")
|
||||
.replace(/{{\s*/g, "<span>")
|
||||
.replace(/\s*}}/g, "</span>")
|
||||
|
||||
function enrichInputs(inputs) {
|
||||
let enrichedInputs = { ...inputs, enriched: {} }
|
||||
const modelId = inputs.modelId || inputs.record?.modelId
|
||||
if (modelId) {
|
||||
enrichedInputs.enriched.model = $backendUiStore.models.find(
|
||||
model => model._id === modelId
|
||||
)
|
||||
}
|
||||
return enrichedInputs
|
||||
}
|
||||
|
||||
function formatTagline(tagline, schema, inputs) {
|
||||
// Add bold tags around inputs
|
||||
let formattedTagline = tagline
|
||||
.replace(/{{/g, "<b>{{")
|
||||
.replace(/}}/, "}}</b>")
|
||||
|
||||
// Extract schema paths for any input bindings
|
||||
const inputPaths = formattedTagline
|
||||
.match(/{{\s*\S+\s*}}/g)
|
||||
.map(x => x.replace(/[{}]/g, "").trim())
|
||||
const schemaPaths = inputPaths.map(x => x.replace(/\./g, ".properties."))
|
||||
|
||||
// Replace any enum bindings with their pretty equivalents
|
||||
schemaPaths.forEach((path, idx) => {
|
||||
const prettyValues = get(`${path}.pretty`, schema)
|
||||
if (prettyValues) {
|
||||
const enumValues = get(`${path}.enum`, schema)
|
||||
const inputPath = inputPaths[idx]
|
||||
const value = get(inputPath, { inputs })
|
||||
const valueIdx = enumValues.indexOf(value)
|
||||
const prettyValue = prettyValues[valueIdx]
|
||||
if (prettyValue == null) {
|
||||
return
|
||||
}
|
||||
formattedTagline = formattedTagline.replace(
|
||||
new RegExp(`{{\s*${inputPath}\s*}}`),
|
||||
prettyValue
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Fill in bindings with mustache
|
||||
return mustache.render(formattedTagline, { inputs })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{@html html}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
line-height: 1.25;
|
||||
}
|
||||
div :global(span) {
|
||||
font-size: 0.9em;
|
||||
background-color: var(--purple-light);
|
||||
padding: var(--spacing-xs);
|
||||
border-radius: var(--border-radius-m);
|
||||
display: inline-block;
|
||||
margin: 1px;
|
||||
}
|
||||
</style>
|
|
@ -8,9 +8,7 @@
|
|||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
$: selectedWorkflowId =
|
||||
$workflowStore.selectedWorkflow &&
|
||||
$workflowStore.selectedWorkflow.workflow._id
|
||||
$: selectedWorkflowId = $workflowStore.selectedWorkflow?.workflow?._id
|
||||
|
||||
function newWorkflow() {
|
||||
open(
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
class="hoverable"
|
||||
class:selected={selectedTab === 'ADD'}
|
||||
on:click={() => (selectedTab = 'ADD')}>
|
||||
Add
|
||||
Add step
|
||||
</span>
|
||||
{/if}
|
||||
</header>
|
||||
|
|
|
@ -8,6 +8,13 @@
|
|||
dependencies:
|
||||
"@babel/highlight" "^7.8.3"
|
||||
|
||||
"@babel/code-frame@^7.10.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
|
||||
integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.10.4"
|
||||
|
||||
"@babel/compat-data@^7.9.6":
|
||||
version "7.9.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.6.tgz#3f604c40e420131affe6f2c8052e9a275ae2049b"
|
||||
|
@ -184,6 +191,11 @@
|
|||
dependencies:
|
||||
"@babel/types" "^7.8.3"
|
||||
|
||||
"@babel/helper-validator-identifier@^7.10.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
|
||||
integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.9.0", "@babel/helper-validator-identifier@^7.9.5":
|
||||
version "7.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80"
|
||||
|
@ -205,6 +217,15 @@
|
|||
"@babel/traverse" "^7.9.6"
|
||||
"@babel/types" "^7.9.6"
|
||||
|
||||
"@babel/highlight@^7.10.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143"
|
||||
integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.10.4"
|
||||
chalk "^2.0.0"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/highlight@^7.8.3":
|
||||
version "7.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079"
|
||||
|
@ -688,10 +709,10 @@
|
|||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@budibase/bbui@^1.33.0":
|
||||
version "1.33.0"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.33.0.tgz#216b24dd815f45880e9795e66b04848329b0390f"
|
||||
integrity sha512-Rrt5eLbea014TIfAbT40kP0D0AWNUi8Q0kDr3UZO6Aq4UXgjc0f53ZuJ7Kb66YRDWrqiucjf1FtvOUs3/YaD6g==
|
||||
"@budibase/bbui@^1.34.2":
|
||||
version "1.34.2"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.34.2.tgz#e4fcc728dc8d51a918f8ebd5c3f0b0afacfa4047"
|
||||
integrity sha512-6RusGPZnEpHx1gtGcjk/lFLgMgFdDpSIxB8v2MiA+kp+uP1pFlzegbaDh+/JXyqFwK7HO91I0yXXBoPjibi7Aw==
|
||||
dependencies:
|
||||
sirv-cli "^0.4.6"
|
||||
svelte-flatpickr "^2.4.0"
|
||||
|
@ -760,6 +781,7 @@
|
|||
"@fortawesome/fontawesome-free@^5.14.0":
|
||||
version "5.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.14.0.tgz#a371e91029ebf265015e64f81bfbf7d228c9681f"
|
||||
integrity sha512-OfdMsF+ZQgdKHP9jUbmDcRrP0eX90XXrsXIdyjLbkmSBzmMXPABB8eobUJtivaupucYaByz6WNe1PI1JuYm3qA==
|
||||
|
||||
"@hapi/address@^2.1.2":
|
||||
version "2.1.4"
|
||||
|
@ -1137,10 +1159,6 @@
|
|||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||
|
||||
"@types/estree@*":
|
||||
version "0.0.44"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.44.tgz#980cc5a29a3ef3bea6ff1f7d021047d7ea575e21"
|
||||
|
||||
"@types/estree@0.0.39":
|
||||
version "0.0.39"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
||||
|
@ -1283,7 +1301,7 @@ acorn@^6.0.1:
|
|||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
|
||||
|
||||
acorn@^7.1.0, acorn@^7.1.1:
|
||||
acorn@^7.1.1:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.2.0.tgz#17ea7e40d7c8640ff54a694c889c26f31704effe"
|
||||
|
||||
|
@ -1387,6 +1405,7 @@ array-equal@^1.0.0:
|
|||
array-filter@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
|
||||
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
|
||||
|
||||
array-union@^2.1.0:
|
||||
version "2.1.0"
|
||||
|
@ -1445,6 +1464,7 @@ atob@^2.1.2:
|
|||
available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
|
||||
integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
|
||||
dependencies:
|
||||
array-filter "^1.0.0"
|
||||
|
||||
|
@ -1908,7 +1928,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
|
|||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
commander@2, commander@^2.19.0:
|
||||
commander@2, commander@^2.20.0:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
|
||||
|
@ -2400,6 +2420,7 @@ decode-uri-component@^0.2.0:
|
|||
deep-equal@^2.0.1:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0"
|
||||
integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA==
|
||||
dependencies:
|
||||
es-abstract "^1.17.5"
|
||||
es-get-iterator "^1.1.0"
|
||||
|
@ -2588,6 +2609,7 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
|
|||
es-abstract@^1.17.4:
|
||||
version "1.17.6"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
|
||||
integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==
|
||||
dependencies:
|
||||
es-to-primitive "^1.2.1"
|
||||
function-bind "^1.1.1"
|
||||
|
@ -2604,6 +2626,7 @@ es-abstract@^1.17.4:
|
|||
es-abstract@^1.18.0-next.0:
|
||||
version "1.18.0-next.0"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.0.tgz#b302834927e624d8e5837ed48224291f2c66e6fc"
|
||||
integrity sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==
|
||||
dependencies:
|
||||
es-to-primitive "^1.2.1"
|
||||
function-bind "^1.1.1"
|
||||
|
@ -2621,6 +2644,7 @@ es-abstract@^1.18.0-next.0:
|
|||
es-get-iterator@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
|
||||
integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
|
||||
dependencies:
|
||||
es-abstract "^1.17.4"
|
||||
has-symbols "^1.0.1"
|
||||
|
@ -3313,6 +3337,7 @@ is-accessor-descriptor@^1.0.0:
|
|||
is-arguments@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
|
||||
integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
|
||||
|
||||
is-arrayish@^0.2.1:
|
||||
version "0.2.1"
|
||||
|
@ -3321,6 +3346,7 @@ is-arrayish@^0.2.1:
|
|||
is-bigint@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
|
||||
integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
|
||||
|
||||
is-binary-path@~2.1.0:
|
||||
version "2.1.0"
|
||||
|
@ -3331,6 +3357,7 @@ is-binary-path@~2.1.0:
|
|||
is-boolean-object@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
|
||||
integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
|
||||
|
||||
is-buffer@^1.1.5:
|
||||
version "1.1.6"
|
||||
|
@ -3343,6 +3370,7 @@ is-callable@^1.1.4, is-callable@^1.1.5:
|
|||
is-callable@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.1.tgz#4d1e21a4f437509d25ce55f8184350771421c96d"
|
||||
integrity sha512-wliAfSzx6V+6WfMOmus1xy0XvSgf/dlStkvTfq7F0g4bOIW0PSUbnyse3NhDwdyYS1ozfUtAAySqTws3z9Eqgg==
|
||||
|
||||
is-ci@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -3430,6 +3458,7 @@ is-installed-globally@^0.3.2:
|
|||
is-map@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
|
||||
integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
|
||||
|
||||
is-module@^1.0.0:
|
||||
version "1.0.0"
|
||||
|
@ -3438,10 +3467,12 @@ is-module@^1.0.0:
|
|||
is-negative-zero@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461"
|
||||
integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=
|
||||
|
||||
is-number-object@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
|
||||
integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
|
||||
|
||||
is-number@^3.0.0:
|
||||
version "3.0.0"
|
||||
|
@ -3502,12 +3533,14 @@ is-regex@^1.0.5:
|
|||
is-regex@^1.1.0, is-regex@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9"
|
||||
integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==
|
||||
dependencies:
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
is-set@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
|
||||
integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
|
||||
|
||||
is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
|
@ -3520,6 +3553,7 @@ is-stream@^2.0.0:
|
|||
is-string@^1.0.4, is-string@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
|
||||
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
|
||||
|
||||
is-symbol@^1.0.2:
|
||||
version "1.0.3"
|
||||
|
@ -3530,6 +3564,7 @@ is-symbol@^1.0.2:
|
|||
is-typed-array@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d"
|
||||
integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==
|
||||
dependencies:
|
||||
available-typed-arrays "^1.0.0"
|
||||
es-abstract "^1.17.4"
|
||||
|
@ -3543,10 +3578,12 @@ is-typedarray@~1.0.0:
|
|||
is-weakmap@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
|
||||
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
|
||||
|
||||
is-weakset@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
|
||||
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
|
||||
|
||||
is-windows@^1.0.2:
|
||||
version "1.0.2"
|
||||
|
@ -3571,6 +3608,7 @@ isarray@1.0.0, isarray@~1.0.0:
|
|||
isarray@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
|
||||
|
||||
isbuffer@~0.0.0:
|
||||
version "0.0.0"
|
||||
|
@ -3972,13 +4010,22 @@ jest-watcher@^24.9.0:
|
|||
jest-util "^24.9.0"
|
||||
string-length "^2.0.0"
|
||||
|
||||
jest-worker@^24.0.0, jest-worker@^24.6.0, jest-worker@^24.9.0:
|
||||
jest-worker@^24.6.0, jest-worker@^24.9.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5"
|
||||
dependencies:
|
||||
merge-stream "^2.0.0"
|
||||
supports-color "^6.1.0"
|
||||
|
||||
jest-worker@^26.2.1:
|
||||
version "26.3.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f"
|
||||
integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
merge-stream "^2.0.0"
|
||||
supports-color "^7.0.0"
|
||||
|
||||
jest@^24.8.0:
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171"
|
||||
|
@ -4685,10 +4732,12 @@ object-inspect@^1.7.0:
|
|||
object-inspect@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0"
|
||||
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
|
||||
|
||||
object-is@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
|
||||
integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.17.5"
|
||||
|
@ -5086,7 +5135,7 @@ ramda@~0.26.1:
|
|||
version "0.26.1"
|
||||
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
|
||||
|
||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
|
||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||
dependencies:
|
||||
|
@ -5206,6 +5255,7 @@ regex-not@^1.0.0, regex-not@^1.0.2:
|
|||
regexp.prototype.flags@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
|
||||
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.17.0-next.1"
|
||||
|
@ -5213,6 +5263,7 @@ regexp.prototype.flags@^1.3.0:
|
|||
regexparam@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f"
|
||||
integrity sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==
|
||||
|
||||
regexpu-core@^4.7.0:
|
||||
version "4.7.0"
|
||||
|
@ -5450,14 +5501,15 @@ rollup-plugin-svelte@^5.0.3:
|
|||
rollup-pluginutils "^2.8.2"
|
||||
sourcemap-codec "^1.4.8"
|
||||
|
||||
rollup-plugin-terser@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-4.0.4.tgz#6f661ef284fa7c27963d242601691dc3d23f994e"
|
||||
rollup-plugin-terser@^7.0.2:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz#e8fbba4869981b2dc35ae7e8a502d5c6c04d324d"
|
||||
integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.0.0"
|
||||
jest-worker "^24.0.0"
|
||||
serialize-javascript "^1.6.1"
|
||||
terser "^3.14.1"
|
||||
"@babel/code-frame" "^7.10.4"
|
||||
jest-worker "^26.2.1"
|
||||
serialize-javascript "^4.0.0"
|
||||
terser "^5.0.0"
|
||||
|
||||
rollup-plugin-url@^2.2.2:
|
||||
version "2.2.4"
|
||||
|
@ -5473,13 +5525,12 @@ rollup-pluginutils@^2.3.1, rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2:
|
|||
dependencies:
|
||||
estree-walker "^0.6.1"
|
||||
|
||||
rollup@^1.12.0:
|
||||
version "1.32.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.32.1.tgz#4480e52d9d9e2ae4b46ba0d9ddeaf3163940f9c4"
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
"@types/node" "*"
|
||||
acorn "^7.1.0"
|
||||
rollup@^2.11.2:
|
||||
version "2.27.0"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.27.0.tgz#f2b70a8dd583bc3675b36686289aa9a51e27af4f"
|
||||
integrity sha512-1WlbhNdzhLjdhh2wsf6CDxmuBAYG+5O53fYqCcGv8aJOoX/ymCfCY6oZnvllXZzaC/Ng+lPPwq9EMbHOKc5ozA==
|
||||
optionalDependencies:
|
||||
fsevents "~2.1.2"
|
||||
|
||||
rsvp@^4.8.4:
|
||||
version "4.8.5"
|
||||
|
@ -5563,9 +5614,12 @@ semver@~2.3.1:
|
|||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52"
|
||||
|
||||
serialize-javascript@^1.6.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb"
|
||||
serialize-javascript@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa"
|
||||
integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==
|
||||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
|
||||
set-blocking@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -5621,6 +5675,7 @@ shortid@^2.2.15:
|
|||
side-channel@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
|
||||
integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g==
|
||||
dependencies:
|
||||
es-abstract "^1.18.0-next.0"
|
||||
object-inspect "^1.8.0"
|
||||
|
@ -5701,7 +5756,7 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
|
|||
source-map-url "^0.4.0"
|
||||
urix "^0.1.0"
|
||||
|
||||
source-map-support@^0.5.6, source-map-support@~0.5.10:
|
||||
source-map-support@^0.5.6, source-map-support@~0.5.12:
|
||||
version "0.5.19"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
|
||||
dependencies:
|
||||
|
@ -5955,6 +6010,13 @@ supports-color@^6.1.0:
|
|||
dependencies:
|
||||
has-flag "^3.0.0"
|
||||
|
||||
supports-color@^7.0.0:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
|
||||
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
supports-color@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1"
|
||||
|
@ -5985,9 +6047,10 @@ svelte-simple-modal@^0.4.2:
|
|||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/svelte-simple-modal/-/svelte-simple-modal-0.4.2.tgz#2cfe26ec8c0760b89813d65dfee836399620d6b2"
|
||||
|
||||
svelte@3.23.x:
|
||||
version "3.23.0"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.23.0.tgz#bbcd6887cf588c24a975b14467455abfff9acd3f"
|
||||
svelte@^3.24.1:
|
||||
version "3.25.1"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.25.1.tgz#218def1243fea5a97af6eb60f5e232315bb57ac4"
|
||||
integrity sha512-IbrVKTmuR0BvDw4ii8/gBNy8REu7nWTRy9uhUz+Yuae5lIjWgSGwKlWtJGC2Vg95s+UnXPqDu0Kk/sUwe0t2GQ==
|
||||
|
||||
symbol-observable@^1.1.0:
|
||||
version "1.2.0"
|
||||
|
@ -6001,13 +6064,14 @@ synchronous-promise@^2.0.13:
|
|||
version "2.0.13"
|
||||
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.13.tgz#9d8c165ddee69c5a6542862b405bc50095926702"
|
||||
|
||||
terser@^3.14.1:
|
||||
version "3.17.0"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2"
|
||||
terser@^5.0.0:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.3.1.tgz#f50fe20ab48b15234fe9bdd86b10148ad5fca787"
|
||||
integrity sha512-yD80f4hdwCWTH5mojzxe1q8bN1oJbsK/vfJGLcPZM/fl+/jItIVNKhFIHqqR71OipFWMLgj3Kc+GIp6CeIqfnA==
|
||||
dependencies:
|
||||
commander "^2.19.0"
|
||||
commander "^2.20.0"
|
||||
source-map "~0.6.1"
|
||||
source-map-support "~0.5.10"
|
||||
source-map-support "~0.5.12"
|
||||
|
||||
test-exclude@^5.2.3:
|
||||
version "5.2.3"
|
||||
|
@ -6323,6 +6387,7 @@ whatwg-url@^8.0.0:
|
|||
which-boxed-primitive@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
|
||||
integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
|
||||
dependencies:
|
||||
is-bigint "^1.0.0"
|
||||
is-boolean-object "^1.0.0"
|
||||
|
@ -6333,6 +6398,7 @@ which-boxed-primitive@^1.0.1:
|
|||
which-collection@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
|
||||
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
|
||||
dependencies:
|
||||
is-map "^2.0.1"
|
||||
is-set "^2.0.1"
|
||||
|
@ -6346,6 +6412,7 @@ which-module@^2.0.0:
|
|||
which-typed-array@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2"
|
||||
integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==
|
||||
dependencies:
|
||||
available-typed-arrays "^1.0.2"
|
||||
es-abstract "^1.17.5"
|
||||
|
|
|
@ -3,11 +3,16 @@ const validateJs = require("validate.js")
|
|||
const newid = require("../../db/newid")
|
||||
|
||||
function emitEvent(eventType, ctx, record) {
|
||||
// add syntactic sugar for mustache later
|
||||
if (record._id) {
|
||||
record.id = record._id
|
||||
}
|
||||
if (record._rev) {
|
||||
record.revision = record._rev
|
||||
}
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emit(eventType, {
|
||||
args: {
|
||||
record,
|
||||
},
|
||||
record,
|
||||
instanceId: ctx.user.instanceId,
|
||||
})
|
||||
}
|
||||
|
@ -53,7 +58,6 @@ exports.patch = async function(ctx) {
|
|||
ctx.body = record
|
||||
ctx.status = 200
|
||||
ctx.message = `${model.name} updated successfully.`
|
||||
return
|
||||
}
|
||||
|
||||
exports.save = async function(ctx) {
|
||||
|
@ -179,10 +183,13 @@ exports.destroy = async function(ctx) {
|
|||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const record = await db.get(ctx.params.recordId)
|
||||
if (record.modelId !== ctx.params.modelId) {
|
||||
ctx.throw(400, "Supplied modelId doe not match the record's modelId")
|
||||
ctx.throw(400, "Supplied modelId doesn't match the record's modelId")
|
||||
return
|
||||
}
|
||||
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
|
||||
ctx.status = 200
|
||||
// for workflows
|
||||
ctx.record = record
|
||||
emitEvent(`record:delete`, ctx, record)
|
||||
}
|
||||
|
||||
|
|
|
@ -56,6 +56,7 @@ exports.create = async function(ctx) {
|
|||
|
||||
ctx.status = 200
|
||||
ctx.message = "User created successfully."
|
||||
ctx.userId = response._id
|
||||
ctx.body = {
|
||||
_rev: response.rev,
|
||||
username,
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
const CouchDB = require("../../../db")
|
||||
const newid = require("../../../db/newid")
|
||||
const blockDefinitions = require("./blockDefinitions")
|
||||
const triggers = require("../../../workflows/triggers")
|
||||
const CouchDB = require("../../db")
|
||||
const newid = require("../../db/newid")
|
||||
const actions = require("../../workflows/actions")
|
||||
const logic = require("../../workflows/logic")
|
||||
const triggers = require("../../workflows/triggers")
|
||||
|
||||
/*************************
|
||||
* *
|
||||
|
@ -9,13 +10,34 @@ const triggers = require("../../../workflows/triggers")
|
|||
* *
|
||||
*************************/
|
||||
|
||||
function cleanWorkflowInputs(workflow) {
|
||||
if (workflow == null) {
|
||||
return workflow
|
||||
}
|
||||
let steps = workflow.definition.steps
|
||||
let trigger = workflow.definition.trigger
|
||||
let allSteps = [...steps, trigger]
|
||||
for (let step of allSteps) {
|
||||
if (step == null) {
|
||||
continue
|
||||
}
|
||||
for (let inputName of Object.keys(step.inputs)) {
|
||||
if (!step.inputs[inputName] || step.inputs[inputName] === "") {
|
||||
delete step.inputs[inputName]
|
||||
}
|
||||
}
|
||||
}
|
||||
return workflow
|
||||
}
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const workflow = ctx.request.body
|
||||
let workflow = ctx.request.body
|
||||
|
||||
workflow._id = newid()
|
||||
|
||||
workflow.type = "workflow"
|
||||
workflow = cleanWorkflowInputs(workflow)
|
||||
const response = await db.post(workflow)
|
||||
workflow._rev = response.rev
|
||||
|
||||
|
@ -31,8 +53,9 @@ exports.create = async function(ctx) {
|
|||
|
||||
exports.update = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
const workflow = ctx.request.body
|
||||
let workflow = ctx.request.body
|
||||
|
||||
workflow = cleanWorkflowInputs(workflow)
|
||||
const response = await db.put(workflow)
|
||||
workflow._rev = response.rev
|
||||
|
||||
|
@ -67,22 +90,22 @@ exports.destroy = async function(ctx) {
|
|||
}
|
||||
|
||||
exports.getActionList = async function(ctx) {
|
||||
ctx.body = blockDefinitions.ACTION
|
||||
ctx.body = actions.BUILTIN_DEFINITIONS
|
||||
}
|
||||
|
||||
exports.getTriggerList = async function(ctx) {
|
||||
ctx.body = blockDefinitions.TRIGGER
|
||||
ctx.body = triggers.BUILTIN_DEFINITIONS
|
||||
}
|
||||
|
||||
exports.getLogicList = async function(ctx) {
|
||||
ctx.body = blockDefinitions.LOGIC
|
||||
ctx.body = logic.BUILTIN_DEFINITIONS
|
||||
}
|
||||
|
||||
module.exports.getDefinitionList = async function(ctx) {
|
||||
ctx.body = {
|
||||
logic: blockDefinitions.LOGIC,
|
||||
trigger: blockDefinitions.TRIGGER,
|
||||
action: blockDefinitions.ACTION,
|
||||
logic: logic.BUILTIN_DEFINITIONS,
|
||||
trigger: triggers.BUILTIN_DEFINITIONS,
|
||||
action: actions.BUILTIN_DEFINITIONS,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
const ACTION = {
|
||||
SAVE_RECORD: {
|
||||
name: "Save Record",
|
||||
tagline: "<b>Save</b> a <b>{{record.model.name}}</b> record",
|
||||
icon: "ri-save-3-fill",
|
||||
description: "Save a record to your database.",
|
||||
params: {
|
||||
record: "record",
|
||||
},
|
||||
args: {
|
||||
record: {},
|
||||
},
|
||||
type: "ACTION",
|
||||
},
|
||||
DELETE_RECORD: {
|
||||
description: "Delete a record from your database.",
|
||||
icon: "ri-delete-bin-line",
|
||||
name: "Delete Record",
|
||||
tagline: "<b>Delete</b> a <b>{{record.model.name}}</b> record",
|
||||
params: {},
|
||||
args: {},
|
||||
type: "ACTION",
|
||||
},
|
||||
CREATE_USER: {
|
||||
description: "Create a new user.",
|
||||
tagline: "Create user <b>{{username}}</b>",
|
||||
icon: "ri-user-add-fill",
|
||||
name: "Create User",
|
||||
params: {
|
||||
username: "string",
|
||||
password: "password",
|
||||
accessLevelId: "accessLevel",
|
||||
},
|
||||
args: {
|
||||
accessLevelId: "POWER_USER",
|
||||
},
|
||||
type: "ACTION",
|
||||
},
|
||||
SEND_EMAIL: {
|
||||
description: "Send an email.",
|
||||
tagline: "Send email to <b>{{to}}</b>",
|
||||
icon: "ri-mail-open-fill",
|
||||
name: "Send Email",
|
||||
params: {
|
||||
to: "string",
|
||||
from: "string",
|
||||
subject: "longText",
|
||||
text: "longText",
|
||||
},
|
||||
type: "ACTION",
|
||||
},
|
||||
}
|
||||
|
||||
const LOGIC = {
|
||||
FILTER: {
|
||||
name: "Filter",
|
||||
tagline: "{{filter}} <b>{{condition}}</b> {{value}}",
|
||||
icon: "ri-git-branch-line",
|
||||
description: "Filter any workflows which do not meet certain conditions.",
|
||||
params: {
|
||||
filter: "string",
|
||||
condition: ["equals"],
|
||||
value: "string",
|
||||
},
|
||||
args: {
|
||||
condition: "equals",
|
||||
},
|
||||
type: "LOGIC",
|
||||
},
|
||||
DELAY: {
|
||||
name: "Delay",
|
||||
icon: "ri-time-fill",
|
||||
tagline: "Delay for <b>{{time}}</b> milliseconds",
|
||||
description: "Delay the workflow until an amount of time has passed.",
|
||||
params: {
|
||||
time: "number",
|
||||
},
|
||||
type: "LOGIC",
|
||||
},
|
||||
}
|
||||
|
||||
const TRIGGER = {
|
||||
RECORD_SAVED: {
|
||||
name: "Record Saved",
|
||||
event: "record:save",
|
||||
icon: "ri-save-line",
|
||||
tagline: "Record is added to <b>{{model.name}}</b>",
|
||||
description: "Fired when a record is saved to your database.",
|
||||
params: {
|
||||
model: "model",
|
||||
},
|
||||
type: "TRIGGER",
|
||||
},
|
||||
RECORD_DELETED: {
|
||||
name: "Record Deleted",
|
||||
event: "record:delete",
|
||||
icon: "ri-delete-bin-line",
|
||||
tagline: "Record is deleted from <b>{{model.name}}</b>",
|
||||
description: "Fired when a record is deleted from your database.",
|
||||
params: {
|
||||
model: "model",
|
||||
},
|
||||
type: "TRIGGER",
|
||||
},
|
||||
}
|
||||
|
||||
// This contains the definitions for the steps and triggers that make up a workflow, a workflow comprises
|
||||
// of many steps and a single trigger
|
||||
module.exports = {
|
||||
ACTION,
|
||||
LOGIC,
|
||||
TRIGGER,
|
||||
}
|
|
@ -67,6 +67,13 @@ exports.createModel = async (request, appId, instanceId, model) => {
|
|||
return res.body
|
||||
}
|
||||
|
||||
exports.getAllFromModel = async (request, appId, instanceId, modelId) => {
|
||||
const res = await request
|
||||
.get(`/api/${modelId}/records`)
|
||||
.set(exports.defaultHeaders(appId, instanceId))
|
||||
return res.body
|
||||
}
|
||||
|
||||
exports.createView = async (request, appId, instanceId, modelId, view) => {
|
||||
view = view || {
|
||||
map: "function(doc) { emit(doc[doc.key], doc._id); } ",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
module.exports.delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
@ -2,6 +2,8 @@ const {
|
|||
createClientDatabase,
|
||||
createApplication,
|
||||
createInstance,
|
||||
createModel,
|
||||
getAllFromModel,
|
||||
defaultHeaders,
|
||||
supertest,
|
||||
insertDocument,
|
||||
|
@ -9,6 +11,9 @@ const {
|
|||
builderEndpointShouldBlockNormalUsers
|
||||
} = require("./couchTestUtils")
|
||||
|
||||
const { delay } = require("./testUtils")
|
||||
|
||||
const MAX_RETRIES = 4
|
||||
const TEST_WORKFLOW = {
|
||||
_id: "Test Workflow",
|
||||
name: "My Workflow",
|
||||
|
@ -19,24 +24,24 @@ const TEST_WORKFLOW = {
|
|||
|
||||
},
|
||||
definition: {
|
||||
triggers: [
|
||||
|
||||
trigger: {},
|
||||
steps: [
|
||||
],
|
||||
next: {
|
||||
stepId: "abc123",
|
||||
type: "SERVER",
|
||||
conditions: {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
type: "workflow",
|
||||
}
|
||||
|
||||
let ACTION_DEFINITIONS = {}
|
||||
let TRIGGER_DEFINITIONS = {}
|
||||
let LOGIC_DEFINITIONS = {}
|
||||
|
||||
describe("/workflows", () => {
|
||||
let request
|
||||
let server
|
||||
let app
|
||||
let instance
|
||||
let workflow
|
||||
let workflowId
|
||||
|
||||
beforeAll(async () => {
|
||||
({ request, server } = await supertest())
|
||||
|
@ -45,8 +50,8 @@ describe("/workflows", () => {
|
|||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
if (workflow) await destroyDocument(workflow.id)
|
||||
instance = await createInstance(request, app._id)
|
||||
if (workflow) await destroyDocument(workflow.id);
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
|
@ -57,10 +62,72 @@ describe("/workflows", () => {
|
|||
workflow = await insertDocument(instance._id, {
|
||||
type: "workflow",
|
||||
...TEST_WORKFLOW
|
||||
});
|
||||
})
|
||||
workflow = { ...workflow, ...TEST_WORKFLOW }
|
||||
}
|
||||
|
||||
describe("get definitions", () => {
|
||||
it("returns a list of definitions for actions", async () => {
|
||||
const res = await request
|
||||
.get(`/api/workflows/action/list`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(Object.keys(res.body).length).not.toEqual(0)
|
||||
ACTION_DEFINITIONS = res.body
|
||||
})
|
||||
|
||||
it("returns a list of definitions for triggers", async () => {
|
||||
const res = await request
|
||||
.get(`/api/workflows/trigger/list`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(Object.keys(res.body).length).not.toEqual(0)
|
||||
TRIGGER_DEFINITIONS = res.body
|
||||
})
|
||||
|
||||
it("returns a list of definitions for actions", async () => {
|
||||
const res = await request
|
||||
.get(`/api/workflows/logic/list`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(Object.keys(res.body).length).not.toEqual(0)
|
||||
LOGIC_DEFINITIONS = res.body
|
||||
})
|
||||
|
||||
it("returns all of the definitions in one", async () => {
|
||||
const res = await request
|
||||
.get(`/api/workflows/definitions/list`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(Object.keys(res.body.action).length).toEqual(Object.keys(ACTION_DEFINITIONS).length)
|
||||
expect(Object.keys(res.body.trigger).length).toEqual(Object.keys(TRIGGER_DEFINITIONS).length)
|
||||
expect(Object.keys(res.body.logic).length).toEqual(Object.keys(LOGIC_DEFINITIONS).length)
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("should setup the workflow fully", () => {
|
||||
let trigger = TRIGGER_DEFINITIONS["RECORD_SAVED"]
|
||||
trigger.id = "wadiawdo34"
|
||||
let saveAction = ACTION_DEFINITIONS["SAVE_RECORD"]
|
||||
saveAction.inputs.record = {
|
||||
name: "{{trigger.name}}",
|
||||
description: "{{trigger.description}}"
|
||||
}
|
||||
saveAction.id = "awde444wk"
|
||||
|
||||
TEST_WORKFLOW.definition.steps.push(saveAction)
|
||||
TEST_WORKFLOW.definition.trigger = trigger
|
||||
})
|
||||
|
||||
it("returns a success message when the workflow is successfully created", async () => {
|
||||
const res = await request
|
||||
.post(`/api/workflows`)
|
||||
|
@ -69,8 +136,10 @@ describe("/workflows", () => {
|
|||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.message).toEqual("Workflow created successfully");
|
||||
expect(res.body.workflow.name).toEqual("My Workflow");
|
||||
expect(res.body.message).toEqual("Workflow created successfully")
|
||||
expect(res.body.workflow.name).toEqual("My Workflow")
|
||||
expect(res.body.workflow._id).not.toEqual(null)
|
||||
workflowId = res.body.workflow._id
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
|
@ -85,12 +154,43 @@ describe("/workflows", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("trigger", () => {
|
||||
it("trigger the workflow successfully", async () => {
|
||||
let model = await createModel(request, app._id, instance._id)
|
||||
TEST_WORKFLOW.definition.trigger.inputs.modelId = model._id
|
||||
TEST_WORKFLOW.definition.steps[0].inputs.record.modelId = model._id
|
||||
await createWorkflow()
|
||||
const res = await request
|
||||
.post(`/api/workflows/${workflow._id}/trigger`)
|
||||
.send({ name: "Test" })
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
expect(res.body.message).toEqual(`Workflow ${workflow._id} has been triggered.`)
|
||||
expect(res.body.workflow.name).toEqual(TEST_WORKFLOW.name)
|
||||
// wait for workflow to complete in background
|
||||
for (let tries = 0; tries < MAX_RETRIES; tries++) {
|
||||
let elements = await getAllFromModel(request, app._id, instance._id, model._id)
|
||||
// don't test it unless there are values to test
|
||||
if (elements.length === 1) {
|
||||
expect(elements.length).toEqual(1)
|
||||
expect(elements[0].name).toEqual("Test")
|
||||
expect(elements[0].description).toEqual("TEST")
|
||||
return
|
||||
}
|
||||
await delay(500)
|
||||
}
|
||||
throw "Failed to find the records"
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
it("updates a workflows data", async () => {
|
||||
await createWorkflow();
|
||||
await createWorkflow()
|
||||
workflow._id = workflow.id
|
||||
workflow._rev = workflow.rev
|
||||
workflow.name = "Updated Name";
|
||||
workflow.name = "Updated Name"
|
||||
workflow.type = "workflow"
|
||||
|
||||
const res = await request
|
||||
.put(`/api/workflows`)
|
||||
|
@ -99,21 +199,21 @@ describe("/workflows", () => {
|
|||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.message).toEqual("Workflow Test Workflow updated successfully.");
|
||||
expect(res.body.workflow.name).toEqual("Updated Name");
|
||||
expect(res.body.message).toEqual("Workflow Test Workflow updated successfully.")
|
||||
expect(res.body.workflow.name).toEqual("Updated Name")
|
||||
})
|
||||
})
|
||||
|
||||
describe("fetch", () => {
|
||||
it("return all the workflows for an instance", async () => {
|
||||
await createWorkflow();
|
||||
await createWorkflow()
|
||||
const res = await request
|
||||
.get(`/api/workflows`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body[0]).toEqual(expect.objectContaining(TEST_WORKFLOW));
|
||||
expect(res.body[0]).toEqual(expect.objectContaining(TEST_WORKFLOW))
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
|
@ -129,18 +229,18 @@ describe("/workflows", () => {
|
|||
|
||||
describe("destroy", () => {
|
||||
it("deletes a workflow by its ID", async () => {
|
||||
await createWorkflow();
|
||||
await createWorkflow()
|
||||
const res = await request
|
||||
.delete(`/api/workflows/${workflow.id}/${workflow.rev}`)
|
||||
.set(defaultHeaders(app._id, instance._id))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.id).toEqual(TEST_WORKFLOW._id);
|
||||
expect(res.body.id).toEqual(TEST_WORKFLOW._id)
|
||||
})
|
||||
|
||||
it("should apply authorization to endpoint", async () => {
|
||||
await createWorkflow();
|
||||
await createWorkflow()
|
||||
await builderEndpointShouldBlockNormalUsers({
|
||||
request,
|
||||
method: "DELETE",
|
||||
|
@ -150,4 +250,4 @@ describe("/workflows", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
})
|
||||
|
|
|
@ -23,18 +23,20 @@ function generateStepSchema(allowStepTypes) {
|
|||
}).unknown(true)
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const workflowValidator = joiValidator.body(Joi.object({
|
||||
live: Joi.bool(),
|
||||
id: Joi.string().required(),
|
||||
rev: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
type: Joi.string().valid("workflow").required(),
|
||||
definition: Joi.object({
|
||||
steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])),
|
||||
trigger: generateStepSchema(["TRIGGER"]).required(),
|
||||
}).required().unknown(true),
|
||||
}).unknown(true))
|
||||
function generateValidator(existing = false) {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
live: Joi.bool(),
|
||||
_id: existing ? Joi.string().required() : Joi.string(),
|
||||
_rev: existing ? Joi.string().required() : Joi.string(),
|
||||
name: Joi.string().required(),
|
||||
type: Joi.string().valid("workflow").required(),
|
||||
definition: Joi.object({
|
||||
steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])),
|
||||
trigger: generateStepSchema(["TRIGGER"]),
|
||||
}).required().unknown(true),
|
||||
}).unknown(true))
|
||||
}
|
||||
|
||||
router
|
||||
.get(
|
||||
|
@ -62,13 +64,13 @@ router
|
|||
.put(
|
||||
"/api/workflows",
|
||||
authorized(BUILDER),
|
||||
workflowValidator,
|
||||
generateValidator(true),
|
||||
controller.update
|
||||
)
|
||||
.post(
|
||||
"/api/workflows",
|
||||
authorized(BUILDER),
|
||||
workflowValidator,
|
||||
generateValidator(false),
|
||||
controller.create
|
||||
)
|
||||
.post("/api/workflows/:id/trigger", controller.trigger)
|
||||
|
|
|
@ -8,4 +8,5 @@ module.exports = {
|
|||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||
LOGGER: process.env.LOGGER,
|
||||
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT,
|
||||
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
|
||||
}
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
function validate(schema, property) {
|
||||
// Return a Koa middleware function
|
||||
return (ctx, next) => {
|
||||
if (schema) {
|
||||
const { error } = schema.validate(ctx[property])
|
||||
if (error) {
|
||||
ctx.throw(400, `Invalid ${property} - ${error.message}`)
|
||||
}
|
||||
if (!schema) {
|
||||
return next()
|
||||
}
|
||||
let params = null
|
||||
if (ctx[property] != null) {
|
||||
params = ctx[property]
|
||||
} else if (ctx.request[property] != null) {
|
||||
params = ctx.request[property]
|
||||
}
|
||||
const { error } = schema.validate(params)
|
||||
if (error) {
|
||||
ctx.throw(400, `Invalid ${property} - ${error.message}`)
|
||||
return
|
||||
}
|
||||
return next()
|
||||
}
|
||||
|
|
|
@ -1,100 +1,36 @@
|
|||
const viewController = require("../api/controllers/view")
|
||||
const modelController = require("../api/controllers/model")
|
||||
const workflowController = require("../api/controllers/workflow")
|
||||
|
||||
// Access Level IDs
|
||||
const ADMIN_LEVEL_ID = "ADMIN"
|
||||
const POWERUSER_LEVEL_ID = "POWER_USER"
|
||||
const BUILDER_LEVEL_ID = "BUILDER"
|
||||
const ANON_LEVEL_ID = "ANON"
|
||||
|
||||
// Permissions
|
||||
const READ_MODEL = "read-model"
|
||||
const WRITE_MODEL = "write-model"
|
||||
const READ_VIEW = "read-view"
|
||||
const EXECUTE_WORKFLOW = "execute-workflow"
|
||||
const USER_MANAGEMENT = "user-management"
|
||||
const BUILDER = "builder"
|
||||
const LIST_USERS = "list-users"
|
||||
|
||||
const adminPermissions = [
|
||||
module.exports.READ_MODEL = "read-model"
|
||||
module.exports.WRITE_MODEL = "write-model"
|
||||
module.exports.READ_VIEW = "read-view"
|
||||
module.exports.EXECUTE_WORKFLOW = "execute-workflow"
|
||||
module.exports.USER_MANAGEMENT = "user-management"
|
||||
module.exports.BUILDER = "builder"
|
||||
module.exports.LIST_USERS = "list-users"
|
||||
// Access Level IDs
|
||||
module.exports.ADMIN_LEVEL_ID = "ADMIN"
|
||||
module.exports.POWERUSER_LEVEL_ID = "POWER_USER"
|
||||
module.exports.BUILDER_LEVEL_ID = "BUILDER"
|
||||
module.exports.ANON_LEVEL_ID = "ANON"
|
||||
module.exports.ACCESS_LEVELS = [
|
||||
module.exports.ADMIN_LEVEL_ID,
|
||||
module.exports.POWERUSER_LEVEL_ID,
|
||||
module.exports.BUILDER_LEVEL_ID,
|
||||
module.exports.ANON_LEVEL_ID,
|
||||
]
|
||||
module.exports.PRETTY_ACCESS_LEVELS = {
|
||||
[module.exports.ADMIN_LEVEL_ID]: "Admin",
|
||||
[module.exports.POWERUSER_LEVEL_ID]: "Power user",
|
||||
[module.exports.BUILDER_LEVEL_ID]: "Builder",
|
||||
[module.exports.ANON_LEVEL_ID]: "Anonymous",
|
||||
}
|
||||
module.exports.adminPermissions = [
|
||||
{
|
||||
name: USER_MANAGEMENT,
|
||||
name: module.exports.USER_MANAGEMENT,
|
||||
},
|
||||
]
|
||||
|
||||
const generateAdminPermissions = async instanceId => [
|
||||
...adminPermissions,
|
||||
...(await generatePowerUserPermissions(instanceId)),
|
||||
]
|
||||
|
||||
const generatePowerUserPermissions = async instanceId => {
|
||||
const fetchModelsCtx = {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await modelController.fetch(fetchModelsCtx)
|
||||
const models = fetchModelsCtx.body
|
||||
|
||||
const fetchViewsCtx = {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await viewController.fetch(fetchViewsCtx)
|
||||
const views = fetchViewsCtx.body
|
||||
|
||||
const fetchWorkflowsCtx = {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await workflowController.fetch(fetchWorkflowsCtx)
|
||||
const workflows = fetchWorkflowsCtx.body
|
||||
|
||||
const readModelPermissions = models.map(m => ({
|
||||
itemId: m._id,
|
||||
name: READ_MODEL,
|
||||
}))
|
||||
|
||||
const writeModelPermissions = models.map(m => ({
|
||||
itemId: m._id,
|
||||
name: WRITE_MODEL,
|
||||
}))
|
||||
|
||||
const viewPermissions = views.map(v => ({
|
||||
itemId: v.name,
|
||||
name: READ_VIEW,
|
||||
}))
|
||||
|
||||
const executeWorkflowPermissions = workflows.map(w => ({
|
||||
itemId: w._id,
|
||||
name: EXECUTE_WORKFLOW,
|
||||
}))
|
||||
|
||||
return [
|
||||
...readModelPermissions,
|
||||
...writeModelPermissions,
|
||||
...viewPermissions,
|
||||
...executeWorkflowPermissions,
|
||||
{ name: LIST_USERS },
|
||||
]
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ADMIN_LEVEL_ID,
|
||||
POWERUSER_LEVEL_ID,
|
||||
BUILDER_LEVEL_ID,
|
||||
ANON_LEVEL_ID,
|
||||
READ_MODEL,
|
||||
WRITE_MODEL,
|
||||
READ_VIEW,
|
||||
EXECUTE_WORKFLOW,
|
||||
USER_MANAGEMENT,
|
||||
BUILDER,
|
||||
LIST_USERS,
|
||||
adminPermissions,
|
||||
generateAdminPermissions,
|
||||
generatePowerUserPermissions,
|
||||
}
|
||||
// to avoid circular dependencies this is included later, after exporting all enums
|
||||
const permissions = require("./permissions")
|
||||
module.exports.generateAdminPermissions = permissions.generateAdminPermissions
|
||||
module.exports.generatePowerUserPermissions =
|
||||
permissions.generatePowerUserPermissions
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
const viewController = require("../api/controllers/view")
|
||||
const modelController = require("../api/controllers/model")
|
||||
const workflowController = require("../api/controllers/workflow")
|
||||
const accessLevels = require("./accessLevels")
|
||||
|
||||
// this has been broken out to reduce risk of circular dependency from utilities, no enums defined here
|
||||
const generateAdminPermissions = async instanceId => [
|
||||
...accessLevels.adminPermissions,
|
||||
...(await generatePowerUserPermissions(instanceId)),
|
||||
]
|
||||
|
||||
const generatePowerUserPermissions = async instanceId => {
|
||||
const fetchModelsCtx = {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await modelController.fetch(fetchModelsCtx)
|
||||
const models = fetchModelsCtx.body
|
||||
|
||||
const fetchViewsCtx = {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await viewController.fetch(fetchViewsCtx)
|
||||
const views = fetchViewsCtx.body
|
||||
|
||||
const fetchWorkflowsCtx = {
|
||||
user: {
|
||||
instanceId,
|
||||
},
|
||||
}
|
||||
await workflowController.fetch(fetchWorkflowsCtx)
|
||||
const workflows = fetchWorkflowsCtx.body
|
||||
|
||||
const readModelPermissions = models.map(m => ({
|
||||
itemId: m._id,
|
||||
name: accessLevels.READ_MODEL,
|
||||
}))
|
||||
|
||||
const writeModelPermissions = models.map(m => ({
|
||||
itemId: m._id,
|
||||
name: accessLevels.WRITE_MODEL,
|
||||
}))
|
||||
|
||||
const viewPermissions = views.map(v => ({
|
||||
itemId: v.name,
|
||||
name: accessLevels.READ_VIEW,
|
||||
}))
|
||||
|
||||
const executeWorkflowPermissions = workflows.map(w => ({
|
||||
itemId: w._id,
|
||||
name: accessLevels.EXECUTE_WORKFLOW,
|
||||
}))
|
||||
|
||||
return [
|
||||
...readModelPermissions,
|
||||
...writeModelPermissions,
|
||||
...viewPermissions,
|
||||
...executeWorkflowPermissions,
|
||||
{ name: accessLevels.LIST_USERS },
|
||||
]
|
||||
}
|
||||
module.exports.generateAdminPermissions = generateAdminPermissions
|
||||
module.exports.generatePowerUserPermissions = generatePowerUserPermissions
|
|
@ -1,107 +1,20 @@
|
|||
const userController = require("../api/controllers/user")
|
||||
const recordController = require("../api/controllers/record")
|
||||
const sgMail = require("@sendgrid/mail")
|
||||
const sendEmail = require("./steps/sendEmail")
|
||||
const saveRecord = require("./steps/saveRecord")
|
||||
const deleteRecord = require("./steps/deleteRecord")
|
||||
const createUser = require("./steps/createUser")
|
||||
|
||||
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
|
||||
const BUILTIN_ACTIONS = {
|
||||
SEND_EMAIL: sendEmail.run,
|
||||
SAVE_RECORD: saveRecord.run,
|
||||
DELETE_RECORD: deleteRecord.run,
|
||||
CREATE_USER: createUser.run,
|
||||
}
|
||||
|
||||
let BUILTIN_ACTIONS = {
|
||||
CREATE_USER: async function({ args, context }) {
|
||||
const { username, password, accessLevelId } = args
|
||||
const ctx = {
|
||||
user: {
|
||||
instanceId: context.instanceId,
|
||||
},
|
||||
request: {
|
||||
body: { username, password, accessLevelId },
|
||||
},
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
},
|
||||
DELETE_RECORD: async function({ args, context }) {
|
||||
const { model, ...record } = args.record
|
||||
// TODO: better logging of when actions are missed due to missing parameters
|
||||
if (record.recordId == null || record.revId == null) {
|
||||
return
|
||||
}
|
||||
let ctx = {
|
||||
params: {
|
||||
modelId: model._id,
|
||||
recordId: record.recordId,
|
||||
revId: record.revId,
|
||||
},
|
||||
user: { instanceId: context.instanceId },
|
||||
}
|
||||
|
||||
try {
|
||||
await recordController.destroy(ctx)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
record: null,
|
||||
error: err.message,
|
||||
}
|
||||
}
|
||||
},
|
||||
const BUILTIN_DEFINITIONS = {
|
||||
SEND_EMAIL: sendEmail.definition,
|
||||
SAVE_RECORD: saveRecord.definition,
|
||||
DELETE_RECORD: deleteRecord.definition,
|
||||
CREATE_USER: createUser.definition,
|
||||
}
|
||||
|
||||
module.exports.getAction = async function(actionName) {
|
||||
|
@ -110,3 +23,5 @@ module.exports.getAction = async function(actionName) {
|
|||
}
|
||||
// TODO: load async actions here
|
||||
}
|
||||
|
||||
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS
|
||||
|
|
|
@ -25,6 +25,7 @@ module.exports.init = function() {
|
|||
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
|
||||
await runWorker(job)
|
||||
} else {
|
||||
console.log("Testing standard thread")
|
||||
await singleThread(job)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,24 +1,20 @@
|
|||
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
let filter = require("./steps/filter")
|
||||
let delay = require("./steps/delay")
|
||||
|
||||
let LOGIC = {
|
||||
DELAY: async function delay({ args }) {
|
||||
await wait(args.time)
|
||||
},
|
||||
let BUILTIN_LOGIC = {
|
||||
DELAY: delay.run,
|
||||
FILTER: filter.run,
|
||||
}
|
||||
|
||||
FILTER: async function filter({ args }) {
|
||||
const { field, condition, value } = args
|
||||
switch (condition) {
|
||||
case "equals":
|
||||
if (field !== value) return
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
},
|
||||
let BUILTIN_DEFINITIONS = {
|
||||
DELAY: delay.definition,
|
||||
FILTER: filter.definition,
|
||||
}
|
||||
|
||||
module.exports.getLogic = function(logicName) {
|
||||
if (LOGIC[logicName] != null) {
|
||||
return LOGIC[logicName]
|
||||
if (BUILTIN_LOGIC[logicName] != null) {
|
||||
return BUILTIN_LOGIC[logicName]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
const accessLevels = require("../../utilities/accessLevels")
|
||||
const userController = require("../../api/controllers/user")
|
||||
|
||||
module.exports.definition = {
|
||||
description: "Create a new user",
|
||||
tagline: "Create user {{inputs.username}}",
|
||||
icon: "ri-user-add-fill",
|
||||
name: "Create User",
|
||||
type: "ACTION",
|
||||
stepId: "CREATE_USER",
|
||||
inputs: {
|
||||
accessLevelId: accessLevels.POWERUSER_LEVEL_ID,
|
||||
},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
username: {
|
||||
type: "string",
|
||||
title: "Username",
|
||||
},
|
||||
password: {
|
||||
type: "string",
|
||||
customType: "password",
|
||||
title: "Password",
|
||||
},
|
||||
accessLevelId: {
|
||||
type: "string",
|
||||
title: "Access Level",
|
||||
enum: accessLevels.ACCESS_LEVELS,
|
||||
pretty: Object.values(accessLevels.PRETTY_ACCESS_LEVELS),
|
||||
},
|
||||
},
|
||||
required: ["username", "password", "accessLevelId"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
description: "The identifier of the new user",
|
||||
},
|
||||
revision: {
|
||||
type: "string",
|
||||
description: "The revision of the new user",
|
||||
},
|
||||
response: {
|
||||
type: "object",
|
||||
description: "The response from the user table",
|
||||
},
|
||||
success: {
|
||||
type: "boolean",
|
||||
description: "Whether the action was successful",
|
||||
},
|
||||
},
|
||||
required: ["id", "revision", "success"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.run = async function({ inputs, instanceId }) {
|
||||
const { username, password, accessLevelId } = inputs
|
||||
const ctx = {
|
||||
user: {
|
||||
instanceId: instanceId,
|
||||
},
|
||||
request: {
|
||||
body: { username, password, accessLevelId },
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
await userController.create(ctx)
|
||||
return {
|
||||
response: ctx.body,
|
||||
// internal property not returned through the API
|
||||
id: ctx.userId,
|
||||
revision: ctx.body._rev,
|
||||
success: ctx.status === 200,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
success: false,
|
||||
response: err,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
module.exports.definition = {
|
||||
name: "Delay",
|
||||
icon: "ri-time-fill",
|
||||
tagline: "Delay for {{inputs.time}} milliseconds",
|
||||
description: "Delay the workflow until an amount of time has passed",
|
||||
stepId: "DELAY",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
time: {
|
||||
type: "number",
|
||||
title: "Delay in milliseconds",
|
||||
},
|
||||
},
|
||||
required: ["time"],
|
||||
},
|
||||
},
|
||||
type: "LOGIC",
|
||||
}
|
||||
|
||||
module.exports.run = async function delay({ inputs }) {
|
||||
await wait(inputs.time)
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
const recordController = require("../../api/controllers/record")
|
||||
|
||||
module.exports.definition = {
|
||||
description: "Delete a record from your database",
|
||||
icon: "ri-delete-bin-line",
|
||||
name: "Delete Record",
|
||||
tagline: "Delete a {{inputs.enriched.model.name}} record",
|
||||
type: "ACTION",
|
||||
stepId: "DELETE_RECORD",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
modelId: {
|
||||
type: "string",
|
||||
customType: "model",
|
||||
title: "Table",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
title: "Record ID",
|
||||
},
|
||||
revision: {
|
||||
type: "string",
|
||||
title: "Record Revision",
|
||||
},
|
||||
},
|
||||
required: ["modelId", "id", "revision"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
customType: "record",
|
||||
description: "The deleted record",
|
||||
},
|
||||
response: {
|
||||
type: "object",
|
||||
description: "The response from the table",
|
||||
},
|
||||
success: {
|
||||
type: "boolean",
|
||||
description: "Whether the action was successful",
|
||||
},
|
||||
},
|
||||
required: ["record", "success"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.run = async function({ inputs, instanceId }) {
|
||||
// TODO: better logging of when actions are missed due to missing parameters
|
||||
if (inputs.id == null || inputs.revision == null) {
|
||||
return
|
||||
}
|
||||
let ctx = {
|
||||
params: {
|
||||
modelId: inputs.modelId,
|
||||
recordId: inputs.id,
|
||||
revId: inputs.revision,
|
||||
},
|
||||
user: { instanceId },
|
||||
}
|
||||
|
||||
try {
|
||||
await recordController.destroy(ctx)
|
||||
return {
|
||||
response: ctx.body,
|
||||
record: ctx.record,
|
||||
success: ctx.status === 200,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
success: false,
|
||||
response: err,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
const LogicConditions = {
|
||||
EQUAL: "EQUAL",
|
||||
NOT_EQUAL: "NOT_EQUAL",
|
||||
GREATER_THAN: "GREATER_THAN",
|
||||
LESS_THAN: "LESS_THAN",
|
||||
}
|
||||
|
||||
const PrettyLogicConditions = {
|
||||
[LogicConditions.EQUAL]: "Equals",
|
||||
[LogicConditions.NOT_EQUAL]: "Not equals",
|
||||
[LogicConditions.GREATER_THAN]: "Greater than",
|
||||
[LogicConditions.LESS_THAN]: "Less than",
|
||||
}
|
||||
|
||||
module.exports.definition = {
|
||||
name: "Filter",
|
||||
tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}",
|
||||
icon: "ri-git-branch-line",
|
||||
description: "Filter any workflows which do not meet certain conditions",
|
||||
type: "LOGIC",
|
||||
stepId: "FILTER",
|
||||
inputs: {
|
||||
condition: LogicConditions.EQUALS,
|
||||
},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
field: {
|
||||
type: "string",
|
||||
title: "Reference Value",
|
||||
},
|
||||
condition: {
|
||||
type: "string",
|
||||
title: "Condition",
|
||||
enum: Object.values(LogicConditions),
|
||||
pretty: Object.values(PrettyLogicConditions),
|
||||
},
|
||||
value: {
|
||||
type: "string",
|
||||
title: "Comparison Value",
|
||||
},
|
||||
},
|
||||
required: ["field", "condition", "value"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
success: {
|
||||
type: "boolean",
|
||||
description: "Whether the logic block passed",
|
||||
},
|
||||
},
|
||||
required: ["success"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.run = async function filter({ inputs }) {
|
||||
const { field, condition, value } = inputs
|
||||
let success
|
||||
if (typeof field !== "object" && typeof value !== "object") {
|
||||
switch (condition) {
|
||||
case LogicConditions.EQUAL:
|
||||
success = field === value
|
||||
break
|
||||
case LogicConditions.NOT_EQUAL:
|
||||
success = field !== value
|
||||
break
|
||||
case LogicConditions.GREATER_THAN:
|
||||
success = field > value
|
||||
break
|
||||
case LogicConditions.LESS_THAN:
|
||||
success = field < value
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
} else {
|
||||
success = false
|
||||
}
|
||||
return { success }
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
const recordController = require("../../api/controllers/record")
|
||||
|
||||
module.exports.definition = {
|
||||
name: "Save Record",
|
||||
tagline: "Save a {{inputs.enriched.model.name}} record",
|
||||
icon: "ri-save-3-fill",
|
||||
description: "Save a record to your database",
|
||||
type: "ACTION",
|
||||
stepId: "SAVE_RECORD",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
properties: {
|
||||
modelId: {
|
||||
type: "string",
|
||||
customType: "model",
|
||||
},
|
||||
},
|
||||
customType: "record",
|
||||
title: "Table",
|
||||
required: ["modelId"],
|
||||
},
|
||||
},
|
||||
required: ["record"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
customType: "record",
|
||||
description: "The new record",
|
||||
},
|
||||
response: {
|
||||
type: "object",
|
||||
description: "The response from the table",
|
||||
},
|
||||
success: {
|
||||
type: "boolean",
|
||||
description: "Whether the action was successful",
|
||||
},
|
||||
id: {
|
||||
type: "string",
|
||||
description: "The identifier of the new record",
|
||||
},
|
||||
revision: {
|
||||
type: "string",
|
||||
description: "The revision of the new record",
|
||||
},
|
||||
},
|
||||
required: ["success", "id", "revision"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.run = async function({ inputs, instanceId }) {
|
||||
// TODO: better logging of when actions are missed due to missing parameters
|
||||
if (inputs.record == null || inputs.record.modelId == null) {
|
||||
return
|
||||
}
|
||||
// have to clean up the record, remove the model from it
|
||||
const ctx = {
|
||||
params: {
|
||||
modelId: inputs.record.modelId,
|
||||
},
|
||||
request: {
|
||||
body: inputs.record,
|
||||
},
|
||||
user: { instanceId },
|
||||
}
|
||||
|
||||
try {
|
||||
await recordController.save(ctx)
|
||||
return {
|
||||
record: inputs.record,
|
||||
response: ctx.body,
|
||||
id: ctx.body._id,
|
||||
revision: ctx.body._rev,
|
||||
success: ctx.status === 200,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
success: false,
|
||||
response: err,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
const environment = require("../../environment")
|
||||
const sgMail = require("@sendgrid/mail")
|
||||
sgMail.setApiKey(environment.SENDGRID_API_KEY)
|
||||
|
||||
module.exports.definition = {
|
||||
description: "Send an email",
|
||||
tagline: "Send email to {{inputs.to}}",
|
||||
icon: "ri-mail-open-fill",
|
||||
name: "Send Email",
|
||||
type: "ACTION",
|
||||
stepId: "SEND_EMAIL",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
to: {
|
||||
type: "string",
|
||||
title: "Send To",
|
||||
},
|
||||
from: {
|
||||
type: "string",
|
||||
title: "Send From",
|
||||
},
|
||||
subject: {
|
||||
type: "string",
|
||||
title: "Email Subject",
|
||||
},
|
||||
contents: {
|
||||
type: "string",
|
||||
title: "Email Contents",
|
||||
},
|
||||
},
|
||||
required: ["to", "from", "subject", "contents"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
success: {
|
||||
type: "boolean",
|
||||
description: "Whether the email was sent",
|
||||
},
|
||||
response: {
|
||||
type: "object",
|
||||
description: "A response from the email client, this may be an error",
|
||||
},
|
||||
},
|
||||
required: ["success"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.run = async function({ inputs }) {
|
||||
const msg = {
|
||||
to: inputs.to,
|
||||
from: inputs.from,
|
||||
subject: inputs.subject,
|
||||
text: inputs.contents ? inputs.contents : "Empty",
|
||||
}
|
||||
|
||||
try {
|
||||
let response = await sgMail.send(msg)
|
||||
return {
|
||||
success: true,
|
||||
response,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
success: false,
|
||||
response: err,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,18 +2,64 @@ const mustache = require("mustache")
|
|||
const actions = require("./actions")
|
||||
const logic = require("./logic")
|
||||
|
||||
const FILTER_STEP_ID = logic.BUILTIN_DEFINITIONS.FILTER.stepId
|
||||
|
||||
function cleanMustache(string) {
|
||||
let charToReplace = {
|
||||
"[": ".",
|
||||
"]": "",
|
||||
}
|
||||
let regex = new RegExp(/{{[^}}]*}}/g)
|
||||
let matches = string.match(regex)
|
||||
if (matches == null) {
|
||||
return string
|
||||
}
|
||||
for (let match of matches) {
|
||||
let baseIdx = string.indexOf(match)
|
||||
for (let key of Object.keys(charToReplace)) {
|
||||
let idxChar = match.indexOf(key)
|
||||
if (idxChar !== -1) {
|
||||
string =
|
||||
string.slice(baseIdx, baseIdx + idxChar) +
|
||||
charToReplace[key] +
|
||||
string.slice(baseIdx + idxChar + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
function recurseMustache(inputs, context) {
|
||||
for (let key of Object.keys(inputs)) {
|
||||
let val = inputs[key]
|
||||
if (typeof val === "string") {
|
||||
val = cleanMustache(inputs[key])
|
||||
inputs[key] = mustache.render(val, context)
|
||||
}
|
||||
// this covers objects and arrays
|
||||
else if (typeof val === "object") {
|
||||
inputs[key] = recurseMustache(inputs[key], context)
|
||||
}
|
||||
}
|
||||
return inputs
|
||||
}
|
||||
|
||||
/**
|
||||
* The workflow orchestrator is a class responsible for executing workflows.
|
||||
* It handles the context of the workflow and makes sure each step gets the correct
|
||||
* inputs and handles any outputs.
|
||||
*/
|
||||
class Orchestrator {
|
||||
constructor(workflow) {
|
||||
this._context = {}
|
||||
constructor(workflow, triggerOutput) {
|
||||
this._instanceId = triggerOutput.instanceId
|
||||
// remove from context
|
||||
delete triggerOutput.instanceId
|
||||
// step zero is never used as the mustache is zero indexed for customer facing
|
||||
this._context = { steps: [{}], trigger: triggerOutput }
|
||||
this._workflow = workflow
|
||||
}
|
||||
|
||||
async getStep(type, stepId) {
|
||||
async getStepFunctionality(type, stepId) {
|
||||
let step = null
|
||||
if (type === "ACTION") {
|
||||
step = await actions.getAction(stepId)
|
||||
|
@ -26,28 +72,20 @@ class Orchestrator {
|
|||
return step
|
||||
}
|
||||
|
||||
async execute(context) {
|
||||
async execute() {
|
||||
let workflow = this._workflow
|
||||
for (let block of workflow.definition.steps) {
|
||||
let step = await 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,
|
||||
for (let step of workflow.definition.steps) {
|
||||
let stepFn = await this.getStepFunctionality(step.type, step.stepId)
|
||||
step.inputs = recurseMustache(step.inputs, this._context)
|
||||
// instanceId is always passed
|
||||
const outputs = await stepFn({
|
||||
inputs: step.inputs,
|
||||
instanceId: this._instanceId,
|
||||
})
|
||||
|
||||
this._context = {
|
||||
...this._context,
|
||||
[block.id]: response,
|
||||
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
|
||||
break
|
||||
}
|
||||
this._context.steps.push(outputs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,8 +93,11 @@ class Orchestrator {
|
|||
// callback is required for worker-farm to state that the worker thread has completed
|
||||
module.exports = async (job, cb = null) => {
|
||||
try {
|
||||
const workflowOrchestrator = new Orchestrator(job.data.workflow)
|
||||
await workflowOrchestrator.execute(job.data.event)
|
||||
const workflowOrchestrator = new Orchestrator(
|
||||
job.data.workflow,
|
||||
job.data.event
|
||||
)
|
||||
await workflowOrchestrator.execute()
|
||||
if (cb) {
|
||||
cb()
|
||||
}
|
||||
|
|
|
@ -4,7 +4,79 @@ const InMemoryQueue = require("./queue/inMemoryQueue")
|
|||
|
||||
let workflowQueue = new InMemoryQueue()
|
||||
|
||||
async function queueRelevantWorkflows(event, eventType) {
|
||||
const FAKE_STRING = "TEST"
|
||||
const FAKE_BOOL = false
|
||||
const FAKE_NUMBER = 1
|
||||
const FAKE_DATETIME = "1970-01-01T00:00:00.000Z"
|
||||
|
||||
const BUILTIN_DEFINITIONS = {
|
||||
RECORD_SAVED: {
|
||||
name: "Record Saved",
|
||||
event: "record:save",
|
||||
icon: "ri-save-line",
|
||||
tagline: "Record is added to {{inputs.enriched.model.name}}",
|
||||
description: "Fired when a record is saved to your database",
|
||||
stepId: "RECORD_SAVED",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
modelId: {
|
||||
type: "string",
|
||||
customType: "model",
|
||||
title: "Table",
|
||||
},
|
||||
},
|
||||
required: ["modelId"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
customType: "record",
|
||||
description: "The new record that was saved",
|
||||
},
|
||||
},
|
||||
required: ["record"],
|
||||
},
|
||||
},
|
||||
type: "TRIGGER",
|
||||
},
|
||||
RECORD_DELETED: {
|
||||
name: "Record Deleted",
|
||||
event: "record:delete",
|
||||
icon: "ri-delete-bin-line",
|
||||
tagline: "Record is deleted from {{inputs.enriched.model.name}}",
|
||||
description: "Fired when a record is deleted from your database",
|
||||
stepId: "RECORD_DELETED",
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
modelId: {
|
||||
type: "string",
|
||||
customType: "model",
|
||||
title: "Table",
|
||||
},
|
||||
},
|
||||
required: ["modelId"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
record: {
|
||||
type: "object",
|
||||
customType: "record",
|
||||
description: "The record that was deleted",
|
||||
},
|
||||
},
|
||||
required: ["record"],
|
||||
},
|
||||
},
|
||||
type: "TRIGGER",
|
||||
},
|
||||
}
|
||||
|
||||
async function queueRelevantRecordWorkflows(event, eventType) {
|
||||
if (event.instanceId == null) {
|
||||
throw `No instanceId specified for ${eventType} - check event emitters.`
|
||||
}
|
||||
|
@ -16,7 +88,13 @@ async function queueRelevantWorkflows(event, eventType) {
|
|||
|
||||
const workflows = workflowsToTrigger.rows.map(wf => wf.doc)
|
||||
for (let workflow of workflows) {
|
||||
if (!workflow.live) {
|
||||
let workflowDef = workflow.definition
|
||||
let workflowTrigger = workflowDef ? workflowDef.trigger : {}
|
||||
if (
|
||||
!workflow.live ||
|
||||
!workflowTrigger.inputs ||
|
||||
workflowTrigger.inputs.modelId !== event.record.modelId
|
||||
) {
|
||||
continue
|
||||
}
|
||||
workflowQueue.add({ workflow, event })
|
||||
|
@ -24,15 +102,64 @@ async function queueRelevantWorkflows(event, eventType) {
|
|||
}
|
||||
|
||||
emitter.on("record:save", async function(event) {
|
||||
await queueRelevantWorkflows(event, "record:save")
|
||||
if (!event || !event.record || !event.record.modelId) {
|
||||
return
|
||||
}
|
||||
await queueRelevantRecordWorkflows(event, "record:save")
|
||||
})
|
||||
|
||||
emitter.on("record:delete", async function(event) {
|
||||
await queueRelevantWorkflows(event, "record:delete")
|
||||
if (!event || !event.record || !event.record.modelId) {
|
||||
return
|
||||
}
|
||||
await queueRelevantRecordWorkflows(event, "record:delete")
|
||||
})
|
||||
|
||||
async function fillRecordOutput(workflow, params) {
|
||||
let triggerSchema = workflow.definition.trigger
|
||||
let modelId = triggerSchema.inputs.modelId
|
||||
const db = new CouchDB(params.instanceId)
|
||||
try {
|
||||
let model = await db.get(modelId)
|
||||
for (let schemaKey of Object.keys(model.schema)) {
|
||||
if (params[schemaKey] != null) {
|
||||
continue
|
||||
}
|
||||
let propSchema = model.schema[schemaKey]
|
||||
switch (propSchema.constraints.type) {
|
||||
case "string":
|
||||
params[schemaKey] = FAKE_STRING
|
||||
break
|
||||
case "boolean":
|
||||
params[schemaKey] = FAKE_BOOL
|
||||
break
|
||||
case "number":
|
||||
params[schemaKey] = FAKE_NUMBER
|
||||
break
|
||||
case "datetime":
|
||||
params[schemaKey] = FAKE_DATETIME
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
throw "Failed to find model for trigger"
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
module.exports.externalTrigger = async function(workflow, params) {
|
||||
// TODO: replace this with allowing user in builder to input values in future
|
||||
if (
|
||||
workflow.definition != null &&
|
||||
workflow.definition.trigger != null &&
|
||||
workflow.definition.trigger.inputs.modelId != null
|
||||
) {
|
||||
params = await fillRecordOutput(workflow, params)
|
||||
}
|
||||
|
||||
workflowQueue.add({ workflow, event: params })
|
||||
}
|
||||
|
||||
module.exports.workflowQueue = workflowQueue
|
||||
|
||||
module.exports.BUILTIN_DEFINITIONS = BUILTIN_DEFINITIONS
|
||||
|
|
|
@ -36,8 +36,8 @@
|
|||
"gitHead": "284cceb9b703c38566c6e6363c022f79a08d5691",
|
||||
"dependencies": {
|
||||
"@beyonk/svelte-googlemaps": "^2.2.0",
|
||||
"@budibase/bbui": "^1.32.0",
|
||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||
"@budibase/bbui": "^1.34.2",
|
||||
"britecharts": "^2.16.1",
|
||||
"d3-selection": "^1.4.2",
|
||||
"fast-sort": "^2.2.0",
|
||||
|
|
Loading…
Reference in New Issue