Automation overhaul to Typescript, plus type updates.

This commit is contained in:
mike12345567 2022-11-25 19:57:07 +00:00
parent eeebd0fe70
commit a3bb2e0d77
40 changed files with 442 additions and 302 deletions

View File

@ -25,7 +25,7 @@ const DefaultBucketName = {
PLUGINS: "plugins",
}
const env = {
const environment = {
isTest,
isDev,
JS_BCRYPT: process.env.JS_BCRYPT,
@ -80,7 +80,7 @@ const env = {
}
// clean up any environment variable edge cases
for (let [key, value] of Object.entries(env)) {
for (let [key, value] of Object.entries(environment)) {
// handle the edge case of "0" to disable an environment variable
if (value === "0") {
// @ts-ignore
@ -88,4 +88,4 @@ for (let [key, value] of Object.entries(env)) {
}
}
export = env
export = environment

View File

@ -39,7 +39,7 @@ export function createQueue<T>(
return queue
}
exports.shutdown = async () => {
export async function shutdown() {
if (QUEUES.length) {
clearInterval(cleanupInterval)
for (let queue of QUEUES) {

View File

@ -1,5 +1,5 @@
import actions from "../../automations/actions"
import triggers from "../../automations/triggers"
import * as actions from "../../automations/actions"
import * as triggers from "../../automations/triggers"
import {
getAutomationParams,
generateAutomationID,

View File

@ -1,22 +1,26 @@
const sendSmtpEmail = require("./steps/sendSmtpEmail")
const createRow = require("./steps/createRow")
const updateRow = require("./steps/updateRow")
const deleteRow = require("./steps/deleteRow")
const executeScript = require("./steps/executeScript")
const executeQuery = require("./steps/executeQuery")
const outgoingWebhook = require("./steps/outgoingWebhook")
const serverLog = require("./steps/serverLog")
const discord = require("./steps/discord")
const slack = require("./steps/slack")
const zapier = require("./steps/zapier")
const integromat = require("./steps/integromat")
let filter = require("./steps/filter")
let delay = require("./steps/delay")
let queryRow = require("./steps/queryRows")
let loop = require("./steps/loop")
const env = require("../environment")
import * as sendSmtpEmail from "./steps/sendSmtpEmail"
import * as createRow from "./steps/createRow"
import * as updateRow from "./steps/updateRow"
import * as deleteRow from "./steps/deleteRow"
import * as executeScript from "./steps/executeScript"
import * as executeQuery from "./steps/executeQuery"
import * as outgoingWebhook from "./steps/outgoingWebhook"
import * as serverLog from "./steps/serverLog"
import * as discord from "./steps/discord"
import * as slack from "./steps/slack"
import * as zapier from "./steps/zapier"
import * as integromat from "./steps/integromat"
import * as filter from "./steps/filter"
import * as delay from "./steps/delay"
import * as queryRow from "./steps/queryRows"
import * as loop from "./steps/loop"
import env from "../environment"
import { AutomationStep, AutomationStepInput } from "@budibase/types"
const ACTION_IMPLS = {
const ACTION_IMPLS: Record<
string,
(opts: AutomationStepInput) => Promise<any>
> = {
SEND_EMAIL_SMTP: sendSmtpEmail.run,
CREATE_ROW: createRow.run,
UPDATE_ROW: updateRow.run,
@ -28,14 +32,13 @@ const ACTION_IMPLS = {
DELAY: delay.run,
FILTER: filter.run,
QUERY_ROWS: queryRow.run,
LOOP: loop.run,
// these used to be lowercase step IDs, maintain for backwards compat
discord: discord.run,
slack: slack.run,
zapier: zapier.run,
integromat: integromat.run,
}
const ACTION_DEFINITIONS = {
export const ACTION_DEFINITIONS: Record<string, AutomationStep> = {
SEND_EMAIL_SMTP: sendSmtpEmail.definition,
CREATE_ROW: createRow.definition,
UPDATE_ROW: updateRow.definition,
@ -60,15 +63,15 @@ const ACTION_DEFINITIONS = {
// ran at all
if (env.SELF_HOSTED) {
const bash = require("./steps/bash")
// @ts-ignore
ACTION_IMPLS["EXECUTE_BASH"] = bash.run
// @ts-ignore
ACTION_DEFINITIONS["EXECUTE_BASH"] = bash.definition
}
/* istanbul ignore next */
exports.getAction = async function (actionName) {
export async function getAction(actionName: string) {
if (ACTION_IMPLS[actionName] != null) {
return ACTION_IMPLS[actionName]
}
}
exports.ACTION_DEFINITIONS = ACTION_DEFINITIONS

View File

@ -1,9 +1,10 @@
const {
import {
decodeJSBinding,
isJSBinding,
encodeJSBinding,
} = require("@budibase/string-templates")
const sdk = require("../sdk")
} from "@budibase/string-templates"
import sdk from "../sdk"
import { Row } from "@budibase/types"
/**
* When values are input to the system generally they will be of type string as this is required for template strings.
@ -21,7 +22,7 @@ const sdk = require("../sdk")
* @returns {object} The inputs object which has had all the various types supported by this function converted to their
* primitive types.
*/
exports.cleanInputValues = (inputs, schema) => {
export function cleanInputValues(inputs: Record<string, any>, schema: any) {
if (schema == null) {
return inputs
}
@ -62,12 +63,12 @@ exports.cleanInputValues = (inputs, schema) => {
* @param {object} row The input row structure which requires clean-up after having been through template statements.
* @returns {Promise<Object>} The cleaned up rows object, will should now have all the required primitive types.
*/
exports.cleanUpRow = async (tableId, row) => {
export async function cleanUpRow(tableId: string, row: Row) {
let table = await sdk.tables.getTable(tableId)
return exports.cleanInputValues(row, { properties: table.schema })
return cleanInputValues(row, { properties: table.schema })
}
exports.getError = err => {
export function getError(err: any) {
if (err == null) {
return "No error provided."
}
@ -80,13 +81,13 @@ exports.getError = err => {
return typeof err !== "string" ? err.toString() : err
}
exports.substituteLoopStep = (hbsString, substitute) => {
export function substituteLoopStep(hbsString: string, substitute: string) {
let checkForJS = isJSBinding(hbsString)
let substitutedHbsString = ""
let open = checkForJS ? `$("` : "{{"
let closed = checkForJS ? `")` : "}}"
if (checkForJS) {
hbsString = decodeJSBinding(hbsString)
hbsString = decodeJSBinding(hbsString) as string
}
let pointer = 0,
openPointer = 0,
@ -111,9 +112,9 @@ exports.substituteLoopStep = (hbsString, substitute) => {
return substitutedHbsString
}
exports.stringSplit = value => {
if (value == null) {
return []
export function stringSplit(value: string | string[]) {
if (value == null || Array.isArray(value)) {
return value || []
}
if (value.split("\n").length > 1) {
value = value.split("\n")

View File

@ -1,39 +0,0 @@
const { createBullBoard } = require("@bull-board/api")
const { BullAdapter } = require("@bull-board/api/bullAdapter")
const { KoaAdapter } = require("@bull-board/koa")
const { queue } = require("@budibase/backend-core")
const automation = require("../threads/automation")
const { backups } = require("@budibase/pro")
let automationQueue = queue.createQueue(
queue.JobQueue.AUTOMATION,
automation.removeStalled
)
const PATH_PREFIX = "/bulladmin"
exports.init = async () => {
// Set up queues for bull board admin
const backupQueue = await backups.getBackupQueue()
const queues = [automationQueue]
if (backupQueue) {
queues.push(backupQueue)
}
const adapters = []
const serverAdapter = new KoaAdapter()
for (let queue of queues) {
adapters.push(new BullAdapter(queue))
}
createBullBoard({
queues: adapters,
serverAdapter,
})
serverAdapter.setBasePath(PATH_PREFIX)
return serverAdapter.registerPlugin()
}
exports.shutdown = async () => {
await queue.shutdown()
}
exports.automationQueue = automationQueue

View File

@ -0,0 +1,38 @@
import { BullAdapter } from "@bull-board/api/bullAdapter"
import { KoaAdapter } from "@bull-board/koa"
import { queue } from "@budibase/backend-core"
import * as automation from "../threads/automation"
import { backups } from "@budibase/pro"
import { createBullBoard } from "@bull-board/api"
import BullQueue from "bull"
export const automationQueue: BullQueue.Queue = queue.createQueue(
queue.JobQueue.AUTOMATION,
{ removeStalledCb: automation.removeStalled }
)
const PATH_PREFIX = "/bulladmin"
export async function init() {
// Set up queues for bull board admin
const backupQueue = await backups.getBackupQueue()
const queues = [automationQueue]
if (backupQueue) {
queues.push(backupQueue)
}
const adapters = []
const serverAdapter: any = new KoaAdapter()
for (let queue of queues) {
adapters.push(new BullAdapter(queue))
}
createBullBoard({
queues: adapters,
serverAdapter,
})
serverAdapter.setBasePath(PATH_PREFIX)
return serverAdapter.registerPlugin()
}
export async function shutdown() {
await queue.shutdown()
}

View File

@ -1,29 +0,0 @@
const { processEvent } = require("./utils")
const { automationQueue, shutdown } = require("./bullboard")
const { TRIGGER_DEFINITIONS, rebootTrigger } = require("./triggers")
const { ACTION_DEFINITIONS } = require("./actions")
/**
* This module is built purely to kick off the worker farm and manage the inputs/outputs
*/
exports.init = async function () {
// this promise will not complete
const promise = automationQueue.process(async job => {
await processEvent(job)
})
// on init we need to trigger any reboot automations
await rebootTrigger()
return promise
}
exports.getQueues = () => {
return [automationQueue]
}
exports.shutdown = () => {
return shutdown()
}
exports.automationQueue = automationQueue
exports.TRIGGER_DEFINITIONS = TRIGGER_DEFINITIONS
exports.ACTION_DEFINITIONS = ACTION_DEFINITIONS

View File

@ -0,0 +1,26 @@
import { processEvent } from "./utils"
import { automationQueue } from "./bullboard"
import { rebootTrigger } from "./triggers"
import BullQueue from "bull"
export { automationQueue } from "./bullboard"
export { shutdown } from "./bullboard"
export { TRIGGER_DEFINITIONS } from "./triggers"
export { ACTION_DEFINITIONS } from "./actions"
/**
* This module is built purely to kick off the worker farm and manage the inputs/outputs
*/
export async function init() {
// this promise will not complete
const promise = automationQueue.process(async job => {
await processEvent(job)
})
// on init we need to trigger any reboot automations
await rebootTrigger()
return promise
}
export function getQueues(): BullQueue.Queue[] {
return [automationQueue]
}

View File

@ -1,16 +1,21 @@
const { execSync } = require("child_process")
const { processStringSync } = require("@budibase/string-templates")
const automationUtils = require("../automationUtils")
const environment = require("../../environment")
import { execSync } from "child_process"
import { processStringSync } from "@budibase/string-templates"
import automationUtils from "../automationUtils"
import environment from "../../environment"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
exports.definition = {
export const definition: AutomationStep = {
name: "Bash Scripting",
tagline: "Execute a bash command",
icon: "JourneyEvent",
description: "Run a bash script",
type: "ACTION",
internal: true,
stepId: "EXECUTE_BASH",
stepId: AutomationActionStepId.EXECUTE_BASH,
inputs: {},
schema: {
inputs: {
@ -39,7 +44,7 @@ exports.definition = {
},
}
exports.run = async function ({ inputs, context }) {
export async function run({ inputs, context }: AutomationStepInput) {
if (inputs.code == null) {
return {
stdout: "Budibase bash automation failed: Invalid inputs",
@ -55,7 +60,7 @@ exports.run = async function ({ inputs, context }) {
stdout = execSync(command, {
timeout: environment.QUERY_THREAD_TIMEOUT || 500,
}).toString()
} catch (err) {
} catch (err: any) {
stdout = err.message
success = false
}

View File

@ -1,15 +1,20 @@
import { save } from "../../api/controllers/row"
import { cleanUpRow, getError } from "../automationUtils"
import { buildCtx } from "./utils"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
export const definition = {
export const definition: AutomationStep = {
name: "Create Row",
tagline: "Create a {{inputs.enriched.table.name}} row",
icon: "TableRowAddBottom",
description: "Add a row to your database",
type: "ACTION",
internal: true,
stepId: "CREATE_ROW",
stepId: AutomationActionStepId.CREATE_ROW,
inputs: {},
schema: {
inputs: {
@ -58,7 +63,7 @@ export const definition = {
},
}
export async function run({ inputs, appId, emitter }: any) {
export async function run({ inputs, appId, emitter }: AutomationStepInput) {
if (inputs.row == null || inputs.row.tableId == null) {
return {
success: false,

View File

@ -1,11 +1,16 @@
let { wait } = require("../../utilities")
import { wait } from "../../utilities"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
exports.definition = {
export const definition: AutomationStep = {
name: "Delay",
icon: "Clock",
tagline: "Delay for {{inputs.time}} milliseconds",
description: "Delay the automation until an amount of time has passed",
stepId: "DELAY",
stepId: AutomationActionStepId.DELAY,
internal: true,
inputs: {},
schema: {
@ -31,7 +36,7 @@ exports.definition = {
type: "LOGIC",
}
exports.run = async function delay({ inputs }) {
export async function run({ inputs }: AutomationStepInput) {
await wait(inputs.time)
return {
success: true,

View File

@ -1,14 +1,19 @@
import { destroy } from "../../api/controllers/row"
import { buildCtx } from "./utils"
import { getError } from "../automationUtils"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
export const definition = {
export const definition: AutomationStep = {
description: "Delete a row from your database",
icon: "TableRowRemoveCenter",
name: "Delete Row",
tagline: "Delete a {{inputs.enriched.table.name}} row",
type: "ACTION",
stepId: "DELETE_ROW",
stepId: AutomationActionStepId.DELETE_ROW,
internal: true,
inputs: {},
schema: {
@ -47,7 +52,7 @@ export const definition = {
},
}
export async function run({ inputs, appId, emitter }: any) {
export async function run({ inputs, appId, emitter }: AutomationStepInput) {
if (inputs.id == null) {
return {
success: false,

View File

@ -1,15 +1,20 @@
const fetch = require("node-fetch")
const { getFetchResponse } = require("./utils")
import fetch from "node-fetch"
import { getFetchResponse } from "./utils"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
const DEFAULT_USERNAME = "Budibase Automate"
const DEFAULT_AVATAR_URL = "https://i.imgur.com/a1cmTKM.png"
exports.definition = {
export const definition: AutomationStep = {
name: "Discord Message",
tagline: "Send a message to a Discord server",
description: "Send a message to a Discord server",
icon: "ri-discord-line",
stepId: "discord",
stepId: AutomationActionStepId.discord,
type: "ACTION",
internal: false,
inputs: {},
@ -54,7 +59,7 @@ exports.definition = {
},
}
exports.run = async function ({ inputs }) {
export async function run({ inputs }: AutomationStepInput) {
let { url, username, avatar_url, content } = inputs
if (!username) {
username = DEFAULT_USERNAME

View File

@ -1,14 +1,19 @@
const queryController = require("../../api/controllers/query")
const { buildCtx } = require("./utils")
const automationUtils = require("../automationUtils")
import * as queryController from "../../api/controllers/query"
import { buildCtx } from "./utils"
import automationUtils from "../automationUtils"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
exports.definition = {
export const definition: AutomationStep = {
name: "External Data Connector",
tagline: "Execute Data Connector",
icon: "Data",
description: "Execute a query in an external data connector",
type: "ACTION",
stepId: "EXECUTE_QUERY",
stepId: AutomationActionStepId.EXECUTE_QUERY,
internal: true,
inputs: {},
schema: {
@ -50,7 +55,7 @@ exports.definition = {
},
}
exports.run = async function ({ inputs, appId, emitter }) {
export async function run({ inputs, appId, emitter }: AutomationStepInput) {
if (inputs.query == null) {
return {
success: false,
@ -62,7 +67,7 @@ exports.run = async function ({ inputs, appId, emitter }) {
const { queryId, ...rest } = inputs.query
const ctx = buildCtx(appId, emitter, {
const ctx: any = buildCtx(appId, emitter, {
body: {
parameters: rest,
},

View File

@ -1,15 +1,20 @@
const scriptController = require("../../api/controllers/script")
const { buildCtx } = require("./utils")
const automationUtils = require("../automationUtils")
import * as scriptController from "../../api/controllers/script"
import { buildCtx } from "./utils"
import automationUtils from "../automationUtils"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
exports.definition = {
export const definition: AutomationStep = {
name: "JS Scripting",
tagline: "Execute JavaScript Code",
icon: "Code",
description: "Run a piece of JavaScript code in your automation",
type: "ACTION",
internal: true,
stepId: "EXECUTE_SCRIPT",
stepId: AutomationActionStepId.EXECUTE_SCRIPT,
inputs: {},
schema: {
inputs: {
@ -38,7 +43,12 @@ exports.definition = {
},
}
exports.run = async function ({ inputs, appId, context, emitter }) {
export async function run({
inputs,
appId,
context,
emitter,
}: AutomationStepInput) {
if (inputs.code == null) {
return {
success: false,
@ -48,7 +58,7 @@ exports.run = async function ({ inputs, appId, context, emitter }) {
}
}
const ctx = buildCtx(appId, emitter, {
const ctx: any = buildCtx(appId, emitter, {
body: {
script: inputs.code,
context,

View File

@ -1,21 +1,24 @@
const FilterConditions = {
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
export const FilterConditions = {
EQUAL: "EQUAL",
NOT_EQUAL: "NOT_EQUAL",
GREATER_THAN: "GREATER_THAN",
LESS_THAN: "LESS_THAN",
}
const PrettyFilterConditions = {
export const PrettyFilterConditions = {
[FilterConditions.EQUAL]: "Equals",
[FilterConditions.NOT_EQUAL]: "Not equals",
[FilterConditions.GREATER_THAN]: "Greater than",
[FilterConditions.LESS_THAN]: "Less than",
}
exports.FilterConditions = FilterConditions
exports.PrettyFilterConditions = PrettyFilterConditions
exports.definition = {
export const definition: AutomationStep = {
name: "Condition",
tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}",
icon: "Branch2",
@ -23,9 +26,9 @@ exports.definition = {
"Conditionally halt automations which do not meet certain conditions",
type: "LOGIC",
internal: true,
stepId: "FILTER",
stepId: AutomationActionStepId.FILTER,
inputs: {
condition: FilterConditions.EQUALS,
condition: FilterConditions.EQUAL,
},
schema: {
inputs: {
@ -63,7 +66,7 @@ exports.definition = {
},
}
exports.run = async function filter({ inputs }) {
export async function run({ inputs }: AutomationStepInput) {
try {
let { field, condition, value } = inputs
// coerce types so that we can use them

View File

@ -1,13 +1,18 @@
const fetch = require("node-fetch")
const { getFetchResponse } = require("./utils")
import fetch from "node-fetch"
import { getFetchResponse } from "./utils"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
exports.definition = {
export const definition: AutomationStep = {
name: "Integromat Integration",
tagline: "Trigger an Integromat scenario",
description:
"Performs a webhook call to Integromat and gets the response (if configured)",
icon: "ri-shut-down-line",
stepId: "integromat",
stepId: AutomationActionStepId.integromat,
type: "ACTION",
internal: false,
inputs: {},
@ -61,7 +66,7 @@ exports.definition = {
},
}
exports.run = async function ({ inputs }) {
export async function run({ inputs }: AutomationStepInput) {
const { url, value1, value2, value3, value4, value5 } = inputs
const response = await fetch(url, {

View File

@ -1,9 +1,11 @@
exports.definition = {
import { AutomationActionStepId, AutomationStep } from "@budibase/types"
export const definition: AutomationStep = {
name: "Looping",
icon: "Reuse",
tagline: "Loop the block",
description: "Loop",
stepId: "LOOP",
stepId: AutomationActionStepId.LOOP,
internal: true,
inputs: {},
schema: {

View File

@ -1,13 +1,18 @@
const fetch = require("node-fetch")
const { getFetchResponse } = require("./utils")
const automationUtils = require("../automationUtils")
import fetch from "node-fetch"
import { getFetchResponse } from "./utils"
import automationUtils from "../automationUtils"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
const RequestType = {
POST: "POST",
GET: "GET",
PUT: "PUT",
DELETE: "DELETE",
PATCH: "PATCH",
enum RequestType {
POST = "POST",
GET = "GET",
PUT = "PUT",
DELETE = "DELETE",
PATCH = "PATCH",
}
const BODY_REQUESTS = [RequestType.POST, RequestType.PUT, RequestType.PATCH]
@ -16,7 +21,7 @@ const BODY_REQUESTS = [RequestType.POST, RequestType.PUT, RequestType.PATCH]
* NOTE: this functionality is deprecated - it no longer should be used.
*/
exports.definition = {
export const definition: AutomationStep = {
deprecated: true,
name: "Outgoing webhook",
tagline: "Send a {{inputs.requestMethod}} request",
@ -24,7 +29,7 @@ exports.definition = {
description: "Send a request of specified method to a URL",
type: "ACTION",
internal: true,
stepId: "OUTGOING_WEBHOOK",
stepId: AutomationActionStepId.OUTGOING_WEBHOOK,
inputs: {
requestMethod: "POST",
url: "http://",
@ -76,12 +81,12 @@ exports.definition = {
},
}
exports.run = async function ({ inputs }) {
export async function run({ inputs }: AutomationStepInput) {
let { requestMethod, url, requestBody, headers } = inputs
if (!url.startsWith("http")) {
url = `http://${url}`
}
const request = {
const request: any = {
method: requestMethod,
}
if (headers) {

View File

@ -1,36 +1,43 @@
const rowController = require("../../api/controllers/row")
const tableController = require("../../api/controllers/table")
const { FieldTypes } = require("../../constants")
const { buildCtx } = require("./utils")
const automationUtils = require("../automationUtils")
import * as rowController from "../../api/controllers/row"
import * as tableController from "../../api/controllers/table"
import { FieldTypes } from "../../constants"
import { buildCtx } from "./utils"
import automationUtils from "../automationUtils"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
SearchFilters,
Table,
} from "@budibase/types"
const SortOrders = {
ASCENDING: "ascending",
DESCENDING: "descending",
enum SortOrder {
ASCENDING = "ascending",
DESCENDING = "descending",
}
const SortOrdersPretty = {
[SortOrders.ASCENDING]: "Ascending",
[SortOrders.DESCENDING]: "Descending",
const SortOrderPretty = {
[SortOrder.ASCENDING]: "Ascending",
[SortOrder.DESCENDING]: "Descending",
}
const EmptyFilterOptions = {
RETURN_ALL: "all",
RETURN_NONE: "none",
enum EmptyFilterOption {
RETURN_ALL = "all",
RETURN_NONE = "none",
}
const EmptyFilterOptionsPretty = {
[EmptyFilterOptions.RETURN_ALL]: "Return all table rows",
[EmptyFilterOptions.RETURN_NONE]: "Return no rows",
const EmptyFilterOptionPretty = {
[EmptyFilterOption.RETURN_ALL]: "Return all table rows",
[EmptyFilterOption.RETURN_NONE]: "Return no rows",
}
exports.definition = {
export const definition: AutomationStep = {
description: "Query rows from the database",
icon: "Search",
name: "Query rows",
tagline: "Query rows from {{inputs.enriched.table.name}} table",
type: "ACTION",
stepId: "QUERY_ROWS",
stepId: AutomationActionStepId.QUERY_ROWS,
internal: true,
inputs: {},
schema: {
@ -54,8 +61,8 @@ exports.definition = {
sortOrder: {
type: "string",
title: "Sort Order",
enum: Object.values(SortOrders),
pretty: Object.values(SortOrdersPretty),
enum: Object.values(SortOrder),
pretty: Object.values(SortOrderPretty),
},
limit: {
type: "number",
@ -63,8 +70,8 @@ exports.definition = {
customType: "queryLimit",
},
onEmptyFilter: {
pretty: Object.values(EmptyFilterOptionsPretty),
enum: Object.values(EmptyFilterOptions),
pretty: Object.values(EmptyFilterOptionPretty),
enum: Object.values(EmptyFilterOption),
type: "string",
title: "When Filter Empty",
},
@ -88,8 +95,8 @@ exports.definition = {
},
}
async function getTable(appId, tableId) {
const ctx = buildCtx(appId, null, {
async function getTable(appId: string, tableId: string) {
const ctx: any = buildCtx(appId, null, {
params: {
tableId,
},
@ -98,20 +105,22 @@ async function getTable(appId, tableId) {
return ctx.body
}
function typeCoercion(filters, table) {
function typeCoercion(filters: SearchFilters, table: Table) {
if (!filters || !table) {
return filters
}
for (let key of Object.keys(filters)) {
if (typeof filters[key] === "object") {
for (let [property, value] of Object.entries(filters[key])) {
// @ts-ignore
const searchParam = filters[key]
if (typeof searchParam === "object") {
for (let [property, value] of Object.entries(searchParam)) {
const column = table.schema[property]
// convert string inputs
if (!column || typeof value !== "string") {
continue
}
if (column.type === FieldTypes.NUMBER) {
filters[key][property] = parseFloat(value)
searchParam[property] = parseFloat(value)
}
}
}
@ -119,11 +128,14 @@ function typeCoercion(filters, table) {
return filters
}
const hasNullFilters = filters =>
filters.length === 0 ||
filters.some(filter => filter.value === null || filter.value === "")
function hasNullFilters(filters: any[]) {
return (
filters.length === 0 ||
filters.some(filter => filter.value === null || filter.value === "")
)
}
exports.run = async function ({ inputs, appId }) {
export async function run({ inputs, appId }: AutomationStepInput) {
const { tableId, filters, sortColumn, sortOrder, limit } = inputs
if (!tableId) {
return {
@ -140,7 +152,7 @@ exports.run = async function ({ inputs, appId }) {
sortType =
fieldType === FieldTypes.NUMBER ? FieldTypes.NUMBER : FieldTypes.STRING
}
const ctx = buildCtx(appId, null, {
const ctx: any = buildCtx(appId, null, {
params: {
tableId,
},
@ -150,7 +162,7 @@ exports.run = async function ({ inputs, appId }) {
sort: sortColumn,
query: typeCoercion(filters || {}, table),
// default to ascending, like data tab
sortOrder: sortOrder || SortOrders.ASCENDING,
sortOrder: sortOrder || SortOrder.ASCENDING,
},
version: "1",
})
@ -158,7 +170,7 @@ exports.run = async function ({ inputs, appId }) {
let rows
if (
inputs.onEmptyFilter === EmptyFilterOptions.RETURN_NONE &&
inputs.onEmptyFilter === EmptyFilterOption.RETURN_NONE &&
inputs["filters-def"] &&
hasNullFilters(inputs["filters-def"])
) {

View File

@ -1,14 +1,19 @@
const { sendSmtpEmail } = require("../../utilities/workerRequests")
const automationUtils = require("../automationUtils")
import { sendSmtpEmail } from "../../utilities/workerRequests"
import automationUtils from "../automationUtils"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
exports.definition = {
export const definition: AutomationStep = {
description: "Send an email using SMTP",
tagline: "Send SMTP email to {{inputs.to}}",
icon: "Email",
name: "Send Email (SMTP)",
type: "ACTION",
internal: true,
stepId: "SEND_EMAIL_SMTP",
stepId: AutomationActionStepId.SEND_EMAIL_SMTP,
inputs: {},
schema: {
inputs: {
@ -56,7 +61,7 @@ exports.definition = {
},
}
exports.run = async function ({ inputs }) {
export async function run({ inputs }: AutomationStepInput) {
let { to, from, subject, contents, cc, bcc } = inputs
if (!contents) {
contents = "<h1>No content</h1>"

View File

@ -1,17 +1,23 @@
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
/**
* Note, there is some functionality in this that is not currently exposed as it
* is complex and maybe better to be opinionated here.
* GET/DELETE requests cannot handle body elements so they will not be sent if configured.
*/
exports.definition = {
export const definition: AutomationStep = {
name: "Backend log",
tagline: "Console log a value in the backend",
icon: "Monitoring",
description: "Logs the given text to the server (using console.log)",
type: "ACTION",
internal: true,
stepId: "SERVER_LOG",
stepId: AutomationActionStepId.SERVER_LOG,
inputs: {
text: "",
},
@ -41,7 +47,7 @@ exports.definition = {
},
}
exports.run = async function ({ inputs, appId }) {
export async function run({ inputs, appId }: AutomationStepInput) {
const message = `App ${appId} - ${inputs.text}`
console.log(message)
return {

View File

@ -1,12 +1,17 @@
const fetch = require("node-fetch")
const { getFetchResponse } = require("./utils")
import fetch from "node-fetch"
import { getFetchResponse } from "./utils"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
exports.definition = {
export const definition: AutomationStep = {
name: "Slack Message",
tagline: "Send a message to Slack",
description: "Send a message to Slack",
icon: "ri-slack-line",
stepId: "slack",
stepId: AutomationActionStepId.slack,
type: "ACTION",
internal: false,
inputs: {},
@ -43,7 +48,7 @@ exports.definition = {
},
}
exports.run = async function ({ inputs }) {
export async function run({ inputs }: AutomationStepInput) {
let { url, text } = inputs
const response = await fetch(url, {
method: "post",

View File

@ -1,15 +1,20 @@
const rowController = require("../../api/controllers/row")
const automationUtils = require("../automationUtils")
const { buildCtx } = require("./utils")
import * as rowController from "../../api/controllers/row"
import automationUtils from "../automationUtils"
import { buildCtx } from "./utils"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
exports.definition = {
export const definition: AutomationStep = {
name: "Update Row",
tagline: "Update a {{inputs.enriched.table.name}} row",
icon: "Refresh",
description: "Update a row in your database",
type: "ACTION",
internal: true,
stepId: "UPDATE_ROW",
stepId: AutomationActionStepId.UPDATE_ROW,
inputs: {},
schema: {
inputs: {
@ -55,7 +60,7 @@ exports.definition = {
},
}
exports.run = async function ({ inputs, appId, emitter }) {
export async function run({ inputs, appId, emitter }: AutomationStepInput) {
if (inputs.rowId == null || inputs.row == null) {
return {
success: false,
@ -74,7 +79,7 @@ exports.run = async function ({ inputs, appId, emitter }) {
}
// have to clean up the row, remove the table from it
const ctx = buildCtx(appId, emitter, {
const ctx: any = buildCtx(appId, emitter, {
body: {
...inputs.row,
_id: inputs.rowId,

View File

@ -1,4 +1,6 @@
exports.getFetchResponse = async fetched => {
import { EventEmitter } from "events"
export async function getFetchResponse(fetched: any) {
let status = fetched.status,
message
const contentType = fetched.headers.get("content-type")
@ -18,12 +20,16 @@ exports.getFetchResponse = async fetched => {
// throw added to them, so that controllers don't
// throw a ctx.throw undefined when error occurs
// opts can contain, body, params and version
exports.buildCtx = (appId, emitter, opts = {}) => {
const ctx = {
export async function buildCtx(
appId: string,
emitter?: EventEmitter | null,
opts: any = {}
) {
const ctx: any = {
appId,
user: { appId },
eventEmitter: emitter,
throw: (code, error) => {
throw: (code: string, error: any) => {
throw error
},
}

View File

@ -1,14 +1,20 @@
const fetch = require("node-fetch")
const { getFetchResponse } = require("./utils")
import fetch from "node-fetch"
import { getFetchResponse } from "./utils"
import {
AutomationActionStepId,
AutomationStep,
AutomationStepInput,
} from "@budibase/types"
exports.definition = {
export const definition: AutomationStep = {
name: "Zapier Webhook",
stepId: "zapier",
stepId: AutomationActionStepId.zapier,
type: "ACTION",
internal: false,
description: "Trigger a Zapier Zap via webhooks",
tagline: "Trigger a Zapier Zap",
icon: "ri-flashlight-line",
inputs: {},
schema: {
inputs: {
properties: {
@ -54,7 +60,7 @@ exports.definition = {
},
}
exports.run = async function ({ inputs }) {
export async function run({ inputs }: AutomationStepInput) {
const { url, value1, value2, value3, value4, value5 } = inputs
// send the platform to make sure zaps always work, even

View File

@ -1,10 +1,12 @@
exports.definition = {
import { AutomationTrigger, AutomationTriggerStepId } from "@budibase/types"
export const definition: AutomationTrigger = {
name: "App Action",
event: "app:trigger",
icon: "Apps",
tagline: "Automation fired from the frontend",
description: "Trigger an automation from an action inside your app",
stepId: "APP",
stepId: AutomationTriggerStepId.APP,
inputs: {},
schema: {
inputs: {

View File

@ -1,10 +1,12 @@
exports.definition = {
import { AutomationTrigger, AutomationTriggerStepId } from "@budibase/types"
export const definition: AutomationTrigger = {
name: "Cron Trigger",
event: "cron:trigger",
icon: "Clock",
tagline: "Cron Trigger (<b>{{inputs.cron}}</b>)",
description: "Triggers automation on a cron schedule.",
stepId: "CRON",
stepId: AutomationTriggerStepId.CRON,
inputs: {},
schema: {
inputs: {

View File

@ -1,15 +0,0 @@
const app = require("./app")
const cron = require("./cron")
const rowDeleted = require("./rowDeleted")
const rowSaved = require("./rowSaved")
const rowUpdated = require("./rowUpdated")
const webhook = require("./webhook")
exports.definitions = {
ROW_SAVED: rowSaved.definition,
ROW_UPDATED: rowUpdated.definition,
ROW_DELETED: rowDeleted.definition,
WEBHOOK: webhook.definition,
APP: app.definition,
CRON: cron.definition,
}

View File

@ -0,0 +1,15 @@
import app from "./app"
import cron from "./cron"
import rowDeleted from "./rowDeleted"
import rowSaved from "./rowSaved"
import rowUpdated from "./rowUpdated"
import webhook from "./webhook"
export const definitions = {
ROW_SAVED: rowSaved.definition,
ROW_UPDATED: rowUpdated.definition,
ROW_DELETED: rowDeleted.definition,
WEBHOOK: webhook.definition,
APP: app.definition,
CRON: cron.definition,
}

View File

@ -1,10 +1,12 @@
exports.definition = {
import { AutomationTrigger, AutomationTriggerStepId } from "@budibase/types"
export const definition: AutomationTrigger = {
name: "Row Deleted",
event: "row:delete",
icon: "TableRowRemoveCenter",
tagline: "Row is deleted from {{inputs.enriched.table.name}}",
description: "Fired when a row is deleted from your database",
stepId: "ROW_DELETED",
stepId: AutomationTriggerStepId.ROW_DELETED,
inputs: {},
schema: {
inputs: {

View File

@ -1,10 +1,12 @@
exports.definition = {
import { AutomationTrigger, AutomationTriggerStepId } from "@budibase/types"
export const definition: AutomationTrigger = {
name: "Row Created",
event: "row:save",
icon: "TableRowAddBottom",
tagline: "Row is added to {{inputs.enriched.table.name}}",
description: "Fired when a row is added to your database",
stepId: "ROW_SAVED",
stepId: AutomationTriggerStepId.ROW_SAVED,
inputs: {},
schema: {
inputs: {

View File

@ -1,10 +1,12 @@
exports.definition = {
import { AutomationTrigger, AutomationTriggerStepId } from "@budibase/types"
export const definition: AutomationTrigger = {
name: "Row Updated",
event: "row:update",
icon: "Refresh",
tagline: "Row is updated in {{inputs.enriched.table.name}}",
description: "Fired when a row is updated in your database",
stepId: "ROW_UPDATED",
stepId: AutomationTriggerStepId.ROW_UPDATED,
inputs: {},
schema: {
inputs: {

View File

@ -1,10 +1,12 @@
exports.definition = {
import { AutomationTrigger, AutomationTriggerStepId } from "@budibase/types"
export const definition: AutomationTrigger = {
name: "Webhook",
event: "web:trigger",
icon: "Send",
tagline: "Webhook endpoint is hit",
description: "Trigger an automation when a HTTP POST webhook is hit",
stepId: "WEBHOOK",
stepId: AutomationTriggerStepId.WEBHOOK,
inputs: {},
schema: {
inputs: {

View File

@ -1,16 +1,17 @@
const emitter = require("../events/index")
const { getAutomationParams } = require("../db/utils")
const { coerce } = require("../utilities/rowProcessor")
const { definitions } = require("./triggerInfo")
const { isDevAppID } = require("../db/utils")
import emitter from "../events/index"
import { getAutomationParams } from "../db/utils"
import { coerce } from "../utilities/rowProcessor"
import { definitions } from "./triggerInfo"
import { isDevAppID } from "../db/utils"
// need this to call directly, so we can get a response
const { automationQueue } = require("./bullboard")
const { checkTestFlag } = require("../utilities/redis")
const utils = require("./utils")
const env = require("../environment")
const { context, db: dbCore } = require("@budibase/backend-core")
import { automationQueue } from "./bullboard"
import { checkTestFlag } from "../utilities/redis"
import * as utils from "./utils"
import env from "../environment"
import { context, db as dbCore } from "@budibase/backend-core"
import { Automation, Row } from "@budibase/types"
const TRIGGER_DEFINITIONS = definitions
export const TRIGGER_DEFINITIONS = definitions
const JOB_OPTS = {
removeOnComplete: true,
removeOnFail: true,
@ -24,12 +25,15 @@ async function getAllAutomations() {
return automations.rows.map(row => row.doc)
}
async function queueRelevantRowAutomations(event, eventType) {
async function queueRelevantRowAutomations(
event: { appId: string; row: Row },
eventType: string
) {
if (event.appId == null) {
throw `No appId specified for ${eventType} - check event emitters.`
}
context.doInAppContext(event.appId, async () => {
await context.doInAppContext(event.appId, async () => {
let automations = await getAllAutomations()
// filter down to the correct event type
@ -85,20 +89,20 @@ emitter.on("row:delete", async function (event) {
await queueRelevantRowAutomations(event, "row:delete")
})
exports.externalTrigger = async function (
automation,
params,
{ getResponses } = {}
export async function externalTrigger(
automation: Automation,
params: { fields: Record<string, any> },
{ getResponses }: { getResponses?: boolean } = {}
) {
if (
automation.definition != null &&
automation.definition.trigger != null &&
automation.definition.trigger.stepId === definitions.APP.stepId &&
automation.definition.trigger.stepId === "APP" &&
!(await checkTestFlag(automation._id))
!(await checkTestFlag(automation._id!))
) {
// values are likely to be submitted as strings, so we shall convert to correct type
const coercedFields = {}
const coercedFields: any = {}
const fields = automation.definition.trigger.inputs.fields
for (let key of Object.keys(fields || {})) {
coercedFields[key] = coerce(params.fields[key], fields[key])
@ -113,7 +117,7 @@ exports.externalTrigger = async function (
}
}
exports.rebootTrigger = async () => {
export async function rebootTrigger() {
// reboot cron option is only available on the main thread at
// startup and only usable in self host and single tenant environments
if (env.isInThread() || !env.SELF_HOSTED || env.MULTI_TENANCY) {
@ -121,7 +125,10 @@ exports.rebootTrigger = async () => {
}
// iterate through all production apps, find the reboot crons
// and trigger events for them
const appIds = await dbCore.getAllApps({ dev: false, idsOnly: true })
const appIds = (await dbCore.getAllApps({
dev: false,
idsOnly: true,
})) as string[]
for (let prodAppId of appIds) {
await context.doInAppContext(prodAppId, async () => {
let automations = await getAllAutomations()
@ -142,5 +149,3 @@ exports.rebootTrigger = async () => {
})
}
}
exports.TRIGGER_DEFINITIONS = TRIGGER_DEFINITIONS

View File

@ -1,4 +1,4 @@
const { join } = require("path")
import { join } from "path"
function isTest() {
return isCypress() || isJest()
@ -28,7 +28,7 @@ if (!LOADED && isDev() && !isTest()) {
LOADED = true
}
function parseIntSafe(number) {
function parseIntSafe(number?: string) {
if (number) {
return parseInt(number)
}
@ -36,7 +36,7 @@ function parseIntSafe(number) {
let inThread = false
module.exports = {
const environment = {
// important - prefer app port to generic port
PORT: process.env.APP_PORT || process.env.PORT,
JWT_SECRET: process.env.JWT_SECRET,
@ -86,7 +86,7 @@ module.exports = {
SELF_HOSTED: process.env.SELF_HOSTED,
// old
CLIENT_ID: process.env.CLIENT_ID,
_set(key, value) {
_set(key: string, value: any) {
process.env[key] = value
module.exports[key] = value
},
@ -108,13 +108,16 @@ module.exports = {
// threading can cause memory issues with node-ts in development
if (isDev() && module.exports.DISABLE_THREADING == null) {
module.exports._set("DISABLE_THREADING", "1")
environment._set("DISABLE_THREADING", "1")
}
// clean up any environment variable edge cases
for (let [key, value] of Object.entries(module.exports)) {
// handle the edge case of "0" to disable an environment variable
if (value === "0") {
module.exports[key] = 0
// @ts-ignore
environment[key] = 0
}
}
export = environment

View File

@ -62,7 +62,7 @@ export async function sendSmtpEmail(
contents: string,
cc: string,
bcc: string,
automation: Automation
automation: boolean
) {
// tenant ID will be set in header
const response = await fetch(

View File

@ -1,4 +1,5 @@
import { Document } from "../document"
import { EventEmitter } from "events"
export enum AutomationTriggerStepId {
ROW_SAVED = "ROW_SAVED",
@ -14,6 +15,7 @@ export enum AutomationActionStepId {
CREATE_ROW = "CREATE_ROW",
UPDATE_ROW = "UPDATE_ROW",
DELETE_ROW = "DELETE_ROW",
EXECUTE_BASH = "EXECUTE_BASH",
OUTGOING_WEBHOOK = "OUTGOING_WEBHOOK",
EXECUTE_SCRIPT = "EXECUTE_SCRIPT",
EXECUTE_QUERY = "EXECUTE_QUERY",
@ -40,7 +42,14 @@ export interface Automation extends Document {
}
export interface AutomationStep {
id: string
id?: string
name: string
tagline: string
icon: string
description: string
type: string
internal?: boolean
deprecated?: boolean
stepId: AutomationTriggerStepId | AutomationActionStepId
inputs: {
[key: string]: any
@ -52,10 +61,12 @@ export interface AutomationStep {
outputs: {
[key: string]: any
}
required?: string[]
}
}
export interface AutomationTrigger extends AutomationStep {
event?: string
cronJobId?: string
}
@ -91,3 +102,10 @@ export interface AutomationLogPage {
hasNextPage: boolean
nextPage?: string
}
export type AutomationStepInput = {
inputs: Record<string, any>
context: Record<string, any>
emitter: EventEmitter
appId: string
}

View File

@ -26,7 +26,7 @@ function parseIntSafe(number: any) {
}
}
const env = {
const environment = {
// auth
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
@ -77,8 +77,10 @@ const env = {
}
// if some var haven't been set, define them
if (!env.APPS_URL) {
env.APPS_URL = isDev() ? "http://localhost:4001" : "http://app-service:4002"
if (!environment.APPS_URL) {
environment.APPS_URL = isDev()
? "http://localhost:4001"
: "http://app-service:4002"
}
// clean up any environment variable edge cases
@ -90,4 +92,4 @@ for (let [key, value] of Object.entries(module.exports)) {
}
}
export = env
export = environment