Merge branch 'master' of github.com:Budibase/budibase into fix/sql-pagination-fixes

This commit is contained in:
mike12345567 2024-08-01 11:39:01 +01:00
commit f0da4b0366
54 changed files with 1292 additions and 136 deletions

View File

@ -9,7 +9,10 @@ import { Constants } from "@budibase/frontend-core"
const { TypeIconMap } = Constants const { TypeIconMap } = Constants
export { RelationshipType } from "@budibase/types" export {
RelationshipType,
RowExportFormat as ROW_EXPORT_FORMATS,
} from "@budibase/types"
export const AUTO_COLUMN_SUB_TYPES = AutoFieldSubType export const AUTO_COLUMN_SUB_TYPES = AutoFieldSubType
@ -307,9 +310,3 @@ export const DatasourceTypes = {
GRAPH: "Graph", GRAPH: "Graph",
API: "API", API: "API",
} }
export const ROW_EXPORT_FORMATS = {
CSV: "csv",
JSON: "json",
JSON_WITH_SCHEMA: "jsonWithSchema",
}

View File

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/mssql/server:2022-latest FROM mcr.microsoft.com/mssql/server@sha256:c4369c38385eba011c10906dc8892425831275bb035d5ce69656da8e29de50d8
ENV ACCEPT_EULA=Y ENV ACCEPT_EULA=Y
ENV SA_PASSWORD=Passw0rd ENV SA_PASSWORD=Passw0rd

View File

@ -17,6 +17,7 @@ import {
CsvToJsonRequest, CsvToJsonRequest,
CsvToJsonResponse, CsvToJsonResponse,
FetchTablesResponse, FetchTablesResponse,
FieldType,
MigrateRequest, MigrateRequest,
MigrateResponse, MigrateResponse,
SaveTableRequest, SaveTableRequest,
@ -178,9 +179,17 @@ export async function validateExistingTableImport(
const { rows, tableId } = ctx.request.body const { rows, tableId } = ctx.request.body
let schema = null let schema = null
if (tableId) { if (tableId) {
const table = await sdk.tables.getTable(tableId) const table = await sdk.tables.getTable(tableId)
schema = table.schema schema = table.schema
if (!isExternalTable(table)) {
schema._id = {
name: "_id",
type: FieldType.STRING,
}
}
} else { } else {
ctx.status = 422 ctx.status = 422
return return

View File

@ -3,6 +3,7 @@ import { handleDataImport } from "./utils"
import { import {
BulkImportRequest, BulkImportRequest,
BulkImportResponse, BulkImportResponse,
FieldType,
RenameColumn, RenameColumn,
SaveTableRequest, SaveTableRequest,
SaveTableResponse, SaveTableResponse,
@ -69,10 +70,22 @@ export async function bulkImport(
) { ) {
const table = await sdk.tables.getTable(ctx.params.tableId) const table = await sdk.tables.getTable(ctx.params.tableId)
const { rows, identifierFields } = ctx.request.body const { rows, identifierFields } = ctx.request.body
await handleDataImport(table, { await handleDataImport(
{
...table,
schema: {
_id: {
name: "_id",
type: FieldType.STRING,
},
...table.schema,
},
},
{
importRows: rows, importRows: rows,
identifierFields, identifierFields,
user: ctx.user, user: ctx.user,
}) }
)
return table return table
} }

View File

@ -122,13 +122,15 @@ export function makeSureTableUpToDate(table: Table, tableToSave: Table) {
export async function importToRows( export async function importToRows(
data: Row[], data: Row[],
table: Table, table: Table,
user?: ContextUser user?: ContextUser,
opts?: { keepCouchId: boolean }
) { ) {
let originalTable = table const originalTable = table
let finalData: any = [] const finalData: Row[] = []
const keepCouchId = !!opts?.keepCouchId
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
let row = data[i] let row = data[i]
row._id = generateRowID(table._id!) row._id = (keepCouchId && row._id) || generateRowID(table._id!)
row.type = "row" row.type = "row"
row.tableId = table._id row.tableId = table._id
@ -180,7 +182,11 @@ export async function handleDataImport(
const db = context.getAppDB() const db = context.getAppDB()
const data = parse(importRows, table) const data = parse(importRows, table)
let finalData: any = await importToRows(data, table, user) const finalData = await importToRows(data, table, user, {
keepCouchId: identifierFields.includes("_id"),
})
let newRowCount = finalData.length
//Set IDs of finalData to match existing row if an update is expected //Set IDs of finalData to match existing row if an update is expected
if (identifierFields.length > 0) { if (identifierFields.length > 0) {
@ -203,12 +209,14 @@ export async function handleDataImport(
if (match) { if (match) {
finalItem._id = doc._id finalItem._id = doc._id
finalItem._rev = doc._rev finalItem._rev = doc._rev
newRowCount--
} }
}) })
}) })
} }
await quotas.addRows(finalData.length, () => db.bulkDocs(finalData), { await quotas.addRows(newRowCount, () => db.bulkDocs(finalData), {
tableId: table._id, tableId: table._id,
}) })

View File

@ -1,4 +1,6 @@
import { Row, TableSchema } from "@budibase/types" import { Row, RowExportFormat, TableSchema } from "@budibase/types"
export { RowExportFormat as Format } from "@budibase/types"
function getHeaders( function getHeaders(
headers: string[], headers: string[],
@ -46,14 +48,8 @@ export function jsonWithSchema(schema: TableSchema, rows: Row[]) {
return JSON.stringify({ schema: newSchema, rows }, undefined, 2) return JSON.stringify({ schema: newSchema, rows }, undefined, 2)
} }
export enum Format { export function isFormat(format: any): format is RowExportFormat {
CSV = "csv", return Object.values(RowExportFormat).includes(format as RowExportFormat)
JSON = "json",
JSON_WITH_SCHEMA = "jsonWithSchema",
}
export function isFormat(format: any): format is Format {
return Object.values(Format).includes(format as Format)
} }
export function parseCsvExport<T>(value: string) { export function parseCsvExport<T>(value: string) {

View File

@ -1298,6 +1298,113 @@ describe.each([
await assertRowUsage(isInternal ? rowUsage + 2 : rowUsage) await assertRowUsage(isInternal ? rowUsage + 2 : rowUsage)
}) })
isInternal &&
it("should be able to update existing rows on bulkImport", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
name: {
type: FieldType.STRING,
name: "name",
},
description: {
type: FieldType.STRING,
name: "description",
},
},
})
)
const existingRow = await config.api.row.save(table._id!, {
name: "Existing row",
description: "Existing description",
})
const rowUsage = await getRowUsage()
await config.api.row.bulkImport(table._id!, {
rows: [
{
name: "Row 1",
description: "Row 1 description",
},
{ ...existingRow, name: "Updated existing row" },
{
name: "Row 2",
description: "Row 2 description",
},
],
identifierFields: ["_id"],
})
const rows = await config.api.row.fetch(table._id!)
expect(rows.length).toEqual(3)
rows.sort((a, b) => a.name.localeCompare(b.name))
expect(rows[0].name).toEqual("Row 1")
expect(rows[0].description).toEqual("Row 1 description")
expect(rows[1].name).toEqual("Row 2")
expect(rows[1].description).toEqual("Row 2 description")
expect(rows[2].name).toEqual("Updated existing row")
expect(rows[2].description).toEqual("Existing description")
await assertRowUsage(rowUsage + 2)
})
isInternal &&
it("should create new rows if not identifierFields are provided", async () => {
const table = await config.api.table.save(
saveTableRequest({
schema: {
name: {
type: FieldType.STRING,
name: "name",
},
description: {
type: FieldType.STRING,
name: "description",
},
},
})
)
const existingRow = await config.api.row.save(table._id!, {
name: "Existing row",
description: "Existing description",
})
const rowUsage = await getRowUsage()
await config.api.row.bulkImport(table._id!, {
rows: [
{
name: "Row 1",
description: "Row 1 description",
},
{ ...existingRow, name: "Updated existing row" },
{
name: "Row 2",
description: "Row 2 description",
},
],
})
const rows = await config.api.row.fetch(table._id!)
expect(rows.length).toEqual(4)
rows.sort((a, b) => a.name.localeCompare(b.name))
expect(rows[0].name).toEqual("Existing row")
expect(rows[0].description).toEqual("Existing description")
expect(rows[1].name).toEqual("Row 1")
expect(rows[1].description).toEqual("Row 1 description")
expect(rows[2].name).toEqual("Row 2")
expect(rows[2].description).toEqual("Row 2 description")
expect(rows[3].name).toEqual("Updated existing row")
expect(rows[3].description).toEqual("Existing description")
await assertRowUsage(rowUsage + 3)
})
// Upserting isn't yet supported in MSSQL, see: // Upserting isn't yet supported in MSSQL, see:
// https://github.com/knex/knex/pull/6050 // https://github.com/knex/knex/pull/6050
!isMSSQL && !isMSSQL &&
@ -1640,6 +1747,23 @@ describe.each([
table = await config.api.table.save(defaultTable()) table = await config.api.table.save(defaultTable())
}) })
isInternal &&
it("should not export internal couchdb fields", async () => {
const existing = await config.api.row.save(table._id!, {
name: generator.guid(),
description: generator.paragraph(),
})
const res = await config.api.row.exportRows(table._id!, {
rows: [existing._id!],
})
const results = JSON.parse(res)
expect(results.length).toEqual(1)
const row = results[0]
expect(Object.keys(row)).toEqual(["_id", "name", "description"])
})
!isInternal &&
it("should allow exporting all columns", async () => { it("should allow exporting all columns", async () => {
const existing = await config.api.row.save(table._id!, {}) const existing = await config.api.row.save(table._id!, {})
const res = await config.api.row.exportRows(table._id!, { const res = await config.api.row.exportRows(table._id!, {
@ -1650,9 +1774,7 @@ describe.each([
const row = results[0] const row = results[0]
// Ensure all original columns were exported // Ensure all original columns were exported
expect(Object.keys(row).length).toBeGreaterThanOrEqual( expect(Object.keys(row).length).toBe(Object.keys(existing).length)
Object.keys(existing).length
)
Object.keys(existing).forEach(key => { Object.keys(existing).forEach(key => {
expect(row[key]).toEqual(existing[key]) expect(row[key]).toEqual(existing[key])
}) })

View File

@ -1,4 +1,4 @@
import { context, events } from "@budibase/backend-core" import { context, docIds, events } from "@budibase/backend-core"
import { import {
AutoFieldSubType, AutoFieldSubType,
BBReferenceFieldSubType, BBReferenceFieldSubType,
@ -10,6 +10,7 @@ import {
Row, Row,
SaveTableRequest, SaveTableRequest,
Table, Table,
TableSchema,
TableSourceType, TableSourceType,
User, User,
ViewCalculation, ViewCalculation,
@ -1022,4 +1023,92 @@ describe.each([
}) })
}) })
}) })
describe("import validation", () => {
const basicSchema: TableSchema = {
id: {
type: FieldType.NUMBER,
name: "id",
},
name: {
type: FieldType.STRING,
name: "name",
},
}
describe("validateNewTableImport", () => {
it("can validate basic imports", async () => {
const result = await config.api.table.validateNewTableImport(
[{ id: generator.natural(), name: generator.first() }],
basicSchema
)
expect(result).toEqual({
allValid: true,
errors: {},
invalidColumns: [],
schemaValidation: {
id: true,
name: true,
},
})
})
})
describe("validateExistingTableImport", () => {
it("can validate basic imports", async () => {
const table = await config.api.table.save(
tableForDatasource(datasource, {
primary: ["id"],
schema: basicSchema,
})
)
const result = await config.api.table.validateExistingTableImport({
tableId: table._id,
rows: [{ id: generator.natural(), name: generator.first() }],
})
expect(result).toEqual({
allValid: true,
errors: {},
invalidColumns: [],
schemaValidation: {
id: true,
name: true,
},
})
})
isInternal &&
it("can reimport _id fields for internal tables", async () => {
const table = await config.api.table.save(
tableForDatasource(datasource, {
primary: ["id"],
schema: basicSchema,
})
)
const result = await config.api.table.validateExistingTableImport({
tableId: table._id,
rows: [
{
_id: docIds.generateRowID(table._id!),
id: generator.natural(),
name: generator.first(),
},
],
})
expect(result).toEqual({
allValid: true,
errors: {},
invalidColumns: [],
schemaValidation: {
_id: true,
id: true,
name: true,
},
})
})
})
})
}) })

View File

@ -20,17 +20,21 @@ import * as triggerAutomationRun from "./steps/triggerAutomationRun"
import env from "../environment" import env from "../environment"
import { import {
AutomationStepSchema, AutomationStepSchema,
AutomationStepInput,
PluginType, PluginType,
AutomationStep, AutomationStep,
AutomationActionStepId,
ActionImplementations,
Hosting,
ActionImplementation,
} from "@budibase/types" } from "@budibase/types"
import sdk from "../sdk" import sdk from "../sdk"
import { getAutomationPlugin } from "../utilities/fileSystem" import { getAutomationPlugin } from "../utilities/fileSystem"
const ACTION_IMPLS: Record< type ActionImplType = ActionImplementations<
string, typeof env.SELF_HOSTED extends "true" ? Hosting.SELF : Hosting.CLOUD
(opts: AutomationStepInput) => Promise<any> >
> = {
const ACTION_IMPLS: ActionImplType = {
SEND_EMAIL_SMTP: sendSmtpEmail.run, SEND_EMAIL_SMTP: sendSmtpEmail.run,
CREATE_ROW: createRow.run, CREATE_ROW: createRow.run,
UPDATE_ROW: updateRow.run, UPDATE_ROW: updateRow.run,
@ -51,6 +55,7 @@ const ACTION_IMPLS: Record<
integromat: make.run, integromat: make.run,
n8n: n8n.run, n8n: n8n.run,
} }
export const BUILTIN_ACTION_DEFINITIONS: Record<string, AutomationStepSchema> = export const BUILTIN_ACTION_DEFINITIONS: Record<string, AutomationStepSchema> =
{ {
SEND_EMAIL_SMTP: sendSmtpEmail.definition, SEND_EMAIL_SMTP: sendSmtpEmail.definition,
@ -86,7 +91,7 @@ if (env.SELF_HOSTED) {
ACTION_IMPLS["EXECUTE_BASH"] = bash.run ACTION_IMPLS["EXECUTE_BASH"] = bash.run
// @ts-ignore // @ts-ignore
BUILTIN_ACTION_DEFINITIONS["EXECUTE_BASH"] = bash.definition BUILTIN_ACTION_DEFINITIONS["EXECUTE_BASH"] = bash.definition
// @ts-ignore
ACTION_IMPLS.OPENAI = openai.run ACTION_IMPLS.OPENAI = openai.run
BUILTIN_ACTION_DEFINITIONS.OPENAI = openai.definition BUILTIN_ACTION_DEFINITIONS.OPENAI = openai.definition
} }
@ -107,10 +112,13 @@ export async function getActionDefinitions() {
} }
/* istanbul ignore next */ /* istanbul ignore next */
export async function getAction(stepId: string) { export async function getAction(
if (ACTION_IMPLS[stepId] != null) { stepId: AutomationActionStepId
return ACTION_IMPLS[stepId] ): Promise<ActionImplementation<any, any> | undefined> {
if (ACTION_IMPLS[stepId as keyof ActionImplType] != null) {
return ACTION_IMPLS[stepId as keyof ActionImplType]
} }
// must be a plugin // must be a plugin
if (env.SELF_HOSTED) { if (env.SELF_HOSTED) {
const plugins = await sdk.plugins.fetch(PluginType.AUTOMATION) const plugins = await sdk.plugins.fetch(PluginType.AUTOMATION)

View File

@ -4,8 +4,13 @@ import {
encodeJSBinding, encodeJSBinding,
} from "@budibase/string-templates" } from "@budibase/string-templates"
import sdk from "../sdk" import sdk from "../sdk"
import { AutomationAttachment, FieldType, Row } from "@budibase/types" import {
import { LoopInput, LoopStepType } from "../definitions/automations" AutomationAttachment,
FieldType,
Row,
LoopStepType,
} from "@budibase/types"
import { LoopInput } from "../definitions/automations"
import { objectStore, context } from "@budibase/backend-core" import { objectStore, context } from "@budibase/backend-core"
import * as uuid from "uuid" import * as uuid from "uuid"
import path from "path" import path from "path"

View File

@ -7,9 +7,10 @@ import {
AutomationCustomIOType, AutomationCustomIOType,
AutomationFeature, AutomationFeature,
AutomationIOType, AutomationIOType,
AutomationStepInput,
AutomationStepSchema, AutomationStepSchema,
AutomationStepType, AutomationStepType,
BashStepInputs,
BashStepOutputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
@ -51,7 +52,13 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs, context }: AutomationStepInput) { export async function run({
inputs,
context,
}: {
inputs: BashStepInputs
context: object
}): Promise<BashStepOutputs> {
if (inputs.code == null) { if (inputs.code == null) {
return { return {
stdout: "Budibase bash automation failed: Invalid inputs", stdout: "Budibase bash automation failed: Invalid inputs",

View File

@ -1,9 +1,10 @@
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationStepSchema, AutomationStepSchema,
AutomationStepInput,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
CollectStepInputs,
CollectStepOutputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
@ -43,7 +44,11 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({
inputs,
}: {
inputs: CollectStepInputs
}): Promise<CollectStepOutputs> {
if (!inputs.collection) { if (!inputs.collection) {
return { return {
success: false, success: false,

View File

@ -10,10 +10,12 @@ import {
AutomationCustomIOType, AutomationCustomIOType,
AutomationFeature, AutomationFeature,
AutomationIOType, AutomationIOType,
AutomationStepInput,
AutomationStepSchema, AutomationStepSchema,
AutomationStepType, AutomationStepType,
CreateRowStepInputs,
CreateRowStepOutputs,
} from "@budibase/types" } from "@budibase/types"
import { EventEmitter } from "events"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
name: "Create Row", name: "Create Row",
@ -74,7 +76,15 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs, appId, emitter }: AutomationStepInput) { export async function run({
inputs,
appId,
emitter,
}: {
inputs: CreateRowStepInputs
appId: string
emitter: EventEmitter
}): Promise<CreateRowStepOutputs> {
if (inputs.row == null || inputs.row.tableId == null) { if (inputs.row == null || inputs.row.tableId == null) {
return { return {
success: false, success: false,
@ -93,7 +103,7 @@ export async function run({ inputs, appId, emitter }: AutomationStepInput) {
try { try {
inputs.row = await cleanUpRow(inputs.row.tableId, inputs.row) inputs.row = await cleanUpRow(inputs.row.tableId, inputs.row)
inputs.row = await sendAutomationAttachmentsToStorage( inputs.row = await sendAutomationAttachmentsToStorage(
inputs.row.tableId, inputs.row.tableId!,
inputs.row inputs.row
) )
await save(ctx) await save(ctx)

View File

@ -2,9 +2,10 @@ import { wait } from "../../utilities"
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationIOType, AutomationIOType,
AutomationStepInput,
AutomationStepSchema, AutomationStepSchema,
AutomationStepType, AutomationStepType,
DelayStepInputs,
DelayStepOutputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
@ -39,7 +40,11 @@ export const definition: AutomationStepSchema = {
type: AutomationStepType.LOGIC, type: AutomationStepType.LOGIC,
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({
inputs,
}: {
inputs: DelayStepInputs
}): Promise<DelayStepOutputs> {
await wait(inputs.time) await wait(inputs.time)
return { return {
success: true, success: true,

View File

@ -1,14 +1,16 @@
import { EventEmitter } from "events"
import { destroy } from "../../api/controllers/row" import { destroy } from "../../api/controllers/row"
import { buildCtx } from "./utils" import { buildCtx } from "./utils"
import { getError } from "../automationUtils" import { getError } from "../automationUtils"
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationStepInput,
AutomationStepSchema, AutomationStepSchema,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
AutomationCustomIOType, AutomationCustomIOType,
AutomationFeature, AutomationFeature,
DeleteRowStepInputs,
DeleteRowStepOutputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
@ -59,7 +61,15 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs, appId, emitter }: AutomationStepInput) { export async function run({
inputs,
appId,
emitter,
}: {
inputs: DeleteRowStepInputs
appId: string
emitter: EventEmitter
}): Promise<DeleteRowStepOutputs> {
if (inputs.id == null) { if (inputs.id == null) {
return { return {
success: false, success: false,

View File

@ -3,10 +3,11 @@ import { getFetchResponse } from "./utils"
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationStepSchema, AutomationStepSchema,
AutomationStepInput,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
AutomationFeature, AutomationFeature,
ExternalAppStepOutputs,
DiscordStepInputs,
} from "@budibase/types" } from "@budibase/types"
const DEFAULT_USERNAME = "Budibase Automate" const DEFAULT_USERNAME = "Budibase Automate"
@ -65,7 +66,11 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({
inputs,
}: {
inputs: DiscordStepInputs
}): Promise<ExternalAppStepOutputs> {
let { url, username, avatar_url, content } = inputs let { url, username, avatar_url, content } = inputs
if (!username) { if (!username) {
username = DEFAULT_USERNAME username = DEFAULT_USERNAME

View File

@ -1,3 +1,4 @@
import { EventEmitter } from "events"
import * as queryController from "../../api/controllers/query" import * as queryController from "../../api/controllers/query"
import { buildCtx } from "./utils" import { buildCtx } from "./utils"
import * as automationUtils from "../automationUtils" import * as automationUtils from "../automationUtils"
@ -6,9 +7,10 @@ import {
AutomationCustomIOType, AutomationCustomIOType,
AutomationFeature, AutomationFeature,
AutomationIOType, AutomationIOType,
AutomationStepInput,
AutomationStepSchema, AutomationStepSchema,
AutomationStepType, AutomationStepType,
ExecuteQueryStepInputs,
ExecuteQueryStepOutputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
@ -62,7 +64,15 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs, appId, emitter }: AutomationStepInput) { export async function run({
inputs,
appId,
emitter,
}: {
inputs: ExecuteQueryStepInputs
appId: string
emitter: EventEmitter
}): Promise<ExecuteQueryStepOutputs> {
if (inputs.query == null) { if (inputs.query == null) {
return { return {
success: false, success: false,

View File

@ -6,10 +6,12 @@ import {
AutomationCustomIOType, AutomationCustomIOType,
AutomationFeature, AutomationFeature,
AutomationIOType, AutomationIOType,
AutomationStepInput,
AutomationStepSchema, AutomationStepSchema,
AutomationStepType, AutomationStepType,
ExecuteScriptStepInputs,
ExecuteScriptStepOutputs,
} from "@budibase/types" } from "@budibase/types"
import { EventEmitter } from "events"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
name: "JS Scripting", name: "JS Scripting",
@ -55,7 +57,12 @@ export async function run({
appId, appId,
context, context,
emitter, emitter,
}: AutomationStepInput) { }: {
inputs: ExecuteScriptStepInputs
appId: string
context: object
emitter: EventEmitter
}): Promise<ExecuteScriptStepOutputs> {
if (inputs.code == null) { if (inputs.code == null) {
return { return {
success: false, success: false,

View File

@ -1,9 +1,10 @@
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationStepSchema, AutomationStepSchema,
AutomationStepInput,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
FilterStepInputs,
FilterStepOutputs,
} from "@budibase/types" } from "@budibase/types"
export const FilterConditions = { export const FilterConditions = {
@ -69,7 +70,11 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({
inputs,
}: {
inputs: FilterStepInputs
}): Promise<FilterStepOutputs> {
try { try {
let { field, condition, value } = inputs let { field, condition, value } = inputs
// coerce types so that we can use them // coerce types so that we can use them

View File

@ -3,10 +3,11 @@ import { getFetchResponse } from "./utils"
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationStepSchema, AutomationStepSchema,
AutomationStepInput,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
AutomationFeature, AutomationFeature,
ExternalAppStepOutputs,
MakeIntegrationInputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
@ -57,7 +58,11 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({
inputs,
}: {
inputs: MakeIntegrationInputs
}): Promise<ExternalAppStepOutputs> {
const { url, body } = inputs const { url, body } = inputs
let payload = {} let payload = {}

View File

@ -3,11 +3,12 @@ import { getFetchResponse } from "./utils"
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationStepSchema, AutomationStepSchema,
AutomationStepInput,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
AutomationFeature, AutomationFeature,
HttpMethod, HttpMethod,
ExternalAppStepOutputs,
n8nStepInputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
@ -67,7 +68,11 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({
inputs,
}: {
inputs: n8nStepInputs
}): Promise<ExternalAppStepOutputs> {
const { url, body, method, authorization } = inputs const { url, body, method, authorization } = inputs
let payload = {} let payload = {}

View File

@ -3,9 +3,10 @@ import { OpenAI } from "openai"
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationStepSchema, AutomationStepSchema,
AutomationStepInput,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
OpenAIStepInputs,
OpenAIStepOutputs,
} from "@budibase/types" } from "@budibase/types"
import { env } from "@budibase/backend-core" import { env } from "@budibase/backend-core"
import * as automationUtils from "../automationUtils" import * as automationUtils from "../automationUtils"
@ -59,7 +60,11 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({
inputs,
}: {
inputs: OpenAIStepInputs
}): Promise<OpenAIStepOutputs> {
if (!env.OPENAI_API_KEY) { if (!env.OPENAI_API_KEY) {
return { return {
success: false, success: false,

View File

@ -6,9 +6,10 @@ import {
AutomationCustomIOType, AutomationCustomIOType,
AutomationFeature, AutomationFeature,
AutomationIOType, AutomationIOType,
AutomationStepInput,
AutomationStepSchema, AutomationStepSchema,
AutomationStepType, AutomationStepType,
ExternalAppStepOutputs,
OutgoingWebhookStepInputs,
} from "@budibase/types" } from "@budibase/types"
enum RequestType { enum RequestType {
@ -88,7 +89,13 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({
inputs,
}: {
inputs: OutgoingWebhookStepInputs
}): Promise<
Omit<ExternalAppStepOutputs, "httpStatus"> | ExternalAppStepOutputs
> {
let { requestMethod, url, requestBody, headers } = inputs let { requestMethod, url, requestBody, headers } = inputs
if (!url.startsWith("http")) { if (!url.startsWith("http")) {
url = `http://${url}` url = `http://${url}`

View File

@ -8,13 +8,14 @@ import {
AutomationCustomIOType, AutomationCustomIOType,
AutomationFeature, AutomationFeature,
AutomationIOType, AutomationIOType,
AutomationStepInput,
AutomationStepSchema, AutomationStepSchema,
AutomationStepType, AutomationStepType,
EmptyFilterOption, EmptyFilterOption,
SearchFilters, SearchFilters,
Table, Table,
SortOrder, SortOrder,
QueryRowsStepInputs,
QueryRowsStepOutputs,
} from "@budibase/types" } from "@budibase/types"
import { db as dbCore } from "@budibase/backend-core" import { db as dbCore } from "@budibase/backend-core"
@ -133,7 +134,13 @@ function hasNullFilters(filters: any[]) {
) )
} }
export async function run({ inputs, appId }: AutomationStepInput) { export async function run({
inputs,
appId,
}: {
inputs: QueryRowsStepInputs
appId: string
}): Promise<QueryRowsStepOutputs> {
const { tableId, filters, sortColumn, sortOrder, limit } = inputs const { tableId, filters, sortColumn, sortOrder, limit } = inputs
if (!tableId) { if (!tableId) {
return { return {
@ -145,7 +152,7 @@ export async function run({ inputs, appId }: AutomationStepInput) {
} }
const table = await getTable(appId, tableId) const table = await getTable(appId, tableId)
let sortType = FieldType.STRING let sortType = FieldType.STRING
if (table && table.schema && table.schema[sortColumn] && sortColumn) { if (sortColumn && table && table.schema && table.schema[sortColumn]) {
const fieldType = table.schema[sortColumn].type const fieldType = table.schema[sortColumn].type
sortType = sortType =
fieldType === FieldType.NUMBER ? FieldType.NUMBER : FieldType.STRING fieldType === FieldType.NUMBER ? FieldType.NUMBER : FieldType.STRING

View File

@ -3,11 +3,12 @@ import * as automationUtils from "../automationUtils"
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationStepSchema, AutomationStepSchema,
AutomationStepInput,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
AutomationFeature, AutomationFeature,
AutomationCustomIOType, AutomationCustomIOType,
SmtpEmailStepInputs,
BaseAutomationOutputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
@ -97,7 +98,11 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({
inputs,
}: {
inputs: SmtpEmailStepInputs
}): Promise<BaseAutomationOutputs> {
let { let {
to, to,
from, from,
@ -116,8 +121,8 @@ export async function run({ inputs }: AutomationStepInput) {
if (!contents) { if (!contents) {
contents = "<h1>No content</h1>" contents = "<h1>No content</h1>"
} }
to = to || undefined
try {
if (attachments) { if (attachments) {
if (Array.isArray(attachments)) { if (Array.isArray(attachments)) {
attachments.forEach(item => automationUtils.guardAttachment(item)) attachments.forEach(item => automationUtils.guardAttachment(item))
@ -126,7 +131,6 @@ export async function run({ inputs }: AutomationStepInput) {
} }
} }
try {
let response = await sendSmtpEmail({ let response = await sendSmtpEmail({
to, to,
from, from,

View File

@ -1,10 +1,11 @@
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationStepSchema, AutomationStepSchema,
AutomationStepInput,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
AutomationFeature, AutomationFeature,
ServerLogStepInputs,
ServerLogStepOutputs,
} from "@budibase/types" } from "@budibase/types"
/** /**
@ -53,7 +54,13 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs, appId }: AutomationStepInput) { export async function run({
inputs,
appId,
}: {
inputs: ServerLogStepInputs
appId: string
}): Promise<ServerLogStepOutputs> {
const message = `App ${appId} - ${inputs.text}` const message = `App ${appId} - ${inputs.text}`
console.log(message) console.log(message)
return { return {

View File

@ -3,10 +3,11 @@ import { getFetchResponse } from "./utils"
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationStepSchema, AutomationStepSchema,
AutomationStepInput,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
AutomationFeature, AutomationFeature,
ExternalAppStepOutputs,
SlackStepInputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
@ -54,7 +55,11 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({
inputs,
}: {
inputs: SlackStepInputs
}): Promise<ExternalAppStepOutputs> {
let { url, text } = inputs let { url, text } = inputs
if (!url?.trim()?.length) { if (!url?.trim()?.length) {
return { return {

View File

@ -1,12 +1,13 @@
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationStepSchema, AutomationStepSchema,
AutomationStepInput,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
AutomationResults, AutomationResults,
Automation, Automation,
AutomationCustomIOType, AutomationCustomIOType,
TriggerAutomationStepInputs,
TriggerAutomationStepOutputs,
} from "@budibase/types" } from "@budibase/types"
import * as triggers from "../triggers" import * as triggers from "../triggers"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
@ -61,7 +62,11 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({
inputs,
}: {
inputs: TriggerAutomationStepInputs
}): Promise<TriggerAutomationStepOutputs> {
const { automationId, ...fieldParams } = inputs.automation const { automationId, ...fieldParams } = inputs.automation
if (await features.isTriggerAutomationRunEnabled()) { if (await features.isTriggerAutomationRunEnabled()) {
@ -88,5 +93,9 @@ export async function run({ inputs }: AutomationStepInput) {
value: response.steps, value: response.steps,
} }
} }
} else {
return {
success: false,
}
} }
} }

View File

@ -1,3 +1,4 @@
import { EventEmitter } from "events"
import * as rowController from "../../api/controllers/row" import * as rowController from "../../api/controllers/row"
import * as automationUtils from "../automationUtils" import * as automationUtils from "../automationUtils"
import { buildCtx } from "./utils" import { buildCtx } from "./utils"
@ -6,9 +7,10 @@ import {
AutomationCustomIOType, AutomationCustomIOType,
AutomationFeature, AutomationFeature,
AutomationIOType, AutomationIOType,
AutomationStepInput,
AutomationStepSchema, AutomationStepSchema,
AutomationStepType, AutomationStepType,
UpdateRowStepInputs,
UpdateRowStepOutputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
@ -70,8 +72,15 @@ export const definition: AutomationStepSchema = {
}, },
}, },
} }
export async function run({
export async function run({ inputs, appId, emitter }: AutomationStepInput) { inputs,
appId,
emitter,
}: {
inputs: UpdateRowStepInputs
appId: string
emitter: EventEmitter
}): Promise<UpdateRowStepOutputs> {
if (inputs.rowId == null || inputs.row == null) { if (inputs.rowId == null || inputs.row == null) {
return { return {
success: false, success: false,

View File

@ -3,10 +3,11 @@ import { getFetchResponse } from "./utils"
import { import {
AutomationActionStepId, AutomationActionStepId,
AutomationStepSchema, AutomationStepSchema,
AutomationStepInput,
AutomationStepType, AutomationStepType,
AutomationIOType, AutomationIOType,
AutomationFeature, AutomationFeature,
ZapierStepInputs,
ZapierStepOutputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepSchema = { export const definition: AutomationStepSchema = {
@ -50,7 +51,11 @@ export const definition: AutomationStepSchema = {
}, },
} }
export async function run({ inputs }: AutomationStepInput) { export async function run({
inputs,
}: {
inputs: ZapierStepInputs
}): Promise<ZapierStepOutputs> {
const { url, body } = inputs const { url, body } = inputs
let payload = {} let payload = {}

View File

@ -3,9 +3,9 @@ import * as triggers from "../triggers"
import { loopAutomation } from "../../tests/utilities/structures" import { loopAutomation } from "../../tests/utilities/structures"
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import * as setup from "./utilities" import * as setup from "./utilities"
import { Table } from "@budibase/types" import { Table, LoopStepType } from "@budibase/types"
import * as loopUtils from "../loopUtils" import * as loopUtils from "../loopUtils"
import { LoopInput, LoopStepType } from "../../definitions/automations" import { LoopInput } from "../../definitions/automations"
describe("Attempt to run a basic loop automation", () => { describe("Attempt to run a basic loop automation", () => {
let config = setup.getConfig(), let config = setup.getConfig(),

View File

@ -0,0 +1,160 @@
import * as automation from "../../index"
import * as setup from "../utilities"
import { Table, LoopStepType } from "@budibase/types"
import { createAutomationBuilder } from "../utilities/AutomationBuilder"
describe("Automation Scenarios", () => {
let config = setup.getConfig(),
table: Table
beforeEach(async () => {
await automation.init()
await config.init()
table = await config.createTable()
await config.createRow()
})
afterAll(setup.afterAll)
describe("Loop automations", () => {
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
.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,
},
})
.run()
expect(results.trigger).toBeDefined()
expect(results.steps).toHaveLength(1)
expect(results.steps[0].outputs.iterations).toBe(3)
expect(results.steps[0].outputs.items).toHaveLength(3)
results.steps[0].outputs.items.forEach((output: any, index: number) => {
expect(output).toMatchObject({
success: true,
row: {
name: `Item ${index + 1}`,
description: "Created from loop",
},
})
})
})
})
describe("Row Automations", () => {
it("should trigger an automation which then creates a row", async () => {
const table = await config.createTable()
const builder = createAutomationBuilder({
name: "Test Row Save and Create",
})
const results = await builder
.rowUpdated(
{ tableId: table._id! },
{
row: { name: "Test", description: "TEST" },
id: "1234",
}
)
.createRow({
row: {
name: "{{trigger.row.name}}",
description: "{{trigger.row.description}}",
tableId: table._id,
},
})
.run()
expect(results.steps).toHaveLength(1)
expect(results.steps[0].outputs).toMatchObject({
success: true,
row: {
name: "Test",
description: "TEST",
},
})
})
})
it("should trigger an automation which querys the database", async () => {
const table = await config.createTable()
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: {} })
.queryRows({
tableId: table._id!,
})
.run()
expect(results.steps).toHaveLength(1)
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()
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: {} })
.queryRows({
tableId: table._id!,
})
.deleteRow({
tableId: table._id!,
id: "{{ steps.1.rows.0._id }}",
})
.queryRows({
tableId: table._id!,
})
.run()
expect(results.steps).toHaveLength(3)
expect(results.steps[1].outputs.success).toBeTruthy()
expect(results.steps[2].outputs.rows).toHaveLength(1)
})
})

View File

@ -0,0 +1,174 @@
import { v4 as uuidv4 } from "uuid"
import { testAutomation } from "../../../api/routes/tests/utilities/TestFunctions"
import {
RowCreatedTriggerInputs,
RowCreatedTriggerOutputs,
} from "../../triggerInfo/rowSaved"
import {
RowUpdatedTriggerInputs,
RowUpdatedTriggerOutputs,
} from "../../triggerInfo/rowUpdated"
import {} from "../../steps/createRow"
import { BUILTIN_ACTION_DEFINITIONS } from "../../actions"
import { TRIGGER_DEFINITIONS } from "../../triggers"
import {
RowDeletedTriggerInputs,
RowDeletedTriggerOutputs,
} from "../../triggerInfo/rowDeleted"
import {
AutomationStepSchema,
AutomationTriggerSchema,
LoopStepInputs,
DeleteRowStepInputs,
UpdateRowStepInputs,
CreateRowStepInputs,
Automation,
AutomationTrigger,
AutomationResults,
SmtpEmailStepInputs,
ExecuteQueryStepInputs,
QueryRowsStepInputs,
} from "@budibase/types"
import {} from "../../steps/loop"
import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import * as setup from "../utilities"
import {
AppActionTriggerInputs,
AppActionTriggerOutputs,
} from "../../triggerInfo/app"
import { CronTriggerOutputs } from "../../triggerInfo/cron"
type TriggerOutputs =
| RowCreatedTriggerOutputs
| RowUpdatedTriggerOutputs
| RowDeletedTriggerOutputs
| AppActionTriggerOutputs
| CronTriggerOutputs
| undefined
class AutomationBuilder {
private automationConfig: Automation = {
name: "",
definition: {
steps: [],
trigger: {} as AutomationTrigger,
},
type: "automation",
appId: setup.getConfig().getAppId(),
}
private config: TestConfiguration = setup.getConfig()
private triggerOutputs: TriggerOutputs
private triggerSet: boolean = false
constructor(options: { name?: string } = {}) {
this.automationConfig.name = options.name || `Test Automation ${uuidv4()}`
}
// TRIGGERS
rowSaved(inputs: RowCreatedTriggerInputs, outputs: RowCreatedTriggerOutputs) {
this.triggerOutputs = outputs
return this.trigger(TRIGGER_DEFINITIONS.ROW_SAVED, inputs, outputs)
}
rowUpdated(
inputs: RowUpdatedTriggerInputs,
outputs: RowUpdatedTriggerOutputs
) {
this.triggerOutputs = outputs
return this.trigger(TRIGGER_DEFINITIONS.ROW_UPDATED, inputs, outputs)
}
rowDeleted(
inputs: RowDeletedTriggerInputs,
outputs: RowDeletedTriggerOutputs
) {
this.triggerOutputs = outputs
return this.trigger(TRIGGER_DEFINITIONS.ROW_DELETED, inputs, outputs)
}
appAction(outputs: AppActionTriggerOutputs, inputs?: AppActionTriggerInputs) {
this.triggerOutputs = outputs
return this.trigger(TRIGGER_DEFINITIONS.APP, inputs, outputs)
}
// STEPS
createRow(inputs: CreateRowStepInputs): this {
return this.step(BUILTIN_ACTION_DEFINITIONS.CREATE_ROW, inputs)
}
updateRow(inputs: UpdateRowStepInputs): this {
return this.step(BUILTIN_ACTION_DEFINITIONS.UPDATE_ROW, inputs)
}
deleteRow(inputs: DeleteRowStepInputs): this {
return this.step(BUILTIN_ACTION_DEFINITIONS.DELETE_ROW, inputs)
}
sendSmtpEmail(inputs: SmtpEmailStepInputs): this {
return this.step(BUILTIN_ACTION_DEFINITIONS.SEND_EMAIL_SMTP, inputs)
}
executeQuery(inputs: ExecuteQueryStepInputs): this {
return this.step(BUILTIN_ACTION_DEFINITIONS.EXECUTE_QUERY, inputs)
}
queryRows(inputs: QueryRowsStepInputs): this {
return this.step(BUILTIN_ACTION_DEFINITIONS.QUERY_ROWS, inputs)
}
loop(inputs: LoopStepInputs): this {
return this.step(BUILTIN_ACTION_DEFINITIONS.LOOP, inputs)
}
private trigger<T extends { [key: string]: any }>(
triggerSchema: AutomationTriggerSchema,
inputs?: T,
outputs?: TriggerOutputs
): this {
if (this.triggerSet) {
throw new Error("Only one trigger can be set for an automation.")
}
this.automationConfig.definition.trigger = {
...triggerSchema,
inputs: inputs || {},
id: uuidv4(),
}
this.triggerOutputs = outputs
this.triggerSet = true
return this
}
private step<T extends { [key: string]: any }>(
stepSchema: AutomationStepSchema,
inputs: T
): this {
this.automationConfig.definition.steps.push({
...stepSchema,
inputs,
id: uuidv4(),
})
return this
}
async run() {
const automation = await this.config.createAutomation(this.automationConfig)
const results = await testAutomation(
this.config,
automation,
this.triggerOutputs
)
return this.processResults(results)
}
private processResults(results: { body: AutomationResults }) {
results.body.steps.shift()
return {
trigger: results.body.trigger,
steps: results.body.steps,
}
}
}
export function createAutomationBuilder(options?: { name?: string }) {
return new AutomationBuilder(options)
}

View File

@ -3,6 +3,7 @@ import { context } from "@budibase/backend-core"
import { BUILTIN_ACTION_DEFINITIONS, getAction } from "../../actions" import { BUILTIN_ACTION_DEFINITIONS, getAction } from "../../actions"
import emitter from "../../../events/index" import emitter from "../../../events/index"
import env from "../../../environment" import env from "../../../environment"
import { AutomationActionStepId } from "@budibase/types"
let config: TestConfig let config: TestConfig
@ -33,7 +34,7 @@ export async function runInProd(fn: any) {
export async function runStep(stepId: string, inputs: any, stepContext?: any) { export async function runStep(stepId: string, inputs: any, stepContext?: any) {
async function run() { async function run() {
let step = await getAction(stepId) let step = await getAction(stepId as AutomationActionStepId)
expect(step).toBeDefined() expect(step).toBeDefined()
if (!step) { if (!step) {
throw new Error("No step found") throw new Error("No step found")
@ -41,7 +42,7 @@ export async function runStep(stepId: string, inputs: any, stepContext?: any) {
return step({ return step({
context: stepContext || {}, context: stepContext || {},
inputs, inputs,
appId: config ? config.getAppId() : null, appId: config ? config.getAppId() : "",
// don't really need an API key, mocked out usage quota, not being tested here // don't really need an API key, mocked out usage quota, not being tested here
apiKey, apiKey,
emitter, emitter,

View File

@ -39,3 +39,11 @@ export const definition: AutomationTriggerSchema = {
}, },
type: AutomationStepType.TRIGGER, type: AutomationStepType.TRIGGER,
} }
export type AppActionTriggerInputs = {
fields: object
}
export type AppActionTriggerOutputs = {
fields: object
}

View File

@ -38,3 +38,11 @@ export const definition: AutomationTriggerSchema = {
}, },
type: AutomationStepType.TRIGGER, type: AutomationStepType.TRIGGER,
} }
export type CronTriggerInputs = {
cron: string
}
export type CronTriggerOutputs = {
timestamp: number
}

View File

@ -5,6 +5,7 @@ import {
AutomationTriggerSchema, AutomationTriggerSchema,
AutomationTriggerStepId, AutomationTriggerStepId,
AutomationEventType, AutomationEventType,
Row,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationTriggerSchema = { export const definition: AutomationTriggerSchema = {
@ -39,3 +40,11 @@ export const definition: AutomationTriggerSchema = {
}, },
type: AutomationStepType.TRIGGER, type: AutomationStepType.TRIGGER,
} }
export type RowDeletedTriggerInputs = {
tableId: string
}
export type RowDeletedTriggerOutputs = {
row: Row
}

View File

@ -5,7 +5,9 @@ import {
AutomationTriggerSchema, AutomationTriggerSchema,
AutomationTriggerStepId, AutomationTriggerStepId,
AutomationEventType, AutomationEventType,
Row,
} from "@budibase/types" } from "@budibase/types"
import { SearchFilters } from "aws-sdk/clients/elasticbeanstalk"
export const definition: AutomationTriggerSchema = { export const definition: AutomationTriggerSchema = {
name: "Row Created", name: "Row Created",
@ -52,3 +54,14 @@ export const definition: AutomationTriggerSchema = {
}, },
type: AutomationStepType.TRIGGER, type: AutomationStepType.TRIGGER,
} }
export type RowCreatedTriggerInputs = {
tableId: string
filters?: SearchFilters
}
export type RowCreatedTriggerOutputs = {
row: Row
id: string
revision: string
}

View File

@ -5,6 +5,8 @@ import {
AutomationTriggerSchema, AutomationTriggerSchema,
AutomationTriggerStepId, AutomationTriggerStepId,
AutomationEventType, AutomationEventType,
Row,
SearchFilters,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationTriggerSchema = { export const definition: AutomationTriggerSchema = {
@ -59,3 +61,14 @@ export const definition: AutomationTriggerSchema = {
}, },
type: AutomationStepType.TRIGGER, type: AutomationStepType.TRIGGER,
} }
export type RowUpdatedTriggerInputs = {
tableId: string
filters?: SearchFilters
}
export type RowUpdatedTriggerOutputs = {
row: Row
id: string
revision?: string
}

View File

@ -1,9 +1,9 @@
import { LoopStepType } from "../../definitions/automations"
import { import {
typecastForLooping, typecastForLooping,
cleanInputValues, cleanInputValues,
substituteLoopStep, substituteLoopStep,
} from "../automationUtils" } from "../automationUtils"
import { LoopStepType } from "@budibase/types"
describe("automationUtils", () => { describe("automationUtils", () => {
describe("substituteLoopStep", () => { describe("substituteLoopStep", () => {

View File

@ -651,10 +651,10 @@ export async function buildDefaultDocs() {
return new LinkDocument( return new LinkDocument(
employeeData.table._id!, employeeData.table._id!,
"Jobs", "Jobs",
employeeData.rows[index]._id, employeeData.rows[index]._id!,
jobData.table._id!, jobData.table._id!,
"Assigned", "Assigned",
jobData.rows[index]._id jobData.rows[index]._id!
) )
} }
) )

View File

@ -1,9 +1,8 @@
import { AutomationResults, AutomationStep } from "@budibase/types" import {
AutomationResults,
export enum LoopStepType { AutomationStep,
ARRAY = "Array", LoopStepType,
STRING = "String", } from "@budibase/types"
}
export interface LoopStep extends AutomationStep { export interface LoopStep extends AutomationStep {
inputs: LoopInput inputs: LoopInput

View File

@ -29,6 +29,7 @@ import { getReadableErrorMessage } from "./base/errorMapping"
import sqlServer from "mssql" import sqlServer from "mssql"
import { sql } from "@budibase/backend-core" import { sql } from "@budibase/backend-core"
import { ConfidentialClientApplication } from "@azure/msal-node" import { ConfidentialClientApplication } from "@azure/msal-node"
import env from "../environment"
import { utils } from "@budibase/shared-core" import { utils } from "@budibase/shared-core"
@ -246,6 +247,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
options: { options: {
encrypt, encrypt,
enableArithAbort: true, enableArithAbort: true,
requestTimeout: env.QUERY_THREAD_TIMEOUT,
}, },
} }
if (encrypt) { if (encrypt) {

View File

@ -11,6 +11,7 @@ import {
SearchResponse, SearchResponse,
SortType, SortType,
Table, Table,
TableSchema,
User, User,
} from "@budibase/types" } from "@budibase/types"
import { getGlobalUsersFromMetadata } from "../../../../utilities/global" import { getGlobalUsersFromMetadata } from "../../../../utilities/global"
@ -137,6 +138,9 @@ export async function exportRows(
let rows: Row[] = [] let rows: Row[] = []
let schema = table.schema let schema = table.schema
let headers let headers
result = trimFields(result, schema)
// Filter data to only specified columns if required // Filter data to only specified columns if required
if (columns && columns.length) { if (columns && columns.length) {
for (let i = 0; i < result.length; i++) { for (let i = 0; i < result.length; i++) {
@ -299,3 +303,13 @@ async function getView(db: Database, viewName: string) {
} }
return viewInfo return viewInfo
} }
function trimFields(rows: Row[], schema: TableSchema) {
const allowedFields = ["_id", ...Object.keys(schema)]
const result = rows.map(row =>
Object.keys(row)
.filter(key => allowedFields.includes(key))
.reduce((acc, key) => ({ ...acc, [key]: row[key] }), {} as Row)
)
return result
}

View File

@ -76,7 +76,7 @@ export async function getDatasourceAndQuery(
} }
export function cleanExportRows( export function cleanExportRows(
rows: any[], rows: Row[],
schema: TableSchema, schema: TableSchema,
format: string, format: string,
columns?: string[], columns?: string[],

View File

@ -1,5 +1,5 @@
import { Expectations, TestAPI } from "./base" import { Expectations, TestAPI } from "./base"
import { Row, View, ViewCalculation } from "@budibase/types" import { Row, RowExportFormat, View, ViewCalculation } from "@budibase/types"
export class LegacyViewAPI extends TestAPI { export class LegacyViewAPI extends TestAPI {
get = async ( get = async (
@ -24,7 +24,7 @@ export class LegacyViewAPI extends TestAPI {
export = async ( export = async (
viewName: string, viewName: string,
format: "json" | "csv" | "jsonWithSchema", format: `${RowExportFormat}`,
expectations?: Expectations expectations?: Expectations
) => { ) => {
const response = await this._requestRaw("get", `/api/views/export`, { const response = await this._requestRaw("get", `/api/views/export`, {

View File

@ -3,9 +3,13 @@ import {
BulkImportResponse, BulkImportResponse,
MigrateRequest, MigrateRequest,
MigrateResponse, MigrateResponse,
Row,
SaveTableRequest, SaveTableRequest,
SaveTableResponse, SaveTableResponse,
Table, Table,
TableSchema,
ValidateTableImportRequest,
ValidateTableImportResponse,
} from "@budibase/types" } from "@budibase/types"
import { Expectations, TestAPI } from "./base" import { Expectations, TestAPI } from "./base"
@ -61,8 +65,38 @@ export class TableAPI extends TestAPI {
revId: string, revId: string,
expectations?: Expectations expectations?: Expectations
): Promise<void> => { ): Promise<void> => {
return await this._delete<void>(`/api/tables/${tableId}/${revId}`, { return await this._delete(`/api/tables/${tableId}/${revId}`, {
expectations, expectations,
}) })
} }
validateNewTableImport = async (
rows: Row[],
schema: TableSchema,
expectations?: Expectations
): Promise<ValidateTableImportResponse> => {
return await this._post<ValidateTableImportResponse>(
`/api/tables/validateNewTableImport`,
{
body: {
rows,
schema,
},
expectations,
}
)
}
validateExistingTableImport = async (
body: ValidateTableImportRequest,
expectations?: Expectations
): Promise<ValidateTableImportResponse> => {
return await this._post<ValidateTableImportResponse>(
`/api/tables/validateExistingTableImport`,
{
body,
expectations,
}
)
}
} }

View File

@ -25,8 +25,9 @@ import {
Webhook, Webhook,
WebhookActionType, WebhookActionType,
AutomationEventType, AutomationEventType,
LoopStepType,
} from "@budibase/types" } from "@budibase/types"
import { LoopInput, LoopStepType } from "../../definitions/automations" import { LoopInput } from "../../definitions/automations"
import { merge } from "lodash" import { merge } from "lodash"
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"

View File

@ -16,6 +16,7 @@ import { AutomationErrors, MAX_AUTOMATION_RECURRING_ERRORS } from "../constants"
import { storeLog } from "../automations/logging" import { storeLog } from "../automations/logging"
import { import {
Automation, Automation,
AutomationActionStepId,
AutomationData, AutomationData,
AutomationJob, AutomationJob,
AutomationMetadata, AutomationMetadata,
@ -108,7 +109,7 @@ class Orchestrator {
return triggerOutput return triggerOutput
} }
async getStepFunctionality(stepId: string) { async getStepFunctionality(stepId: AutomationActionStepId) {
let step = await actions.getAction(stepId) let step = await actions.getAction(stepId)
if (step == null) { if (step == null) {
throw `Cannot find automation step by name ${stepId}` throw `Cannot find automation step by name ${stepId}`
@ -422,7 +423,9 @@ class Orchestrator {
continue continue
} }
let stepFn = await this.getStepFunctionality(step.stepId) let stepFn = await this.getStepFunctionality(
step.stepId as AutomationActionStepId
)
let inputs = await processObject(originalStepInput, this._context) let inputs = await processObject(originalStepInput, this._context)
inputs = automationUtils.cleanInputValues( inputs = automationUtils.cleanInputValues(
inputs, inputs,

View File

@ -103,8 +103,8 @@ export async function sendSmtpEmail({
from: string from: string
subject: string subject: string
contents: string contents: string
cc: string cc?: string
bcc: string bcc?: string
automation: boolean automation: boolean
attachments?: EmailAttachment[] attachments?: EmailAttachment[]
invite?: EmailInvite invite?: EmailInvite

View File

@ -1,9 +1,9 @@
import { Document } from "../document" import { Document } from "../../document"
import { EventEmitter } from "events" import { EventEmitter } from "events"
import { User } from "../global" import { User } from "../../global"
import { ReadStream } from "fs" import { ReadStream } from "fs"
import { Row } from "./row" import { Row } from "../row"
import { Table } from "./table" import { Table } from "../table"
export enum AutomationIOType { export enum AutomationIOType {
OBJECT = "object", OBJECT = "object",
@ -93,6 +93,7 @@ export interface EmailAttachment {
} }
export interface SendEmailOpts { export interface SendEmailOpts {
to?: string
// workspaceId If finer grain controls being used then this will lookup config for workspace. // workspaceId If finer grain controls being used then this will lookup config for workspace.
workspaceId?: string workspaceId?: string
// user If sending to an existing user the object can be provided, this is used in the context. // user If sending to an existing user the object can be provided, this is used in the context.
@ -102,7 +103,7 @@ export interface SendEmailOpts {
// contents If sending a custom email then can supply contents which will be added to it. // contents If sending a custom email then can supply contents which will be added to it.
contents?: string contents?: string
// subject A custom subject can be specified if the config one is not desired. // subject A custom subject can be specified if the config one is not desired.
subject?: string subject: string
// info Pass in a structure of information to be stored alongside the invitation. // info Pass in a structure of information to be stored alongside the invitation.
info?: any info?: any
cc?: boolean cc?: boolean
@ -242,14 +243,18 @@ export interface AutomationLogPage {
nextPage?: string nextPage?: string
} }
export type AutomationStepInput = { export interface AutomationStepInputBase {
inputs: Record<string, any>
context: Record<string, any> context: Record<string, any>
emitter: EventEmitter emitter: EventEmitter
appId: string appId: string
apiKey?: string apiKey?: string
} }
export type ActionImplementation<TInputs, TOutputs> = (
params: {
inputs: TInputs
} & AutomationStepInputBase
) => Promise<TOutputs>
export interface AutomationMetadata extends Document { export interface AutomationMetadata extends Document {
errorCount?: number errorCount?: number
automationChainCount?: number automationChainCount?: number
@ -286,3 +291,8 @@ export type UpdatedRowEventEmitter = {
table: Table table: Table
appId: string appId: string
} }
export enum LoopStepType {
ARRAY = "Array",
STRING = "String",
}

View File

@ -0,0 +1,2 @@
export * from "./automation"
export * from "./schema"

View File

@ -0,0 +1,320 @@
import { SortOrder } from "../../../api"
import { EmptyFilterOption, Hosting, SearchFilters } from "../../../sdk"
import { HttpMethod } from "../query"
import { Row } from "../row"
import {
AutomationActionStepId,
AutomationResults,
EmailAttachment,
LoopStepType,
ActionImplementation,
} from "./automation"
export type ActionImplementations<T extends Hosting> = {
[AutomationActionStepId.COLLECT]: ActionImplementation<
CollectStepInputs,
CollectStepOutputs
>
[AutomationActionStepId.CREATE_ROW]: ActionImplementation<
CreateRowStepInputs,
CreateRowStepOutputs
>
[AutomationActionStepId.DELAY]: ActionImplementation<
DelayStepInputs,
DelayStepOutputs
>
[AutomationActionStepId.DELETE_ROW]: ActionImplementation<
DeleteRowStepInputs,
DeleteRowStepOutputs
>
[AutomationActionStepId.EXECUTE_QUERY]: ActionImplementation<
ExecuteQueryStepInputs,
ExecuteQueryStepOutputs
>
[AutomationActionStepId.EXECUTE_SCRIPT]: ActionImplementation<
ExecuteScriptStepInputs,
ExecuteScriptStepOutputs
>
[AutomationActionStepId.FILTER]: ActionImplementation<
FilterStepInputs,
FilterStepOutputs
>
[AutomationActionStepId.QUERY_ROWS]: ActionImplementation<
QueryRowsStepInputs,
QueryRowsStepOutputs
>
[AutomationActionStepId.SEND_EMAIL_SMTP]: ActionImplementation<
SmtpEmailStepInputs,
BaseAutomationOutputs
>
[AutomationActionStepId.SERVER_LOG]: ActionImplementation<
ServerLogStepInputs,
ServerLogStepOutputs
>
[AutomationActionStepId.TRIGGER_AUTOMATION_RUN]: ActionImplementation<
TriggerAutomationStepInputs,
TriggerAutomationStepOutputs
>
[AutomationActionStepId.UPDATE_ROW]: ActionImplementation<
UpdateRowStepInputs,
UpdateRowStepOutputs
>
[AutomationActionStepId.OUTGOING_WEBHOOK]: ActionImplementation<
OutgoingWebhookStepInputs,
ExternalAppStepOutputs
>
[AutomationActionStepId.discord]: ActionImplementation<
DiscordStepInputs,
ExternalAppStepOutputs
>
[AutomationActionStepId.slack]: ActionImplementation<
SlackStepInputs,
ExternalAppStepOutputs
>
[AutomationActionStepId.zapier]: ActionImplementation<
ZapierStepInputs,
ZapierStepOutputs
>
[AutomationActionStepId.integromat]: ActionImplementation<
MakeIntegrationInputs,
ExternalAppStepOutputs
>
[AutomationActionStepId.n8n]: ActionImplementation<
n8nStepInputs,
ExternalAppStepOutputs
>
} & (T extends "self"
? {
[AutomationActionStepId.EXECUTE_BASH]: ActionImplementation<
BashStepInputs,
BashStepOutputs
>
[AutomationActionStepId.OPENAI]: ActionImplementation<
OpenAIStepInputs,
OpenAIStepOutputs
>
}
: {})
export type BaseAutomationOutputs = {
success?: boolean
response?: {
[key: string]: any
message?: string
}
}
export type ExternalAppStepOutputs = {
httpStatus?: number
response: string
success: boolean
}
export type BashStepInputs = {
code: string
}
export type BashStepOutputs = BaseAutomationOutputs & {
stdout?: string
}
export type CollectStepInputs = {
collection: string
}
export type CollectStepOutputs = BaseAutomationOutputs & {
value?: any
}
export type CreateRowStepInputs = {
row: Row
}
export type CreateRowStepOutputs = BaseAutomationOutputs & {
row?: Row
id?: string
revision?: string
}
export type DelayStepInputs = {
time: number
}
export type DelayStepOutputs = BaseAutomationOutputs
export type DeleteRowStepInputs = {
tableId: string
id: string
revision?: string
}
export type DeleteRowStepOutputs = BaseAutomationOutputs & {
row?: Row
}
export type DiscordStepInputs = {
url: string
username?: string
avatar_url?: string
content: string
}
export type ExecuteQueryStepInputs = {
query: {
queryId: string
}
}
export type ExecuteQueryStepOutputs = BaseAutomationOutputs & {
info?: any
}
export type ExecuteScriptStepInputs = {
code: string
}
export type ExecuteScriptStepOutputs = BaseAutomationOutputs & {
value?: string
}
export type FilterStepInputs = {
field: any
condition: string
value: any
}
export type FilterStepOutputs = BaseAutomationOutputs & {
result: boolean
refValue?: any
comparisonValue?: any
}
export type LoopStepInputs = {
option: LoopStepType
binding: any
iterations?: number
failure?: string
}
export type LoopStepOutputs = {
items: string
success: boolean
iterations: number
}
export type MakeIntegrationInputs = {
url: string
body: any
}
export type n8nStepInputs = {
url: string
method: HttpMethod
authorization: string
body: any
}
export type OpenAIStepInputs = {
prompt: string
model: Model
}
enum Model {
GPT_35_TURBO = "gpt-3.5-turbo",
// will only work with api keys that have access to the GPT4 API
GPT_4 = "gpt-4",
}
export type OpenAIStepOutputs = Omit<BaseAutomationOutputs, "response"> & {
response?: string | null
}
export type QueryRowsStepInputs = {
tableId: string
filters?: SearchFilters
"filters-def"?: any
sortColumn?: string
sortOrder?: SortOrder
limit?: number
onEmptyFilter?: EmptyFilterOption
}
export type QueryRowsStepOutputs = BaseAutomationOutputs & {
rows?: Row[]
}
export type SmtpEmailStepInputs = {
to: string
from: string
subject: string
contents: string
cc: string
bcc: string
addInvite?: boolean
startTime: Date
endTime: Date
summary: string
location?: string
url?: string
attachments?: EmailAttachment[]
}
export type ServerLogStepInputs = {
text: string
}
export type ServerLogStepOutputs = BaseAutomationOutputs & {
message: string
}
export type SlackStepInputs = {
url: string
text: string
}
export type TriggerAutomationStepInputs = {
automation: {
automationId: string
}
timeout: number
}
export type TriggerAutomationStepOutputs = BaseAutomationOutputs & {
value?: AutomationResults["steps"]
}
export type UpdateRowStepInputs = {
meta: Record<string, any>
row: Row
rowId: string
}
export type UpdateRowStepOutputs = BaseAutomationOutputs & {
row?: Row
id?: string
revision?: string
}
export type ZapierStepInputs = {
url: string
body: any
}
export type ZapierStepOutputs = Omit<ExternalAppStepOutputs, "response"> & {
response: string
}
enum RequestType {
POST = "POST",
GET = "GET",
PUT = "PUT",
DELETE = "DELETE",
PATCH = "PATCH",
}
export type OutgoingWebhookStepInputs = {
requestMethod: RequestType
url: string
requestBody: string
headers: string
}

View File

@ -30,3 +30,9 @@ export interface SearchResponse<T> {
bookmark?: string | number bookmark?: string | number
totalRows?: number totalRows?: number
} }
export enum RowExportFormat {
CSV = "csv",
JSON = "json",
JSON_WITH_SCHEMA = "jsonWithSchema",
}