Merge pull request #607 from Budibase/server-workflows
Server workflows
This commit is contained in:
commit
1930aa2296
|
@ -1,46 +1,52 @@
|
|||
context('Create a workflow', () => {
|
||||
context("Create a workflow", () => {
|
||||
before(() => {
|
||||
cy.server()
|
||||
cy.visit("localhost:4001/_builder")
|
||||
|
||||
before(() => {
|
||||
cy.server()
|
||||
cy.visit('localhost:4001/_builder')
|
||||
cy.createApp(
|
||||
"Workflow Test App",
|
||||
"This app is used to test that workflows do in fact work!"
|
||||
)
|
||||
})
|
||||
|
||||
cy.createApp('Workflow Test App', 'This app is used to test that workflows do in fact work!')
|
||||
})
|
||||
// https://on.cypress.io/interacting-with-elements
|
||||
it("should create a workflow", () => {
|
||||
cy.createTestTableWithData()
|
||||
|
||||
// https://on.cypress.io/interacting-with-elements
|
||||
it('should create a workflow', () => {
|
||||
cy.createTestTableWithData()
|
||||
cy.contains("workflow").click()
|
||||
cy.contains("Create New Workflow").click()
|
||||
cy.get("input").type("Add Record")
|
||||
cy.contains("Save").click()
|
||||
|
||||
cy.contains('workflow').click()
|
||||
cy.contains('Create New Workflow').click()
|
||||
cy.get('input').type('Add Record')
|
||||
cy.contains('Save').click()
|
||||
// Add trigger
|
||||
cy.get("[data-cy=add-workflow-component]").click()
|
||||
cy.get("[data-cy=RECORD_SAVED]").click()
|
||||
cy.get(".budibase__input").select("dog")
|
||||
|
||||
// Add trigger
|
||||
cy.get('[data-cy=add-workflow-component]').click()
|
||||
cy.get('[data-cy=RECORD_SAVED]').click()
|
||||
cy.get('.budibase__input').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")
|
||||
|
||||
// Create action
|
||||
cy.get('[data-cy=SAVE_RECORD]').click()
|
||||
cy.get('.container input').first().type('goodboy')
|
||||
cy.get('.container input').eq(1).type('11')
|
||||
// Save
|
||||
cy.contains("Save Workflow").click()
|
||||
|
||||
// Save
|
||||
cy.contains('Save Workflow').click()
|
||||
// Activate Workflow
|
||||
cy.get("[data-cy=activate-workflow]").click()
|
||||
cy.contains("Add Record").should("be.visible")
|
||||
cy.get(".stop-button.highlighted").should("be.visible")
|
||||
})
|
||||
|
||||
// Activate Workflow
|
||||
cy.get('[data-cy=activate-workflow]').click()
|
||||
cy.contains("Add Record").should("be.visible")
|
||||
cy.get(".stop-button.highlighted").should("be.visible")
|
||||
})
|
||||
it("should add record when a new record is added", () => {
|
||||
cy.contains("backend").click()
|
||||
|
||||
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')
|
||||
|
||||
})
|
||||
})
|
||||
cy.addRecord(["Rover", 15])
|
||||
cy.reload()
|
||||
cy.contains("goodboy").should("have.text", "goodboy")
|
||||
})
|
||||
})
|
||||
|
|
Binary file not shown.
|
@ -234,7 +234,7 @@ export default {
|
|||
|
||||
// Watch the `dist` directory and refresh the
|
||||
// browser on changes when not in production
|
||||
!production && livereload(outputpath),
|
||||
!production && livereload({ watch: outputpath, delay: 500 }),
|
||||
|
||||
// If we're building for production (npm run build
|
||||
// instead of npm run dev), minify
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import mustache from "mustache"
|
||||
import blockDefinitions from "components/workflow/WorkflowPanel/blockDefinitions"
|
||||
import { generate } from "shortid"
|
||||
|
||||
/**
|
||||
|
@ -18,27 +16,31 @@ export default class Workflow {
|
|||
addBlock(block) {
|
||||
// Make sure to add trigger if doesn't exist
|
||||
if (!this.hasTrigger() && block.type === "TRIGGER") {
|
||||
this.workflow.definition.trigger = { id: generate(), ...block }
|
||||
return
|
||||
const trigger = { id: generate(), ...block }
|
||||
this.workflow.definition.trigger = trigger
|
||||
return trigger
|
||||
}
|
||||
|
||||
this.workflow.definition.steps.push({
|
||||
id: generate(),
|
||||
...block,
|
||||
})
|
||||
const newBlock = { id: generate(), ...block }
|
||||
this.workflow.definition.steps = [
|
||||
...this.workflow.definition.steps,
|
||||
newBlock,
|
||||
]
|
||||
return newBlock
|
||||
}
|
||||
|
||||
updateBlock(updatedBlock, id) {
|
||||
const { steps, trigger } = this.workflow.definition
|
||||
|
||||
if (trigger && trigger.id === id) {
|
||||
this.workflow.definition.trigger = null
|
||||
this.workflow.definition.trigger = updatedBlock
|
||||
return
|
||||
}
|
||||
|
||||
const stepIdx = steps.findIndex(step => step.id === id)
|
||||
if (stepIdx < 0) throw new Error("Block not found.")
|
||||
steps.splice(stepIdx, 1, updatedBlock)
|
||||
this.workflow.definition.steps = steps
|
||||
}
|
||||
|
||||
deleteBlock(id) {
|
||||
|
@ -52,44 +54,6 @@ export default class Workflow {
|
|||
const stepIdx = steps.findIndex(step => step.id === id)
|
||||
if (stepIdx < 0) throw new Error("Block not found.")
|
||||
steps.splice(stepIdx, 1)
|
||||
}
|
||||
|
||||
createUiTree() {
|
||||
if (!this.workflow.definition) return []
|
||||
return Workflow.buildUiTree(this.workflow.definition)
|
||||
}
|
||||
|
||||
static buildUiTree(definition) {
|
||||
const steps = []
|
||||
if (definition.trigger) steps.push(definition.trigger)
|
||||
|
||||
return [...steps, ...definition.steps].map(step => {
|
||||
// The client side display definition for the block
|
||||
const definition = blockDefinitions[step.type][step.actionId]
|
||||
if (!definition) {
|
||||
throw new Error(
|
||||
`No block definition exists for the chosen block. Check there's an entry in the block definitions for ${step.actionId}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!definition.params) {
|
||||
throw new Error(
|
||||
`Blocks should always have parameters. Ensure that the block definition is correct for ${step.actionId}`
|
||||
)
|
||||
}
|
||||
|
||||
const tagline = definition.tagline || ""
|
||||
const args = step.args || {}
|
||||
|
||||
return {
|
||||
id: step.id,
|
||||
type: step.type,
|
||||
params: step.params,
|
||||
args,
|
||||
heading: step.actionId,
|
||||
body: mustache.render(tagline, args),
|
||||
name: definition.name,
|
||||
}
|
||||
})
|
||||
this.workflow.definition.steps = steps
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
import { writable } from "svelte/store"
|
||||
import api from "../../api"
|
||||
import Workflow from "./Workflow"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
const workflowActions = store => ({
|
||||
fetch: async () => {
|
||||
const WORKFLOWS_URL = `/api/workflows`
|
||||
const workflowResponse = await api.get(WORKFLOWS_URL)
|
||||
const json = await workflowResponse.json()
|
||||
const responses = await Promise.all([
|
||||
api.get(`/api/workflows`),
|
||||
api.get(`/api/workflows/definitions/list`),
|
||||
])
|
||||
const jsonResponses = await Promise.all(responses.map(x => x.json()))
|
||||
store.update(state => {
|
||||
state.workflows = json
|
||||
state.workflows = jsonResponses[0]
|
||||
state.blockDefinitions = {
|
||||
TRIGGER: jsonResponses[1].trigger,
|
||||
ACTION: jsonResponses[1].action,
|
||||
LOGIC: jsonResponses[1].logic,
|
||||
}
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -23,8 +31,8 @@ const workflowActions = store => ({
|
|||
const response = await api.post(CREATE_WORKFLOW_URL, workflow)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
state.workflows = state.workflows.concat(json.workflow)
|
||||
state.currentWorkflow = new Workflow(json.workflow)
|
||||
state.workflows = [...state.workflows, json.workflow]
|
||||
store.actions.select(json.workflow)
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -38,20 +46,7 @@ const workflowActions = store => ({
|
|||
)
|
||||
state.workflows.splice(existingIdx, 1, json.workflow)
|
||||
state.workflows = state.workflows
|
||||
state.currentWorkflow = new Workflow(json.workflow)
|
||||
return state
|
||||
})
|
||||
},
|
||||
update: async ({ workflow }) => {
|
||||
const UPDATE_WORKFLOW_URL = `/api/workflows`
|
||||
const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
|
||||
const json = await response.json()
|
||||
store.update(state => {
|
||||
const existingIdx = state.workflows.findIndex(
|
||||
existing => existing._id === workflow._id
|
||||
)
|
||||
state.workflows.splice(existingIdx, 1, json.workflow)
|
||||
state.workflows = state.workflows
|
||||
store.actions.select(json.workflow)
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -66,28 +61,49 @@ const workflowActions = store => ({
|
|||
)
|
||||
state.workflows.splice(existingIdx, 1)
|
||||
state.workflows = state.workflows
|
||||
state.currentWorkflow = null
|
||||
state.selectedWorkflow = null
|
||||
state.selectedBlock = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
trigger: async ({ workflow }) => {
|
||||
const { _id } = workflow
|
||||
const TRIGGER_WORKFLOW_URL = `/api/workflows/${_id}/trigger`
|
||||
return await api.post(TRIGGER_WORKFLOW_URL)
|
||||
},
|
||||
select: workflow => {
|
||||
store.update(state => {
|
||||
state.currentWorkflow = new Workflow(workflow)
|
||||
state.selectedWorkflowBlock = null
|
||||
state.selectedWorkflow = new Workflow(cloneDeep(workflow))
|
||||
state.selectedBlock = null
|
||||
return state
|
||||
})
|
||||
},
|
||||
addBlockToWorkflow: block => {
|
||||
store.update(state => {
|
||||
state.currentWorkflow.addBlock(block)
|
||||
state.selectedWorkflowBlock = block
|
||||
const newBlock = state.selectedWorkflow.addBlock(cloneDeep(block))
|
||||
state.selectedBlock = newBlock
|
||||
return state
|
||||
})
|
||||
},
|
||||
deleteWorkflowBlock: block => {
|
||||
store.update(state => {
|
||||
state.currentWorkflow.deleteBlock(block.id)
|
||||
state.selectedWorkflowBlock = null
|
||||
const idx = state.selectedWorkflow.workflow.definition.steps.findIndex(
|
||||
x => x.id === block.id
|
||||
)
|
||||
state.selectedWorkflow.deleteBlock(block.id)
|
||||
|
||||
// Select next closest step
|
||||
const steps = state.selectedWorkflow.workflow.definition.steps
|
||||
let nextSelectedBlock
|
||||
if (steps[idx] != null) {
|
||||
nextSelectedBlock = steps[idx]
|
||||
} else if (steps[idx - 1] != null) {
|
||||
nextSelectedBlock = steps[idx - 1]
|
||||
} else {
|
||||
nextSelectedBlock =
|
||||
state.selectedWorkflow.workflow.definition.trigger || null
|
||||
}
|
||||
state.selectedBlock = nextSelectedBlock
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -96,11 +112,14 @@ const workflowActions = store => ({
|
|||
export const getWorkflowStore = () => {
|
||||
const INITIAL_WORKFLOW_STATE = {
|
||||
workflows: [],
|
||||
blockDefinitions: {
|
||||
TRIGGER: [],
|
||||
ACTION: [],
|
||||
LOGIC: [],
|
||||
},
|
||||
selectedWorkflow: null,
|
||||
}
|
||||
|
||||
const store = writable(INITIAL_WORKFLOW_STATE)
|
||||
|
||||
store.actions = workflowActions(store)
|
||||
|
||||
return store
|
||||
}
|
||||
|
|
|
@ -1,57 +1,48 @@
|
|||
import Workflow from "../Workflow";
|
||||
import TEST_WORKFLOW from "./testWorkflow";
|
||||
import Workflow from "../Workflow"
|
||||
import TEST_WORKFLOW from "./testWorkflow"
|
||||
|
||||
const TEST_BLOCK = {
|
||||
id: "VFWeZcIPx",
|
||||
name: "Update UI State",
|
||||
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>",
|
||||
icon: "ri-refresh-line",
|
||||
description: "Update your User Interface with some data.",
|
||||
environment: "CLIENT",
|
||||
params: {
|
||||
path: "string",
|
||||
value: "longText",
|
||||
},
|
||||
args: {
|
||||
path: "foo",
|
||||
value: "started...",
|
||||
},
|
||||
actionId: "SET_STATE",
|
||||
type: "ACTION",
|
||||
id: "AUXJQGZY7",
|
||||
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",
|
||||
args: { time: "5000" },
|
||||
stepId: "DELAY",
|
||||
}
|
||||
|
||||
describe("Workflow Data Object", () => {
|
||||
let workflow
|
||||
|
||||
beforeEach(() => {
|
||||
workflow = new Workflow({ ...TEST_WORKFLOW });
|
||||
});
|
||||
workflow = new Workflow({ ...TEST_WORKFLOW })
|
||||
})
|
||||
|
||||
it("adds a workflow block to the workflow", () => {
|
||||
workflow.addBlock(TEST_BLOCK);
|
||||
workflow.addBlock(TEST_BLOCK)
|
||||
expect(workflow.workflow.definition)
|
||||
})
|
||||
|
||||
it("updates a workflow block with new attributes", () => {
|
||||
const firstBlock = workflow.workflow.definition.steps[0];
|
||||
const firstBlock = workflow.workflow.definition.steps[0]
|
||||
const updatedBlock = {
|
||||
...firstBlock,
|
||||
name: "UPDATED"
|
||||
};
|
||||
workflow.updateBlock(updatedBlock, firstBlock.id);
|
||||
name: "UPDATED",
|
||||
}
|
||||
workflow.updateBlock(updatedBlock, firstBlock.id)
|
||||
expect(workflow.workflow.definition.steps[0]).toEqual(updatedBlock)
|
||||
})
|
||||
|
||||
it("deletes a workflow block successfully", () => {
|
||||
const { steps } = workflow.workflow.definition
|
||||
const originalLength = steps.length
|
||||
const originalLength = steps.length
|
||||
|
||||
const lastBlock = steps[steps.length - 1];
|
||||
workflow.deleteBlock(lastBlock.id);
|
||||
expect(workflow.workflow.definition.steps.length).toBeLessThan(originalLength);
|
||||
})
|
||||
|
||||
it("builds a tree that gets rendered in the flowchart builder", () => {
|
||||
expect(Workflow.buildUiTree(TEST_WORKFLOW.definition)).toMatchSnapshot();
|
||||
const lastBlock = steps[steps.length - 1]
|
||||
workflow.deleteBlock(lastBlock.id)
|
||||
expect(workflow.workflow.definition.steps.length).toBeLessThan(
|
||||
originalLength
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Workflow Data Object builds a tree that gets rendered in the flowchart builder 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"args": Object {
|
||||
"time": 3000,
|
||||
},
|
||||
"body": "Delay for <b>3000</b> milliseconds",
|
||||
"heading": "DELAY",
|
||||
"id": "zJQcZUgDS",
|
||||
"name": "Delay",
|
||||
"params": Object {
|
||||
"time": "number",
|
||||
},
|
||||
"type": "LOGIC",
|
||||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"path": "foo",
|
||||
"value": "finished",
|
||||
},
|
||||
"body": "Update <b>foo</b> to <b>finished</b>",
|
||||
"heading": "SET_STATE",
|
||||
"id": "3RSTO7BMB",
|
||||
"name": "Update UI State",
|
||||
"params": Object {
|
||||
"path": "string",
|
||||
"value": "longText",
|
||||
},
|
||||
"type": "ACTION",
|
||||
},
|
||||
Object {
|
||||
"args": Object {
|
||||
"path": "foo",
|
||||
"value": "started...",
|
||||
},
|
||||
"body": "Update <b>foo</b> to <b>started...</b>",
|
||||
"heading": "SET_STATE",
|
||||
"id": "VFWeZcIPx",
|
||||
"name": "Update UI State",
|
||||
"params": Object {
|
||||
"path": "string",
|
||||
"value": "longText",
|
||||
},
|
||||
"type": "ACTION",
|
||||
},
|
||||
]
|
||||
`;
|
|
@ -1,63 +1,78 @@
|
|||
export default {
|
||||
_id: "53b6148c65d1429c987e046852d11611",
|
||||
_rev: "4-02c6659734934895812fa7be0215ee59",
|
||||
name: "Test Workflow",
|
||||
name: "Test workflow",
|
||||
definition: {
|
||||
steps: [
|
||||
{
|
||||
id: "VFWeZcIPx",
|
||||
name: "Update UI State",
|
||||
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>",
|
||||
icon: "ri-refresh-line",
|
||||
description: "Update your User Interface with some data.",
|
||||
environment: "CLIENT",
|
||||
id: "ANBDINAPS",
|
||||
description: "Send an email.",
|
||||
tagline: "Send email to <b>{{to}}</b>",
|
||||
icon: "ri-mail-open-fill",
|
||||
name: "Send Email",
|
||||
params: {
|
||||
path: "string",
|
||||
value: "longText",
|
||||
to: "string",
|
||||
from: "string",
|
||||
subject: "longText",
|
||||
text: "longText",
|
||||
},
|
||||
args: {
|
||||
path: "foo",
|
||||
value: "started...",
|
||||
},
|
||||
actionId: "SET_STATE",
|
||||
type: "ACTION",
|
||||
},
|
||||
{
|
||||
id: "zJQcZUgDS",
|
||||
name: "Delay",
|
||||
icon: "ri-time-fill",
|
||||
tagline: "Delay for <b>{{time}}</b> milliseconds",
|
||||
description: "Delay the workflow until an amount of time has passed.",
|
||||
environment: "CLIENT",
|
||||
params: {
|
||||
time: "number",
|
||||
},
|
||||
args: {
|
||||
time: 3000,
|
||||
text: "A user was created!",
|
||||
subject: "New Budibase User",
|
||||
from: "budimaster@budibase.com",
|
||||
to: "test@test.com",
|
||||
},
|
||||
actionId: "DELAY",
|
||||
type: "LOGIC",
|
||||
},
|
||||
{
|
||||
id: "3RSTO7BMB",
|
||||
name: "Update UI State",
|
||||
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>",
|
||||
icon: "ri-refresh-line",
|
||||
description: "Update your User Interface with some data.",
|
||||
environment: "CLIENT",
|
||||
params: {
|
||||
path: "string",
|
||||
value: "longText",
|
||||
},
|
||||
args: {
|
||||
path: "foo",
|
||||
value: "finished",
|
||||
},
|
||||
actionId: "SET_STATE",
|
||||
type: "ACTION",
|
||||
stepId: "SEND_EMAIL",
|
||||
},
|
||||
],
|
||||
trigger: {
|
||||
id: "iRzYMOqND",
|
||||
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",
|
||||
args: {
|
||||
model: {
|
||||
type: "model",
|
||||
views: {},
|
||||
name: "users",
|
||||
schema: {
|
||||
name: {
|
||||
type: "string",
|
||||
constraints: {
|
||||
type: "string",
|
||||
length: { maximum: 123 },
|
||||
presence: { allowEmpty: false },
|
||||
},
|
||||
name: "name",
|
||||
},
|
||||
age: {
|
||||
type: "number",
|
||||
constraints: {
|
||||
type: "number",
|
||||
presence: { allowEmpty: false },
|
||||
numericality: {
|
||||
greaterThanOrEqualTo: "",
|
||||
lessThanOrEqualTo: "",
|
||||
},
|
||||
},
|
||||
name: "age",
|
||||
},
|
||||
},
|
||||
_id: "c6b4e610cd984b588837bca27188a451",
|
||||
_rev: "7-b8aa1ce0b53e88928bb88fc11bdc0aff",
|
||||
},
|
||||
},
|
||||
stepId: "RECORD_SAVED",
|
||||
},
|
||||
},
|
||||
type: "workflow",
|
||||
live: true,
|
||||
ok: true,
|
||||
id: "b384f861f4754e1693835324a7fcca62",
|
||||
rev: "1-aa1c2cbd868ef02e26f8fad531dd7e37",
|
||||
live: false,
|
||||
_id: "b384f861f4754e1693835324a7fcca62",
|
||||
_rev: "108-4116829ec375e0481d0ecab9e83a2caf",
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
async function deleteWorkflow() {
|
||||
await workflowStore.actions.delete({
|
||||
instanceId,
|
||||
workflow: $workflowStore.currentWorkflow.workflow,
|
||||
workflow: $workflowStore.selectedWorkflow.workflow,
|
||||
})
|
||||
onClosed()
|
||||
notifier.danger("Workflow deleted.")
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import deepmerge from "deepmerge"
|
||||
import { Label } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
|
||||
let pages = []
|
||||
let components = []
|
||||
let pageName
|
||||
|
||||
let selectedPage
|
||||
let selectedScreen
|
||||
|
||||
$: pages = $store.pages
|
||||
$: selectedPage = pages[pageName]
|
||||
$: screens = selectedPage ? selectedPage._screens : []
|
||||
$: if (selectedPage) {
|
||||
let result = selectedPage
|
||||
for (screen of screens) {
|
||||
result = deepmerge(result, screen)
|
||||
}
|
||||
components = result.props._children
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bb-margin-xl block-field">
|
||||
<Label small forAttr={'page'}>Page</Label>
|
||||
<select class="budibase__input" bind:value={pageName}>
|
||||
{#each Object.keys(pages) as page}
|
||||
<option value={page}>{page}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if components.length > 0}
|
||||
<Label small forAttr={'component'}>Component</Label>
|
||||
<select class="budibase__input" bind:value>
|
||||
{#each components as component}
|
||||
<option value={component._id}>{component._id}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
|
@ -2,13 +2,22 @@
|
|||
import { backendUiStore } from "builderStore"
|
||||
|
||||
export let value
|
||||
$: modelId = value ? value._id : ""
|
||||
|
||||
function onChange(e) {
|
||||
value = $backendUiStore.models.find(model => model._id === e.target.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bb-margin-xl block-field">
|
||||
<select class="budibase__input" bind:value>
|
||||
<option value="" />
|
||||
<div class="block-field">
|
||||
<select
|
||||
class="budibase__input"
|
||||
value={modelId}
|
||||
on:blur={onChange}
|
||||
on:change={onChange}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model}>{model.name}</option>
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,14 @@
|
|||
import { Input, Label } from "@budibase/bbui"
|
||||
|
||||
export let value
|
||||
$: modelId = value && value.model ? value.model._id : ""
|
||||
$: schemaFields = Object.keys(value && value.model ? value.model.schema : {})
|
||||
|
||||
function onChangeModel(e) {
|
||||
value.model = $backendUiStore.models.find(
|
||||
model => model._id === e.target.value
|
||||
)
|
||||
}
|
||||
|
||||
function setParsedValue(evt, field) {
|
||||
const fieldSchema = value.model.schema[field]
|
||||
|
@ -10,23 +18,27 @@
|
|||
value[field] = parseInt(evt.target.value)
|
||||
return
|
||||
}
|
||||
|
||||
value[field] = evt.target.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bb-margin-xl block-field">
|
||||
<select class="budibase__input" bind:value={value.model}>
|
||||
<div class="block-field">
|
||||
<select
|
||||
class="budibase__input"
|
||||
value={modelId}
|
||||
on:blur={onChangeModel}
|
||||
on:change={onChangeModel}>
|
||||
<option value="">Choose an option</option>
|
||||
{#each $backendUiStore.models as model}
|
||||
<option value={model}>{model.name}</option>
|
||||
<option value={model._id}>{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{#if value.model}
|
||||
{#if schemaFields.length}
|
||||
<div class="bb-margin-xl block-field">
|
||||
<Label small forAttr={'fields'}>Fields</Label>
|
||||
{#each Object.keys(value.model.schema) as field}
|
||||
{#each schemaFields as field}
|
||||
<div class="bb-margin-xl">
|
||||
<Input
|
||||
thin
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { fade } from "svelte/transition"
|
||||
import { onMount, getContext } from "svelte"
|
||||
import { getContext } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
|
||||
|
@ -9,49 +8,35 @@
|
|||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
const ACCESS_LEVELS = [
|
||||
{
|
||||
name: "Admin",
|
||||
key: "ADMIN",
|
||||
canExecute: true,
|
||||
editable: false,
|
||||
},
|
||||
{
|
||||
name: "Power User",
|
||||
key: "POWER_USER",
|
||||
canExecute: true,
|
||||
editable: false,
|
||||
},
|
||||
]
|
||||
|
||||
let selectedTab = "SETUP"
|
||||
let testResult
|
||||
|
||||
$: workflow =
|
||||
$workflowStore.currentWorkflow && $workflowStore.currentWorkflow.workflow
|
||||
$: workflowBlock = $workflowStore.selectedWorkflowBlock
|
||||
$workflowStore.selectedWorkflow && $workflowStore.selectedWorkflow.workflow
|
||||
|
||||
function deleteWorkflow() {
|
||||
open(
|
||||
DeleteWorkflowModal,
|
||||
{
|
||||
onClosed: close,
|
||||
},
|
||||
{ onClosed: close },
|
||||
{ styleContent: { padding: "0" } }
|
||||
)
|
||||
}
|
||||
|
||||
function deleteWorkflowBlock() {
|
||||
workflowStore.actions.deleteWorkflowBlock(workflowBlock)
|
||||
notifier.info("Workflow block deleted.")
|
||||
workflowStore.actions.deleteWorkflowBlock($workflowStore.selectedBlock)
|
||||
}
|
||||
|
||||
function testWorkflow() {
|
||||
testResult = "PASSED"
|
||||
async function testWorkflow() {
|
||||
const result = await workflowStore.actions.trigger({
|
||||
workflow: $workflowStore.selectedWorkflow.workflow,
|
||||
})
|
||||
if (result.status === 200) {
|
||||
notifier.success(`Workflow ${workflow.name} triggered successfully.`)
|
||||
} else {
|
||||
notifier.danger(`Failed to trigger workflow ${workflow.name}.`)
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWorkflow() {
|
||||
const workflow = $workflowStore.currentWorkflow.workflow
|
||||
await workflowStore.actions.save({
|
||||
instanceId: $backendUiStore.selectedDatabase._id,
|
||||
workflow,
|
||||
|
@ -65,38 +50,27 @@
|
|||
<span
|
||||
class="hoverable"
|
||||
class:selected={selectedTab === 'SETUP'}
|
||||
on:click={() => {
|
||||
selectedTab = 'SETUP'
|
||||
testResult = null
|
||||
}}>
|
||||
on:click={() => (selectedTab = 'SETUP')}>
|
||||
Setup
|
||||
</span>
|
||||
{#if !workflowBlock}
|
||||
<span
|
||||
class="test-tab"
|
||||
class:selected={selectedTab === 'TEST'}
|
||||
on:click={() => (selectedTab = 'TEST')}>
|
||||
Test
|
||||
</span>
|
||||
{/if}
|
||||
</header>
|
||||
{#if selectedTab === 'TEST'}
|
||||
<div class="bb-margin-m">
|
||||
{#if testResult}
|
||||
<button
|
||||
transition:fade
|
||||
class:passed={testResult === 'PASSED'}
|
||||
class:failed={testResult === 'FAILED'}
|
||||
class="test-result">
|
||||
{testResult}
|
||||
</button>
|
||||
{/if}
|
||||
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
|
||||
{#if $workflowStore.selectedBlock}
|
||||
<WorkflowBlockSetup bind:block={$workflowStore.selectedBlock} />
|
||||
<div class="buttons">
|
||||
<Button green wide data-cy="save-workflow-setup" on:click={saveWorkflow}>
|
||||
Save Workflow
|
||||
</Button>
|
||||
<Button red wide on:click={deleteWorkflowBlock}>Delete Block</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedTab === 'SETUP'}
|
||||
{#if workflowBlock}
|
||||
<WorkflowBlockSetup {workflowBlock} />
|
||||
{:else if $workflowStore.selectedWorkflow}
|
||||
<div class="panel">
|
||||
<div class="panel-body">
|
||||
<div class="block-label">
|
||||
Workflow
|
||||
<b>{workflow.name}</b>
|
||||
</div>
|
||||
</div>
|
||||
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
|
||||
<div class="buttons">
|
||||
<Button
|
||||
green
|
||||
|
@ -105,40 +79,9 @@
|
|||
on:click={saveWorkflow}>
|
||||
Save Workflow
|
||||
</Button>
|
||||
<Button red wide on:click={deleteWorkflowBlock}>Delete Block</Button>
|
||||
<Button red wide on:click={deleteWorkflow}>Delete Workflow</Button>
|
||||
</div>
|
||||
{:else if $workflowStore.currentWorkflow}
|
||||
<div class="panel">
|
||||
<div class="panel-body">
|
||||
<div class="block-label">Workflow: {workflow.name}</div>
|
||||
<div class="config-item">
|
||||
<Label small forAttr={'useraccess'}>User Access</Label>
|
||||
<div class="access-levels">
|
||||
|
||||
{#each ACCESS_LEVELS as level}
|
||||
<span class="access-level">
|
||||
<label>{level.name}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={!level.editable}
|
||||
bind:checked={level.canExecute} />
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<Button
|
||||
green
|
||||
wide
|
||||
data-cy="save-workflow-setup"
|
||||
on:click={saveWorkflow}>
|
||||
Save Workflow
|
||||
</Button>
|
||||
<Button red wide on:click={deleteWorkflow}>Delete Workflow</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
|
@ -181,10 +124,6 @@
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
header > span {
|
||||
color: var(--grey-5);
|
||||
margin-right: 20px;
|
||||
|
@ -205,35 +144,8 @@
|
|||
gap: 12px;
|
||||
}
|
||||
|
||||
.access-level {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.access-level label {
|
||||
font-weight: normal;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.test-result {
|
||||
border: none;
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--white);
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.passed {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.failed {
|
||||
background: var(--red);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,55 +1,45 @@
|
|||
<script>
|
||||
import { backendUiStore, store } from "builderStore"
|
||||
import ComponentSelector from "./ParamInputs/ComponentSelector.svelte"
|
||||
import ModelSelector from "./ParamInputs/ModelSelector.svelte"
|
||||
import RecordSelector from "./ParamInputs/RecordSelector.svelte"
|
||||
import { Input, TextArea, Select } from "@budibase/bbui"
|
||||
|
||||
export let workflowBlock
|
||||
|
||||
let params
|
||||
|
||||
$: workflowParams = workflowBlock.params
|
||||
? Object.entries(workflowBlock.params)
|
||||
: []
|
||||
export let block
|
||||
$: params = block.params ? Object.entries(block.params) : []
|
||||
</script>
|
||||
|
||||
<label class="selected-label">{workflowBlock.type}: {workflowBlock.name}</label>
|
||||
{#each workflowParams as [parameter, type]}
|
||||
<div class="block-field">
|
||||
<label class="label">{parameter}</label>
|
||||
{#if Array.isArray(type)}
|
||||
<Select bind:value={workflowBlock.args[parameter]} thin>
|
||||
{#each type as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</Select>
|
||||
{:else if type === 'component'}
|
||||
<ComponentSelector bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'accessLevel'}
|
||||
<Select bind:value={workflowBlock.args[parameter]} thin>
|
||||
<option value="ADMIN">Admin</option>
|
||||
<option value="POWER_USER">Power User</option>
|
||||
</Select>
|
||||
{:else if type === 'password'}
|
||||
<Input type="password" thin bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'number'}
|
||||
<Input type="number" thin bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'longText'}
|
||||
<TextArea
|
||||
type="text"
|
||||
thin
|
||||
bind:value={workflowBlock.args[parameter]}
|
||||
label="" />
|
||||
{:else if type === 'model'}
|
||||
<ModelSelector bind:value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'record'}
|
||||
<RecordSelector value={workflowBlock.args[parameter]} />
|
||||
{:else if type === 'string'}
|
||||
<Input type="text" thin bind:value={workflowBlock.args[parameter]} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<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>
|
||||
<option value="">Choose an option</option>
|
||||
{#each type as option}
|
||||
<option value={option}>{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]} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.block-field {
|
||||
|
@ -57,16 +47,19 @@
|
|||
}
|
||||
|
||||
label {
|
||||
text-transform: capitalize;
|
||||
font-size: 14px;
|
||||
font-family: sans-serif;
|
||||
font-weight: 500;
|
||||
color: var(--ink);
|
||||
margin-bottom: 12px;
|
||||
text-transform: capitalize;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.selected-label {
|
||||
text-transform: capitalize;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--grey-7);
|
||||
}
|
||||
|
||||
textarea {
|
||||
|
|
|
@ -1,30 +1,22 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { afterUpdate } from "svelte"
|
||||
import { workflowStore, backendUiStore } from "builderStore"
|
||||
import { notifier } from "builderStore/store/notifications"
|
||||
import Flowchart from "./flowchart/FlowChart.svelte"
|
||||
|
||||
let selectedWorkflow
|
||||
let uiTree
|
||||
let instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
$: selectedWorkflow = $workflowStore.currentWorkflow
|
||||
|
||||
$: workflowLive = selectedWorkflow && selectedWorkflow.workflow.live
|
||||
|
||||
$: uiTree = selectedWorkflow ? selectedWorkflow.createUiTree() : []
|
||||
|
||||
$: workflow =
|
||||
$workflowStore.selectedWorkflow && $workflowStore.selectedWorkflow.workflow
|
||||
$: workflowLive = workflow && workflow.live
|
||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
||||
|
||||
function onSelect(block) {
|
||||
workflowStore.update(state => {
|
||||
state.selectedWorkflowBlock = block
|
||||
state.selectedBlock = block
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
function setWorkflowLive(live) {
|
||||
const { workflow } = selectedWorkflow
|
||||
workflow.live = live
|
||||
workflowStore.actions.save({ instanceId, workflow })
|
||||
if (live) {
|
||||
|
@ -36,32 +28,42 @@
|
|||
</script>
|
||||
|
||||
<section>
|
||||
<Flowchart blocks={uiTree} {onSelect} />
|
||||
<footer>
|
||||
{#if selectedWorkflow}
|
||||
<button
|
||||
class:highlighted={workflowLive}
|
||||
class:hoverable={workflowLive}
|
||||
class="stop-button hoverable">
|
||||
<i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} />
|
||||
</button>
|
||||
<button
|
||||
class:highlighted={!workflowLive}
|
||||
class:hoverable={!workflowLive}
|
||||
class="play-button hoverable"
|
||||
data-cy="activate-workflow"
|
||||
on:click={() => setWorkflowLive(true)}>
|
||||
<i class="ri-play-fill" />
|
||||
</button>
|
||||
{/if}
|
||||
</footer>
|
||||
<Flowchart {workflow} {onSelect} />
|
||||
</section>
|
||||
<footer>
|
||||
{#if workflow}
|
||||
<button
|
||||
class:highlighted={workflowLive}
|
||||
class:hoverable={workflowLive}
|
||||
class="stop-button hoverable">
|
||||
<i class="ri-stop-fill" on:click={() => setWorkflowLive(false)} />
|
||||
</button>
|
||||
<button
|
||||
class:highlighted={!workflowLive}
|
||||
class:hoverable={!workflowLive}
|
||||
class="play-button hoverable"
|
||||
data-cy="activate-workflow"
|
||||
on:click={() => setWorkflowLive(true)}>
|
||||
<i class="ri-play-fill" />
|
||||
</button>
|
||||
{/if}
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
bottom: 20px;
|
||||
right: 30px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
@ -77,7 +79,9 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24px;
|
||||
}
|
||||
footer > button:first-child {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.play-button.highlighted {
|
||||
|
|
|
@ -7,3 +7,9 @@
|
|||
<path d="M5.0625 70H9L4.5 75L0 70H3.9375V65H5.0625V70Z" fill="#ADAEC4" />
|
||||
<rect x="4" width="1" height="65" fill="#ADAEC4" />
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
margin: 8px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
Before Width: | Height: | Size: 241 B After Width: | Height: | Size: 290 B |
|
@ -1,24 +1,52 @@
|
|||
<script>
|
||||
import FlowItem from "./FlowItem.svelte"
|
||||
import Arrow from "./Arrow.svelte"
|
||||
import { flip } from "svelte/animate"
|
||||
import { fade, fly } from "svelte/transition"
|
||||
|
||||
export let blocks = []
|
||||
export let workflow
|
||||
export let onSelect
|
||||
let blocks
|
||||
|
||||
$: {
|
||||
blocks = []
|
||||
if (workflow) {
|
||||
if (workflow.definition.trigger) {
|
||||
blocks.push(workflow.definition.trigger)
|
||||
}
|
||||
blocks = blocks.concat(workflow.definition.steps || [])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="canvas">
|
||||
{#each blocks as block, idx}
|
||||
<FlowItem {onSelect} {block} />
|
||||
{#if idx !== blocks.length - 1}
|
||||
<Arrow />
|
||||
{/if}
|
||||
{#each blocks as block, idx (block.id)}
|
||||
<div
|
||||
class="block"
|
||||
animate:flip={{ duration: 600 }}
|
||||
in:fade|local
|
||||
out:fly|local={{ x: 100 }}>
|
||||
<FlowItem {onSelect} {block} />
|
||||
{#if idx !== blocks.length - 1}
|
||||
<Arrow />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.canvas {
|
||||
section {
|
||||
position: absolute;
|
||||
padding: 20px 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
<script>
|
||||
import { fade } from "svelte/transition"
|
||||
import mustache from "mustache"
|
||||
import { workflowStore } from "builderStore"
|
||||
|
||||
export let onSelect
|
||||
export let block
|
||||
let selected
|
||||
|
||||
$: selected =
|
||||
$workflowStore.selectedBlock != null &&
|
||||
$workflowStore.selectedBlock.id === block.id
|
||||
|
||||
function selectBlock() {
|
||||
onSelect(block)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div transition:fade class={`${block.type} hoverable`} on:click={selectBlock}>
|
||||
<div class={`${block.type} hoverable`} class:selected on:click={selectBlock}>
|
||||
<header>
|
||||
{#if block.type === 'TRIGGER'}
|
||||
<i class="ri-lightbulb-fill" />
|
||||
|
@ -24,7 +30,7 @@
|
|||
</header>
|
||||
<hr />
|
||||
<p>
|
||||
{@html block.body}
|
||||
{@html mustache.render(block.tagline, block.args)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -32,8 +38,8 @@
|
|||
div {
|
||||
width: 320px;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
transition: 0.3s all;
|
||||
border-radius: var(--border-radius-m);
|
||||
transition: 0.3s all ease;
|
||||
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.08);
|
||||
background-color: var(--ink);
|
||||
font-size: 16px;
|
||||
|
@ -69,9 +75,12 @@
|
|||
|
||||
p {
|
||||
color: inherit;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.selected,
|
||||
div:hover {
|
||||
transform: scale(1.05);
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 4px 30px 0 rgba(57, 60, 68, 0.15);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,82 +1,39 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import { WorkflowList } from "../"
|
||||
import { workflowStore } from "builderStore"
|
||||
import WorkflowBlock from "./WorkflowBlock.svelte"
|
||||
import blockDefinitions from "../blockDefinitions"
|
||||
import FlatButtonGroup from "components/userInterface/FlatButtonGroup.svelte"
|
||||
|
||||
let selectedTab = "TRIGGER"
|
||||
let definitions = []
|
||||
|
||||
$: definitions = Object.entries(blockDefinitions[selectedTab])
|
||||
let buttonProps = []
|
||||
$: blocks = Object.entries($workflowStore.blockDefinitions[selectedTab])
|
||||
|
||||
$: {
|
||||
if (
|
||||
$workflowStore.currentWorkflow.hasTrigger() &&
|
||||
selectedTab === "TRIGGER"
|
||||
) {
|
||||
selectedTab = "ACTION"
|
||||
if ($workflowStore.selectedWorkflow.hasTrigger()) {
|
||||
buttonProps = [
|
||||
{ value: "ACTION", text: "Action" },
|
||||
{ value: "LOGIC", text: "Logic" },
|
||||
]
|
||||
if (selectedTab === "TRIGGER") {
|
||||
selectedTab = "ACTION"
|
||||
}
|
||||
} else {
|
||||
buttonProps = [{ value: "TRIGGER", text: "Trigger" }]
|
||||
if (selectedTab !== "TRIGGER") {
|
||||
selectedTab = "TRIGGER"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onChangeTab(tab) {
|
||||
selectedTab = tab
|
||||
}
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<div class="subtabs">
|
||||
{#if !$workflowStore.currentWorkflow.hasTrigger()}
|
||||
<span
|
||||
class="hoverable"
|
||||
class:selected={'TRIGGER' === selectedTab}
|
||||
on:click={() => (selectedTab = 'TRIGGER')}>
|
||||
Triggers
|
||||
</span>
|
||||
{/if}
|
||||
<span
|
||||
class="hoverable"
|
||||
class:selected={'ACTION' === selectedTab}
|
||||
on:click={() => (selectedTab = 'ACTION')}>
|
||||
Actions
|
||||
</span>
|
||||
<span
|
||||
class="hoverable"
|
||||
class:selected={'LOGIC' === selectedTab}
|
||||
on:click={() => (selectedTab = 'LOGIC')}>
|
||||
Logic
|
||||
</span>
|
||||
</div>
|
||||
<FlatButtonGroup value={selectedTab} {buttonProps} onChange={onChangeTab} />
|
||||
<div id="blocklist">
|
||||
{#each definitions as [actionId, blockDefinition]}
|
||||
<WorkflowBlock {blockDefinition} {actionId} blockType={selectedTab} />
|
||||
{#each blocks as [stepId, blockDefinition]}
|
||||
<WorkflowBlock {blockDefinition} {stepId} blockType={selectedTab} />
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.subtabs {
|
||||
margin-top: 20px;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: 1fr 1fr 1fr;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.subtabs span {
|
||||
transition: 0.3s all;
|
||||
text-align: center;
|
||||
color: var(--grey-7);
|
||||
font-weight: 400;
|
||||
padding: 8px 16px;
|
||||
text-rendering: optimizeLegibility;
|
||||
border: none !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.subtabs span.selected {
|
||||
background: var(--grey-3);
|
||||
color: var(--ink);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.subtabs span:not(.selected) {
|
||||
color: var(--ink);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<script>
|
||||
import { workflowStore } from "builderStore"
|
||||
|
||||
export let blockType
|
||||
export let blockDefinition
|
||||
export let actionId
|
||||
export let stepId
|
||||
export let blockType
|
||||
|
||||
function addBlockToWorkflow() {
|
||||
workflowStore.actions.addBlockToWorkflow({
|
||||
...blockDefinition,
|
||||
args: blockDefinition.args || {},
|
||||
actionId,
|
||||
stepId,
|
||||
type: blockType,
|
||||
})
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
|||
<div
|
||||
class="workflow-block hoverable"
|
||||
on:click={addBlockToWorkflow}
|
||||
data-cy={actionId}>
|
||||
data-cy={stepId}>
|
||||
<div>
|
||||
<i class={blockDefinition.icon} />
|
||||
</div>
|
||||
|
@ -31,11 +31,11 @@
|
|||
<style>
|
||||
.workflow-block {
|
||||
display: grid;
|
||||
grid-template-columns: 40px auto;
|
||||
grid-template-columns: 20px auto;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
padding: 16px 0px;
|
||||
border-radius: var(--border);
|
||||
padding: 12px;
|
||||
border-radius: var(--border-radius-m);
|
||||
}
|
||||
|
||||
.workflow-block:hover {
|
||||
|
@ -43,7 +43,7 @@
|
|||
}
|
||||
|
||||
.workflow-text {
|
||||
margin-left: 12px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
@ -64,6 +64,7 @@
|
|||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
|
||||
const { open, close } = getContext("simple-modal")
|
||||
|
||||
$: currentWorkflowId =
|
||||
$workflowStore.currentWorkflow &&
|
||||
$workflowStore.currentWorkflow.workflow._id
|
||||
$: selectedWorkflowId =
|
||||
$workflowStore.selectedWorkflow &&
|
||||
$workflowStore.selectedWorkflow.workflow._id
|
||||
|
||||
function newWorkflow() {
|
||||
open(
|
||||
|
@ -33,7 +33,7 @@
|
|||
{#each $workflowStore.workflows as workflow}
|
||||
<li
|
||||
class="workflow-item"
|
||||
class:selected={workflow._id === currentWorkflowId}
|
||||
class:selected={workflow._id === selectedWorkflowId}
|
||||
on:click={() => workflowStore.actions.select(workflow)}>
|
||||
<i class="ri-stackshare-line" class:live={workflow.live} />
|
||||
{workflow.name}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { backendUiStore, workflowStore } from "builderStore"
|
||||
import { WorkflowList, BlockList } from "./"
|
||||
import blockDefinitions from "./blockDefinitions"
|
||||
import { workflowStore } from "builderStore"
|
||||
import WorkflowList from "./WorkflowList/WorkflowList.svelte"
|
||||
import BlockList from "./BlockList/BlockList.svelte"
|
||||
|
||||
let selectedTab = "WORKFLOWS"
|
||||
let definitions = []
|
||||
</script>
|
||||
|
||||
<header>
|
||||
|
@ -16,7 +14,7 @@
|
|||
on:click={() => (selectedTab = 'WORKFLOWS')}>
|
||||
Workflows
|
||||
</span>
|
||||
{#if $workflowStore.currentWorkflow}
|
||||
{#if $workflowStore.selectedWorkflow}
|
||||
<span
|
||||
data-cy="add-workflow-component"
|
||||
class="hoverable"
|
||||
|
|
|
@ -12,11 +12,13 @@
|
|||
<div class="content">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="nav">
|
||||
<div class="inner">
|
||||
<SetupPanel />
|
||||
{#if $workflowStore.selectedWorkflow}
|
||||
<div class="nav">
|
||||
<div class="inner">
|
||||
<SetupPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -35,13 +37,11 @@
|
|||
|
||||
.content {
|
||||
flex: 1 1 auto;
|
||||
margin: 20px 40px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
overflow: auto;
|
||||
width: 300px;
|
||||
border-right: 1px solid var(--grey-2);
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { authenticate } from "./authenticate"
|
||||
import { triggerWorkflow } from "./workflow"
|
||||
import appStore from "../state/store"
|
||||
|
||||
const apiCall = method => async ({ url, body }) => {
|
||||
|
@ -96,7 +95,6 @@ const makeRecordRequestBody = parameters => {
|
|||
|
||||
export default {
|
||||
authenticate: authenticate(apiOpts),
|
||||
triggerWorkflow: triggerWorkflow(apiOpts),
|
||||
createRecord,
|
||||
updateRecord,
|
||||
}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
export default {
|
||||
NAVIGATE: () => {
|
||||
// TODO client navigation
|
||||
},
|
||||
DELAY: async ({ args }) => await delay(args.time),
|
||||
FILTER: ({ args }) => {
|
||||
const { field, condition, value } = args
|
||||
switch (condition) {
|
||||
case "equals":
|
||||
if (field !== value) return
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
import renderTemplateString from "../../state/renderTemplateString"
|
||||
import appStore from "../../state/store"
|
||||
import Orchestrator from "./orchestrator"
|
||||
import clientActions from "./actions"
|
||||
|
||||
// Execute a workflow from a running budibase app
|
||||
export const clientStrategy = ({ api }) => ({
|
||||
context: {},
|
||||
bindContextArgs: function(args) {
|
||||
const mappedArgs = { ...args }
|
||||
|
||||
// bind the workflow action args to the workflow context, if required
|
||||
for (let arg in args) {
|
||||
const argValue = args[arg]
|
||||
|
||||
// We don't want to render mustache templates on non-strings
|
||||
if (typeof argValue !== "string") continue
|
||||
|
||||
// Render the string with values from the workflow context and state
|
||||
mappedArgs[arg] = renderTemplateString(argValue, {
|
||||
context: this.context,
|
||||
state: appStore.get(),
|
||||
})
|
||||
}
|
||||
|
||||
return mappedArgs
|
||||
},
|
||||
run: async function(workflow) {
|
||||
for (let block of workflow.steps) {
|
||||
// This code gets run in the browser
|
||||
if (block.environment === "CLIENT") {
|
||||
const action = clientActions[block.actionId]
|
||||
await action({
|
||||
context: this.context,
|
||||
args: this.bindContextArgs(block.args),
|
||||
id: block.id,
|
||||
})
|
||||
}
|
||||
|
||||
// this workflow block gets executed on the server
|
||||
if (block.environment === "SERVER") {
|
||||
const EXECUTE_WORKFLOW_URL = `/api/workflows/action`
|
||||
const response = await api.post({
|
||||
url: EXECUTE_WORKFLOW_URL,
|
||||
body: {
|
||||
action: block.actionId,
|
||||
args: this.bindContextArgs(block.args, api),
|
||||
},
|
||||
})
|
||||
|
||||
this.context = {
|
||||
...this.context,
|
||||
[block.actionId]: response,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const triggerWorkflow = api => async ({ workflow }) => {
|
||||
const workflowOrchestrator = new Orchestrator(api)
|
||||
workflowOrchestrator.strategy = clientStrategy
|
||||
|
||||
const EXECUTE_WORKFLOW_URL = `/api/workflows/${workflow}`
|
||||
const workflowDefinition = await api.get({ url: EXECUTE_WORKFLOW_URL })
|
||||
|
||||
workflowOrchestrator.execute(workflowDefinition)
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
/**
|
||||
* The workflow orchestrator is a class responsible for executing workflows.
|
||||
* It relies on the strategy pattern, which allows composable behaviour to be
|
||||
* passed into its execute() function. This allows custom execution behaviour based
|
||||
* on where the orchestrator is run.
|
||||
*
|
||||
*/
|
||||
export default class Orchestrator {
|
||||
constructor(api) {
|
||||
this.api = api
|
||||
}
|
||||
|
||||
set strategy(strategy) {
|
||||
this._strategy = strategy({ api: this.api })
|
||||
}
|
||||
|
||||
async execute(workflow) {
|
||||
if (workflow.live) {
|
||||
this._strategy.run(workflow.definition)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import api from "../api"
|
||||
import renderTemplateString from "./renderTemplateString"
|
||||
|
||||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||
|
@ -6,9 +5,6 @@ export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
|||
export const eventHandlers = routeTo => {
|
||||
const handlers = {
|
||||
"Navigate To": param => routeTo(param && param.url),
|
||||
"Create Record": api.createRecord,
|
||||
"Update Record": api.updateRecord,
|
||||
"Trigger Workflow": api.triggerWorkflow,
|
||||
}
|
||||
|
||||
// when an event is called, this is what gets run
|
||||
|
|
|
@ -4,6 +4,7 @@ WORKDIR /app
|
|||
|
||||
ENV CLOUD=1
|
||||
ENV COUCH_DB_URL=https://couchdb.budi.live:5984
|
||||
env BUDIBASE_ENVIRONMENT=PRODUCTION
|
||||
|
||||
# copy files and install dependencies
|
||||
COPY . ./
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
"electron-updater": "^4.3.1",
|
||||
"fix-path": "^3.0.0",
|
||||
"fs-extra": "^8.1.0",
|
||||
"joi": "^17.2.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"koa": "^2.7.0",
|
||||
"koa-body": "^4.1.0",
|
||||
|
@ -73,6 +74,7 @@
|
|||
"tar-fs": "^2.1.0",
|
||||
"uuid": "^3.3.2",
|
||||
"validate.js": "^0.13.1",
|
||||
"worker-farm": "^1.7.0",
|
||||
"yargs": "^13.2.4",
|
||||
"zlib": "^1.0.5"
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@ exports.authenticate = async ctx => {
|
|||
const { clientId } = await masterDb.get(ctx.user.appId)
|
||||
|
||||
if (!clientId) {
|
||||
ctx.throw(400, "ClientId not suplied")
|
||||
ctx.throw(400, "ClientId not supplied")
|
||||
}
|
||||
// find the instance that the user is associated with
|
||||
const db = new CouchDB(ClientDb.name(clientId))
|
||||
|
|
|
@ -2,6 +2,16 @@ const CouchDB = require("../../db")
|
|||
const validateJs = require("validate.js")
|
||||
const newid = require("../../db/newid")
|
||||
|
||||
function emitEvent(eventType, ctx, record) {
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emit(eventType, {
|
||||
args: {
|
||||
record,
|
||||
},
|
||||
instanceId: ctx.user.instanceId,
|
||||
})
|
||||
}
|
||||
|
||||
validateJs.extend(validateJs.validators.datetime, {
|
||||
parse: function(value) {
|
||||
return new Date(value).getTime()
|
||||
|
@ -110,13 +120,7 @@ exports.save = async function(ctx) {
|
|||
}
|
||||
}
|
||||
|
||||
ctx.eventEmitter &&
|
||||
ctx.eventEmitter.emit(`record:save`, {
|
||||
args: {
|
||||
record,
|
||||
},
|
||||
instanceId: ctx.user.instanceId,
|
||||
})
|
||||
emitEvent(`record:save`, ctx, record)
|
||||
ctx.body = record
|
||||
ctx.status = 200
|
||||
ctx.message = `${model.name} created successfully`
|
||||
|
@ -179,7 +183,7 @@ exports.destroy = async function(ctx) {
|
|||
return
|
||||
}
|
||||
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
|
||||
ctx.eventEmitter && ctx.eventEmitter.emit(`record:delete`, record)
|
||||
emitEvent(`record:delete`, ctx, record)
|
||||
}
|
||||
|
||||
exports.validate = async function(ctx) {
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
const userController = require("../../user")
|
||||
|
||||
module.exports = async function createUser({ args, instanceId }) {
|
||||
const ctx = {
|
||||
params: {
|
||||
instanceId,
|
||||
},
|
||||
request: {
|
||||
body: args.user,
|
||||
},
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await userController.create(ctx)
|
||||
return {
|
||||
user: response,
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return {
|
||||
user: null,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
const recordController = require("../../record")
|
||||
|
||||
module.exports = async function saveRecord({ 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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
const sgMail = require("@sendgrid/mail")
|
||||
|
||||
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
|
||||
|
||||
module.exports = async function sendEmail({ 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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,85 +1,81 @@
|
|||
const ACTION = {
|
||||
SET_STATE: {
|
||||
name: "Update UI State",
|
||||
tagline: "Update <b>{{path}}</b> to <b>{{value}}</b>",
|
||||
icon: "ri-refresh-line",
|
||||
description: "Update your User Interface with some data.",
|
||||
environment: "CLIENT",
|
||||
params: {
|
||||
path: "string",
|
||||
value: "longText",
|
||||
},
|
||||
},
|
||||
NAVIGATE: {
|
||||
name: "Navigate",
|
||||
tagline: "Navigate to <b>{{url}}</b>",
|
||||
icon: "ri-navigation-line",
|
||||
description: "Navigate to another page.",
|
||||
environment: "CLIENT",
|
||||
params: {
|
||||
url: "string",
|
||||
},
|
||||
},
|
||||
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.",
|
||||
environment: "SERVER",
|
||||
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",
|
||||
environment: "SERVER",
|
||||
params: {
|
||||
record: "record",
|
||||
},
|
||||
args: {
|
||||
record: {},
|
||||
},
|
||||
params: {},
|
||||
args: {},
|
||||
type: "ACTION",
|
||||
},
|
||||
// FIND_RECORD: {
|
||||
// description: "Find a record in your database.",
|
||||
// tagline: "<b>Find</b> a <b>{{record.model.name}}</b> record",
|
||||
// icon: "ri-search-line",
|
||||
// name: "Find Record",
|
||||
// environment: "SERVER",
|
||||
// params: {
|
||||
// record: "string",
|
||||
// },
|
||||
// },
|
||||
CREATE_USER: {
|
||||
description: "Create a new user.",
|
||||
tagline: "Create user <b>{{username}}</b>",
|
||||
icon: "ri-user-add-fill",
|
||||
name: "Create User",
|
||||
environment: "SERVER",
|
||||
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",
|
||||
environment: "SERVER",
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -89,11 +85,11 @@ const TRIGGER = {
|
|||
event: "record:save",
|
||||
icon: "ri-save-line",
|
||||
tagline: "Record is added to <b>{{model.name}}</b>",
|
||||
description: "Save a record to your database.",
|
||||
environment: "SERVER",
|
||||
description: "Fired when a record is saved to your database.",
|
||||
params: {
|
||||
model: "model",
|
||||
},
|
||||
type: "TRIGGER",
|
||||
},
|
||||
RECORD_DELETED: {
|
||||
name: "Record Deleted",
|
||||
|
@ -101,70 +97,17 @@ const TRIGGER = {
|
|||
icon: "ri-delete-bin-line",
|
||||
tagline: "Record is deleted from <b>{{model.name}}</b>",
|
||||
description: "Fired when a record is deleted from your database.",
|
||||
environment: "SERVER",
|
||||
params: {
|
||||
model: "model",
|
||||
},
|
||||
},
|
||||
// CLICK: {
|
||||
// name: "Click",
|
||||
// icon: "ri-cursor-line",
|
||||
// tagline: "{{component}} is clicked",
|
||||
// description: "Trigger when you click on an element in the UI.",
|
||||
// environment: "CLIENT",
|
||||
// params: {
|
||||
// component: "component"
|
||||
// }
|
||||
// },
|
||||
// LOAD: {
|
||||
// name: "Load",
|
||||
// icon: "ri-loader-line",
|
||||
// tagline: "{{component}} is loaded",
|
||||
// description: "Trigger an element has finished loading.",
|
||||
// environment: "CLIENT",
|
||||
// params: {
|
||||
// component: "component"
|
||||
// }
|
||||
// },
|
||||
// INPUT: {
|
||||
// name: "Input",
|
||||
// icon: "ri-text",
|
||||
// tagline: "Text entered into {{component}",
|
||||
// description: "Trigger when you type into an input box.",
|
||||
// environment: "CLIENT",
|
||||
// params: {
|
||||
// component: "component"
|
||||
// }
|
||||
// },
|
||||
}
|
||||
|
||||
const LOGIC = {
|
||||
FILTER: {
|
||||
name: "Filter",
|
||||
tagline: "{{field}} <b>{{condition}}</b> {{value}}",
|
||||
icon: "ri-git-branch-line",
|
||||
description: "Filter any workflows which do not meet certain conditions.",
|
||||
environment: "CLIENT",
|
||||
params: {
|
||||
filter: "string",
|
||||
condition: ["equals"],
|
||||
value: "string",
|
||||
},
|
||||
},
|
||||
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.",
|
||||
environment: "CLIENT",
|
||||
params: {
|
||||
time: "number",
|
||||
},
|
||||
type: "TRIGGER",
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
// 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,
|
||||
TRIGGER,
|
||||
LOGIC,
|
||||
TRIGGER,
|
||||
}
|
|
@ -1,5 +1,13 @@
|
|||
const CouchDB = require("../../../db")
|
||||
const newid = require("../../../db/newid")
|
||||
const blockDefinitions = require("./blockDefinitions")
|
||||
const triggers = require("../../../workflows/triggers")
|
||||
|
||||
/*************************
|
||||
* *
|
||||
* BUILDER FUNCTIONS *
|
||||
* *
|
||||
*************************/
|
||||
|
||||
exports.create = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
|
@ -53,22 +61,47 @@ exports.find = async function(ctx) {
|
|||
ctx.body = await db.get(ctx.params.id)
|
||||
}
|
||||
|
||||
exports.executeAction = async function(ctx) {
|
||||
const { args, action } = ctx.request.body
|
||||
const workflowAction = require(`./actions/${action}`)
|
||||
const response = await workflowAction({
|
||||
args,
|
||||
instanceId: ctx.user.instanceId,
|
||||
})
|
||||
ctx.body = response
|
||||
}
|
||||
|
||||
exports.fetchActionScript = async function(ctx) {
|
||||
const workflowAction = require(`./actions/${ctx.action}`)
|
||||
ctx.body = workflowAction
|
||||
}
|
||||
|
||||
exports.destroy = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
|
||||
}
|
||||
|
||||
exports.getActionList = async function(ctx) {
|
||||
ctx.body = blockDefinitions.ACTION
|
||||
}
|
||||
|
||||
exports.getTriggerList = async function(ctx) {
|
||||
ctx.body = blockDefinitions.TRIGGER
|
||||
}
|
||||
|
||||
exports.getLogicList = async function(ctx) {
|
||||
ctx.body = blockDefinitions.LOGIC
|
||||
}
|
||||
|
||||
module.exports.getDefinitionList = async function(ctx) {
|
||||
ctx.body = {
|
||||
logic: blockDefinitions.LOGIC,
|
||||
trigger: blockDefinitions.TRIGGER,
|
||||
action: blockDefinitions.ACTION,
|
||||
}
|
||||
}
|
||||
|
||||
/*********************
|
||||
* *
|
||||
* API FUNCTIONS *
|
||||
* *
|
||||
*********************/
|
||||
|
||||
exports.trigger = async function(ctx) {
|
||||
const db = new CouchDB(ctx.user.instanceId)
|
||||
let workflow = await db.get(ctx.params.id)
|
||||
await triggers.externalTrigger(workflow, {
|
||||
...ctx.request.body,
|
||||
instanceId: ctx.user.instanceId,
|
||||
})
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: `Workflow ${workflow._id} has been triggered.`,
|
||||
workflow,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ describe("/accesslevels", () => {
|
|||
beforeEach(async () => {
|
||||
instanceId = (await createInstance(request, appId))._id
|
||||
model = await createModel(request, appId, instanceId)
|
||||
view = await createView(request, appId, instanceId)
|
||||
view = await createView(request, appId, instanceId, model._id)
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
|
@ -111,7 +111,7 @@ describe("/accesslevels", () => {
|
|||
await request
|
||||
.get(`/api/accesslevels/${customLevel._id}`)
|
||||
.set(defaultHeaders(appId, instanceId))
|
||||
.expect(404)
|
||||
.expect(404)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -67,9 +67,10 @@ exports.createModel = async (request, appId, instanceId, model) => {
|
|||
return res.body
|
||||
}
|
||||
|
||||
exports.createView = async (request, appId, instanceId, view) => {
|
||||
exports.createView = async (request, appId, instanceId, modelId, view) => {
|
||||
view = view || {
|
||||
map: "function(doc) { emit(doc[doc.key], doc._id); } ",
|
||||
modelId: modelId,
|
||||
}
|
||||
|
||||
const res = await request
|
||||
|
|
|
@ -120,6 +120,10 @@ describe("/models", () => {
|
|||
testModel = await createModel(request, app._id, instance._id, testModel)
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete testModel._rev
|
||||
});
|
||||
|
||||
it("returns all the models for that instance in the response body", done => {
|
||||
request
|
||||
.get(`/api/models`)
|
||||
|
|
|
@ -23,7 +23,7 @@ const TEST_WORKFLOW = {
|
|||
|
||||
],
|
||||
next: {
|
||||
actionId: "abc123",
|
||||
stepId: "abc123",
|
||||
type: "SERVER",
|
||||
conditions: {
|
||||
}
|
||||
|
|
|
@ -1,21 +1,77 @@
|
|||
const Router = require("@koa/router")
|
||||
const controller = require("../controllers/workflow")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const joiValidator = require("../../middleware/joi-validator")
|
||||
const { BUILDER } = require("../../utilities/accessLevels")
|
||||
const Joi = require("joi")
|
||||
|
||||
const router = Router()
|
||||
|
||||
// prettier-ignore
|
||||
function generateStepSchema(allowStepTypes) {
|
||||
return Joi.object({
|
||||
stepId: Joi.string().required(),
|
||||
id: Joi.string().required(),
|
||||
description: Joi.string().required(),
|
||||
name: Joi.string().required(),
|
||||
tagline: Joi.string().required(),
|
||||
icon: Joi.string().required(),
|
||||
params: Joi.object(),
|
||||
// TODO: validate args a bit more deeply
|
||||
args: Joi.object(),
|
||||
type: Joi.string().required().valid(...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))
|
||||
|
||||
router
|
||||
.get(
|
||||
"/api/workflows/trigger/list",
|
||||
authorized(BUILDER),
|
||||
controller.getTriggerList
|
||||
)
|
||||
.get(
|
||||
"/api/workflows/action/list",
|
||||
authorized(BUILDER),
|
||||
controller.getActionList
|
||||
)
|
||||
.get(
|
||||
"/api/workflows/logic/list",
|
||||
authorized(BUILDER),
|
||||
controller.getLogicList
|
||||
)
|
||||
.get(
|
||||
"/api/workflows/definitions/list",
|
||||
authorized(BUILDER),
|
||||
controller.getDefinitionList
|
||||
)
|
||||
.get("/api/workflows", authorized(BUILDER), controller.fetch)
|
||||
.get("/api/workflows/:id", authorized(BUILDER), controller.find)
|
||||
.get(
|
||||
"/api/workflows/:id/:action",
|
||||
.put(
|
||||
"/api/workflows",
|
||||
authorized(BUILDER),
|
||||
controller.fetchActionScript
|
||||
workflowValidator,
|
||||
controller.update
|
||||
)
|
||||
.put("/api/workflows", authorized(BUILDER), controller.update)
|
||||
.post("/api/workflows", authorized(BUILDER), controller.create)
|
||||
.post("/api/workflows/action", controller.executeAction)
|
||||
.post(
|
||||
"/api/workflows",
|
||||
authorized(BUILDER),
|
||||
workflowValidator,
|
||||
controller.create
|
||||
)
|
||||
.post("/api/workflows/:id/trigger", controller.trigger)
|
||||
.delete("/api/workflows/:id/:rev", authorized(BUILDER), controller.destroy)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -6,6 +6,7 @@ const http = require("http")
|
|||
const api = require("./api")
|
||||
const env = require("./environment")
|
||||
const eventEmitter = require("./events")
|
||||
const workflows = require("./workflows/index")
|
||||
const Sentry = require("@sentry/node")
|
||||
|
||||
const app = new Koa()
|
||||
|
@ -49,4 +50,5 @@ process.on("SIGINT", () => process.exit(1))
|
|||
|
||||
module.exports = server.listen(env.PORT || 4001, () => {
|
||||
console.log(`Budibase running on ${JSON.stringify(server.address())}`)
|
||||
workflows.init()
|
||||
})
|
||||
|
|
|
@ -7,4 +7,5 @@ module.exports = {
|
|||
COUCH_DB_URL: process.env.COUCH_DB_URL,
|
||||
SALT_ROUNDS: process.env.SALT_ROUNDS,
|
||||
LOGGER: process.env.LOGGER,
|
||||
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT,
|
||||
}
|
||||
|
|
|
@ -1,33 +1,11 @@
|
|||
const EventEmitter = require("events").EventEmitter
|
||||
const CouchDB = require("../db")
|
||||
const { Orchestrator, serverStrategy } = require("./workflow")
|
||||
|
||||
/**
|
||||
* keeping event emitter in one central location as it might be used for things other than
|
||||
* workflows (what it was for originally) - having a central emitter will be useful in the
|
||||
* future.
|
||||
*/
|
||||
|
||||
const emitter = new EventEmitter()
|
||||
|
||||
async function executeRelevantWorkflows(event, eventType) {
|
||||
const db = new CouchDB(event.instanceId)
|
||||
const workflowsToTrigger = await db.query("database/by_workflow_trigger", {
|
||||
key: [eventType],
|
||||
include_docs: true,
|
||||
})
|
||||
|
||||
const workflows = workflowsToTrigger.rows.map(wf => wf.doc)
|
||||
|
||||
// Create orchestrator
|
||||
const workflowOrchestrator = new Orchestrator()
|
||||
workflowOrchestrator.strategy = serverStrategy
|
||||
|
||||
for (let workflow of workflows) {
|
||||
workflowOrchestrator.execute(workflow, event)
|
||||
}
|
||||
}
|
||||
|
||||
emitter.on("record:save", async function(event) {
|
||||
await executeRelevantWorkflows(event, "record:save")
|
||||
})
|
||||
|
||||
emitter.on("record:delete", async function(event) {
|
||||
await executeRelevantWorkflows(event, "record:delete")
|
||||
})
|
||||
|
||||
module.exports = emitter
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
const mustache = require("mustache")
|
||||
|
||||
/**
|
||||
* The workflow orchestrator is a class responsible for executing workflows.
|
||||
* It relies on the strategy pattern, which allows composable behaviour to be
|
||||
* passed into its execute() function. This allows custom execution behaviour based
|
||||
* on where the orchestrator is run.
|
||||
*
|
||||
*/
|
||||
exports.Orchestrator = class Orchestrator {
|
||||
set strategy(strategy) {
|
||||
this._strategy = strategy()
|
||||
}
|
||||
|
||||
async execute(workflow, context) {
|
||||
if (workflow.live) {
|
||||
this._strategy.run(workflow.definition, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.serverStrategy = () => ({
|
||||
context: {},
|
||||
bindContextArgs: function(args) {
|
||||
const mappedArgs = { ...args }
|
||||
|
||||
// bind the workflow action args to the workflow context, if required
|
||||
for (let arg in args) {
|
||||
const argValue = args[arg]
|
||||
// We don't want to render mustache templates on non-strings
|
||||
if (typeof argValue !== "string") continue
|
||||
|
||||
mappedArgs[arg] = mustache.render(argValue, { context: this.context })
|
||||
}
|
||||
|
||||
return mappedArgs
|
||||
},
|
||||
run: async function(workflow, context) {
|
||||
for (let block of workflow.steps) {
|
||||
if (block.type === "CLIENT") continue
|
||||
|
||||
const action = require(`../api/controllers/workflow/actions/${block.actionId}`)
|
||||
const response = await action({
|
||||
args: this.bindContextArgs(block.args),
|
||||
context,
|
||||
})
|
||||
|
||||
this.context = {
|
||||
...this.context,
|
||||
[block.id]: response,
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
|
@ -0,0 +1,16 @@
|
|||
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}`)
|
||||
}
|
||||
}
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.body = schema => {
|
||||
return validate(schema, "body")
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
const userController = require("../api/controllers/user")
|
||||
const recordController = require("../api/controllers/record")
|
||||
const sgMail = require("@sendgrid/mail")
|
||||
|
||||
sgMail.setApiKey(process.env.SENDGRID_API_KEY)
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.getAction = async function(actionName) {
|
||||
if (BUILTIN_ACTIONS[actionName] != null) {
|
||||
return BUILTIN_ACTIONS[actionName]
|
||||
}
|
||||
// TODO: load async actions here
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
const triggers = require("./triggers")
|
||||
const environment = require("../environment")
|
||||
const workerFarm = require("worker-farm")
|
||||
const singleThread = require("./thread")
|
||||
|
||||
let workers = workerFarm(require.resolve("./thread"))
|
||||
|
||||
function runWorker(job) {
|
||||
return new Promise((resolve, reject) => {
|
||||
workers(job, err => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* This module is built purely to kick off the worker farm and manage the inputs/outputs
|
||||
*/
|
||||
module.exports.init = function() {
|
||||
triggers.workflowQueue.process(async job => {
|
||||
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
|
||||
await runWorker(job)
|
||||
} else {
|
||||
await singleThread(job)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
let LOGIC = {
|
||||
DELAY: async function delay({ args }) {
|
||||
await wait(args.time)
|
||||
},
|
||||
|
||||
FILTER: async function filter({ args }) {
|
||||
const { field, condition, value } = args
|
||||
switch (condition) {
|
||||
case "equals":
|
||||
if (field !== value) return
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
module.exports.getLogic = function(logicName) {
|
||||
if (LOGIC[logicName] != null) {
|
||||
return LOGIC[logicName]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
let events = require("events")
|
||||
|
||||
// Bull works with a Job wrapper around all messages that contains a lot more information about
|
||||
// the state of the message, implement this for the sake of maintaining API consistency
|
||||
function newJob(queue, message) {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
queue: queue,
|
||||
data: message,
|
||||
}
|
||||
}
|
||||
|
||||
// designed to replicate Bull (https://github.com/OptimalBits/bull) in memory as a sort of mock
|
||||
class InMemoryQueue {
|
||||
// opts is not used by this as there is no real use case when in memory, but is the same API as Bull
|
||||
constructor(name, opts) {
|
||||
this._name = name
|
||||
this._opts = opts
|
||||
this._messages = []
|
||||
this._emitter = new events.EventEmitter()
|
||||
}
|
||||
|
||||
// same API as bull, provide a callback and it will respond when messages are available
|
||||
process(func) {
|
||||
this._emitter.on("message", async () => {
|
||||
if (this._messages.length <= 0) {
|
||||
return
|
||||
}
|
||||
let msg = this._messages.shift()
|
||||
let resp = func(msg)
|
||||
if (resp.then != null) {
|
||||
await resp
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// simply puts a message to the queue and emits to the queue for processing
|
||||
add(msg) {
|
||||
this._messages.push(newJob(this._name, msg))
|
||||
this._emitter.emit("message")
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InMemoryQueue
|
|
@ -0,0 +1,68 @@
|
|||
const mustache = require("mustache")
|
||||
const actions = require("./actions")
|
||||
const logic = require("./logic")
|
||||
|
||||
/**
|
||||
* 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 = {}
|
||||
this._workflow = workflow
|
||||
}
|
||||
|
||||
async getStep(type, stepId) {
|
||||
let step = null
|
||||
if (type === "ACTION") {
|
||||
step = await actions.getAction(stepId)
|
||||
} else if (type === "LOGIC") {
|
||||
step = logic.getLogic(stepId)
|
||||
}
|
||||
if (step == null) {
|
||||
throw `Cannot find workflow step by name ${stepId}`
|
||||
}
|
||||
return step
|
||||
}
|
||||
|
||||
async execute(context) {
|
||||
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,
|
||||
})
|
||||
|
||||
this._context = {
|
||||
...this._context,
|
||||
[block.id]: response,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if (cb) {
|
||||
cb()
|
||||
}
|
||||
} catch (err) {
|
||||
if (cb) {
|
||||
cb(err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
const CouchDB = require("../db")
|
||||
const emitter = require("../events/index")
|
||||
const InMemoryQueue = require("./queue/inMemoryQueue")
|
||||
|
||||
let workflowQueue = new InMemoryQueue()
|
||||
|
||||
async function queueRelevantWorkflows(event, eventType) {
|
||||
if (event.instanceId == null) {
|
||||
throw `No instanceId specified for ${eventType} - check event emitters.`
|
||||
}
|
||||
const db = new CouchDB(event.instanceId)
|
||||
const workflowsToTrigger = await db.query("database/by_workflow_trigger", {
|
||||
key: [eventType],
|
||||
include_docs: true,
|
||||
})
|
||||
|
||||
const workflows = workflowsToTrigger.rows.map(wf => wf.doc)
|
||||
for (let workflow of workflows) {
|
||||
if (!workflow.live) {
|
||||
continue
|
||||
}
|
||||
workflowQueue.add({ workflow, event })
|
||||
}
|
||||
}
|
||||
|
||||
emitter.on("record:save", async function(event) {
|
||||
await queueRelevantWorkflows(event, "record:save")
|
||||
})
|
||||
|
||||
emitter.on("record:delete", async function(event) {
|
||||
await queueRelevantWorkflows(event, "record:delete")
|
||||
})
|
||||
|
||||
module.exports.externalTrigger = async function(workflow, params) {
|
||||
workflowQueue.add({ workflow, event: params })
|
||||
}
|
||||
|
||||
module.exports.workflowQueue = workflowQueue
|
|
@ -172,6 +172,21 @@
|
|||
lodash "^4.17.13"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@budibase/client@^0.1.19":
|
||||
version "0.1.19"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.1.19.tgz#3906781423ab4626118c981657ecf7a4578c547c"
|
||||
integrity sha512-crxnLgebsh6CR0aMleDahY/1vFPbveG6JuSS/EVZeoBmckzK8hwiUQYQhIlf68nZfzWsCE/M9TX7SJxsrKY3bQ==
|
||||
dependencies:
|
||||
"@nx-js/compiler-util" "^2.0.0"
|
||||
bcryptjs "^2.4.3"
|
||||
deep-equal "^2.0.1"
|
||||
lodash "^4.17.15"
|
||||
lunr "^2.3.5"
|
||||
mustache "^4.0.1"
|
||||
regexparam "^1.3.0"
|
||||
shortid "^2.2.8"
|
||||
svelte "^3.9.2"
|
||||
|
||||
"@cnakazawa/watch@^1.0.3":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a"
|
||||
|
@ -201,10 +216,39 @@
|
|||
global-agent "^2.0.2"
|
||||
global-tunnel-ng "^2.7.1"
|
||||
|
||||
"@hapi/address@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d"
|
||||
integrity sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@hapi/bourne@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d"
|
||||
|
||||
"@hapi/formula@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-2.0.0.tgz#edade0619ed58c8e4f164f233cda70211e787128"
|
||||
integrity sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==
|
||||
|
||||
"@hapi/hoek@^9.0.0":
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6"
|
||||
integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==
|
||||
|
||||
"@hapi/pinpoint@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.0.tgz#805b40d4dbec04fc116a73089494e00f073de8df"
|
||||
integrity sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==
|
||||
|
||||
"@hapi/topo@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7"
|
||||
integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==
|
||||
dependencies:
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@jest/console@^24.7.1", "@jest/console@^24.9.0":
|
||||
version "24.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0"
|
||||
|
@ -354,6 +398,11 @@
|
|||
path-to-regexp "1.x"
|
||||
urijs "^1.19.2"
|
||||
|
||||
"@nx-js/compiler-util@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@nx-js/compiler-util/-/compiler-util-2.0.0.tgz#c74c12165fa2f017a292bb79af007e8fce0af297"
|
||||
integrity sha512-AxSQbwj9zqt8DYPZ6LwZdytqnwfiOEdcFdq4l8sdjkZmU2clTht7RDLCI8xvkp7KqgcNaOGlTeCM55TULWruyQ==
|
||||
|
||||
"@sendgrid/client@^7.1.1":
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.1.1.tgz#09a25e58ac7e5321d66807e7110ff0fb61bb534f"
|
||||
|
@ -811,6 +860,11 @@ array-equal@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
|
||||
|
||||
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-unique@^0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
|
||||
|
@ -869,6 +923,13 @@ atomic-sleep@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
|
||||
|
||||
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"
|
||||
|
||||
aws-sdk@^2.706.0:
|
||||
version "2.706.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.706.0.tgz#09f65e9a91ecac5a635daf934082abae30eca953"
|
||||
|
@ -1537,6 +1598,26 @@ decompress-response@^3.3.0:
|
|||
dependencies:
|
||||
mimic-response "^1.0.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"
|
||||
is-arguments "^1.0.4"
|
||||
is-date-object "^1.0.2"
|
||||
is-regex "^1.0.5"
|
||||
isarray "^2.0.5"
|
||||
object-is "^1.1.2"
|
||||
object-keys "^1.1.1"
|
||||
object.assign "^4.1.0"
|
||||
regexp.prototype.flags "^1.3.0"
|
||||
side-channel "^1.0.2"
|
||||
which-boxed-primitive "^1.0.1"
|
||||
which-collection "^1.0.1"
|
||||
which-typed-array "^1.1.2"
|
||||
|
||||
deep-equal@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
|
||||
|
@ -1831,7 +1912,7 @@ env-paths@^2.2.0:
|
|||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43"
|
||||
|
||||
errno@~0.1.1:
|
||||
errno@~0.1.1, errno@~0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
|
||||
dependencies:
|
||||
|
@ -1863,6 +1944,54 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
|
|||
string.prototype.trimleft "^2.1.1"
|
||||
string.prototype.trimright "^2.1.1"
|
||||
|
||||
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"
|
||||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
is-callable "^1.2.0"
|
||||
is-regex "^1.1.0"
|
||||
object-inspect "^1.7.0"
|
||||
object-keys "^1.1.1"
|
||||
object.assign "^4.1.0"
|
||||
string.prototype.trimend "^1.0.1"
|
||||
string.prototype.trimstart "^1.0.1"
|
||||
|
||||
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"
|
||||
has "^1.0.3"
|
||||
has-symbols "^1.0.1"
|
||||
is-callable "^1.2.0"
|
||||
is-negative-zero "^2.0.0"
|
||||
is-regex "^1.1.1"
|
||||
object-inspect "^1.8.0"
|
||||
object-keys "^1.1.1"
|
||||
object.assign "^4.1.0"
|
||||
string.prototype.trimend "^1.0.1"
|
||||
string.prototype.trimstart "^1.0.1"
|
||||
|
||||
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"
|
||||
is-arguments "^1.0.4"
|
||||
is-map "^2.0.1"
|
||||
is-set "^2.0.1"
|
||||
is-string "^1.0.5"
|
||||
isarray "^2.0.5"
|
||||
|
||||
es-to-primitive@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
|
||||
|
@ -2753,16 +2882,31 @@ is-accessor-descriptor@^1.0.0:
|
|||
dependencies:
|
||||
kind-of "^6.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"
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
|
||||
dependencies:
|
||||
binary-extensions "^2.0.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"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
|
@ -2771,6 +2915,11 @@ is-callable@^1.1.4, is-callable@^1.1.5:
|
|||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab"
|
||||
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
|
||||
|
@ -2793,7 +2942,7 @@ is-data-descriptor@^1.0.0:
|
|||
dependencies:
|
||||
kind-of "^6.0.0"
|
||||
|
||||
is-date-object@^1.0.1:
|
||||
is-date-object@^1.0.1, is-date-object@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
|
||||
|
||||
|
@ -2856,10 +3005,25 @@ is-installed-globally@^0.3.1:
|
|||
global-dirs "^2.0.1"
|
||||
is-path-inside "^3.0.1"
|
||||
|
||||
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-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-npm@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d"
|
||||
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
|
||||
|
@ -2890,10 +3054,27 @@ is-regex@^1.0.5:
|
|||
dependencies:
|
||||
has "^1.0.3"
|
||||
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
|
||||
|
@ -2908,10 +3089,30 @@ is-type-of@^1.0.0:
|
|||
is-class-hotfix "~0.0.6"
|
||||
isstream "~0.1.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"
|
||||
foreach "^2.0.5"
|
||||
has-symbols "^1.0.1"
|
||||
|
||||
is-typedarray@^1.0.0, is-typedarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
|
||||
|
@ -2932,6 +3133,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
|
||||
|
||||
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==
|
||||
|
||||
isbinaryfile@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b"
|
||||
|
@ -3333,6 +3539,17 @@ jmespath@0.15.0, jmespath@^0.15.0:
|
|||
version "0.15.0"
|
||||
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
|
||||
|
||||
joi@^17.2.1:
|
||||
version "17.2.1"
|
||||
resolved "https://registry.yarnpkg.com/joi/-/joi-17.2.1.tgz#e5140fdf07e8fecf9bc977c2832d1bdb1e3f2a0a"
|
||||
integrity sha512-YT3/4Ln+5YRpacdmfEfrrKh50/kkgX3LgBltjqnlMPIYiZ4hxXZuVJcxmsvxsdeHg9soZfE3qXxHC2tMpCCBOA==
|
||||
dependencies:
|
||||
"@hapi/address" "^4.1.0"
|
||||
"@hapi/formula" "^2.0.0"
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
"@hapi/pinpoint" "^2.0.0"
|
||||
"@hapi/topo" "^5.0.0"
|
||||
|
||||
joycon@^2.2.5:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/joycon/-/joycon-2.2.5.tgz#8d4cf4cbb2544d7b7583c216fcdfec19f6be1615"
|
||||
|
@ -3871,6 +4088,11 @@ ltgt@~2.1.3:
|
|||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.1.3.tgz#10851a06d9964b971178441c23c9e52698eece34"
|
||||
|
||||
lunr@^2.3.5:
|
||||
version "2.3.9"
|
||||
resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1"
|
||||
integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==
|
||||
|
||||
make-dir@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
|
||||
|
@ -4035,6 +4257,11 @@ nan@^2.12.1:
|
|||
version "2.14.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
|
||||
|
||||
nanoid@^2.1.0:
|
||||
version "2.1.11"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
|
||||
integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
||||
|
@ -4182,6 +4409,19 @@ object-inspect@^1.7.0:
|
|||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
|
||||
|
||||
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"
|
||||
|
||||
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
|
||||
|
@ -4828,6 +5068,19 @@ regex-not@^1.0.0, regex-not@^1.0.2:
|
|||
extend-shallow "^3.0.2"
|
||||
safe-regex "^1.1.0"
|
||||
|
||||
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"
|
||||
|
||||
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==
|
||||
|
||||
regexpp@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
|
||||
|
@ -5123,6 +5376,21 @@ shellwords@^0.1.1:
|
|||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
||||
|
||||
shortid@^2.2.8:
|
||||
version "2.2.15"
|
||||
resolved "https://registry.yarnpkg.com/shortid/-/shortid-2.2.15.tgz#2b902eaa93a69b11120373cd42a1f1fe4437c122"
|
||||
integrity sha512-5EaCy2mx2Jgc/Fdn9uuDuNIIfWBpzY4XIlhoqtXF6qsf+/+SGZ+FxDdX/ZsMZiWupIWNqAEmiNY4RC+LSmCeOw==
|
||||
dependencies:
|
||||
nanoid "^2.1.0"
|
||||
|
||||
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"
|
||||
|
||||
signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||
|
@ -5339,7 +5607,7 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
|
|||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
string.prototype.trimend@^1.0.0:
|
||||
string.prototype.trimend@^1.0.0, string.prototype.trimend@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913"
|
||||
dependencies:
|
||||
|
@ -5362,7 +5630,7 @@ string.prototype.trimright@^2.1.1:
|
|||
es-abstract "^1.17.5"
|
||||
string.prototype.trimend "^1.0.0"
|
||||
|
||||
string.prototype.trimstart@^1.0.0:
|
||||
string.prototype.trimstart@^1.0.0, string.prototype.trimstart@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54"
|
||||
dependencies:
|
||||
|
@ -5480,6 +5748,11 @@ supports-color@^7.1.0:
|
|||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
svelte@^3.9.2:
|
||||
version "3.24.1"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.24.1.tgz#aca364937dd1df27fe131e2a4c234acb6061db4b"
|
||||
integrity sha512-OX/IBVUJSFo1rnznXdwf9rv6LReJ3qQ0PwRjj76vfUWyTfbHbR9OXqJBnUrpjyis2dwYcbT2Zm1DFjOOF1ZbbQ==
|
||||
|
||||
symbol-tree@^3.2.2:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
|
@ -5912,10 +6185,43 @@ whatwg-url@^7.0.0:
|
|||
tr46 "^1.0.1"
|
||||
webidl-conversions "^4.0.2"
|
||||
|
||||
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"
|
||||
is-number-object "^1.0.3"
|
||||
is-string "^1.0.4"
|
||||
is-symbol "^1.0.2"
|
||||
|
||||
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"
|
||||
is-weakmap "^2.0.1"
|
||||
is-weakset "^2.0.1"
|
||||
|
||||
which-module@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||
|
||||
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"
|
||||
foreach "^2.0.5"
|
||||
function-bind "^1.1.1"
|
||||
has-symbols "^1.0.1"
|
||||
is-typed-array "^1.1.3"
|
||||
|
||||
which@^1.2.9, which@^1.3.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||
|
@ -5932,6 +6238,13 @@ word-wrap@~1.2.3:
|
|||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||
|
||||
worker-farm@^1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8"
|
||||
integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==
|
||||
dependencies:
|
||||
errno "~0.1.7"
|
||||
|
||||
wrap-ansi@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
|
||||
|
|
Loading…
Reference in New Issue