Automation trigger filtering (#14123)
* backend for triggering automation based on filters * frontend for handling triggering automations on filter / old row * lint and bug fix * fix issue with test header * make test data optional * improve safety on trigger gate * add support for running trigger with filter if no change happened but filter matches * update var naming to actually make sense * tests * fix lint * improve gating for shouldTrigger check * remove unecessary cast * unecessary tableId check * frontend text updates * resolving comments * pro * Update packages/types/src/documents/app/automation.ts Co-authored-by: Sam Rose <hello@samwho.dev> * link out to docs for trigger filtering * fix pro * more pr comments * use getAppId --------- Co-authored-by: Sam Rose <hello@samwho.dev>
This commit is contained in:
parent
5e3bec86ed
commit
7fd55fe27d
|
@ -16,13 +16,12 @@
|
||||||
export let enableNaming = true
|
export let enableNaming = true
|
||||||
let validRegex = /^[A-Za-z0-9_\s]+$/
|
let validRegex = /^[A-Za-z0-9_\s]+$/
|
||||||
let typing = false
|
let typing = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: stepNames = $selectedAutomation?.definition.stepNames
|
$: stepNames = $selectedAutomation?.definition.stepNames
|
||||||
$: automationName = stepNames?.[block.id] || block?.name || ""
|
$: automationName = stepNames?.[block.id] || block?.name || ""
|
||||||
$: automationNameError = getAutomationNameError(automationName)
|
$: automationNameError = getAutomationNameError(automationName)
|
||||||
$: status = updateStatus(testResult, isTrigger)
|
$: status = updateStatus(testResult)
|
||||||
$: isHeaderTrigger = isTrigger || block.type === "TRIGGER"
|
$: isHeaderTrigger = isTrigger || block.type === "TRIGGER"
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
@ -43,7 +42,7 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatus(results, isTrigger) {
|
function updateStatus(results) {
|
||||||
if (!results) {
|
if (!results) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +55,6 @@
|
||||||
return { negative: true, message: "Error" }
|
return { negative: true, message: "Error" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAutomationNameError = name => {
|
const getAutomationNameError = name => {
|
||||||
if (stepNames) {
|
if (stepNames) {
|
||||||
for (const [key, value] of Object.entries(stepNames)) {
|
for (const [key, value] of Object.entries(stepNames)) {
|
||||||
|
|
|
@ -12,14 +12,31 @@
|
||||||
let blocks
|
let blocks
|
||||||
|
|
||||||
function prepTestResults(results) {
|
function prepTestResults(results) {
|
||||||
return results?.steps.filter(x => x.stepId !== ActionStepID.LOOP || [])
|
if (results.message) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
inputs: {},
|
||||||
|
outputs: {
|
||||||
|
success: results.outputs?.success || false,
|
||||||
|
status: results.outputs?.status || "unknown",
|
||||||
|
message: results.message,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
return results?.steps?.filter(x => x.stepId !== ActionStepID.LOOP) || []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: filteredResults = prepTestResults(testResults)
|
$: filteredResults = prepTestResults(testResults)
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
blocks = []
|
if (testResults.message) {
|
||||||
if (automation) {
|
blocks = automation?.definition?.trigger
|
||||||
|
? [automation.definition.trigger]
|
||||||
|
: []
|
||||||
|
} else if (automation) {
|
||||||
|
blocks = []
|
||||||
if (automation.definition.trigger) {
|
if (automation.definition.trigger) {
|
||||||
blocks.push(automation.definition.trigger)
|
blocks.push(automation.definition.trigger)
|
||||||
}
|
}
|
||||||
|
@ -46,7 +63,9 @@
|
||||||
open={!!openBlocks[block.id]}
|
open={!!openBlocks[block.id]}
|
||||||
on:toggle={() => (openBlocks[block.id] = !openBlocks[block.id])}
|
on:toggle={() => (openBlocks[block.id] = !openBlocks[block.id])}
|
||||||
isTrigger={idx === 0}
|
isTrigger={idx === 0}
|
||||||
testResult={filteredResults?.[idx]}
|
testResult={testResults.message
|
||||||
|
? testResults
|
||||||
|
: filteredResults?.[idx]}
|
||||||
showTestStatus
|
showTestStatus
|
||||||
{block}
|
{block}
|
||||||
{idx}
|
{idx}
|
||||||
|
@ -68,7 +87,9 @@
|
||||||
<Tabs quiet noHorizPadding selected="Input">
|
<Tabs quiet noHorizPadding selected="Input">
|
||||||
<Tab title="Input">
|
<Tab title="Input">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
{#if filteredResults?.[idx]?.inputs}
|
{#if testResults.message}
|
||||||
|
No input
|
||||||
|
{:else if filteredResults?.[idx]?.inputs}
|
||||||
<JsonView depth={2} json={filteredResults?.[idx]?.inputs} />
|
<JsonView depth={2} json={filteredResults?.[idx]?.inputs} />
|
||||||
{:else}
|
{:else}
|
||||||
No input
|
No input
|
||||||
|
@ -77,13 +98,22 @@
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="Output">
|
<Tab title="Output">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
{#if filteredResults?.[idx]?.outputs}
|
{#if testResults.message}
|
||||||
|
<JsonView
|
||||||
|
depth={2}
|
||||||
|
json={{
|
||||||
|
success: testResults.outputs?.success || false,
|
||||||
|
status: testResults.outputs?.status || "unknown",
|
||||||
|
message: testResults.message,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else if filteredResults?.[idx]?.outputs}
|
||||||
<JsonView
|
<JsonView
|
||||||
depth={2}
|
depth={2}
|
||||||
json={filteredResults?.[idx]?.outputs}
|
json={filteredResults?.[idx]?.outputs}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
No input
|
No output
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
|
@ -17,7 +17,9 @@
|
||||||
Helpers,
|
Helpers,
|
||||||
Toggle,
|
Toggle,
|
||||||
Divider,
|
Divider,
|
||||||
|
Icon,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
import { automationStore, selectedAutomation, tables } from "stores/builder"
|
||||||
import { environment, licensing } from "stores/portal"
|
import { environment, licensing } from "stores/portal"
|
||||||
|
@ -365,41 +367,74 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for row trigger automation updates.
|
* Handler for row trigger automation updates.
|
||||||
@param {object} update - An automation block.inputs update object
|
* @param {object} update - An automation block.inputs update object
|
||||||
@example
|
* @param {string} [update.tableId] - The ID of the table
|
||||||
onRowTriggerUpdate({
|
* @param {object} [update.filters] - Filter configuration for the row trigger
|
||||||
"tableId" : "ta_bb_employee"
|
* @param {object} [update.filters-def] - Filter definitions for the row trigger
|
||||||
})
|
* @example
|
||||||
|
* // Example with tableId
|
||||||
|
* onRowTriggerUpdate({
|
||||||
|
* "tableId" : "ta_bb_employee"
|
||||||
|
* })
|
||||||
|
* @example
|
||||||
|
* // Example with filters
|
||||||
|
* onRowTriggerUpdate({
|
||||||
|
* filters: {
|
||||||
|
* equal: { "1:Approved": "true" }
|
||||||
|
* },
|
||||||
|
* "filters-def": [{
|
||||||
|
* id: "oH1T4S49n",
|
||||||
|
* field: "1:Approved",
|
||||||
|
* operator: "equal",
|
||||||
|
* value: "true",
|
||||||
|
* valueType: "Value",
|
||||||
|
* type: "string"
|
||||||
|
* }]
|
||||||
|
* })
|
||||||
*/
|
*/
|
||||||
const onRowTriggerUpdate = async update => {
|
const onRowTriggerUpdate = async update => {
|
||||||
if (
|
if (
|
||||||
Object.hasOwn(update, "tableId") &&
|
["tableId", "filters", "meta"].some(key => Object.hasOwn(update, key))
|
||||||
$selectedAutomation.testData?.row?.tableId !== update.tableId
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const reqSchema = getSchemaForDatasourcePlus(update.tableId, {
|
let updatedAutomation
|
||||||
searchableSchema: true,
|
|
||||||
}).schema
|
|
||||||
|
|
||||||
// Parse the block inputs as usual
|
if (
|
||||||
const updatedAutomation =
|
Object.hasOwn(update, "tableId") &&
|
||||||
await automationStore.actions.processBlockInputs(block, {
|
$selectedAutomation.testData?.row?.tableId !== update.tableId
|
||||||
schema: reqSchema,
|
) {
|
||||||
...update,
|
const reqSchema = getSchemaForDatasourcePlus(update.tableId, {
|
||||||
})
|
searchableSchema: true,
|
||||||
|
}).schema
|
||||||
|
|
||||||
// Save the entire automation and reset the testData
|
updatedAutomation = await automationStore.actions.processBlockInputs(
|
||||||
await automationStore.actions.save({
|
block,
|
||||||
...updatedAutomation,
|
{
|
||||||
testData: {
|
schema: reqSchema,
|
||||||
// Reset Core fields
|
...update,
|
||||||
row: { tableId: update.tableId },
|
}
|
||||||
oldRow: { tableId: update.tableId },
|
)
|
||||||
meta: {},
|
|
||||||
id: "",
|
// Reset testData when tableId changes
|
||||||
revision: "",
|
updatedAutomation = {
|
||||||
},
|
...updatedAutomation,
|
||||||
})
|
testData: {
|
||||||
|
row: { tableId: update.tableId },
|
||||||
|
oldRow: { tableId: update.tableId },
|
||||||
|
meta: {},
|
||||||
|
id: "",
|
||||||
|
revision: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For filters update, just process block inputs without resetting testData
|
||||||
|
updatedAutomation = await automationStore.actions.processBlockInputs(
|
||||||
|
block,
|
||||||
|
update
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
await automationStore.actions.save(updatedAutomation)
|
||||||
|
|
||||||
return
|
return
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -408,7 +443,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for App trigger automation updates.
|
* Handler for App trigger automation updates.
|
||||||
* Ensure updates to the field list are reflected in testData
|
* Ensure updates to the field list are reflected in testData
|
||||||
|
@ -743,6 +777,7 @@
|
||||||
value.customType !== "triggerSchema" &&
|
value.customType !== "triggerSchema" &&
|
||||||
value.customType !== "automationFields" &&
|
value.customType !== "automationFields" &&
|
||||||
value.customType !== "fields" &&
|
value.customType !== "fields" &&
|
||||||
|
value.customType !== "trigger_filter_setting" &&
|
||||||
value.type !== "signature_single" &&
|
value.type !== "signature_single" &&
|
||||||
value.type !== "attachment" &&
|
value.type !== "attachment" &&
|
||||||
value.type !== "attachment_single"
|
value.type !== "attachment_single"
|
||||||
|
@ -807,13 +842,23 @@
|
||||||
{@const label = getFieldLabel(key, value)}
|
{@const label = getFieldLabel(key, value)}
|
||||||
<div class:block-field={shouldRenderField(value)}>
|
<div class:block-field={shouldRenderField(value)}>
|
||||||
{#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)}
|
{#if key !== "fields" && value.type !== "boolean" && shouldRenderField(value)}
|
||||||
<Label
|
<div class="label-container">
|
||||||
tooltip={value.title === "Binding / Value"
|
<Label>
|
||||||
? "If using the String input type, please use a comma or newline separated string"
|
{label}
|
||||||
: null}
|
</Label>
|
||||||
>
|
{#if value.customType === "trigger_filter"}
|
||||||
{label}
|
<Icon
|
||||||
</Label>
|
hoverable
|
||||||
|
on:click={() =>
|
||||||
|
window.open(
|
||||||
|
"https://docs.budibase.com/docs/row-trigger-filters",
|
||||||
|
"_blank"
|
||||||
|
)}
|
||||||
|
size="XS"
|
||||||
|
name="InfoOutline"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class:field-width={shouldRenderField(value)}>
|
<div class:field-width={shouldRenderField(value)}>
|
||||||
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
||||||
|
@ -932,8 +977,12 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if value.customType === "filters"}
|
{:else if value.customType === "filters" || value.customType === "trigger_filter"}
|
||||||
<ActionButton on:click={drawer.show}>Define filters</ActionButton>
|
<ActionButton fullWidth on:click={drawer.show}
|
||||||
|
>{filters.length > 0
|
||||||
|
? "Update Filter"
|
||||||
|
: "No Filter set"}</ActionButton
|
||||||
|
>
|
||||||
<Drawer bind:this={drawer} title="Filtering">
|
<Drawer bind:this={drawer} title="Filtering">
|
||||||
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
<Button cta slot="buttons" on:click={() => saveFilters(key)}>
|
||||||
Save
|
Save
|
||||||
|
@ -945,6 +994,7 @@
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
datasource={{ type: "table", tableId }}
|
datasource={{ type: "table", tableId }}
|
||||||
panel={AutomationBindingPanel}
|
panel={AutomationBindingPanel}
|
||||||
|
showFilterEmptyDropdown={!rowTriggers.includes(stepId)}
|
||||||
on:change={e => (tempFilters = e.detail)}
|
on:change={e => (tempFilters = e.detail)}
|
||||||
/>
|
/>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
|
@ -1085,6 +1135,11 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.label-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
.field-width {
|
.field-width {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
export let panel = ClientBindingPanel
|
export let panel = ClientBindingPanel
|
||||||
export let allowBindings = true
|
export let allowBindings = true
|
||||||
export let datasource
|
export let datasource
|
||||||
|
export let showFilterEmptyDropdown
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
let rawFilters
|
let rawFilters
|
||||||
|
@ -63,6 +63,7 @@
|
||||||
{schemaFields}
|
{schemaFields}
|
||||||
{datasource}
|
{datasource}
|
||||||
{allowBindings}
|
{allowBindings}
|
||||||
|
{showFilterEmptyDropdown}
|
||||||
>
|
>
|
||||||
<div slot="filtering-hero-content" />
|
<div slot="filtering-hero-content" />
|
||||||
|
|
||||||
|
|
|
@ -208,7 +208,7 @@ const automationActions = store => ({
|
||||||
const message = err.message || err.status || JSON.stringify(err)
|
const message = err.message || err.status || JSON.stringify(err)
|
||||||
throw `Automation test failed - ${message}`
|
throw `Automation test failed - ${message}`
|
||||||
}
|
}
|
||||||
if (!result?.trigger && !result?.steps?.length) {
|
if (!result?.trigger && !result?.steps?.length && !result?.message) {
|
||||||
if (result?.err?.code === "usage_limit_exceeded") {
|
if (result?.err?.code === "usage_limit_exceeded") {
|
||||||
throw "You have exceeded your automation quota"
|
throw "You have exceeded your automation quota"
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
export let behaviourFilters = false
|
export let behaviourFilters = false
|
||||||
export let allowBindings = false
|
export let allowBindings = false
|
||||||
export let filtersLabel = "Filters"
|
export let filtersLabel = "Filters"
|
||||||
|
export let showFilterEmptyDropdown = true
|
||||||
$: {
|
$: {
|
||||||
if (
|
if (
|
||||||
tables.find(
|
tables.find(
|
||||||
|
@ -218,7 +218,7 @@
|
||||||
on:change={e => handleAllOr(e.detail)}
|
on:change={e => handleAllOr(e.detail)}
|
||||||
placeholder={null}
|
placeholder={null}
|
||||||
/>
|
/>
|
||||||
{#if datasource?.type === "table"}
|
{#if datasource?.type === "table" && showFilterEmptyDropdown}
|
||||||
<Select
|
<Select
|
||||||
label="When filter empty"
|
label="When filter empty"
|
||||||
value={onEmptyFilter}
|
value={onEmptyFilter}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from "../../../automations"
|
} from "../../../automations"
|
||||||
import { events } from "@budibase/backend-core"
|
import { events } from "@budibase/backend-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { Automation } from "@budibase/types"
|
import { Automation, FieldType, Table } from "@budibase/types"
|
||||||
import { mocks } from "@budibase/backend-core/tests"
|
import { mocks } from "@budibase/backend-core/tests"
|
||||||
import { FilterConditions } from "../../../automations/steps/filter"
|
import { FilterConditions } from "../../../automations/steps/filter"
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ let {
|
||||||
automationStep,
|
automationStep,
|
||||||
collectAutomation,
|
collectAutomation,
|
||||||
filterAutomation,
|
filterAutomation,
|
||||||
|
updateRowAutomationWithFilters,
|
||||||
} = setup.structures
|
} = setup.structures
|
||||||
|
|
||||||
describe("/automations", () => {
|
describe("/automations", () => {
|
||||||
|
@ -452,14 +453,13 @@ describe("/automations", () => {
|
||||||
|
|
||||||
let table = await config.createTable()
|
let table = await config.createTable()
|
||||||
|
|
||||||
let automation = await filterAutomation()
|
let automation = await filterAutomation(config.getAppId())
|
||||||
automation.definition.trigger.inputs.tableId = table._id
|
automation.definition.trigger.inputs.tableId = table._id
|
||||||
automation.definition.steps[0].inputs = {
|
automation.definition.steps[0].inputs = {
|
||||||
condition: FilterConditions.EQUAL,
|
condition: FilterConditions.EQUAL,
|
||||||
field: "{{ trigger.row.City }}",
|
field: "{{ trigger.row.City }}",
|
||||||
value: "{{ trigger.oldRow.City }}",
|
value: "{{ trigger.oldRow.City }}",
|
||||||
}
|
}
|
||||||
automation.appId = config.appId!
|
|
||||||
automation = await config.createAutomation(automation)
|
automation = await config.createAutomation(automation)
|
||||||
let triggerInputs = {
|
let triggerInputs = {
|
||||||
oldRow: {
|
oldRow: {
|
||||||
|
@ -474,4 +474,91 @@ describe("/automations", () => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
describe("Automation Update / Creator row trigger filtering", () => {
|
||||||
|
let table: Table
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
table = await config.createTable({
|
||||||
|
name: "table",
|
||||||
|
type: "table",
|
||||||
|
schema: {
|
||||||
|
Approved: {
|
||||||
|
name: "Approved",
|
||||||
|
type: FieldType.BOOLEAN,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
description: "should run when Approved changes from false to true",
|
||||||
|
filters: {
|
||||||
|
equal: { "1:Approved": true },
|
||||||
|
},
|
||||||
|
row: { Approved: "true" },
|
||||||
|
oldRow: { Approved: "false" },
|
||||||
|
expectToRun: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "should run when Approved is true in both old and new row",
|
||||||
|
filters: { equal: { "1:Approved": true } },
|
||||||
|
row: { Approved: "true" },
|
||||||
|
oldRow: { Approved: "true" },
|
||||||
|
expectToRun: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"should run when a contains filter matches the correct options",
|
||||||
|
filters: {
|
||||||
|
contains: { "1:opts": ["Option 1", "Option 3"] },
|
||||||
|
},
|
||||||
|
row: { opts: ["Option 1", "Option 3"] },
|
||||||
|
oldRow: { opts: ["Option 3"] },
|
||||||
|
expectToRun: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"should not run when opts doesn't contain any specified option",
|
||||||
|
filters: {
|
||||||
|
contains: { "1:opts": ["Option 1", "Option 2"] },
|
||||||
|
},
|
||||||
|
row: { opts: ["Option 3", "Option 4"] },
|
||||||
|
oldRow: { opts: ["Option 3", "Option 4"] },
|
||||||
|
expectToRun: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
it.each(testCases)(
|
||||||
|
"$description",
|
||||||
|
async ({ filters, row, oldRow, expectToRun }) => {
|
||||||
|
let automation = await updateRowAutomationWithFilters(config.getAppId())
|
||||||
|
automation.definition.trigger.inputs = {
|
||||||
|
tableId: table._id,
|
||||||
|
filters,
|
||||||
|
}
|
||||||
|
automation = await config.createAutomation(automation)
|
||||||
|
|
||||||
|
const inputs = {
|
||||||
|
row: {
|
||||||
|
tableId: table._id,
|
||||||
|
...row,
|
||||||
|
},
|
||||||
|
oldRow: {
|
||||||
|
tableId: table._id,
|
||||||
|
...oldRow,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await testAutomation(config, automation, inputs)
|
||||||
|
|
||||||
|
if (expectToRun) {
|
||||||
|
expect(res.body.steps[1].outputs.success).toEqual(true)
|
||||||
|
} else {
|
||||||
|
expect(res.body.outputs.success).toEqual(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,6 +23,11 @@ export const definition: AutomationTriggerSchema = {
|
||||||
customType: AutomationCustomIOType.TABLE,
|
customType: AutomationCustomIOType.TABLE,
|
||||||
title: "Table",
|
title: "Table",
|
||||||
},
|
},
|
||||||
|
filters: {
|
||||||
|
type: AutomationIOType.OBJECT,
|
||||||
|
customType: AutomationCustomIOType.FILTERS,
|
||||||
|
title: "Filtering",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ["tableId"],
|
required: ["tableId"],
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,6 +23,11 @@ export const definition: AutomationTriggerSchema = {
|
||||||
customType: AutomationCustomIOType.TABLE,
|
customType: AutomationCustomIOType.TABLE,
|
||||||
title: "Table",
|
title: "Table",
|
||||||
},
|
},
|
||||||
|
filters: {
|
||||||
|
type: AutomationIOType.OBJECT,
|
||||||
|
customType: AutomationCustomIOType.TRIGGER_FILTER,
|
||||||
|
title: "Filtering",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ["tableId"],
|
required: ["tableId"],
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,14 +15,19 @@ import {
|
||||||
AutomationJob,
|
AutomationJob,
|
||||||
AutomationEventType,
|
AutomationEventType,
|
||||||
UpdatedRowEventEmitter,
|
UpdatedRowEventEmitter,
|
||||||
|
SearchFilters,
|
||||||
|
AutomationStoppedReason,
|
||||||
|
AutomationStatus,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { executeInThread } from "../threads/automation"
|
import { executeInThread } from "../threads/automation"
|
||||||
|
import { dataFilters } from "@budibase/shared-core"
|
||||||
|
|
||||||
export const TRIGGER_DEFINITIONS = definitions
|
export const TRIGGER_DEFINITIONS = definitions
|
||||||
const JOB_OPTS = {
|
const JOB_OPTS = {
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
removeOnFail: true,
|
removeOnFail: true,
|
||||||
}
|
}
|
||||||
|
import * as automationUtils from "../automations/automationUtils"
|
||||||
|
|
||||||
async function getAllAutomations() {
|
async function getAllAutomations() {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
@ -33,7 +38,7 @@ async function getAllAutomations() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function queueRelevantRowAutomations(
|
async function queueRelevantRowAutomations(
|
||||||
event: { appId: string; row: Row },
|
event: { appId: string; row: Row; oldRow: Row },
|
||||||
eventType: string
|
eventType: string
|
||||||
) {
|
) {
|
||||||
if (event.appId == null) {
|
if (event.appId == null) {
|
||||||
|
@ -62,9 +67,15 @@ async function queueRelevantRowAutomations(
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shouldTrigger = await checkTriggerFilters(automation, {
|
||||||
|
row: event.row,
|
||||||
|
oldRow: event.oldRow,
|
||||||
|
})
|
||||||
if (
|
if (
|
||||||
automationTrigger?.inputs &&
|
automationTrigger?.inputs &&
|
||||||
automationTrigger.inputs.tableId === event.row.tableId
|
automationTrigger.inputs.tableId === event.row.tableId &&
|
||||||
|
shouldTrigger
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await automationQueue.add({ automation, event }, JOB_OPTS)
|
await automationQueue.add({ automation, event }, JOB_OPTS)
|
||||||
|
@ -103,6 +114,11 @@ emitter.on(AutomationEventType.ROW_DELETE, async function (event) {
|
||||||
await queueRelevantRowAutomations(event, AutomationEventType.ROW_DELETE)
|
await queueRelevantRowAutomations(event, AutomationEventType.ROW_DELETE)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function rowPassesFilters(row: Row, filters: SearchFilters) {
|
||||||
|
const filteredRows = dataFilters.runQuery([row], filters)
|
||||||
|
return filteredRows.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
export async function externalTrigger(
|
export async function externalTrigger(
|
||||||
automation: Automation,
|
automation: Automation,
|
||||||
params: { fields: Record<string, any>; timeout?: number },
|
params: { fields: Record<string, any>; timeout?: number },
|
||||||
|
@ -126,7 +142,23 @@ export async function externalTrigger(
|
||||||
}
|
}
|
||||||
params.fields = coercedFields
|
params.fields = coercedFields
|
||||||
}
|
}
|
||||||
const data: AutomationData = { automation, event: params as any }
|
const data: AutomationData = { automation, event: params }
|
||||||
|
|
||||||
|
const shouldTrigger = await checkTriggerFilters(automation, {
|
||||||
|
row: data.event?.row ?? {},
|
||||||
|
oldRow: data.event?.oldRow ?? {},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!shouldTrigger) {
|
||||||
|
return {
|
||||||
|
outputs: {
|
||||||
|
success: false,
|
||||||
|
status: AutomationStatus.STOPPED,
|
||||||
|
},
|
||||||
|
message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (getResponses) {
|
if (getResponses) {
|
||||||
data.event = {
|
data.event = {
|
||||||
...data.event,
|
...data.event,
|
||||||
|
@ -171,3 +203,25 @@ export async function rebootTrigger() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkTriggerFilters(
|
||||||
|
automation: Automation,
|
||||||
|
event: { row: Row; oldRow: Row }
|
||||||
|
): Promise<boolean> {
|
||||||
|
const trigger = automation.definition.trigger
|
||||||
|
const filters = trigger?.inputs?.filters
|
||||||
|
const tableId = trigger?.inputs?.tableId
|
||||||
|
|
||||||
|
if (!filters) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
trigger.stepId === definitions.ROW_UPDATED.stepId ||
|
||||||
|
trigger.stepId === definitions.ROW_SAVED.stepId
|
||||||
|
) {
|
||||||
|
const newRow = await automationUtils.cleanUpRow(tableId, event.row)
|
||||||
|
return rowPassesFilters(newRow, filters)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
@ -357,18 +357,23 @@ export function collectAutomation(tableId?: string): Automation {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return automation as Automation
|
return automation
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterAutomation(tableId?: string): Automation {
|
export function filterAutomation(appId: string, tableId?: string): Automation {
|
||||||
const automation: any = {
|
const automation: Automation = {
|
||||||
name: "looping",
|
name: "looping",
|
||||||
type: "automation",
|
type: "automation",
|
||||||
|
appId,
|
||||||
definition: {
|
definition: {
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
|
name: "Filter Step",
|
||||||
|
tagline: "An automation filter step",
|
||||||
|
description: "A filter automation",
|
||||||
id: "b",
|
id: "b",
|
||||||
type: "ACTION",
|
icon: "Icon",
|
||||||
|
type: AutomationStepType.ACTION,
|
||||||
internal: true,
|
internal: true,
|
||||||
stepId: AutomationActionStepId.FILTER,
|
stepId: AutomationActionStepId.FILTER,
|
||||||
inputs: {},
|
inputs: {},
|
||||||
|
@ -376,8 +381,12 @@ export function filterAutomation(tableId?: string): Automation {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
trigger: {
|
trigger: {
|
||||||
|
name: "trigger Step",
|
||||||
|
tagline: "An automation trigger",
|
||||||
|
description: "A trigger",
|
||||||
|
icon: "Icon",
|
||||||
id: "a",
|
id: "a",
|
||||||
type: "TRIGGER",
|
type: AutomationStepType.TRIGGER,
|
||||||
event: "row:save",
|
event: "row:save",
|
||||||
stepId: AutomationTriggerStepId.ROW_SAVED,
|
stepId: AutomationTriggerStepId.ROW_SAVED,
|
||||||
inputs: {
|
inputs: {
|
||||||
|
@ -387,7 +396,45 @@ export function filterAutomation(tableId?: string): Automation {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return automation as Automation
|
return automation
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateRowAutomationWithFilters(appId: string): Automation {
|
||||||
|
const automation: Automation = {
|
||||||
|
name: "updateRowWithFilters",
|
||||||
|
type: "automation",
|
||||||
|
appId,
|
||||||
|
definition: {
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
name: "Filter Step",
|
||||||
|
tagline: "An automation filter step",
|
||||||
|
description: "A filter automation",
|
||||||
|
icon: "Icon",
|
||||||
|
id: "b",
|
||||||
|
type: AutomationStepType.ACTION,
|
||||||
|
internal: true,
|
||||||
|
stepId: AutomationActionStepId.SERVER_LOG,
|
||||||
|
inputs: {},
|
||||||
|
schema: BUILTIN_ACTION_DEFINITIONS.SERVER_LOG.schema,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
trigger: {
|
||||||
|
name: "trigger Step",
|
||||||
|
tagline: "An automation trigger",
|
||||||
|
description: "A trigger",
|
||||||
|
icon: "Icon",
|
||||||
|
|
||||||
|
id: "a",
|
||||||
|
type: AutomationStepType.TRIGGER,
|
||||||
|
event: "row:update",
|
||||||
|
stepId: AutomationTriggerStepId.ROW_UPDATED,
|
||||||
|
inputs: {},
|
||||||
|
schema: TRIGGER_DEFINITIONS.ROW_UPDATED.schema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return automation
|
||||||
}
|
}
|
||||||
|
|
||||||
export function basicAutomationResults(
|
export function basicAutomationResults(
|
||||||
|
|
|
@ -35,6 +35,7 @@ export enum AutomationCustomIOType {
|
||||||
AUTOMATION = "automation",
|
AUTOMATION = "automation",
|
||||||
AUTOMATION_FIELDS = "automationFields",
|
AUTOMATION_FIELDS = "automationFields",
|
||||||
MULTI_ATTACHMENTS = "multi_attachments",
|
MULTI_ATTACHMENTS = "multi_attachments",
|
||||||
|
TRIGGER_FILTER = "trigger_filter",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AutomationTriggerStepId {
|
export enum AutomationTriggerStepId {
|
||||||
|
@ -128,6 +129,15 @@ export interface Automation extends Document {
|
||||||
internal?: boolean
|
internal?: boolean
|
||||||
type?: string
|
type?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
testData?: {
|
||||||
|
row?: Row
|
||||||
|
meta: {
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
id: string
|
||||||
|
revision: string
|
||||||
|
oldRow?: Row
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseIOStructure {
|
interface BaseIOStructure {
|
||||||
|
@ -201,6 +211,10 @@ export enum AutomationStatus {
|
||||||
STOPPED_ERROR = "stopped_error",
|
STOPPED_ERROR = "stopped_error",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AutomationStoppedReason {
|
||||||
|
TRIGGER_FILTER_NOT_MET = "Automation did not run. Filter conditions in trigger were not met.",
|
||||||
|
}
|
||||||
|
|
||||||
export interface AutomationResults {
|
export interface AutomationResults {
|
||||||
automationId?: string
|
automationId?: string
|
||||||
status?: AutomationStatus
|
status?: AutomationStatus
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Automation, AutomationMetadata } from "../../documents"
|
import { Automation, AutomationMetadata, Row } from "../../documents"
|
||||||
import { Job } from "bull"
|
import { Job } from "bull"
|
||||||
|
|
||||||
export interface AutomationDataEvent {
|
export interface AutomationDataEvent {
|
||||||
|
@ -6,6 +6,8 @@ export interface AutomationDataEvent {
|
||||||
metadata?: AutomationMetadata
|
metadata?: AutomationMetadata
|
||||||
automation?: Automation
|
automation?: Automation
|
||||||
timeout?: number
|
timeout?: number
|
||||||
|
row?: Row
|
||||||
|
oldRow?: Row
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutomationData {
|
export interface AutomationData {
|
||||||
|
|
Loading…
Reference in New Issue