diff --git a/lerna.json b/lerna.json
index f8e3772d9f..6c507bdd92 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "2.14.7",
+ "version": "2.14.8",
"npmClient": "yarn",
"packages": [
"packages/*",
diff --git a/packages/builder/package.json b/packages/builder/package.json
index 9472c51965..273994ed0e 100644
--- a/packages/builder/package.json
+++ b/packages/builder/package.json
@@ -66,6 +66,7 @@
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
+ "@zerodevx/svelte-json-view": "^1.0.7",
"codemirror": "^5.59.0",
"dayjs": "^1.10.8",
"downloadjs": "1.4.7",
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte
index 9f7aaa68ce..ef591d5635 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/ActionModal.svelte
@@ -19,10 +19,15 @@
export let lastStep
let syncAutomationsEnabled = $licensing.syncAutomationsEnabled
+ let triggerAutomationRunEnabled = $licensing.triggerAutomationRunEnabled
let collectBlockAllowedSteps = [TriggerStepID.APP, TriggerStepID.WEBHOOK]
let selectedAction
let actionVal
let actions = Object.entries($automationStore.blockDefinitions.ACTION)
+ let lockedFeatures = [
+ ActionStepID.COLLECT,
+ ActionStepID.TRIGGER_AUTOMATION_RUN,
+ ]
$: collectBlockExists = checkForCollectStep($selectedAutomation)
@@ -36,6 +41,10 @@
disabled: !lastStep || !syncAutomationsEnabled || collectBlockExists,
message: collectDisabledMessage(),
},
+ TRIGGER_AUTOMATION_RUN: {
+ disabled: !triggerAutomationRunEnabled,
+ message: collectDisabledMessage(),
+ },
}
}
@@ -149,7 +158,7 @@
{action.name}
- {#if isDisabled && !syncAutomationsEnabled && action.stepId === ActionStepID.COLLECT}
+ {#if isDisabled && !syncAutomationsEnabled && !triggerAutomationRunEnabled && lockedFeatures.includes(action.stepId)}
Premium
diff --git a/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte
index 89406f15d8..9fbc4b6bc1 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/TestDisplay.svelte
@@ -1,7 +1,8 @@
-
-
-
-
-
-
+
+
+ modal.show()}
+ />
+
+
+ {#each filteredAutomations as automation}
+
selectAutomation(automation._id)}
+ selectedBy={$userSelectedResourceMap[automation._id]}
+ >
+
+
+ {/each}
+
+ {#if showNoResults}
+
+
+ There aren't any automations matching that name
+
+
+ {/if}
+
+
diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
index 158ecd8281..a5a3165aeb 100644
--- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
+++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte
@@ -28,6 +28,7 @@
import CodeEditorModal from "./CodeEditorModal.svelte"
import QuerySelector from "./QuerySelector.svelte"
import QueryParamSelector from "./QueryParamSelector.svelte"
+ import AutomationSelector from "./AutomationSelector.svelte"
import CronBuilder from "./CronBuilder.svelte"
import Editor from "components/integration/QueryEditor.svelte"
import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte"
@@ -51,7 +52,6 @@
export let testData
export let schemaProperties
export let isTestModal = false
-
let webhookModal
let drawer
let fillWidth = true
@@ -101,7 +101,6 @@
}
}
}
-
const onChange = Utils.sequential(async (e, key) => {
// We need to cache the schema as part of the definition because it is
// used in the server to detect relationships. It would be far better to
@@ -145,6 +144,7 @@
if (!block || !automation) {
return []
}
+
// Find previous steps to the selected one
let allSteps = [...automation.steps]
@@ -156,22 +156,96 @@
// Extract all outputs from all previous steps as available bindingsx§x
let bindings = []
let loopBlockCount = 0
+ const addBinding = (name, value, icon, idx, isLoopBlock, bindingName) => {
+ const runtimeBinding = determineRuntimeBinding(name, idx, isLoopBlock)
+ const categoryName = determineCategoryName(idx, isLoopBlock, bindingName)
+
+ bindings.push(
+ createBindingObject(
+ name,
+ value,
+ icon,
+ idx,
+ loopBlockCount,
+ isLoopBlock,
+ runtimeBinding,
+ categoryName,
+ bindingName
+ )
+ )
+ }
+
+ const determineRuntimeBinding = (name, idx, isLoopBlock) => {
+ let runtimeName
+
+ /* Begin special cases for generating custom schemas based on triggers */
+ if (idx === 0 && automation.trigger?.event === "app:trigger") {
+ return `trigger.fields.${name}`
+ }
+
+ if (
+ (idx === 0 && automation.trigger?.event === "row:update") ||
+ automation.trigger?.event === "row:save"
+ ) {
+ if (name !== "id" && name !== "revision") return `trigger.row.${name}`
+ }
+ /* End special cases for generating custom schemas based on triggers */
+
+ if (isLoopBlock) {
+ runtimeName = `loop.${name}`
+ } else if (block.name.startsWith("JS")) {
+ runtimeName = `steps[${idx - loopBlockCount}].${name}`
+ } else {
+ runtimeName = `steps.${idx - loopBlockCount}.${name}`
+ }
+ return idx === 0 ? `trigger.${name}` : runtimeName
+ }
+
+ const determineCategoryName = (idx, isLoopBlock, bindingName) => {
+ if (idx === 0) return "Trigger outputs"
+ if (isLoopBlock) return "Loop Outputs"
+ return bindingName
+ ? `${bindingName} outputs`
+ : `Step ${idx - loopBlockCount} outputs`
+ }
+
+ const createBindingObject = (
+ name,
+ value,
+ icon,
+ idx,
+ loopBlockCount,
+ isLoopBlock,
+ runtimeBinding,
+ categoryName,
+ bindingName
+ ) => {
+ return {
+ readableBinding: bindingName
+ ? `${bindingName}.${name}`
+ : runtimeBinding,
+ runtimeBinding,
+ type: value.type,
+ description: value.description,
+ icon,
+ category: categoryName,
+ display: {
+ type: value.type,
+ name,
+ rank: isLoopBlock ? idx + 1 : idx - loopBlockCount,
+ },
+ }
+ }
+
for (let idx = 0; idx < blockIdx; idx++) {
let wasLoopBlock = allSteps[idx - 1]?.stepId === ActionStepID.LOOP
let isLoopBlock =
allSteps[idx]?.stepId === ActionStepID.LOOP &&
- allSteps.find(x => x.blockToLoop === block.id)
+ allSteps.some(x => x.blockToLoop === block.id)
+ let schema = cloneDeep(allSteps[idx]?.schema?.outputs?.properties) ?? {}
+ let bindingName =
+ automation.stepNames?.[allSteps[idx - loopBlockCount].id]
- // If the previous block was a loop block, decrement the index so the following
- // steps are in the correct order
- if (wasLoopBlock) {
- loopBlockCount++
- continue
- }
-
- let schema = allSteps[idx]?.schema?.outputs?.properties ?? {}
-
- // If its a Loop Block, we need to add this custom schema
if (isLoopBlock) {
schema = {
currentItem: {
@@ -180,54 +254,45 @@
},
}
}
- const outputs = Object.entries(schema)
- let bindingIcon = ""
- let bindingRank = 0
- if (idx === 0) {
- bindingIcon = automation.trigger.icon
- } else if (isLoopBlock) {
- bindingIcon = "Reuse"
- bindingRank = idx + 1
- } else {
- bindingIcon = allSteps[idx].icon
- bindingRank = idx - loopBlockCount
+
+ if (idx === 0 && automation.trigger?.event === "app:trigger") {
+ schema = Object.fromEntries(
+ Object.keys(automation.trigger.inputs.fields || []).map(key => [
+ key,
+ { type: automation.trigger.inputs.fields[key] },
+ ])
+ )
}
- let bindingName =
- automation.stepNames?.[allSteps[idx - loopBlockCount].id]
- bindings = bindings.concat(
- outputs.map(([name, value]) => {
- let runtimeName = isLoopBlock
- ? `loop.${name}`
- : block.name.startsWith("JS")
- ? `steps[${idx - loopBlockCount}].${name}`
- : `steps.${idx - loopBlockCount}.${name}`
- const runtime = idx === 0 ? `trigger.${name}` : runtimeName
-
- let categoryName
- if (idx === 0) {
- categoryName = "Trigger outputs"
- } else if (isLoopBlock) {
- categoryName = "Loop Outputs"
- } else if (bindingName) {
- categoryName = `${bindingName} outputs`
- } else {
- categoryName = `Step ${idx - loopBlockCount} outputs`
+ if (
+ (idx === 0 && automation.trigger.event === "row:update") ||
+ (idx === 0 && automation.trigger.event === "row:save")
+ ) {
+ let table = $tables.list.find(
+ table => table._id === automation.trigger.inputs.tableId
+ )
+ // We want to generate our own schema for the bindings from the table schema itself
+ for (const key in table?.schema) {
+ schema[key] = {
+ type: table.schema[key].type,
}
+ }
+ // remove the original binding
+ delete schema.row
+ }
+ let icon =
+ idx === 0
+ ? automation.trigger.icon
+ : isLoopBlock
+ ? "Reuse"
+ : allSteps[idx].icon
- return {
- readableBinding: bindingName ? `${bindingName}.${name}` : runtime,
- runtimeBinding: runtime,
- type: value.type,
- description: value.description,
- icon: bindingIcon,
- category: categoryName,
- display: {
- type: value.type,
- name: name,
- rank: bindingRank,
- },
- }
- })
+ if (wasLoopBlock) {
+ loopBlockCount++
+ continue
+ }
+
+ Object.entries(schema).forEach(([name, value]) =>
+ addBinding(name, value, icon, idx, isLoopBlock, bindingName)
)
}
@@ -245,10 +310,8 @@
})
)
}
-
return bindings
}
-
function lookForFilters(properties) {
if (!properties) {
return []
@@ -286,7 +349,8 @@
value.customType !== "code" &&
value.customType !== "queryParams" &&
value.customType !== "cron" &&
- value.customType !== "triggerSchema"
+ value.customType !== "triggerSchema" &&
+ value.customType !== "automationFields"
)
}
@@ -421,6 +485,12 @@
on:change={e => onChange(e, key)}
value={inputData[key]}
/>
+ {:else if value.customType === "automationFields"}
+ onChange(e, key)}
+ value={inputData[key]}
+ {bindings}
+ />
{:else if value.customType === "queryParams"}
onChange(e, key)}
diff --git a/packages/builder/src/components/automation/SetupPanel/AutomationSelector.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationSelector.svelte
new file mode 100644
index 0000000000..7e3ba92420
--- /dev/null
+++ b/packages/builder/src/components/automation/SetupPanel/AutomationSelector.svelte
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+{#if Object.keys(automationFields)}
+ {#each Object.keys(automationFields) as field}
+
+
+
+ onChange(e, field)}
+ type="string"
+ {bindings}
+ fillWidth={true}
+ updateOnChange={false}
+ />
+
+
+ {/each}
+{/if}
+
+
diff --git a/packages/builder/src/constants/backend/automations.js b/packages/builder/src/constants/backend/automations.js
index f89a126d3c..6981418fa7 100644
--- a/packages/builder/src/constants/backend/automations.js
+++ b/packages/builder/src/constants/backend/automations.js
@@ -21,6 +21,7 @@ export const ActionStepID = {
QUERY_ROWS: "QUERY_ROWS",
LOOP: "LOOP",
COLLECT: "COLLECT",
+ TRIGGER_AUTOMATION_RUN: "TRIGGER_AUTOMATION_RUN",
// these used to be lowercase step IDs, maintain for backwards compat
discord: "discord",
slack: "slack",
diff --git a/packages/builder/src/stores/portal/licensing.js b/packages/builder/src/stores/portal/licensing.js
index 3197822e53..8fef367f77 100644
--- a/packages/builder/src/stores/portal/licensing.js
+++ b/packages/builder/src/stores/portal/licensing.js
@@ -125,6 +125,10 @@ export const createLicensingStore = () => {
const syncAutomationsEnabled = license.features.includes(
Constants.Features.SYNC_AUTOMATIONS
)
+ const triggerAutomationRunEnabled = license.features.includes(
+ Constants.Features.TRIGGER_AUTOMATION_RUN
+ )
+
const perAppBuildersEnabled = license.features.includes(
Constants.Features.APP_BUILDERS
)
@@ -147,6 +151,7 @@ export const createLicensingStore = () => {
auditLogsEnabled,
enforceableSSO,
syncAutomationsEnabled,
+ triggerAutomationRunEnabled,
isViewPermissionsEnabled,
perAppBuildersEnabled,
}
diff --git a/packages/pro b/packages/pro
index 8c466d6ef2..9d80daaa5b 160000
--- a/packages/pro
+++ b/packages/pro
@@ -1 +1 @@
-Subproject commit 8c466d6ef2a0c09b843ef63276793ab5af2e96f7
+Subproject commit 9d80daaa5b79da68730d6c5f497f629c47a78ef8
diff --git a/packages/server/__mocks__/@budibase/pro.ts b/packages/server/__mocks__/@budibase/pro.ts
new file mode 100644
index 0000000000..3f0c35d725
--- /dev/null
+++ b/packages/server/__mocks__/@budibase/pro.ts
@@ -0,0 +1,12 @@
+const actual = jest.requireActual("@budibase/pro")
+const pro = {
+ ...actual,
+ features: {
+ ...actual.features,
+ isTriggerAutomationRunEnabled: () => {
+ return true
+ },
+ },
+}
+
+export = pro
diff --git a/packages/server/src/automations/actions.ts b/packages/server/src/automations/actions.ts
index 81cf4d8176..ac8a340e82 100644
--- a/packages/server/src/automations/actions.ts
+++ b/packages/server/src/automations/actions.ts
@@ -15,6 +15,7 @@ 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 * as triggerAutomationRun from "./steps/triggerAutomationRun"
import env from "../environment"
import {
AutomationStepSchema,
@@ -41,6 +42,7 @@ const ACTION_IMPLS: Record<
FILTER: filter.run,
QUERY_ROWS: queryRow.run,
COLLECT: collect.run,
+ TRIGGER_AUTOMATION_RUN: triggerAutomationRun.run,
// these used to be lowercase step IDs, maintain for backwards compat
discord: discord.run,
slack: slack.run,
@@ -62,6 +64,7 @@ export const BUILTIN_ACTION_DEFINITIONS: Record =
QUERY_ROWS: queryRow.definition,
LOOP: loop.definition,
COLLECT: collect.definition,
+ TRIGGER_AUTOMATION_RUN: triggerAutomationRun.definition,
// these used to be lowercase step IDs, maintain for backwards compat
discord: discord.definition,
slack: slack.definition,
diff --git a/packages/server/src/automations/steps/filter.ts b/packages/server/src/automations/steps/filter.ts
index 3b000f8f29..6867809500 100644
--- a/packages/server/src/automations/steps/filter.ts
+++ b/packages/server/src/automations/steps/filter.ts
@@ -99,7 +99,7 @@ export async function run({ inputs }: AutomationStepInput) {
} else {
result = false
}
- return { success: true, result }
+ return { success: true, result, refValue: field, comparisonValue: value }
} catch (err) {
return { success: false, result: false }
}
diff --git a/packages/server/src/automations/steps/triggerAutomationRun.ts b/packages/server/src/automations/steps/triggerAutomationRun.ts
new file mode 100644
index 0000000000..cb6126ca01
--- /dev/null
+++ b/packages/server/src/automations/steps/triggerAutomationRun.ts
@@ -0,0 +1,90 @@
+import {
+ AutomationActionStepId,
+ AutomationStepSchema,
+ AutomationStepInput,
+ AutomationStepType,
+ AutomationIOType,
+ AutomationResults,
+ Automation,
+ AutomationCustomIOType,
+} from "@budibase/types"
+import * as triggers from "../triggers"
+import { db as dbCore, context } from "@budibase/backend-core"
+import { features } from "@budibase/pro"
+
+export const definition: AutomationStepSchema = {
+ name: "Trigger an automation",
+ tagline: "Triggers an automation synchronously",
+ icon: "Sync",
+ description: "Triggers an automation synchronously",
+ type: AutomationStepType.ACTION,
+ internal: true,
+ features: {},
+ stepId: AutomationActionStepId.TRIGGER_AUTOMATION_RUN,
+ inputs: {},
+ schema: {
+ inputs: {
+ properties: {
+ automation: {
+ type: AutomationIOType.OBJECT,
+ properties: {
+ automationId: {
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.AUTOMATION,
+ },
+ },
+ customType: AutomationCustomIOType.AUTOMATION_FIELDS,
+ title: "automatioFields",
+ required: ["automationId"],
+ },
+ timeout: {
+ type: AutomationIOType.NUMBER,
+ title: "Timeout (ms)",
+ },
+ },
+ required: ["automationId"],
+ },
+ outputs: {
+ properties: {
+ success: {
+ type: AutomationIOType.BOOLEAN,
+ description: "Whether the automation was successful",
+ },
+ value: {
+ type: AutomationIOType.OBJECT,
+ description: "Automation Result",
+ },
+ },
+ required: ["success", "value"],
+ },
+ },
+}
+
+export async function run({ inputs }: AutomationStepInput) {
+ const { automationId, ...fieldParams } = inputs.automation
+
+ if (await features.isTriggerAutomationRunEnabled()) {
+ if (!inputs.automation.automationId) {
+ return {
+ success: false,
+ }
+ } else {
+ const db = context.getAppDB()
+ let automation = await db.get(inputs.automation.automationId)
+
+ const response: AutomationResults = await triggers.externalTrigger(
+ automation,
+ {
+ fields: { ...fieldParams },
+ timeout: inputs.timeout * 1000 || 120000,
+ },
+ { getResponses: true }
+ )
+
+ return {
+ success: true,
+ value: response.steps,
+ }
+ }
+ }
+}
diff --git a/packages/server/src/automations/tests/triggerAutomationRun.spec.ts b/packages/server/src/automations/tests/triggerAutomationRun.spec.ts
new file mode 100644
index 0000000000..f8cf647e79
--- /dev/null
+++ b/packages/server/src/automations/tests/triggerAutomationRun.spec.ts
@@ -0,0 +1,43 @@
+jest.spyOn(global.console, "error")
+
+import * as setup from "./utilities"
+import * as automation from "../index"
+import { serverLogAutomation } from "../../tests/utilities/structures"
+
+describe("Test triggering an automation from another automation", () => {
+ let config = setup.getConfig()
+
+ beforeAll(async () => {
+ await automation.init()
+ await config.init()
+ })
+
+ afterAll(async () => {
+ await automation.shutdown()
+ setup.afterAll()
+ })
+
+ it("should trigger an other server log automation", async () => {
+ let automation = serverLogAutomation()
+ let newAutomation = await config.createAutomation(automation)
+
+ const inputs: any = {
+ automation: { automationId: newAutomation._id, timeout: 12000 },
+ }
+ const res = await setup.runStep(
+ setup.actions.TRIGGER_AUTOMATION_RUN.stepId,
+ inputs
+ )
+ // Check if the SERVER_LOG step was successful
+ expect(res.value[1].outputs.success).toBe(true)
+ })
+
+ it("should fail gracefully if the automation id is incorrect", async () => {
+ const inputs: any = { automation: { automationId: null, timeout: 12000 } }
+ const res = await setup.runStep(
+ setup.actions.TRIGGER_AUTOMATION_RUN.stepId,
+ inputs
+ )
+ expect(res.success).toBe(false)
+ })
+})
diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts
index 83be8b6d58..80aad3c1e2 100644
--- a/packages/server/src/tests/utilities/structures.ts
+++ b/packages/server/src/tests/utilities/structures.ts
@@ -21,6 +21,7 @@ import {
Table,
INTERNAL_TABLE_SOURCE_ID,
TableSourceType,
+ AutomationIOType,
} from "@budibase/types"
const { BUILTIN_ROLE_IDS } = roles
@@ -153,6 +154,56 @@ export function basicAutomation(appId?: string): Automation {
}
}
+export function serverLogAutomation(appId?: string): Automation {
+ return {
+ name: "My Automation",
+ screenId: "kasdkfldsafkl",
+ live: true,
+ uiTree: {},
+ definition: {
+ trigger: {
+ stepId: AutomationTriggerStepId.APP,
+ name: "test",
+ tagline: "test",
+ icon: "test",
+ description: "test",
+ type: AutomationStepType.TRIGGER,
+ id: "test",
+ inputs: {},
+ schema: {
+ inputs: {
+ properties: {},
+ },
+ outputs: {
+ properties: {},
+ },
+ },
+ },
+ steps: [
+ {
+ stepId: AutomationActionStepId.SERVER_LOG,
+ name: "Backend log",
+ tagline: "Console log a value in the backend",
+ icon: "Monitoring",
+ description: "Logs the given text to the server (using console.log)",
+ internal: true,
+ features: {
+ LOOPING: true,
+ },
+ inputs: {
+ text: "log statement",
+ },
+ schema: BUILTIN_ACTION_DEFINITIONS.SERVER_LOG.schema,
+ id: "y8lkZbeSe",
+ type: AutomationStepType.ACTION,
+ },
+ ],
+ },
+ type: "automation",
+ appId: appId!,
+ }
+}
+
export function loopAutomation(tableId: string, loopOpts?: any): Automation {
if (!loopOpts) {
loopOpts = {
diff --git a/packages/types/src/documents/app/automation.ts b/packages/types/src/documents/app/automation.ts
index 88ce5e9b9a..91a1a2ab68 100644
--- a/packages/types/src/documents/app/automation.ts
+++ b/packages/types/src/documents/app/automation.ts
@@ -28,6 +28,8 @@ export enum AutomationCustomIOType {
TRIGGER_SCHEMA = "triggerSchema",
CRON = "cron",
WEBHOOK_URL = "webhookUrl",
+ AUTOMATION = "automation",
+ AUTOMATION_FIELDS = "automationFields",
}
export enum AutomationTriggerStepId {
@@ -61,6 +63,7 @@ export enum AutomationActionStepId {
LOOP = "LOOP",
COLLECT = "COLLECT",
OPENAI = "OPENAI",
+ TRIGGER_AUTOMATION_RUN = "TRIGGER_AUTOMATION_RUN",
// these used to be lowercase step IDs, maintain for backwards compat
discord = "discord",
slack = "slack",
diff --git a/packages/types/src/sdk/licensing/feature.ts b/packages/types/src/sdk/licensing/feature.ts
index 732a4a6c77..a3f149d6da 100644
--- a/packages/types/src/sdk/licensing/feature.ts
+++ b/packages/types/src/sdk/licensing/feature.ts
@@ -9,6 +9,7 @@ export enum Feature {
BRANDING = "branding",
SCIM = "scim",
SYNC_AUTOMATIONS = "syncAutomations",
+ TRIGGER_AUTOMATION_RUN = "triggerAutomationRun",
APP_BUILDERS = "appBuilders",
OFFLINE = "offline",
EXPANDED_PUBLIC_API = "expandedPublicApi",
diff --git a/packages/types/src/sdk/locks.ts b/packages/types/src/sdk/locks.ts
index 0e6053a4db..c7c028a135 100644
--- a/packages/types/src/sdk/locks.ts
+++ b/packages/types/src/sdk/locks.ts
@@ -22,6 +22,7 @@ export enum LockName {
QUOTA_USAGE_EVENT = "quota_usage_event",
APP_MIGRATION = "app_migrations",
PROCESS_AUTO_COLUMNS = "process_auto_columns",
+ PROCESS_USER_INVITE = "process_user_invite",
}
export type LockOptions = {
diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts
index b0e3219656..28ba97b4e2 100644
--- a/packages/worker/src/api/controllers/global/users.ts
+++ b/packages/worker/src/api/controllers/global/users.ts
@@ -12,6 +12,8 @@ import {
InviteUserRequest,
InviteUsersRequest,
InviteUsersResponse,
+ LockName,
+ LockType,
MigrationType,
SaveUserResponse,
SearchUsersRequest,
@@ -27,6 +29,7 @@ import {
platform,
tenancy,
db,
+ locks,
} from "@budibase/backend-core"
import { checkAnyUserExists } from "../../../utilities/users"
import { isEmailConfigured } from "../../../utilities/email"
@@ -380,51 +383,60 @@ export const inviteAccept = async (
) => {
const { inviteCode, password, firstName, lastName } = ctx.request.body
try {
- // info is an extension of the user object that was stored by global
- const { email, info }: any = await cache.invite.getCode(inviteCode)
- await cache.invite.deleteCode(inviteCode)
- const user = await tenancy.doInTenant(info.tenantId, async () => {
- let request: any = {
- firstName,
- lastName,
- password,
- email,
- admin: { global: info?.admin?.global || false },
- roles: info.apps,
- tenantId: info.tenantId,
- }
- let builder: { global: boolean; apps?: string[] } = {
- global: info?.builder?.global || false,
- }
+ await locks.doWithLock(
+ {
+ type: LockType.AUTO_EXTEND,
+ name: LockName.PROCESS_USER_INVITE,
+ resource: inviteCode,
+ systemLock: true,
+ },
+ async () => {
+ // info is an extension of the user object that was stored by global
+ const { email, info } = await cache.invite.getCode(inviteCode)
+ const user = await tenancy.doInTenant(info.tenantId, async () => {
+ let request: any = {
+ firstName,
+ lastName,
+ password,
+ email,
+ admin: { global: info?.admin?.global || false },
+ roles: info.apps,
+ tenantId: info.tenantId,
+ }
+ const builder: { global: boolean; apps?: string[] } = {
+ global: info?.builder?.global || false,
+ }
- if (info?.builder?.apps) {
- builder.apps = info.builder.apps
- request.builder = builder
- }
- delete info.apps
- request = {
- ...request,
- ...info,
- }
+ if (info?.builder?.apps) {
+ builder.apps = info.builder.apps
+ request.builder = builder
+ }
+ delete info.apps
+ request = {
+ ...request,
+ ...info,
+ }
- const saved = await userSdk.db.save(request)
- const db = tenancy.getGlobalDB()
- const user = await db.get(saved._id)
- await events.user.inviteAccepted(user)
- return saved
- })
+ const saved = await userSdk.db.save(request)
+ await events.user.inviteAccepted(saved)
+ return saved
+ })
- ctx.body = {
- _id: user._id!,
- _rev: user._rev!,
- email: user.email,
- }
+ await cache.invite.deleteCode(inviteCode)
+
+ ctx.body = {
+ _id: user._id!,
+ _rev: user._rev!,
+ email: user.email,
+ }
+ }
+ )
} catch (err: any) {
if (err.code === ErrorCode.USAGE_LIMIT_EXCEEDED) {
// explicitly re-throw limit exceeded errors
ctx.throw(400, err)
}
console.warn("Error inviting user", err)
- ctx.throw(400, "Unable to create new user, invitation invalid.")
+ ctx.throw(400, err || "Unable to create new user, invitation invalid.")
}
}
diff --git a/scripts/removeWorkspaceDependencies.sh b/scripts/removeWorkspaceDependencies.sh
index ba27ae7edc..61eee4a10d 100755
--- a/scripts/removeWorkspaceDependencies.sh
+++ b/scripts/removeWorkspaceDependencies.sh
@@ -3,26 +3,14 @@
packages_to_remove="@budibase/backend-core @budibase/bbui @budibase/builder @budibase/cli @budibase/client @budibase/frontend-core @budibase/pro @budibase/sdk @budibase/server @budibase/shared-core @budibase/string-templates @budibase/types @budibase/worker"
package_json_path="$1"
-package_json=$(cat "$package_json_path")
process_package() {
pkg_path="$1"
- package_json=$(cat "$pkg_path")
- has_changes=false
for package_name in $packages_to_remove; do
- if echo "$package_json" | jq -e --arg package_name "$package_name" '.dependencies | has($package_name)' > /dev/null; then
- package_json=$(echo "$package_json" | jq "del(.dependencies[\"$package_name\"])")
- has_changes=true
- fi
+ jq "del(.dependencies[\"$package_name\"])" $pkg_path > tmp_file.json && mv tmp_file.json $pkg_path
+ jq "del(.resolutions[\"$package_name\"])" $pkg_path > tmp_file.json && mv tmp_file.json $pkg_path
done
-
- if [ "$has_changes" = true ]; then
- echo "$package_json" > "$pkg_path"
- fi
}
process_package "$package_json_path"
-
-package_json=$(cat "$package_json_path")
-echo "$package_json" | jq "del(.resolutions)" > "$1"