diff --git a/lerna.json b/lerna.json index 81b35e2a6e..6c507bdd92 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.14.5", + "version": "2.14.8", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/account-portal b/packages/account-portal index b23fb3b179..319c8499e7 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit b23fb3b17961fb04badd9487913a683fcf26dbe6 +Subproject commit 319c8499e7c3d33fbb96cf4d73a922690709686c diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index 3fec573bb9..0e2b4173b0 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -19,6 +19,8 @@ import { WriteStream, ReadStream } from "fs" import { newid } from "../../docIds/newid" import { DDInstrumentedDatabase } from "../instrumentation" +const DATABASE_NOT_FOUND = "Database does not exist." + function buildNano(couchInfo: { url: string; cookie: string }) { return Nano({ url: couchInfo.url, @@ -31,6 +33,8 @@ function buildNano(couchInfo: { url: string; cookie: string }) { }) } +type DBCall = () => Promise + export function DatabaseWithConnection( dbName: string, connection: string, @@ -78,7 +82,11 @@ export class DatabaseImpl implements Database { return this.instanceNano || DatabaseImpl.nano } - async checkSetup() { + private getDb() { + return this.nano().db.use(this.name) + } + + private async checkAndCreateDb() { let shouldCreate = !this.pouchOpts?.skip_setup // check exists in a lightweight fashion let exists = await this.exists() @@ -95,14 +103,22 @@ export class DatabaseImpl implements Database { } } } - return this.nano().db.use(this.name) + return this.getDb() } - private async updateOutput(fnc: any) { + // this function fetches the DB and handles if DB creation is needed + private async performCall( + call: (db: Nano.DocumentScope) => Promise> | DBCall + ): Promise { + const db = this.getDb() + const fnc = await call(db) try { return await fnc() } catch (err: any) { - if (err.statusCode) { + if (err.statusCode === 404 && err.reason === DATABASE_NOT_FOUND) { + await this.checkAndCreateDb() + return await this.performCall(call) + } else if (err.statusCode) { err.status = err.statusCode } throw err @@ -110,11 +126,12 @@ export class DatabaseImpl implements Database { } async get(id?: string): Promise { - const db = await this.checkSetup() - if (!id) { - throw new Error("Unable to get doc without a valid _id.") - } - return this.updateOutput(() => db.get(id)) + return this.performCall(db => { + if (!id) { + throw new Error("Unable to get doc without a valid _id.") + } + return () => db.get(id) + }) } async getMultiple( @@ -147,22 +164,23 @@ export class DatabaseImpl implements Database { } async remove(idOrDoc: string | Document, rev?: string) { - const db = await this.checkSetup() - let _id: string - let _rev: string + return this.performCall(db => { + let _id: string + let _rev: string - if (isDocument(idOrDoc)) { - _id = idOrDoc._id! - _rev = idOrDoc._rev! - } else { - _id = idOrDoc - _rev = rev! - } + if (isDocument(idOrDoc)) { + _id = idOrDoc._id! + _rev = idOrDoc._rev! + } else { + _id = idOrDoc + _rev = rev! + } - if (!_id || !_rev) { - throw new Error("Unable to remove doc without a valid _id and _rev.") - } - return this.updateOutput(() => db.destroy(_id, _rev)) + if (!_id || !_rev) { + throw new Error("Unable to remove doc without a valid _id and _rev.") + } + return () => db.destroy(_id, _rev) + }) } async post(document: AnyDocument, opts?: DatabasePutOpts) { @@ -176,45 +194,49 @@ export class DatabaseImpl implements Database { if (!document._id) { throw new Error("Cannot store document without _id field.") } - const db = await this.checkSetup() - if (!document.createdAt) { - document.createdAt = new Date().toISOString() - } - document.updatedAt = new Date().toISOString() - if (opts?.force && document._id) { - try { - const existing = await this.get(document._id) - if (existing) { - document._rev = existing._rev - } - } catch (err: any) { - if (err.status !== 404) { - throw err + return this.performCall(async db => { + if (!document.createdAt) { + document.createdAt = new Date().toISOString() + } + document.updatedAt = new Date().toISOString() + if (opts?.force && document._id) { + try { + const existing = await this.get(document._id) + if (existing) { + document._rev = existing._rev + } + } catch (err: any) { + if (err.status !== 404) { + throw err + } } } - } - return this.updateOutput(() => db.insert(document)) + return () => db.insert(document) + }) } async bulkDocs(documents: AnyDocument[]) { - const db = await this.checkSetup() - return this.updateOutput(() => db.bulk({ docs: documents })) + return this.performCall(db => { + return () => db.bulk({ docs: documents }) + }) } async allDocs( params: DatabaseQueryOpts ): Promise> { - const db = await this.checkSetup() - return this.updateOutput(() => db.list(params)) + return this.performCall(db => { + return () => db.list(params) + }) } async query( viewName: string, params: DatabaseQueryOpts ): Promise> { - const db = await this.checkSetup() - const [database, view] = viewName.split("/") - return this.updateOutput(() => db.view(database, view, params)) + return this.performCall(db => { + const [database, view] = viewName.split("/") + return () => db.view(database, view, params) + }) } async destroy() { @@ -231,8 +253,9 @@ export class DatabaseImpl implements Database { } async compact() { - const db = await this.checkSetup() - return this.updateOutput(() => db.compact()) + return this.performCall(db => { + return () => db.compact() + }) } // All below functions are in-frequently called, just utilise PouchDB diff --git a/packages/backend-core/src/db/instrumentation.ts b/packages/backend-core/src/db/instrumentation.ts index ba5febcba6..aa2ac424ae 100644 --- a/packages/backend-core/src/db/instrumentation.ts +++ b/packages/backend-core/src/db/instrumentation.ts @@ -31,13 +31,6 @@ export class DDInstrumentedDatabase implements Database { }) } - checkSetup(): Promise> { - return tracer.trace("db.checkSetup", span => { - span?.addTags({ db_name: this.name }) - return this.db.checkSetup() - }) - } - get(id?: string | undefined): Promise { return tracer.trace("db.get", span => { span?.addTags({ db_name: this.name, doc_id: id }) diff --git a/packages/bbui/src/Modal/ModalContent.svelte b/packages/bbui/src/Modal/ModalContent.svelte index 3ca584504c..189ef70c2b 100644 --- a/packages/bbui/src/Modal/ModalContent.svelte +++ b/packages/bbui/src/Modal/ModalContent.svelte @@ -40,7 +40,7 @@ loading = false } - async function confirm() { + export async function confirm() { loading = true if (!onConfirm || (await onConfirm()) !== keepOpen) { hide() 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/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 @@ + + +
+ +
+ -
- {appUrl} -
+
modal.confirm()}> + +
+ {appUrl} +
+