Merge remote-tracking branch 'origin/develop' into feature/new-app-publish-workflow

This commit is contained in:
Dean 2022-04-19 14:45:46 +01:00
commit c7b6453862
25 changed files with 523 additions and 134 deletions

View File

@ -7,6 +7,15 @@ assignees: ''
--- ---
**Hosting**
<!-- Delete as appropriate -->
- Self
- Method: <method> <!-- One of: k8s, docker single image, docker compose, digital ocean: -->
- Budibase Version: <version> <!-- e.g. 1.0.105 -->
- App Version: <version> <!-- Indicate app version if bug is related to an application -->
- Cloud
- Tenant ID: <tenantId> <!-- shown in URL as <tenantID>.budibase.app -->
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.

View File

@ -114,6 +114,9 @@ spec:
value: {{ .Values.globals.google.clientId | quote }} value: {{ .Values.globals.google.clientId | quote }}
- name: GOOGLE_CLIENT_SECRET - name: GOOGLE_CLIENT_SECRET
value: {{ .Values.globals.google.secret | quote }} value: {{ .Values.globals.google.secret | quote }}
- name: AUTOMATION_MAX_ITERATIONS
value: {{ .Values.globals.automationMaxIterations | quote }}
image: budibase/apps:{{ .Values.globals.appVersion }} image: budibase/apps:{{ .Values.globals.appVersion }}
imagePullPolicy: Always imagePullPolicy: Always
name: bbapps name: bbapps

View File

@ -103,6 +103,7 @@ globals:
google: google:
clientId: "" clientId: ""
secret: "" secret: ""
automationMaxIterations: "500"
createSecrets: true # creates an internal API key, JWT secrets and redis password for you createSecrets: true # creates an internal API key, JWT secrets and redis password for you

View File

@ -1,5 +1,5 @@
{ {
"version": "1.0.105-alpha.19", "version": "1.0.105-alpha.21",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/backend-core", "name": "@budibase/backend-core",
"version": "1.0.105-alpha.19", "version": "1.0.105-alpha.21",
"description": "Budibase backend core libraries used in server and worker", "description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js", "main": "src/index.js",
"author": "Budibase", "author": "Budibase",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/bbui", "name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.", "description": "A UI solution used in the different Budibase projects.",
"version": "1.0.105-alpha.19", "version": "1.0.105-alpha.21",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"module": "dist/bbui.es.js", "module": "dist/bbui.es.js",
@ -38,7 +38,7 @@
], ],
"dependencies": { "dependencies": {
"@adobe/spectrum-css-workflow-icons": "^1.2.1", "@adobe/spectrum-css-workflow-icons": "^1.2.1",
"@budibase/string-templates": "^1.0.105-alpha.19", "@budibase/string-templates": "^1.0.105-alpha.21",
"@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actionbutton": "^1.0.1",
"@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1",
"@spectrum-css/avatar": "^3.0.2", "@spectrum-css/avatar": "^3.0.2",

View File

@ -20,7 +20,6 @@ filterTests(['smoke', 'all'], () => {
}) })
// Setup trigger // Setup trigger
cy.contains("Setup").click()
cy.get(".spectrum-Picker-label").click() cy.get(".spectrum-Picker-label").click()
cy.wait(500) cy.wait(500)
cy.contains("dog").click() cy.contains("dog").click()
@ -32,7 +31,6 @@ filterTests(['smoke', 'all'], () => {
cy.contains("Create Row").trigger('mouseover').click().click() cy.contains("Create Row").trigger('mouseover').click().click()
cy.get(".spectrum-Button--cta").click() cy.get(".spectrum-Button--cta").click()
}) })
cy.contains("Setup").click()
cy.get(".spectrum-Picker-label").eq(1).click() cy.get(".spectrum-Picker-label").eq(1).click()
cy.contains("dog").click() cy.contains("dog").click()
cy.get(".spectrum-Textfield-input") cy.get(".spectrum-Textfield-input")

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "1.0.105-alpha.19", "version": "1.0.105-alpha.21",
"license": "GPL-3.0", "license": "GPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -65,10 +65,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.105-alpha.19", "@budibase/bbui": "^1.0.105-alpha.21",
"@budibase/client": "^1.0.105-alpha.19", "@budibase/client": "^1.0.105-alpha.21",
"@budibase/frontend-core": "^1.0.105-alpha.19", "@budibase/frontend-core": "^1.0.105-alpha.21",
"@budibase/string-templates": "^1.0.105-alpha.19", "@budibase/string-templates": "^1.0.105-alpha.21",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1", "@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1", "@spectrum-css/vars": "^3.0.1",

View File

@ -39,6 +39,7 @@
if (v.internal) { if (v.internal) {
acc[k] = v acc[k] = v
} }
delete acc.LOOP
return acc return acc
}, {}) }, {})

View File

@ -72,7 +72,9 @@
animate:flip={{ duration: 500 }} animate:flip={{ duration: 500 }}
in:fly|local={{ x: 500, duration: 1500 }} in:fly|local={{ x: 500, duration: 1500 }}
> >
{#if block.stepId !== "LOOP"}
<FlowItem {testDataModal} {block} /> <FlowItem {testDataModal} {block} />
{/if}
</div> </div>
{/each} {/each}
</div> </div>

View File

@ -9,8 +9,8 @@
Modal, Modal,
Button, Button,
StatusLight, StatusLight,
ActionButton,
Select, Select,
ActionButton,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte" import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
@ -25,8 +25,8 @@
let webhookModal let webhookModal
let actionModal let actionModal
let resultsModal let resultsModal
let setupToggled
let blockComplete let blockComplete
let showLooping = false
$: rowControl = $automationStore.selectedAutomation.automation.rowControl $: rowControl = $automationStore.selectedAutomation.automation.rowControl
$: showBindingPicker = $: showBindingPicker =
@ -52,8 +52,21 @@
block.schema?.inputs?.properties || {} block.schema?.inputs?.properties || {}
).every(x => block?.inputs[x]) ).every(x => block?.inputs[x])
$: loopingSelected =
$automationStore.selectedAutomation?.automation.definition.steps.find(
x => x.blockToLoop === block.id
)
async function deleteStep() { async function deleteStep() {
let loopBlock =
$automationStore.selectedAutomation?.automation.definition.steps.find(
x => x.blockToLoop === block.id
)
try { try {
if (loopBlock) {
automationStore.actions.deleteAutomationBlock(loopBlock)
}
automationStore.actions.deleteAutomationBlock(block) automationStore.actions.deleteAutomationBlock(block)
await automationStore.actions.save( await automationStore.actions.save(
$automationStore.selectedAutomation?.automation $automationStore.selectedAutomation?.automation
@ -76,6 +89,23 @@
) )
} }
async function addLooping() {
loopingSelected = true
const loopDefinition = $automationStore.blockDefinitions.ACTION.LOOP
const loopBlock = $automationStore.selectedAutomation.constructBlock(
"ACTION",
"LOOP",
loopDefinition
)
loopBlock.blockToLoop = block.id
block.loopBlock = loopBlock.id
automationStore.actions.addBlockToAutomation(loopBlock, blockIdx)
await automationStore.actions.save(
$automationStore.selectedAutomation?.automation
)
}
async function onSelect(block) { async function onSelect(block) {
await automationStore.update(state => { await automationStore.update(state => {
state.selectedBlock = block state.selectedBlock = block
@ -84,13 +114,68 @@
} }
</script> </script>
<div class={`block ${block.type} hoverable`} class:selected on:click={() => {}}>
{#if loopingSelected}
<div class="blockSection">
<div <div
class={`block ${block.type} hoverable`} on:click={() => {
class:selected showLooping = !showLooping
}}
class="splitHeader"
>
<div class="center-items">
<svg
width="28px"
height="28px"
class="spectrum-Icon"
style="color:grey;"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Reuse" />
</svg>
<div class="iconAlign">
<Detail size="S">Looping</Detail>
</div>
</div>
<div class="blockTitle">
<div
style="margin-left: 10px;"
on:click={() => { on:click={() => {
onSelect(block) onSelect(block)
}} }}
> >
<Icon name={showLooping ? "ChevronDown" : "ChevronUp"} />
</div>
</div>
</div>
</div>
<Divider noMargin />
{#if !showLooping}
<div class="blockSection">
<div class="block-options">
<div class="delete-padding" on:click={() => deleteStep()}>
<Icon name="DeleteOutline" />
</div>
</div>
<Layout noPadding gap="S">
<AutomationBlockSetup
schemaProperties={Object.entries(
$automationStore.blockDefinitions.ACTION.LOOP.schema.inputs
.properties
)}
block={$automationStore.selectedAutomation?.automation.definition.steps.find(
x => x.blockToLoop === block.id
)}
{webhookModal}
/>
</Layout>
</div>
<Divider noMargin />
{/if}
{/if}
<div class="blockSection"> <div class="blockSection">
<div <div
on:click={() => { on:click={() => {
@ -127,55 +212,57 @@
<Detail size="S">{block?.name?.toUpperCase() || ""}</Detail> <Detail size="S">{block?.name?.toUpperCase() || ""}</Detail>
</div> </div>
</div> </div>
<div class="blockTitle">
{#if testResult && testResult[0]} {#if testResult && testResult[0]}
<span on:click={() => resultsModal.show()}> <div style="float: right;" on:click={() => resultsModal.show()}>
<StatusLight <StatusLight
positive={isTrigger || testResult[0].outputs?.success} positive={isTrigger || testResult[0].outputs?.success}
negative={!testResult[0].outputs?.success} negative={!testResult[0].outputs?.success}
><Body size="XS">View response</Body></StatusLight ><Body size="XS">View response</Body></StatusLight
> >
</span> </div>
{/if} {/if}
<div
style="margin-left: 10px;"
on:click={() => {
onSelect(block)
}}
>
<Icon name={blockComplete ? "ChevronDown" : "ChevronUp"} />
</div>
</div>
</div> </div>
</div> </div>
{#if !blockComplete} {#if !blockComplete}
<Divider noMargin /> <Divider noMargin />
<div class="blockSection"> <div class="blockSection">
<Layout noPadding gap="S"> <Layout noPadding gap="S">
<div class="splitHeader">
<ActionButton
on:click={() => {
onSelect(block)
setupToggled = !setupToggled
}}
quiet
icon={setupToggled ? "ChevronDown" : "ChevronRight"}
>
<Detail size="S">Setup</Detail>
</ActionButton>
{#if !isTrigger} {#if !isTrigger}
<div class="block-options">
{#if showBindingPicker}
<div> <div>
<div class="block-options">
{#if !loopingSelected}
<ActionButton on:click={() => addLooping()} icon="Reuse"
>Add Looping</ActionButton
>
{/if}
{#if showBindingPicker}
<Select <Select
on:change={toggleFieldControl} on:change={toggleFieldControl}
quiet
defaultValue="Use values" defaultValue="Use values"
autoWidth autoWidth
value={rowControl ? "Use bindings" : "Use values"} value={rowControl ? "Use bindings" : "Use values"}
options={["Use values", "Use bindings"]} options={["Use values", "Use bindings"]}
placeholder={null} placeholder={null}
/> />
</div>
{/if} {/if}
<div class="delete-padding" on:click={() => deleteStep()}> <ActionButton
<Icon name="DeleteOutline" /> on:click={() => deleteStep()}
icon="DeleteOutline"
/>
</div> </div>
</div> </div>
{/if} {/if}
</div>
{#if setupToggled}
<AutomationBlockSetup <AutomationBlockSetup
schemaProperties={Object.entries(block.schema.inputs.properties)} schemaProperties={Object.entries(block.schema.inputs.properties)}
{block} {block}
@ -186,7 +273,6 @@
>Finish and test automation</Button >Finish and test automation</Button
> >
{/if} {/if}
{/if}
</Layout> </Layout>
</div> </div>
{/if} {/if}
@ -220,8 +306,10 @@
padding-left: 30px; padding-left: 30px;
} }
.block-options { .block-options {
display: flex; justify-content: flex-end;
align-items: center; align-items: center;
display: flex;
gap: var(--spacing-m);
} }
.center-items { .center-items {
display: flex; display: flex;
@ -256,4 +344,9 @@
/* center horizontally */ /* center horizontally */
align-self: center; align-self: center;
} }
.blockTitle {
display: flex;
align-items: center;
}
</style> </style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { ModalContent, Icon, Detail, TextArea } from "@budibase/bbui" import { ModalContent, Icon, Detail, TextArea, Label } from "@budibase/bbui"
export let testResult export let testResult
export let isTrigger export let isTrigger
@ -13,7 +13,7 @@
cancelText="Close" cancelText="Close"
> >
<div slot="header" class="result-modal-header"> <div slot="header" class="result-modal-header">
<span>Test Automation</span> <span>Test Results</span>
<div> <div>
{#if isTrigger || testResult[0].outputs.success} {#if isTrigger || testResult[0].outputs.success}
<div class="iconSuccess"> <div class="iconSuccess">
@ -26,7 +26,18 @@
{/if} {/if}
</div> </div>
</div> </div>
<span>
{#if testResult[0].outputs.iterations}
<div style="display: flex;">
<Icon name="Reuse" />
<div style="margin-left: 10px;">
<Label>
This loop ran {testResult[0].outputs.iterations} times.</Label
>
</div>
</div>
{/if}
</span>
<div <div
on:click={() => { on:click={() => {
inputToggled = !inputToggled inputToggled = !inputToggled

View File

@ -88,36 +88,65 @@
if (!block || !automation) { if (!block || !automation) {
return [] return []
} }
// Find previous steps to the selected one // Find previous steps to the selected one
let allSteps = [...automation.steps] let allSteps = [...automation.steps]
if (automation.trigger) { if (automation.trigger) {
allSteps = [automation.trigger, ...allSteps] allSteps = [automation.trigger, ...allSteps]
} }
const blockIdx = allSteps.findIndex(step => step.id === block.id) let blockIdx = allSteps.findIndex(step => step.id === block.id)
// Extract all outputs from all previous steps as available bindings // Extract all outputs from all previous steps as available bindins
let bindings = [] let bindings = []
for (let idx = 0; idx < blockIdx; idx++) { for (let idx = 0; idx < blockIdx; idx++) {
const outputs = Object.entries( let wasLoopBlock = allSteps[idx]?.stepId === "LOOP"
allSteps[idx].schema?.outputs?.properties ?? {} let isLoopBlock =
) allSteps[idx]?.stepId === "LOOP" &&
allSteps.find(x => x.blockToLoop === block.id)
// If the previous block was a loop block, decerement the index so the following
// steps are in the correct order
if (wasLoopBlock) {
blockIdx--
}
let schema = allSteps[idx]?.schema?.outputs?.properties ?? {}
// If its a Loop Block, we need to add this custom schema
if (isLoopBlock) {
schema = {
currentItem: {
type: "string",
description: "the item currently being executed",
},
}
}
const outputs = Object.entries(schema)
bindings = bindings.concat( bindings = bindings.concat(
outputs.map(([name, value]) => { outputs.map(([name, value]) => {
const stepsLabel = block.name.startsWith("JS") let runtimeName = isLoopBlock
? `loop.${name}`
: block.name.startsWith("JS")
? `steps[${idx}].${name}` ? `steps[${idx}].${name}`
: `steps.${idx}.${name}` : `steps.${idx}.${name}`
const runtime = idx === 0 ? `trigger.${name}` : stepsLabel const runtime = idx === 0 ? `trigger.${name}` : runtimeName
return { return {
label: runtime, label: runtime,
type: value.type, type: value.type,
description: value.description, description: value.description,
category: idx === 0 ? "Trigger outputs" : `Step ${idx} outputs`, category:
idx === 0
? "Trigger outputs"
: isLoopBlock
? "Loop Outputs"
: `Step ${idx} outputs`,
path: runtime, path: runtime,
} }
}) })
) )
} }
return bindings return bindings
} }
@ -264,6 +293,14 @@
value={inputData[key]} value={inputData[key]}
/> />
</CodeEditorModal> </CodeEditorModal>
{:else if value.customType === "loopOption"}
<Select
on:change={e => onChange(e, key)}
autoWidth
value={inputData[key]}
options={["Array", "String"]}
defaultValue={"Array"}
/>
{:else if value.type === "string" || value.type === "number" || value.type === "integer"} {:else if value.type === "string" || value.type === "number" || value.type === "integer"}
{#if isTestModal} {#if isTestModal}
<ModalBindableInput <ModalBindableInput

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/cli", "name": "@budibase/cli",
"version": "1.0.105-alpha.19", "version": "1.0.105-alpha.21",
"description": "Budibase CLI, for developers, self hosting and migrations.", "description": "Budibase CLI, for developers, self hosting and migrations.",
"main": "src/index.js", "main": "src/index.js",
"bin": { "bin": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/client", "name": "@budibase/client",
"version": "1.0.105-alpha.19", "version": "1.0.105-alpha.21",
"license": "MPL-2.0", "license": "MPL-2.0",
"module": "dist/budibase-client.js", "module": "dist/budibase-client.js",
"main": "dist/budibase-client.js", "main": "dist/budibase-client.js",
@ -19,9 +19,9 @@
"dev:builder": "rollup -cw" "dev:builder": "rollup -cw"
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.105-alpha.19", "@budibase/bbui": "^1.0.105-alpha.21",
"@budibase/frontend-core": "^1.0.105-alpha.19", "@budibase/frontend-core": "^1.0.105-alpha.21",
"@budibase/string-templates": "^1.0.105-alpha.19", "@budibase/string-templates": "^1.0.105-alpha.21",
"@spectrum-css/button": "^3.0.3", "@spectrum-css/button": "^3.0.3",
"@spectrum-css/card": "^3.0.3", "@spectrum-css/card": "^3.0.3",
"@spectrum-css/divider": "^1.0.3", "@spectrum-css/divider": "^1.0.3",

View File

@ -1,12 +1,12 @@
{ {
"name": "@budibase/frontend-core", "name": "@budibase/frontend-core",
"version": "1.0.105-alpha.19", "version": "1.0.105-alpha.21",
"description": "Budibase frontend core libraries used in builder and client", "description": "Budibase frontend core libraries used in builder and client",
"author": "Budibase", "author": "Budibase",
"license": "MPL-2.0", "license": "MPL-2.0",
"svelte": "src/index.js", "svelte": "src/index.js",
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.0.105-alpha.19", "@budibase/bbui": "^1.0.105-alpha.21",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"svelte": "^3.46.2" "svelte": "^3.46.2"
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/server", "name": "@budibase/server",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.105-alpha.19", "version": "1.0.105-alpha.21",
"description": "Budibase Web Server", "description": "Budibase Web Server",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -68,9 +68,9 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.0.3", "@apidevtools/swagger-parser": "^10.0.3",
"@budibase/backend-core": "^1.0.105-alpha.19", "@budibase/backend-core": "^1.0.105-alpha.21",
"@budibase/client": "^1.0.105-alpha.19", "@budibase/client": "^1.0.105-alpha.21",
"@budibase/string-templates": "^1.0.105-alpha.19", "@budibase/string-templates": "^1.0.105-alpha.21",
"@bull-board/api": "^3.7.0", "@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0", "@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0", "@elastic/elasticsearch": "7.10.0",

View File

@ -13,6 +13,7 @@ const integromat = require("./steps/integromat")
let filter = require("./steps/filter") let filter = require("./steps/filter")
let delay = require("./steps/delay") let delay = require("./steps/delay")
let queryRow = require("./steps/queryRows") let queryRow = require("./steps/queryRows")
let loop = require("./steps/loop")
const env = require("../environment") const env = require("../environment")
const ACTION_IMPLS = { const ACTION_IMPLS = {
@ -27,6 +28,7 @@ const ACTION_IMPLS = {
DELAY: delay.run, DELAY: delay.run,
FILTER: filter.run, FILTER: filter.run,
QUERY_ROWS: queryRow.run, QUERY_ROWS: queryRow.run,
LOOP: loop.run,
// these used to be lowercase step IDs, maintain for backwards compat // these used to be lowercase step IDs, maintain for backwards compat
discord: discord.run, discord: discord.run,
slack: slack.run, slack: slack.run,
@ -45,6 +47,7 @@ const ACTION_DEFINITIONS = {
DELAY: delay.definition, DELAY: delay.definition,
FILTER: filter.definition, FILTER: filter.definition,
QUERY_ROWS: queryRow.definition, QUERY_ROWS: queryRow.definition,
LOOP: loop.definition,
// these used to be lowercase step IDs, maintain for backwards compat // these used to be lowercase step IDs, maintain for backwards compat
discord: discord.definition, discord: discord.definition,
slack: slack.definition, slack: slack.definition,

View File

@ -1,4 +1,5 @@
const { getTable } = require("../api/controllers/table/utils") const { getTable } = require("../api/controllers/table/utils")
const { findHBSBlocks } = require("@budibase/string-templates")
/** /**
* When values are input to the system generally they will be of type string as this is required for template strings. * When values are input to the system generally they will be of type string as this is required for template strings.
@ -74,3 +75,14 @@ exports.getError = err => {
} }
return typeof err !== "string" ? err.toString() : err return typeof err !== "string" ? err.toString() : err
} }
exports.substituteLoopStep = (hbsString, substitute) => {
let blocks = findHBSBlocks(hbsString)
for (let block of blocks) {
let oldBlock = block
block = block.replace(/loop/, substitute)
hbsString = hbsString.replace(new RegExp(oldBlock, "g"), block)
}
return hbsString
}

View File

@ -0,0 +1,50 @@
exports.definition = {
name: "Looping",
icon: "Reuse",
tagline: "Loop the block",
description: "Loop",
stepId: "LOOP",
internal: true,
inputs: {},
schema: {
inputs: {
properties: {
option: {
customType: "loopOption",
title: "Input type",
},
binding: {
type: "string",
title: "Binding / Value",
},
iterations: {
type: "number",
title: "Max loop iterations",
},
failure: {
type: "string",
title: "Failure Condition",
},
},
required: ["type", "value", "iterations", "failure"],
},
outputs: {
properties: {
items: {
customType: "item",
description: "The item currently being executed",
},
success: {
type: "boolean",
description: "Whether the message loop was successfully",
},
iterations: {
type: "number",
descriptions: "The amount of times the block ran",
},
},
required: ["success", "items", "iterations"],
},
},
type: "LOGIC",
}

View File

@ -190,5 +190,11 @@ exports.WebhookType = {
AUTOMATION: "automation", AUTOMATION: "automation",
} }
exports.AutomationErrors = {
INCORRECT_TYPE: "INCORRECT_TYPE",
MAX_ITERATIONS: "MAX_ITERATIONS_REACHED",
FAILURE_CONDITION: "FAILURE_CONDITION_MET",
}
// pass through the list from the auth/core lib // pass through the list from the auth/core lib
exports.ObjectStoreBuckets = ObjectStoreBuckets exports.ObjectStoreBuckets = ObjectStoreBuckets

View File

@ -59,6 +59,7 @@ module.exports = {
LOG_LEVEL: process.env.LOG_LEVEL, LOG_LEVEL: process.env.LOG_LEVEL,
AUTOMATION_DIRECTORY: process.env.AUTOMATION_DIRECTORY, AUTOMATION_DIRECTORY: process.env.AUTOMATION_DIRECTORY,
AUTOMATION_BUCKET: process.env.AUTOMATION_BUCKET, AUTOMATION_BUCKET: process.env.AUTOMATION_BUCKET,
AUTOMATION_MAX_ITERATIONS: process.env.AUTOMATION_MAX_ITERATIONS,
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY, SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
DYNAMO_ENDPOINT: process.env.DYNAMO_ENDPOINT, DYNAMO_ENDPOINT: process.env.DYNAMO_ENDPOINT,
POSTHOG_TOKEN: process.env.POSTHOG_TOKEN, POSTHOG_TOKEN: process.env.POSTHOG_TOKEN,

View File

@ -8,10 +8,14 @@ const { DocumentTypes } = require("../db/utils")
const { doInTenant } = require("@budibase/backend-core/tenancy") const { doInTenant } = require("@budibase/backend-core/tenancy")
const { definitions: triggerDefs } = require("../automations/triggerInfo") const { definitions: triggerDefs } = require("../automations/triggerInfo")
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context") const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
const { AutomationErrors } = require("../constants")
const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId
const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId
const CRON_STEP_ID = triggerDefs.CRON.stepId const CRON_STEP_ID = triggerDefs.CRON.stepId
const STOPPED_STATUS = { success: false, status: "STOPPED" } const STOPPED_STATUS = { success: false, status: "STOPPED" }
const { cloneDeep } = require("lodash/fp")
const env = require("../environment")
/** /**
* The automation orchestrator is a class responsible for executing automations. * The automation orchestrator is a class responsible for executing automations.
@ -74,34 +78,156 @@ class Orchestrator {
this.executionOutput.steps.push(stepObj) this.executionOutput.steps.push(stepObj)
} }
updateContextAndOutput(loopStepNumber, step, output, result) {
this.executionOutput.steps.splice(loopStepNumber, 0, {
id: step.id,
stepId: step.stepId,
outputs: {
...output,
success: result.success,
status: result.status,
},
inputs: step.inputs,
})
this._context.steps.splice(loopStepNumber, 0, {
...output,
success: result.success,
status: result.status,
})
}
async execute() { async execute() {
let automation = this._automation let automation = this._automation
const app = await this.getApp() const app = await this.getApp()
let stopped = false let stopped = false
let loopStep
let stepCount = 0
let loopStepNumber
let loopSteps = []
for (let step of automation.definition.steps) { for (let step of automation.definition.steps) {
stepCount++
let input
if (step.stepId === LOOP_STEP_ID) {
loopStep = step
loopStepNumber = stepCount
continue
}
if (loopStep) {
input = await processObject(loopStep.inputs, this._context)
}
let iterations = loopStep ? input.binding.length : 1
let iterationCount = 0
for (let index = 0; index < iterations; index++) {
let originalStepInput = cloneDeep(step.inputs)
// Handle if the user has set a max iteration count or if it reaches the max limit set by us
if (loopStep) {
// lets first of all handle the input
// if the input is array then use it, if it is a string then split it on every new line
let newInput = await processObject(
loopStep.inputs,
cloneDeep(this._context)
)
newInput = automationUtils.cleanInputValues(
newInput,
loopStep.schema.inputs
)
this._context.steps[loopStepNumber] = {
currentItem: newInput.binding[index],
}
let tempOutput = { items: loopSteps, iterations: iterationCount }
if (
(loopStep.inputs.option === "Array" &&
!Array.isArray(newInput.binding)) ||
(loopStep.inputs.option === "String" &&
typeof newInput.binding !== "string")
) {
this.updateContextAndOutput(loopStepNumber, step, tempOutput, {
status: AutomationErrors.INCORRECT_TYPE,
success: false,
})
loopSteps = null
loopStep = null
break
}
// The "Loop" binding in the front end is "fake", so replace it here so the context can understand it
// Pretty hacky because we need to account for the row object
for (let [key, value] of Object.entries(originalStepInput)) {
if (typeof value === "object") {
for (let [innerKey, innerValue] of Object.entries(
originalStepInput[key]
)) {
if (typeof innerValue === "string") {
originalStepInput[key][innerKey] =
automationUtils.substituteLoopStep(
innerValue,
`steps.${loopStepNumber}`
)
}
}
} else {
if (typeof value === "string") {
originalStepInput[key] = automationUtils.substituteLoopStep(
value,
`steps.${loopStepNumber}`
)
}
}
}
if (
index === parseInt(env.AUTOMATION_MAX_ITERATIONS) ||
index === loopStep.inputs.iterations
) {
this.updateContextAndOutput(loopStepNumber, step, tempOutput, {
status: AutomationErrors.MAX_ITERATIONS,
success: true,
})
loopSteps = null
loopStep = null
break
}
if (
this._context.steps[loopStepNumber]?.currentItem ===
loopStep.inputs.failure
) {
this.updateContextAndOutput(loopStepNumber, step, tempOutput, {
status: AutomationErrors.FAILURE_CONDITION,
success: false,
})
loopSteps = null
loopStep = null
break
}
}
// execution stopped, record state for that // execution stopped, record state for that
if (stopped) { if (stopped) {
this.updateExecutionOutput(step.id, step.stepId, {}, STOPPED_STATUS) this.updateExecutionOutput(step.id, step.stepId, {}, STOPPED_STATUS)
continue continue
} }
// If it's a loop step, we need to manually add the bindings to the context
let stepFn = await this.getStepFunctionality(step.stepId) let stepFn = await this.getStepFunctionality(step.stepId)
step.inputs = await processObject(step.inputs, this._context) let inputs = await processObject(originalStepInput, this._context)
step.inputs = automationUtils.cleanInputValues( inputs = automationUtils.cleanInputValues(inputs, step.schema.inputs)
step.inputs,
step.schema.inputs
)
// appId is always passed
try { try {
// appId is always passed
let tenantId = app.tenantId || DEFAULT_TENANT_ID let tenantId = app.tenantId || DEFAULT_TENANT_ID
const outputs = await doInTenant(tenantId, () => { const outputs = await doInTenant(tenantId, () => {
return stepFn({ return stepFn({
inputs: step.inputs, inputs: inputs,
appId: this._appId, appId: this._appId,
emitter: this._emitter, emitter: this._emitter,
context: this._context, context: this._context,
}) })
}) })
this._context.steps.push(outputs) this._context.steps[stepCount] = outputs
// if filter causes us to stop execution don't break the loop, set a var // if filter causes us to stop execution don't break the loop, set a var
// so that we can finish iterating through the steps and record that it stopped // so that we can finish iterating through the steps and record that it stopped
if (step.stepId === FILTER_STEP_ID && !outputs.success) { if (step.stepId === FILTER_STEP_ID && !outputs.success) {
@ -112,12 +238,48 @@ class Orchestrator {
}) })
continue continue
} }
this.updateExecutionOutput(step.id, step.stepId, step.inputs, outputs) if (loopStep && loopSteps) {
loopSteps.push(outputs)
} else {
this.updateExecutionOutput(
step.id,
step.stepId,
step.inputs,
outputs
)
}
} catch (err) { } catch (err) {
console.error(`Automation error - ${step.stepId} - ${err}`) console.error(`Automation error - ${step.stepId} - ${err}`)
return err return err
} }
if (loopStep) {
iterationCount++
if (index === iterations - 1) {
loopStep = null
this._context.steps.splice(loopStepNumber, 1)
break
} }
}
}
if (loopSteps && loopSteps.length) {
let tempOutput = {
success: true,
items: loopSteps,
iterations: iterationCount,
}
this.executionOutput.steps.splice(loopStepNumber + 1, 0, {
id: step.id,
stepId: step.stepId,
outputs: tempOutput,
inputs: step.inputs,
})
this._context.steps.splice(loopStepNumber, 0, tempOutput)
loopSteps = null
}
}
return this.executionOutput return this.executionOutput
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@budibase/string-templates", "name": "@budibase/string-templates",
"version": "1.0.105-alpha.19", "version": "1.0.105-alpha.21",
"description": "Handlebars wrapper for Budibase templating.", "description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs", "main": "src/index.cjs",
"module": "dist/bundle.mjs", "module": "dist/bundle.mjs",

View File

@ -1,7 +1,7 @@
{ {
"name": "@budibase/worker", "name": "@budibase/worker",
"email": "hi@budibase.com", "email": "hi@budibase.com",
"version": "1.0.105-alpha.19", "version": "1.0.105-alpha.21",
"description": "Budibase background service", "description": "Budibase background service",
"main": "src/index.ts", "main": "src/index.ts",
"repository": { "repository": {
@ -31,8 +31,8 @@
"author": "Budibase", "author": "Budibase",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"@budibase/backend-core": "^1.0.105-alpha.19", "@budibase/backend-core": "^1.0.105-alpha.21",
"@budibase/string-templates": "^1.0.105-alpha.19", "@budibase/string-templates": "^1.0.105-alpha.21",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@sentry/node": "^6.0.0", "@sentry/node": "^6.0.0",
"@techpass/passport-openidconnect": "^0.3.0", "@techpass/passport-openidconnect": "^0.3.0",