Merge branch 'master' of github.com:Budibase/budibase into default-app-design
This commit is contained in:
commit
c063cefd7e
|
@ -8,11 +8,13 @@
|
|||
export let invalid: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let closable: boolean = false
|
||||
export let emphasized: boolean = false
|
||||
</script>
|
||||
|
||||
<div
|
||||
class:is-invalid={invalid}
|
||||
class:is-disabled={disabled}
|
||||
class:is-emphasized={emphasized}
|
||||
class="spectrum-Tags-item"
|
||||
role="listitem"
|
||||
>
|
||||
|
@ -40,4 +42,9 @@
|
|||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.is-emphasized {
|
||||
border-color: var(--spectrum-global-color-blue-700);
|
||||
color: var(--spectrum-global-color-blue-700);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,9 +23,8 @@
|
|||
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
|
||||
let selectedAction
|
||||
let actions = Object.entries($automationStore.blockDefinitions.ACTION).filter(
|
||||
entry => {
|
||||
const [key] = entry
|
||||
return key !== AutomationActionStepId.BRANCH
|
||||
([key, action]) => {
|
||||
return key !== AutomationActionStepId.BRANCH && action.deprecated !== true
|
||||
}
|
||||
)
|
||||
let lockedFeatures = [
|
||||
|
@ -186,6 +185,10 @@
|
|||
</div>
|
||||
{:else if isDisabled}
|
||||
<Icon name="Help" tooltip={disabled()[idx].message} />
|
||||
{:else if action.new}
|
||||
<Tags>
|
||||
<Tag emphasized>New</Tag>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -227,6 +230,10 @@
|
|||
grid-gap: var(--spectrum-alias-grid-baseline);
|
||||
}
|
||||
|
||||
.item :global(.spectrum-Tags-itemLabel) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item {
|
||||
cursor: pointer;
|
||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
||||
|
@ -237,6 +244,8 @@
|
|||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
border-width: 2px;
|
||||
min-height: 3.5rem;
|
||||
display: flex;
|
||||
}
|
||||
.item:not(.disabled):hover,
|
||||
.selected {
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
<script>
|
||||
import { automationStore, selectedAutomation } from "@/stores/builder"
|
||||
import { Icon, Body, AbsTooltip, StatusLight } from "@budibase/bbui"
|
||||
import {
|
||||
Icon,
|
||||
Body,
|
||||
AbsTooltip,
|
||||
StatusLight,
|
||||
Tags,
|
||||
Tag,
|
||||
} from "@budibase/bbui"
|
||||
import { externalActions } from "./ExternalActions"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { Features } from "@/constants/backend/automations"
|
||||
|
@ -24,6 +31,7 @@
|
|||
$: blockRefs = $selectedAutomation?.blockRefs || {}
|
||||
$: stepNames = automation?.definition.stepNames || {}
|
||||
$: allSteps = automation?.definition.steps || []
|
||||
$: blockDefinition = $automationStore.blockDefinitions.ACTION[block.stepId]
|
||||
$: automationName = itemName || stepNames?.[block.id] || block?.name || ""
|
||||
$: automationNameError = getAutomationNameError(automationName)
|
||||
$: status = updateStatus(testResult)
|
||||
|
@ -135,7 +143,16 @@
|
|||
{#if isHeaderTrigger}
|
||||
<Body size="XS"><b>Trigger</b></Body>
|
||||
{:else}
|
||||
<Body size="XS"><b>{isBranch ? "Branch" : "Step"}</b></Body>
|
||||
<Body size="XS">
|
||||
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<b>{isBranch ? "Branch" : "Step"}</b>
|
||||
{#if blockDefinition.deprecated}
|
||||
<Tags>
|
||||
<Tag invalid>Deprecated</Tag>
|
||||
</Tags>
|
||||
{/if}
|
||||
</div>
|
||||
</Body>
|
||||
{/if}
|
||||
|
||||
{#if enableNaming}
|
||||
|
|
|
@ -102,6 +102,10 @@
|
|||
if (rowTriggers.includes(trigger?.event)) {
|
||||
const tableId = trigger?.inputs?.tableId
|
||||
|
||||
if (!jsonUpdate.row) {
|
||||
jsonUpdate.row = {}
|
||||
}
|
||||
|
||||
// Reset the tableId as it must match the trigger
|
||||
if (jsonUpdate?.row?.tableId !== tableId) {
|
||||
jsonUpdate.row.tableId = tableId
|
||||
|
@ -161,7 +165,7 @@
|
|||
block={trigger}
|
||||
on:update={e => {
|
||||
const { testData: updatedTestData } = e.detail
|
||||
testData = updatedTestData
|
||||
testData = parseTestData(updatedTestData)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
Toggle,
|
||||
Divider,
|
||||
Icon,
|
||||
CoreSelect,
|
||||
} from "@budibase/bbui"
|
||||
|
||||
import CreateWebhookModal from "@/components/automation/Shared/CreateWebhookModal.svelte"
|
||||
|
@ -48,7 +49,13 @@
|
|||
EditorModes,
|
||||
} from "@/components/common/CodeEditor"
|
||||
import FilterBuilder from "@/components/design/settings/controls/FilterEditor/FilterBuilder.svelte"
|
||||
import { QueryUtils, Utils, search, memo } from "@budibase/frontend-core"
|
||||
import {
|
||||
QueryUtils,
|
||||
Utils,
|
||||
search,
|
||||
memo,
|
||||
fetchData,
|
||||
} from "@budibase/frontend-core"
|
||||
import { getSchemaForDatasourcePlus } from "@/dataBinding"
|
||||
import { TriggerStepID, ActionStepID } from "@/constants/backend/automations"
|
||||
import { onMount, createEventDispatcher } from "svelte"
|
||||
|
@ -59,9 +66,12 @@
|
|||
AutomationStepType,
|
||||
AutomationActionStepId,
|
||||
AutomationCustomIOType,
|
||||
SortOrder,
|
||||
} from "@budibase/types"
|
||||
import PropField from "./PropField.svelte"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
import { API } from "@/api"
|
||||
import InfoDisplay from "@/pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/InfoDisplay.svelte"
|
||||
|
||||
export let automation
|
||||
export let block
|
||||
|
@ -95,6 +105,8 @@
|
|||
let inputData
|
||||
let insertAtPos, getCaretPosition
|
||||
let stepLayouts = {}
|
||||
let rowSearchTerm = ""
|
||||
let selectedRow
|
||||
|
||||
$: memoBlock.set(block)
|
||||
|
||||
|
@ -109,9 +121,13 @@
|
|||
$: stepId = $memoBlock.stepId
|
||||
|
||||
$: getInputData(testData, $memoBlock.inputs)
|
||||
$: tableId = inputData ? inputData.tableId : null
|
||||
$: tableId =
|
||||
inputData?.row?.tableId ||
|
||||
testData?.row?.tableId ||
|
||||
inputData?.tableId ||
|
||||
null
|
||||
$: table = tableId
|
||||
? $tables.list.find(table => table._id === inputData.tableId)
|
||||
? $tables.list.find(table => table._id === tableId)
|
||||
: { schema: {} }
|
||||
$: schema = getSchemaForDatasourcePlus(tableId, {
|
||||
searchableSchema: true,
|
||||
|
@ -140,6 +156,40 @@
|
|||
? [hbAutocomplete([...bindingsToCompletions(bindings, codeMode)])]
|
||||
: []
|
||||
|
||||
$: fetch = createFetch({ type: "table", tableId })
|
||||
$: fetchedRows = $fetch?.rows
|
||||
$: fetch?.update({
|
||||
query: {
|
||||
fuzzy: {
|
||||
[primaryDisplay]: rowSearchTerm || "",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
$: fetchLoading = $fetch?.loading
|
||||
$: primaryDisplay = table?.primaryDisplay
|
||||
|
||||
const createFetch = datasource => {
|
||||
if (!datasource) {
|
||||
return
|
||||
}
|
||||
|
||||
return fetchData({
|
||||
API,
|
||||
datasource,
|
||||
options: {
|
||||
sortColumn: primaryDisplay,
|
||||
sortOrder: SortOrder.ASCENDING,
|
||||
query: {
|
||||
fuzzy: {
|
||||
[primaryDisplay]: rowSearchTerm || "",
|
||||
},
|
||||
},
|
||||
limit: 20,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const getInputData = (testData, blockInputs) => {
|
||||
// Test data is not cloned for reactivity
|
||||
let newInputData = testData || cloneDeep(blockInputs)
|
||||
|
@ -167,7 +217,7 @@
|
|||
const stepStore = writable({})
|
||||
$: stepState = $stepStore?.[block.id]
|
||||
|
||||
$: customStepLayouts($memoBlock, schemaProperties, stepState)
|
||||
$: customStepLayouts($memoBlock, schemaProperties, stepState, fetchedRows)
|
||||
|
||||
const customStepLayouts = block => {
|
||||
if (
|
||||
|
@ -362,6 +412,49 @@
|
|||
disabled: isTestModal,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: CoreSelect,
|
||||
title: "Row",
|
||||
props: {
|
||||
disabled: !table,
|
||||
placeholder: "Select a row",
|
||||
options: fetchedRows,
|
||||
loading: fetchLoading,
|
||||
value: selectedRow,
|
||||
autocomplete: true,
|
||||
filter: false,
|
||||
getOptionLabel: row => row?.[primaryDisplay] || "",
|
||||
compare: (a, b) => a?.[primaryDisplay] === b?.[primaryDisplay],
|
||||
onChange: e => {
|
||||
if (isTestModal) {
|
||||
onChange({
|
||||
id: e.detail?._id,
|
||||
revision: e.detail?._rev,
|
||||
row: e.detail,
|
||||
oldRow: e.detail,
|
||||
meta: {
|
||||
fields: inputData["meta"]?.fields || {},
|
||||
oldFields: e.detail?.meta?.fields || {},
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: InfoDisplay,
|
||||
props: {
|
||||
warning: true,
|
||||
icon: "AlertCircleFilled",
|
||||
body: `Be careful when testing this automation because your data may be modified or deleted.`,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: Divider,
|
||||
props: {
|
||||
noMargin: true,
|
||||
},
|
||||
},
|
||||
...getIdConfig(),
|
||||
...getRevConfig(),
|
||||
...getRowTypeConfig(),
|
||||
|
@ -556,6 +649,15 @@
|
|||
...request,
|
||||
}
|
||||
|
||||
if (
|
||||
newTestData?.row == null ||
|
||||
Object.keys(newTestData?.row).length === 0
|
||||
) {
|
||||
selectedRow = null
|
||||
} else {
|
||||
selectedRow = newTestData.row
|
||||
}
|
||||
|
||||
const updatedAuto =
|
||||
automationStore.actions.addTestDataToAutomation(newTestData)
|
||||
|
||||
|
@ -668,6 +770,7 @@
|
|||
{...config.props}
|
||||
{bindings}
|
||||
on:change={config.props.onChange}
|
||||
bind:searchTerm={rowSearchTerm}
|
||||
/>
|
||||
</PropField>
|
||||
{:else}
|
||||
|
|
|
@ -303,13 +303,22 @@
|
|||
>
|
||||
<ActionButton
|
||||
icon="Add"
|
||||
fullWidth
|
||||
on:click={() => {
|
||||
customPopover.show()
|
||||
}}
|
||||
disabled={!schemaFields}
|
||||
>Add fields
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
icon="Remove"
|
||||
on:click={() => {
|
||||
dispatch("change", {
|
||||
meta: { fields: {} },
|
||||
row: {},
|
||||
})
|
||||
}}
|
||||
>Clear
|
||||
</ActionButton>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
@ -375,4 +384,11 @@
|
|||
.prop-control-wrap :global(.icon.json-slot-icon) {
|
||||
right: 1px !important;
|
||||
}
|
||||
|
||||
.add-fields-btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -15,6 +15,7 @@ export const ActionStepID = {
|
|||
DELETE_ROW: "DELETE_ROW",
|
||||
OUTGOING_WEBHOOK: "OUTGOING_WEBHOOK",
|
||||
EXECUTE_SCRIPT: "EXECUTE_SCRIPT",
|
||||
EXECUTE_SCRIPT_V2: "EXECUTE_SCRIPT_V2",
|
||||
EXECUTE_QUERY: "EXECUTE_QUERY",
|
||||
SERVER_LOG: "SERVER_LOG",
|
||||
DELAY: "DELAY",
|
||||
|
|
|
@ -33,6 +33,8 @@ import {
|
|||
isRowSaveTrigger,
|
||||
isAppTrigger,
|
||||
BranchStep,
|
||||
GetAutomationTriggerDefinitionsResponse,
|
||||
GetAutomationActionDefinitionsResponse,
|
||||
} from "@budibase/types"
|
||||
import { ActionStepID } from "@/constants/backend/automations"
|
||||
import { FIELDS } from "@/constants/backend"
|
||||
|
@ -68,16 +70,19 @@ const initialAutomationState: AutomationState = {
|
|||
}
|
||||
|
||||
const getFinalDefinitions = (
|
||||
triggers: Record<string, any>,
|
||||
actions: Record<string, any>
|
||||
triggers: GetAutomationTriggerDefinitionsResponse,
|
||||
actions: GetAutomationActionDefinitionsResponse
|
||||
): BlockDefinitions => {
|
||||
const creatable: Record<string, any> = {}
|
||||
Object.entries(triggers).forEach(entry => {
|
||||
if (entry[0] === AutomationTriggerStepId.ROW_ACTION) {
|
||||
return
|
||||
const creatable: Partial<GetAutomationTriggerDefinitionsResponse> = {}
|
||||
for (const [key, trigger] of Object.entries(triggers)) {
|
||||
if (key === AutomationTriggerStepId.ROW_ACTION) {
|
||||
continue
|
||||
}
|
||||
creatable[entry[0]] = entry[1]
|
||||
})
|
||||
if (trigger.deprecated === true) {
|
||||
continue
|
||||
}
|
||||
creatable[key as keyof GetAutomationTriggerDefinitionsResponse] = trigger
|
||||
}
|
||||
return {
|
||||
TRIGGER: triggers,
|
||||
CREATABLE_TRIGGER: creatable,
|
||||
|
@ -679,7 +684,10 @@ const automationActions = (store: AutomationStore) => ({
|
|||
runtimeName = `loop.${name}`
|
||||
} else if (idx === 0) {
|
||||
runtimeName = `trigger.${name}`
|
||||
} else if (currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT) {
|
||||
} else if (
|
||||
currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT ||
|
||||
currentBlock?.stepId === AutomationActionStepId.EXECUTE_SCRIPT_V2
|
||||
) {
|
||||
const stepId = pathSteps[idx].id
|
||||
if (!stepId) {
|
||||
notifications.error("Error generating binding: Step ID not found.")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as triggers from "../../automations/triggers"
|
||||
import { sdk as coreSdk } from "@budibase/shared-core"
|
||||
import { DocumentType } from "../../db/utils"
|
||||
import { updateTestHistory, removeDeprecated } from "../../automations/utils"
|
||||
import { updateTestHistory } from "../../automations/utils"
|
||||
import { withTestFlag } from "../../utilities/redis"
|
||||
import { context, cache, events, db as dbCore } from "@budibase/backend-core"
|
||||
import { automations, features } from "@budibase/pro"
|
||||
|
@ -34,14 +34,6 @@ import sdk from "../../sdk"
|
|||
import { builderSocket } from "../../websockets"
|
||||
import env from "../../environment"
|
||||
|
||||
async function getActionDefinitions() {
|
||||
return removeDeprecated(await actionDefs())
|
||||
}
|
||||
|
||||
function getTriggerDefinitions() {
|
||||
return removeDeprecated(triggers.TRIGGER_DEFINITIONS)
|
||||
}
|
||||
|
||||
/*************************
|
||||
* *
|
||||
* BUILDER FUNCTIONS *
|
||||
|
@ -141,21 +133,21 @@ export async function clearLogError(
|
|||
export async function getActionList(
|
||||
ctx: UserCtx<void, GetAutomationActionDefinitionsResponse>
|
||||
) {
|
||||
ctx.body = await getActionDefinitions()
|
||||
ctx.body = await actionDefs()
|
||||
}
|
||||
|
||||
export async function getTriggerList(
|
||||
ctx: UserCtx<void, GetAutomationTriggerDefinitionsResponse>
|
||||
) {
|
||||
ctx.body = getTriggerDefinitions()
|
||||
ctx.body = triggers.TRIGGER_DEFINITIONS
|
||||
}
|
||||
|
||||
export async function getDefinitionList(
|
||||
ctx: UserCtx<void, GetAutomationStepDefinitionsResponse>
|
||||
) {
|
||||
ctx.body = {
|
||||
trigger: getTriggerDefinitions(),
|
||||
action: await getActionDefinitions(),
|
||||
trigger: triggers.TRIGGER_DEFINITIONS,
|
||||
action: await actionDefs(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import {
|
|||
Table,
|
||||
} from "@budibase/types"
|
||||
import { mocks } from "@budibase/backend-core/tests"
|
||||
import { removeDeprecated } from "../../../automations/utils"
|
||||
import { createAutomationBuilder } from "../../../automations/tests/utilities/AutomationTestBuilder"
|
||||
import { basicTable } from "../../../tests/utilities/structures"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
|
@ -64,15 +63,11 @@ describe("/automations", () => {
|
|||
it("returns all of the definitions in one", async () => {
|
||||
const { action, trigger } = await config.api.automation.getDefinitions()
|
||||
|
||||
let definitionsLength = Object.keys(
|
||||
removeDeprecated(BUILTIN_ACTION_DEFINITIONS)
|
||||
).length
|
||||
|
||||
expect(Object.keys(action).length).toBeGreaterThanOrEqual(
|
||||
definitionsLength
|
||||
Object.keys(BUILTIN_ACTION_DEFINITIONS).length
|
||||
)
|
||||
expect(Object.keys(trigger).length).toEqual(
|
||||
Object.keys(removeDeprecated(TRIGGER_DEFINITIONS)).length
|
||||
Object.keys(TRIGGER_DEFINITIONS).length
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as createRow from "./steps/createRow"
|
|||
import * as updateRow from "./steps/updateRow"
|
||||
import * as deleteRow from "./steps/deleteRow"
|
||||
import * as executeScript from "./steps/executeScript"
|
||||
import * as executeScriptV2 from "./steps/executeScriptV2"
|
||||
import * as executeQuery from "./steps/executeQuery"
|
||||
import * as outgoingWebhook from "./steps/outgoingWebhook"
|
||||
import * as serverLog from "./steps/serverLog"
|
||||
|
@ -44,6 +45,7 @@ const ACTION_IMPLS: ActionImplType = {
|
|||
DELETE_ROW: deleteRow.run,
|
||||
OUTGOING_WEBHOOK: outgoingWebhook.run,
|
||||
EXECUTE_SCRIPT: executeScript.run,
|
||||
EXECUTE_SCRIPT_V2: executeScriptV2.run,
|
||||
EXECUTE_QUERY: executeQuery.run,
|
||||
SERVER_LOG: serverLog.run,
|
||||
DELAY: delay.run,
|
||||
|
@ -70,6 +72,7 @@ export const BUILTIN_ACTION_DEFINITIONS: Record<
|
|||
DELETE_ROW: automations.steps.deleteRow.definition,
|
||||
OUTGOING_WEBHOOK: automations.steps.outgoingWebhook.definition,
|
||||
EXECUTE_SCRIPT: automations.steps.executeScript.definition,
|
||||
EXECUTE_SCRIPT_V2: automations.steps.executeScriptV2.definition,
|
||||
EXECUTE_QUERY: automations.steps.executeQuery.definition,
|
||||
SERVER_LOG: automations.steps.serverLog.definition,
|
||||
DELAY: automations.steps.delay.definition,
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import * as automationUtils from "../automationUtils"
|
||||
import {
|
||||
ExecuteScriptStepInputs,
|
||||
ExecuteScriptStepOutputs,
|
||||
} from "@budibase/types"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
|
||||
export async function run({
|
||||
inputs,
|
||||
context,
|
||||
}: {
|
||||
inputs: ExecuteScriptStepInputs
|
||||
context: Record<string, any>
|
||||
}): Promise<ExecuteScriptStepOutputs> {
|
||||
let { code } = inputs
|
||||
|
||||
if (code == null) {
|
||||
return {
|
||||
success: false,
|
||||
response: {
|
||||
message: "Invalid inputs",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
code = code.trim()
|
||||
|
||||
if (!code.startsWith("{{ js ")) {
|
||||
return {
|
||||
success: false,
|
||||
response: {
|
||||
message: "Expected code to be a {{ js }} template block",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
success: true,
|
||||
value: processStringSync(inputs.code, context, { noThrow: false }),
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
response: automationUtils.getError(err),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
import * as automation from "../index"
|
||||
import * as setup from "./utilities"
|
||||
import { Table } from "@budibase/types"
|
||||
|
||||
function encodeJS(js: string): string {
|
||||
return `{{ js "${Buffer.from(js, "utf-8").toString("base64")}" }}`
|
||||
}
|
||||
|
||||
describe("Execute Script Automations", () => {
|
||||
let config = setup.getConfig(),
|
||||
table: Table
|
||||
|
||||
beforeEach(async () => {
|
||||
await automation.init()
|
||||
await config.init()
|
||||
table = await config.createTable()
|
||||
await config.createRow()
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
it("should execute a basic script and return the result", async () => {
|
||||
config.name = "Basic Script Execution"
|
||||
const builder = createAutomationBuilder(config)
|
||||
|
||||
const results = await builder
|
||||
.onAppAction()
|
||||
.executeScriptV2({ code: encodeJS("return 2 + 2") })
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.value).toEqual(4)
|
||||
})
|
||||
|
||||
it("should access bindings from previous steps", async () => {
|
||||
config.name = "Access Bindings"
|
||||
const builder = createAutomationBuilder(config)
|
||||
|
||||
const results = await builder
|
||||
.onAppAction()
|
||||
.executeScriptV2(
|
||||
{
|
||||
code: encodeJS(`return $("trigger.fields.data").map(x => x * 2)`),
|
||||
},
|
||||
{ stepId: "binding-script-step" }
|
||||
)
|
||||
.test({ fields: { data: [1, 2, 3] } })
|
||||
|
||||
expect(results.steps[0].outputs.value).toEqual([2, 4, 6])
|
||||
})
|
||||
|
||||
it("should handle script execution errors gracefully", async () => {
|
||||
config.name = "Handle Script Errors"
|
||||
const builder = createAutomationBuilder(config)
|
||||
|
||||
const results = await builder
|
||||
.onAppAction()
|
||||
.executeScriptV2({
|
||||
code: encodeJS("return nonexistentVariable.map(x => x)"),
|
||||
})
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.response).toContain(
|
||||
"ReferenceError: nonexistentVariable is not defined"
|
||||
)
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should handle conditional logic in scripts", async () => {
|
||||
config.name = "Conditional Script Logic"
|
||||
const builder = createAutomationBuilder(config)
|
||||
|
||||
const results = await builder
|
||||
.onAppAction()
|
||||
.executeScriptV2({
|
||||
code: encodeJS(`
|
||||
if ($("trigger.fields.value") > 5) {
|
||||
return "Value is greater than 5";
|
||||
} else {
|
||||
return "Value is 5 or less";
|
||||
}
|
||||
`),
|
||||
})
|
||||
.test({ fields: { value: 10 } })
|
||||
|
||||
expect(results.steps[0].outputs.value).toEqual("Value is greater than 5")
|
||||
})
|
||||
|
||||
it("should use multiple steps and validate script execution", async () => {
|
||||
config.name = "Multi-Step Script Execution"
|
||||
const builder = createAutomationBuilder(config)
|
||||
|
||||
const results = await builder
|
||||
.onAppAction()
|
||||
.serverLog(
|
||||
{ text: "Starting multi-step automation" },
|
||||
{ stepId: "start-log-step" }
|
||||
)
|
||||
.createRow(
|
||||
{ row: { name: "Test Row", value: 42, tableId: table._id } },
|
||||
{ stepId: "abc123" }
|
||||
)
|
||||
.executeScriptV2(
|
||||
{
|
||||
code: encodeJS(`
|
||||
const createdRow = $("steps")['abc123'];
|
||||
return createdRow.row.value * 2;
|
||||
`),
|
||||
},
|
||||
{ stepId: "ScriptingStep1" }
|
||||
)
|
||||
.serverLog({
|
||||
text: `Final result is {{ steps.ScriptingStep1.value }}`,
|
||||
})
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.message).toContain(
|
||||
"Starting multi-step automation"
|
||||
)
|
||||
expect(results.steps[1].outputs.row.value).toEqual(42)
|
||||
expect(results.steps[2].outputs.value).toEqual(84)
|
||||
expect(results.steps[3].outputs.message).toContain("Final result is 84")
|
||||
})
|
||||
|
||||
it("should fail if the code has not been encoded as a handlebars template", async () => {
|
||||
config.name = "Invalid Code Encoding"
|
||||
const builder = createAutomationBuilder(config)
|
||||
|
||||
const results = await builder
|
||||
.onAppAction()
|
||||
.executeScriptV2({
|
||||
code: "return 2 + 2",
|
||||
})
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.response.message).toEqual(
|
||||
"Expected code to be a {{ js }} template block"
|
||||
)
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("does not process embedded handlebars templates", async () => {
|
||||
config.name = "Embedded Handlebars"
|
||||
const builder = createAutomationBuilder(config)
|
||||
|
||||
const results = await builder
|
||||
.onAppAction()
|
||||
.executeScriptV2({
|
||||
code: encodeJS(`return "{{ triggers.row.whatever }}"`),
|
||||
})
|
||||
.test({ fields: {} })
|
||||
|
||||
expect(results.steps[0].outputs.value).toEqual(
|
||||
"{{ triggers.row.whatever }}"
|
||||
)
|
||||
expect(results.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
})
|
|
@ -100,6 +100,7 @@ class BranchStepBuilder<TStep extends AutomationTriggerStepId> {
|
|||
loop = this.step(AutomationActionStepId.LOOP)
|
||||
serverLog = this.step(AutomationActionStepId.SERVER_LOG)
|
||||
executeScript = this.step(AutomationActionStepId.EXECUTE_SCRIPT)
|
||||
executeScriptV2 = this.step(AutomationActionStepId.EXECUTE_SCRIPT_V2)
|
||||
filter = this.step(AutomationActionStepId.FILTER)
|
||||
bash = this.step(AutomationActionStepId.EXECUTE_BASH)
|
||||
openai = this.step(AutomationActionStepId.OPENAI)
|
||||
|
|
|
@ -4,17 +4,8 @@ import { automationQueue } from "./bullboard"
|
|||
import { updateEntityMetadata } from "../utilities"
|
||||
import { context, db as dbCore, utils } from "@budibase/backend-core"
|
||||
import { getAutomationMetadataParams } from "../db/utils"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { quotas } from "@budibase/pro"
|
||||
import {
|
||||
Automation,
|
||||
AutomationActionStepId,
|
||||
AutomationJob,
|
||||
AutomationStepDefinition,
|
||||
AutomationTriggerDefinition,
|
||||
AutomationTriggerStepId,
|
||||
MetadataType,
|
||||
} from "@budibase/types"
|
||||
import { Automation, AutomationJob, MetadataType } from "@budibase/types"
|
||||
import { automationsEnabled } from "../features"
|
||||
import { helpers, REBOOT_CRON } from "@budibase/shared-core"
|
||||
import tracer from "dd-trace"
|
||||
|
@ -113,23 +104,6 @@ export async function updateTestHistory(
|
|||
)
|
||||
}
|
||||
|
||||
export function removeDeprecated<
|
||||
T extends
|
||||
| Record<keyof typeof AutomationTriggerStepId, AutomationTriggerDefinition>
|
||||
| Record<keyof typeof AutomationActionStepId, AutomationStepDefinition>
|
||||
>(definitions: T): T {
|
||||
const base: Record<
|
||||
string,
|
||||
AutomationTriggerDefinition | AutomationStepDefinition
|
||||
> = cloneDeep(definitions)
|
||||
for (let key of Object.keys(base)) {
|
||||
if (base[key].deprecated) {
|
||||
delete base[key]
|
||||
}
|
||||
}
|
||||
return base as T
|
||||
}
|
||||
|
||||
// end the repetition and the job itself
|
||||
export async function disableAllCrons(appId: any) {
|
||||
const promises = []
|
||||
|
|
|
@ -99,6 +99,7 @@ export default class TestConfiguration {
|
|||
request?: supertest.SuperTest<supertest.Test>
|
||||
started: boolean
|
||||
appId?: string
|
||||
name?: string
|
||||
allApps: App[]
|
||||
app?: App
|
||||
prodApp?: App
|
||||
|
|
|
@ -555,8 +555,16 @@ class Orchestrator {
|
|||
throw new Error(`Cannot find automation step by name ${step.stepId}`)
|
||||
}
|
||||
|
||||
const inputs = automationUtils.cleanInputValues(
|
||||
await processObject(cloneDeep(step.inputs), ctx),
|
||||
let inputs = cloneDeep(step.inputs)
|
||||
if (step.stepId !== AutomationActionStepId.EXECUTE_SCRIPT_V2) {
|
||||
// The EXECUTE_SCRIPT_V2 step saves its input.code value as a `{{ js
|
||||
// "..." }}` template, and expects to receive it that way in the
|
||||
// function that runs it. So we skip this next bit for that step.
|
||||
inputs = await processObject(inputs, ctx)
|
||||
}
|
||||
|
||||
inputs = automationUtils.cleanInputValues(
|
||||
inputs,
|
||||
step.schema.inputs.properties
|
||||
)
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
export const definition: AutomationStepDefinition = {
|
||||
name: "JS Scripting",
|
||||
tagline: "Execute JavaScript Code",
|
||||
deprecated: true,
|
||||
icon: "Code",
|
||||
description: "Run a piece of JavaScript code in your automation",
|
||||
type: AutomationStepType.ACTION,
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import {
|
||||
AutomationActionStepId,
|
||||
AutomationCustomIOType,
|
||||
AutomationFeature,
|
||||
AutomationIOType,
|
||||
AutomationStepDefinition,
|
||||
AutomationStepType,
|
||||
} from "@budibase/types"
|
||||
|
||||
export const definition: AutomationStepDefinition = {
|
||||
name: "JavaScript",
|
||||
tagline: "Execute JavaScript Code",
|
||||
icon: "Brackets",
|
||||
description: "Run a piece of JavaScript code in your automation",
|
||||
type: AutomationStepType.ACTION,
|
||||
internal: true,
|
||||
new: true,
|
||||
stepId: AutomationActionStepId.EXECUTE_SCRIPT_V2,
|
||||
inputs: {},
|
||||
features: {
|
||||
[AutomationFeature.LOOPING]: true,
|
||||
},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {
|
||||
code: {
|
||||
type: AutomationIOType.STRING,
|
||||
customType: AutomationCustomIOType.CODE,
|
||||
title: "Code",
|
||||
},
|
||||
},
|
||||
required: ["code"],
|
||||
},
|
||||
outputs: {
|
||||
properties: {
|
||||
value: {
|
||||
type: AutomationIOType.STRING,
|
||||
description: "The result of the return statement",
|
||||
},
|
||||
success: {
|
||||
type: AutomationIOType.BOOLEAN,
|
||||
description: "Whether the action was successful",
|
||||
},
|
||||
},
|
||||
required: ["success"],
|
||||
},
|
||||
},
|
||||
}
|
|
@ -7,6 +7,7 @@ export * as deleteRow from "./deleteRow"
|
|||
export * as discord from "./discord"
|
||||
export * as executeQuery from "./executeQuery"
|
||||
export * as executeScript from "./executeScript"
|
||||
export * as executeScriptV2 from "./executeScriptV2"
|
||||
export * as filter from "./filter"
|
||||
export * as loop from "./loop"
|
||||
export * as make from "./make"
|
||||
|
|
|
@ -44,6 +44,19 @@ const ADDED_HELPERS = {
|
|||
description:
|
||||
"Produce a humanized duration left/until given an amount of time and the type of time measurement.",
|
||||
},
|
||||
difference: {
|
||||
args: ["from", "to", "[unitType=ms]"],
|
||||
example:
|
||||
'{{ difference "2025-09-30" "2025-06-17" "seconds" }} -> 9072000',
|
||||
description:
|
||||
"Gets the difference between two dates, in milliseconds. Pass a third parameter to adjust the unit measurement.",
|
||||
},
|
||||
durationFromNow: {
|
||||
args: ["time"],
|
||||
example: '{{durationFromNow "2021-09-30"}} -> 8 months',
|
||||
description:
|
||||
"Produce a humanized duration left/until given an amount of time and the type of time measurement.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import dayjs from "dayjs"
|
||||
import dayjs, { UnitType } from "dayjs"
|
||||
|
||||
import dayjsDurationPlugin from "dayjs/plugin/duration"
|
||||
import dayjsAdvancedFormatPlugin from "dayjs/plugin/advancedFormat"
|
||||
|
@ -121,7 +121,7 @@ export const date = (str: any, pattern: any, options: any) => {
|
|||
return date.format(config.pattern)
|
||||
}
|
||||
|
||||
export const duration = (str: any, pattern: any, format: any) => {
|
||||
export const duration = (str: any, pattern: any, format?: any) => {
|
||||
const config = initialConfig(str, pattern)
|
||||
|
||||
setLocale(config.str, config.pattern)
|
||||
|
@ -133,3 +133,13 @@ export const duration = (str: any, pattern: any, format: any) => {
|
|||
return duration.humanize()
|
||||
}
|
||||
}
|
||||
|
||||
export const difference = (from: string, to: string, units?: UnitType) => {
|
||||
const result = dayjs(new Date(from)).diff(dayjs(new Date(to)), units)
|
||||
return result
|
||||
}
|
||||
|
||||
export const durationFromNow = (from: string) => {
|
||||
const diff = difference(from, new Date().toISOString(), "ms")
|
||||
return duration(diff, "ms")
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @ts-ignore we don't have types for it
|
||||
import helpers from "@budibase/handlebars-helpers"
|
||||
|
||||
import { date, duration } from "./date"
|
||||
import { date, difference, duration, durationFromNow } from "./date"
|
||||
import {
|
||||
HelperFunctionBuiltin,
|
||||
EXTERNAL_FUNCTION_COLLECTIONS,
|
||||
|
@ -9,8 +9,10 @@ import {
|
|||
import Handlebars from "handlebars"
|
||||
|
||||
const ADDED_HELPERS = {
|
||||
date: date,
|
||||
duration: duration,
|
||||
date,
|
||||
duration,
|
||||
difference,
|
||||
durationFromNow,
|
||||
}
|
||||
|
||||
export const externalCollections = EXTERNAL_FUNCTION_COLLECTIONS
|
||||
|
|
|
@ -1222,6 +1222,22 @@
|
|||
],
|
||||
"example": "{{duration 8 \"seconds\"}} -> a few seconds",
|
||||
"description": "<p>Produce a humanized duration left/until given an amount of time and the type of time measurement.</p>\n"
|
||||
},
|
||||
"difference": {
|
||||
"args": [
|
||||
"from",
|
||||
"to",
|
||||
"[unitType=ms]"
|
||||
],
|
||||
"example": "{{ difference \"2025-09-30\" \"2025-06-17\" \"seconds\" }} -> 9072000",
|
||||
"description": "<p>Gets the difference between two dates, in milliseconds. Pass a third parameter to adjust the unit measurement.</p>\n"
|
||||
},
|
||||
"durationFromNow": {
|
||||
"args": [
|
||||
"time"
|
||||
],
|
||||
"example": "{{durationFromNow \"2021-09-30\"}} -> 8 months",
|
||||
"description": "<p>Produce a humanized duration left/until given an amount of time and the type of time measurement.</p>\n"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import tk from "timekeeper"
|
||||
import * as date from "../../src/helpers/date"
|
||||
|
||||
const frozenDate = new Date("2025-03-06T11:38:41.000Z")
|
||||
tk.freeze(frozenDate)
|
||||
|
||||
describe("date helper", () => {
|
||||
describe("difference", () => {
|
||||
it("should return the difference between two dates", () => {
|
||||
const result = date.difference(
|
||||
"2021-01-02T12:34:56.789Z",
|
||||
"2021-01-01T01:00:00.000Z"
|
||||
)
|
||||
const expected =
|
||||
1 * 24 * 60 * 60 * 1000 + // 1 day
|
||||
11 * 60 * 60 * 1000 + // 11 hours
|
||||
34 * 60 * 1000 + // 34 minutes
|
||||
56 * 1000 + // seconds
|
||||
789 // milliseconds
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
|
||||
it("should be able to set the time unit", () => {
|
||||
const result = date.difference(
|
||||
"2021-01-02T12:34:56",
|
||||
"2021-01-01T01:00:00",
|
||||
"days"
|
||||
)
|
||||
expect(result).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("durationFromNow", () => {
|
||||
it("should return the difference between two close dates", () => {
|
||||
const result = date.durationFromNow("2025-03-06T11:38:43.000Z")
|
||||
expect(result).toEqual("a few seconds")
|
||||
})
|
||||
|
||||
it("should return the difference between two days hours apart", () => {
|
||||
const result = date.durationFromNow("2025-03-06T01:00:00.000Z")
|
||||
expect(result).toEqual("11 hours")
|
||||
})
|
||||
|
||||
it("accepts days in the past", () => {
|
||||
const result = date.durationFromNow("2025-03-01")
|
||||
expect(result).toEqual("5 days")
|
||||
})
|
||||
|
||||
it("accepts days in the future", () => {
|
||||
const result = date.durationFromNow("2025-03-08")
|
||||
expect(result).toEqual("2 days")
|
||||
})
|
||||
})
|
||||
})
|
|
@ -63,6 +63,7 @@ export enum AutomationActionStepId {
|
|||
EXECUTE_BASH = "EXECUTE_BASH",
|
||||
OUTGOING_WEBHOOK = "OUTGOING_WEBHOOK",
|
||||
EXECUTE_SCRIPT = "EXECUTE_SCRIPT",
|
||||
EXECUTE_SCRIPT_V2 = "EXECUTE_SCRIPT_V2",
|
||||
EXECUTE_QUERY = "EXECUTE_QUERY",
|
||||
SERVER_LOG = "SERVER_LOG",
|
||||
DELAY = "DELAY",
|
||||
|
|
|
@ -84,6 +84,10 @@ export type ActionImplementations<T extends Hosting> = {
|
|||
ExecuteScriptStepInputs,
|
||||
ExecuteScriptStepOutputs
|
||||
>
|
||||
[AutomationActionStepId.EXECUTE_SCRIPT_V2]: ActionImplementation<
|
||||
ExecuteScriptStepInputs,
|
||||
ExecuteScriptStepOutputs
|
||||
>
|
||||
[AutomationActionStepId.FILTER]: ActionImplementation<
|
||||
FilterStepInputs,
|
||||
FilterStepOutputs
|
||||
|
@ -155,6 +159,7 @@ export interface AutomationStepSchemaBase {
|
|||
type: AutomationStepType
|
||||
internal?: boolean
|
||||
deprecated?: boolean
|
||||
new?: boolean
|
||||
blockToLoop?: string
|
||||
schema: {
|
||||
inputs: InputOutputBlock
|
||||
|
@ -177,6 +182,8 @@ export type AutomationStepInputs<T extends AutomationActionStepId> =
|
|||
? ExecuteQueryStepInputs
|
||||
: T extends AutomationActionStepId.EXECUTE_SCRIPT
|
||||
? ExecuteScriptStepInputs
|
||||
: T extends AutomationActionStepId.EXECUTE_SCRIPT_V2
|
||||
? ExecuteScriptStepInputs
|
||||
: T extends AutomationActionStepId.FILTER
|
||||
? FilterStepInputs
|
||||
: T extends AutomationActionStepId.QUERY_ROWS
|
||||
|
@ -279,6 +286,9 @@ export type ExecuteQueryStep =
|
|||
export type ExecuteScriptStep =
|
||||
AutomationStepSchema<AutomationActionStepId.EXECUTE_SCRIPT>
|
||||
|
||||
export type ExecuteScriptV2Step =
|
||||
AutomationStepSchema<AutomationActionStepId.EXECUTE_SCRIPT_V2>
|
||||
|
||||
export type FilterStep = AutomationStepSchema<AutomationActionStepId.FILTER>
|
||||
|
||||
export type QueryRowsStep =
|
||||
|
@ -325,6 +335,7 @@ export type AutomationStep =
|
|||
| DeleteRowStep
|
||||
| ExecuteQueryStep
|
||||
| ExecuteScriptStep
|
||||
| ExecuteScriptV2Step
|
||||
| FilterStep
|
||||
| QueryRowsStep
|
||||
| SendEmailSmtpStep
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
import {
|
||||
GetAutomationActionDefinitionsResponse,
|
||||
GetAutomationTriggerDefinitionsResponse,
|
||||
} from "../../api"
|
||||
|
||||
export interface BranchPath {
|
||||
stepIdx: number
|
||||
branchIdx: number
|
||||
|
@ -6,7 +11,7 @@ export interface BranchPath {
|
|||
}
|
||||
|
||||
export interface BlockDefinitions {
|
||||
TRIGGER: Record<string, any>
|
||||
CREATABLE_TRIGGER: Record<string, any>
|
||||
ACTION: Record<string, any>
|
||||
TRIGGER: Partial<GetAutomationTriggerDefinitionsResponse>
|
||||
CREATABLE_TRIGGER: Partial<GetAutomationTriggerDefinitionsResponse>
|
||||
ACTION: Partial<GetAutomationActionDefinitionsResponse>
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue