diff --git a/lerna.json b/lerna.json
index 5e28c36166..1728208df0 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
- "version": "2.29.25",
+ "version": "2.29.26",
"npmClient": "yarn",
"packages": [
"packages/*",
diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts
index feeba6061e..61fbb3d61e 100644
--- a/packages/backend-core/src/db/couch/DatabaseImpl.ts
+++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts
@@ -308,8 +308,12 @@ export class DatabaseImpl implements Database {
}
async bulkDocs(documents: AnyDocument[]) {
+ const now = new Date().toISOString()
return this.performCall(db => {
- return () => db.bulk({ docs: documents })
+ return () =>
+ db.bulk({
+ docs: documents.map(d => ({ createdAt: now, ...d, updatedAt: now })),
+ })
})
}
diff --git a/packages/backend-core/src/db/couch/tests/DatabaseImpl.spec.ts b/packages/backend-core/src/db/couch/tests/DatabaseImpl.spec.ts
new file mode 100644
index 0000000000..89eecc3785
--- /dev/null
+++ b/packages/backend-core/src/db/couch/tests/DatabaseImpl.spec.ts
@@ -0,0 +1,118 @@
+import tk from "timekeeper"
+
+import { DatabaseImpl } from ".."
+
+import { generator, structures } from "../../../../tests"
+
+const initialTime = new Date()
+tk.freeze(initialTime)
+
+describe("DatabaseImpl", () => {
+ const db = new DatabaseImpl(structures.db.id())
+
+ beforeEach(() => {
+ tk.freeze(initialTime)
+ })
+
+ describe("put", () => {
+ it("persists createdAt and updatedAt fields", async () => {
+ const id = generator.guid()
+ await db.put({ _id: id })
+
+ expect(await db.get(id)).toEqual({
+ _id: id,
+ _rev: expect.any(String),
+ createdAt: initialTime.toISOString(),
+ updatedAt: initialTime.toISOString(),
+ })
+ })
+
+ it("updates updated at fields", async () => {
+ const id = generator.guid()
+
+ await db.put({ _id: id })
+ tk.travel(100)
+
+ await db.put({ ...(await db.get(id)), newValue: 123 })
+
+ expect(await db.get(id)).toEqual({
+ _id: id,
+ _rev: expect.any(String),
+ newValue: 123,
+ createdAt: initialTime.toISOString(),
+ updatedAt: new Date().toISOString(),
+ })
+ })
+ })
+
+ describe("bulkDocs", () => {
+ it("persists createdAt and updatedAt fields", async () => {
+ const ids = generator.unique(() => generator.guid(), 5)
+ await db.bulkDocs(ids.map(id => ({ _id: id })))
+
+ for (const id of ids) {
+ expect(await db.get(id)).toEqual({
+ _id: id,
+ _rev: expect.any(String),
+ createdAt: initialTime.toISOString(),
+ updatedAt: initialTime.toISOString(),
+ })
+ }
+ })
+
+ it("updates updated at fields", async () => {
+ const ids = generator.unique(() => generator.guid(), 5)
+
+ await db.bulkDocs(ids.map(id => ({ _id: id })))
+ tk.travel(100)
+
+ const docsToUpdate = await Promise.all(
+ ids.map(async id => ({ ...(await db.get(id)), newValue: 123 }))
+ )
+ await db.bulkDocs(docsToUpdate)
+
+ for (const id of ids) {
+ expect(await db.get(id)).toEqual({
+ _id: id,
+ _rev: expect.any(String),
+ newValue: 123,
+ createdAt: initialTime.toISOString(),
+ updatedAt: new Date().toISOString(),
+ })
+ }
+ })
+
+ it("keeps existing createdAt", async () => {
+ const ids = generator.unique(() => generator.guid(), 2)
+
+ await db.bulkDocs(ids.map(id => ({ _id: id })))
+ tk.travel(100)
+
+ const newDocs = generator
+ .unique(() => generator.guid(), 3)
+ .map(id => ({ _id: id }))
+ const docsToUpdate = await Promise.all(
+ ids.map(async id => ({ ...(await db.get(id)), newValue: 123 }))
+ )
+ await db.bulkDocs([...newDocs, ...docsToUpdate])
+
+ for (const { _id } of docsToUpdate) {
+ expect(await db.get(_id)).toEqual({
+ _id,
+ _rev: expect.any(String),
+ newValue: 123,
+ createdAt: initialTime.toISOString(),
+ updatedAt: new Date().toISOString(),
+ })
+ }
+ for (const { _id } of newDocs) {
+ expect(await db.get(_id)).toEqual({
+ _id,
+ _rev: expect.any(String),
+ createdAt: new Date().toISOString(),
+ updatedAt: new Date().toISOString(),
+ })
+ }
+ })
+ })
+})
diff --git a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte
index f79b36b1ca..c263468f3b 100644
--- a/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte
+++ b/packages/builder/src/components/automation/AutomationBuilder/FlowChart/FlowChart.svelte
@@ -54,6 +54,7 @@
{
testDataModal.show()
}}
@@ -80,6 +81,7 @@
automation._id,
automation.disabled
)}
+ disabled={!$selectedAutomation?.definition?.trigger}
value={!automation.disabled}
/>
diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte
index df5ac3bd98..6e4d7c0099 100644
--- a/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/AutomationNavItem.svelte
@@ -54,7 +54,7 @@
name: "Edit",
keyBind: null,
visible: true,
- disabled: false,
+ disabled: !automation.definition.trigger,
callback: updateAutomationDialog.show,
},
{
@@ -62,7 +62,9 @@
name: "Duplicate",
keyBind: null,
visible: true,
- disabled: automation.definition.trigger.name === "Webhook",
+ disabled:
+ !automation.definition.trigger ||
+ automation.definition.trigger?.name === "Webhook",
callback: duplicateAutomation,
},
]
@@ -74,7 +76,7 @@
name: automation.disabled ? "Activate" : "Pause",
keyBind: null,
visible: true,
- disabled: false,
+ disabled: !automation.definition.trigger,
callback: () => {
automationStore.actions.toggleDisabled(
automation._id,
diff --git a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte
index e017e6a26a..58eebfdd3e 100644
--- a/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/AutomationPanel.svelte
@@ -20,7 +20,7 @@
.map(automation => ({
...automation,
displayName:
- $automationStore.automationDisplayData[automation._id].displayName ||
+ $automationStore.automationDisplayData[automation._id]?.displayName ||
automation.name,
}))
.sort((a, b) => {
@@ -30,12 +30,13 @@
})
$: groupedAutomations = filteredAutomations.reduce((acc, auto) => {
- acc[auto.definition.trigger.event] ??= {
- icon: auto.definition.trigger.icon,
- name: (auto.definition.trigger?.name || "").toUpperCase(),
+ const catName = auto.definition?.trigger?.event || "No Trigger"
+ acc[catName] ??= {
+ icon: auto.definition?.trigger?.icon || "AlertCircle",
+ name: (auto.definition?.trigger?.name || "No Trigger").toUpperCase(),
entries: [],
}
- acc[auto.definition.trigger.event].entries.push(auto)
+ acc[catName].entries.push(auto)
return acc
}, {})
diff --git a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
index 41799cd7f3..365d3d358f 100644
--- a/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
+++ b/packages/builder/src/components/automation/AutomationPanel/CreateAutomationModal.svelte
@@ -21,7 +21,9 @@
$: nameError =
nameTouched && !name ? "Please specify a name for the automation." : null
- $: triggers = Object.entries($automationStore.blockDefinitions.TRIGGER)
+ $: triggers = Object.entries(
+ $automationStore.blockDefinitions.CREATABLE_TRIGGER
+ )
async function createAutomation() {
try {
diff --git a/packages/builder/src/components/backend/DataTable/buttons/grid/GridCreateAutomationButton.svelte b/packages/builder/src/components/backend/DataTable/buttons/grid/GridCreateAutomationButton.svelte
index 8e3d90be41..148db7554c 100644
--- a/packages/builder/src/components/backend/DataTable/buttons/grid/GridCreateAutomationButton.svelte
+++ b/packages/builder/src/components/backend/DataTable/buttons/grid/GridCreateAutomationButton.svelte
@@ -13,7 +13,7 @@
const { datasource } = getContext("grid")
- $: triggers = $automationStore.blockDefinitions.TRIGGER
+ $: triggers = $automationStore.blockDefinitions.CREATABLE_TRIGGER
$: table = $tables.list.find(table => table._id === $datasource.tableId)
diff --git a/packages/builder/src/stores/builder/automations.js b/packages/builder/src/stores/builder/automations.js
index 57c823da9b..fdb0991911 100644
--- a/packages/builder/src/stores/builder/automations.js
+++ b/packages/builder/src/stores/builder/automations.js
@@ -5,14 +5,16 @@ import { generate } from "shortid"
import { createHistoryStore } from "stores/builder/history"
import { notifications } from "@budibase/bbui"
import { updateReferencesInObject } from "dataBinding"
+import { AutomationTriggerStepId } from "@budibase/types"
const initialAutomationState = {
automations: [],
testResults: null,
showTestPanel: false,
blockDefinitions: {
- TRIGGER: [],
- ACTION: [],
+ TRIGGER: {},
+ CREATABLE_TRIGGER: {},
+ ACTION: {},
},
selectedAutomationId: null,
automationDisplayData: {},
@@ -46,14 +48,29 @@ const updateStepReferences = (steps, modifiedIndex, action) => {
})
}
+const getFinalDefinitions = (triggers, actions) => {
+ const creatable = {}
+ Object.entries(triggers).forEach(entry => {
+ if (entry[0] === AutomationTriggerStepId.ROW_ACTION) {
+ return
+ }
+ creatable[entry[0]] = entry[1]
+ })
+ return {
+ TRIGGER: triggers,
+ CREATABLE_TRIGGER: creatable,
+ ACTION: actions,
+ }
+}
+
const automationActions = store => ({
definitions: async () => {
const response = await API.getAutomationDefinitions()
store.update(state => {
- state.blockDefinitions = {
- TRIGGER: response.trigger,
- ACTION: response.action,
- }
+ state.blockDefinitions = getFinalDefinitions(
+ response.trigger,
+ response.action
+ )
return state
})
return response
@@ -69,10 +86,10 @@ const automationActions = store => ({
return a.name < b.name ? -1 : 1
})
state.automationDisplayData = automationResponse.builderData
- state.blockDefinitions = {
- TRIGGER: definitions.trigger,
- ACTION: definitions.action,
- }
+ state.blockDefinitions = getFinalDefinitions(
+ definitions.trigger,
+ definitions.action
+ )
return state
})
},
@@ -87,8 +104,6 @@ const automationActions = store => ({
disabled: false,
}
const response = await store.actions.save(automation)
- await store.actions.fetch()
- store.actions.select(response._id)
return response
},
duplicate: async automation => {
@@ -98,14 +113,13 @@ const automationActions = store => ({
_id: undefined,
_ref: undefined,
})
- await store.actions.fetch()
- store.actions.select(response._id)
return response
},
save: async automation => {
const response = await API.updateAutomation(automation)
await store.actions.fetch()
+ store.actions.select(response._id)
return response.automation
},
delete: async automation => {
@@ -113,18 +127,22 @@ const automationActions = store => ({
automationId: automation?._id,
automationRev: automation?._rev,
})
+
store.update(state => {
// Remove the automation
state.automations = state.automations.filter(
x => x._id !== automation._id
)
+
// Select a new automation if required
if (automation._id === state.selectedAutomationId) {
- store.actions.select(state.automations[0]?._id)
+ state.selectedAutomationId = state.automations[0]?._id || null
}
+
+ // Clear out automationDisplayData for the automation
+ delete state.automationDisplayData[automation._id]
return state
})
- await store.actions.fetch()
},
toggleDisabled: async automationId => {
let automation
@@ -381,7 +399,7 @@ export const selectedAutomation = derived(automationStore, $automationStore => {
export const selectedAutomationDisplayData = derived(
[automationStore, selectedAutomation],
([$automationStore, $selectedAutomation]) => {
- if (!$selectedAutomation._id) {
+ if (!$selectedAutomation?._id) {
return null
}
return $automationStore.automationDisplayData[$selectedAutomation._id]
diff --git a/packages/server/src/api/routes/tests/automation.spec.ts b/packages/server/src/api/routes/tests/automation.spec.ts
index 990828dcde..d9d48ede38 100644
--- a/packages/server/src/api/routes/tests/automation.spec.ts
+++ b/packages/server/src/api/routes/tests/automation.spec.ts
@@ -14,6 +14,7 @@ import sdk from "../../../sdk"
import { Automation, FieldType, Table } from "@budibase/types"
import { mocks } from "@budibase/backend-core/tests"
import { FilterConditions } from "../../../automations/steps/filter"
+import { removeDeprecated } from "../../../automations/utils"
const MAX_RETRIES = 4
let {
@@ -69,14 +70,15 @@ describe("/automations", () => {
.expect("Content-Type", /json/)
.expect(200)
- let definitionsLength = Object.keys(BUILTIN_ACTION_DEFINITIONS).length
- definitionsLength-- // OUTGOING_WEBHOOK is deprecated
+ let definitionsLength = Object.keys(
+ removeDeprecated(BUILTIN_ACTION_DEFINITIONS)
+ ).length
expect(Object.keys(res.body.action).length).toBeGreaterThanOrEqual(
definitionsLength
)
expect(Object.keys(res.body.trigger).length).toEqual(
- Object.keys(TRIGGER_DEFINITIONS).length
+ Object.keys(removeDeprecated(TRIGGER_DEFINITIONS)).length
)
})
})
diff --git a/packages/server/src/api/routes/tests/table.spec.ts b/packages/server/src/api/routes/tests/table.spec.ts
index 20c83549d2..a8bf9447e8 100644
--- a/packages/server/src/api/routes/tests/table.spec.ts
+++ b/packages/server/src/api/routes/tests/table.spec.ts
@@ -9,6 +9,7 @@ import {
RelationshipType,
Row,
SaveTableRequest,
+ SourceName,
Table,
TableSourceType,
User,
@@ -33,7 +34,8 @@ describe.each([
[DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)],
[DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)],
[DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)],
-])("/tables (%s)", (_, dsProvider) => {
+ [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)],
+])("/tables (%s)", (name, dsProvider) => {
const isInternal: boolean = !dsProvider
let datasource: Datasource | undefined
let config = setup.getConfig()
@@ -52,15 +54,20 @@ describe.each([
jest.clearAllMocks()
})
- it.each([
+ let names = [
"alphanum",
"with spaces",
"with-dashes",
"with_underscores",
- 'with "double quotes"',
- "with 'single quotes'",
"with `backticks`",
- ])("creates a table with name: %s", async name => {
+ ]
+
+ if (name !== DatabaseName.ORACLE) {
+ names.push(`with "double quotes"`)
+ names.push(`with 'single quotes'`)
+ }
+
+ it.each(names)("creates a table with name: %s", async name => {
const table = await config.api.table.save(
tableForDatasource(datasource, { name })
)
diff --git a/packages/server/src/automations/utils.ts b/packages/server/src/automations/utils.ts
index c75cc5e8dc..93b8f907fd 100644
--- a/packages/server/src/automations/utils.ts
+++ b/packages/server/src/automations/utils.ts
@@ -3,11 +3,15 @@ import { definitions } from "./triggerInfo"
import { automationQueue } from "./bullboard"
import { updateEntityMetadata } from "../utilities"
import { MetadataTypes } from "../constants"
-import { db as dbCore, context, utils } from "@budibase/backend-core"
+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, AutomationJob } from "@budibase/types"
+import {
+ Automation,
+ AutomationJob,
+ AutomationStepSchema,
+} from "@budibase/types"
import { automationsEnabled } from "../features"
import { helpers, REBOOT_CRON } from "@budibase/shared-core"
import tracer from "dd-trace"
@@ -111,7 +115,9 @@ export async function updateTestHistory(
)
}
-export function removeDeprecated(definitions: any) {
+export function removeDeprecated(
+ definitions: Record
+) {
const base = cloneDeep(definitions)
for (let key of Object.keys(base)) {
if (base[key].deprecated) {
diff --git a/packages/server/src/integrations/tests/utils/index.ts b/packages/server/src/integrations/tests/utils/index.ts
index d42fab7a71..b6f8b5b92a 100644
--- a/packages/server/src/integrations/tests/utils/index.ts
+++ b/packages/server/src/integrations/tests/utils/index.ts
@@ -83,7 +83,7 @@ export async function startContainer(container: GenericContainer) {
container = container
.withReuse()
.withLabels({ "com.budibase": "true" })
- .withName(key)
+ .withName(`${key}_testcontainer`)
let startedContainer: StartedTestContainer | undefined = undefined
let lastError = undefined
diff --git a/packages/server/src/sdk/app/automations/crud.ts b/packages/server/src/sdk/app/automations/crud.ts
index 2b36e32397..3888e6882a 100644
--- a/packages/server/src/sdk/app/automations/crud.ts
+++ b/packages/server/src/sdk/app/automations/crud.ts
@@ -87,10 +87,10 @@ export async function fetch() {
include_docs: true,
})
)
- return response.rows
- .map(row => row.doc)
- .filter(doc => !!doc)
- .map(trimUnexpectedObjectFields)
+ const automations: PersistedAutomation[] = response.rows
+ .filter(row => !!row.doc)
+ .map(row => row.doc!)
+ return automations.map(trimUnexpectedObjectFields)
}
export async function get(automationId: string) {
diff --git a/packages/server/src/sdk/app/automations/utils.ts b/packages/server/src/sdk/app/automations/utils.ts
index e89006d618..5d057697ca 100644
--- a/packages/server/src/sdk/app/automations/utils.ts
+++ b/packages/server/src/sdk/app/automations/utils.ts
@@ -29,8 +29,7 @@ export async function getBuilderData(
const rowActionNameCache: Record = {}
async function getRowActionName(tableId: string, rowActionId: string) {
if (!rowActionNameCache[tableId]) {
- const rowActions = await sdk.rowActions.get(tableId)
- rowActionNameCache[tableId] = rowActions
+ rowActionNameCache[tableId] = await sdk.rowActions.get(tableId)
}
return rowActionNameCache[tableId].actions[rowActionId]?.name
@@ -45,9 +44,11 @@ export async function getBuilderData(
}
const { tableId, rowActionId } = automation.definition.trigger.inputs
+ if (!tableId || !rowActionId) {
+ continue
+ }
const tableName = await getTableName(tableId)
-
const rowActionName = await getRowActionName(tableId, rowActionId)
result[automation._id!] = {
diff --git a/packages/types/src/documents/app/automation.ts b/packages/types/src/documents/app/automation.ts
index b53895e57b..d5d7fe667c 100644
--- a/packages/types/src/documents/app/automation.ts
+++ b/packages/types/src/documents/app/automation.ts
@@ -174,9 +174,7 @@ export interface AutomationStepSchema {
deprecated?: boolean
stepId: AutomationTriggerStepId | AutomationActionStepId
blockToLoop?: string
- inputs: {
- [key: string]: any
- }
+ inputs: Record
schema: {
inputs: InputOutputBlock
outputs: InputOutputBlock