@@ -1028,6 +1033,7 @@
{isTrigger}
value={inputData[key]}
on:change={e => onChange({ [key]: e.detail })}
+ disabled={value.readonly}
/>
{:else if value.customType === "webhookUrl"}
diff --git a/packages/builder/src/stores/builder/automations.js b/packages/builder/src/stores/builder/automations.js
index b2335ff7e52..57c823da9b1 100644
--- a/packages/builder/src/stores/builder/automations.js
+++ b/packages/builder/src/stores/builder/automations.js
@@ -15,6 +15,7 @@ const initialAutomationState = {
ACTION: [],
},
selectedAutomationId: null,
+ automationDisplayData: {},
}
// If this functions, remove the actions elements
@@ -58,18 +59,19 @@ const automationActions = store => ({
return response
},
fetch: async () => {
- const responses = await Promise.all([
- API.getAutomations(),
+ const [automationResponse, definitions] = await Promise.all([
+ API.getAutomations({ enrich: true }),
API.getAutomationDefinitions(),
])
store.update(state => {
- state.automations = responses[0]
+ state.automations = automationResponse.automations
state.automations.sort((a, b) => {
return a.name < b.name ? -1 : 1
})
+ state.automationDisplayData = automationResponse.builderData
state.blockDefinitions = {
- TRIGGER: responses[1].trigger,
- ACTION: responses[1].action,
+ TRIGGER: definitions.trigger,
+ ACTION: definitions.action,
}
return state
})
@@ -102,19 +104,8 @@ const automationActions = store => ({
},
save: async automation => {
const response = await API.updateAutomation(automation)
- store.update(state => {
- const updatedAutomation = response.automation
- const existingIdx = state.automations.findIndex(
- existing => existing._id === automation._id
- )
- if (existingIdx !== -1) {
- state.automations.splice(existingIdx, 1, updatedAutomation)
- return state
- } else {
- state.automations = [...state.automations, updatedAutomation]
- }
- return state
- })
+
+ await store.actions.fetch()
return response.automation
},
delete: async automation => {
@@ -308,7 +299,9 @@ const automationActions = store => ({
if (!automation) {
return
}
- delete newAutomation.definition.stepNames[blockId]
+ if (newAutomation.definition.stepNames) {
+ delete newAutomation.definition.stepNames[blockId]
+ }
await store.actions.save(newAutomation)
},
@@ -384,3 +377,13 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
x => x._id === $automationStore.selectedAutomationId
)
})
+
+export const selectedAutomationDisplayData = derived(
+ [automationStore, selectedAutomation],
+ ([$automationStore, $selectedAutomation]) => {
+ if (!$selectedAutomation._id) {
+ return null
+ }
+ return $automationStore.automationDisplayData[$selectedAutomation._id]
+ }
+)
diff --git a/packages/builder/src/stores/builder/index.js b/packages/builder/src/stores/builder/index.js
index c47782c0f5d..aa0062dd7d5 100644
--- a/packages/builder/src/stores/builder/index.js
+++ b/packages/builder/src/stores/builder/index.js
@@ -11,6 +11,7 @@ import {
automationStore,
selectedAutomation,
automationHistoryStore,
+ selectedAutomationDisplayData,
} from "./automations.js"
import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
import { deploymentStore } from "./deployments.js"
@@ -44,6 +45,7 @@ export {
previewStore,
automationStore,
selectedAutomation,
+ selectedAutomationDisplayData,
automationHistoryStore,
sortedScreens,
userStore,
diff --git a/packages/frontend-core/src/api/automations.js b/packages/frontend-core/src/api/automations.js
index 37a834cf04b..ce5f7d3aba1 100644
--- a/packages/frontend-core/src/api/automations.js
+++ b/packages/frontend-core/src/api/automations.js
@@ -26,9 +26,14 @@ export const buildAutomationEndpoints = API => ({
/**
* Gets a list of all automations.
*/
- getAutomations: async () => {
+ getAutomations: async ({ enrich }) => {
+ const params = new URLSearchParams()
+ if (enrich) {
+ params.set("enrich", true)
+ }
+
return await API.get({
- url: "/api/automations",
+ url: `/api/automations?${params.toString()}`,
})
},
diff --git a/packages/server/src/api/controllers/automation.ts b/packages/server/src/api/controllers/automation.ts
index 7eca17e0e88..6177868303c 100644
--- a/packages/server/src/api/controllers/automation.ts
+++ b/packages/server/src/api/controllers/automation.ts
@@ -1,4 +1,5 @@
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 { setTestFlag, clearTestFlag } from "../../utilities/redis"
@@ -11,6 +12,7 @@ import {
AutomationResults,
UserCtx,
DeleteAutomationResponse,
+ FetchAutomationResponse,
} from "@budibase/types"
import { getActionDefinitions as actionDefs } from "../../automations/actions"
import sdk from "../../sdk"
@@ -73,8 +75,17 @@ export async function update(ctx: UserCtx) {
builderSocket?.emitAutomationUpdate(ctx, automation)
}
-export async function fetch(ctx: UserCtx) {
- ctx.body = await sdk.automations.fetch()
+export async function fetch(ctx: UserCtx
) {
+ const query: { enrich?: string } = ctx.request.query || {}
+ const enrich = query.enrich === "true"
+
+ const automations = await sdk.automations.fetch()
+ ctx.body = { automations }
+ if (enrich) {
+ ctx.body.builderData = await sdk.automations.utils.getBuilderData(
+ automations
+ )
+ }
}
export async function find(ctx: UserCtx) {
@@ -84,6 +95,11 @@ export async function find(ctx: UserCtx) {
export async function destroy(ctx: UserCtx) {
const automationId = ctx.params.id
+ const automation = await sdk.automations.get(ctx.params.id)
+ if (coreSdk.automations.isRowAction(automation)) {
+ ctx.throw("Row actions automations cannot be deleted", 422)
+ }
+
ctx.body = await sdk.automations.remove(automationId, ctx.params.rev)
builderSocket?.emitAutomationDeletion(ctx, automationId)
}
diff --git a/packages/server/src/api/controllers/rowAction/crud.ts b/packages/server/src/api/controllers/rowAction/crud.ts
index 640bc353788..bd5326d957a 100644
--- a/packages/server/src/api/controllers/rowAction/crud.ts
+++ b/packages/server/src/api/controllers/rowAction/crud.ts
@@ -31,7 +31,12 @@ export async function find(ctx: Ctx) {
actions: Object.entries(actions).reduce>(
(acc, [key, action]) => ({
...acc,
- [key]: { id: key, tableId: table._id!, ...action },
+ [key]: {
+ id: key,
+ tableId: table._id!,
+ name: action.name,
+ automationId: action.automationId,
+ },
}),
{}
),
@@ -50,7 +55,9 @@ export async function create(
ctx.body = {
tableId: table._id!,
- ...createdAction,
+ id: createdAction.id,
+ name: createdAction.name,
+ automationId: createdAction.automationId,
}
ctx.status = 201
}
@@ -61,13 +68,15 @@ export async function update(
const table = await getTable(ctx)
const { actionId } = ctx.params
- const actions = await sdk.rowActions.update(table._id!, actionId, {
+ const action = await sdk.rowActions.update(table._id!, actionId, {
name: ctx.request.body.name,
})
ctx.body = {
tableId: table._id!,
- ...actions,
+ id: action.id,
+ name: action.name,
+ automationId: action.automationId,
}
}
diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts
index 98d38f4973b..990828dcdec 100644
--- a/packages/server/src/api/routes/tests/automation.spec.ts
+++ b/packages/server/src/api/routes/tests/automation.spec.ts
@@ -398,7 +398,9 @@ describe("/automations", () => {
.expect("Content-Type", /json/)
.expect(200)
- expect(res.body[0]).toEqual(expect.objectContaining(autoConfig))
+ expect(res.body.automations[0]).toEqual(
+ expect.objectContaining(autoConfig)
+ )
})
it("should apply authorization to endpoint", async () => {
@@ -423,6 +425,22 @@ describe("/automations", () => {
expect(events.automation.deleted).toHaveBeenCalledTimes(1)
})
+ it("cannot delete a row action automation", async () => {
+ const automation = await config.createAutomation(
+ setup.structures.rowActionAutomation()
+ )
+ await request
+ .delete(`/api/automations/${automation._id}/${automation._rev}`)
+ .set(config.defaultHeaders())
+ .expect("Content-Type", /json/)
+ .expect(422, {
+ message: "Row actions automations cannot be deleted",
+ status: 422,
+ })
+
+ expect(events.automation.deleted).not.toHaveBeenCalled()
+ })
+
it("should apply authorization to endpoint", async () => {
const automation = await config.createAutomation()
await checkBuilderEndpoint({
diff --git a/packages/server/src/api/routes/tests/rowAction.spec.ts b/packages/server/src/api/routes/tests/rowAction.spec.ts
index efdfdd23925..5e043cb42cd 100644
--- a/packages/server/src/api/routes/tests/rowAction.spec.ts
+++ b/packages/server/src/api/routes/tests/rowAction.spec.ts
@@ -1,10 +1,17 @@
import _ from "lodash"
import tk from "timekeeper"
-import { CreateRowActionRequest, RowActionResponse } from "@budibase/types"
+import {
+ CreateRowActionRequest,
+ DocumentType,
+ RowActionResponse,
+} from "@budibase/types"
import * as setup from "./utilities"
import { generator } from "@budibase/backend-core/tests"
+const expectAutomationId = () =>
+ expect.stringMatching(`^${DocumentType.AUTOMATION}_.+`)
+
describe("/rowsActions", () => {
const config = setup.getConfig()
@@ -79,17 +86,19 @@ describe("/rowsActions", () => {
})
expect(res).toEqual({
+ name: rowAction.name,
id: expect.stringMatching(/^row_action_\w+/),
tableId: tableId,
- ...rowAction,
+ automationId: expectAutomationId(),
})
expect(await config.api.rowAction.find(tableId)).toEqual({
actions: {
[res.id]: {
- ...rowAction,
+ name: rowAction.name,
id: res.id,
tableId: tableId,
+ automationId: expectAutomationId(),
},
},
})
@@ -97,19 +106,13 @@ describe("/rowsActions", () => {
it("trims row action names", async () => {
const name = " action name "
- const res = await createRowAction(
- tableId,
- { name },
- {
- status: 201,
- }
- )
+ const res = await createRowAction(tableId, { name }, { status: 201 })
- expect(res).toEqual({
- id: expect.stringMatching(/^row_action_\w+/),
- tableId: tableId,
- name: "action name",
- })
+ expect(res).toEqual(
+ expect.objectContaining({
+ name: "action name",
+ })
+ )
expect(await config.api.rowAction.find(tableId)).toEqual({
actions: {
@@ -129,9 +132,24 @@ describe("/rowsActions", () => {
expect(await config.api.rowAction.find(tableId)).toEqual({
actions: {
- [responses[0].id]: { ...rowActions[0], id: responses[0].id, tableId },
- [responses[1].id]: { ...rowActions[1], id: responses[1].id, tableId },
- [responses[2].id]: { ...rowActions[2], id: responses[2].id, tableId },
+ [responses[0].id]: {
+ name: rowActions[0].name,
+ id: responses[0].id,
+ tableId,
+ automationId: expectAutomationId(),
+ },
+ [responses[1].id]: {
+ name: rowActions[1].name,
+ id: responses[1].id,
+ tableId,
+ automationId: expectAutomationId(),
+ },
+ [responses[2].id]: {
+ name: rowActions[2].name,
+ id: responses[2].id,
+ tableId,
+ automationId: expectAutomationId(),
+ },
},
})
})
@@ -152,7 +170,7 @@ describe("/rowsActions", () => {
it("ignores not valid row action data", async () => {
const rowAction = createRowActionRequest()
const dirtyRowAction = {
- ...rowAction,
+ name: rowAction.name,
id: generator.guid(),
valueToIgnore: generator.string(),
}
@@ -161,17 +179,19 @@ describe("/rowsActions", () => {
})
expect(res).toEqual({
+ name: rowAction.name,
id: expect.any(String),
tableId,
- ...rowAction,
+ automationId: expectAutomationId(),
})
expect(await config.api.rowAction.find(tableId)).toEqual({
actions: {
[res.id]: {
+ name: rowAction.name,
id: res.id,
tableId: tableId,
- ...rowAction,
+ automationId: expectAutomationId(),
},
},
})
@@ -213,6 +233,17 @@ describe("/rowsActions", () => {
await createRowAction(otherTable._id!, { name: action.name })
})
+
+ it("an automation is created when creating a new row action", async () => {
+ const action1 = await createRowAction(tableId, createRowActionRequest())
+ const action2 = await createRowAction(tableId, createRowActionRequest())
+
+ for (const automationId of [action1.automationId, action2.automationId]) {
+ expect(
+ await config.api.automation.get(automationId, { status: 200 })
+ ).toEqual(expect.objectContaining({ _id: automationId }))
+ }
+ })
})
describe("find", () => {
@@ -264,7 +295,6 @@ describe("/rowsActions", () => {
const updatedName = generator.string()
const res = await config.api.rowAction.update(tableId, actionId, {
- ...actionData,
name: updatedName,
})
@@ -272,14 +302,17 @@ describe("/rowsActions", () => {
id: actionId,
tableId,
name: updatedName,
+ automationId: actionData.automationId,
})
expect(await config.api.rowAction.find(tableId)).toEqual(
expect.objectContaining({
actions: expect.objectContaining({
[actionId]: {
- ...actionData,
name: updatedName,
+ id: actionData.id,
+ tableId: actionData.tableId,
+ automationId: actionData.automationId,
},
}),
})
@@ -296,7 +329,6 @@ describe("/rowsActions", () => {
)
const res = await config.api.rowAction.update(tableId, rowAction.id, {
- ...rowAction,
name: " action name ",
})
@@ -408,5 +440,26 @@ describe("/rowsActions", () => {
status: 400,
})
})
+
+ it("deletes the linked automation", async () => {
+ const actions: RowActionResponse[] = []
+ for (const rowAction of createRowActionRequests(3)) {
+ actions.push(await createRowAction(tableId, rowAction))
+ }
+
+ const actionToDelete = _.sample(actions)!
+ await config.api.rowAction.delete(tableId, actionToDelete.id, {
+ status: 204,
+ })
+
+ await config.api.automation.get(actionToDelete.automationId, {
+ status: 404,
+ })
+ for (const action of actions.filter(a => a.id !== actionToDelete.id)) {
+ await config.api.automation.get(action.automationId, {
+ status: 200,
+ })
+ }
+ })
})
})
diff --git a/packages/server/src/api/routes/tests/utilities/TestFunctions.ts b/packages/server/src/api/routes/tests/utilities/TestFunctions.ts
index 15a3ede39b5..a9a8e7051bd 100644
--- a/packages/server/src/api/routes/tests/utilities/TestFunctions.ts
+++ b/packages/server/src/api/routes/tests/utilities/TestFunctions.ts
@@ -54,7 +54,7 @@ export const clearAllApps = async (
}
export const clearAllAutomations = async (config: TestConfiguration) => {
- const automations = await config.getAllAutomations()
+ const { automations } = await config.getAllAutomations()
for (let auto of automations) {
await context.doInAppContext(config.getAppId(), async () => {
await config.deleteAutomation(auto)
diff --git a/packages/server/src/automations/triggerInfo/index.ts b/packages/server/src/automations/triggerInfo/index.ts
index b35d915ea84..58a6fea3ada 100644
--- a/packages/server/src/automations/triggerInfo/index.ts
+++ b/packages/server/src/automations/triggerInfo/index.ts
@@ -1,15 +1,24 @@
+import {
+ AutomationTriggerSchema,
+ AutomationTriggerStepId,
+} from "@budibase/types"
import * as app from "./app"
import * as cron from "./cron"
import * as rowDeleted from "./rowDeleted"
import * as rowSaved from "./rowSaved"
import * as rowUpdated from "./rowUpdated"
import * as webhook from "./webhook"
+import * as rowAction from "./rowAction"
-export const definitions = {
+export const definitions: Record<
+ keyof typeof AutomationTriggerStepId,
+ AutomationTriggerSchema
+> = {
ROW_SAVED: rowSaved.definition,
ROW_UPDATED: rowUpdated.definition,
ROW_DELETED: rowDeleted.definition,
WEBHOOK: webhook.definition,
APP: app.definition,
CRON: cron.definition,
+ ROW_ACTION: rowAction.definition,
}
diff --git a/packages/server/src/automations/triggerInfo/rowAction.ts b/packages/server/src/automations/triggerInfo/rowAction.ts
new file mode 100644
index 00000000000..a7454e8606f
--- /dev/null
+++ b/packages/server/src/automations/triggerInfo/rowAction.ts
@@ -0,0 +1,35 @@
+import {
+ AutomationCustomIOType,
+ AutomationIOType,
+ AutomationStepType,
+ AutomationTriggerSchema,
+ AutomationTriggerStepId,
+ AutomationEventType,
+} from "@budibase/types"
+
+export const definition: AutomationTriggerSchema = {
+ type: AutomationStepType.TRIGGER,
+ tagline:
+ "Row action triggered in {{inputs.enriched.table.name}} by {{inputs.enriched.row._id}}",
+ name: "Row Action",
+ description: "TODO description",
+ icon: "Workflow",
+ stepId: AutomationTriggerStepId.ROW_ACTION,
+ inputs: {},
+ schema: {
+ inputs: {
+ properties: {
+ tableId: {
+ type: AutomationIOType.STRING,
+ customType: AutomationCustomIOType.TABLE,
+ title: "Table",
+ readonly: true,
+ },
+ },
+ required: ["tableId"],
+ },
+ outputs: { properties: {} },
+ },
+
+ event: AutomationEventType.ROW_SAVE,
+}
diff --git a/packages/server/src/sdk/app/automations/crud.ts b/packages/server/src/sdk/app/automations/crud.ts
index 418da02c1cb..2b36e32397f 100644
--- a/packages/server/src/sdk/app/automations/crud.ts
+++ b/packages/server/src/sdk/app/automations/crud.ts
@@ -1,3 +1,4 @@
+import { sdk } from "@budibase/shared-core"
import {
Automation,
RequiredKeys,
@@ -16,6 +17,11 @@ import {
import { definitions } from "../../../automations/triggerInfo"
import automations from "."
+export interface PersistedAutomation extends Automation {
+ _id: string
+ _rev: string
+}
+
function getDb() {
return context.getAppDB()
}
@@ -76,7 +82,7 @@ async function handleStepEvents(
export async function fetch() {
const db = getDb()
- const response = await db.allDocs(
+ const response = await db.allDocs(
getAutomationParams(null, {
include_docs: true,
})
@@ -89,7 +95,7 @@ export async function fetch() {
export async function get(automationId: string) {
const db = getDb()
- const result = await db.get(automationId)
+ const result = await db.get(automationId)
return trimUnexpectedObjectFields(result)
}
@@ -127,6 +133,9 @@ export async function update(automation: Automation) {
const db = getDb()
const oldAutomation = await db.get(automation._id)
+
+ guardInvalidUpdatesAndThrow(automation, oldAutomation)
+
automation = cleanAutomationInputs(automation)
automation = await checkForWebhooks({
oldAuto: oldAutomation,
@@ -254,6 +263,41 @@ async function checkForWebhooks({ oldAuto, newAuto }: any) {
return newAuto
}
+function guardInvalidUpdatesAndThrow(
+ automation: Automation,
+ oldAutomation: Automation
+) {
+ const stepDefinitions = [
+ automation.definition.trigger,
+ ...automation.definition.steps,
+ ]
+ const oldStepDefinitions = [
+ oldAutomation.definition.trigger,
+ ...oldAutomation.definition.steps,
+ ]
+ for (const step of stepDefinitions) {
+ const readonlyFields = Object.keys(
+ step.schema.inputs.properties || {}
+ ).filter(k => step.schema.inputs.properties[k].readonly)
+ readonlyFields.forEach(readonlyField => {
+ const oldStep = oldStepDefinitions.find(i => i.id === step.id)
+ if (step.inputs[readonlyField] !== oldStep?.inputs[readonlyField]) {
+ throw new HTTPError(
+ `Field ${readonlyField} is readonly and it cannot be modified`,
+ 400
+ )
+ }
+ })
+ }
+
+ if (
+ sdk.automations.isRowAction(automation) &&
+ automation.name !== oldAutomation.name
+ ) {
+ throw new Error("Row actions cannot be renamed")
+ }
+}
+
function trimUnexpectedObjectFields(automation: T): T {
// This will ensure all the automation fields (and nothing else) is mapped to the result
const allRequired: RequiredKeys = {
diff --git a/packages/server/src/sdk/app/automations/tests/index.spec.ts b/packages/server/src/sdk/app/automations/tests/index.spec.ts
new file mode 100644
index 00000000000..868dfb30ac4
--- /dev/null
+++ b/packages/server/src/sdk/app/automations/tests/index.spec.ts
@@ -0,0 +1,88 @@
+import { sample } from "lodash/fp"
+import { Automation, AutomationTriggerStepId } from "@budibase/types"
+import { generator } from "@budibase/backend-core/tests"
+import automationSdk from "../"
+import { structures } from "../../../../api/routes/tests/utilities"
+import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
+
+describe("automation sdk", () => {
+ const config = new TestConfiguration()
+
+ beforeAll(async () => {
+ await config.init()
+ })
+
+ describe("update", () => {
+ it("can rename existing automations", async () => {
+ await config.doInContext(config.getAppId(), async () => {
+ const automation = structures.newAutomation()
+
+ const response = await automationSdk.create(automation)
+
+ const newName = generator.guid()
+ const update = { ...response, name: newName }
+ const result = await automationSdk.update(update)
+ expect(result.name).toEqual(newName)
+ })
+ })
+
+ it("cannot rename row action automations", async () => {
+ await config.doInContext(config.getAppId(), async () => {
+ const automation = structures.newAutomation({
+ trigger: {
+ ...structures.automationTrigger(),
+ stepId: AutomationTriggerStepId.ROW_ACTION,
+ },
+ })
+
+ const response = await automationSdk.create(automation)
+
+ const newName = generator.guid()
+ const update = { ...response, name: newName }
+ await expect(automationSdk.update(update)).rejects.toThrow(
+ "Row actions cannot be renamed"
+ )
+ })
+ })
+
+ it.each([
+ ["trigger", (a: Automation) => a.definition.trigger],
+ ["step", (a: Automation) => a.definition.steps[0]],
+ ])("can update input fields (for a %s)", async (_, getStep) => {
+ await config.doInContext(config.getAppId(), async () => {
+ const automation = structures.newAutomation()
+
+ const keyToUse = sample(Object.keys(getStep(automation).inputs))!
+ getStep(automation).inputs[keyToUse] = "anyValue"
+
+ const response = await automationSdk.create(automation)
+
+ const update = { ...response }
+ getStep(update).inputs[keyToUse] = "anyUpdatedValue"
+ const result = await automationSdk.update(update)
+ expect(getStep(result).inputs[keyToUse]).toEqual("anyUpdatedValue")
+ })
+ })
+
+ it.each([
+ ["trigger", (a: Automation) => a.definition.trigger],
+ ["step", (a: Automation) => a.definition.steps[0]],
+ ])("cannot update readonly fields (for a %s)", async (_, getStep) => {
+ await config.doInContext(config.getAppId(), async () => {
+ const automation = structures.newAutomation()
+ getStep(automation).schema.inputs.properties["readonlyProperty"] = {
+ readonly: true,
+ }
+ getStep(automation).inputs["readonlyProperty"] = "anyValue"
+
+ const response = await automationSdk.create(automation)
+
+ const update = { ...response }
+ getStep(update).inputs["readonlyProperty"] = "anyUpdatedValue"
+ await expect(automationSdk.update(update)).rejects.toThrow(
+ "Field readonlyProperty is readonly and it cannot be modified"
+ )
+ })
+ })
+ })
+})
diff --git a/packages/server/src/sdk/app/automations/utils.ts b/packages/server/src/sdk/app/automations/utils.ts
index 79e70923f12..ad06e55418b 100644
--- a/packages/server/src/sdk/app/automations/utils.ts
+++ b/packages/server/src/sdk/app/automations/utils.ts
@@ -1,7 +1,67 @@
-import { Automation, AutomationActionStepId } from "@budibase/types"
+import {
+ Automation,
+ AutomationActionStepId,
+ AutomationBuilderData,
+ TableRowActions,
+} from "@budibase/types"
+import { sdk as coreSdk } from "@budibase/shared-core"
export function checkForCollectStep(automation: Automation) {
return automation.definition.steps.some(
(step: any) => step.stepId === AutomationActionStepId.COLLECT
)
}
+
+export async function getBuilderData(
+ automations: Automation[]
+): Promise> {
+ const sdk = (await import("../../../sdk")).default
+
+ const tableNameCache: Record = {}
+ async function getTableName(tableId: string) {
+ if (!tableNameCache[tableId]) {
+ const table = await sdk.tables.getTable(tableId)
+ tableNameCache[tableId] = table.name
+ }
+
+ return tableNameCache[tableId]
+ }
+
+ const rowActionNameCache: Record = {}
+ async function getRowActionName(tableId: string, rowActionId: string) {
+ if (!rowActionNameCache[tableId]) {
+ const rowActions = await sdk.rowActions.get(tableId)
+ rowActionNameCache[tableId] = rowActions
+ }
+
+ return rowActionNameCache[tableId].actions[rowActionId]?.name
+ }
+
+ const result: Record = {}
+ for (const automation of automations) {
+ const isRowAction = coreSdk.automations.isRowAction(automation)
+ if (!isRowAction) {
+ result[automation._id!] = { displayName: automation.name }
+ continue
+ }
+
+ const { tableId, rowActionId } = automation.definition.trigger.inputs
+
+ const tableName = await getTableName(tableId)
+
+ const rowActionName = await getRowActionName(tableId, rowActionId)
+
+ result[automation._id!] = {
+ displayName: `${tableName}: ${automation.name}`,
+ triggerInfo: {
+ type: "Automation trigger",
+ table: { id: tableId, name: tableName },
+ rowAction: {
+ id: rowActionId,
+ name: rowActionName,
+ },
+ },
+ }
+ }
+ return result
+}
diff --git a/packages/server/src/sdk/app/rowActions.ts b/packages/server/src/sdk/app/rowActions.ts
index 8bff216ab9e..e59337a7c98 100644
--- a/packages/server/src/sdk/app/rowActions.ts
+++ b/packages/server/src/sdk/app/rowActions.ts
@@ -6,6 +6,8 @@ import {
TableRowActions,
VirtualDocumentType,
} from "@budibase/types"
+import automations from "./automations"
+import { definitions as TRIGGER_DEFINITIONS } from "../../automations/triggerInfo"
function ensureUniqueAndThrow(
doc: TableRowActions,
@@ -41,13 +43,40 @@ export async function create(tableId: string, rowAction: { name: string }) {
ensureUniqueAndThrow(doc, action.name)
- const newId = `${VirtualDocumentType.ROW_ACTION}${SEPARATOR}${utils.newid()}`
- doc.actions[newId] = action
+ const appId = context.getAppId()
+ if (!appId) {
+ throw new Error("Could not get the current appId")
+ }
+
+ const newRowActionId = `${
+ VirtualDocumentType.ROW_ACTION
+ }${SEPARATOR}${utils.newid()}`
+
+ const automation = await automations.create({
+ name: action.name,
+ appId,
+ definition: {
+ trigger: {
+ id: "trigger",
+ ...TRIGGER_DEFINITIONS.ROW_ACTION,
+ inputs: {
+ tableId,
+ rowActionId: newRowActionId,
+ },
+ },
+ steps: [],
+ },
+ })
+
+ doc.actions[newRowActionId] = {
+ name: action.name,
+ automationId: automation._id!,
+ }
await db.put(doc)
return {
- id: newId,
- ...action,
+ id: newRowActionId,
+ ...doc.actions[newRowActionId],
}
}
@@ -81,27 +110,34 @@ export async function update(
ensureUniqueAndThrow(actionsDoc, action.name, rowActionId)
- actionsDoc.actions[rowActionId] = action
+ actionsDoc.actions[rowActionId] = {
+ automationId: actionsDoc.actions[rowActionId].automationId,
+ ...action,
+ }
const db = context.getAppDB()
await db.put(actionsDoc)
return {
id: rowActionId,
- ...action,
+ ...actionsDoc.actions[rowActionId],
}
}
export async function remove(tableId: string, rowActionId: string) {
const actionsDoc = await get(tableId)
- if (!actionsDoc.actions[rowActionId]) {
+ const rowAction = actionsDoc.actions[rowActionId]
+ if (!rowAction) {
throw new HTTPError(
`Row action '${rowActionId}' not found in '${tableId}'`,
400
)
}
+ const { automationId } = rowAction
+ const automation = await automations.get(automationId)
+ await automations.remove(automation._id, automation._rev)
delete actionsDoc.actions[rowActionId]
const db = context.getAppDB()
diff --git a/packages/server/src/tests/utilities/api/automation.ts b/packages/server/src/tests/utilities/api/automation.ts
new file mode 100644
index 00000000000..9620e2011cf
--- /dev/null
+++ b/packages/server/src/tests/utilities/api/automation.ts
@@ -0,0 +1,17 @@
+import { Automation } from "@budibase/types"
+import { Expectations, TestAPI } from "./base"
+
+export class AutomationAPI extends TestAPI {
+ get = async (
+ automationId: string,
+ expectations?: Expectations
+ ): Promise => {
+ const result = await this._get(
+ `/api/automations/${automationId}`,
+ {
+ expectations,
+ }
+ )
+ return result
+ }
+}
diff --git a/packages/server/src/tests/utilities/api/index.ts b/packages/server/src/tests/utilities/api/index.ts
index a19b68a8722..36a6ed02227 100644
--- a/packages/server/src/tests/utilities/api/index.ts
+++ b/packages/server/src/tests/utilities/api/index.ts
@@ -14,6 +14,7 @@ import { QueryAPI } from "./query"
import { RoleAPI } from "./role"
import { TemplateAPI } from "./template"
import { RowActionAPI } from "./rowAction"
+import { AutomationAPI } from "./automation"
export default class API {
table: TableAPI
@@ -31,6 +32,7 @@ export default class API {
roles: RoleAPI
templates: TemplateAPI
rowAction: RowActionAPI
+ automation: AutomationAPI
constructor(config: TestConfiguration) {
this.table = new TableAPI(config)
@@ -48,5 +50,6 @@ export default class API {
this.roles = new RoleAPI(config)
this.templates = new TemplateAPI(config)
this.rowAction = new RowActionAPI(config)
+ this.automation = new AutomationAPI(config)
}
}
diff --git a/packages/server/src/tests/utilities/structures.ts b/packages/server/src/tests/utilities/structures.ts
index 970df2e2c93..16ab049eb40 100644
--- a/packages/server/src/tests/utilities/structures.ts
+++ b/packages/server/src/tests/utilities/structures.ts
@@ -158,7 +158,10 @@ export function automationTrigger(
}
}
-export function newAutomation({ steps, trigger }: any = {}) {
+export function newAutomation({
+ steps,
+ trigger,
+}: { steps?: AutomationStep[]; trigger?: AutomationTrigger } = {}) {
const automation = basicAutomation()
if (trigger) {
@@ -176,6 +179,16 @@ export function newAutomation({ steps, trigger }: any = {}) {
return automation
}
+export function rowActionAutomation() {
+ const automation = newAutomation({
+ trigger: {
+ ...automationTrigger(),
+ stepId: AutomationTriggerStepId.ROW_ACTION,
+ },
+ })
+ return automation
+}
+
export function basicAutomation(appId?: string): Automation {
return {
name: "My Automation",
diff --git a/packages/shared-core/src/sdk/documents/automations.ts b/packages/shared-core/src/sdk/documents/automations.ts
new file mode 100644
index 00000000000..93e08c86e08
--- /dev/null
+++ b/packages/shared-core/src/sdk/documents/automations.ts
@@ -0,0 +1,7 @@
+import { Automation, AutomationTriggerStepId } from "@budibase/types"
+
+export function isRowAction(automation: Automation) {
+ const result =
+ automation.definition.trigger.stepId === AutomationTriggerStepId.ROW_ACTION
+ return result
+}
diff --git a/packages/shared-core/src/sdk/documents/index.ts b/packages/shared-core/src/sdk/documents/index.ts
index d20631eef48..4b17c1ea083 100644
--- a/packages/shared-core/src/sdk/documents/index.ts
+++ b/packages/shared-core/src/sdk/documents/index.ts
@@ -1,2 +1,3 @@
export * as applications from "./applications"
+export * as automations from "./automations"
export * as users from "./users"
diff --git a/packages/types/src/api/web/app/rowAction.ts b/packages/types/src/api/web/app/rowAction.ts
index ba95ba6b955..305c42b4738 100644
--- a/packages/types/src/api/web/app/rowAction.ts
+++ b/packages/types/src/api/web/app/rowAction.ts
@@ -7,6 +7,7 @@ export interface UpdateRowActionRequest extends RowActionData {}
export interface RowActionResponse extends RowActionData {
id: string
tableId: string
+ automationId: string
}
export interface RowActionsResponse {
diff --git a/packages/types/src/api/web/automation.ts b/packages/types/src/api/web/automation.ts
index c1f3d01b2fc..6c810d5e781 100644
--- a/packages/types/src/api/web/automation.ts
+++ b/packages/types/src/api/web/automation.ts
@@ -1,3 +1,24 @@
import { DocumentDestroyResponse } from "@budibase/nano"
+import { Automation } from "../../documents"
export interface DeleteAutomationResponse extends DocumentDestroyResponse {}
+
+export interface AutomationBuilderData {
+ displayName: string
+ triggerInfo?: {
+ type: string
+ table: {
+ id: string
+ name: string
+ }
+ rowAction: {
+ id: string
+ name: string
+ }
+ }
+}
+
+export interface FetchAutomationResponse {
+ automations: Automation[]
+ builderData?: Record // The key will be the automationId
+}
diff --git a/packages/types/src/documents/app/automation.ts b/packages/types/src/documents/app/automation.ts
index 0432c118517..dd9990951e9 100644
--- a/packages/types/src/documents/app/automation.ts
+++ b/packages/types/src/documents/app/automation.ts
@@ -45,6 +45,7 @@ export enum AutomationTriggerStepId {
WEBHOOK = "WEBHOOK",
APP = "APP",
CRON = "CRON",
+ ROW_ACTION = "ROW_ACTION",
}
export enum AutomationStepType {
@@ -152,6 +153,7 @@ interface BaseIOStructure {
[key: string]: BaseIOStructure
}
required?: string[]
+ readonly?: true
}
export interface InputOutputBlock {
@@ -192,6 +194,7 @@ export interface AutomationStep extends AutomationStepSchema {
}
export interface AutomationTriggerSchema extends AutomationStepSchema {
+ type: AutomationStepType.TRIGGER
event?: string
cronJobId?: string
}
diff --git a/packages/types/src/documents/app/rowAction.ts b/packages/types/src/documents/app/rowAction.ts
index ea55d5dcd22..84fa0e7f008 100644
--- a/packages/types/src/documents/app/rowAction.ts
+++ b/packages/types/src/documents/app/rowAction.ts
@@ -6,6 +6,7 @@ export interface TableRowActions extends Document {
string,
{
name: string
+ automationId: string
}
>
}