diff --git a/packages/server/src/automations/tests/executeQuery.spec.ts b/packages/server/src/automations/tests/executeQuery.spec.ts index 20f906e695..5598641866 100644 --- a/packages/server/src/automations/tests/executeQuery.spec.ts +++ b/packages/server/src/automations/tests/executeQuery.spec.ts @@ -1,55 +1,36 @@ import { Datasource, Query } from "@budibase/types" import * as setup from "./utilities" -import { - DatabaseName, - getDatasource, - knexClient, -} from "../../integrations/tests/utils" +import { DatabaseName } from "../../integrations/tests/utils" import { Knex } from "knex" -import { generator } from "@budibase/backend-core/tests" -describe.each( - [ - DatabaseName.POSTGRES, - DatabaseName.MYSQL, - DatabaseName.SQL_SERVER, - DatabaseName.MARIADB, - DatabaseName.ORACLE, - ].map(name => [name, getDatasource(name)]) -)("execute query action (%s)", (_, dsProvider) => { +describe.each([ + DatabaseName.POSTGRES, + DatabaseName.MYSQL, + DatabaseName.SQL_SERVER, + DatabaseName.MARIADB, + DatabaseName.ORACLE, +])("execute query action (%s)", name => { let tableName: string let client: Knex let datasource: Datasource let query: Query - let config = setup.getConfig() + const config = setup.getConfig() beforeAll(async () => { await config.init() - const ds = await dsProvider - datasource = await config.api.datasource.create(ds) - client = await knexClient(ds) + const testSetup = await setup.setupTestDatasource(config, name) + datasource = testSetup.datasource + client = testSetup.client }) beforeEach(async () => { - tableName = generator.guid() - await client.schema.createTable(tableName, table => { - table.string("a") - table.integer("b") - }) - await client(tableName).insert({ a: "string", b: 1 }) - 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", + tableName = await setup.createTestTable(client, { + a: { type: "string" }, + b: { type: "number" }, }) + await setup.insertTestData(client, tableName, [{ a: "string", b: 1 }]) + query = await setup.saveTestQuery(config, client, tableName, datasource) }) afterEach(async () => { diff --git a/packages/server/src/automations/tests/scenarios/scenarios.spec.ts b/packages/server/src/automations/tests/scenarios/scenarios.spec.ts index 4f4ad70814..7df902d61c 100644 --- a/packages/server/src/automations/tests/scenarios/scenarios.spec.ts +++ b/packages/server/src/automations/tests/scenarios/scenarios.spec.ts @@ -1,7 +1,14 @@ import * as automation from "../../index" import * as setup from "../utilities" -import { Table, LoopStepType } from "@budibase/types" +import { + Table, + LoopStepType, + CreateRowStepOutputs, + ServerLogStepOutputs, + FieldType, +} from "@budibase/types" import { createAutomationBuilder } from "../utilities/AutomationBuilder" +import { DatabaseName } from "../../../integrations/tests/utils" describe("Automation Scenarios", () => { let config = setup.getConfig(), @@ -63,6 +70,72 @@ describe("Automation Scenarios", () => { }) }) }) + + 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 + .rowSaved( + { tableId: table._id! }, + { + row: { + name: "Trigger Row", + description: "This row triggers the automation", + }, + id: "1234", + revision: "1", + } + ) + .loop({ + option: LoopStepType.ARRAY, + binding: [1, 2, 3], + }) + .createRow({ + row: { + name: "Item {{ loop.currentItem }}", + description: "Created from loop", + tableId: table._id, + }, + }) + .loop({ + option: LoopStepType.STRING, + binding: "Message 1,Message 2,Message 3", + }) + .serverLog({ text: "{{loop.currentItem}}" }) + .run() + + expect(results.trigger).toBeDefined() + expect(results.steps).toHaveLength(2) + + expect(results.steps[0].outputs.iterations).toBe(3) + expect(results.steps[0].outputs.items).toHaveLength(3) + + results.steps[0].outputs.items.forEach( + (output: CreateRowStepOutputs, index: number) => { + expect(output).toMatchObject({ + success: true, + row: { + name: `Item ${index + 1}`, + description: "Created from loop", + }, + }) + } + ) + + expect(results.steps[1].outputs.iterations).toBe(3) + expect(results.steps[1].outputs.items).toHaveLength(3) + + results.steps[1].outputs.items.forEach( + (output: ServerLogStepOutputs, index: number) => { + expect(output).toMatchObject({ + success: true, + }) + expect(output.message).toContain(`Message ${index + 1}`) + } + ) + }) }) describe("Row Automations", () => { @@ -157,4 +230,94 @@ describe("Automation Scenarios", () => { expect(results.steps[1].outputs.success).toBeTruthy() expect(results.steps[2].outputs.rows).toHaveLength(1) }) + + it("should query an external database for some data then insert than into an internal table", async () => { + const { datasource, client } = await setup.setupTestDatasource( + config, + DatabaseName.MYSQL + ) + + const newTable = await config.createTable({ + name: "table", + type: "table", + schema: { + name: { + name: "name", + type: FieldType.STRING, + constraints: { + presence: true, + }, + }, + age: { + name: "age", + type: FieldType.NUMBER, + constraints: { + presence: true, + }, + }, + }, + }) + + const tableName = await setup.createTestTable(client, { + name: { type: "string" }, + age: { type: "number" }, + }) + + const rows = [ + { name: "Joe", age: 20 }, + { name: "Bob", age: 25 }, + { name: "Paul", age: 30 }, + ] + + await setup.insertTestData(client, tableName, rows) + + const query = await setup.saveTestQuery( + config, + client, + tableName, + datasource + ) + + const builder = createAutomationBuilder({ + name: "Test external query and save", + }) + + const results = await builder + .appAction({ + fields: {}, + }) + .executeQuery({ + query: { + queryId: query._id!, + }, + }) + .loop({ + option: LoopStepType.ARRAY, + binding: "{{ steps.1.response }}", + }) + .createRow({ + row: { + name: "{{ loop.currentItem.name }}", + age: "{{ loop.currentItem.age }}", + tableId: newTable._id!, + }, + }) + .queryRows({ + tableId: newTable._id!, + }) + .run() + + expect(results.steps).toHaveLength(3) + + expect(results.steps[1].outputs.iterations).toBe(3) + expect(results.steps[1].outputs.items).toHaveLength(3) + + expect(results.steps[2].outputs.rows).toHaveLength(3) + + rows.forEach(expectedRow => { + expect(results.steps[2].outputs.rows).toEqual( + expect.arrayContaining([expect.objectContaining(expectedRow)]) + ) + }) + }) }) diff --git a/packages/server/src/automations/tests/utilities/AutomationBuilder.ts b/packages/server/src/automations/tests/utilities/AutomationBuilder.ts index 49f60795d1..0598d69c17 100644 --- a/packages/server/src/automations/tests/utilities/AutomationBuilder.ts +++ b/packages/server/src/automations/tests/utilities/AutomationBuilder.ts @@ -28,6 +28,7 @@ import { SmtpEmailStepInputs, ExecuteQueryStepInputs, QueryRowsStepInputs, + ServerLogStepInputs, } from "@budibase/types" import {} from "../../steps/loop" import TestConfiguration from "../../../tests/utilities/TestConfiguration" @@ -119,6 +120,10 @@ class AutomationBuilder { return this.step(BUILTIN_ACTION_DEFINITIONS.LOOP, inputs) } + serverLog(input: ServerLogStepInputs): this { + return this.step(BUILTIN_ACTION_DEFINITIONS.SERVER_LOG, input) + } + private trigger( triggerSchema: AutomationTriggerSchema, inputs?: T, diff --git a/packages/server/src/automations/tests/utilities/index.ts b/packages/server/src/automations/tests/utilities/index.ts index 7952f7a80b..8e3bbfe409 100644 --- a/packages/server/src/automations/tests/utilities/index.ts +++ b/packages/server/src/automations/tests/utilities/index.ts @@ -3,7 +3,14 @@ import { context } from "@budibase/backend-core" import { BUILTIN_ACTION_DEFINITIONS, getAction } from "../../actions" import emitter from "../../../events/index" import env from "../../../environment" -import { AutomationActionStepId } from "@budibase/types" +import { AutomationActionStepId, Datasource } from "@budibase/types" +import { Knex } from "knex" +import { generator } from "@budibase/backend-core/tests" +import { + getDatasource, + knexClient, + DatabaseName, +} from "../../../integrations/tests/utils" let config: TestConfig @@ -57,5 +64,58 @@ export async function runStep(stepId: string, inputs: any, stepContext?: any) { } } +export async function createTestTable(client: Knex, schema: any) { + const tableName = generator.guid() + await client.schema.createTable(tableName, table => { + for (const fieldName in schema) { + const field = schema[fieldName] + if (field.type === "string") { + table.string(fieldName) + } else if (field.type === "number") { + table.integer(fieldName) + } + } + }) + return tableName +} + +export async function insertTestData( + client: Knex, + tableName: string, + rows: any[] +) { + await client(tableName).insert(rows) +} + +export async function saveTestQuery( + config: TestConfig, + client: Knex, + tableName: string, + datasource: Datasource +) { + return 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", + }) +} + +export async function setupTestDatasource( + config: TestConfig, + dbName: DatabaseName +) { + const db = await getDatasource(dbName) + const datasource = await config.api.datasource.create(db) + const client = await knexClient(db) + return { datasource, client } +} + export const apiKey = "test" export const actions = BUILTIN_ACTION_DEFINITIONS diff --git a/packages/types/src/documents/app/automation/schema.ts b/packages/types/src/documents/app/automation/schema.ts index 0da82f2f6e..2c7af3e463 100644 --- a/packages/types/src/documents/app/automation/schema.ts +++ b/packages/types/src/documents/app/automation/schema.ts @@ -98,6 +98,24 @@ export type ActionImplementations = { } : {}) +export type AutomationStepOutputs = + | CollectStepOutputs + | CreateRowStepOutputs + | DelayStepOutputs + | DeleteRowStepOutputs + | ExecuteQueryStepOutputs + | ExecuteScriptStepOutputs + | FilterStepOutputs + | QueryRowsStepOutputs + | BaseAutomationOutputs + | BashStepOutputs + | ExternalAppStepOutputs + | OpenAIStepOutputs + | ServerLogStepOutputs + | TriggerAutomationStepOutputs + | UpdateRowStepOutputs + | ZapierStepOutputs + export type BaseAutomationOutputs = { success?: boolean response?: { @@ -199,7 +217,7 @@ export type LoopStepInputs = { } export type LoopStepOutputs = { - items: string + items: AutomationStepOutputs[] success: boolean iterations: number }