Merge branch 'grid-new-component-dnd' of github.com:Budibase/budibase into grid-new-component-dnd
This commit is contained in:
commit
5ac5f90775
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"version": "3.4.1",
|
||||
"version": "3.4.3",
|
||||
"npmClient": "yarn",
|
||||
"concurrency": 20,
|
||||
"command": {
|
||||
|
|
|
@ -1,45 +1,58 @@
|
|||
import events from "events"
|
||||
import { newid } from "../utils"
|
||||
import { Queue, QueueOptions, JobOptions } from "./queue"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
import { Job, JobId, JobInformation } from "bull"
|
||||
|
||||
interface JobMessage {
|
||||
function jobToJobInformation(job: Job): JobInformation {
|
||||
let cron = ""
|
||||
let every = -1
|
||||
let tz: string | undefined = undefined
|
||||
let endDate: number | undefined = undefined
|
||||
|
||||
const repeat = job.opts?.repeat
|
||||
if (repeat) {
|
||||
endDate = repeat.endDate ? new Date(repeat.endDate).getTime() : Date.now()
|
||||
tz = repeat.tz
|
||||
if ("cron" in repeat) {
|
||||
cron = repeat.cron
|
||||
} else {
|
||||
every = repeat.every
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: job.id.toString(),
|
||||
name: "",
|
||||
key: job.id.toString(),
|
||||
tz,
|
||||
endDate,
|
||||
cron,
|
||||
every,
|
||||
next: 0,
|
||||
}
|
||||
}
|
||||
|
||||
interface JobMessage<T = any> extends Partial<Job<T>> {
|
||||
id: string
|
||||
timestamp: number
|
||||
queue: string
|
||||
queue: Queue<T>
|
||||
data: any
|
||||
opts?: JobOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Bull works with a Job wrapper around all messages that contains a lot more information about
|
||||
* the state of the message, this object constructor implements the same schema of Bull jobs
|
||||
* for the sake of maintaining API consistency.
|
||||
* @param queue The name of the queue which the message will be carried on.
|
||||
* @param message The JSON message which will be passed back to the consumer.
|
||||
* @returns A new job which can now be put onto the queue, this is mostly an
|
||||
* internal structure so that an in memory queue can be easily swapped for a Bull queue.
|
||||
*/
|
||||
function newJob(queue: string, message: any, opts?: JobOptions): JobMessage {
|
||||
return {
|
||||
id: newid(),
|
||||
timestamp: Date.now(),
|
||||
queue: queue,
|
||||
data: message,
|
||||
opts,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is designed to replicate Bull (https://github.com/OptimalBits/bull) in memory as a sort of mock.
|
||||
* It is relatively simple, using an event emitter internally to register when messages are available
|
||||
* to the consumers - in can support many inputs and many consumers.
|
||||
* This is designed to replicate Bull (https://github.com/OptimalBits/bull) in
|
||||
* memory as a sort of mock. It is relatively simple, using an event emitter
|
||||
* internally to register when messages are available to the consumers - in can
|
||||
* support many inputs and many consumers.
|
||||
*/
|
||||
class InMemoryQueue implements Partial<Queue> {
|
||||
_name: string
|
||||
_opts?: QueueOptions
|
||||
_messages: JobMessage[]
|
||||
_queuedJobIds: Set<string>
|
||||
_emitter: NodeJS.EventEmitter
|
||||
_emitter: NodeJS.EventEmitter<{ message: [JobMessage]; completed: [Job] }>
|
||||
_runCount: number
|
||||
_addCount: number
|
||||
|
||||
|
@ -69,34 +82,29 @@ class InMemoryQueue implements Partial<Queue> {
|
|||
*/
|
||||
async process(concurrencyOrFunc: number | any, func?: any) {
|
||||
func = typeof concurrencyOrFunc === "number" ? func : concurrencyOrFunc
|
||||
this._emitter.on("message", async () => {
|
||||
if (this._messages.length <= 0) {
|
||||
return
|
||||
}
|
||||
let msg = this._messages.shift()
|
||||
|
||||
let resp = func(msg)
|
||||
this._emitter.on("message", async message => {
|
||||
let resp = func(message)
|
||||
|
||||
async function retryFunc(fnc: any) {
|
||||
try {
|
||||
await fnc
|
||||
} catch (e: any) {
|
||||
await new Promise<void>(r => setTimeout(() => r(), 50))
|
||||
|
||||
await retryFunc(func(msg))
|
||||
await helpers.wait(50)
|
||||
await retryFunc(func(message))
|
||||
}
|
||||
}
|
||||
|
||||
if (resp.then != null) {
|
||||
try {
|
||||
await retryFunc(resp)
|
||||
this._emitter.emit("completed", message as Job)
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
this._runCount++
|
||||
const jobId = msg?.opts?.jobId?.toString()
|
||||
if (jobId && msg?.opts?.removeOnComplete) {
|
||||
const jobId = message.opts?.jobId?.toString()
|
||||
if (jobId && message.opts?.removeOnComplete) {
|
||||
this._queuedJobIds.delete(jobId)
|
||||
}
|
||||
})
|
||||
|
@ -130,9 +138,16 @@ class InMemoryQueue implements Partial<Queue> {
|
|||
}
|
||||
|
||||
const pushMessage = () => {
|
||||
this._messages.push(newJob(this._name, data, opts))
|
||||
const message: JobMessage = {
|
||||
id: newid(),
|
||||
timestamp: Date.now(),
|
||||
queue: this as unknown as Queue,
|
||||
data,
|
||||
opts,
|
||||
}
|
||||
this._messages.push(message)
|
||||
this._addCount++
|
||||
this._emitter.emit("message")
|
||||
this._emitter.emit("message", message)
|
||||
}
|
||||
|
||||
const delay = opts?.delay
|
||||
|
@ -158,13 +173,6 @@ class InMemoryQueue implements Partial<Queue> {
|
|||
console.log(cronJobId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implemented for tests
|
||||
*/
|
||||
async getRepeatableJobs() {
|
||||
return []
|
||||
}
|
||||
|
||||
async removeJobs(_pattern: string) {
|
||||
// no-op
|
||||
}
|
||||
|
@ -176,13 +184,31 @@ class InMemoryQueue implements Partial<Queue> {
|
|||
return []
|
||||
}
|
||||
|
||||
async getJob() {
|
||||
async getJob(id: JobId) {
|
||||
for (const message of this._messages) {
|
||||
if (message.id === id) {
|
||||
return message as Job
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
on() {
|
||||
// do nothing
|
||||
return this as any
|
||||
on(event: string, callback: (...args: any[]) => void): Queue {
|
||||
// @ts-expect-error - this callback can be one of many types
|
||||
this._emitter.on(event, callback)
|
||||
return this as unknown as Queue
|
||||
}
|
||||
|
||||
async count() {
|
||||
return this._messages.length
|
||||
}
|
||||
|
||||
async getCompletedCount() {
|
||||
return this._runCount
|
||||
}
|
||||
|
||||
async getRepeatableJobs() {
|
||||
return this._messages.map(job => jobToJobInformation(job as Job))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -388,7 +388,7 @@ class InternalBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
if (typeof input === "string") {
|
||||
if (typeof input === "string" && schema.type === FieldType.DATETIME) {
|
||||
if (isInvalidISODateString(input)) {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
import { Feature, License, Quotas } from "@budibase/types"
|
||||
import {
|
||||
Feature,
|
||||
License,
|
||||
MonthlyQuotaName,
|
||||
QuotaType,
|
||||
QuotaUsageType,
|
||||
} from "@budibase/types"
|
||||
import cloneDeep from "lodash/cloneDeep"
|
||||
import merge from "lodash/merge"
|
||||
|
||||
let CLOUD_FREE_LICENSE: License
|
||||
let UNLIMITED_LICENSE: License
|
||||
|
@ -27,18 +34,19 @@ export function initInternal(opts: {
|
|||
|
||||
export interface UseLicenseOpts {
|
||||
features?: Feature[]
|
||||
quotas?: Quotas
|
||||
monthlyQuotas?: [MonthlyQuotaName, number][]
|
||||
}
|
||||
|
||||
// LICENSES
|
||||
|
||||
export const useLicense = (license: License, opts?: UseLicenseOpts) => {
|
||||
if (opts) {
|
||||
if (opts.features) {
|
||||
license.features.push(...opts.features)
|
||||
}
|
||||
if (opts.quotas) {
|
||||
license.quotas = opts.quotas
|
||||
if (opts?.features) {
|
||||
license.features.push(...opts.features)
|
||||
}
|
||||
if (opts?.monthlyQuotas) {
|
||||
for (const [name, value] of opts.monthlyQuotas) {
|
||||
license.quotas[QuotaType.USAGE][QuotaUsageType.MONTHLY][name].value =
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,12 +65,9 @@ export const useCloudFree = () => {
|
|||
|
||||
// FEATURES
|
||||
|
||||
const useFeature = (feature: Feature) => {
|
||||
const useFeature = (feature: Feature, extra?: Partial<UseLicenseOpts>) => {
|
||||
const license = cloneDeep(getCachedLicense() || UNLIMITED_LICENSE)
|
||||
const opts: UseLicenseOpts = {
|
||||
features: [feature],
|
||||
}
|
||||
|
||||
const opts: UseLicenseOpts = merge({ features: [feature] }, extra)
|
||||
return useLicense(license, opts)
|
||||
}
|
||||
|
||||
|
@ -102,8 +107,12 @@ export const useAppBuilders = () => {
|
|||
return useFeature(Feature.APP_BUILDERS)
|
||||
}
|
||||
|
||||
export const useBudibaseAI = () => {
|
||||
return useFeature(Feature.BUDIBASE_AI)
|
||||
export const useBudibaseAI = (opts?: { monthlyQuota?: number }) => {
|
||||
return useFeature(Feature.BUDIBASE_AI, {
|
||||
monthlyQuotas: [
|
||||
[MonthlyQuotaName.BUDIBASE_AI_CREDITS, opts?.monthlyQuota || 1000],
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
export const useAICustomConfigs = () => {
|
||||
|
|
|
@ -34,7 +34,7 @@ let candidateTarget: HTMLElement | undefined
|
|||
// Processes a "click outside" event and invokes callbacks if our source element
|
||||
// is valid
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
const target = (e.target || e.relatedTarget) as HTMLElement
|
||||
|
||||
// Ignore click if this is an ignored class
|
||||
if (target.closest('[data-ignore-click-outside="true"]')) {
|
||||
|
@ -91,9 +91,19 @@ const handleMouseDown = (e: MouseEvent) => {
|
|||
document.addEventListener("click", handleMouseUp, true)
|
||||
}
|
||||
|
||||
// Handle iframe clicks by detecting a loss of focus on the main window
|
||||
const handleBlur = () => {
|
||||
if (document.activeElement?.tagName === "IFRAME") {
|
||||
handleClick(
|
||||
new MouseEvent("click", { relatedTarget: document.activeElement })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Global singleton listeners for our events
|
||||
document.addEventListener("mousedown", handleMouseDown)
|
||||
document.addEventListener("contextmenu", handleClick)
|
||||
window.addEventListener("blur", handleBlur)
|
||||
|
||||
/**
|
||||
* Adds or updates a click handler
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 43a5785ccb4f83ce929b29f05ea0a62199fcdf23
|
||||
Subproject commit 8cbaa80a9cc1152c6cd53722e64da7d824da6e16
|
|
@ -107,10 +107,7 @@ describe("/automations", () => {
|
|||
})
|
||||
|
||||
it("Should ensure you can't have a branch as not a last step", async () => {
|
||||
const automation = createAutomationBuilder({
|
||||
name: "String Equality Branching",
|
||||
appId: config.getAppId(),
|
||||
})
|
||||
const automation = createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "active" } })
|
||||
.branch({
|
||||
activeBranch: {
|
||||
|
@ -134,10 +131,7 @@ describe("/automations", () => {
|
|||
})
|
||||
|
||||
it("Should check validation on an automation that has a branch step with no children", async () => {
|
||||
const automation = createAutomationBuilder({
|
||||
name: "String Equality Branching",
|
||||
appId: config.getAppId(),
|
||||
})
|
||||
const automation = createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "active" } })
|
||||
.branch({})
|
||||
.serverLog({ text: "Inactive user" })
|
||||
|
@ -153,10 +147,7 @@ describe("/automations", () => {
|
|||
})
|
||||
|
||||
it("Should check validation on a branch step with empty conditions", async () => {
|
||||
const automation = createAutomationBuilder({
|
||||
name: "String Equality Branching",
|
||||
appId: config.getAppId(),
|
||||
})
|
||||
const automation = createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "active" } })
|
||||
.branch({
|
||||
activeBranch: {
|
||||
|
@ -177,10 +168,7 @@ describe("/automations", () => {
|
|||
})
|
||||
|
||||
it("Should check validation on an branch that has a condition that is not valid", async () => {
|
||||
const automation = createAutomationBuilder({
|
||||
name: "String Equality Branching",
|
||||
appId: config.getAppId(),
|
||||
})
|
||||
const automation = createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "active" } })
|
||||
.branch({
|
||||
activeBranch: {
|
||||
|
@ -252,12 +240,7 @@ describe("/automations", () => {
|
|||
})
|
||||
|
||||
it("should be able to access platformUrl, logoUrl and company in the automation", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Automation",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.serverLog({
|
||||
text: "{{ settings.url }}",
|
||||
})
|
||||
|
|
|
@ -169,6 +169,18 @@ if (descriptions.length) {
|
|||
)
|
||||
}
|
||||
|
||||
const resetRowUsage = async () => {
|
||||
await config.doInContext(
|
||||
undefined,
|
||||
async () =>
|
||||
await quotas.setUsage(
|
||||
0,
|
||||
StaticQuotaName.ROWS,
|
||||
QuotaUsageType.STATIC
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const getRowUsage = async () => {
|
||||
const { total } = await config.doInContext(undefined, () =>
|
||||
quotas.getCurrentUsageValues(
|
||||
|
@ -206,6 +218,10 @@ if (descriptions.length) {
|
|||
table = await config.api.table.save(defaultTable())
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetRowUsage()
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
it("creates a new row successfully", async () => {
|
||||
const rowUsage = await getRowUsage()
|
||||
|
@ -3317,6 +3333,7 @@ if (descriptions.length) {
|
|||
beforeAll(async () => {
|
||||
mocks.licenses.useBudibaseAI()
|
||||
mocks.licenses.useAICustomConfigs()
|
||||
|
||||
envCleanup = setEnv({
|
||||
OPENAI_API_KEY: "sk-abcdefghijklmnopqrstuvwxyz1234567890abcd",
|
||||
})
|
||||
|
|
|
@ -1040,6 +1040,12 @@ if (descriptions.length) {
|
|||
string: { name: "FO" },
|
||||
}).toContainExactly([{ name: "foo" }])
|
||||
})
|
||||
|
||||
it("should not coerce string to date for string columns", async () => {
|
||||
await expectQuery({
|
||||
string: { name: "2020-01-01" },
|
||||
}).toFindNothing()
|
||||
})
|
||||
})
|
||||
|
||||
describe("range", () => {
|
||||
|
|
|
@ -5,9 +5,9 @@ import * as automation from "../threads/automation"
|
|||
import { backups } from "@budibase/pro"
|
||||
import { getAppMigrationQueue } from "../appMigrations/queue"
|
||||
import { createBullBoard } from "@bull-board/api"
|
||||
import BullQueue from "bull"
|
||||
import { AutomationData } from "@budibase/types"
|
||||
|
||||
export const automationQueue: BullQueue.Queue = queue.createQueue(
|
||||
export const automationQueue = queue.createQueue<AutomationData>(
|
||||
queue.JobQueue.AUTOMATION,
|
||||
{ removeStalledCb: automation.removeStalled }
|
||||
)
|
||||
|
@ -16,24 +16,20 @@ const PATH_PREFIX = "/bulladmin"
|
|||
|
||||
export async function init() {
|
||||
// Set up queues for bull board admin
|
||||
const queues = [new BullAdapter(automationQueue)]
|
||||
|
||||
const backupQueue = backups.getBackupQueue()
|
||||
const appMigrationQueue = getAppMigrationQueue()
|
||||
const queues = [automationQueue]
|
||||
if (backupQueue) {
|
||||
queues.push(backupQueue)
|
||||
queues.push(new BullAdapter(backupQueue))
|
||||
}
|
||||
|
||||
const appMigrationQueue = getAppMigrationQueue()
|
||||
if (appMigrationQueue) {
|
||||
queues.push(appMigrationQueue)
|
||||
queues.push(new BullAdapter(appMigrationQueue))
|
||||
}
|
||||
const adapters = []
|
||||
const serverAdapter: any = new KoaAdapter()
|
||||
for (let queue of queues) {
|
||||
adapters.push(new BullAdapter(queue))
|
||||
}
|
||||
createBullBoard({
|
||||
queues: adapters,
|
||||
serverAdapter,
|
||||
})
|
||||
|
||||
const serverAdapter = new KoaAdapter()
|
||||
createBullBoard({ queues, serverAdapter })
|
||||
serverAdapter.setBasePath(PATH_PREFIX)
|
||||
return serverAdapter.registerPlugin()
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { processEvent } from "./utils"
|
||||
import { automationQueue } from "./bullboard"
|
||||
import { rebootTrigger } from "./triggers"
|
||||
import BullQueue from "bull"
|
||||
import { automationsEnabled } from "../features"
|
||||
|
||||
export { automationQueue } from "./bullboard"
|
||||
|
@ -25,6 +24,6 @@ export async function init() {
|
|||
return promise
|
||||
}
|
||||
|
||||
export function getQueues(): BullQueue.Queue[] {
|
||||
return [automationQueue]
|
||||
export function getQueue() {
|
||||
return automationQueue
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@ import { basicAutomation } from "../../tests/utilities/structures"
|
|||
import { wait } from "../../utilities"
|
||||
import { makePartial } from "../../tests/utilities"
|
||||
import { cleanInputValues } from "../automationUtils"
|
||||
import * as setup from "./utilities"
|
||||
import { Automation } from "@budibase/types"
|
||||
import TestConfiguration from "../../tests/utilities/TestConfiguration"
|
||||
|
||||
describe("Run through some parts of the automations system", () => {
|
||||
let config = setup.getConfig()
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await automation.init()
|
||||
|
@ -30,7 +30,7 @@ describe("Run through some parts of the automations system", () => {
|
|||
|
||||
afterAll(async () => {
|
||||
await automation.shutdown()
|
||||
setup.afterAll()
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should be able to init in builder", async () => {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import * as automation from "../../index"
|
||||
import * as setup from "../utilities"
|
||||
import * as automation from "../index"
|
||||
import { Table, AutomationStatus } from "@budibase/types"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
import TestConfiguration from "../../tests/utilities/TestConfiguration"
|
||||
|
||||
describe("Branching automations", () => {
|
||||
let config = setup.getConfig(),
|
||||
table: Table
|
||||
const config = new TestConfiguration()
|
||||
let table: Table
|
||||
|
||||
beforeEach(async () => {
|
||||
await automation.init()
|
||||
|
@ -14,7 +14,9 @@ describe("Branching automations", () => {
|
|||
await config.createRow()
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should run a multiple nested branching automation", async () => {
|
||||
const firstLogId = "11111111-1111-1111-1111-111111111111"
|
||||
|
@ -22,12 +24,7 @@ describe("Branching automations", () => {
|
|||
const branch2LogId = "33333333-3333-3333-3333-333333333333"
|
||||
const branch2Id = "44444444-4444-4444-4444-444444444444"
|
||||
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Trigger with Loop and Create Row",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.serverLog(
|
||||
{ text: "Starting automation" },
|
||||
{ stepName: "FirstLog", stepId: firstLogId }
|
||||
|
@ -85,11 +82,7 @@ describe("Branching automations", () => {
|
|||
})
|
||||
|
||||
it("should execute correct branch based on string equality", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "String Equality Branching",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "active" } })
|
||||
.branch({
|
||||
activeBranch: {
|
||||
|
@ -114,11 +107,7 @@ describe("Branching automations", () => {
|
|||
})
|
||||
|
||||
it("should handle multiple conditions with AND operator", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Multiple AND Conditions Branching",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "active", role: "admin" } })
|
||||
.branch({
|
||||
activeAdminBranch: {
|
||||
|
@ -146,11 +135,7 @@ describe("Branching automations", () => {
|
|||
})
|
||||
|
||||
it("should handle multiple conditions with OR operator", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Multiple OR Conditions Branching",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "test", role: "user" } })
|
||||
.branch({
|
||||
specialBranch: {
|
||||
|
@ -182,11 +167,7 @@ describe("Branching automations", () => {
|
|||
})
|
||||
|
||||
it("should stop the branch automation when no conditions are met", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Multiple OR Conditions Branching",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "test", role: "user" } })
|
||||
.createRow({ row: { name: "Test", tableId: table._id } })
|
||||
.branch({
|
||||
|
@ -213,7 +194,6 @@ describe("Branching automations", () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
.serverLog({ text: "Test" })
|
||||
.run()
|
||||
|
||||
expect(results.steps[1].outputs.status).toEqual(
|
||||
|
@ -223,11 +203,7 @@ describe("Branching automations", () => {
|
|||
})
|
||||
|
||||
it("evaluate multiple conditions", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "evaluate multiple conditions",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { test_trigger: true } })
|
||||
.serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" })
|
||||
.branch({
|
||||
|
@ -268,11 +244,7 @@ describe("Branching automations", () => {
|
|||
})
|
||||
|
||||
it("evaluate multiple conditions with interpolated text", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "evaluate multiple conditions",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { test_trigger: true } })
|
||||
.serverLog({ text: "Starting automation" }, { stepId: "aN6znRYHG" })
|
||||
.branch({
|
|
@ -1,22 +0,0 @@
|
|||
import { runStep, actions, getConfig } from "./utilities"
|
||||
import { reset } from "timekeeper"
|
||||
|
||||
// need real Date for this test
|
||||
reset()
|
||||
|
||||
describe("test the delay logic", () => {
|
||||
const config = getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
it("should be able to run the delay", async () => {
|
||||
const time = 100
|
||||
const before = Date.now()
|
||||
await runStep(config, actions.DELAY.stepId, { time: time })
|
||||
const now = Date.now()
|
||||
// divide by two just so that test will always pass as long as there was some sort of delay
|
||||
expect(now - before).toBeGreaterThanOrEqual(time / 2)
|
||||
})
|
||||
})
|
|
@ -1,65 +0,0 @@
|
|||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
import * as setup from "./utilities"
|
||||
|
||||
describe("test the delete row action", () => {
|
||||
let table: any,
|
||||
row: any,
|
||||
config = setup.getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
table = await config.createTable()
|
||||
row = await config.createRow()
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
it("should be able to run the delete row action", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Delete Row Automation",
|
||||
})
|
||||
|
||||
await builder
|
||||
.appAction({ fields: {} })
|
||||
.deleteRow({
|
||||
tableId: table._id,
|
||||
id: row._id,
|
||||
revision: row._rev,
|
||||
})
|
||||
.run()
|
||||
|
||||
await config.api.row.get(table._id, row._id, {
|
||||
status: 404,
|
||||
})
|
||||
})
|
||||
|
||||
it("should check invalid inputs return an error", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Invalid Inputs Automation",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
.deleteRow({ tableId: "", id: "", revision: "" })
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should return an error when table doesn't exist", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Nonexistent Table Automation",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
.deleteRow({
|
||||
tableId: "invalid",
|
||||
id: "invalid",
|
||||
revision: "invalid",
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
})
|
|
@ -1,26 +0,0 @@
|
|||
import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities"
|
||||
import nock from "nock"
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
let config = getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(_afterAll)
|
||||
|
||||
beforeEach(() => {
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
|
||||
const res = await runStep(config, actions.discord.stepId, {
|
||||
url: "http://www.example.com",
|
||||
username: "joe_bloggs",
|
||||
})
|
||||
expect(res.response.foo).toEqual("bar")
|
||||
expect(res.success).toEqual(true)
|
||||
})
|
||||
})
|
|
@ -1,64 +0,0 @@
|
|||
import * as setup from "./utilities"
|
||||
import { automations } from "@budibase/shared-core"
|
||||
|
||||
const FilterConditions = automations.steps.filter.FilterConditions
|
||||
|
||||
describe("test the filter logic", () => {
|
||||
const config = setup.getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
async function checkFilter(
|
||||
field: any,
|
||||
condition: string,
|
||||
value: any,
|
||||
pass = true
|
||||
) {
|
||||
let res = await setup.runStep(config, setup.actions.FILTER.stepId, {
|
||||
field,
|
||||
condition,
|
||||
value,
|
||||
})
|
||||
expect(res.result).toEqual(pass)
|
||||
expect(res.success).toEqual(true)
|
||||
}
|
||||
|
||||
it("should be able test equality", async () => {
|
||||
await checkFilter("hello", FilterConditions.EQUAL, "hello", true)
|
||||
await checkFilter("hello", FilterConditions.EQUAL, "no", false)
|
||||
})
|
||||
|
||||
it("should be able to test greater than", async () => {
|
||||
await checkFilter(10, FilterConditions.GREATER_THAN, 5, true)
|
||||
await checkFilter(10, FilterConditions.GREATER_THAN, 15, false)
|
||||
})
|
||||
|
||||
it("should be able to test less than", async () => {
|
||||
await checkFilter(5, FilterConditions.LESS_THAN, 10, true)
|
||||
await checkFilter(15, FilterConditions.LESS_THAN, 10, false)
|
||||
})
|
||||
|
||||
it("should be able to in-equality", async () => {
|
||||
await checkFilter("hello", FilterConditions.NOT_EQUAL, "no", true)
|
||||
await checkFilter(10, FilterConditions.NOT_EQUAL, 10, false)
|
||||
})
|
||||
|
||||
it("check number coercion", async () => {
|
||||
await checkFilter("10", FilterConditions.GREATER_THAN, "5", true)
|
||||
})
|
||||
|
||||
it("check date coercion", async () => {
|
||||
await checkFilter(
|
||||
new Date().toISOString(),
|
||||
FilterConditions.GREATER_THAN,
|
||||
new Date(-10000).toISOString(),
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("check objects always false", async () => {
|
||||
await checkFilter({}, FilterConditions.EQUAL, {}, false)
|
||||
})
|
||||
})
|
|
@ -1,153 +0,0 @@
|
|||
import * as automation from "../index"
|
||||
import * as triggers from "../triggers"
|
||||
import { loopAutomation } from "../../tests/utilities/structures"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import * as setup from "./utilities"
|
||||
import { Table, LoopStepType, AutomationResults } from "@budibase/types"
|
||||
import * as loopUtils from "../loopUtils"
|
||||
import { LoopInput } from "../../definitions/automations"
|
||||
|
||||
describe("Attempt to run a basic loop automation", () => {
|
||||
let config = setup.getConfig(),
|
||||
table: Table
|
||||
|
||||
beforeEach(async () => {
|
||||
await automation.init()
|
||||
await config.init()
|
||||
table = await config.createTable()
|
||||
await config.createRow()
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
|
||||
async function runLoop(loopOpts?: LoopInput): Promise<AutomationResults> {
|
||||
const appId = config.getAppId()
|
||||
return await context.doInAppContext(appId, async () => {
|
||||
const params = { fields: { appId } }
|
||||
const result = await triggers.externalTrigger(
|
||||
loopAutomation(table._id!, loopOpts),
|
||||
params,
|
||||
{ getResponses: true }
|
||||
)
|
||||
if ("outputs" in result && !result.outputs.success) {
|
||||
throw new Error("Unable to proceed - failed to return anything.")
|
||||
}
|
||||
return result as AutomationResults
|
||||
})
|
||||
}
|
||||
|
||||
it("attempt to run a basic loop", async () => {
|
||||
const resp = await runLoop()
|
||||
expect(resp.steps[2].outputs.iterations).toBe(1)
|
||||
})
|
||||
|
||||
it("test a loop with a string", async () => {
|
||||
const resp = await runLoop({
|
||||
option: LoopStepType.STRING,
|
||||
binding: "a,b,c",
|
||||
})
|
||||
expect(resp.steps[2].outputs.iterations).toBe(3)
|
||||
})
|
||||
|
||||
it("test a loop with a binding that returns an integer", async () => {
|
||||
const resp = await runLoop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: "{{ 1 }}",
|
||||
})
|
||||
expect(resp.steps[2].outputs.iterations).toBe(1)
|
||||
})
|
||||
|
||||
describe("replaceFakeBindings", () => {
|
||||
it("should replace loop bindings in nested objects", () => {
|
||||
const originalStepInput = {
|
||||
schema: {
|
||||
name: {
|
||||
type: "string",
|
||||
constraints: {
|
||||
type: "string",
|
||||
length: { maximum: null },
|
||||
presence: false,
|
||||
},
|
||||
name: "name",
|
||||
display: { type: "Text" },
|
||||
},
|
||||
},
|
||||
row: {
|
||||
tableId: "ta_aaad4296e9f74b12b1b90ef7a84afcad",
|
||||
name: "{{ loop.currentItem.pokemon }}",
|
||||
},
|
||||
}
|
||||
|
||||
const loopStepNumber = 3
|
||||
|
||||
const result = loopUtils.replaceFakeBindings(
|
||||
originalStepInput,
|
||||
loopStepNumber
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
schema: {
|
||||
name: {
|
||||
type: "string",
|
||||
constraints: {
|
||||
type: "string",
|
||||
length: { maximum: null },
|
||||
presence: false,
|
||||
},
|
||||
name: "name",
|
||||
display: { type: "Text" },
|
||||
},
|
||||
},
|
||||
row: {
|
||||
tableId: "ta_aaad4296e9f74b12b1b90ef7a84afcad",
|
||||
name: "{{ steps.3.currentItem.pokemon }}",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle null values in nested objects", () => {
|
||||
const originalStepInput = {
|
||||
nullValue: null,
|
||||
nestedNull: {
|
||||
someKey: null,
|
||||
},
|
||||
validValue: "{{ loop.someValue }}",
|
||||
}
|
||||
|
||||
const loopStepNumber = 2
|
||||
|
||||
const result = loopUtils.replaceFakeBindings(
|
||||
originalStepInput,
|
||||
loopStepNumber
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
nullValue: null,
|
||||
nestedNull: {
|
||||
someKey: null,
|
||||
},
|
||||
validValue: "{{ steps.2.someValue }}",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle empty objects and arrays", () => {
|
||||
const originalStepInput = {
|
||||
emptyObject: {},
|
||||
emptyArray: [],
|
||||
nestedEmpty: {
|
||||
emptyObj: {},
|
||||
emptyArr: [],
|
||||
},
|
||||
}
|
||||
|
||||
const loopStepNumber = 1
|
||||
|
||||
const result = loopUtils.replaceFakeBindings(
|
||||
originalStepInput,
|
||||
loopStepNumber
|
||||
)
|
||||
|
||||
expect(result).toEqual(originalStepInput)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,58 +0,0 @@
|
|||
import { getConfig, afterAll, runStep, actions } from "./utilities"
|
||||
import nock from "nock"
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
let config = getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll()
|
||||
|
||||
beforeEach(() => {
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
|
||||
const res = await runStep(config, actions.integromat.stepId, {
|
||||
url: "http://www.example.com",
|
||||
})
|
||||
expect(res.response.foo).toEqual("bar")
|
||||
expect(res.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should add the payload props when a JSON string is provided", async () => {
|
||||
const payload = {
|
||||
value1: 1,
|
||||
value2: 2,
|
||||
value3: 3,
|
||||
value4: 4,
|
||||
value5: 5,
|
||||
name: "Adam",
|
||||
age: 9,
|
||||
}
|
||||
|
||||
nock("http://www.example.com/")
|
||||
.post("/", payload)
|
||||
.reply(200, { foo: "bar" })
|
||||
|
||||
const res = await runStep(config, actions.integromat.stepId, {
|
||||
body: { value: JSON.stringify(payload) },
|
||||
url: "http://www.example.com",
|
||||
})
|
||||
expect(res.response.foo).toEqual("bar")
|
||||
expect(res.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should return a 400 if the JSON payload string is malformed", async () => {
|
||||
const res = await runStep(config, actions.integromat.stepId, {
|
||||
body: { value: "{ invalid json }" },
|
||||
url: "http://www.example.com",
|
||||
})
|
||||
expect(res.httpStatus).toEqual(400)
|
||||
expect(res.response).toEqual("Invalid payload JSON")
|
||||
expect(res.success).toEqual(false)
|
||||
})
|
||||
})
|
|
@ -1,71 +0,0 @@
|
|||
import { getConfig, afterAll, runStep, actions } from "./utilities"
|
||||
import nock from "nock"
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
let config = getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll()
|
||||
|
||||
beforeEach(() => {
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it("should be able to run the action and default to 'get'", async () => {
|
||||
nock("http://www.example.com/").get("/").reply(200, { foo: "bar" })
|
||||
const res = await runStep(config, actions.n8n.stepId, {
|
||||
url: "http://www.example.com",
|
||||
body: {
|
||||
test: "IGNORE_ME",
|
||||
},
|
||||
})
|
||||
expect(res.response.foo).toEqual("bar")
|
||||
expect(res.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should add the payload props when a JSON string is provided", async () => {
|
||||
nock("http://www.example.com/")
|
||||
.post("/", { name: "Adam", age: 9 })
|
||||
.reply(200)
|
||||
const res = await runStep(config, actions.n8n.stepId, {
|
||||
body: {
|
||||
value: JSON.stringify({ name: "Adam", age: 9 }),
|
||||
},
|
||||
method: "POST",
|
||||
url: "http://www.example.com",
|
||||
})
|
||||
expect(res.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should return a 400 if the JSON payload string is malformed", async () => {
|
||||
const payload = `{ value1 1 }`
|
||||
const res = await runStep(config, actions.n8n.stepId, {
|
||||
value1: "ONE",
|
||||
body: {
|
||||
value: payload,
|
||||
},
|
||||
method: "POST",
|
||||
url: "http://www.example.com",
|
||||
})
|
||||
expect(res.httpStatus).toEqual(400)
|
||||
expect(res.response).toEqual("Invalid payload JSON")
|
||||
expect(res.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should not append the body if the method is HEAD", async () => {
|
||||
nock("http://www.example.com/")
|
||||
.head("/", body => body === "")
|
||||
.reply(200)
|
||||
const res = await runStep(config, actions.n8n.stepId, {
|
||||
url: "http://www.example.com",
|
||||
method: "HEAD",
|
||||
body: {
|
||||
test: "IGNORE_ME",
|
||||
},
|
||||
})
|
||||
expect(res.success).toEqual(true)
|
||||
})
|
||||
})
|
|
@ -1,165 +0,0 @@
|
|||
import { getConfig, afterAll as _afterAll } from "./utilities"
|
||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
import { OpenAI } from "openai"
|
||||
import { setEnv as setCoreEnv } from "@budibase/backend-core"
|
||||
import * as pro from "@budibase/pro"
|
||||
import { Model } from "@budibase/types"
|
||||
|
||||
jest.mock("openai", () => ({
|
||||
OpenAI: jest.fn().mockImplementation(() => ({
|
||||
chat: {
|
||||
completions: {
|
||||
create: jest.fn(() => ({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: "This is a test",
|
||||
},
|
||||
},
|
||||
],
|
||||
})),
|
||||
},
|
||||
},
|
||||
})),
|
||||
}))
|
||||
jest.mock("@budibase/pro", () => ({
|
||||
...jest.requireActual("@budibase/pro"),
|
||||
ai: {
|
||||
LargeLanguageModel: {
|
||||
forCurrentTenant: jest.fn().mockImplementation(() => ({
|
||||
llm: {},
|
||||
init: jest.fn(),
|
||||
run: jest.fn(),
|
||||
})),
|
||||
},
|
||||
},
|
||||
features: {
|
||||
isAICustomConfigsEnabled: jest.fn(),
|
||||
isBudibaseAIEnabled: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const mockedPro = jest.mocked(pro)
|
||||
const mockedOpenAI = OpenAI as jest.MockedClass<typeof OpenAI>
|
||||
|
||||
const OPENAI_PROMPT = "What is the meaning of life?"
|
||||
|
||||
describe("test the openai action", () => {
|
||||
let config = getConfig()
|
||||
let resetEnv: () => void | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
setCoreEnv({ SELF_HOSTED: true })
|
||||
await config.init()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
resetEnv = setCoreEnv({ OPENAI_API_KEY: "abc123" })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
resetEnv()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(_afterAll)
|
||||
|
||||
it("should be able to receive a response from ChatGPT given a prompt", async () => {
|
||||
setCoreEnv({ SELF_HOSTED: true })
|
||||
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test OpenAI Response",
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
.openai(
|
||||
{ prompt: OPENAI_PROMPT, model: Model.GPT_4O_MINI },
|
||||
{ stepName: "Basic OpenAI Query" }
|
||||
)
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual("This is a test")
|
||||
expect(result.steps[0].outputs.success).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should present the correct error message when a prompt is not provided", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test OpenAI No Prompt",
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
.openai(
|
||||
{ prompt: "", model: Model.GPT_4O_MINI },
|
||||
{ stepName: "Empty Prompt Query" }
|
||||
)
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual(
|
||||
"Budibase OpenAI Automation Failed: No prompt supplied"
|
||||
)
|
||||
expect(result.steps[0].outputs.success).toBeFalsy()
|
||||
})
|
||||
|
||||
it("should present the correct error message when an error is thrown from the createChatCompletion call", async () => {
|
||||
mockedOpenAI.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
chat: {
|
||||
completions: {
|
||||
create: jest.fn(() => {
|
||||
throw new Error(
|
||||
"An error occurred while calling createChatCompletion"
|
||||
)
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as any)
|
||||
)
|
||||
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test OpenAI Error",
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
.openai(
|
||||
{ prompt: OPENAI_PROMPT, model: Model.GPT_4O_MINI },
|
||||
{ stepName: "Error Producing Query" }
|
||||
)
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual(
|
||||
"Error: An error occurred while calling createChatCompletion"
|
||||
)
|
||||
expect(result.steps[0].outputs.success).toBeFalsy()
|
||||
})
|
||||
|
||||
it("should ensure that the pro AI module is called when the budibase AI features are enabled", async () => {
|
||||
jest.spyOn(pro.features, "isBudibaseAIEnabled").mockResolvedValue(true)
|
||||
jest.spyOn(pro.features, "isAICustomConfigsEnabled").mockResolvedValue(true)
|
||||
|
||||
const prompt = "What is the meaning of life?"
|
||||
await createAutomationBuilder({
|
||||
name: "Test OpenAI Pro Features",
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
.openai(
|
||||
{
|
||||
model: Model.GPT_4O_MINI,
|
||||
prompt,
|
||||
},
|
||||
{ stepName: "Pro Features Query" }
|
||||
)
|
||||
.run()
|
||||
|
||||
expect(pro.ai.LargeLanguageModel.forCurrentTenant).toHaveBeenCalledWith(
|
||||
"gpt-4o-mini"
|
||||
)
|
||||
|
||||
const llmInstance =
|
||||
mockedPro.ai.LargeLanguageModel.forCurrentTenant.mock.results[0].value
|
||||
// init does not appear to be called currently
|
||||
// expect(llmInstance.init).toHaveBeenCalled()
|
||||
expect(llmInstance.run).toHaveBeenCalledWith(prompt)
|
||||
})
|
||||
})
|
|
@ -1,37 +0,0 @@
|
|||
import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities"
|
||||
import nock from "nock"
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
const config = getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(_afterAll)
|
||||
|
||||
beforeEach(() => {
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
nock("http://www.example.com")
|
||||
.post("/", { a: 1 })
|
||||
.reply(200, { foo: "bar" })
|
||||
const res = await runStep(config, actions.OUTGOING_WEBHOOK.stepId, {
|
||||
requestMethod: "POST",
|
||||
url: "www.example.com",
|
||||
requestBody: JSON.stringify({ a: 1 }),
|
||||
})
|
||||
expect(res.success).toEqual(true)
|
||||
expect(res.response.foo).toEqual("bar")
|
||||
})
|
||||
|
||||
it("should return an error if something goes wrong in fetch", async () => {
|
||||
const res = await runStep(config, actions.OUTGOING_WEBHOOK.stepId, {
|
||||
requestMethod: "GET",
|
||||
url: "www.invalid.com",
|
||||
})
|
||||
expect(res.success).toEqual(false)
|
||||
})
|
||||
})
|
|
@ -1,36 +1,35 @@
|
|||
import * as automation from "../../index"
|
||||
import * as setup from "../utilities"
|
||||
import * as automation from "../index"
|
||||
import { LoopStepType, FieldType, Table, Datasource } from "@budibase/types"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
import {
|
||||
DatabaseName,
|
||||
datasourceDescribe,
|
||||
} from "../../../integrations/tests/utils"
|
||||
} from "../../integrations/tests/utils"
|
||||
import { Knex } from "knex"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
import { automations } from "@budibase/shared-core"
|
||||
import TestConfiguration from "../../tests/utilities/TestConfiguration"
|
||||
import { basicTable } from "../../tests/utilities/structures"
|
||||
|
||||
const FilterConditions = automations.steps.filter.FilterConditions
|
||||
|
||||
describe("Automation Scenarios", () => {
|
||||
let config = setup.getConfig()
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeEach(async () => {
|
||||
await automation.init()
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
describe("Row Automations", () => {
|
||||
it("should trigger an automation which then creates a row", async () => {
|
||||
const table = await config.createTable()
|
||||
const table = await config.api.table.save(basicTable())
|
||||
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Row Save and Create",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.rowUpdated(
|
||||
{ tableId: table._id! },
|
||||
{
|
||||
|
@ -58,21 +57,15 @@ describe("Automation Scenarios", () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should trigger an automation which querys the database", async () => {
|
||||
const table = await config.createTable()
|
||||
it("should trigger an automation which queries the database", async () => {
|
||||
const table = await config.api.table.save(basicTable())
|
||||
const row = {
|
||||
name: "Test Row",
|
||||
description: "original description",
|
||||
tableId: table._id,
|
||||
}
|
||||
await config.createRow(row)
|
||||
await config.createRow(row)
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Row Save and Create",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
await config.api.row.save(table._id!, row)
|
||||
await config.api.row.save(table._id!, row)
|
||||
const results = await createAutomationBuilder(config)
|
||||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
|
@ -82,21 +75,15 @@ describe("Automation Scenarios", () => {
|
|||
expect(results.steps[0].outputs.rows).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("should trigger an automation which querys the database then deletes a row", async () => {
|
||||
const table = await config.createTable()
|
||||
it("should trigger an automation which queries the database then deletes a row", async () => {
|
||||
const table = await config.api.table.save(basicTable())
|
||||
const row = {
|
||||
name: "DFN",
|
||||
description: "original description",
|
||||
tableId: table._id,
|
||||
}
|
||||
await config.createRow(row)
|
||||
await config.createRow(row)
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Row Save and Create",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
await config.api.row.save(table._id!, row)
|
||||
await config.api.row.save(table._id!, row)
|
||||
const results = await createAutomationBuilder(config)
|
||||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
|
@ -115,7 +102,8 @@ describe("Automation Scenarios", () => {
|
|||
})
|
||||
|
||||
it("should trigger an automation which creates and then updates a row", async () => {
|
||||
const table = await config.createTable({
|
||||
const table = await config.api.table.save({
|
||||
...basicTable(),
|
||||
name: "TestTable",
|
||||
type: "table",
|
||||
schema: {
|
||||
|
@ -136,12 +124,7 @@ describe("Automation Scenarios", () => {
|
|||
},
|
||||
})
|
||||
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Create and Update Row",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.createRow(
|
||||
{
|
||||
row: {
|
||||
|
@ -202,20 +185,14 @@ describe("Automation Scenarios", () => {
|
|||
|
||||
describe("Name Based Automations", () => {
|
||||
it("should fetch and delete a rpw using automation naming", async () => {
|
||||
const table = await config.createTable()
|
||||
const table = await config.api.table.save(basicTable())
|
||||
const row = {
|
||||
name: "DFN",
|
||||
description: "original description",
|
||||
tableId: table._id,
|
||||
}
|
||||
await config.createRow(row)
|
||||
await config.createRow(row)
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Query and Delete Row",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
await config.api.row.save(table._id!, row)
|
||||
await config.api.row.save(table._id!, row)
|
||||
const results = await createAutomationBuilder(config)
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -240,7 +217,8 @@ describe("Automation Scenarios", () => {
|
|||
let table: Table
|
||||
|
||||
beforeEach(async () => {
|
||||
table = await config.createTable({
|
||||
table = await config.api.table.save({
|
||||
...basicTable(),
|
||||
name: "TestTable",
|
||||
type: "table",
|
||||
schema: {
|
||||
|
@ -263,12 +241,7 @@ describe("Automation Scenarios", () => {
|
|||
})
|
||||
|
||||
it("should stop an automation if the condition is not met", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Equal",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.createRow({
|
||||
row: {
|
||||
name: "Equal Test",
|
||||
|
@ -293,12 +266,7 @@ describe("Automation Scenarios", () => {
|
|||
})
|
||||
|
||||
it("should continue the automation if the condition is met", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Not Equal",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.createRow({
|
||||
row: {
|
||||
name: "Not Equal Test",
|
||||
|
@ -364,12 +332,7 @@ describe("Automation Scenarios", () => {
|
|||
it.each(testCases)(
|
||||
"should pass the filter when condition is $condition",
|
||||
async ({ condition, value, rowValue, expectPass }) => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: `Test ${condition}`,
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.createRow({
|
||||
row: {
|
||||
name: `${condition} Test`,
|
||||
|
@ -401,13 +364,9 @@ describe("Automation Scenarios", () => {
|
|||
})
|
||||
|
||||
it("Check user is passed through from row trigger", async () => {
|
||||
const table = await config.createTable()
|
||||
const table = await config.api.table.save(basicTable())
|
||||
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test a user is successfully passed from the trigger",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.rowUpdated(
|
||||
{ tableId: table._id! },
|
||||
{
|
||||
|
@ -422,12 +381,7 @@ describe("Automation Scenarios", () => {
|
|||
})
|
||||
|
||||
it("Check user is passed through from app trigger", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test a user is successfully passed from the trigger",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.serverLog({ text: "{{ [user].[email] }}" })
|
||||
.run()
|
||||
|
||||
|
@ -449,7 +403,8 @@ if (descriptions.length) {
|
|||
})
|
||||
|
||||
it("should query an external database for some data then insert than into an internal table", async () => {
|
||||
const newTable = await config.createTable({
|
||||
const newTable = await config.api.table.save({
|
||||
...basicTable(),
|
||||
name: "table",
|
||||
type: "table",
|
||||
schema: {
|
||||
|
@ -484,19 +439,20 @@ if (descriptions.length) {
|
|||
|
||||
await client(tableName).insert(rows)
|
||||
|
||||
const query = await setup.saveTestQuery(
|
||||
config,
|
||||
client,
|
||||
tableName,
|
||||
datasource
|
||||
)
|
||||
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test external query and save",
|
||||
config,
|
||||
const query = await config.api.query.save({
|
||||
name: "test query",
|
||||
datasourceId: datasource._id!,
|
||||
parameters: [],
|
||||
fields: {
|
||||
sql: client(tableName).select("*").toSQL().toNative().sql,
|
||||
},
|
||||
transformer: "",
|
||||
schema: {},
|
||||
readable: true,
|
||||
queryVerb: "read",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({
|
||||
fields: {},
|
||||
})
|
|
@ -1,20 +0,0 @@
|
|||
import { getConfig, afterAll as _afterAll, runStep, actions } from "./utilities"
|
||||
|
||||
describe("test the server log action", () => {
|
||||
let config = getConfig()
|
||||
let inputs: any
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
inputs = {
|
||||
text: "log message",
|
||||
}
|
||||
})
|
||||
afterAll(_afterAll)
|
||||
|
||||
it("should be able to log the text", async () => {
|
||||
let res = await runStep(config, actions.SERVER_LOG.stepId, inputs)
|
||||
expect(res.message).toEqual(`App ${config.getAppId()} - ${inputs.text}`)
|
||||
expect(res.success).toEqual(true)
|
||||
})
|
||||
})
|
|
@ -1,30 +1,30 @@
|
|||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
import * as automation from "../index"
|
||||
import * as setup from "./utilities"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import * as automation from "../../index"
|
||||
import { Table } from "@budibase/types"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import { basicTable } from "../../../tests/utilities/structures"
|
||||
|
||||
describe("Execute Bash Automations", () => {
|
||||
let config = setup.getConfig(),
|
||||
table: Table
|
||||
const config = new TestConfiguration()
|
||||
let table: Table
|
||||
|
||||
beforeAll(async () => {
|
||||
await automation.init()
|
||||
await config.init()
|
||||
table = await config.createTable()
|
||||
await config.createRow({
|
||||
table = await config.api.table.save(basicTable())
|
||||
await config.api.row.save(table._id!, {
|
||||
name: "test row",
|
||||
description: "test description",
|
||||
tableId: table._id!,
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
afterAll(() => {
|
||||
automation.shutdown()
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should use trigger data in bash command and pass output to subsequent steps", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Bash with Trigger Data",
|
||||
config,
|
||||
})
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { command: "hello world" } })
|
||||
.bash(
|
||||
{ code: "echo '{{ trigger.fields.command }}'" },
|
||||
|
@ -43,10 +43,7 @@ describe("Execute Bash Automations", () => {
|
|||
})
|
||||
|
||||
it("should chain multiple bash commands using previous outputs", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Chained Bash Commands",
|
||||
config,
|
||||
})
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { filename: "testfile.txt" } })
|
||||
.bash(
|
||||
{ code: "echo 'initial content' > {{ trigger.fields.filename }}" },
|
||||
|
@ -67,11 +64,7 @@ describe("Execute Bash Automations", () => {
|
|||
})
|
||||
|
||||
it("should integrate bash output with row operations", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Bash with Row Operations",
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -100,10 +93,7 @@ describe("Execute Bash Automations", () => {
|
|||
})
|
||||
|
||||
it("should handle bash output in conditional logic", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Bash with Conditional",
|
||||
config,
|
||||
})
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { threshold: "5" } })
|
||||
.bash(
|
||||
{ code: "echo $(( {{ trigger.fields.threshold }} + 5 ))" },
|
||||
|
@ -130,13 +120,9 @@ describe("Execute Bash Automations", () => {
|
|||
})
|
||||
|
||||
it("should handle null values gracefully", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Null Bash Input",
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.bash(
|
||||
//@ts-ignore
|
||||
// @ts-expect-error - testing null input
|
||||
{ code: null },
|
||||
{ stepName: "Null Command" }
|
||||
)
|
|
@ -1,7 +1,11 @@
|
|||
import * as setup from "./utilities"
|
||||
import { basicTableWithAttachmentField } from "../../tests/utilities/structures"
|
||||
import {
|
||||
basicTable,
|
||||
basicTableWithAttachmentField,
|
||||
} from "../../../tests/utilities/structures"
|
||||
import { objectStore } from "@budibase/backend-core"
|
||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import { Row, Table } from "@budibase/types"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
|
||||
async function uploadTestFile(filename: string) {
|
||||
let bucket = "testbucket"
|
||||
|
@ -10,19 +14,20 @@ async function uploadTestFile(filename: string) {
|
|||
filename,
|
||||
body: Buffer.from("test data"),
|
||||
})
|
||||
let presignedUrl = await objectStore.getPresignedUrl(bucket, filename, 60000)
|
||||
let presignedUrl = objectStore.getPresignedUrl(bucket, filename, 60000)
|
||||
|
||||
return presignedUrl
|
||||
}
|
||||
|
||||
describe("test the create row action", () => {
|
||||
let table: any
|
||||
let row: any
|
||||
let config = setup.getConfig()
|
||||
const config = new TestConfiguration()
|
||||
|
||||
let table: Table
|
||||
let row: Row
|
||||
|
||||
beforeEach(async () => {
|
||||
await config.init()
|
||||
table = await config.createTable()
|
||||
table = await config.api.table.save(basicTable())
|
||||
row = {
|
||||
tableId: table._id,
|
||||
name: "test",
|
||||
|
@ -30,14 +35,12 @@ describe("test the create row action", () => {
|
|||
}
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Create Row Flow",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "new" } })
|
||||
.serverLog({ text: "Starting create row flow" }, { stepName: "StartLog" })
|
||||
.createRow({ row }, { stepName: "CreateRow" })
|
||||
|
@ -50,8 +53,9 @@ describe("test the create row action", () => {
|
|||
expect(result.steps[1].outputs.success).toBeDefined()
|
||||
expect(result.steps[1].outputs.id).toBeDefined()
|
||||
expect(result.steps[1].outputs.revision).toBeDefined()
|
||||
|
||||
const gottenRow = await config.api.row.get(
|
||||
table._id,
|
||||
table._id!,
|
||||
result.steps[1].outputs.id
|
||||
)
|
||||
expect(gottenRow.name).toEqual("test")
|
||||
|
@ -62,11 +66,7 @@ describe("test the create row action", () => {
|
|||
})
|
||||
|
||||
it("should return an error (not throw) when bad info provided", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Create Row Error Flow",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "error" } })
|
||||
.serverLog({ text: "Starting error test flow" }, { stepName: "StartLog" })
|
||||
.createRow(
|
||||
|
@ -84,11 +84,7 @@ describe("test the create row action", () => {
|
|||
})
|
||||
|
||||
it("should check invalid inputs return an error", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Create Row Invalid Flow",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { status: "invalid" } })
|
||||
.serverLog({ text: "Testing invalid input" }, { stepName: "StartLog" })
|
||||
.createRow({ row: {} }, { stepName: "CreateRow" })
|
||||
|
@ -108,11 +104,11 @@ describe("test the create row action", () => {
|
|||
})
|
||||
|
||||
it("should check that an attachment field is sent to storage and parsed", async () => {
|
||||
let attachmentTable = await config.createTable(
|
||||
let attachmentTable = await config.api.table.save(
|
||||
basicTableWithAttachmentField()
|
||||
)
|
||||
|
||||
let attachmentRow: any = {
|
||||
let attachmentRow: Row = {
|
||||
tableId: attachmentTable._id,
|
||||
}
|
||||
|
||||
|
@ -126,11 +122,7 @@ describe("test the create row action", () => {
|
|||
]
|
||||
|
||||
attachmentRow.file_attachment = attachmentObject
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Create Row Attachment Flow",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { type: "attachment" } })
|
||||
.serverLog(
|
||||
{ text: "Processing attachment upload" },
|
||||
|
@ -165,11 +157,11 @@ describe("test the create row action", () => {
|
|||
})
|
||||
|
||||
it("should check that an single attachment field is sent to storage and parsed", async () => {
|
||||
let attachmentTable = await config.createTable(
|
||||
let attachmentTable = await config.api.table.save(
|
||||
basicTableWithAttachmentField()
|
||||
)
|
||||
|
||||
let attachmentRow: any = {
|
||||
let attachmentRow: Row = {
|
||||
tableId: attachmentTable._id,
|
||||
}
|
||||
|
||||
|
@ -181,11 +173,7 @@ describe("test the create row action", () => {
|
|||
}
|
||||
|
||||
attachmentRow.single_file_attachment = attachmentObject
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Create Row Single Attachment Flow",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { type: "single-attachment" } })
|
||||
.serverLog(
|
||||
{ text: "Processing single attachment" },
|
||||
|
@ -240,11 +228,11 @@ describe("test the create row action", () => {
|
|||
})
|
||||
|
||||
it("should check that attachment without the correct keys throws an error", async () => {
|
||||
let attachmentTable = await config.createTable(
|
||||
let attachmentTable = await config.api.table.save(
|
||||
basicTableWithAttachmentField()
|
||||
)
|
||||
|
||||
let attachmentRow: any = {
|
||||
let attachmentRow: Row = {
|
||||
tableId: attachmentTable._id,
|
||||
}
|
||||
|
||||
|
@ -256,11 +244,7 @@ describe("test the create row action", () => {
|
|||
}
|
||||
|
||||
attachmentRow.single_file_attachment = attachmentObject
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Test Create Row Invalid Attachment Flow",
|
||||
appId: config.getAppId(),
|
||||
config,
|
||||
})
|
||||
const result = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { type: "invalid-attachment" } })
|
||||
.serverLog(
|
||||
{ text: "Testing invalid attachment keys" },
|
|
@ -1,8 +1,8 @@
|
|||
import tk from "timekeeper"
|
||||
import "../../environment"
|
||||
import * as automations from "../index"
|
||||
import * as setup from "./utilities"
|
||||
import { basicCronAutomation } from "../../tests/utilities/structures"
|
||||
import "../../../environment"
|
||||
import * as automations from "../../index"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
|
||||
const initialTime = Date.now()
|
||||
tk.freeze(initialTime)
|
||||
|
@ -10,7 +10,7 @@ tk.freeze(initialTime)
|
|||
const oneMinuteInMs = 60 * 1000
|
||||
|
||||
describe("cron automations", () => {
|
||||
let config = setup.getConfig()
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await automations.init()
|
||||
|
@ -19,26 +19,22 @@ describe("cron automations", () => {
|
|||
|
||||
afterAll(async () => {
|
||||
await automations.shutdown()
|
||||
setup.afterAll()
|
||||
config.end()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
tk.freeze(initialTime)
|
||||
})
|
||||
|
||||
async function travel(ms: number) {
|
||||
tk.travel(Date.now() + ms)
|
||||
}
|
||||
|
||||
it("should initialise the automation timestamp", async () => {
|
||||
const automation = basicCronAutomation(config.appId!, "* * * * *")
|
||||
await config.api.automation.post(automation)
|
||||
await travel(oneMinuteInMs)
|
||||
await createAutomationBuilder(config).cron({ cron: "* * * * *" }).save()
|
||||
|
||||
tk.travel(Date.now() + oneMinuteInMs)
|
||||
await config.publish()
|
||||
|
||||
const automationLogs = await config.getAutomationLogs()
|
||||
expect(automationLogs.data).toHaveLength(1)
|
||||
expect(automationLogs.data).toEqual([
|
||||
const { data } = await config.getAutomationLogs()
|
||||
expect(data).toHaveLength(1)
|
||||
expect(data).toEqual([
|
||||
expect.objectContaining({
|
||||
trigger: expect.objectContaining({
|
||||
outputs: { timestamp: initialTime + oneMinuteInMs },
|
|
@ -0,0 +1,26 @@
|
|||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
|
||||
describe("test the delay logic", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should be able to run the delay", async () => {
|
||||
const time = 100
|
||||
const before = performance.now()
|
||||
|
||||
await createAutomationBuilder(config).delay({ time }).run()
|
||||
|
||||
const now = performance.now()
|
||||
|
||||
// divide by two just so that test will always pass as long as there was some sort of delay
|
||||
expect(now - before).toBeGreaterThanOrEqual(time / 2)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,55 @@
|
|||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import { Row, Table } from "@budibase/types"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import { basicTable } from "../../../tests/utilities/structures"
|
||||
|
||||
describe("test the delete row action", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
let table: Table
|
||||
let row: Row
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
table = await config.api.table.save(basicTable())
|
||||
row = await config.api.row.save(table._id!, {})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should be able to run the delete row action", async () => {
|
||||
await createAutomationBuilder(config)
|
||||
.deleteRow({
|
||||
tableId: table._id!,
|
||||
id: row._id!,
|
||||
revision: row._rev,
|
||||
})
|
||||
.run()
|
||||
|
||||
await config.api.row.get(table._id!, row._id!, {
|
||||
status: 404,
|
||||
})
|
||||
})
|
||||
|
||||
it("should check invalid inputs return an error", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.deleteRow({ tableId: "", id: "", revision: "" })
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should return an error when table doesn't exist", async () => {
|
||||
const results = await createAutomationBuilder(config)
|
||||
.deleteRow({
|
||||
tableId: "invalid",
|
||||
id: "invalid",
|
||||
revision: "invalid",
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(results.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,32 @@
|
|||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import nock from "nock"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.discord({
|
||||
url: "http://www.example.com",
|
||||
username: "joe_bloggs",
|
||||
content: "Hello, world",
|
||||
})
|
||||
.run()
|
||||
expect(result.steps[0].outputs.response.foo).toEqual("bar")
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
})
|
|
@ -1,9 +1,9 @@
|
|||
import { Datasource, Query } from "@budibase/types"
|
||||
import * as setup from "./utilities"
|
||||
import * as setup from "../utilities"
|
||||
import {
|
||||
DatabaseName,
|
||||
datasourceDescribe,
|
||||
} from "../../integrations/tests/utils"
|
||||
} from "../../../integrations/tests/utils"
|
||||
import { Knex } from "knex"
|
||||
import { generator } from "@budibase/backend-core/tests"
|
||||
|
|
@ -1,28 +1,26 @@
|
|||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
import * as automation from "../index"
|
||||
import * as setup from "./utilities"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import * as automation from "../../index"
|
||||
import { Table } from "@budibase/types"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import { basicTable } from "../../../tests/utilities/structures"
|
||||
|
||||
describe("Execute Script Automations", () => {
|
||||
let config = setup.getConfig(),
|
||||
table: Table
|
||||
const config = new TestConfiguration()
|
||||
let table: Table
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeAll(async () => {
|
||||
await automation.init()
|
||||
await config.init()
|
||||
table = await config.createTable()
|
||||
await config.createRow()
|
||||
table = await config.api.table.save(basicTable())
|
||||
await config.api.row.save(table._id!, {})
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should execute a basic script and return the result", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Basic Script Execution",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.executeScript({ code: "return 2 + 2" })
|
||||
.run()
|
||||
|
||||
|
@ -30,11 +28,7 @@ describe("Execute Script Automations", () => {
|
|||
})
|
||||
|
||||
it("should access bindings from previous steps", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Access Bindings",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { data: [1, 2, 3] } })
|
||||
.executeScript(
|
||||
{
|
||||
|
@ -48,12 +42,7 @@ describe("Execute Script Automations", () => {
|
|||
})
|
||||
|
||||
it("should handle script execution errors gracefully", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Handle Script Errors",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.executeScript({ code: "return nonexistentVariable.map(x => x)" })
|
||||
.run()
|
||||
|
||||
|
@ -64,11 +53,7 @@ describe("Execute Script Automations", () => {
|
|||
})
|
||||
|
||||
it("should handle conditional logic in scripts", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Conditional Script Logic",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: { value: 10 } })
|
||||
.executeScript({
|
||||
code: `
|
||||
|
@ -85,11 +70,7 @@ describe("Execute Script Automations", () => {
|
|||
})
|
||||
|
||||
it("should use multiple steps and validate script execution", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Multi-Step Script Execution",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.appAction({ fields: {} })
|
||||
.serverLog(
|
||||
{ text: "Starting multi-step automation" },
|
|
@ -0,0 +1,69 @@
|
|||
import { automations } from "@budibase/shared-core"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
|
||||
const FilterConditions = automations.steps.filter.FilterConditions
|
||||
|
||||
function stringToFilterCondition(condition: "==" | "!=" | ">" | "<"): string {
|
||||
switch (condition) {
|
||||
case "==":
|
||||
return FilterConditions.EQUAL
|
||||
case "!=":
|
||||
return FilterConditions.NOT_EQUAL
|
||||
case ">":
|
||||
return FilterConditions.GREATER_THAN
|
||||
case "<":
|
||||
return FilterConditions.LESS_THAN
|
||||
}
|
||||
}
|
||||
|
||||
type TestCase = [any, "==" | "!=" | ">" | "<", any]
|
||||
|
||||
describe("test the filter logic", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
const pass: TestCase[] = [
|
||||
[10, ">", 5],
|
||||
["10", ">", 5],
|
||||
[10, ">", "5"],
|
||||
["10", ">", "5"],
|
||||
[10, "==", 10],
|
||||
[10, "<", 15],
|
||||
["hello", "==", "hello"],
|
||||
["hello", "!=", "no"],
|
||||
[new Date().toISOString(), ">", new Date(-10000).toISOString()],
|
||||
]
|
||||
it.each(pass)("should pass %p %p %p", async (field, condition, value) => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.filter({ field, condition: stringToFilterCondition(condition), value })
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.result).toEqual(true)
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
|
||||
const fail: TestCase[] = [
|
||||
[10, ">", 15],
|
||||
[10, "<", 5],
|
||||
[10, "==", 5],
|
||||
["hello", "==", "no"],
|
||||
["hello", "!=", "hello"],
|
||||
[{}, "==", {}],
|
||||
]
|
||||
it.each(fail)("should fail %p %p %p", async (field, condition, value) => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.filter({ field, condition: stringToFilterCondition(condition), value })
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.result).toEqual(false)
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
})
|
|
@ -1,33 +1,78 @@
|
|||
import * as automation from "../../index"
|
||||
import * as setup from "../utilities"
|
||||
import * as triggers from "../../triggers"
|
||||
import { basicTable, loopAutomation } from "../../../tests/utilities/structures"
|
||||
import { context } from "@budibase/backend-core"
|
||||
import {
|
||||
Table,
|
||||
LoopStepType,
|
||||
CreateRowStepOutputs,
|
||||
AutomationResults,
|
||||
ServerLogStepOutputs,
|
||||
CreateRowStepOutputs,
|
||||
FieldType,
|
||||
} from "@budibase/types"
|
||||
import * as loopUtils from "../../loopUtils"
|
||||
import { LoopInput } from "../../../definitions/automations"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
|
||||
describe("Loop automations", () => {
|
||||
let config = setup.getConfig(),
|
||||
table: Table
|
||||
describe("Attempt to run a basic loop automation", () => {
|
||||
const config = new TestConfiguration()
|
||||
let table: Table
|
||||
|
||||
beforeEach(async () => {
|
||||
await automation.init()
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
table = await config.createTable()
|
||||
await config.createRow()
|
||||
await automation.init()
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
beforeEach(async () => {
|
||||
table = await config.api.table.save(basicTable())
|
||||
await config.api.row.save(table._id!, {})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
automation.shutdown()
|
||||
config.end()
|
||||
})
|
||||
|
||||
async function runLoop(loopOpts?: LoopInput): Promise<AutomationResults> {
|
||||
const appId = config.getAppId()
|
||||
return await context.doInAppContext(appId, async () => {
|
||||
const params = { fields: { appId } }
|
||||
const result = await triggers.externalTrigger(
|
||||
loopAutomation(table._id!, loopOpts),
|
||||
params,
|
||||
{ getResponses: true }
|
||||
)
|
||||
if ("outputs" in result && !result.outputs.success) {
|
||||
throw new Error("Unable to proceed - failed to return anything.")
|
||||
}
|
||||
return result as AutomationResults
|
||||
})
|
||||
}
|
||||
|
||||
it("attempt to run a basic loop", async () => {
|
||||
const resp = await runLoop()
|
||||
expect(resp.steps[2].outputs.iterations).toBe(1)
|
||||
})
|
||||
|
||||
it("test a loop with a string", async () => {
|
||||
const resp = await runLoop({
|
||||
option: LoopStepType.STRING,
|
||||
binding: "a,b,c",
|
||||
})
|
||||
expect(resp.steps[2].outputs.iterations).toBe(3)
|
||||
})
|
||||
|
||||
it("test a loop with a binding that returns an integer", async () => {
|
||||
const resp = await runLoop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: "{{ 1 }}",
|
||||
})
|
||||
expect(resp.steps[2].outputs.iterations).toBe(1)
|
||||
})
|
||||
|
||||
it("should run an automation with a trigger, loop, and create row step", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Trigger with Loop and Create Row",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.rowSaved(
|
||||
{ tableId: table._id! },
|
||||
{
|
||||
|
@ -70,11 +115,7 @@ describe("Loop automations", () => {
|
|||
})
|
||||
|
||||
it("should run an automation where a loop step is between two normal steps to ensure context correctness", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Trigger with Loop and Create Row",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.rowSaved(
|
||||
{ tableId: table._id! },
|
||||
{
|
||||
|
@ -110,12 +151,7 @@ describe("Loop automations", () => {
|
|||
})
|
||||
|
||||
it("if an incorrect type is passed to the loop it should return an error", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Loop error",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: "1, 2, 3",
|
||||
|
@ -130,12 +166,7 @@ describe("Loop automations", () => {
|
|||
})
|
||||
|
||||
it("ensure the loop stops if the failure condition is reached", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Loop error",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: ["test", "test2", "test3"],
|
||||
|
@ -153,12 +184,7 @@ describe("Loop automations", () => {
|
|||
})
|
||||
|
||||
it("ensure the loop stops if the max iterations are reached", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Loop max iterations",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: ["test", "test2", "test3"],
|
||||
|
@ -172,12 +198,7 @@ describe("Loop automations", () => {
|
|||
})
|
||||
|
||||
it("should run an automation with loop and max iterations to ensure context correctness further down the tree", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test context down tree with Loop and max iterations",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: ["test", "test2", "test3"],
|
||||
|
@ -191,11 +212,7 @@ describe("Loop automations", () => {
|
|||
})
|
||||
|
||||
it("should run an automation where a loop is successfully run twice", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Trigger with Loop and Create Row",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
const results = await createAutomationBuilder(config)
|
||||
.rowSaved(
|
||||
{ tableId: table._id! },
|
||||
{
|
||||
|
@ -257,12 +274,7 @@ describe("Loop automations", () => {
|
|||
})
|
||||
|
||||
it("should run an automation where a loop is used twice to ensure context correctness further down the tree", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Trigger with Loop and Create Row",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.loop({
|
||||
option: LoopStepType.ARRAY,
|
||||
binding: [1, 2, 3],
|
||||
|
@ -283,12 +295,7 @@ describe("Loop automations", () => {
|
|||
})
|
||||
|
||||
it("should use automation names to loop with", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Trigger with Loop and Create Row",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.loop(
|
||||
{
|
||||
option: LoopStepType.ARRAY,
|
||||
|
@ -339,12 +346,7 @@ describe("Loop automations", () => {
|
|||
|
||||
await config.api.row.bulkImport(table._id!, { rows })
|
||||
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Loop and Update Row",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
|
@ -423,12 +425,7 @@ describe("Loop automations", () => {
|
|||
|
||||
await config.api.row.bulkImport(table._id!, { rows })
|
||||
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Loop and Update Row",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -510,12 +507,7 @@ describe("Loop automations", () => {
|
|||
|
||||
await config.api.row.bulkImport(table._id!, { rows })
|
||||
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Test Loop and Delete Row",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.queryRows({
|
||||
tableId: table._id!,
|
||||
})
|
||||
|
@ -536,4 +528,98 @@ describe("Loop automations", () => {
|
|||
|
||||
expect(results.steps[2].outputs.rows).toHaveLength(0)
|
||||
})
|
||||
|
||||
describe("replaceFakeBindings", () => {
|
||||
it("should replace loop bindings in nested objects", () => {
|
||||
const originalStepInput = {
|
||||
schema: {
|
||||
name: {
|
||||
type: "string",
|
||||
constraints: {
|
||||
type: "string",
|
||||
length: { maximum: null },
|
||||
presence: false,
|
||||
},
|
||||
name: "name",
|
||||
display: { type: "Text" },
|
||||
},
|
||||
},
|
||||
row: {
|
||||
tableId: "ta_aaad4296e9f74b12b1b90ef7a84afcad",
|
||||
name: "{{ loop.currentItem.pokemon }}",
|
||||
},
|
||||
}
|
||||
|
||||
const loopStepNumber = 3
|
||||
|
||||
const result = loopUtils.replaceFakeBindings(
|
||||
originalStepInput,
|
||||
loopStepNumber
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
schema: {
|
||||
name: {
|
||||
type: "string",
|
||||
constraints: {
|
||||
type: "string",
|
||||
length: { maximum: null },
|
||||
presence: false,
|
||||
},
|
||||
name: "name",
|
||||
display: { type: "Text" },
|
||||
},
|
||||
},
|
||||
row: {
|
||||
tableId: "ta_aaad4296e9f74b12b1b90ef7a84afcad",
|
||||
name: "{{ steps.3.currentItem.pokemon }}",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle null values in nested objects", () => {
|
||||
const originalStepInput = {
|
||||
nullValue: null,
|
||||
nestedNull: {
|
||||
someKey: null,
|
||||
},
|
||||
validValue: "{{ loop.someValue }}",
|
||||
}
|
||||
|
||||
const loopStepNumber = 2
|
||||
|
||||
const result = loopUtils.replaceFakeBindings(
|
||||
originalStepInput,
|
||||
loopStepNumber
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
nullValue: null,
|
||||
nestedNull: {
|
||||
someKey: null,
|
||||
},
|
||||
validValue: "{{ steps.2.someValue }}",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle empty objects and arrays", () => {
|
||||
const originalStepInput = {
|
||||
emptyObject: {},
|
||||
emptyArray: [],
|
||||
nestedEmpty: {
|
||||
emptyObj: {},
|
||||
emptyArr: [],
|
||||
},
|
||||
}
|
||||
|
||||
const loopStepNumber = 1
|
||||
|
||||
const result = loopUtils.replaceFakeBindings(
|
||||
originalStepInput,
|
||||
loopStepNumber
|
||||
)
|
||||
|
||||
expect(result).toEqual(originalStepInput)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,71 @@
|
|||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import nock from "nock"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.make({
|
||||
url: "http://www.example.com",
|
||||
body: null,
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.response.foo).toEqual("bar")
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should add the payload props when a JSON string is provided", async () => {
|
||||
const payload = {
|
||||
value1: 1,
|
||||
value2: 2,
|
||||
value3: 3,
|
||||
value4: 4,
|
||||
value5: 5,
|
||||
name: "Adam",
|
||||
age: 9,
|
||||
}
|
||||
|
||||
nock("http://www.example.com/")
|
||||
.post("/", payload)
|
||||
.reply(200, { foo: "bar" })
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.make({
|
||||
body: { value: JSON.stringify(payload) },
|
||||
url: "http://www.example.com",
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.response.foo).toEqual("bar")
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should return a 400 if the JSON payload string is malformed", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.make({
|
||||
body: { value: "{ invalid json }" },
|
||||
url: "http://www.example.com",
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.httpStatus).toEqual(400)
|
||||
expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON")
|
||||
expect(result.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,84 @@
|
|||
import TestConfiguration from "../../..//tests/utilities/TestConfiguration"
|
||||
import nock from "nock"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import { HttpMethod } from "@budibase/types"
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it("should be able to run the action and default to 'get'", async () => {
|
||||
nock("http://www.example.com/").get("/").reply(200, { foo: "bar" })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.n8n({
|
||||
url: "http://www.example.com",
|
||||
body: { test: "IGNORE_ME" },
|
||||
authorization: "",
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual({ foo: "bar" })
|
||||
expect(result.steps[0].outputs.httpStatus).toEqual(200)
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should add the payload props when a JSON string is provided", async () => {
|
||||
nock("http://www.example.com/")
|
||||
.post("/", { name: "Adam", age: 9 })
|
||||
.reply(200)
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.n8n({
|
||||
url: "http://www.example.com",
|
||||
body: { value: JSON.stringify({ name: "Adam", age: 9 }) },
|
||||
method: HttpMethod.POST,
|
||||
authorization: "",
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should return a 400 if the JSON payload string is malformed", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.n8n({
|
||||
url: "http://www.example.com",
|
||||
body: { value: "{ value1 1 }" },
|
||||
method: HttpMethod.POST,
|
||||
authorization: "",
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.httpStatus).toEqual(400)
|
||||
expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON")
|
||||
expect(result.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
|
||||
it("should not append the body if the method is HEAD", async () => {
|
||||
nock("http://www.example.com/")
|
||||
.head("/", body => body === "")
|
||||
.reply(200)
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.n8n({
|
||||
url: "http://www.example.com",
|
||||
method: HttpMethod.HEAD,
|
||||
body: { test: "IGNORE_ME" },
|
||||
authorization: "",
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,115 @@
|
|||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import { setEnv as setCoreEnv } from "@budibase/backend-core"
|
||||
import { Model, MonthlyQuotaName, QuotaUsageType } from "@budibase/types"
|
||||
import TestConfiguration from "../../..//tests/utilities/TestConfiguration"
|
||||
import {
|
||||
mockChatGPTError,
|
||||
mockChatGPTResponse,
|
||||
} from "../../../tests/utilities/mocks/openai"
|
||||
import nock from "nock"
|
||||
import { mocks } from "@budibase/backend-core/tests"
|
||||
import { quotas } from "@budibase/pro"
|
||||
|
||||
describe("test the openai action", () => {
|
||||
const config = new TestConfiguration()
|
||||
let resetEnv: () => void | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
resetEnv = setCoreEnv({ SELF_HOSTED: true, OPENAI_API_KEY: "abc123" })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
resetEnv()
|
||||
jest.clearAllMocks()
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
const getAIUsage = async () => {
|
||||
const { total } = await config.doInContext(config.getProdAppId(), () =>
|
||||
quotas.getCurrentUsageValues(
|
||||
QuotaUsageType.MONTHLY,
|
||||
MonthlyQuotaName.BUDIBASE_AI_CREDITS
|
||||
)
|
||||
)
|
||||
return total
|
||||
}
|
||||
|
||||
const expectAIUsage = async <T>(expected: number, f: () => Promise<T>) => {
|
||||
const before = await getAIUsage()
|
||||
const result = await f()
|
||||
const after = await getAIUsage()
|
||||
expect(after - before).toEqual(expected)
|
||||
return result
|
||||
}
|
||||
|
||||
it("should be able to receive a response from ChatGPT given a prompt", async () => {
|
||||
mockChatGPTResponse("This is a test")
|
||||
|
||||
// The AI usage is 0 because the AI feature is disabled by default, which
|
||||
// means it goes through the "legacy" path which requires you to set your
|
||||
// own API key. We don't count this against your quota.
|
||||
const result = await expectAIUsage(0, () =>
|
||||
createAutomationBuilder(config)
|
||||
.openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI })
|
||||
.run()
|
||||
)
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual("This is a test")
|
||||
expect(result.steps[0].outputs.success).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should present the correct error message when a prompt is not provided", async () => {
|
||||
const result = await expectAIUsage(0, () =>
|
||||
createAutomationBuilder(config)
|
||||
.openai({ prompt: "", model: Model.GPT_4O_MINI })
|
||||
.run()
|
||||
)
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual(
|
||||
"Budibase OpenAI Automation Failed: No prompt supplied"
|
||||
)
|
||||
expect(result.steps[0].outputs.success).toBeFalsy()
|
||||
})
|
||||
|
||||
it("should present the correct error message when an error is thrown from the createChatCompletion call", async () => {
|
||||
mockChatGPTError()
|
||||
|
||||
const result = await expectAIUsage(0, () =>
|
||||
createAutomationBuilder(config)
|
||||
.openai({ prompt: "Hello, world", model: Model.GPT_4O_MINI })
|
||||
.run()
|
||||
)
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual(
|
||||
"Error: 500 Internal Server Error"
|
||||
)
|
||||
expect(result.steps[0].outputs.success).toBeFalsy()
|
||||
})
|
||||
|
||||
it("should ensure that the pro AI module is called when the budibase AI features are enabled", async () => {
|
||||
mocks.licenses.useBudibaseAI()
|
||||
mocks.licenses.useAICustomConfigs()
|
||||
|
||||
mockChatGPTResponse("This is a test")
|
||||
|
||||
// We expect a non-0 AI usage here because it goes through the @budibase/pro
|
||||
// path, because we've enabled Budibase AI. The exact value depends on a
|
||||
// calculation we use to approximate cost. This uses Budibase's OpenAI API
|
||||
// key, so we charge users for it.
|
||||
const result = await expectAIUsage(14, () =>
|
||||
createAutomationBuilder(config)
|
||||
.openai({ model: Model.GPT_4O_MINI, prompt: "Hello, world" })
|
||||
.run()
|
||||
)
|
||||
|
||||
expect(result.steps[0].outputs.response).toEqual("This is a test")
|
||||
})
|
||||
})
|
|
@ -0,0 +1,51 @@
|
|||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import nock from "nock"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import { RequestType } from "@budibase/types"
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
nock("http://www.example.com")
|
||||
.post("/", { a: 1 })
|
||||
.reply(200, { foo: "bar" })
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.outgoingWebhook({
|
||||
requestMethod: RequestType.POST,
|
||||
url: "http://www.example.com",
|
||||
requestBody: JSON.stringify({ a: 1 }),
|
||||
headers: {},
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
expect(result.steps[0].outputs.httpStatus).toEqual(200)
|
||||
expect(result.steps[0].outputs.response.foo).toEqual("bar")
|
||||
})
|
||||
|
||||
it("should return an error if something goes wrong in fetch", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.outgoingWebhook({
|
||||
requestMethod: RequestType.GET,
|
||||
url: "www.invalid.com",
|
||||
requestBody: "",
|
||||
headers: {},
|
||||
})
|
||||
.run()
|
||||
expect(result.steps[0].outputs.success).toEqual(false)
|
||||
})
|
||||
})
|
|
@ -1,37 +1,34 @@
|
|||
import { EmptyFilterOption, SortOrder, Table } from "@budibase/types"
|
||||
import * as setup from "./utilities"
|
||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
import * as automation from "../index"
|
||||
import { basicTable } from "../../tests/utilities/structures"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import * as automation from "../../index"
|
||||
import { basicTable } from "../../../tests/utilities/structures"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
|
||||
const NAME = "Test"
|
||||
|
||||
describe("Test a query step automation", () => {
|
||||
const config = new TestConfiguration()
|
||||
let table: Table
|
||||
let config = setup.getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await automation.init()
|
||||
await config.init()
|
||||
table = await config.createTable()
|
||||
table = await config.api.table.save(basicTable())
|
||||
|
||||
const row = {
|
||||
name: NAME,
|
||||
description: "original description",
|
||||
tableId: table._id,
|
||||
}
|
||||
await config.createRow(row)
|
||||
await config.createRow(row)
|
||||
await config.api.row.save(table._id!, row)
|
||||
await config.api.row.save(table._id!, row)
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should be able to run the query step", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Basic Query Test",
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -55,11 +52,7 @@ describe("Test a query step automation", () => {
|
|||
})
|
||||
|
||||
it("Returns all rows when onEmptyFilter has no value and no filters are passed", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Empty Filter Test",
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -79,11 +72,7 @@ describe("Test a query step automation", () => {
|
|||
})
|
||||
|
||||
it("Returns no rows when onEmptyFilter is RETURN_NONE and theres no filters", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Return None Test",
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -104,11 +93,7 @@ describe("Test a query step automation", () => {
|
|||
})
|
||||
|
||||
it("Returns no rows when onEmptyFilters RETURN_NONE and a filter is passed with a null value", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Null Filter Test",
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -133,11 +118,7 @@ describe("Test a query step automation", () => {
|
|||
})
|
||||
|
||||
it("Returns rows when onEmptyFilter is RETURN_ALL and no filter is passed", async () => {
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Return All Test",
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.queryRows(
|
||||
{
|
||||
tableId: table._id!,
|
||||
|
@ -157,19 +138,14 @@ describe("Test a query step automation", () => {
|
|||
})
|
||||
|
||||
it("return rows when querying a table with a space in the name", async () => {
|
||||
const tableWithSpaces = await config.createTable({
|
||||
const tableWithSpaces = await config.api.table.save({
|
||||
...basicTable(),
|
||||
name: "table with spaces",
|
||||
})
|
||||
await config.createRow({
|
||||
await config.api.row.save(tableWithSpaces._id!, {
|
||||
name: NAME,
|
||||
tableId: tableWithSpaces._id,
|
||||
})
|
||||
const result = await createAutomationBuilder({
|
||||
name: "Return All Test",
|
||||
config,
|
||||
})
|
||||
.appAction({ fields: {} })
|
||||
const result = await createAutomationBuilder(config)
|
||||
.queryRows(
|
||||
{
|
||||
tableId: tableWithSpaces._id!,
|
|
@ -1,6 +1,7 @@
|
|||
import * as workerRequests from "../../utilities/workerRequests"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import * as workerRequests from "../../../utilities/workerRequests"
|
||||
|
||||
jest.mock("../../utilities/workerRequests", () => ({
|
||||
jest.mock("../../../utilities/workerRequests", () => ({
|
||||
sendSmtpEmail: jest.fn(),
|
||||
}))
|
||||
|
||||
|
@ -18,16 +19,18 @@ function generateResponse(to: string, from: string) {
|
|||
}
|
||||
}
|
||||
|
||||
import * as setup from "./utilities"
|
||||
import * as setup from "../utilities"
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
let inputs
|
||||
let config = setup.getConfig()
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
jest
|
||||
|
@ -42,7 +45,7 @@ describe("test the outgoing webhook action", () => {
|
|||
location: "location",
|
||||
url: "url",
|
||||
}
|
||||
inputs = {
|
||||
const inputs = {
|
||||
to: "user1@example.com",
|
||||
from: "admin@example.com",
|
||||
subject: "hello",
|
|
@ -0,0 +1,24 @@
|
|||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
|
||||
describe("test the server log action", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should be able to log the text", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.serverLog({ text: "Hello World" })
|
||||
.run()
|
||||
expect(result.steps[0].outputs.message).toEqual(
|
||||
`App ${config.getAppId()} - Hello World`
|
||||
)
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,49 @@
|
|||
import * as automation from "../../index"
|
||||
import env from "../../../environment"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
|
||||
describe("Test triggering an automation from another automation", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await automation.init()
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await automation.shutdown()
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should trigger an other server log automation", async () => {
|
||||
const automation = await createAutomationBuilder(config)
|
||||
.serverLog({ text: "Hello World" })
|
||||
.save()
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.triggerAutomationRun({
|
||||
automation: {
|
||||
automationId: automation._id!,
|
||||
},
|
||||
timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT,
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.success).toBe(true)
|
||||
})
|
||||
|
||||
it("should fail gracefully if the automation id is incorrect", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.triggerAutomationRun({
|
||||
automation: {
|
||||
// @ts-expect-error - incorrect on purpose
|
||||
automationId: null,
|
||||
},
|
||||
timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT,
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.success).toBe(false)
|
||||
})
|
||||
})
|
|
@ -8,15 +8,16 @@ import {
|
|||
Table,
|
||||
TableSourceType,
|
||||
} from "@budibase/types"
|
||||
import { createAutomationBuilder } from "./utilities/AutomationTestBuilder"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
|
||||
import * as setup from "./utilities"
|
||||
import * as uuid from "uuid"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
|
||||
describe("test the update row action", () => {
|
||||
let table: Table,
|
||||
row: Row,
|
||||
config = setup.getConfig()
|
||||
const config = new TestConfiguration()
|
||||
|
||||
let table: Table
|
||||
let row: Row
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
|
@ -24,15 +25,12 @@ describe("test the update row action", () => {
|
|||
row = await config.createRow()
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should be able to run the update row action", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Update Row Automation",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.updateRow({
|
||||
rowId: row._id!,
|
||||
row: {
|
||||
|
@ -54,12 +52,7 @@ describe("test the update row action", () => {
|
|||
})
|
||||
|
||||
it("should check invalid inputs return an error", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Invalid Inputs Automation",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.updateRow({ meta: {}, row: {}, rowId: "" })
|
||||
.run()
|
||||
|
||||
|
@ -67,12 +60,7 @@ describe("test the update row action", () => {
|
|||
})
|
||||
|
||||
it("should return an error when table doesn't exist", async () => {
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Nonexistent Table Automation",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.updateRow({
|
||||
row: { _id: "invalid" },
|
||||
rowId: "invalid",
|
||||
|
@ -115,12 +103,7 @@ describe("test the update row action", () => {
|
|||
user2: [{ _id: user2._id }],
|
||||
})
|
||||
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Link Preservation Automation",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.updateRow({
|
||||
rowId: row._id!,
|
||||
row: {
|
||||
|
@ -173,12 +156,7 @@ describe("test the update row action", () => {
|
|||
user2: [{ _id: user2._id }],
|
||||
})
|
||||
|
||||
const builder = createAutomationBuilder({
|
||||
name: "Link Overwrite Automation",
|
||||
})
|
||||
|
||||
const results = await builder
|
||||
.appAction({ fields: {} })
|
||||
const results = await createAutomationBuilder(config)
|
||||
.updateRow({
|
||||
rowId: row._id!,
|
||||
row: {
|
|
@ -0,0 +1,69 @@
|
|||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import nock from "nock"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.zapier({ url: "http://www.example.com", body: null })
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.response.foo).toEqual("bar")
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should add the payload props when a JSON string is provided", async () => {
|
||||
const payload = {
|
||||
value1: 1,
|
||||
value2: 2,
|
||||
value3: 3,
|
||||
value4: 4,
|
||||
value5: 5,
|
||||
name: "Adam",
|
||||
age: 9,
|
||||
}
|
||||
|
||||
nock("http://www.example.com/")
|
||||
.post("/", { ...payload, platform: "budibase" })
|
||||
.reply(200, { foo: "bar" })
|
||||
|
||||
const result = await createAutomationBuilder(config)
|
||||
.zapier({
|
||||
url: "http://www.example.com",
|
||||
body: { value: JSON.stringify(payload) },
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.response.foo).toEqual("bar")
|
||||
expect(result.steps[0].outputs.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should return a 400 if the JSON payload string is malformed", async () => {
|
||||
const result = await createAutomationBuilder(config)
|
||||
.zapier({
|
||||
url: "http://www.example.com",
|
||||
body: { value: "{ invalid json }" },
|
||||
})
|
||||
.run()
|
||||
|
||||
expect(result.steps[0].outputs.success).toEqual(false)
|
||||
expect(result.steps[0].outputs.response).toEqual("Invalid payload JSON")
|
||||
expect(result.steps[0].outputs.httpStatus).toEqual(400)
|
||||
})
|
||||
})
|
|
@ -1,54 +0,0 @@
|
|||
jest.spyOn(global.console, "error")
|
||||
|
||||
import * as setup from "./utilities"
|
||||
import * as automation from "../index"
|
||||
import { serverLogAutomation } from "../../tests/utilities/structures"
|
||||
import env from "../../environment"
|
||||
|
||||
describe("Test triggering an automation from another automation", () => {
|
||||
let config = setup.getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await automation.init()
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await automation.shutdown()
|
||||
setup.afterAll()
|
||||
})
|
||||
|
||||
it("should trigger an other server log automation", async () => {
|
||||
let automation = serverLogAutomation()
|
||||
let newAutomation = await config.createAutomation(automation)
|
||||
|
||||
const inputs: any = {
|
||||
automation: {
|
||||
automationId: newAutomation._id,
|
||||
timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT,
|
||||
},
|
||||
}
|
||||
const res = await setup.runStep(
|
||||
config,
|
||||
setup.actions.TRIGGER_AUTOMATION_RUN.stepId,
|
||||
inputs
|
||||
)
|
||||
// Check if the SERVER_LOG step was successful
|
||||
expect(res.value[1].outputs.success).toBe(true)
|
||||
})
|
||||
|
||||
it("should fail gracefully if the automation id is incorrect", async () => {
|
||||
const inputs: any = {
|
||||
automation: {
|
||||
automationId: null,
|
||||
timeout: env.getDefaults().AUTOMATION_THREAD_TIMEOUT,
|
||||
},
|
||||
}
|
||||
const res = await setup.runStep(
|
||||
config,
|
||||
setup.actions.TRIGGER_AUTOMATION_RUN.stepId,
|
||||
inputs
|
||||
)
|
||||
expect(res.success).toBe(false)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,62 @@
|
|||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
import { getQueue } from "../.."
|
||||
import { Job } from "bull"
|
||||
|
||||
describe("cron trigger", () => {
|
||||
const config = new TestConfiguration()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should queue a Bull cron job", async () => {
|
||||
const queue = getQueue()
|
||||
expect(await queue.getCompletedCount()).toEqual(0)
|
||||
|
||||
const jobPromise = new Promise<Job>(resolve => {
|
||||
queue.on("completed", async job => {
|
||||
resolve(job)
|
||||
})
|
||||
})
|
||||
|
||||
await createAutomationBuilder(config)
|
||||
.cron({ cron: "* * * * *" })
|
||||
.serverLog({
|
||||
text: "Hello, world!",
|
||||
})
|
||||
.save()
|
||||
|
||||
await config.api.application.publish(config.getAppId())
|
||||
|
||||
expect(await queue.getCompletedCount()).toEqual(1)
|
||||
|
||||
const job = await jobPromise
|
||||
const repeat = job.opts?.repeat
|
||||
if (!repeat || !("cron" in repeat)) {
|
||||
throw new Error("Expected cron repeat")
|
||||
}
|
||||
expect(repeat.cron).toEqual("* * * * *")
|
||||
})
|
||||
|
||||
it("should fail if the cron expression is invalid", async () => {
|
||||
await createAutomationBuilder(config)
|
||||
.cron({ cron: "* * * * * *" })
|
||||
.serverLog({
|
||||
text: "Hello, world!",
|
||||
})
|
||||
.save()
|
||||
|
||||
await config.api.application.publish(config.getAppId(), {
|
||||
status: 500,
|
||||
body: {
|
||||
message:
|
||||
'Deployment Failed: Invalid automation CRON "* * * * * *" - Expected 5 values, but got 6.',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,21 +1,17 @@
|
|||
import * as automation from "../../index"
|
||||
import * as setup from "../utilities"
|
||||
import { Table, Webhook, WebhookActionType } from "@budibase/types"
|
||||
import { createAutomationBuilder } from "../utilities/AutomationTestBuilder"
|
||||
import { mocks } from "@budibase/backend-core/tests"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
|
||||
mocks.licenses.useSyncAutomations()
|
||||
|
||||
describe("Branching automations", () => {
|
||||
let config = setup.getConfig(),
|
||||
table: Table,
|
||||
webhook: Webhook
|
||||
const config = new TestConfiguration()
|
||||
let table: Table
|
||||
let webhook: Webhook
|
||||
|
||||
async function createWebhookAutomation(testName: string) {
|
||||
const builder = createAutomationBuilder({
|
||||
name: testName,
|
||||
})
|
||||
const automation = await builder
|
||||
async function createWebhookAutomation() {
|
||||
const automation = await createAutomationBuilder(config)
|
||||
.webhook({ fields: { parameter: "string" } })
|
||||
.createRow({
|
||||
row: { tableId: table._id!, name: "{{ trigger.parameter }}" },
|
||||
|
@ -40,17 +36,16 @@ describe("Branching automations", () => {
|
|||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await automation.init()
|
||||
await config.init()
|
||||
table = await config.createTable()
|
||||
})
|
||||
|
||||
afterAll(setup.afterAll)
|
||||
afterAll(() => {
|
||||
config.end()
|
||||
})
|
||||
|
||||
it("should run the webhook automation - checking for parameters", async () => {
|
||||
const { webhook } = await createWebhookAutomation(
|
||||
"Check a basic webhook works as expected"
|
||||
)
|
||||
const { webhook } = await createWebhookAutomation()
|
||||
const res = await config.api.webhook.trigger(
|
||||
config.getProdAppId(),
|
||||
webhook._id!,
|
|
@ -2,42 +2,23 @@ import { v4 as uuidv4 } from "uuid"
|
|||
import { BUILTIN_ACTION_DEFINITIONS } from "../../actions"
|
||||
import { TRIGGER_DEFINITIONS } from "../../triggers"
|
||||
import {
|
||||
AppActionTriggerInputs,
|
||||
AppActionTriggerOutputs,
|
||||
Automation,
|
||||
AutomationActionStepId,
|
||||
AutomationStep,
|
||||
AutomationStepInputs,
|
||||
AutomationTrigger,
|
||||
AutomationTriggerDefinition,
|
||||
AutomationTriggerInputs,
|
||||
AutomationTriggerOutputs,
|
||||
AutomationTriggerStepId,
|
||||
BashStepInputs,
|
||||
Branch,
|
||||
BranchStepInputs,
|
||||
CollectStepInputs,
|
||||
CreateRowStepInputs,
|
||||
CronTriggerOutputs,
|
||||
DeleteRowStepInputs,
|
||||
ExecuteQueryStepInputs,
|
||||
ExecuteScriptStepInputs,
|
||||
FilterStepInputs,
|
||||
isDidNotTriggerResponse,
|
||||
LoopStepInputs,
|
||||
OpenAIStepInputs,
|
||||
QueryRowsStepInputs,
|
||||
RowCreatedTriggerInputs,
|
||||
RowCreatedTriggerOutputs,
|
||||
RowDeletedTriggerInputs,
|
||||
RowDeletedTriggerOutputs,
|
||||
RowUpdatedTriggerInputs,
|
||||
RowUpdatedTriggerOutputs,
|
||||
SearchFilters,
|
||||
ServerLogStepInputs,
|
||||
SmtpEmailStepInputs,
|
||||
TestAutomationRequest,
|
||||
UpdateRowStepInputs,
|
||||
WebhookTriggerInputs,
|
||||
WebhookTriggerOutputs,
|
||||
} from "@budibase/types"
|
||||
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
|
||||
|
@ -66,28 +47,53 @@ class BaseStepBuilder {
|
|||
protected steps: AutomationStep[] = []
|
||||
protected stepNames: { [key: string]: string } = {}
|
||||
|
||||
protected step<TStep extends AutomationActionStepId>(
|
||||
stepId: TStep,
|
||||
stepSchema: Omit<AutomationStep, "id" | "stepId" | "inputs">,
|
||||
inputs: AutomationStepInputs<TStep>,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
const id = opts?.stepId || uuidv4()
|
||||
this.steps.push({
|
||||
...stepSchema,
|
||||
inputs: inputs as any,
|
||||
id,
|
||||
stepId,
|
||||
name: opts?.stepName || stepSchema.name,
|
||||
})
|
||||
if (opts?.stepName) {
|
||||
this.stepNames[id] = opts.stepName
|
||||
protected createStepFn<TStep extends AutomationActionStepId>(stepId: TStep) {
|
||||
return (
|
||||
inputs: AutomationStepInputs<TStep>,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
) => {
|
||||
const schema = BUILTIN_ACTION_DEFINITIONS[stepId]
|
||||
const id = opts?.stepId || uuidv4()
|
||||
this.steps.push({
|
||||
...schema,
|
||||
inputs: inputs as any,
|
||||
id,
|
||||
stepId,
|
||||
name: opts?.stepName || schema.name,
|
||||
})
|
||||
if (opts?.stepName) {
|
||||
this.stepNames[id] = opts.stepName
|
||||
}
|
||||
return this
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
createRow = this.createStepFn(AutomationActionStepId.CREATE_ROW)
|
||||
updateRow = this.createStepFn(AutomationActionStepId.UPDATE_ROW)
|
||||
deleteRow = this.createStepFn(AutomationActionStepId.DELETE_ROW)
|
||||
sendSmtpEmail = this.createStepFn(AutomationActionStepId.SEND_EMAIL_SMTP)
|
||||
executeQuery = this.createStepFn(AutomationActionStepId.EXECUTE_QUERY)
|
||||
queryRows = this.createStepFn(AutomationActionStepId.QUERY_ROWS)
|
||||
loop = this.createStepFn(AutomationActionStepId.LOOP)
|
||||
serverLog = this.createStepFn(AutomationActionStepId.SERVER_LOG)
|
||||
executeScript = this.createStepFn(AutomationActionStepId.EXECUTE_SCRIPT)
|
||||
filter = this.createStepFn(AutomationActionStepId.FILTER)
|
||||
bash = this.createStepFn(AutomationActionStepId.EXECUTE_BASH)
|
||||
openai = this.createStepFn(AutomationActionStepId.OPENAI)
|
||||
collect = this.createStepFn(AutomationActionStepId.COLLECT)
|
||||
zapier = this.createStepFn(AutomationActionStepId.zapier)
|
||||
triggerAutomationRun = this.createStepFn(
|
||||
AutomationActionStepId.TRIGGER_AUTOMATION_RUN
|
||||
)
|
||||
outgoingWebhook = this.createStepFn(AutomationActionStepId.OUTGOING_WEBHOOK)
|
||||
n8n = this.createStepFn(AutomationActionStepId.n8n)
|
||||
make = this.createStepFn(AutomationActionStepId.integromat)
|
||||
discord = this.createStepFn(AutomationActionStepId.discord)
|
||||
delay = this.createStepFn(AutomationActionStepId.DELAY)
|
||||
|
||||
protected addBranchStep(branchConfig: BranchConfig): void {
|
||||
const branchStepInputs: BranchStepInputs = {
|
||||
branches: [] as Branch[],
|
||||
branches: [],
|
||||
children: {},
|
||||
}
|
||||
|
||||
|
@ -110,159 +116,6 @@ class BaseStepBuilder {
|
|||
}
|
||||
this.steps.push(branchStep)
|
||||
}
|
||||
|
||||
// STEPS
|
||||
createRow(
|
||||
inputs: CreateRowStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.CREATE_ROW,
|
||||
BUILTIN_ACTION_DEFINITIONS.CREATE_ROW,
|
||||
inputs,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
updateRow(
|
||||
inputs: UpdateRowStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.UPDATE_ROW,
|
||||
BUILTIN_ACTION_DEFINITIONS.UPDATE_ROW,
|
||||
inputs,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
deleteRow(
|
||||
inputs: DeleteRowStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.DELETE_ROW,
|
||||
BUILTIN_ACTION_DEFINITIONS.DELETE_ROW,
|
||||
inputs,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
sendSmtpEmail(
|
||||
inputs: SmtpEmailStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.SEND_EMAIL_SMTP,
|
||||
BUILTIN_ACTION_DEFINITIONS.SEND_EMAIL_SMTP,
|
||||
inputs,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
executeQuery(
|
||||
inputs: ExecuteQueryStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.EXECUTE_QUERY,
|
||||
BUILTIN_ACTION_DEFINITIONS.EXECUTE_QUERY,
|
||||
inputs,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
queryRows(
|
||||
inputs: QueryRowsStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.QUERY_ROWS,
|
||||
BUILTIN_ACTION_DEFINITIONS.QUERY_ROWS,
|
||||
inputs,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
loop(
|
||||
inputs: LoopStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.LOOP,
|
||||
BUILTIN_ACTION_DEFINITIONS.LOOP,
|
||||
inputs,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
serverLog(
|
||||
input: ServerLogStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.SERVER_LOG,
|
||||
BUILTIN_ACTION_DEFINITIONS.SERVER_LOG,
|
||||
input,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
executeScript(
|
||||
input: ExecuteScriptStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.EXECUTE_SCRIPT,
|
||||
BUILTIN_ACTION_DEFINITIONS.EXECUTE_SCRIPT,
|
||||
input,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
filter(input: FilterStepInputs): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.FILTER,
|
||||
BUILTIN_ACTION_DEFINITIONS.FILTER,
|
||||
input
|
||||
)
|
||||
}
|
||||
|
||||
bash(
|
||||
input: BashStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.EXECUTE_BASH,
|
||||
BUILTIN_ACTION_DEFINITIONS.EXECUTE_BASH,
|
||||
input,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
openai(
|
||||
input: OpenAIStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.OPENAI,
|
||||
BUILTIN_ACTION_DEFINITIONS.OPENAI,
|
||||
input,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
collect(
|
||||
input: CollectStepInputs,
|
||||
opts?: { stepName?: string; stepId?: string }
|
||||
): this {
|
||||
return this.step(
|
||||
AutomationActionStepId.COLLECT,
|
||||
BUILTIN_ACTION_DEFINITIONS.COLLECT,
|
||||
input,
|
||||
opts
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class StepBuilder extends BaseStepBuilder {
|
||||
|
@ -282,101 +135,79 @@ class AutomationBuilder extends BaseStepBuilder {
|
|||
private triggerOutputs: TriggerOutputs
|
||||
private triggerSet = false
|
||||
|
||||
constructor(
|
||||
options: { name?: string; appId?: string; config?: TestConfiguration } = {}
|
||||
) {
|
||||
constructor(config?: TestConfiguration) {
|
||||
super()
|
||||
this.config = config || setup.getConfig()
|
||||
this.triggerOutputs = { fields: {} }
|
||||
this.automationConfig = {
|
||||
name: options.name || `Test Automation ${uuidv4()}`,
|
||||
name: `Test Automation ${uuidv4()}`,
|
||||
definition: {
|
||||
steps: [],
|
||||
trigger: {} as AutomationTrigger,
|
||||
trigger: {
|
||||
...TRIGGER_DEFINITIONS[AutomationTriggerStepId.APP],
|
||||
stepId: AutomationTriggerStepId.APP,
|
||||
inputs: this.triggerOutputs,
|
||||
id: uuidv4(),
|
||||
},
|
||||
stepNames: {},
|
||||
},
|
||||
type: "automation",
|
||||
appId: options.appId ?? setup.getConfig().getAppId(),
|
||||
appId: this.config.getAppId(),
|
||||
}
|
||||
this.config = options.config || setup.getConfig()
|
||||
}
|
||||
|
||||
// TRIGGERS
|
||||
rowSaved(inputs: RowCreatedTriggerInputs, outputs: RowCreatedTriggerOutputs) {
|
||||
this.triggerOutputs = outputs
|
||||
return this.trigger(
|
||||
TRIGGER_DEFINITIONS.ROW_SAVED,
|
||||
AutomationTriggerStepId.ROW_SAVED,
|
||||
inputs,
|
||||
outputs
|
||||
)
|
||||
}
|
||||
|
||||
rowUpdated(
|
||||
inputs: RowUpdatedTriggerInputs,
|
||||
outputs: RowUpdatedTriggerOutputs
|
||||
) {
|
||||
this.triggerOutputs = outputs
|
||||
return this.trigger(
|
||||
TRIGGER_DEFINITIONS.ROW_UPDATED,
|
||||
AutomationTriggerStepId.ROW_UPDATED,
|
||||
inputs,
|
||||
outputs
|
||||
)
|
||||
}
|
||||
|
||||
rowDeleted(
|
||||
inputs: RowDeletedTriggerInputs,
|
||||
outputs: RowDeletedTriggerOutputs
|
||||
) {
|
||||
this.triggerOutputs = outputs
|
||||
return this.trigger(
|
||||
TRIGGER_DEFINITIONS.ROW_DELETED,
|
||||
AutomationTriggerStepId.ROW_DELETED,
|
||||
inputs,
|
||||
outputs
|
||||
)
|
||||
}
|
||||
|
||||
appAction(outputs: AppActionTriggerOutputs, inputs?: AppActionTriggerInputs) {
|
||||
this.triggerOutputs = outputs
|
||||
return this.trigger(
|
||||
TRIGGER_DEFINITIONS.APP,
|
||||
AutomationTriggerStepId.APP,
|
||||
inputs,
|
||||
outputs
|
||||
)
|
||||
}
|
||||
|
||||
webhook(outputs: WebhookTriggerOutputs, inputs?: WebhookTriggerInputs) {
|
||||
this.triggerOutputs = outputs
|
||||
return this.trigger(
|
||||
TRIGGER_DEFINITIONS.WEBHOOK,
|
||||
AutomationTriggerStepId.WEBHOOK,
|
||||
inputs,
|
||||
outputs
|
||||
)
|
||||
}
|
||||
|
||||
private trigger<TStep extends AutomationTriggerStepId>(
|
||||
triggerSchema: AutomationTriggerDefinition,
|
||||
stepId: TStep,
|
||||
inputs?: AutomationTriggerInputs<TStep>,
|
||||
outputs?: TriggerOutputs
|
||||
): this {
|
||||
if (this.triggerSet) {
|
||||
throw new Error("Only one trigger can be set for an automation.")
|
||||
}
|
||||
this.automationConfig.definition.trigger = {
|
||||
...triggerSchema,
|
||||
stepId,
|
||||
inputs: inputs || ({} as any),
|
||||
id: uuidv4(),
|
||||
}
|
||||
this.triggerOutputs = outputs
|
||||
this.triggerSet = true
|
||||
|
||||
name(n: string): this {
|
||||
this.automationConfig.name = n
|
||||
return this
|
||||
}
|
||||
|
||||
protected triggerInputOutput<
|
||||
TStep extends AutomationTriggerStepId,
|
||||
TInput = AutomationTriggerInputs<TStep>,
|
||||
TOutput = AutomationTriggerOutputs<TStep>
|
||||
>(stepId: TStep) {
|
||||
return (inputs: TInput, outputs?: TOutput) => {
|
||||
if (this.triggerSet) {
|
||||
throw new Error("Only one trigger can be set for an automation.")
|
||||
}
|
||||
this.triggerOutputs = outputs as TriggerOutputs | undefined
|
||||
this.automationConfig.definition.trigger = {
|
||||
...TRIGGER_DEFINITIONS[stepId],
|
||||
stepId,
|
||||
inputs,
|
||||
id: uuidv4(),
|
||||
} as AutomationTrigger
|
||||
this.triggerSet = true
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
protected triggerOutputOnly<
|
||||
TStep extends AutomationTriggerStepId,
|
||||
TOutput = AutomationTriggerOutputs<TStep>
|
||||
>(stepId: TStep) {
|
||||
return (outputs: TOutput) => {
|
||||
this.triggerOutputs = outputs as TriggerOutputs
|
||||
this.automationConfig.definition.trigger = {
|
||||
...TRIGGER_DEFINITIONS[stepId],
|
||||
stepId,
|
||||
id: uuidv4(),
|
||||
} as AutomationTrigger
|
||||
this.triggerSet = true
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
// The input and output for appAction is identical, and we only ever seem to
|
||||
// set the output, so we're ignoring the input for now.
|
||||
appAction = this.triggerOutputOnly(AutomationTriggerStepId.APP)
|
||||
|
||||
rowSaved = this.triggerInputOutput(AutomationTriggerStepId.ROW_SAVED)
|
||||
rowUpdated = this.triggerInputOutput(AutomationTriggerStepId.ROW_UPDATED)
|
||||
rowDeleted = this.triggerInputOutput(AutomationTriggerStepId.ROW_DELETED)
|
||||
webhook = this.triggerInputOutput(AutomationTriggerStepId.WEBHOOK)
|
||||
cron = this.triggerInputOutput(AutomationTriggerStepId.CRON)
|
||||
|
||||
branch(branchConfig: BranchConfig): this {
|
||||
this.addBranchStep(branchConfig)
|
||||
return this
|
||||
|
@ -389,11 +220,9 @@ class AutomationBuilder extends BaseStepBuilder {
|
|||
}
|
||||
|
||||
async save() {
|
||||
if (!Object.keys(this.automationConfig.definition.trigger).length) {
|
||||
throw new Error("Please add a trigger to this automation test")
|
||||
}
|
||||
this.automationConfig.definition.steps = this.steps
|
||||
return await this.config.createAutomation(this.build())
|
||||
const { automation } = await this.config.api.automation.post(this.build())
|
||||
return automation
|
||||
}
|
||||
|
||||
async run() {
|
||||
|
@ -415,10 +244,6 @@ class AutomationBuilder extends BaseStepBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
export function createAutomationBuilder(options?: {
|
||||
name?: string
|
||||
appId?: string
|
||||
config?: TestConfiguration
|
||||
}) {
|
||||
return new AutomationBuilder(options)
|
||||
export function createAutomationBuilder(config: TestConfiguration) {
|
||||
return new AutomationBuilder(config)
|
||||
}
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import { getConfig, afterAll, runStep, actions } from "./utilities"
|
||||
import nock from "nock"
|
||||
|
||||
describe("test the outgoing webhook action", () => {
|
||||
let config = getConfig()
|
||||
|
||||
beforeAll(async () => {
|
||||
await config.init()
|
||||
})
|
||||
|
||||
afterAll()
|
||||
|
||||
beforeEach(() => {
|
||||
nock.cleanAll()
|
||||
})
|
||||
|
||||
it("should be able to run the action", async () => {
|
||||
nock("http://www.example.com/").post("/").reply(200, { foo: "bar" })
|
||||
const res = await runStep(config, actions.zapier.stepId, {
|
||||
url: "http://www.example.com",
|
||||
})
|
||||
expect(res.response.foo).toEqual("bar")
|
||||
expect(res.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should add the payload props when a JSON string is provided", async () => {
|
||||
const payload = {
|
||||
value1: 1,
|
||||
value2: 2,
|
||||
value3: 3,
|
||||
value4: 4,
|
||||
value5: 5,
|
||||
name: "Adam",
|
||||
age: 9,
|
||||
}
|
||||
|
||||
nock("http://www.example.com/")
|
||||
.post("/", { ...payload, platform: "budibase" })
|
||||
.reply(200, { foo: "bar" })
|
||||
|
||||
const res = await runStep(config, actions.zapier.stepId, {
|
||||
body: { value: JSON.stringify(payload) },
|
||||
url: "http://www.example.com",
|
||||
})
|
||||
expect(res.response.foo).toEqual("bar")
|
||||
expect(res.success).toEqual(true)
|
||||
})
|
||||
|
||||
it("should return a 400 if the JSON payload string is malformed", async () => {
|
||||
const res = await runStep(config, actions.zapier.stepId, {
|
||||
body: { value: "{ invalid json }" },
|
||||
url: "http://www.example.com",
|
||||
})
|
||||
expect(res.httpStatus).toEqual(400)
|
||||
expect(res.response).toEqual("Invalid payload JSON")
|
||||
expect(res.success).toEqual(false)
|
||||
})
|
||||
})
|
|
@ -230,7 +230,7 @@ export async function enableCronTrigger(appId: any, automation: Automation) {
|
|||
// can't use getAppDB here as this is likely to be called from dev app,
|
||||
// but this call could be for dev app or prod app, need to just use what
|
||||
// was passed in
|
||||
await dbCore.doWithDB(appId, async (db: any) => {
|
||||
await dbCore.doWithDB(appId, async db => {
|
||||
const response = await db.put(automation)
|
||||
automation._id = response.id
|
||||
automation._rev = response.rev
|
||||
|
|
|
@ -609,7 +609,15 @@ export class GoogleSheetsMock {
|
|||
for (let col = startColumnIndex; col <= endColumnIndex; col++) {
|
||||
const cell = this.getCellNumericIndexes(sheetId, row, col)
|
||||
if (!cell) {
|
||||
throw new Error("Cell not found")
|
||||
const sheet = this.getSheetById(sheetId)
|
||||
if (!sheet) {
|
||||
throw new Error(`Sheet ${sheetId} not found`)
|
||||
}
|
||||
const sheetRows = sheet.data[0].rowData.length
|
||||
const sheetCols = sheet.data[0].rowData[0].values.length
|
||||
throw new Error(
|
||||
`Failed to find cell at ${row}, ${col}. Range: ${valueRange.range}. Sheet dimensions: ${sheetRows}x${sheetCols}.`
|
||||
)
|
||||
}
|
||||
const value =
|
||||
valueRange.values[row - startRowIndex][col - startColumnIndex]
|
||||
|
@ -638,7 +646,15 @@ export class GoogleSheetsMock {
|
|||
for (let col = startColumnIndex; col <= endColumnIndex; col++) {
|
||||
const cell = this.getCellNumericIndexes(sheetId, row, col)
|
||||
if (!cell) {
|
||||
throw new Error("Cell not found")
|
||||
const sheet = this.getSheetById(sheetId)
|
||||
if (!sheet) {
|
||||
throw new Error(`Sheet ${sheetId} not found`)
|
||||
}
|
||||
const sheetRows = sheet.data[0].rowData.length
|
||||
const sheetCols = sheet.data[0].rowData[0].values.length
|
||||
throw new Error(
|
||||
`Failed to find cell at ${row}, ${col}. Range: ${valueRange.range}. Sheet dimensions: ${sheetRows}x${sheetCols}.`
|
||||
)
|
||||
}
|
||||
values.push(this.cellValue(cell))
|
||||
}
|
||||
|
|
|
@ -33,7 +33,10 @@ export class ApplicationAPI extends TestAPI {
|
|||
await this._delete(`/api/applications/${appId}`, { expectations })
|
||||
}
|
||||
|
||||
publish = async (appId: string): Promise<PublishResponse> => {
|
||||
publish = async (
|
||||
appId: string,
|
||||
expectations?: Expectations
|
||||
): Promise<PublishResponse> => {
|
||||
return await this._post<PublishResponse>(
|
||||
`/api/applications/${appId}/publish`,
|
||||
{
|
||||
|
@ -42,14 +45,16 @@ export class ApplicationAPI extends TestAPI {
|
|||
headers: {
|
||||
[constants.Header.APP_ID]: appId,
|
||||
},
|
||||
expectations,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
unpublish = async (appId: string): Promise<void> => {
|
||||
await this._post(`/api/applications/${appId}/unpublish`, {
|
||||
expectations: { status: 200 },
|
||||
})
|
||||
unpublish = async (
|
||||
appId: string,
|
||||
expectations?: Expectations
|
||||
): Promise<void> => {
|
||||
await this._post(`/api/applications/${appId}/unpublish`, { expectations })
|
||||
}
|
||||
|
||||
sync = async (
|
||||
|
@ -144,13 +149,20 @@ export class ApplicationAPI extends TestAPI {
|
|||
})
|
||||
}
|
||||
|
||||
fetch = async ({ status }: { status?: AppStatus } = {}): Promise<App[]> => {
|
||||
fetch = async (
|
||||
{ status }: { status?: AppStatus } = {},
|
||||
expectations?: Expectations
|
||||
): Promise<App[]> => {
|
||||
return await this._get<App[]>("/api/applications", {
|
||||
query: { status },
|
||||
expectations,
|
||||
})
|
||||
}
|
||||
|
||||
addSampleData = async (appId: string): Promise<void> => {
|
||||
await this._post(`/api/applications/${appId}/sample`)
|
||||
addSampleData = async (
|
||||
appId: string,
|
||||
expectations?: Expectations
|
||||
): Promise<void> => {
|
||||
await this._post(`/api/applications/${appId}/sample`, { expectations })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,82 @@
|
|||
import nock from "nock"
|
||||
|
||||
let chatID = 1
|
||||
const SPACE_REGEX = /\s+/g
|
||||
|
||||
interface MockChatGPTResponseOpts {
|
||||
host?: string
|
||||
}
|
||||
|
||||
interface Message {
|
||||
role: string
|
||||
content: string
|
||||
}
|
||||
|
||||
interface Choice {
|
||||
index: number
|
||||
message: Message
|
||||
logprobs: null
|
||||
finish_reason: string
|
||||
}
|
||||
|
||||
interface CompletionTokensDetails {
|
||||
reasoning_tokens: number
|
||||
accepted_prediction_tokens: number
|
||||
rejected_prediction_tokens: number
|
||||
}
|
||||
|
||||
interface Usage {
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
total_tokens: number
|
||||
completion_tokens_details: CompletionTokensDetails
|
||||
}
|
||||
|
||||
interface ChatCompletionRequest {
|
||||
messages: Message[]
|
||||
model: string
|
||||
}
|
||||
|
||||
interface ChatCompletionResponse {
|
||||
id: string
|
||||
object: string
|
||||
created: number
|
||||
model: string
|
||||
system_fingerprint: string
|
||||
choices: Choice[]
|
||||
usage: Usage
|
||||
}
|
||||
|
||||
export function mockChatGPTResponse(
|
||||
response: string | ((prompt: string) => string)
|
||||
answer: string | ((prompt: string) => string),
|
||||
opts?: MockChatGPTResponseOpts
|
||||
) {
|
||||
return nock("https://api.openai.com")
|
||||
return nock(opts?.host || "https://api.openai.com")
|
||||
.post("/v1/chat/completions")
|
||||
.reply(200, (uri, requestBody) => {
|
||||
let content = response
|
||||
if (typeof response === "function") {
|
||||
const messages = (requestBody as any).messages
|
||||
content = response(messages[0].content)
|
||||
.reply(200, (uri: string, requestBody: ChatCompletionRequest) => {
|
||||
const messages = requestBody.messages
|
||||
const prompt = messages[0].content
|
||||
|
||||
let content
|
||||
if (typeof answer === "function") {
|
||||
content = answer(prompt)
|
||||
} else {
|
||||
content = answer
|
||||
}
|
||||
|
||||
chatID++
|
||||
|
||||
return {
|
||||
// We mock token usage because we use it to calculate Budibase AI quota
|
||||
// usage when Budibase AI is enabled, and some tests assert against quota
|
||||
// usage to make sure we're tracking correctly.
|
||||
const prompt_tokens = messages[0].content.split(SPACE_REGEX).length
|
||||
const completion_tokens = content.split(SPACE_REGEX).length
|
||||
|
||||
const response: ChatCompletionResponse = {
|
||||
id: `chatcmpl-${chatID}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: "gpt-4o-mini",
|
||||
model: requestBody.model,
|
||||
system_fingerprint: `fp_${chatID}`,
|
||||
choices: [
|
||||
{
|
||||
|
@ -31,9 +87,9 @@ export function mockChatGPTResponse(
|
|||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
total_tokens: prompt_tokens + completion_tokens,
|
||||
completion_tokens_details: {
|
||||
reasoning_tokens: 0,
|
||||
accepted_prediction_tokens: 0,
|
||||
|
@ -41,6 +97,14 @@ export function mockChatGPTResponse(
|
|||
},
|
||||
},
|
||||
}
|
||||
return response
|
||||
})
|
||||
.persist()
|
||||
}
|
||||
|
||||
export function mockChatGPTError() {
|
||||
return nock("https://api.openai.com")
|
||||
.post("/v1/chat/completions")
|
||||
.reply(500, "Internal Server Error")
|
||||
.persist()
|
||||
}
|
||||
|
|
|
@ -238,88 +238,6 @@ export function basicAutomation(opts?: DeepPartial<Automation>): Automation {
|
|||
return merge(baseAutomation, opts)
|
||||
}
|
||||
|
||||
export function basicCronAutomation(appId: string, cron: string): Automation {
|
||||
const automation: Automation = {
|
||||
name: `Automation ${generator.guid()}`,
|
||||
definition: {
|
||||
trigger: {
|
||||
stepId: AutomationTriggerStepId.CRON,
|
||||
name: "test",
|
||||
tagline: "test",
|
||||
icon: "test",
|
||||
description: "test",
|
||||
type: AutomationStepType.TRIGGER,
|
||||
id: "test",
|
||||
inputs: {
|
||||
cron,
|
||||
},
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {},
|
||||
},
|
||||
outputs: {
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
steps: [],
|
||||
},
|
||||
type: "automation",
|
||||
appId,
|
||||
}
|
||||
return automation
|
||||
}
|
||||
|
||||
export function serverLogAutomation(appId?: string): Automation {
|
||||
return {
|
||||
name: "My Automation",
|
||||
screenId: "kasdkfldsafkl",
|
||||
live: true,
|
||||
uiTree: {},
|
||||
definition: {
|
||||
trigger: {
|
||||
stepId: AutomationTriggerStepId.APP,
|
||||
name: "test",
|
||||
tagline: "test",
|
||||
icon: "test",
|
||||
description: "test",
|
||||
type: AutomationStepType.TRIGGER,
|
||||
id: "test",
|
||||
inputs: { fields: {} },
|
||||
schema: {
|
||||
inputs: {
|
||||
properties: {},
|
||||
},
|
||||
outputs: {
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
stepId: AutomationActionStepId.SERVER_LOG,
|
||||
name: "Backend log",
|
||||
tagline: "Console log a value in the backend",
|
||||
icon: "Monitoring",
|
||||
description: "Logs the given text to the server (using console.log)",
|
||||
internal: true,
|
||||
features: {
|
||||
LOOPING: true,
|
||||
},
|
||||
inputs: {
|
||||
text: "log statement",
|
||||
},
|
||||
schema: BUILTIN_ACTION_DEFINITIONS.SERVER_LOG.schema,
|
||||
id: "y8lkZbeSe",
|
||||
type: AutomationStepType.ACTION,
|
||||
},
|
||||
],
|
||||
},
|
||||
type: "automation",
|
||||
appId: appId!,
|
||||
}
|
||||
}
|
||||
|
||||
export function loopAutomation(
|
||||
tableId: string,
|
||||
loopOpts?: LoopInput
|
||||
|
|
|
@ -20,7 +20,8 @@ export interface QuotaTriggeredRequest {
|
|||
}
|
||||
|
||||
export interface LicenseActivateRequest {
|
||||
installVersion?: string
|
||||
installVersion: string
|
||||
installId: string
|
||||
}
|
||||
|
||||
export interface UpdateLicenseRequest {
|
||||
|
|
|
@ -141,7 +141,7 @@ export type MakeIntegrationInputs = {
|
|||
|
||||
export type n8nStepInputs = {
|
||||
url: string
|
||||
method: HttpMethod
|
||||
method?: HttpMethod
|
||||
authorization: string
|
||||
body: any
|
||||
}
|
||||
|
@ -237,7 +237,8 @@ export type ZapierStepInputs = {
|
|||
export type ZapierStepOutputs = Omit<ExternalAppStepOutputs, "response"> & {
|
||||
response: string
|
||||
}
|
||||
enum RequestType {
|
||||
|
||||
export enum RequestType {
|
||||
POST = "POST",
|
||||
GET = "GET",
|
||||
PUT = "PUT",
|
||||
|
@ -249,7 +250,7 @@ export type OutgoingWebhookStepInputs = {
|
|||
requestMethod: RequestType
|
||||
url: string
|
||||
requestBody: string
|
||||
headers: string
|
||||
headers: string | Record<string, string>
|
||||
}
|
||||
|
||||
export type AppActionTriggerInputs = {
|
||||
|
|
|
@ -52,6 +52,12 @@ import {
|
|||
RowDeletedTriggerInputs,
|
||||
BranchStepInputs,
|
||||
BaseAutomationOutputs,
|
||||
AppActionTriggerOutputs,
|
||||
CronTriggerOutputs,
|
||||
RowDeletedTriggerOutputs,
|
||||
RowCreatedTriggerOutputs,
|
||||
RowUpdatedTriggerOutputs,
|
||||
WebhookTriggerOutputs,
|
||||
} from "./StepInputsOutputs"
|
||||
|
||||
export type ActionImplementations<T extends Hosting> = {
|
||||
|
@ -341,6 +347,23 @@ export type AutomationTriggerInputs<T extends AutomationTriggerStepId> =
|
|||
? Record<string, any>
|
||||
: never
|
||||
|
||||
export type AutomationTriggerOutputs<T extends AutomationTriggerStepId> =
|
||||
T extends AutomationTriggerStepId.APP
|
||||
? AppActionTriggerOutputs
|
||||
: T extends AutomationTriggerStepId.CRON
|
||||
? CronTriggerOutputs
|
||||
: T extends AutomationTriggerStepId.ROW_ACTION
|
||||
? Record<string, any>
|
||||
: T extends AutomationTriggerStepId.ROW_DELETED
|
||||
? RowDeletedTriggerOutputs
|
||||
: T extends AutomationTriggerStepId.ROW_SAVED
|
||||
? RowCreatedTriggerOutputs
|
||||
: T extends AutomationTriggerStepId.ROW_UPDATED
|
||||
? RowUpdatedTriggerOutputs
|
||||
: T extends AutomationTriggerStepId.WEBHOOK
|
||||
? WebhookTriggerOutputs
|
||||
: never
|
||||
|
||||
export interface AutomationTriggerSchema<
|
||||
TTrigger extends AutomationTriggerStepId
|
||||
> extends AutomationStepSchemaBase {
|
||||
|
|
Loading…
Reference in New Issue