Merge pull request #10568 from Budibase/feature/sync-automations
Synchronous automation for App actions and webhook
This commit is contained in:
commit
70dcbbd292
|
@ -90,6 +90,10 @@ export const useScimIntegration = () => {
|
|||
return useFeature(Feature.SCIM)
|
||||
}
|
||||
|
||||
export const useSyncAutomations = () => {
|
||||
return useFeature(Feature.SYNC_AUTOMATIONS)
|
||||
}
|
||||
|
||||
// QUOTAS
|
||||
|
||||
export const setAutomationLogsQuota = (value: number) => {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ActionStepID } from "constants/backend/automations"
|
||||
import { TableNames } from "../constants"
|
||||
import {
|
||||
AUTO_COLUMN_DISPLAY_NAMES,
|
||||
|
@ -53,3 +54,9 @@ export function buildAutoColumn(tableName, name, subtype) {
|
|||
}
|
||||
return base
|
||||
}
|
||||
|
||||
export function checkForCollectStep(automation) {
|
||||
return automation.definition.steps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,24 +6,48 @@
|
|||
Body,
|
||||
Icon,
|
||||
notifications,
|
||||
Tags,
|
||||
Tag,
|
||||
} from "@budibase/bbui"
|
||||
import { automationStore } from "builderStore"
|
||||
import { admin } from "stores/portal"
|
||||
import { automationStore, selectedAutomation } from "builderStore"
|
||||
import { admin, licensing } from "stores/portal"
|
||||
import { externalActions } from "./ExternalActions"
|
||||
import { TriggerStepID } from "constants/backend/automations"
|
||||
import { checkForCollectStep } from "builderStore/utils"
|
||||
|
||||
export let blockIdx
|
||||
export let lastStep
|
||||
|
||||
const disabled = {
|
||||
SEND_EMAIL_SMTP: {
|
||||
disabled: !$admin.checklist.smtp.checked,
|
||||
message: "Please configure SMTP",
|
||||
},
|
||||
}
|
||||
|
||||
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
|
||||
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
|
||||
let selectedAction
|
||||
let actionVal
|
||||
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
|
||||
|
||||
$: collectBlockExists = checkForCollectStep($selectedAutomation)
|
||||
|
||||
const disabled = () => {
|
||||
return {
|
||||
SEND_EMAIL_SMTP: {
|
||||
disabled: !$admin.checklist.smtp.checked,
|
||||
message: "Please configure SMTP",
|
||||
},
|
||||
COLLECT: {
|
||||
disabled: !lastStep || !syncAutomationsEnabled || collectBlockExists,
|
||||
message: collectDisabledMessage(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const collectDisabledMessage = () => {
|
||||
if (collectBlockExists) {
|
||||
return "Only one Collect step allowed"
|
||||
}
|
||||
if (!lastStep) {
|
||||
return "Only available as the last step"
|
||||
}
|
||||
}
|
||||
|
||||
const external = actions.reduce((acc, elm) => {
|
||||
const [k, v] = elm
|
||||
if (!v.internal && !v.custom) {
|
||||
|
@ -38,6 +62,15 @@
|
|||
acc[k] = v
|
||||
}
|
||||
delete acc.LOOP
|
||||
|
||||
// Filter out Collect block if not App Action or Webhook
|
||||
if (
|
||||
!collectBlockAllowedSteps.includes(
|
||||
$selectedAutomation.definition.trigger.stepId
|
||||
)
|
||||
) {
|
||||
delete acc.COLLECT
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
|
@ -48,7 +81,6 @@
|
|||
}
|
||||
return acc
|
||||
}, {})
|
||||
console.log(plugins)
|
||||
|
||||
const selectAction = action => {
|
||||
actionVal = action
|
||||
|
@ -72,7 +104,7 @@
|
|||
<ModalContent
|
||||
title="Add automation step"
|
||||
confirmText="Save"
|
||||
size="M"
|
||||
size="L"
|
||||
disabled={!selectedAction}
|
||||
onConfirm={addBlockToAutomation}
|
||||
>
|
||||
|
@ -107,7 +139,7 @@
|
|||
<Detail size="S">Actions</Detail>
|
||||
<div class="item-list">
|
||||
{#each Object.entries(internal) as [idx, action]}
|
||||
{@const isDisabled = disabled[idx] && disabled[idx].disabled}
|
||||
{@const isDisabled = disabled()[idx] && disabled()[idx].disabled}
|
||||
<div
|
||||
class="item"
|
||||
class:disabled={isDisabled}
|
||||
|
@ -117,8 +149,14 @@
|
|||
<div class="item-body">
|
||||
<Icon name={action.icon} />
|
||||
<Body size="XS">{action.name}</Body>
|
||||
{#if isDisabled}
|
||||
<Icon name="Help" tooltip={disabled[idx].message} />
|
||||
{#if isDisabled && !syncAutomationsEnabled}
|
||||
<div class="tag-color">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">Business</Tag>
|
||||
</Tags>
|
||||
</div>
|
||||
{:else if isDisabled}
|
||||
<Icon name="Help" tooltip={disabled()[idx].message} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -152,6 +190,7 @@
|
|||
display: flex;
|
||||
margin-left: var(--spacing-m);
|
||||
gap: var(--spacing-m);
|
||||
align-items: center;
|
||||
}
|
||||
.item-list {
|
||||
display: grid;
|
||||
|
@ -181,4 +220,8 @@
|
|||
.disabled :global(.spectrum-Body) {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.tag-color :global(.spectrum-Tags-item) {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -17,7 +17,11 @@
|
|||
import ActionModal from "./ActionModal.svelte"
|
||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||
import { ActionStepID, TriggerStepID } from "constants/backend/automations"
|
||||
import {
|
||||
ActionStepID,
|
||||
TriggerStepID,
|
||||
Features,
|
||||
} from "constants/backend/automations"
|
||||
import { permissions } from "stores/backend"
|
||||
|
||||
export let block
|
||||
|
@ -31,6 +35,9 @@
|
|||
let showLooping = false
|
||||
let role
|
||||
|
||||
$: collectBlockExists = $selectedAutomation.definition.steps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
$: automationId = $selectedAutomation?._id
|
||||
$: showBindingPicker =
|
||||
block.stepId === ActionStepID.CREATE_ROW ||
|
||||
|
@ -184,7 +191,7 @@
|
|||
{#if !isTrigger}
|
||||
<div>
|
||||
<div class="block-options">
|
||||
{#if !loopBlock}
|
||||
{#if block?.features?.[Features.LOOPING] || !block.features}
|
||||
<ActionButton on:click={() => addLooping()} icon="Reuse">
|
||||
Add Looping
|
||||
</ActionButton>
|
||||
|
@ -224,21 +231,28 @@
|
|||
</Layout>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal bind:this={actionModal} width="30%">
|
||||
<ActionModal {blockIdx} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={webhookModal} width="30%">
|
||||
<CreateWebhookModal />
|
||||
</Modal>
|
||||
</div>
|
||||
<div class="separator" />
|
||||
<Icon on:click={() => actionModal.show()} hoverable name="AddCircle" size="S" />
|
||||
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
|
||||
{#if !collectBlockExists || !lastStep}
|
||||
<div class="separator" />
|
||||
<Icon
|
||||
on:click={() => actionModal.show()}
|
||||
hoverable
|
||||
name="AddCircle"
|
||||
size="S"
|
||||
/>
|
||||
{#if isTrigger ? totalBlocks > 1 : blockIdx !== totalBlocks - 2}
|
||||
<div class="separator" />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<Modal bind:this={actionModal} width="30%">
|
||||
<ActionModal {lastStep} {blockIdx} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={webhookModal} width="30%">
|
||||
<CreateWebhookModal />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.delete-padding {
|
||||
padding-left: 30px;
|
||||
|
|
|
@ -126,8 +126,7 @@
|
|||
}
|
||||
|
||||
const getAllBindings = (bindings, eventContextBindings, actions) => {
|
||||
let allBindings = eventContextBindings.concat(bindings)
|
||||
|
||||
let allBindings = []
|
||||
if (!actions) {
|
||||
return []
|
||||
}
|
||||
|
@ -145,14 +144,35 @@
|
|||
.forEach(action => {
|
||||
// Check we have a binding for this action, and generate one if not
|
||||
const stateBinding = makeStateBinding(action.parameters.key)
|
||||
const hasKey = allBindings.some(binding => {
|
||||
const hasKey = bindings.some(binding => {
|
||||
return binding.runtimeBinding === stateBinding.runtimeBinding
|
||||
})
|
||||
if (!hasKey) {
|
||||
allBindings.push(stateBinding)
|
||||
bindings.push(stateBinding)
|
||||
}
|
||||
})
|
||||
// Get which indexes are asynchronous automations as we want to filter them out from the bindings
|
||||
const asynchronousAutomationIndexes = actions
|
||||
.map((action, index) => {
|
||||
if (
|
||||
action[EVENT_TYPE_KEY] === "Trigger Automation" &&
|
||||
!action.parameters?.synchronous
|
||||
) {
|
||||
return index
|
||||
}
|
||||
})
|
||||
.filter(index => index !== undefined)
|
||||
|
||||
// Based on the above, filter out the asynchronous automations from the bindings
|
||||
if (asynchronousAutomationIndexes) {
|
||||
allBindings = eventContextBindings
|
||||
.filter((binding, index) => {
|
||||
return !asynchronousAutomationIndexes.includes(index)
|
||||
})
|
||||
.concat(bindings)
|
||||
} else {
|
||||
allBindings = eventContextBindings.concat(bindings)
|
||||
}
|
||||
return allBindings
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import { Select, Label, Input, Checkbox } from "@budibase/bbui"
|
||||
import { Select, Label, Input, Checkbox, Icon } from "@budibase/bbui"
|
||||
import { automationStore } from "builderStore"
|
||||
import SaveFields from "./SaveFields.svelte"
|
||||
import { TriggerStepID } from "constants/backend/automations"
|
||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
|
||||
export let parameters = {}
|
||||
export let bindings = []
|
||||
|
@ -16,6 +16,14 @@
|
|||
? AUTOMATION_STATUS.EXISTING
|
||||
: AUTOMATION_STATUS.NEW
|
||||
|
||||
$: {
|
||||
if (automationStatus === AUTOMATION_STATUS.NEW) {
|
||||
parameters.synchronous = false
|
||||
}
|
||||
parameters.synchronous = automations.find(
|
||||
automation => automation._id === parameters.automationId
|
||||
)?.synchronous
|
||||
}
|
||||
$: automations = $automationStore.automations
|
||||
.filter(a => a.definition.trigger?.stepId === TriggerStepID.APP)
|
||||
.map(automation => {
|
||||
|
@ -23,10 +31,15 @@
|
|||
automation.definition.trigger.inputs.fields || {}
|
||||
).map(([name, type]) => ({ name, type }))
|
||||
|
||||
let hasCollectBlock = automation.definition.steps.some(
|
||||
step => step.stepId === ActionStepID.COLLECT
|
||||
)
|
||||
|
||||
return {
|
||||
name: automation.name,
|
||||
_id: automation._id,
|
||||
schema,
|
||||
synchronous: hasCollectBlock,
|
||||
}
|
||||
})
|
||||
$: hasAutomations = automations && automations.length > 0
|
||||
|
@ -35,6 +48,8 @@
|
|||
)
|
||||
$: selectedSchema = selectedAutomation?.schema
|
||||
|
||||
$: error = parameters.timeout > 120 ? "Timeout must be less than 120s" : null
|
||||
|
||||
const onFieldsChanged = e => {
|
||||
parameters.fields = Object.entries(e.detail || {}).reduce(
|
||||
(acc, [key, value]) => {
|
||||
|
@ -57,6 +72,14 @@
|
|||
parameters.fields = {}
|
||||
parameters.automationId = automations[0]?._id
|
||||
}
|
||||
|
||||
const onChange = value => {
|
||||
let automationId = value.detail
|
||||
parameters.synchronous = automations.find(
|
||||
automation => automation._id === automationId
|
||||
)?.synchronous
|
||||
parameters.automationId = automationId
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
|
@ -85,6 +108,7 @@
|
|||
|
||||
{#if automationStatus === AUTOMATION_STATUS.EXISTING}
|
||||
<Select
|
||||
on:change={onChange}
|
||||
bind:value={parameters.automationId}
|
||||
placeholder="Choose automation"
|
||||
options={automations}
|
||||
|
@ -98,6 +122,29 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
{#if parameters.synchronous}
|
||||
<Label small />
|
||||
|
||||
<div class="synchronous-info">
|
||||
<Icon name="Info" />
|
||||
<div>
|
||||
<i
|
||||
>This automation will run synchronously as it contains a Collect
|
||||
step</i
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<Label small />
|
||||
|
||||
<div class="timeout-width">
|
||||
<Input
|
||||
label="Timeout in seconds (120 max)"
|
||||
type="number"
|
||||
{error}
|
||||
bind:value={parameters.timeout}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Label small />
|
||||
<Checkbox
|
||||
text="Do not display default notification"
|
||||
|
@ -133,6 +180,9 @@
|
|||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.timeout-width {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.params {
|
||||
display: grid;
|
||||
|
@ -142,6 +192,11 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.synchronous-info {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.fields {
|
||||
margin-top: var(--spacing-l);
|
||||
display: grid;
|
||||
|
|
|
@ -57,7 +57,13 @@
|
|||
{
|
||||
"name": "Trigger Automation",
|
||||
"type": "application",
|
||||
"component": "TriggerAutomation"
|
||||
"component": "TriggerAutomation",
|
||||
"context": [
|
||||
{
|
||||
"label": "Automation Result",
|
||||
"value": "result"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Update Field Value",
|
||||
|
|
|
@ -20,9 +20,14 @@ export const ActionStepID = {
|
|||
FILTER: "FILTER",
|
||||
QUERY_ROWS: "QUERY_ROWS",
|
||||
LOOP: "LOOP",
|
||||
COLLECT: "COLLECT",
|
||||
// these used to be lowercase step IDs, maintain for backwards compat
|
||||
discord: "discord",
|
||||
slack: "slack",
|
||||
zapier: "zapier",
|
||||
integromat: "integromat",
|
||||
}
|
||||
|
||||
export const Features = {
|
||||
LOOPING: "LOOPING",
|
||||
}
|
||||
|
|
|
@ -116,6 +116,9 @@ export const createLicensingStore = () => {
|
|||
const auditLogsEnabled = license.features.includes(
|
||||
Constants.Features.AUDIT_LOGS
|
||||
)
|
||||
const syncAutomationsEnabled = license.features.includes(
|
||||
Constants.Features.SYNC_AUTOMATIONS
|
||||
)
|
||||
store.update(state => {
|
||||
return {
|
||||
...state,
|
||||
|
@ -130,6 +133,7 @@ export const createLicensingStore = () => {
|
|||
environmentVariablesEnabled,
|
||||
auditLogsEnabled,
|
||||
enforceableSSO,
|
||||
syncAutomationsEnabled,
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
|
@ -122,13 +122,23 @@ const deleteRowHandler = async action => {
|
|||
}
|
||||
|
||||
const triggerAutomationHandler = async action => {
|
||||
const { fields, notificationOverride } = action.parameters
|
||||
const { fields, notificationOverride, timeout } = action.parameters
|
||||
if (fields) {
|
||||
try {
|
||||
await API.triggerAutomation({
|
||||
const result = await API.triggerAutomation({
|
||||
automationId: action.parameters.automationId,
|
||||
fields,
|
||||
timeout,
|
||||
})
|
||||
|
||||
// Value will exist if automation is synchronous, so return it.
|
||||
if (result.value) {
|
||||
if (!notificationOverride) {
|
||||
notificationStore.actions.success("Automation ran successfully")
|
||||
}
|
||||
return { result }
|
||||
}
|
||||
|
||||
if (!notificationOverride) {
|
||||
notificationStore.actions.success("Automation triggered")
|
||||
}
|
||||
|
@ -138,7 +148,6 @@ const triggerAutomationHandler = async action => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const navigationHandler = action => {
|
||||
const { url, peek, externalNewTab } = action.parameters
|
||||
routeStore.actions.navigate(url, peek, externalNewTab)
|
||||
|
|
|
@ -4,10 +4,10 @@ export const buildAutomationEndpoints = API => ({
|
|||
* @param automationId the ID of the automation to trigger
|
||||
* @param fields the fields to trigger the automation with
|
||||
*/
|
||||
triggerAutomation: async ({ automationId, fields }) => {
|
||||
triggerAutomation: async ({ automationId, fields, timeout }) => {
|
||||
return await API.post({
|
||||
url: `/api/automations/${automationId}/trigger`,
|
||||
body: { fields },
|
||||
body: { fields, timeout },
|
||||
})
|
||||
},
|
||||
|
||||
|
|
|
@ -70,6 +70,7 @@ export const Features = {
|
|||
ENFORCEABLE_SSO: "enforceableSSO",
|
||||
BRANDING: "branding",
|
||||
SCIM: "scim",
|
||||
SYNC_AUTOMATIONS: "syncAutomations",
|
||||
}
|
||||
|
||||
// Role IDs
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 2d6f999586fcb62bc98b26416ee406f6328e6615
|
||||
Subproject commit 3d307df17a53ba25bbf5d9ddc94b1706c813eb6f
|
|
@ -14,9 +14,16 @@ import { deleteEntityMetadata } from "../../utilities"
|
|||
import { MetadataTypes } from "../../constants"
|
||||
import { setTestFlag, clearTestFlag } from "../../utilities/redis"
|
||||
import { context, cache, events } from "@budibase/backend-core"
|
||||
import { automations } from "@budibase/pro"
|
||||
import { Automation, BBContext } from "@budibase/types"
|
||||
import { automations, features } from "@budibase/pro"
|
||||
import {
|
||||
Automation,
|
||||
AutomationActionStepId,
|
||||
AutomationResults,
|
||||
BBContext,
|
||||
} from "@budibase/types"
|
||||
import { getActionDefinitions as actionDefs } from "../../automations/actions"
|
||||
import sdk from "../../sdk"
|
||||
import { db as dbCore } from "@budibase/backend-core"
|
||||
|
||||
async function getActionDefinitions() {
|
||||
return removeDeprecated(await actionDefs())
|
||||
|
@ -257,13 +264,34 @@ export async function getDefinitionList(ctx: BBContext) {
|
|||
export async function trigger(ctx: BBContext) {
|
||||
const db = context.getAppDB()
|
||||
let automation = await db.get(ctx.params.id)
|
||||
await triggers.externalTrigger(automation, {
|
||||
...ctx.request.body,
|
||||
appId: ctx.appId,
|
||||
})
|
||||
ctx.body = {
|
||||
message: `Automation ${automation._id} has been triggered.`,
|
||||
automation,
|
||||
|
||||
let hasCollectStep = sdk.automations.utils.checkForCollectStep(automation)
|
||||
if (hasCollectStep && (await features.isSyncAutomationsEnabled())) {
|
||||
const response: AutomationResults = await triggers.externalTrigger(
|
||||
automation,
|
||||
{
|
||||
fields: ctx.request.body.fields,
|
||||
timeout: ctx.request.body.timeout * 1000 || 120000,
|
||||
},
|
||||
{ getResponses: true }
|
||||
)
|
||||
|
||||
let collectedValue = response.steps.find(
|
||||
step => step.stepId === AutomationActionStepId.COLLECT
|
||||
)
|
||||
ctx.body = collectedValue?.outputs
|
||||
} else {
|
||||
if (ctx.appId && !dbCore.isProdAppID(ctx.appId)) {
|
||||
ctx.throw(400, "Only apps in production support this endpoint")
|
||||
}
|
||||
await triggers.externalTrigger(automation, {
|
||||
...ctx.request.body,
|
||||
appId: ctx.appId,
|
||||
})
|
||||
ctx.body = {
|
||||
message: `Automation ${automation._id} has been triggered.`,
|
||||
automation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,11 @@ import {
|
|||
WebhookActionType,
|
||||
BBContext,
|
||||
Automation,
|
||||
AutomationActionStepId,
|
||||
} from "@budibase/types"
|
||||
import sdk from "../../sdk"
|
||||
import * as pro from "@budibase/pro"
|
||||
|
||||
const toJsonSchema = require("to-json-schema")
|
||||
const validate = require("jsonschema").validate
|
||||
|
||||
|
@ -78,15 +81,36 @@ export async function trigger(ctx: BBContext) {
|
|||
if (webhook.action.type === WebhookActionType.AUTOMATION) {
|
||||
// trigger with both the pure request and then expand it
|
||||
// incase the user has produced a schema to bind to
|
||||
await triggers.externalTrigger(target, {
|
||||
body: ctx.request.body,
|
||||
...ctx.request.body,
|
||||
appId: prodAppId,
|
||||
})
|
||||
}
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: "Webhook trigger fired successfully",
|
||||
let hasCollectStep = sdk.automations.utils.checkForCollectStep(target)
|
||||
|
||||
if (hasCollectStep && (await pro.features.isSyncAutomationsEnabled())) {
|
||||
const response = await triggers.externalTrigger(
|
||||
target,
|
||||
{
|
||||
body: ctx.request.body,
|
||||
...ctx.request.body,
|
||||
appId: prodAppId,
|
||||
},
|
||||
{ getResponses: true }
|
||||
)
|
||||
|
||||
let collectedValue = response.steps.find(
|
||||
(step: any) => step.stepId === AutomationActionStepId.COLLECT
|
||||
)
|
||||
|
||||
ctx.status = 200
|
||||
ctx.body = collectedValue.outputs
|
||||
} else {
|
||||
await triggers.externalTrigger(target, {
|
||||
body: ctx.request.body,
|
||||
...ctx.request.body,
|
||||
appId: prodAppId,
|
||||
})
|
||||
ctx.status = 200
|
||||
ctx.body = {
|
||||
message: "Webhook trigger fired successfully",
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.status === 404) {
|
||||
|
|
|
@ -65,7 +65,6 @@ router
|
|||
)
|
||||
.post(
|
||||
"/api/automations/:id/trigger",
|
||||
appInfoMiddleware({ appType: AppType.PROD }),
|
||||
paramResource("id"),
|
||||
authorized(
|
||||
permissions.PermissionType.AUTOMATION,
|
||||
|
|
|
@ -1,15 +1,27 @@
|
|||
const {
|
||||
import {
|
||||
checkBuilderEndpoint,
|
||||
getAllTableRows,
|
||||
clearAllAutomations,
|
||||
testAutomation,
|
||||
} = require("./utilities/TestFunctions")
|
||||
const setup = require("./utilities")
|
||||
const { basicAutomation, newAutomation, automationTrigger, automationStep } = setup.structures
|
||||
const MAX_RETRIES = 4
|
||||
const { TRIGGER_DEFINITIONS, BUILTIN_ACTION_DEFINITIONS } = require("../../../automations")
|
||||
const { events } = require("@budibase/backend-core")
|
||||
} from "./utilities/TestFunctions"
|
||||
import * as setup from "./utilities"
|
||||
import {
|
||||
TRIGGER_DEFINITIONS,
|
||||
BUILTIN_ACTION_DEFINITIONS,
|
||||
} from "../../../automations"
|
||||
import { events } from "@budibase/backend-core"
|
||||
import sdk from "../../../sdk"
|
||||
import { Automation } from "@budibase/types"
|
||||
import { mocks } from "@budibase/backend-core/tests"
|
||||
|
||||
const MAX_RETRIES = 4
|
||||
let {
|
||||
basicAutomation,
|
||||
newAutomation,
|
||||
automationTrigger,
|
||||
automationStep,
|
||||
collectAutomation,
|
||||
} = setup.structures
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
|
@ -24,6 +36,7 @@ describe("/automations", () => {
|
|||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
events.automation.deleted.mockClear()
|
||||
})
|
||||
|
||||
|
@ -32,7 +45,7 @@ describe("/automations", () => {
|
|||
const res = await request
|
||||
.get(`/api/automations/action/list`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(Object.keys(res.body).length).not.toEqual(0)
|
||||
|
@ -42,7 +55,7 @@ describe("/automations", () => {
|
|||
const res = await request
|
||||
.get(`/api/automations/trigger/list`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(Object.keys(res.body).length).not.toEqual(0)
|
||||
|
@ -52,14 +65,18 @@ describe("/automations", () => {
|
|||
const res = await request
|
||||
.get(`/api/automations/definitions/list`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
let definitionsLength = Object.keys(BUILTIN_ACTION_DEFINITIONS).length
|
||||
definitionsLength-- // OUTGOING_WEBHOOK is deprecated
|
||||
|
||||
expect(Object.keys(res.body.action).length).toBeGreaterThanOrEqual(definitionsLength)
|
||||
expect(Object.keys(res.body.trigger).length).toEqual(Object.keys(TRIGGER_DEFINITIONS).length)
|
||||
expect(Object.keys(res.body.action).length).toBeGreaterThanOrEqual(
|
||||
definitionsLength
|
||||
)
|
||||
expect(Object.keys(res.body.trigger).length).toEqual(
|
||||
Object.keys(TRIGGER_DEFINITIONS).length
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -72,7 +89,7 @@ describe("/automations", () => {
|
|||
.post(`/api/automations`)
|
||||
.set(config.defaultHeaders())
|
||||
.send(automation)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.message).toEqual("Automation created successfully")
|
||||
|
@ -91,7 +108,7 @@ describe("/automations", () => {
|
|||
.post(`/api/automations`)
|
||||
.set(config.defaultHeaders())
|
||||
.send(automation)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.message).toEqual("Automation created successfully")
|
||||
|
@ -107,7 +124,7 @@ describe("/automations", () => {
|
|||
config,
|
||||
method: "POST",
|
||||
url: `/api/automations`,
|
||||
body: automation
|
||||
body: automation,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -118,7 +135,7 @@ describe("/automations", () => {
|
|||
const res = await request
|
||||
.get(`/api/automations/${automation._id}`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body._id).toEqual(automation._id)
|
||||
expect(res.body._rev).toEqual(automation._rev)
|
||||
|
@ -134,8 +151,8 @@ describe("/automations", () => {
|
|||
row: {
|
||||
name: "{{trigger.row.name}}",
|
||||
description: "{{trigger.row.description}}",
|
||||
tableId: table._id
|
||||
}
|
||||
tableId: table._id,
|
||||
},
|
||||
}
|
||||
automation.appId = config.appId
|
||||
automation = await config.createAutomation(automation)
|
||||
|
@ -162,23 +179,68 @@ describe("/automations", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
describe("trigger", () => {
|
||||
it("does not trigger an automation when not synchronous and in dev", async () => {
|
||||
let automation = newAutomation()
|
||||
automation = await config.createAutomation(automation)
|
||||
const res = await request
|
||||
.post(`/api/automations/${automation._id}/trigger`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(400)
|
||||
|
||||
const update = async (automation) => {
|
||||
expect(res.body.message).toEqual(
|
||||
"Only apps in production support this endpoint"
|
||||
)
|
||||
})
|
||||
|
||||
it("triggers a synchronous automation", async () => {
|
||||
mocks.licenses.useSyncAutomations()
|
||||
let automation = collectAutomation()
|
||||
automation = await config.createAutomation(automation)
|
||||
const res = await request
|
||||
.post(`/api/automations/${automation._id}/trigger`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.success).toEqual(true)
|
||||
expect(res.body.value).toEqual([1, 2, 3])
|
||||
})
|
||||
|
||||
it("triggers an asynchronous automation", async () => {
|
||||
let automation = newAutomation()
|
||||
automation = await config.createAutomation(automation)
|
||||
await config.publish()
|
||||
|
||||
const res = await request
|
||||
.post(`/api/automations/${automation._id}/trigger`)
|
||||
.set(config.defaultHeaders({}, true))
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.message).toEqual(
|
||||
`Automation ${automation._id} has been triggered.`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const update = async (automation: Automation) => {
|
||||
return request
|
||||
.put(`/api/automations`)
|
||||
.set(config.defaultHeaders())
|
||||
.send(automation)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
}
|
||||
|
||||
const updateWithPost = async (automation) => {
|
||||
const updateWithPost = async (automation: Automation) => {
|
||||
return request
|
||||
.post(`/api/automations`)
|
||||
.set(config.defaultHeaders())
|
||||
.send(automation)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
}
|
||||
|
||||
|
@ -199,7 +261,9 @@ describe("/automations", () => {
|
|||
expect(automationRes._rev).not.toEqual(automation._rev)
|
||||
// content updates
|
||||
expect(automationRes.name).toEqual("Updated Name")
|
||||
expect(message).toEqual(`Automation ${automation._id} updated successfully.`)
|
||||
expect(message).toEqual(
|
||||
`Automation ${automation._id} updated successfully.`
|
||||
)
|
||||
// events
|
||||
expect(events.automation.created).not.toBeCalled()
|
||||
expect(events.automation.stepCreated).not.toBeCalled()
|
||||
|
@ -207,7 +271,6 @@ describe("/automations", () => {
|
|||
expect(events.automation.triggerUpdated).not.toBeCalled()
|
||||
})
|
||||
|
||||
|
||||
it("updates a automations name using POST request", async () => {
|
||||
let automation = newAutomation()
|
||||
await config.createAutomation(automation)
|
||||
|
@ -226,7 +289,9 @@ describe("/automations", () => {
|
|||
expect(automationRes._rev).not.toEqual(automation._rev)
|
||||
// content updates
|
||||
expect(automationRes.name).toEqual("Updated Name")
|
||||
expect(message).toEqual(`Automation ${automation._id} updated successfully.`)
|
||||
expect(message).toEqual(
|
||||
`Automation ${automation._id} updated successfully.`
|
||||
)
|
||||
// events
|
||||
expect(events.automation.created).not.toBeCalled()
|
||||
expect(events.automation.stepCreated).not.toBeCalled()
|
||||
|
@ -237,7 +302,9 @@ describe("/automations", () => {
|
|||
it("updates an automation trigger", async () => {
|
||||
let automation = newAutomation()
|
||||
automation = await config.createAutomation(automation)
|
||||
automation.definition.trigger = automationTrigger(TRIGGER_DEFINITIONS.WEBHOOK)
|
||||
automation.definition.trigger = automationTrigger(
|
||||
TRIGGER_DEFINITIONS.WEBHOOK
|
||||
)
|
||||
jest.clearAllMocks()
|
||||
|
||||
await update(automation)
|
||||
|
@ -266,7 +333,6 @@ describe("/automations", () => {
|
|||
expect(events.automation.triggerUpdated).not.toBeCalled()
|
||||
})
|
||||
|
||||
|
||||
it("removes automation steps", async () => {
|
||||
let automation = newAutomation()
|
||||
automation.definition.steps.push(automationStep())
|
||||
|
@ -305,11 +371,11 @@ describe("/automations", () => {
|
|||
it("return all the automations for an instance", async () => {
|
||||
await clearAllAutomations(config)
|
||||
const autoConfig = basicAutomation()
|
||||
automation = await config.createAutomation(autoConfig)
|
||||
await config.createAutomation(autoConfig)
|
||||
const res = await request
|
||||
.get(`/api/automations`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body[0]).toEqual(expect.objectContaining(autoConfig))
|
||||
|
@ -330,7 +396,7 @@ describe("/automations", () => {
|
|||
const res = await request
|
||||
.delete(`/api/automations/${automation.id}/${automation.rev}`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect('Content-Type', /json/)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
||||
expect(res.body.id).toEqual(automation._id)
|
||||
|
@ -346,4 +412,13 @@ describe("/automations", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("checkForCollectStep", () => {
|
||||
it("should return true if a collect step exists in an automation", async () => {
|
||||
let automation = collectAutomation()
|
||||
await config.createAutomation(automation)
|
||||
let res = await sdk.automations.utils.checkForCollectStep(automation)
|
||||
expect(res).toEqual(true)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,11 +1,13 @@
|
|||
const setup = require("./utilities")
|
||||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions")
|
||||
const { basicWebhook, basicAutomation } = setup.structures
|
||||
import { Webhook } from "@budibase/types"
|
||||
import * as setup from "./utilities"
|
||||
import { checkBuilderEndpoint } from "./utilities/TestFunctions"
|
||||
import { mocks } from "@budibase/backend-core/tests"
|
||||
const { basicWebhook, basicAutomation, collectAutomation } = setup.structures
|
||||
|
||||
describe("/webhooks", () => {
|
||||
let request = setup.getRequest()
|
||||
let config = setup.getConfig()
|
||||
let webhook
|
||||
let webhook: Webhook
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
|
@ -13,10 +15,11 @@ describe("/webhooks", () => {
|
|||
config.modeSelf()
|
||||
await config.init()
|
||||
const autoConfig = basicAutomation()
|
||||
autoConfig.definition.trigger = {
|
||||
schema: { outputs: { properties: {} } },
|
||||
inputs: {},
|
||||
autoConfig.definition.trigger.schema = {
|
||||
outputs: { properties: {} },
|
||||
inputs: { properties: {} },
|
||||
}
|
||||
autoConfig.definition.trigger.inputs = {}
|
||||
await config.createAutomation(autoConfig)
|
||||
webhook = await config.createWebhook()
|
||||
}
|
||||
|
@ -70,7 +73,7 @@ describe("/webhooks", () => {
|
|||
|
||||
describe("delete", () => {
|
||||
beforeAll(setupTest)
|
||||
|
||||
|
||||
it("should successfully delete", async () => {
|
||||
const res = await request
|
||||
.delete(`/api/webhooks/${webhook._id}/${webhook._rev}`)
|
||||
|
@ -97,7 +100,7 @@ describe("/webhooks", () => {
|
|||
const res = await request
|
||||
.post(`/api/webhooks/schema/${config.getAppId()}/${webhook._id}`)
|
||||
.send({
|
||||
a: 1
|
||||
a: 1,
|
||||
})
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
|
@ -112,7 +115,7 @@ describe("/webhooks", () => {
|
|||
expect(fetch.body[0]).toBeDefined()
|
||||
expect(fetch.body[0].bodySchema).toEqual({
|
||||
properties: {
|
||||
a: { type: "integer" }
|
||||
a: { type: "integer" },
|
||||
},
|
||||
type: "object",
|
||||
})
|
||||
|
@ -131,4 +134,23 @@ describe("/webhooks", () => {
|
|||
expect(res.body.message).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("should trigger a synchronous webhook call ", async () => {
|
||||
mocks.licenses.useSyncAutomations()
|
||||
let automation = collectAutomation()
|
||||
let newAutomation = await config.createAutomation(automation)
|
||||
let syncWebhook = await config.createWebhook(
|
||||
basicWebhook(newAutomation._id)
|
||||
)
|
||||
|
||||
// replicate changes before checking webhook
|
||||
await config.publish()
|
||||
|
||||
const res = await request
|
||||
.post(`/api/webhooks/trigger/${config.prodAppId}/${syncWebhook._id}`)
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
expect(res.body.success).toEqual(true)
|
||||
expect(res.body.value).toEqual([1, 2, 3])
|
||||
})
|
||||
})
|
|
@ -14,6 +14,7 @@ import * as filter from "./steps/filter"
|
|||
import * as delay from "./steps/delay"
|
||||
import * as queryRow from "./steps/queryRows"
|
||||
import * as loop from "./steps/loop"
|
||||
import * as collect from "./steps/collect"
|
||||
import env from "../environment"
|
||||
import {
|
||||
AutomationStepSchema,
|
||||
|
@ -39,6 +40,7 @@ const ACTION_IMPLS: Record<
|
|||
DELAY: delay.run,
|
||||
FILTER: filter.run,
|
||||
QUERY_ROWS: queryRow.run,
|
||||
COLLECT: collect.run,
|
||||
// these used to be lowercase step IDs, maintain for backwards compat
|
||||
discord: discord.run,
|
||||
slack: slack.run,
|
||||
|
@ -59,6 +61,7 @@ export const BUILTIN_ACTION_DEFINITIONS: Record<string, AutomationStepSchema> =
|
|||
FILTER: filter.definition,
|
||||
QUERY_ROWS: queryRow.definition,
|
||||
LOOP: loop.definition,
|
||||
COLLECT: collect.definition,
|
||||
// these used to be lowercase step IDs, maintain for backwards compat
|
||||
discord: discord.definition,
|
||||
slack: slack.definition,
|
||||
|
|
|
@ -5,6 +5,7 @@ import environment from "../../environment"
|
|||
import {
|
||||
AutomationActionStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationFeature,
|
||||
AutomationIOType,
|
||||
AutomationStepInput,
|
||||
AutomationStepSchema,
|
||||
|
@ -18,6 +19,9 @@ export const definition: AutomationStepSchema = {
|
|||
description: "Run a bash script",
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: true,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
stepId: AutomationActionStepId.EXECUTE_BASH,
|
||||
inputs: {},
|
||||
schema: {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import {
|
||||
AutomationActionStepId,
|
||||
AutomationStepSchema,
|
||||
AutomationStepInput,
|
||||
AutomationStepType,
|
||||
AutomationIOType,
|
||||
AutomationFeature,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationStepSchema = {
|
||||
name: "Collect Data",
|
||||
tagline: "Collect data to be sent to design",
|
||||
icon: "Collection",
|
||||
description:
|
||||
"Collects specified data so it can be provided to the design section",
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: true,
|
||||
features: {},
|
||||
stepId: AutomationActionStepId.COLLECT,
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
collection: {
|
||||
type: AutomationIOType.STRING,
|
||||
title: "What to Collect",
|
||||
},
|
||||
},
|
||||
required: ["collection"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
success: {
|
||||
type: AutomationIOType.BOOLEAN,
|
||||
description: "Whether the action was successful",
|
||||
},
|
||||
value: {
|
||||
type: AutomationIOType.STRING,
|
||||
description: "Collected data",
|
||||
},
|
||||
},
|
||||
required: ["success", "value"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export async function run({ inputs }: AutomationStepInput) {
|
||||
if (!inputs.collection) {
|
||||
return {
|
||||
success: false,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
value: inputs.collection,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import { buildCtx } from "./utils"
|
|||
import {
|
||||
AutomationActionStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationFeature,
|
||||
AutomationIOType,
|
||||
AutomationStepInput,
|
||||
AutomationStepSchema,
|
||||
|
@ -17,6 +18,9 @@ export const definition: AutomationStepSchema = {
|
|||
description: "Add a row to your database",
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: true,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
stepId: AutomationActionStepId.CREATE_ROW,
|
||||
inputs: {},
|
||||
schema: {
|
||||
|
|
|
@ -14,6 +14,7 @@ export const definition: AutomationStepSchema = {
|
|||
description: "Delay the automation until an amount of time has passed",
|
||||
stepId: AutomationActionStepId.DELAY,
|
||||
internal: true,
|
||||
features: {},
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
AutomationStepType,
|
||||
AutomationIOType,
|
||||
AutomationCustomIOType,
|
||||
AutomationFeature,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationStepSchema = {
|
||||
|
@ -18,6 +19,9 @@ export const definition: AutomationStepSchema = {
|
|||
type: AutomationStepType.ACTION,
|
||||
stepId: AutomationActionStepId.DELETE_ROW,
|
||||
internal: true,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
AutomationStepInput,
|
||||
AutomationStepType,
|
||||
AutomationIOType,
|
||||
AutomationFeature,
|
||||
} from "@budibase/types"
|
||||
|
||||
const DEFAULT_USERNAME = "Budibase Automate"
|
||||
|
@ -19,6 +20,9 @@ export const definition: AutomationStepSchema = {
|
|||
stepId: AutomationActionStepId.discord,
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: false,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as automationUtils from "../automationUtils"
|
|||
import {
|
||||
AutomationActionStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationFeature,
|
||||
AutomationIOType,
|
||||
AutomationStepInput,
|
||||
AutomationStepSchema,
|
||||
|
@ -18,6 +19,9 @@ export const definition: AutomationStepSchema = {
|
|||
type: AutomationStepType.ACTION,
|
||||
stepId: AutomationActionStepId.EXECUTE_QUERY,
|
||||
internal: true,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as automationUtils from "../automationUtils"
|
|||
import {
|
||||
AutomationActionStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationFeature,
|
||||
AutomationIOType,
|
||||
AutomationStepInput,
|
||||
AutomationStepSchema,
|
||||
|
@ -19,6 +20,9 @@ export const definition: AutomationStepSchema = {
|
|||
internal: true,
|
||||
stepId: AutomationActionStepId.EXECUTE_SCRIPT,
|
||||
inputs: {},
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
|
|
|
@ -28,6 +28,7 @@ export const definition: AutomationStepSchema = {
|
|||
"Conditionally halt automations which do not meet certain conditions",
|
||||
type: AutomationStepType.LOGIC,
|
||||
internal: true,
|
||||
features: {},
|
||||
stepId: AutomationActionStepId.FILTER,
|
||||
inputs: {
|
||||
condition: FilterConditions.EQUAL,
|
||||
|
|
|
@ -13,6 +13,7 @@ export const definition: AutomationStepSchema = {
|
|||
description: "Loop",
|
||||
stepId: AutomationActionStepId.LOOP,
|
||||
internal: true,
|
||||
features: {},
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
AutomationStepInput,
|
||||
AutomationStepType,
|
||||
AutomationIOType,
|
||||
AutomationFeature,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationStepSchema = {
|
||||
|
@ -18,6 +19,9 @@ export const definition: AutomationStepSchema = {
|
|||
stepId: AutomationActionStepId.integromat,
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: false,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
|
|
|
@ -22,6 +22,7 @@ export const definition: AutomationStepSchema = {
|
|||
description: "Interact with the OpenAI ChatGPT API.",
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: true,
|
||||
features: {},
|
||||
stepId: AutomationActionStepId.OPENAI,
|
||||
inputs: {
|
||||
prompt: "",
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as automationUtils from "../automationUtils"
|
|||
import {
|
||||
AutomationActionStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationFeature,
|
||||
AutomationIOType,
|
||||
AutomationStepInput,
|
||||
AutomationStepSchema,
|
||||
|
@ -32,6 +33,9 @@ export const definition: AutomationStepSchema = {
|
|||
description: "Send a request of specified method to a URL",
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: true,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
stepId: AutomationActionStepId.OUTGOING_WEBHOOK,
|
||||
inputs: {
|
||||
requestMethod: "POST",
|
||||
|
|
|
@ -6,6 +6,7 @@ import * as automationUtils from "../automationUtils"
|
|||
import {
|
||||
AutomationActionStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationFeature,
|
||||
AutomationIOType,
|
||||
AutomationStepInput,
|
||||
AutomationStepSchema,
|
||||
|
@ -42,6 +43,9 @@ export const definition: AutomationStepSchema = {
|
|||
type: AutomationStepType.ACTION,
|
||||
stepId: AutomationActionStepId.QUERY_ROWS,
|
||||
internal: true,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
AutomationStepInput,
|
||||
AutomationStepType,
|
||||
AutomationIOType,
|
||||
AutomationFeature,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationStepSchema = {
|
||||
|
@ -15,6 +16,9 @@ export const definition: AutomationStepSchema = {
|
|||
name: "Send Email (SMTP)",
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: true,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
stepId: AutomationActionStepId.SEND_EMAIL_SMTP,
|
||||
inputs: {},
|
||||
schema: {
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
AutomationStepInput,
|
||||
AutomationStepType,
|
||||
AutomationIOType,
|
||||
AutomationFeature,
|
||||
} from "@budibase/types"
|
||||
|
||||
/**
|
||||
|
@ -19,6 +20,9 @@ export const definition: AutomationStepSchema = {
|
|||
description: "Logs the given text to the server (using console.log)",
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: true,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
stepId: AutomationActionStepId.SERVER_LOG,
|
||||
inputs: {
|
||||
text: "",
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
AutomationStepInput,
|
||||
AutomationStepType,
|
||||
AutomationIOType,
|
||||
AutomationFeature,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationStepSchema = {
|
||||
|
@ -16,6 +17,9 @@ export const definition: AutomationStepSchema = {
|
|||
stepId: AutomationActionStepId.slack,
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: false,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
inputs: {},
|
||||
schema: {
|
||||
inputs: {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { buildCtx } from "./utils"
|
|||
import {
|
||||
AutomationActionStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationFeature,
|
||||
AutomationIOType,
|
||||
AutomationStepInput,
|
||||
AutomationStepSchema,
|
||||
|
@ -17,6 +18,9 @@ export const definition: AutomationStepSchema = {
|
|||
description: "Update a row in your database",
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: true,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
stepId: AutomationActionStepId.UPDATE_ROW,
|
||||
inputs: {},
|
||||
schema: {
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
AutomationStepInput,
|
||||
AutomationStepType,
|
||||
AutomationIOType,
|
||||
AutomationFeature,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationStepSchema = {
|
||||
|
@ -13,6 +14,9 @@ export const definition: AutomationStepSchema = {
|
|||
stepId: AutomationActionStepId.zapier,
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: false,
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
description: "Trigger a Zapier Zap via webhooks",
|
||||
tagline: "Trigger a Zapier Zap",
|
||||
icon: "ri-flashlight-line",
|
||||
|
|
|
@ -10,6 +10,7 @@ import * as utils from "./utils"
|
|||
import env from "../environment"
|
||||
import { context, db as dbCore } from "@budibase/backend-core"
|
||||
import { Automation, Row, AutomationData, AutomationJob } from "@budibase/types"
|
||||
import { executeSynchronously } from "../threads/automation"
|
||||
|
||||
export const TRIGGER_DEFINITIONS = definitions
|
||||
const JOB_OPTS = {
|
||||
|
@ -91,7 +92,7 @@ emitter.on("row:delete", async function (event) {
|
|||
|
||||
export async function externalTrigger(
|
||||
automation: Automation,
|
||||
params: { fields: Record<string, any> },
|
||||
params: { fields: Record<string, any>; timeout?: number },
|
||||
{ getResponses }: { getResponses?: boolean } = {}
|
||||
) {
|
||||
if (
|
||||
|
@ -118,7 +119,7 @@ export async function externalTrigger(
|
|||
automation,
|
||||
}
|
||||
const job = { data } as AutomationJob
|
||||
return utils.processEvent(job)
|
||||
return executeSynchronously(job)
|
||||
} else {
|
||||
return automationQueue.add(data, JOB_OPTS)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import * as webhook from "./webhook"
|
||||
import * as utils from "./utils"
|
||||
|
||||
export default {
|
||||
webhook,
|
||||
utils,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { Automation, AutomationActionStepId } from "@budibase/types"
|
||||
|
||||
export function checkForCollectStep(automation: Automation) {
|
||||
return automation.definition.steps.some(
|
||||
(step: any) => step.stepId === AutomationActionStepId.COLLECT
|
||||
)
|
||||
}
|
|
@ -373,7 +373,7 @@ class TestConfiguration {
|
|||
|
||||
// HEADERS
|
||||
|
||||
defaultHeaders(extras = {}) {
|
||||
defaultHeaders(extras = {}, prodApp = false) {
|
||||
const tenantId = this.getTenantId()
|
||||
const authObj: AuthToken = {
|
||||
userId: this.defaultUserValues.globalUserId,
|
||||
|
@ -390,7 +390,9 @@ class TestConfiguration {
|
|||
...extras,
|
||||
}
|
||||
|
||||
if (this.appId) {
|
||||
if (prodApp) {
|
||||
headers[constants.Header.APP_ID] = this.prodAppId
|
||||
} else if (this.appId) {
|
||||
headers[constants.Header.APP_ID] = this.appId
|
||||
}
|
||||
return headers
|
||||
|
|
|
@ -199,6 +199,48 @@ export function loopAutomation(tableId: string, loopOpts?: any): Automation {
|
|||
return automation as Automation
|
||||
}
|
||||
|
||||
export function collectAutomation(tableId?: string): Automation {
|
||||
const automation: any = {
|
||||
name: "looping",
|
||||
type: "automation",
|
||||
definition: {
|
||||
steps: [
|
||||
{
|
||||
id: "b",
|
||||
type: "ACTION",
|
||||
internal: true,
|
||||
stepId: AutomationActionStepId.EXECUTE_SCRIPT,
|
||||
inputs: {
|
||||
code: "return [1,2,3]",
|
||||
},
|
||||
schema: BUILTIN_ACTION_DEFINITIONS.EXECUTE_SCRIPT.schema,
|
||||
},
|
||||
{
|
||||
id: "c",
|
||||
type: "ACTION",
|
||||
internal: true,
|
||||
stepId: AutomationActionStepId.COLLECT,
|
||||
inputs: {
|
||||
collection: "{{ literal steps.1.value }}",
|
||||
},
|
||||
schema: BUILTIN_ACTION_DEFINITIONS.SERVER_LOG.schema,
|
||||
},
|
||||
],
|
||||
trigger: {
|
||||
id: "a",
|
||||
type: "TRIGGER",
|
||||
event: "row:save",
|
||||
stepId: AutomationTriggerStepId.ROW_SAVED,
|
||||
inputs: {
|
||||
tableId,
|
||||
},
|
||||
schema: TRIGGER_DEFINITIONS.ROW_SAVED.schema,
|
||||
},
|
||||
},
|
||||
}
|
||||
return automation as Automation
|
||||
}
|
||||
|
||||
export function basicRow(tableId: string) {
|
||||
return {
|
||||
name: "Test Contact",
|
||||
|
|
|
@ -68,6 +68,7 @@ class Orchestrator {
|
|||
constructor(job: AutomationJob) {
|
||||
let automation = job.data.automation
|
||||
let triggerOutput = job.data.event
|
||||
let timeout = job.data.event.timeout
|
||||
const metadata = triggerOutput.metadata
|
||||
this._chainCount = metadata ? metadata.automationChainCount! : 0
|
||||
this._appId = triggerOutput.appId as string
|
||||
|
@ -240,7 +241,9 @@ class Orchestrator {
|
|||
let loopStepNumber: any = undefined
|
||||
let loopSteps: LoopStep[] | undefined = []
|
||||
let metadata
|
||||
let timeoutFlag = false
|
||||
let wasLoopStep = false
|
||||
let timeout = this._job.data.event.timeout
|
||||
// check if this is a recurring automation,
|
||||
if (isProdAppID(this._appId) && isRecurring(automation)) {
|
||||
metadata = await this.getMetadata()
|
||||
|
@ -251,6 +254,16 @@ class Orchestrator {
|
|||
}
|
||||
|
||||
for (let step of automation.definition.steps) {
|
||||
if (timeoutFlag) {
|
||||
break
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
setTimeout(() => {
|
||||
timeoutFlag = true
|
||||
}, timeout || 12000)
|
||||
}
|
||||
|
||||
stepCount++
|
||||
let input: any,
|
||||
iterations = 1,
|
||||
|
@ -495,6 +508,32 @@ export function execute(job: Job, callback: WorkerCallback) {
|
|||
})
|
||||
}
|
||||
|
||||
export function executeSynchronously(job: Job) {
|
||||
const appId = job.data.event.appId
|
||||
if (!appId) {
|
||||
throw new Error("Unable to execute, event doesn't contain app ID.")
|
||||
}
|
||||
|
||||
const timeoutPromise = new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error("Timeout exceeded"))
|
||||
}, job.data.event.timeout || 12000)
|
||||
})
|
||||
|
||||
return context.doInAppContext(appId, async () => {
|
||||
const envVars = await sdkUtils.getEnvironmentVariables()
|
||||
// put into automation thread for whole context
|
||||
return context.doInEnvironmentContext(envVars, async () => {
|
||||
const automationOrchestrator = new Orchestrator(job)
|
||||
const response = await Promise.race([
|
||||
automationOrchestrator.execute(),
|
||||
timeoutPromise,
|
||||
])
|
||||
return response
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const removeStalled = async (job: Job) => {
|
||||
const appId = job.data.event.appId
|
||||
if (!appId) {
|
||||
|
|
|
@ -57,6 +57,7 @@ export enum AutomationActionStepId {
|
|||
FILTER = "FILTER",
|
||||
QUERY_ROWS = "QUERY_ROWS",
|
||||
LOOP = "LOOP",
|
||||
COLLECT = "COLLECT",
|
||||
OPENAI = "OPENAI",
|
||||
// these used to be lowercase step IDs, maintain for backwards compat
|
||||
discord = "discord",
|
||||
|
@ -123,6 +124,11 @@ export interface AutomationStepSchema {
|
|||
outputs: InputOutputBlock
|
||||
}
|
||||
custom?: boolean
|
||||
features?: Partial<Record<AutomationFeature, boolean>>
|
||||
}
|
||||
|
||||
export enum AutomationFeature {
|
||||
LOOPING = "LOOPING",
|
||||
}
|
||||
|
||||
export interface AutomationStep extends AutomationStepSchema {
|
||||
|
|
|
@ -5,6 +5,7 @@ export interface AutomationDataEvent {
|
|||
appId?: string
|
||||
metadata?: AutomationMetadata
|
||||
automation?: Automation
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
export interface AutomationData {
|
||||
|
|
|
@ -8,6 +8,7 @@ export enum Feature {
|
|||
ENFORCEABLE_SSO = "enforceableSSO",
|
||||
BRANDING = "branding",
|
||||
SCIM = "scim",
|
||||
SYNC_AUTOMATIONS = "syncAutomations",
|
||||
}
|
||||
|
||||
export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }
|
||||
|
|
Loading…
Reference in New Issue