Merge branch 'master' of github.com:budibase/budibase into test-oracle

This commit is contained in:
Sam Rose 2024-08-02 10:48:14 +01:00
commit 40e886b34d
No known key found for this signature in database
62 changed files with 1488 additions and 162 deletions

View File

@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "2.29.26", "version": "2.29.27",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*", "packages/*",

View File

@ -1,9 +1,9 @@
<script> <script>
import { Select, Icon } from "@budibase/bbui" import { Select, Icon } from "@budibase/bbui"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import { canBeDisplayColumn, utils } from "@budibase/shared-core"
import { API } from "api" import { API } from "api"
import { parseFile } from "./utils" import { parseFile } from "./utils"
import { canBeDisplayColumn } from "@budibase/shared-core"
export let rows = [] export let rows = []
export let schema = {} export let schema = {}
@ -97,6 +97,8 @@
let errors = {} let errors = {}
let selectedColumnTypes = {} let selectedColumnTypes = {}
let rawRows = []
$: displayColumnOptions = Object.keys(schema || {}).filter(column => { $: displayColumnOptions = Object.keys(schema || {}).filter(column => {
return validation[column] && canBeDisplayColumn(schema[column].type) return validation[column] && canBeDisplayColumn(schema[column].type)
}) })
@ -106,6 +108,8 @@
} }
$: { $: {
rows = rawRows.map(row => utils.trimOtherProps(row, Object.keys(schema)))
// binding in consumer is causing double renders here // binding in consumer is causing double renders here
const newValidateHash = JSON.stringify(rows) + JSON.stringify(schema) const newValidateHash = JSON.stringify(rows) + JSON.stringify(schema)
if (newValidateHash !== validateHash) { if (newValidateHash !== validateHash) {
@ -122,7 +126,7 @@
try { try {
const response = await parseFile(e) const response = await parseFile(e)
rows = response.rows rawRows = response.rows
schema = response.schema schema = response.schema
fileName = response.fileName fileName = response.fileName
selectedColumnTypes = Object.entries(response.schema).reduce( selectedColumnTypes = Object.entries(response.schema).reduce(
@ -188,7 +192,7 @@
type="file" type="file"
on:change={handleFile} on:change={handleFile}
/> />
<label for="file-upload" class:uploaded={rows.length > 0}> <label for="file-upload" class:uploaded={rawRows.length > 0}>
{#if error} {#if error}
Error: {error} Error: {error}
{:else if fileName} {:else if fileName}
@ -198,7 +202,7 @@
{/if} {/if}
</label> </label>
</div> </div>
{#if rows.length > 0 && !error} {#if rawRows.length > 0 && !error}
<div class="schema-fields"> <div class="schema-fields">
{#each Object.entries(schema) as [name, column]} {#each Object.entries(schema) as [name, column]}
<div class="field"> <div class="field">

View File

@ -78,7 +78,7 @@
await datasources.fetch() await datasources.fetch()
await afterSave(table) await afterSave(table)
} catch (e) { } catch (e) {
notifications.error(e) notifications.error(e.message || e)
// reload in case the table was created // reload in case the table was created
await tables.fetch() await tables.fetch()
} }

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,
@ -33,7 +34,11 @@ import sdk from "../../../sdk"
import { jsonFromCsvString } from "../../../utilities/csv" import { jsonFromCsvString } from "../../../utilities/csv"
import { builderSocket } from "../../../websockets" import { builderSocket } from "../../../websockets"
import { cloneDeep, isEqual } from "lodash" import { cloneDeep, isEqual } from "lodash"
import { helpers } from "@budibase/shared-core" import {
helpers,
PROTECTED_EXTERNAL_COLUMNS,
PROTECTED_INTERNAL_COLUMNS,
} from "@budibase/shared-core"
function pickApi({ tableId, table }: { tableId?: string; table?: Table }) { function pickApi({ tableId, table }: { tableId?: string; table?: Table }) {
if (table && isExternalTable(table)) { if (table && isExternalTable(table)) {
@ -166,7 +171,7 @@ export async function validateNewTableImport(
if (isRows(rows) && isSchema(schema)) { if (isRows(rows) && isSchema(schema)) {
ctx.status = 200 ctx.status = 200
ctx.body = validateSchema(rows, schema) ctx.body = validateSchema(rows, schema, PROTECTED_INTERNAL_COLUMNS)
} else { } else {
ctx.status = 422 ctx.status = 422
} }
@ -178,9 +183,21 @@ export async function validateExistingTableImport(
const { rows, tableId } = ctx.request.body const { rows, tableId } = ctx.request.body
let schema = null let schema = null
let protectedColumnNames
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,
}
protectedColumnNames = PROTECTED_INTERNAL_COLUMNS.filter(x => x !== "_id")
} else {
protectedColumnNames = PROTECTED_EXTERNAL_COLUMNS
}
} else { } else {
ctx.status = 422 ctx.status = 422
return return
@ -188,7 +205,7 @@ export async function validateExistingTableImport(
if (tableId && isRows(rows) && isSchema(schema)) { if (tableId && isRows(rows) && isSchema(schema)) {
ctx.status = 200 ctx.status = 200
ctx.body = validateSchema(rows, schema) ctx.body = validateSchema(rows, schema, protectedColumnNames)
} else { } else {
ctx.status = 422 ctx.status = 422
} }

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(
importRows: rows, {
identifierFields, ...table,
user: ctx.user, schema: {
}) _id: {
name: "_id",
type: FieldType.STRING,
},
...table.schema,
},
},
{
importRows: rows,
identifierFields,
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

@ -1301,6 +1301,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 &&
@ -1645,23 +1752,38 @@ describe.each([
table = await config.api.table.save(defaultTable()) table = await config.api.table.save(defaultTable())
}) })
it("should allow exporting all columns", async () => { isInternal &&
const existing = await config.api.row.save(table._id!, {}) it("should not export internal couchdb fields", async () => {
const res = await config.api.row.exportRows(table._id!, { const existing = await config.api.row.save(table._id!, {
rows: [existing._id!], name: generator.guid(),
}) description: generator.paragraph(),
const results = JSON.parse(res) })
expect(results.length).toEqual(1) const res = await config.api.row.exportRows(table._id!, {
const row = results[0] rows: [existing._id!],
})
const results = JSON.parse(res)
expect(results.length).toEqual(1)
const row = results[0]
// Ensure all original columns were exported expect(Object.keys(row)).toEqual(["_id", "name", "description"])
expect(Object.keys(row).length).toBeGreaterThanOrEqual( })
Object.keys(existing).length
) !isInternal &&
Object.keys(existing).forEach(key => { it("should allow exporting all columns", async () => {
expect(row[key]).toEqual(existing[key]) const existing = await config.api.row.save(table._id!, {})
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]
// Ensure all original columns were exported
expect(Object.keys(row).length).toBe(Object.keys(existing).length)
Object.keys(existing).forEach(key => {
expect(row[key]).toEqual(existing[key])
})
}) })
})
it("should allow exporting only certain columns", async () => { it("should allow exporting only certain columns", async () => {
const existing = await config.api.row.save(table._id!, {}) const existing = await config.api.row.save(table._id!, {})

View File

@ -1,4 +1,8 @@
import { context, events } from "@budibase/backend-core" import { context, docIds, events } from "@budibase/backend-core"
import {
PROTECTED_EXTERNAL_COLUMNS,
PROTECTED_INTERNAL_COLUMNS,
} from "@budibase/shared-core"
import { import {
AutoFieldSubType, AutoFieldSubType,
BBReferenceFieldSubType, BBReferenceFieldSubType,
@ -11,6 +15,7 @@ import {
SaveTableRequest, SaveTableRequest,
SourceName, SourceName,
Table, Table,
TableSchema,
TableSourceType, TableSourceType,
User, User,
ViewCalculation, ViewCalculation,
@ -125,6 +130,64 @@ describe.each([
body: basicTable(), body: basicTable(),
}) })
}) })
it("does not persist the row fields that are not on the table schema", async () => {
const table: SaveTableRequest = basicTable()
table.rows = [
{
name: "test-name",
description: "test-desc",
nonValid: "test-non-valid",
},
]
const res = await config.api.table.save(table)
const persistedRows = await config.api.row.search(res._id!)
expect(persistedRows.rows).toEqual([
expect.objectContaining({
name: "test-name",
description: "test-desc",
}),
])
expect(persistedRows.rows[0].nonValid).toBeUndefined()
})
it.each(
isInternal ? PROTECTED_INTERNAL_COLUMNS : PROTECTED_EXTERNAL_COLUMNS
)(
"cannot use protected column names (%s) while importing a table",
async columnName => {
const table: SaveTableRequest = basicTable()
table.rows = [
{
name: "test-name",
description: "test-desc",
},
]
await config.api.table.save(
{
...table,
schema: {
...table.schema,
[columnName]: {
name: columnName,
type: FieldType.STRING,
},
},
},
{
status: 400,
body: {
message: `Column(s) "${columnName}" are duplicated - check for other columns with these name (case in-sensitive)`,
status: 400,
},
}
)
}
)
}) })
describe("update", () => { describe("update", () => {
@ -1029,4 +1092,156 @@ 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,
},
})
})
it.each(
isInternal ? PROTECTED_INTERNAL_COLUMNS : PROTECTED_EXTERNAL_COLUMNS
)("don't allow protected names in schema (%s)", async columnName => {
const result = await config.api.table.validateNewTableImport(
[
{
id: generator.natural(),
name: generator.first(),
[columnName]: generator.word(),
},
],
{
...basicSchema,
}
)
expect(result).toEqual({
allValid: false,
errors: {
[columnName]: `${columnName} is a protected column name`,
},
invalidColumns: [],
schemaValidation: {
id: true,
name: true,
[columnName]: false,
},
})
})
isInternal &&
it.each(
isInternal ? PROTECTED_INTERNAL_COLUMNS : PROTECTED_EXTERNAL_COLUMNS
)("don't allow protected names in the rows (%s)", async columnName => {
const result = await config.api.table.validateNewTableImport(
[
{
id: generator.natural(),
name: generator.first(),
},
],
{
...basicSchema,
[columnName]: {
name: columnName,
type: FieldType.STRING,
},
}
)
expect(result).toEqual({
allValid: false,
errors: {
[columnName]: `${columnName} is a protected column name`,
},
invalidColumns: [],
schemaValidation: {
id: true,
name: true,
[columnName]: false,
},
})
})
})
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,17 +121,16 @@ export async function run({ inputs }: AutomationStepInput) {
if (!contents) { if (!contents) {
contents = "<h1>No content</h1>" contents = "<h1>No content</h1>"
} }
to = to || undefined
if (attachments) {
if (Array.isArray(attachments)) {
attachments.forEach(item => automationUtils.guardAttachment(item))
} else {
automationUtils.guardAttachment(attachments)
}
}
try { try {
if (attachments) {
if (Array.isArray(attachments)) {
attachments.forEach(item => automationUtils.guardAttachment(item))
} else {
automationUtils.guardAttachment(attachments)
}
}
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

@ -43,6 +43,7 @@ import {
PROTECTED_INTERNAL_COLUMNS, PROTECTED_INTERNAL_COLUMNS,
} from "@budibase/shared-core" } from "@budibase/shared-core"
import { isSearchingByRowID } from "./utils" import { isSearchingByRowID } from "./utils"
import tracer from "dd-trace"
const builder = new sql.Sql(SqlClient.SQL_LITE) const builder = new sql.Sql(SqlClient.SQL_LITE)
const MISSING_COLUMN_REGEX = new RegExp(`no such column: .+`) const MISSING_COLUMN_REGEX = new RegExp(`no such column: .+`)
@ -72,10 +73,14 @@ function buildInternalFieldList(
} }
if (isRelationship) { if (isRelationship) {
const linkCol = col as RelationshipFieldMetadata const linkCol = col as RelationshipFieldMetadata
const relatedTable = tables.find(table => table._id === linkCol.tableId)! const relatedTable = tables.find(table => table._id === linkCol.tableId)
// no relationships provided, don't go more than a layer deep // no relationships provided, don't go more than a layer deep
fieldList = fieldList.concat(buildInternalFieldList(relatedTable, tables)) if (relatedTable) {
addJunctionFields(relatedTable, ["doc1.fieldName", "doc2.fieldName"]) fieldList = fieldList.concat(
buildInternalFieldList(relatedTable, tables)
)
addJunctionFields(relatedTable, ["doc1.fieldName", "doc2.fieldName"])
}
} else { } else {
fieldList.push(`${table._id}.${mapToUserColumn(col.name)}`) fieldList.push(`${table._id}.${mapToUserColumn(col.name)}`)
} }
@ -221,7 +226,11 @@ async function runSqlQuery(
} }
const db = context.getAppDB() const db = context.getAppDB()
return await db.sql<Row>(sql, bindings)
return await tracer.trace("sqs.runSqlQuery", async span => {
span?.addTags({ sql })
return await db.sql<Row>(sql, bindings)
})
} }
const response = await alias.queryWithAliasing(json, processSQLQuery) const response = await alias.queryWithAliasing(json, processSQLQuery)
if (opts?.countTotalRows) { if (opts?.countTotalRows) {

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

@ -48,9 +48,7 @@ export async function save(
} }
// check for case sensitivity - we don't want to allow duplicated columns // check for case sensitivity - we don't want to allow duplicated columns
const duplicateColumn = findDuplicateInternalColumns(table, { const duplicateColumn = findDuplicateInternalColumns(table)
ignoreProtectedColumnNames: !oldTable && !!opts?.isImport,
})
if (duplicateColumn.length) { if (duplicateColumn.length) {
throw new Error( throw new Error(
`Column(s) "${duplicateColumn.join( `Column(s) "${duplicateColumn.join(

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

@ -41,7 +41,11 @@ export function isRows(rows: any): rows is Rows {
return Array.isArray(rows) && rows.every(row => typeof row === "object") return Array.isArray(rows) && rows.every(row => typeof row === "object")
} }
export function validate(rows: Rows, schema: TableSchema): ValidationResults { export function validate(
rows: Rows,
schema: TableSchema,
protectedColumnNames: readonly string[]
): ValidationResults {
const results: ValidationResults = { const results: ValidationResults = {
schemaValidation: {}, schemaValidation: {},
allValid: false, allValid: false,
@ -49,6 +53,8 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
errors: {}, errors: {},
} }
protectedColumnNames = protectedColumnNames.map(x => x.toLowerCase())
rows.forEach(row => { rows.forEach(row => {
Object.entries(row).forEach(([columnName, columnData]) => { Object.entries(row).forEach(([columnName, columnData]) => {
const { const {
@ -63,6 +69,12 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
return return
} }
if (protectedColumnNames.includes(columnName.toLowerCase())) {
results.schemaValidation[columnName] = false
results.errors[columnName] = `${columnName} is a protected column name`
return
}
// If the columnType is not a string, then it's not present in the schema, and should be added to the invalid columns array // If the columnType is not a string, then it's not present in the schema, and should be added to the invalid columns array
if (typeof columnType !== "string") { if (typeof columnType !== "string") {
results.invalidColumns.push(columnName) results.invalidColumns.push(columnName)
@ -109,6 +121,13 @@ export function validate(rows: Rows, schema: TableSchema): ValidationResults {
}) })
}) })
for (const schemaField of Object.keys(schema)) {
if (protectedColumnNames.includes(schemaField.toLowerCase())) {
results.schemaValidation[schemaField] = false
results.errors[schemaField] = `${schemaField} is a protected column name`
}
}
results.allValid = results.allValid =
Object.values(results.schemaValidation).length > 0 && Object.values(results.schemaValidation).length > 0 &&
Object.values(results.schemaValidation).every(column => column) Object.values(results.schemaValidation).every(column => column)

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

@ -53,10 +53,7 @@ export function canBeSortColumn(type: FieldType): boolean {
return !!allowSortColumnByType[type] return !!allowSortColumnByType[type]
} }
export function findDuplicateInternalColumns( export function findDuplicateInternalColumns(table: Table): string[] {
table: Table,
opts?: { ignoreProtectedColumnNames: boolean }
): string[] {
// maintains the case of keys // maintains the case of keys
const casedKeys = Object.keys(table.schema) const casedKeys = Object.keys(table.schema)
// get the column names // get the column names
@ -72,11 +69,10 @@ export function findDuplicateInternalColumns(
} }
} }
} }
if (!opts?.ignoreProtectedColumnNames) {
for (let internalColumn of PROTECTED_INTERNAL_COLUMNS) { for (let internalColumn of PROTECTED_INTERNAL_COLUMNS) {
if (casedKeys.find(key => key === internalColumn)) { if (casedKeys.find(key => key === internalColumn)) {
duplicates.push(internalColumn) duplicates.push(internalColumn)
}
} }
} }
return duplicates return duplicates

View File

@ -67,3 +67,13 @@ export function hasSchema(test: any) {
Object.keys(test).length > 0 Object.keys(test).length > 0
) )
} }
export function trimOtherProps(object: any, allowedProps: string[]) {
const result = Object.keys(object)
.filter(key => allowedProps.includes(key))
.reduce<Record<string, any>>(
(acc, key) => ({ ...acc, [key]: object[key] }),
{}
)
return result
}

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",
}