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