Merge branch 'master' into screen-store-ts-conversion

This commit is contained in:
deanhannigan 2025-01-27 10:42:34 +00:00 committed by GitHub
commit 46c6e403ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
190 changed files with 2388 additions and 4146 deletions

View File

@ -41,11 +41,12 @@ module.exports = {
if ( if (
/^@budibase\/[^/]+\/.*$/.test(importPath) && /^@budibase\/[^/]+\/.*$/.test(importPath) &&
importPath !== "@budibase/backend-core/tests" && importPath !== "@budibase/backend-core/tests" &&
importPath !== "@budibase/string-templates/test/utils" importPath !== "@budibase/string-templates/test/utils" &&
importPath !== "@budibase/client/manifest.json"
) { ) {
context.report({ context.report({
node, node,
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests and @budibase/string-templates/test/utils.`, message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests, @budibase/string-templates/test/utils and @budibase/client/manifest.json.`,
}) })
} }
}, },

View File

@ -1,6 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "3.2.46", "version": "3.3.1",
"npmClient": "yarn", "npmClient": "yarn",
"concurrency": 20, "concurrency": 20,
"command": { "command": {

View File

@ -1,6 +1,5 @@
export * as configs from "./configs" export * as configs from "./configs"
export * as events from "./events" export * as events from "./events"
export * as migrations from "./migrations"
export * as users from "./users" export * as users from "./users"
export * as userUtils from "./users/utils" export * as userUtils from "./users/utils"
export * as roles from "./security/roles" export * as roles from "./security/roles"

View File

@ -1,40 +0,0 @@
import {
MigrationType,
MigrationName,
MigrationDefinition,
} from "@budibase/types"
export const DEFINITIONS: MigrationDefinition[] = [
{
type: MigrationType.GLOBAL,
name: MigrationName.USER_EMAIL_VIEW_CASING,
},
{
type: MigrationType.GLOBAL,
name: MigrationName.SYNC_QUOTAS,
},
{
type: MigrationType.APP,
name: MigrationName.APP_URLS,
},
{
type: MigrationType.APP,
name: MigrationName.EVENT_APP_BACKFILL,
},
{
type: MigrationType.APP,
name: MigrationName.TABLE_SETTINGS_LINKS_TO_ACTIONS,
},
{
type: MigrationType.GLOBAL,
name: MigrationName.EVENT_GLOBAL_BACKFILL,
},
{
type: MigrationType.INSTALLATION,
name: MigrationName.EVENT_INSTALLATION_BACKFILL,
},
{
type: MigrationType.GLOBAL,
name: MigrationName.GLOBAL_INFO_SYNC_USERS,
},
]

View File

@ -1,2 +0,0 @@
export * from "./migrations"
export * from "./definitions"

View File

@ -1,186 +0,0 @@
import { DEFAULT_TENANT_ID } from "../constants"
import {
DocumentType,
StaticDatabases,
getAllApps,
getGlobalDBName,
getDB,
} from "../db"
import environment from "../environment"
import * as platform from "../platform"
import * as context from "../context"
import { DEFINITIONS } from "."
import {
Migration,
MigrationOptions,
MigrationType,
MigrationNoOpOptions,
App,
} from "@budibase/types"
export const getMigrationsDoc = async (db: any) => {
// get the migrations doc
try {
return await db.get(DocumentType.MIGRATIONS)
} catch (err: any) {
if (err.status && err.status === 404) {
return { _id: DocumentType.MIGRATIONS }
} else {
throw err
}
}
}
export const backPopulateMigrations = async (opts: MigrationNoOpOptions) => {
// filter migrations to the type and populate a no-op migration
const migrations: Migration[] = DEFINITIONS.filter(
def => def.type === opts.type
).map(d => ({ ...d, fn: async () => {} }))
await runMigrations(migrations, { noOp: opts })
}
export const runMigration = async (
migration: Migration,
options: MigrationOptions = {}
) => {
const migrationType = migration.type
const migrationName = migration.name
const silent = migration.silent
const log = (message: string) => {
if (!silent) {
console.log(message)
}
}
// get the db to store the migration in
let dbNames: string[]
if (migrationType === MigrationType.GLOBAL) {
dbNames = [getGlobalDBName()]
} else if (migrationType === MigrationType.APP) {
if (options.noOp) {
if (!options.noOp.appId) {
throw new Error("appId is required for noOp app migration")
}
dbNames = [options.noOp.appId]
} else {
const apps = (await getAllApps(migration.appOpts)) as App[]
dbNames = apps.map(app => app.appId)
}
} else if (migrationType === MigrationType.INSTALLATION) {
dbNames = [StaticDatabases.PLATFORM_INFO.name]
} else {
throw new Error(`Unrecognised migration type [${migrationType}]`)
}
const length = dbNames.length
let count = 0
// run the migration against each db
for (const dbName of dbNames) {
count++
const lengthStatement = length > 1 ? `[${count}/${length}]` : ""
const db = getDB(dbName)
try {
const doc = await getMigrationsDoc(db)
// the migration has already been run
if (doc[migrationName]) {
// check for force
if (
options.force &&
options.force[migrationType] &&
options.force[migrationType].includes(migrationName)
) {
log(`[Migration: ${migrationName}] [DB: ${dbName}] Forcing`)
} else {
// no force, exit
return
}
}
// check if the migration is not a no-op
if (!options.noOp) {
log(
`[Migration: ${migrationName}] [DB: ${dbName}] Running ${lengthStatement}`
)
if (migration.preventRetry) {
// eagerly set the completion date
// so that we never run this migration twice even upon failure
doc[migrationName] = Date.now()
const response = await db.put(doc)
doc._rev = response.rev
}
// run the migration
if (migrationType === MigrationType.APP) {
await context.doInAppContext(db.name, async () => {
await migration.fn(db)
})
} else {
await migration.fn(db)
}
log(`[Migration: ${migrationName}] [DB: ${dbName}] Complete`)
}
// mark as complete
doc[migrationName] = Date.now()
await db.put(doc)
} catch (err) {
console.error(
`[Migration: ${migrationName}] [DB: ${dbName}] Error: `,
err
)
throw err
}
}
}
export const runMigrations = async (
migrations: Migration[],
options: MigrationOptions = {}
) => {
let tenantIds
if (environment.MULTI_TENANCY) {
if (options.noOp) {
tenantIds = [options.noOp.tenantId]
} else if (!options.tenantIds || !options.tenantIds.length) {
// run for all tenants
tenantIds = await platform.tenants.getTenantIds()
} else {
tenantIds = options.tenantIds
}
} else {
// single tenancy
tenantIds = [DEFAULT_TENANT_ID]
}
if (tenantIds.length > 1) {
console.log(`Checking migrations for ${tenantIds.length} tenants`)
} else {
console.log("Checking migrations")
}
let count = 0
// for all tenants
for (const tenantId of tenantIds) {
count++
if (tenantIds.length > 1) {
console.log(`Progress [${count}/${tenantIds.length}]`)
}
// for all migrations
for (const migration of migrations) {
// run the migration
await context.doInTenant(
tenantId,
async () => await runMigration(migration, options)
)
}
}
console.log("Migrations complete")
}

View File

@ -1,11 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`migrations should match snapshot 1`] = `
{
"_id": "migrations",
"_rev": "1-2f64479842a0513aa8b97f356b0b9127",
"createdAt": "2020-01-01T00:00:00.000Z",
"test": 1577836800000,
"updatedAt": "2020-01-01T00:00:00.000Z",
}
`;

View File

@ -1,64 +0,0 @@
import { testEnv, DBTestConfiguration } from "../../../tests/extra"
import * as migrations from "../index"
import * as context from "../../context"
import { MigrationType } from "@budibase/types"
testEnv.multiTenant()
describe("migrations", () => {
const config = new DBTestConfiguration()
const migrationFunction = jest.fn()
const MIGRATIONS = [
{
type: MigrationType.GLOBAL,
name: "test" as any,
fn: migrationFunction,
},
]
beforeEach(() => {
config.newTenant()
})
afterEach(async () => {
jest.clearAllMocks()
})
const migrate = () => {
return migrations.runMigrations(MIGRATIONS, {
tenantIds: [config.tenantId],
})
}
it("should run a new migration", async () => {
await config.doInTenant(async () => {
await migrate()
expect(migrationFunction).toHaveBeenCalled()
const db = context.getGlobalDB()
const doc = await migrations.getMigrationsDoc(db)
expect(doc.test).toBeDefined()
})
})
it("should match snapshot", async () => {
await config.doInTenant(async () => {
await migrate()
const doc = await migrations.getMigrationsDoc(context.getGlobalDB())
expect(doc).toMatchSnapshot()
})
})
it("should skip a previously run migration", async () => {
await config.doInTenant(async () => {
const db = context.getGlobalDB()
await migrate()
const previousDoc = await migrations.getMigrationsDoc(db)
await migrate()
const currentDoc = await migrations.getMigrationsDoc(db)
expect(migrationFunction).toHaveBeenCalledTimes(1)
expect(currentDoc.test).toBe(previousDoc.test)
})
})
})

View File

@ -1,23 +1,23 @@
<script> <script lang="ts">
import { import {
default as AbsTooltip, default as AbsTooltip,
TooltipPosition, TooltipPosition,
TooltipType, TooltipType,
} from "../Tooltip/AbsTooltip.svelte" } from "../Tooltip/AbsTooltip.svelte"
export let name = "Add" export let name: string = "Add"
export let hidden = false export let hidden: boolean = false
export let size = "M" export let size = "M"
export let hoverable = false export let hoverable: boolean = false
export let disabled = false export let disabled: boolean = false
export let color = undefined export let color: string | undefined = undefined
export let hoverColor = undefined export let hoverColor: string | undefined = undefined
export let tooltip = undefined export let tooltip: string | undefined = undefined
export let tooltipPosition = TooltipPosition.Bottom export let tooltipPosition = TooltipPosition.Bottom
export let tooltipType = TooltipType.Default export let tooltipType = TooltipType.Default
export let tooltipColor = undefined export let tooltipColor: string | undefined = undefined
export let tooltipWrap = true export let tooltipWrap: boolean = true
export let newStyles = false export let newStyles: boolean = false
</script> </script>
<AbsTooltip <AbsTooltip

View File

@ -23,7 +23,7 @@
export let type = TooltipType.Default export let type = TooltipType.Default
export let text = "" export let text = ""
export let fixed = false export let fixed = false
export let color = null export let color = ""
export let noWrap = false export let noWrap = false
let wrapper let wrapper

View File

@ -45,6 +45,11 @@
--purple: #806fde; --purple: #806fde;
--purple-dark: #130080; --purple-dark: #130080;
--error-bg: rgba(226, 109, 105, 0.3);
--warning-bg: rgba(255, 210, 106, 0.3);
--error-content: rgba(226, 109, 105, 0.6);
--warning-content: rgba(255, 210, 106, 0.6);
--rounded-small: 4px; --rounded-small: 4px;
--rounded-medium: 8px; --rounded-medium: 8px;
--rounded-large: 16px; --rounded-large: 16px;

View File

@ -293,7 +293,7 @@
type: RowSelector, type: RowSelector,
props: { props: {
row: inputData["oldRow"] || { row: inputData["oldRow"] || {
tableId: inputData["row"].tableId, tableId: inputData["row"]?.tableId,
}, },
meta: { meta: {
fields: inputData["meta"]?.oldFields || {}, fields: inputData["meta"]?.oldFields || {},

View File

@ -12,7 +12,7 @@
decodeJSBinding, decodeJSBinding,
encodeJSBinding, encodeJSBinding,
processObjectSync, processObjectSync,
processStringSync, processStringWithLogsSync,
} from "@budibase/string-templates" } from "@budibase/string-templates"
import { readableToRuntimeBinding } from "@/dataBinding" import { readableToRuntimeBinding } from "@/dataBinding"
import CodeEditor from "../CodeEditor/CodeEditor.svelte" import CodeEditor from "../CodeEditor/CodeEditor.svelte"
@ -41,6 +41,7 @@
InsertAtPositionFn, InsertAtPositionFn,
JSONValue, JSONValue,
} from "@budibase/types" } from "@budibase/types"
import type { Log } from "@budibase/string-templates"
import type { CompletionContext } from "@codemirror/autocomplete" import type { CompletionContext } from "@codemirror/autocomplete"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -66,6 +67,7 @@
let insertAtPos: InsertAtPositionFn | undefined let insertAtPos: InsertAtPositionFn | undefined
let targetMode: BindingMode | null = null let targetMode: BindingMode | null = null
let expressionResult: string | undefined let expressionResult: string | undefined
let expressionLogs: Log[] | undefined
let expressionError: string | undefined let expressionError: string | undefined
let evaluating = false let evaluating = false
@ -157,7 +159,7 @@
(expression: string | null, context: any, snippets: Snippet[]) => { (expression: string | null, context: any, snippets: Snippet[]) => {
try { try {
expressionError = undefined expressionError = undefined
expressionResult = processStringSync( const output = processStringWithLogsSync(
expression || "", expression || "",
{ {
...context, ...context,
@ -167,6 +169,8 @@
noThrow: false, noThrow: false,
} }
) )
expressionResult = output.result
expressionLogs = output.logs
} catch (err: any) { } catch (err: any) {
expressionResult = undefined expressionResult = undefined
expressionError = err expressionError = err
@ -421,6 +425,7 @@
<EvaluationSidePanel <EvaluationSidePanel
{expressionResult} {expressionResult}
{expressionError} {expressionError}
{expressionLogs}
{evaluating} {evaluating}
expression={editorValue ? editorValue : ""} expression={editorValue ? editorValue : ""}
/> />

View File

@ -4,11 +4,13 @@
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { UserScriptError } from "@budibase/string-templates" import { UserScriptError } from "@budibase/string-templates"
import type { Log } from "@budibase/string-templates"
import type { JSONValue } from "@budibase/types" import type { JSONValue } from "@budibase/types"
// this can be essentially any primitive response from the JS function // this can be essentially any primitive response from the JS function
export let expressionResult: JSONValue | undefined = undefined export let expressionResult: JSONValue | undefined = undefined
export let expressionError: string | undefined = undefined export let expressionError: string | undefined = undefined
export let expressionLogs: Log[] = []
export let evaluating = false export let evaluating = false
export let expression: string | null = null export let expression: string | null = null
@ -16,6 +18,11 @@
$: empty = expression == null || expression?.trim() === "" $: empty = expression == null || expression?.trim() === ""
$: success = !error && !empty $: success = !error && !empty
$: highlightedResult = highlight(expressionResult) $: highlightedResult = highlight(expressionResult)
$: highlightedLogs = expressionLogs.map(l => ({
log: highlight(l.log.join(", ")),
line: l.line,
type: l.type,
}))
const formatError = (err: any) => { const formatError = (err: any) => {
if (err.code === UserScriptError.code) { if (err.code === UserScriptError.code) {
@ -25,14 +32,14 @@
} }
// json can be any primitive type // json can be any primitive type
const highlight = (json?: any | null) => { const highlight = (json?: JSONValue | null) => {
if (json == null) { if (json == null) {
return "" return ""
} }
// Attempt to parse and then stringify, in case this is valid result // Attempt to parse and then stringify, in case this is valid result
try { try {
json = JSON.stringify(JSON.parse(json), null, 2) json = JSON.stringify(JSON.parse(json as any), null, 2)
} catch (err) { } catch (err) {
// couldn't parse/stringify, just treat it as the raw input // couldn't parse/stringify, just treat it as the raw input
} }
@ -61,7 +68,7 @@
<div class="header" class:success class:error> <div class="header" class:success class:error>
<div class="header-content"> <div class="header-content">
{#if error} {#if error}
<Icon name="Alert" color="var(--spectrum-global-color-red-600)" /> <Icon name="Alert" color="var(--error-content)" />
<div>Error</div> <div>Error</div>
{#if evaluating} {#if evaluating}
<div transition:fade|local={{ duration: 130 }}> <div transition:fade|local={{ duration: 130 }}>
@ -90,8 +97,36 @@
{:else if error} {:else if error}
{formatError(expressionError)} {formatError(expressionError)}
{:else} {:else}
<div class="output-lines">
{#each highlightedLogs as logLine}
<div
class="line"
class:error-log={logLine.type === "error"}
class:warn-log={logLine.type === "warn"}
>
<div class="icon-log">
{#if logLine.type === "error"}
<Icon
size="XS"
name="CloseCircle"
color="var(--error-content)"
/>
{:else if logLine.type === "warn"}
<Icon size="XS" name="Alert" color="var(--warning-content)" />
{/if}
<!-- eslint-disable-next-line svelte/no-at-html-tags-->
<span>{@html logLine.log}</span>
</div>
{#if logLine.line}
<span style="color: var(--blue)">:{logLine.line}</span>
{/if}
</div>
{/each}
<div class="line">
<!-- eslint-disable-next-line svelte/no-at-html-tags--> <!-- eslint-disable-next-line svelte/no-at-html-tags-->
{@html highlightedResult} {@html highlightedResult}
</div>
</div>
{/if} {/if}
</div> </div>
</div> </div>
@ -130,20 +165,37 @@
height: 100%; height: 100%;
z-index: 1; z-index: 1;
position: absolute; position: absolute;
opacity: 10%;
} }
.header.error::before { .header.error::before {
background: var(--spectrum-global-color-red-400); background: var(--error-bg);
} }
.body { .body {
flex: 1 1 auto; flex: 1 1 auto;
padding: var(--spacing-m) var(--spacing-l); padding: var(--spacing-m) var(--spacing-l);
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 12px; font-size: 12px;
overflow-y: scroll; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
white-space: pre-wrap; white-space: pre-line;
word-wrap: break-word; word-wrap: break-word;
height: 0; height: 0;
} }
.output-lines {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.line {
border-bottom: var(--border-light);
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: end;
padding: var(--spacing-s);
}
.icon-log {
display: flex;
gap: var(--spacing-s);
align-items: start;
}
</style> </style>

View File

@ -1,4 +1,5 @@
<script> <script>
import { datasources } from "@/stores/builder"
import { Divider, Heading } from "@budibase/bbui" import { Divider, Heading } from "@budibase/bbui"
export let dividerState export let dividerState
@ -6,6 +7,21 @@
export let dataSet export let dataSet
export let value export let value
export let onSelect export let onSelect
export let identifiers = ["resourceId"]
$: displayDatasourceName = $datasources.list.length > 1
function isSelected(entry) {
if (!identifiers.length) {
return false
}
for (const identifier of identifiers) {
if (entry[identifier] !== value?.[identifier]) {
return false
}
}
return true
}
</script> </script>
{#if dividerState} {#if dividerState}
@ -21,15 +37,16 @@
{#each dataSet as data} {#each dataSet as data}
<li <li
class="spectrum-Menu-item" class="spectrum-Menu-item"
class:is-selected={value?.label === data.label && class:is-selected={isSelected(data) && value?.type === data.type}
value?.type === data.type}
role="option" role="option"
aria-selected="true" aria-selected="true"
tabindex="0" tabindex="0"
on:click={() => onSelect(data)} on:click={() => onSelect(data)}
> >
<span class="spectrum-Menu-itemLabel"> <span class="spectrum-Menu-itemLabel">
{data.datasourceName ? `${data.datasourceName} - ` : ""}{data.label} {data.datasourceName && displayDatasourceName
? `${data.datasourceName} - `
: ""}{data.label}
</span> </span>
<svg <svg
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon" class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"

View File

@ -34,7 +34,7 @@
import ClientBindingPanel from "@/components/common/bindings/ClientBindingPanel.svelte" import ClientBindingPanel from "@/components/common/bindings/ClientBindingPanel.svelte"
import DataSourceCategory from "@/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte" import DataSourceCategory from "@/components/design/settings/controls/DataSourceSelect/DataSourceCategory.svelte"
import { API } from "@/api" import { API } from "@/api"
import { datasourceSelect as format } from "@/helpers/data/format" import { sortAndFormat } from "@/helpers/data/format"
export let value = {} export let value = {}
export let otherSources export let otherSources
@ -51,25 +51,13 @@
let modal let modal
$: text = value?.label ?? "Choose an option" $: text = value?.label ?? "Choose an option"
$: tables = $tablesStore.list $: tables = sortAndFormat.tables($tablesStore.list, $datasources.list)
.map(table => format.table(table, $datasources.list))
.sort((a, b) => {
// sort tables alphabetically, grouped by datasource
const dsA = a.datasourceName ?? ""
const dsB = b.datasourceName ?? ""
const dsComparison = dsA.localeCompare(dsB)
if (dsComparison !== 0) {
return dsComparison
}
return a.label.localeCompare(b.label)
})
$: viewsV1 = $viewsStore.list.map(view => ({ $: viewsV1 = $viewsStore.list.map(view => ({
...view, ...view,
label: view.name, label: view.name,
type: "view", type: "view",
})) }))
$: viewsV2 = $viewsV2Store.list.map(format.viewV2) $: viewsV2 = sortAndFormat.viewsV2($viewsV2Store.list, $datasources.list)
$: views = [...(viewsV1 || []), ...(viewsV2 || [])] $: views = [...(viewsV1 || []), ...(viewsV2 || [])]
$: queries = $queriesStore.list $: queries = $queriesStore.list
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable) .filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
@ -303,6 +291,7 @@
dataSet={views} dataSet={views}
{value} {value}
onSelect={handleSelected} onSelect={handleSelected}
identifiers={["tableId", "name"]}
/> />
{/if} {/if}
{#if queries?.length} {#if queries?.length}
@ -312,6 +301,7 @@
dataSet={queries} dataSet={queries}
{value} {value}
onSelect={handleSelected} onSelect={handleSelected}
identifiers={["_id"]}
/> />
{/if} {/if}
{#if links?.length} {#if links?.length}
@ -321,6 +311,7 @@
dataSet={links} dataSet={links}
{value} {value}
onSelect={handleSelected} onSelect={handleSelected}
identifiers={["tableId", "fieldName"]}
/> />
{/if} {/if}
{#if fields?.length} {#if fields?.length}
@ -330,6 +321,7 @@
dataSet={fields} dataSet={fields}
{value} {value}
onSelect={handleSelected} onSelect={handleSelected}
identifiers={["providerId", "tableId", "fieldName"]}
/> />
{/if} {/if}
{#if jsonArrays?.length} {#if jsonArrays?.length}
@ -339,6 +331,7 @@
dataSet={jsonArrays} dataSet={jsonArrays}
{value} {value}
onSelect={handleSelected} onSelect={handleSelected}
identifiers={["providerId", "tableId", "fieldName"]}
/> />
{/if} {/if}
{#if showDataProviders && dataProviders?.length} {#if showDataProviders && dataProviders?.length}
@ -348,6 +341,7 @@
dataSet={dataProviders} dataSet={dataProviders}
{value} {value}
onSelect={handleSelected} onSelect={handleSelected}
identifiers={["providerId"]}
/> />
{/if} {/if}
<DataSourceCategory <DataSourceCategory

View File

@ -1,22 +1,32 @@
<script> <script>
import { Select } from "@budibase/bbui" import { Popover, Select } from "@budibase/bbui"
import { createEventDispatcher, onMount } from "svelte" import { createEventDispatcher, onMount } from "svelte"
import { tables as tablesStore, viewsV2 } from "@/stores/builder" import {
import { tableSelect as format } from "@/helpers/data/format" tables as tableStore,
datasources as datasourceStore,
viewsV2 as viewsV2Store,
} from "@/stores/builder"
import DataSourceCategory from "./DataSourceSelect/DataSourceCategory.svelte"
import { sortAndFormat } from "@/helpers/data/format"
export let value export let value
let anchorRight, dropdownRight
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
$: tables = $tablesStore.list.map(format.table) $: tables = sortAndFormat.tables($tableStore.list, $datasourceStore.list)
$: views = $viewsV2.list.map(format.viewV2) $: views = sortAndFormat.viewsV2($viewsV2Store.list, $datasourceStore.list)
$: options = [...(tables || []), ...(views || [])] $: options = [...(tables || []), ...(views || [])]
$: text = value?.label ?? "Choose an option"
const onChange = e => { const onChange = e => {
dispatch( dispatch(
"change", "change",
options.find(x => x.resourceId === e.detail) options.find(x => x.resourceId === e.resourceId)
) )
dropdownRight.hide()
} }
onMount(() => { onMount(() => {
@ -29,10 +39,47 @@
}) })
</script> </script>
<div class="container" bind:this={anchorRight}>
<Select <Select
on:change={onChange} readonly
value={value?.resourceId} value={text}
{options} options={[text]}
getOptionValue={x => x.resourceId} on:click={dropdownRight.show}
getOptionLabel={x => x.label}
/> />
</div>
<Popover bind:this={dropdownRight} anchor={anchorRight}>
<div class="dropdown">
<DataSourceCategory
heading="Tables"
dataSet={tables}
{value}
onSelect={onChange}
/>
{#if views?.length}
<DataSourceCategory
dividerState={true}
heading="Views"
dataSet={views}
{value}
onSelect={onChange}
/>
{/if}
</div>
</Popover>
<style>
.container {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.container :global(:first-child) {
flex: 1 1 auto;
}
.dropdown {
padding: var(--spacing-m) 0;
z-index: 99999999;
}
</style>

View File

@ -20,7 +20,7 @@
const processModals = () => { const processModals = () => {
const defaultCacheFn = key => { const defaultCacheFn = key => {
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds) temporalStore.setExpiring(key, {}, oneDayInSeconds)
} }
const dismissableModals = [ const dismissableModals = [
@ -50,7 +50,7 @@
}, },
] ]
return dismissableModals.filter(modal => { return dismissableModals.filter(modal => {
return !temporalStore.actions.getExpiring(modal.key) && modal.criteria() return !temporalStore.getExpiring(modal.key) && modal.criteria()
}) })
} }

View File

@ -6,7 +6,7 @@ import { BANNER_TYPES } from "@budibase/bbui"
const oneDayInSeconds = 86400 const oneDayInSeconds = 86400
const defaultCacheFn = key => { const defaultCacheFn = key => {
temporalStore.actions.setExpiring(key, {}, oneDayInSeconds) temporalStore.setExpiring(key, {}, oneDayInSeconds)
} }
const upgradeAction = key => { const upgradeAction = key => {
@ -148,7 +148,7 @@ export const getBanners = () => {
buildUsersAboveLimitBanner(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER), buildUsersAboveLimitBanner(ExpiringKeys.LICENSING_USERS_ABOVE_LIMIT_BANNER),
].filter(licensingBanner => { ].filter(licensingBanner => {
return ( return (
!temporalStore.actions.getExpiring(licensingBanner.key) && !temporalStore.getExpiring(licensingBanner.key) &&
licensingBanner.criteria() licensingBanner.criteria()
) )
}) })

View File

@ -9,11 +9,18 @@ export const datasourceSelect = {
datasourceName: datasource?.name, datasourceName: datasource?.name,
} }
}, },
viewV2: view => ({ viewV2: (view, datasources) => {
const datasource = datasources
?.filter(f => f.entities)
.flatMap(d => d.entities)
.find(ds => ds._id === view.tableId)
return {
...view, ...view,
label: view.name, label: view.name,
type: "viewV2", type: "viewV2",
}), datasourceName: datasource?.name,
}
},
} }
export const tableSelect = { export const tableSelect = {
@ -31,3 +38,36 @@ export const tableSelect = {
resourceId: view.id, resourceId: view.id,
}), }),
} }
export const sortAndFormat = {
tables: (tables, datasources) => {
return tables
.map(table => {
const formatted = datasourceSelect.table(table, datasources)
return {
...formatted,
resourceId: table._id,
}
})
.sort((a, b) => {
// sort tables alphabetically, grouped by datasource
const dsA = a.datasourceName ?? ""
const dsB = b.datasourceName ?? ""
const dsComparison = dsA.localeCompare(dsB)
if (dsComparison !== 0) {
return dsComparison
}
return a.label.localeCompare(b.label)
})
},
viewsV2: (views, datasources) => {
return views.map(view => {
const formatted = datasourceSelect.viewV2(view, datasources)
return {
...formatted,
resourceId: view.id,
}
})
},
}

View File

@ -9,3 +9,4 @@ export {
lowercase, lowercase,
isBuilderInputFocused, isBuilderInputFocused,
} from "./helpers" } from "./helpers"
export * as featureFlag from "./featureFlags"

View File

@ -0,0 +1,46 @@
import { Component, Screen, ScreenProps } from "@budibase/types"
import clientManifest from "@budibase/client/manifest.json"
export function findComponentsBySettingsType(
screen: Screen,
type: string | string[]
) {
const typesArray = Array.isArray(type) ? type : [type]
const result: {
component: Component
setting: {
type: string
key: string
}
}[] = []
function recurseFieldComponentsInChildren(component: ScreenProps) {
if (!component) {
return
}
const definition = getManifestDefinition(component)
const setting =
"settings" in definition &&
definition.settings.find((s: any) => typesArray.includes(s.type))
if (setting && "type" in setting) {
result.push({
component,
setting: { type: setting.type!, key: setting.key! },
})
}
component._children?.forEach(child => {
recurseFieldComponentsInChildren(child)
})
}
recurseFieldComponentsInChildren(screen?.props)
return result
}
function getManifestDefinition(component: Component) {
const componentType = component._component.split("/").slice(-1)[0]
const definition =
clientManifest[componentType as keyof typeof clientManifest]
return definition
}

View File

@ -18,7 +18,7 @@
$: useAccountPortal = cloud && !$admin.disableAccountPortal $: useAccountPortal = cloud && !$admin.disableAccountPortal
navigation.actions.init($redirect) navigation.init($redirect)
const validateTenantId = async () => { const validateTenantId = async () => {
const host = window.location.host const host = window.location.host

View File

@ -11,6 +11,7 @@
selectedScreen, selectedScreen,
hoverStore, hoverStore,
componentTreeNodesStore, componentTreeNodesStore,
screenComponentErrors,
snippets, snippets,
} from "@/stores/builder" } from "@/stores/builder"
import ConfirmDialog from "@/components/common/ConfirmDialog.svelte" import ConfirmDialog from "@/components/common/ConfirmDialog.svelte"
@ -68,6 +69,7 @@
port: window.location.port, port: window.location.port,
}, },
snippets: $snippets, snippets: $snippets,
componentErrors: $screenComponentErrors,
} }
// Refresh the preview when required // Refresh the preview when required

View File

@ -15,6 +15,7 @@
import { import {
appsStore, appsStore,
organisation, organisation,
admin,
auth, auth,
groups, groups,
licensing, licensing,
@ -42,6 +43,7 @@
app => app.status === AppStatus.DEPLOYED app => app.status === AppStatus.DEPLOYED
) )
$: userApps = getUserApps(publishedApps, userGroups, $auth.user) $: userApps = getUserApps(publishedApps, userGroups, $auth.user)
$: isOwner = $auth.accountPortalAccess && $admin.cloud
function getUserApps(publishedApps, userGroups, user) { function getUserApps(publishedApps, userGroups, user) {
if (sdk.users.isAdmin(user)) { if (sdk.users.isAdmin(user)) {
@ -111,7 +113,13 @@
</MenuItem> </MenuItem>
<MenuItem <MenuItem
icon="LockClosed" icon="LockClosed"
on:click={() => changePasswordModal.show()} on:click={() => {
if (isOwner) {
window.location.href = `${$admin.accountPortalUrl}/portal/account`
} else {
changePasswordModal.show()
}
}}
> >
Update password Update password
</MenuItem> </MenuItem>

View File

@ -30,10 +30,16 @@
try { try {
loading = true loading = true
if (forceResetPassword) { if (forceResetPassword) {
const email = $auth.user.email
const tenantId = $auth.user.tenantId
await auth.updateSelf({ await auth.updateSelf({
password, password,
forceResetPassword: false, forceResetPassword: false,
}) })
if (!$auth.user) {
// Update self will clear the platform user, so need to login
await auth.login(email, password, tenantId)
}
$goto("../portal/") $goto("../portal/")
} else { } else {
await auth.resetPassword(password, resetCode) await auth.resetPassword(password, resetCode)

View File

@ -1,5 +1,5 @@
<script> <script>
import { auth } from "@/stores/portal" import { admin, auth } from "@/stores/portal"
import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui" import { ActionMenu, MenuItem, Icon, Modal } from "@budibase/bbui"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import ProfileModal from "@/components/settings/ProfileModal.svelte" import ProfileModal from "@/components/settings/ProfileModal.svelte"
@ -13,6 +13,8 @@
let updatePasswordModal let updatePasswordModal
let apiKeyModal let apiKeyModal
$: isOwner = $auth.accountPortalAccess && $admin.cloud
const logout = async () => { const logout = async () => {
try { try {
await auth.logout() await auth.logout()
@ -32,7 +34,16 @@
</MenuItem> </MenuItem>
<MenuItem icon="Moon" on:click={() => themeModal.show()}>Theme</MenuItem> <MenuItem icon="Moon" on:click={() => themeModal.show()}>Theme</MenuItem>
{#if !$auth.isSSO} {#if !$auth.isSSO}
<MenuItem icon="LockClosed" on:click={() => updatePasswordModal.show()}> <MenuItem
icon="LockClosed"
on:click={() => {
if (isOwner) {
window.location.href = `${$admin.accountPortalUrl}/portal/account`
} else {
updatePasswordModal.show()
}
}}
>
Update password Update password
</MenuItem> </MenuItem>
{/if} {/if}

View File

@ -16,6 +16,7 @@ import { userStore, userSelectedResourceMap, isOnlyUser } from "./users.js"
import { deploymentStore } from "./deployments.js" import { deploymentStore } from "./deployments.js"
import { contextMenuStore } from "./contextMenu.js" import { contextMenuStore } from "./contextMenu.js"
import { snippets } from "./snippets" import { snippets } from "./snippets"
import { screenComponentErrors } from "./screenComponent"
// Backend // Backend
import { tables } from "./tables" import { tables } from "./tables"
@ -67,6 +68,7 @@ export {
snippets, snippets,
rowActions, rowActions,
appPublished, appPublished,
screenComponentErrors,
} }
export const reset = () => { export const reset = () => {

View File

@ -0,0 +1,83 @@
import { derived } from "svelte/store"
import { tables } from "./tables"
import { selectedScreen } from "./screens"
import { viewsV2 } from "./viewsV2"
import { findComponentsBySettingsType } from "@/helpers/screen"
import { UIDatasourceType, Screen } from "@budibase/types"
import { queries } from "./queries"
import { views } from "./views"
import { featureFlag } from "@/helpers"
function reduceBy<TItem extends {}, TKey extends keyof TItem>(
key: TKey,
list: TItem[]
) {
return list.reduce(
(result, item) => ({
...result,
[item[key] as string]: item,
}),
{}
)
}
const friendlyNameByType: Partial<Record<UIDatasourceType, string>> = {
viewV2: "view",
}
const validationKeyByType: Record<UIDatasourceType, string | null> = {
table: "tableId",
view: "name",
viewV2: "id",
query: "_id",
custom: null,
}
export const screenComponentErrors = derived(
[selectedScreen, tables, views, viewsV2, queries],
([$selectedScreen, $tables, $views, $viewsV2, $queries]): Record<
string,
string[]
> => {
if (!featureFlag.isEnabled("CHECK_SCREEN_COMPONENT_SETTINGS_ERRORS")) {
return {}
}
function getInvalidDatasources(
screen: Screen,
datasources: Record<string, any>
) {
const result: Record<string, string[]> = {}
for (const { component, setting } of findComponentsBySettingsType(
screen,
["table", "dataSource"]
)) {
const componentSettings = component[setting.key]
const { label } = componentSettings
const type = componentSettings.type as UIDatasourceType
const validationKey = validationKeyByType[type]
if (!validationKey) {
continue
}
const resourceId = componentSettings[validationKey]
if (!datasources[resourceId]) {
const friendlyTypeName = friendlyNameByType[type] ?? type
result[component._id!] = [
`The ${friendlyTypeName} named "${label}" could not be found`,
]
}
}
return result
}
const datasources = {
...reduceBy("_id", $tables.list),
...reduceBy("name", $views.list),
...reduceBy("id", $viewsV2.list),
...reduceBy("_id", $queries.list),
}
return getInvalidDatasources($selectedScreen, datasources)
}
)

View File

@ -1,5 +1,5 @@
import { it, expect, describe, beforeEach, vi } from "vitest" import { it, expect, describe, beforeEach, vi } from "vitest"
import { createAdminStore } from "./admin" import { AdminStore } from "./admin"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { auth } from "@/stores/portal" import { auth } from "@/stores/portal"
@ -46,16 +46,7 @@ describe("admin store", () => {
ctx.writableReturn = { update: vi.fn(), subscribe: vi.fn() } ctx.writableReturn = { update: vi.fn(), subscribe: vi.fn() }
writable.mockReturnValue(ctx.writableReturn) writable.mockReturnValue(ctx.writableReturn)
ctx.returnedStore = createAdminStore() ctx.returnedStore = new AdminStore()
})
it("returns the created store", ctx => {
expect(ctx.returnedStore).toEqual({
subscribe: expect.toBe(ctx.writableReturn.subscribe),
init: expect.toBeFunc(),
unload: expect.toBeFunc(),
getChecklist: expect.toBeFunc(),
})
}) })
describe("init method", () => { describe("init method", () => {

View File

@ -1,4 +1,4 @@
import { writable, get } from "svelte/store" import { get } from "svelte/store"
import { API } from "@/api" import { API } from "@/api"
import { auth } from "@/stores/portal" import { auth } from "@/stores/portal"
import { banner } from "@budibase/bbui" import { banner } from "@budibase/bbui"
@ -7,15 +7,17 @@ import {
GetEnvironmentResponse, GetEnvironmentResponse,
SystemStatusResponse, SystemStatusResponse,
} from "@budibase/types" } from "@budibase/types"
import { BudiStore } from "../BudiStore"
interface PortalAdminStore extends GetEnvironmentResponse { interface AdminState extends GetEnvironmentResponse {
loaded: boolean loaded: boolean
checklist?: ConfigChecklistResponse checklist?: ConfigChecklistResponse
status?: SystemStatusResponse status?: SystemStatusResponse
} }
export function createAdminStore() { export class AdminStore extends BudiStore<AdminState> {
const admin = writable<PortalAdminStore>({ constructor() {
super({
loaded: false, loaded: false,
multiTenancy: false, multiTenancy: false,
cloud: false, cloud: false,
@ -24,25 +26,25 @@ export function createAdminStore() {
offlineMode: false, offlineMode: false,
maintenance: [], maintenance: [],
}) })
async function init() {
await getChecklist()
await getEnvironment()
// enable system status checks in the cloud
if (get(admin).cloud) {
await getSystemStatus()
checkStatus()
} }
admin.update(store => { async init() {
await this.getChecklist()
await this.getEnvironment()
// enable system status checks in the cloud
if (get(this.store).cloud) {
await this.getSystemStatus()
this.checkStatus()
}
this.update(store => {
store.loaded = true store.loaded = true
return store return store
}) })
} }
async function getEnvironment() { async getEnvironment() {
const environment = await API.getEnvironment() const environment = await API.getEnvironment()
admin.update(store => { this.update(store => {
store.multiTenancy = environment.multiTenancy store.multiTenancy = environment.multiTenancy
store.cloud = environment.cloud store.cloud = environment.cloud
store.disableAccountPortal = environment.disableAccountPortal store.disableAccountPortal = environment.disableAccountPortal
@ -56,43 +58,36 @@ export function createAdminStore() {
}) })
} }
const checkStatus = async () => { async checkStatus() {
const health = get(admin)?.status?.health const health = get(this.store).status?.health
if (!health?.passing) { if (!health?.passing) {
await banner.showStatus() await banner.showStatus()
} }
} }
async function getSystemStatus() { async getSystemStatus() {
const status = await API.getSystemStatus() const status = await API.getSystemStatus()
admin.update(store => { this.update(store => {
store.status = status store.status = status
return store return store
}) })
} }
async function getChecklist() { async getChecklist() {
const tenantId = get(auth).tenantId const tenantId = get(auth).tenantId
const checklist = await API.getChecklist(tenantId) const checklist = await API.getChecklist(tenantId)
admin.update(store => { this.update(store => {
store.checklist = checklist store.checklist = checklist
return store return store
}) })
} }
function unload() { unload() {
admin.update(store => { this.update(store => {
store.loaded = false store.loaded = false
return store return store
}) })
} }
return {
subscribe: admin.subscribe,
init,
unload,
getChecklist,
}
} }
export const admin = createAdminStore() export const admin = new AdminStore()

View File

@ -13,7 +13,7 @@ interface PortalAuditLogsStore {
logs?: SearchAuditLogsResponse logs?: SearchAuditLogsResponse
} }
export class AuditLogsStore extends BudiStore<PortalAuditLogsStore> { class AuditLogsStore extends BudiStore<PortalAuditLogsStore> {
constructor() { constructor() {
super({}) super({})
} }

View File

@ -121,8 +121,8 @@ class AuthStore extends BudiStore<PortalAuthStore> {
} }
} }
async login(username: string, password: string) { async login(username: string, password: string, targetTenantId?: string) {
const tenantId = get(this.store).tenantId const tenantId = targetTenantId || get(this.store).tenantId
await API.logIn(tenantId, username, password) await API.logIn(tenantId, username, password)
await this.getSelf() await this.getSelf()
} }

View File

@ -1,38 +1,31 @@
import { writable } from "svelte/store" import { BudiStore } from "../BudiStore"
type GotoFuncType = (path: string) => void type GotoFuncType = (path: string) => void
interface PortalNavigationStore { interface NavigationState {
initialisated: boolean initialisated: boolean
goto: GotoFuncType goto: GotoFuncType
} }
export function createNavigationStore() { class NavigationStore extends BudiStore<NavigationState> {
const store = writable<PortalNavigationStore>({ constructor() {
super({
initialisated: false, initialisated: false,
goto: undefined as any, goto: undefined as any,
}) })
const { set, subscribe } = store }
const init = (gotoFunc: GotoFuncType) => { init(gotoFunc: GotoFuncType) {
if (typeof gotoFunc !== "function") { if (typeof gotoFunc !== "function") {
throw new Error( throw new Error(
`gotoFunc must be a function, found a "${typeof gotoFunc}" instead` `gotoFunc must be a function, found a "${typeof gotoFunc}" instead`
) )
} }
this.set({
set({
initialisated: true, initialisated: true,
goto: gotoFunc, goto: gotoFunc,
}) })
} }
return {
subscribe,
actions: {
init,
},
}
} }
export const navigation = createNavigationStore() export const navigation = new NavigationStore()

View File

@ -1,16 +0,0 @@
import { writable } from "svelte/store"
import { API } from "@/api"
export function templatesStore() {
const { subscribe, set } = writable([])
return {
subscribe,
load: async () => {
const templates = await API.getAppTemplates()
set(templates)
},
}
}
export const templates = templatesStore()

View File

@ -0,0 +1,16 @@
import { API } from "@/api"
import { BudiStore } from "../BudiStore"
import { TemplateMetadata } from "@budibase/types"
class TemplateStore extends BudiStore<TemplateMetadata[]> {
constructor() {
super([])
}
async load() {
const templates = await API.getAppTemplates()
this.set(templates)
}
}
export const templates = new TemplateStore()

View File

@ -1,45 +0,0 @@
import { createLocalStorageStore } from "@budibase/frontend-core"
import { get } from "svelte/store"
export const createTemporalStore = () => {
const initialValue = {}
const localStorageKey = `bb-temporal`
const store = createLocalStorageStore(localStorageKey, initialValue)
const setExpiring = (key, data, duration) => {
const updated = {
...data,
expiry: Date.now() + duration * 1000,
}
store.update(state => ({
...state,
[key]: updated,
}))
}
const getExpiring = key => {
const entry = get(store)[key]
if (!entry) {
return
}
const currentExpiry = entry.expiry
if (currentExpiry < Date.now()) {
store.update(state => {
delete state[key]
return state
})
return null
} else {
return entry
}
}
return {
subscribe: store.subscribe,
actions: { setExpiring, getExpiring },
}
}
export const temporalStore = createTemporalStore()

View File

@ -0,0 +1,53 @@
import { get } from "svelte/store"
import { BudiStore, PersistenceType } from "../BudiStore"
type TemporalItem = Record<string, any> & { expiry: number }
type TemporalState = Record<string, TemporalItem>
class TemporalStore extends BudiStore<TemporalState> {
constructor() {
super(
{},
{
persistence: {
key: "bb-temporal",
type: PersistenceType.LOCAL,
},
}
)
}
setExpiring = (
key: string,
data: Record<string, any>,
durationSeconds: number
) => {
const updated: TemporalItem = {
...data,
expiry: Date.now() + durationSeconds * 1000,
}
this.update(state => ({
...state,
[key]: updated,
}))
}
getExpiring(key: string) {
const entry = get(this.store)[key]
if (!entry) {
return null
}
const currentExpiry = entry.expiry
if (currentExpiry < Date.now()) {
this.update(state => {
delete state[key]
return state
})
return null
} else {
return entry
}
}
}
export const temporalStore = new TemporalStore()

View File

@ -1,37 +0,0 @@
import { createLocalStorageStore } from "@budibase/frontend-core"
import { derived } from "svelte/store"
import {
DefaultBuilderTheme,
ensureValidTheme,
getThemeClassNames,
ThemeOptions,
ThemeClassPrefix,
} from "@budibase/shared-core"
export const getThemeStore = () => {
const themeElement = document.documentElement
const initialValue = {
theme: DefaultBuilderTheme,
}
const store = createLocalStorageStore("bb-theme", initialValue)
const derivedStore = derived(store, $store => ({
...$store,
theme: ensureValidTheme($store.theme, DefaultBuilderTheme),
}))
// Update theme class when store changes
derivedStore.subscribe(({ theme }) => {
const classNames = getThemeClassNames(theme).split(" ")
ThemeOptions.forEach(option => {
const className = `${ThemeClassPrefix}${option.id}`
themeElement.classList.toggle(className, classNames.includes(className))
})
})
return {
...store,
subscribe: derivedStore.subscribe,
}
}
export const themeStore = getThemeStore()

View File

@ -0,0 +1,45 @@
import { derived, Writable } from "svelte/store"
import {
DefaultBuilderTheme,
ensureValidTheme,
getThemeClassNames,
ThemeOptions,
ThemeClassPrefix,
} from "@budibase/shared-core"
import { Theme } from "@budibase/types"
import { DerivedBudiStore, PersistenceType } from "../BudiStore"
interface ThemeState {
theme: Theme
}
class ThemeStore extends DerivedBudiStore<ThemeState, ThemeState> {
constructor() {
const makeDerivedStore = (store: Writable<ThemeState>) => {
return derived(store, $store => ({
...$store,
theme: ensureValidTheme($store.theme, DefaultBuilderTheme),
}))
}
super({ theme: DefaultBuilderTheme }, makeDerivedStore, {
persistence: {
key: "bb-theme",
type: PersistenceType.LOCAL,
},
})
// Update theme class when store changes
this.subscribe(({ theme }) => {
const classNames = getThemeClassNames(theme).split(" ")
ThemeOptions.forEach(option => {
const className = `${ThemeClassPrefix}${option.id}`
document.documentElement.classList.toggle(
className,
classNames.includes(className)
)
})
})
}
}
export const themeStore = new ThemeStore()

View File

@ -103,6 +103,7 @@
let settingsDefinition let settingsDefinition
let settingsDefinitionMap let settingsDefinitionMap
let missingRequiredSettings = false let missingRequiredSettings = false
let componentErrors = false
// Temporary styles which can be added in the app preview for things like // Temporary styles which can be added in the app preview for things like
// DND. We clear these whenever a new instance is received. // DND. We clear these whenever a new instance is received.
@ -137,16 +138,21 @@
// Derive definition properties which can all be optional, so need to be // Derive definition properties which can all be optional, so need to be
// coerced to booleans // coerced to booleans
$: componentErrors = instance?._meta?.errors
$: hasChildren = !!definition?.hasChildren $: hasChildren = !!definition?.hasChildren
$: showEmptyState = definition?.showEmptyState !== false $: showEmptyState = definition?.showEmptyState !== false
$: hasMissingRequiredSettings = missingRequiredSettings?.length > 0 $: hasMissingRequiredSettings = missingRequiredSettings?.length > 0
$: editable = !!definition?.editable && !hasMissingRequiredSettings $: editable = !!definition?.editable && !hasMissingRequiredSettings
$: hasComponentErrors = componentErrors?.length > 0
$: requiredAncestors = definition?.requiredAncestors || [] $: requiredAncestors = definition?.requiredAncestors || []
$: missingRequiredAncestors = requiredAncestors.filter( $: missingRequiredAncestors = requiredAncestors.filter(
ancestor => !$component.ancestors.includes(`${BudibasePrefix}${ancestor}`) ancestor => !$component.ancestors.includes(`${BudibasePrefix}${ancestor}`)
) )
$: hasMissingRequiredAncestors = missingRequiredAncestors?.length > 0 $: hasMissingRequiredAncestors = missingRequiredAncestors?.length > 0
$: errorState = hasMissingRequiredSettings || hasMissingRequiredAncestors $: errorState =
hasMissingRequiredSettings ||
hasMissingRequiredAncestors ||
hasComponentErrors
// Interactive components can be selected, dragged and highlighted inside // Interactive components can be selected, dragged and highlighted inside
// the builder preview // the builder preview
@ -692,6 +698,7 @@
<ComponentErrorState <ComponentErrorState
{missingRequiredSettings} {missingRequiredSettings}
{missingRequiredAncestors} {missingRequiredAncestors}
{componentErrors}
/> />
{:else} {:else}
<svelte:component this={constructor} bind:this={ref} {...initialSettings}> <svelte:component this={constructor} bind:this={ref} {...initialSettings}>

View File

@ -14,7 +14,6 @@
GroupUserDatasource, GroupUserDatasource,
DataFetchOptions, DataFetchOptions,
} from "@budibase/types" } from "@budibase/types"
import { SDK, Component } from "../../index"
type ProviderDatasource = Exclude< type ProviderDatasource = Exclude<
DataFetchDatasource, DataFetchDatasource,
@ -29,8 +28,8 @@
export let paginate: boolean export let paginate: boolean
export let autoRefresh: number export let autoRefresh: number
const { styleable, Provider, ActionTypes, API } = getContext<SDK>("sdk") const { styleable, Provider, ActionTypes, API } = getContext("sdk")
const component = getContext<Component>("component") const component = getContext("component")
let interval: ReturnType<typeof setInterval> let interval: ReturnType<typeof setInterval>
let queryExtensions: Record<string, any> = {} let queryExtensions: Record<string, any> = {}

View File

@ -1,37 +1,43 @@
<script> <script lang="ts">
import { getContext } from "svelte" import { getContext } from "svelte"
import InnerFormBlock from "./InnerFormBlock.svelte" import InnerFormBlock from "./InnerFormBlock.svelte"
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
import FormBlockWrapper from "./FormBlockWrapper.svelte" import FormBlockWrapper from "./FormBlockWrapper.svelte"
import { get } from "svelte/store" import { get } from "svelte/store"
import { TableSchema, UIDatasource } from "@budibase/types"
export let actionType type Field = { name: string; active: boolean }
export let dataSource
export let size export let actionType: string
export let disabled export let dataSource: UIDatasource
export let fields export let size: string
export let buttons export let disabled: boolean
export let buttonPosition export let fields: (Field | string)[]
export let title export let buttons: {
export let description "##eventHandlerType": string
export let rowId parameters: Record<string, string>
export let actionUrl }[]
export let noRowsMessage export let buttonPosition: "top" | "bottom"
export let notificationOverride export let title: string
export let buttonsCollapsed export let description: string
export let buttonsCollapsedText export let rowId: string
export let actionUrl: string
export let noRowsMessage: string
export let notificationOverride: boolean
export let buttonsCollapsed: boolean
export let buttonsCollapsedText: string
// Legacy // Legacy
export let showDeleteButton export let showDeleteButton: boolean
export let showSaveButton export let showSaveButton: boolean
export let saveButtonLabel export let saveButtonLabel: boolean
export let deleteButtonLabel export let deleteButtonLabel: boolean
const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk") const { fetchDatasourceSchema, generateGoldenSample } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
const context = getContext("context") const context = getContext("context")
let schema let schema: TableSchema
$: fetchSchema(dataSource) $: fetchSchema(dataSource)
$: id = $component.id $: id = $component.id
@ -61,7 +67,7 @@
} }
} }
const convertOldFieldFormat = fields => { const convertOldFieldFormat = (fields: (Field | string)[]): Field[] => {
if (!fields) { if (!fields) {
return [] return []
} }
@ -82,11 +88,11 @@
}) })
} }
const getDefaultFields = (fields, schema) => { const getDefaultFields = (fields: Field[], schema: TableSchema) => {
if (!schema) { if (!schema) {
return [] return []
} }
let defaultFields = [] let defaultFields: Field[] = []
if (!fields || fields.length === 0) { if (!fields || fields.length === 0) {
Object.values(schema) Object.values(schema)
@ -101,15 +107,14 @@
return [...fields, ...defaultFields].filter(field => field.active) return [...fields, ...defaultFields].filter(field => field.active)
} }
const fetchSchema = async () => { const fetchSchema = async (datasource: UIDatasource) => {
schema = (await fetchDatasourceSchema(dataSource)) || {} schema = (await fetchDatasourceSchema(datasource)) || {}
} }
</script> </script>
<FormBlockWrapper {actionType} {dataSource} {rowId} {noRowsMessage}> <FormBlockWrapper {actionType} {dataSource} {rowId} {noRowsMessage}>
<InnerFormBlock <InnerFormBlock
{dataSource} {dataSource}
{actionUrl}
{actionType} {actionType}
{size} {size}
{disabled} {disabled}
@ -117,7 +122,6 @@
{title} {title}
{description} {description}
{schema} {schema}
{notificationOverride}
buttons={buttonsOrDefault} buttons={buttonsOrDefault}
buttonPosition={buttons ? buttonPosition : "top"} buttonPosition={buttons ? buttonPosition : "top"}
{buttonsCollapsed} {buttonsCollapsed}

View File

@ -1,11 +1,14 @@
<script> <script lang="ts">
import { getContext } from "svelte" import { getContext } from "svelte"
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import MissingRequiredSetting from "./MissingRequiredSetting.svelte" import MissingRequiredSetting from "./MissingRequiredSetting.svelte"
import MissingRequiredAncestor from "./MissingRequiredAncestor.svelte" import MissingRequiredAncestor from "./MissingRequiredAncestor.svelte"
export let missingRequiredSettings export let missingRequiredSettings:
export let missingRequiredAncestors | { key: string; label: string }[]
| undefined
export let missingRequiredAncestors: string[] | undefined
export let componentErrors: string[] | undefined
const component = getContext("component") const component = getContext("component")
const { styleable, builderStore } = getContext("sdk") const { styleable, builderStore } = getContext("sdk")
@ -13,6 +16,7 @@
$: styles = { ...$component.styles, normal: {}, custom: null, empty: true } $: styles = { ...$component.styles, normal: {}, custom: null, empty: true }
$: requiredSetting = missingRequiredSettings?.[0] $: requiredSetting = missingRequiredSettings?.[0]
$: requiredAncestor = missingRequiredAncestors?.[0] $: requiredAncestor = missingRequiredAncestors?.[0]
$: errorMessage = componentErrors?.[0]
</script> </script>
{#if $builderStore.inBuilder} {#if $builderStore.inBuilder}
@ -21,6 +25,8 @@
<Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" /> <Icon name="Alert" color="var(--spectrum-global-color-static-red-600)" />
{#if requiredAncestor} {#if requiredAncestor}
<MissingRequiredAncestor {requiredAncestor} /> <MissingRequiredAncestor {requiredAncestor} />
{:else if errorMessage}
{errorMessage}
{:else if requiredSetting} {:else if requiredSetting}
<MissingRequiredSetting {requiredSetting} /> <MissingRequiredSetting {requiredSetting} />
{/if} {/if}
@ -32,7 +38,7 @@
.component-placeholder { .component-placeholder {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: center;
align-items: center; align-items: center;
color: var(--spectrum-global-color-gray-600); color: var(--spectrum-global-color-gray-600);
font-size: var(--font-size-s); font-size: var(--font-size-s);

7
packages/client/src/context.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import { Component, Context, SDK } from "."
declare module "svelte" {
export function getContext(key: "sdk"): SDK
export function getContext(key: "component"): Component
export function getContext(key: "context"): Context
}

View File

@ -43,6 +43,7 @@ const loadBudibase = async () => {
usedPlugins: window["##BUDIBASE_USED_PLUGINS##"], usedPlugins: window["##BUDIBASE_USED_PLUGINS##"],
location: window["##BUDIBASE_LOCATION##"], location: window["##BUDIBASE_LOCATION##"],
snippets: window["##BUDIBASE_SNIPPETS##"], snippets: window["##BUDIBASE_SNIPPETS##"],
componentErrors: window["##BUDIBASE_COMPONENT_ERRORS##"],
}) })
// Set app ID - this window flag is set by both the preview and the real // Set app ID - this window flag is set by both the preview and the real

View File

@ -7,9 +7,17 @@ export interface SDK {
styleable: any styleable: any
Provider: any Provider: any
ActionTypes: typeof ActionTypes ActionTypes: typeof ActionTypes
fetchDatasourceSchema: any
generateGoldenSample: any
builderStore: Readable<{
inBuilder: boolean
}>
} }
export type Component = Readable<{ export type Component = Readable<{
id: string id: string
styles: any styles: any
errorState: boolean
}> }>
export type Context = Readable<{}>

View File

@ -19,6 +19,7 @@ const createBuilderStore = () => {
eventResolvers: {}, eventResolvers: {},
metadata: null, metadata: null,
snippets: null, snippets: null,
componentErrors: {},
// Legacy - allow the builder to specify a layout // Legacy - allow the builder to specify a layout
layout: null, layout: null,

View File

@ -42,6 +42,14 @@ const createScreenStore = () => {
if ($builderStore.layout) { if ($builderStore.layout) {
activeLayout = $builderStore.layout activeLayout = $builderStore.layout
} }
// Attach meta
const errors = $builderStore.componentErrors || {}
const attachComponentMeta = component => {
component._meta = { errors: errors[component._id] || [] }
component._children?.forEach(attachComponentMeta)
}
attachComponentMeta(activeScreen.props)
} else { } else {
// Find the correct screen by matching the current route // Find the correct screen by matching the current route
screens = $appStore.screens || [] screens = $appStore.screens || []

View File

@ -1,5 +1,6 @@
import { API } from "api" import { API } from "api"
import { DataFetchMap, DataFetchType } from "@budibase/frontend-core" import { DataFetchMap, DataFetchType } from "@budibase/frontend-core"
import { FieldType, TableSchema } from "@budibase/types"
/** /**
* Constructs a fetch instance for a given datasource. * Constructs a fetch instance for a given datasource.
@ -42,14 +43,14 @@ export const fetchDatasourceSchema = async <
} }
// Get the normal schema as long as we aren't wanting a form schema // Get the normal schema as long as we aren't wanting a form schema
let schema: any let schema: TableSchema | undefined
if (datasource?.type !== "query" || !options?.formSchema) { if (datasource?.type !== "query" || !options?.formSchema) {
schema = instance.getSchema(definition as any) schema = instance.getSchema(definition as any) as TableSchema
} else if ("parameters" in definition && definition.parameters?.length) { } else if ("parameters" in definition && definition.parameters?.length) {
schema = {} schema = {}
definition.parameters.forEach(param => { for (const param of definition.parameters) {
schema[param.name] = { ...param, type: "string" } schema[param.name] = { ...param, type: FieldType.STRING }
}) }
} }
if (!schema) { if (!schema) {
return null return null
@ -57,11 +58,11 @@ export const fetchDatasourceSchema = async <
// Strip hidden fields from views // Strip hidden fields from views
if (datasource.type === "viewV2") { if (datasource.type === "viewV2") {
Object.keys(schema).forEach(field => { for (const field of Object.keys(schema)) {
if (!schema[field].visible) { if (!schema[field].visible) {
delete schema[field] delete schema[field]
} }
}) }
} }
// Enrich schema with relationships if required // Enrich schema with relationships if required

View File

@ -1,8 +1,8 @@
import { GetOldMigrationStatus } from "@budibase/types" import { GetMigrationStatus } from "@budibase/types"
import { BaseAPIClient } from "./types" import { BaseAPIClient } from "./types"
export interface MigrationEndpoints { export interface MigrationEndpoints {
getMigrationStatus: () => Promise<GetOldMigrationStatus> getMigrationStatus: () => Promise<GetMigrationStatus>
} }
export const buildMigrationEndpoints = ( export const buildMigrationEndpoints = (

View File

@ -1,5 +1,3 @@
// TODO: datasource and defitions are unions of the different implementations. At this point, the datasource does not know what type is being used, and the assignations will cause TS exceptions. Casting it "as any" for now. This should be fixed improving the type usages.
import { derived, get, Readable, Writable } from "svelte/store" import { derived, get, Readable, Writable } from "svelte/store"
import { import {
DataFetchDefinition, DataFetchDefinition,
@ -10,12 +8,10 @@ import { enrichSchemaWithRelColumns, memo } from "../../../utils"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import { import {
SaveRowRequest, SaveRowRequest,
SaveTableRequest,
UIDatasource, UIDatasource,
UIFieldMutation, UIFieldMutation,
UIFieldSchema, UIFieldSchema,
UIRow, UIRow,
UpdateViewRequest,
ViewV2Type, ViewV2Type,
} from "@budibase/types" } from "@budibase/types"
import { Store as StoreContext, BaseStoreProps } from "." import { Store as StoreContext, BaseStoreProps } from "."
@ -79,7 +75,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
const schema = derived(definition, $definition => { const schema = derived(definition, $definition => {
const schema: Record<string, any> | undefined = getDatasourceSchema({ const schema: Record<string, any> | undefined = getDatasourceSchema({
API, API,
datasource: get(datasource) as any, // TODO: see line 1 datasource: get(datasource),
definition: $definition ?? undefined, definition: $definition ?? undefined,
}) })
if (!schema) { if (!schema) {
@ -137,7 +133,7 @@ export const deriveStores = (context: StoreContext): DerivedDatasourceStore => {
let type = $datasource?.type let type = $datasource?.type
// @ts-expect-error // @ts-expect-error
if (type === "provider") { if (type === "provider") {
type = ($datasource as any).value?.datasource?.type // TODO: see line 1 type = ($datasource as any).value?.datasource?.type
} }
// Handle calculation views // Handle calculation views
if ( if (
@ -196,15 +192,13 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
const refreshDefinition = async () => { const refreshDefinition = async () => {
const def = await getDatasourceDefinition({ const def = await getDatasourceDefinition({
API, API,
datasource: get(datasource) as any, // TODO: see line 1 datasource: get(datasource),
}) })
definition.set(def as any) // TODO: see line 1 definition.set(def ?? null)
} }
// Saves the datasource definition // Saves the datasource definition
const saveDefinition = async ( const saveDefinition = async (newDefinition: DataFetchDefinition) => {
newDefinition: SaveTableRequest | UpdateViewRequest
) => {
// Update local state // Update local state
const originalDefinition = get(definition) const originalDefinition = get(definition)
definition.set(newDefinition) definition.set(newDefinition)
@ -245,7 +239,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
delete newDefinition.schema[column].default delete newDefinition.schema[column].default
} }
} }
return await saveDefinition(newDefinition as any) // TODO: see line 1 return await saveDefinition(newDefinition)
} }
// Adds a schema mutation for a single field // Adds a schema mutation for a single field
@ -321,7 +315,7 @@ export const createActions = (context: StoreContext): ActionDatasourceStore => {
await saveDefinition({ await saveDefinition({
...$definition, ...$definition,
schema: newSchema, schema: newSchema,
} as any) // TODO: see line 1 })
resetSchemaMutations() resetSchemaMutations()
} }

View File

@ -21,7 +21,7 @@ export default class ViewFetch extends BaseDataFetch<ViewV1Datasource, Table> {
getSchema(definition: Table) { getSchema(definition: Table) {
const { datasource } = this.options const { datasource } = this.options
return definition?.views?.[datasource.name]?.schema return definition?.views?.[datasource?.name]?.schema
} }
async getData() { async getData() {

View File

@ -101,12 +101,12 @@ export const fetchData = <
// Creates an empty fetch instance with no datasource configured, so no data // Creates an empty fetch instance with no datasource configured, so no data
// will initially be loaded // will initially be loaded
const createEmptyFetchInstance = ({ const createEmptyFetchInstance = <T extends DataFetchDatasource>({
API, API,
datasource, datasource,
}: { }: {
API: APIClient API: APIClient
datasource: DataFetchDatasource datasource: T
}) => { }) => {
const handler = DataFetchMap[datasource?.type] const handler = DataFetchMap[datasource?.type]
if (!handler) { if (!handler) {
@ -114,7 +114,7 @@ const createEmptyFetchInstance = ({
} }
return new handler({ return new handler({
API, API,
datasource: null as never, datasource: datasource as any,
query: null as any, query: null as any,
}) })
} }

View File

@ -7,7 +7,7 @@
"../shared-core/src", "../shared-core/src",
"../string-templates/src" "../string-templates/src"
], ],
"ext": "js,ts,json,svelte", "ext": "js,ts,json,svelte,hbs",
"ignore": [ "ignore": [
"**/*.spec.ts", "**/*.spec.ts",
"**/*.spec.js", "**/*.spec.js",

View File

@ -43,7 +43,6 @@ async function init() {
BB_ADMIN_USER_EMAIL: "", BB_ADMIN_USER_EMAIL: "",
BB_ADMIN_USER_PASSWORD: "", BB_ADMIN_USER_PASSWORD: "",
PLUGINS_DIR: "", PLUGINS_DIR: "",
HTTP_MIGRATIONS: "0",
HTTP_LOGGING: "0", HTTP_LOGGING: "0",
VERSION: "0.0.0+local", VERSION: "0.0.0+local",
PASSWORD_MIN_LENGTH: "1", PASSWORD_MIN_LENGTH: "1",

View File

@ -27,7 +27,6 @@ import {
env as envCore, env as envCore,
ErrorCode, ErrorCode,
events, events,
migrations,
objectStore, objectStore,
roles, roles,
tenancy, tenancy,
@ -43,7 +42,6 @@ import { groups, licensing, quotas } from "@budibase/pro"
import { import {
App, App,
Layout, Layout,
MigrationType,
PlanType, PlanType,
Screen, Screen,
UserCtx, UserCtx,
@ -488,13 +486,6 @@ async function creationEvents(request: BBRequest<CreateAppRequest>, app: App) {
} }
async function appPostCreate(ctx: UserCtx<CreateAppRequest, App>, app: App) { async function appPostCreate(ctx: UserCtx<CreateAppRequest, App>, app: App) {
const tenantId = tenancy.getTenantId()
await migrations.backPopulateMigrations({
type: MigrationType.APP,
tenantId,
appId: app.appId,
})
await creationEvents(ctx.request, app) await creationEvents(ctx.request, app)
// app import, template creation and duplication // app import, template creation and duplication

View File

@ -2,7 +2,7 @@ import * as triggers from "../../automations/triggers"
import { sdk as coreSdk } from "@budibase/shared-core" import { sdk as coreSdk } from "@budibase/shared-core"
import { DocumentType } from "../../db/utils" import { DocumentType } from "../../db/utils"
import { updateTestHistory, removeDeprecated } from "../../automations/utils" import { updateTestHistory, removeDeprecated } from "../../automations/utils"
import { setTestFlag, clearTestFlag } from "../../utilities/redis" import { withTestFlag } from "../../utilities/redis"
import { context, cache, events, db as dbCore } from "@budibase/backend-core" import { context, cache, events, db as dbCore } from "@budibase/backend-core"
import { automations, features } from "@budibase/pro" import { automations, features } from "@budibase/pro"
import { import {
@ -231,24 +231,25 @@ export async function test(
ctx: UserCtx<TestAutomationRequest, TestAutomationResponse> ctx: UserCtx<TestAutomationRequest, TestAutomationResponse>
) { ) {
const db = context.getAppDB() const db = context.getAppDB()
let automation = await db.get<Automation>(ctx.params.id) const automation = await db.tryGet<Automation>(ctx.params.id)
await setTestFlag(automation._id!) if (!automation) {
const testInput = prepareTestInput(ctx.request.body) ctx.throw(404, `Automation ${ctx.params.id} not found`)
const response = await triggers.externalTrigger( }
const { request, appId } = ctx
const { body } = request
ctx.body = await withTestFlag(automation._id!, async () => {
const occurredAt = new Date().getTime()
await updateTestHistory(appId, automation, { ...body, occurredAt })
const user = sdk.users.getUserContextBindings(ctx.user)
return await triggers.externalTrigger(
automation, automation,
{ { ...prepareTestInput(body), appId, user },
...testInput,
appId: ctx.appId,
user: sdk.users.getUserContextBindings(ctx.user),
},
{ getResponses: true } { getResponses: true }
) )
// save a test history run
await updateTestHistory(ctx.appId, automation, {
...ctx.request.body,
occurredAt: new Date().getTime(),
}) })
await clearTestFlag(automation._id!)
ctx.body = response
await events.automation.tested(automation) await events.automation.tested(automation)
} }

View File

@ -1,35 +1,11 @@
import { context } from "@budibase/backend-core" import { context } from "@budibase/backend-core"
import { migrate as migrationImpl, MIGRATIONS } from "../../migrations" import { Ctx, GetMigrationStatus } from "@budibase/types"
import {
Ctx,
FetchOldMigrationResponse,
GetOldMigrationStatus,
RuneOldMigrationResponse,
RunOldMigrationRequest,
} from "@budibase/types"
import { import {
getAppMigrationVersion, getAppMigrationVersion,
getLatestEnabledMigrationId, getLatestEnabledMigrationId,
} from "../../appMigrations" } from "../../appMigrations"
export async function migrate( export async function getMigrationStatus(ctx: Ctx<void, GetMigrationStatus>) {
ctx: Ctx<RunOldMigrationRequest, RuneOldMigrationResponse>
) {
const options = ctx.request.body
// don't await as can take a while, just return
migrationImpl(options)
ctx.body = { message: "Migration started." }
}
export async function fetchDefinitions(
ctx: Ctx<void, FetchOldMigrationResponse>
) {
ctx.body = MIGRATIONS
}
export async function getMigrationStatus(
ctx: Ctx<void, GetOldMigrationStatus>
) {
const appId = context.getAppId() const appId = context.getAppId()
if (!appId) { if (!appId) {

View File

@ -73,7 +73,8 @@
hiddenComponentIds, hiddenComponentIds,
usedPlugins, usedPlugins,
location, location,
snippets snippets,
componentErrors
} = parsed } = parsed
// Set some flags so the app knows we're in the builder // Set some flags so the app knows we're in the builder
@ -91,6 +92,7 @@
window["##BUDIBASE_USED_PLUGINS##"] = usedPlugins window["##BUDIBASE_USED_PLUGINS##"] = usedPlugins
window["##BUDIBASE_LOCATION##"] = location window["##BUDIBASE_LOCATION##"] = location
window["##BUDIBASE_SNIPPETS##"] = snippets window["##BUDIBASE_SNIPPETS##"] = snippets
window['##BUDIBASE_COMPONENT_ERRORS##'] = componentErrors
// Initialise app // Initialise app
try { try {

View File

@ -123,9 +123,11 @@ async function parseSchema(view: CreateViewRequest) {
} }
export async function get(ctx: Ctx<void, ViewResponseEnriched>) { export async function get(ctx: Ctx<void, ViewResponseEnriched>) {
ctx.body = { const view = await sdk.views.getEnriched(ctx.params.viewId)
data: await sdk.views.getEnriched(ctx.params.viewId), if (!view) {
ctx.throw(404)
} }
ctx.body = { data: view }
} }
export async function fetch(ctx: Ctx<void, ViewFetchResponseEnriched>) { export async function fetch(ctx: Ctx<void, ViewFetchResponseEnriched>) {

View File

@ -1,16 +1,8 @@
import Router from "@koa/router" import Router from "@koa/router"
import * as migrationsController from "../controllers/migrations" import * as migrationsController from "../controllers/migrations"
import { auth } from "@budibase/backend-core"
const router: Router = new Router() const router: Router = new Router()
router router.get("/api/migrations/status", migrationsController.getMigrationStatus)
.post("/api/migrations/run", auth.internalApi, migrationsController.migrate)
.get(
"/api/migrations/definitions",
auth.internalApi,
migrationsController.fetchDefinitions
)
.get("/api/migrations/status", migrationsController.getMigrationStatus)
export default router export default router

View File

@ -19,9 +19,11 @@ import {
Table, Table,
} from "@budibase/types" } from "@budibase/types"
import { mocks } from "@budibase/backend-core/tests" import { mocks } from "@budibase/backend-core/tests"
import { FilterConditions } from "../../../automations/steps/filter"
import { removeDeprecated } from "../../../automations/utils" import { removeDeprecated } from "../../../automations/utils"
import { createAutomationBuilder } from "../../../automations/tests/utilities/AutomationTestBuilder" import { createAutomationBuilder } from "../../../automations/tests/utilities/AutomationTestBuilder"
import { automations } from "@budibase/shared-core"
const FilterConditions = automations.steps.filter.FilterConditions
const MAX_RETRIES = 4 const MAX_RETRIES = 4
let { let {

View File

@ -1,3 +1,4 @@
import { automations } from "@budibase/shared-core"
import * as sendSmtpEmail from "./steps/sendSmtpEmail" import * as sendSmtpEmail from "./steps/sendSmtpEmail"
import * as createRow from "./steps/createRow" import * as createRow from "./steps/createRow"
import * as updateRow from "./steps/updateRow" import * as updateRow from "./steps/updateRow"
@ -14,11 +15,10 @@ import * as make from "./steps/make"
import * as filter from "./steps/filter" import * as filter from "./steps/filter"
import * as delay from "./steps/delay" import * as delay from "./steps/delay"
import * as queryRow from "./steps/queryRows" import * as queryRow from "./steps/queryRows"
import * as loop from "./steps/loop"
import * as collect from "./steps/collect" import * as collect from "./steps/collect"
import * as branch from "./steps/branch"
import * as triggerAutomationRun from "./steps/triggerAutomationRun" import * as triggerAutomationRun from "./steps/triggerAutomationRun"
import * as openai from "./steps/openai" import * as openai from "./steps/openai"
import * as bash from "./steps/bash"
import env from "../environment" import env from "../environment"
import { import {
PluginType, PluginType,
@ -62,42 +62,39 @@ export const BUILTIN_ACTION_DEFINITIONS: Record<
string, string,
AutomationStepDefinition AutomationStepDefinition
> = { > = {
SEND_EMAIL_SMTP: sendSmtpEmail.definition, SEND_EMAIL_SMTP: automations.steps.sendSmtpEmail.definition,
CREATE_ROW: createRow.definition, CREATE_ROW: automations.steps.createRow.definition,
UPDATE_ROW: updateRow.definition, UPDATE_ROW: automations.steps.updateRow.definition,
DELETE_ROW: deleteRow.definition, DELETE_ROW: automations.steps.deleteRow.definition,
OUTGOING_WEBHOOK: outgoingWebhook.definition, OUTGOING_WEBHOOK: automations.steps.outgoingWebhook.definition,
EXECUTE_SCRIPT: executeScript.definition, EXECUTE_SCRIPT: automations.steps.executeScript.definition,
EXECUTE_QUERY: executeQuery.definition, EXECUTE_QUERY: automations.steps.executeQuery.definition,
SERVER_LOG: serverLog.definition, SERVER_LOG: automations.steps.serverLog.definition,
DELAY: delay.definition, DELAY: automations.steps.delay.definition,
FILTER: filter.definition, FILTER: automations.steps.filter.definition,
QUERY_ROWS: queryRow.definition, QUERY_ROWS: automations.steps.queryRows.definition,
LOOP: loop.definition, LOOP: automations.steps.loop.definition,
COLLECT: collect.definition, COLLECT: automations.steps.collect.definition,
TRIGGER_AUTOMATION_RUN: triggerAutomationRun.definition, TRIGGER_AUTOMATION_RUN: automations.steps.triggerAutomationRun.definition,
BRANCH: branch.definition, BRANCH: automations.steps.branch.definition,
// these used to be lowercase step IDs, maintain for backwards compat // these used to be lowercase step IDs, maintain for backwards compat
discord: discord.definition, discord: automations.steps.discord.definition,
slack: slack.definition, slack: automations.steps.slack.definition,
zapier: zapier.definition, zapier: automations.steps.zapier.definition,
integromat: make.definition, integromat: automations.steps.make.definition,
n8n: n8n.definition, n8n: automations.steps.n8n.definition,
} }
// don't add the bash script/definitions unless in self host // don't add the bash script/definitions unless in self host
// the fact this isn't included in any definitions means it cannot be // the fact this isn't included in any definitions means it cannot be
// ran at all // ran at all
if (env.SELF_HOSTED) { if (env.SELF_HOSTED) {
const bash = require("./steps/bash") // @ts-expect-error
// @ts-ignore
ACTION_IMPLS["EXECUTE_BASH"] = bash.run ACTION_IMPLS["EXECUTE_BASH"] = bash.run
// @ts-ignore BUILTIN_ACTION_DEFINITIONS["EXECUTE_BASH"] = automations.steps.bash.definition
BUILTIN_ACTION_DEFINITIONS["EXECUTE_BASH"] = bash.definition
if (env.isTest()) { if (env.isTest()) {
BUILTIN_ACTION_DEFINITIONS["OPENAI"] = openai.definition BUILTIN_ACTION_DEFINITIONS["OPENAI"] = automations.steps.openai.definition
} }
} }
@ -105,7 +102,7 @@ export async function getActionDefinitions(): Promise<
Record<keyof typeof AutomationActionStepId, AutomationStepDefinition> Record<keyof typeof AutomationActionStepId, AutomationStepDefinition>
> { > {
if (env.SELF_HOSTED) { if (env.SELF_HOSTED) {
BUILTIN_ACTION_DEFINITIONS["OPENAI"] = openai.definition BUILTIN_ACTION_DEFINITIONS["OPENAI"] = automations.steps.openai.definition
} }
const actionDefinitions = BUILTIN_ACTION_DEFINITIONS const actionDefinitions = BUILTIN_ACTION_DEFINITIONS

View File

@ -2,55 +2,7 @@ import { execSync } from "child_process"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
import * as automationUtils from "../automationUtils" import * as automationUtils from "../automationUtils"
import environment from "../../environment" import environment from "../../environment"
import { import { BashStepInputs, BashStepOutputs } from "@budibase/types"
AutomationActionStepId,
AutomationCustomIOType,
AutomationFeature,
AutomationIOType,
AutomationStepDefinition,
AutomationStepType,
BashStepInputs,
BashStepOutputs,
} from "@budibase/types"
export const definition: AutomationStepDefinition = {
name: "Bash Scripting",
tagline: "Execute a bash command",
icon: "JourneyEvent",
description: "Run a bash script",
type: AutomationStepType.ACTION,
internal: true,
features: {
[AutomationFeature.LOOPING]: true,
},
stepId: AutomationActionStepId.EXECUTE_BASH,
inputs: {},
schema: {
inputs: {
properties: {
code: {
type: AutomationIOType.STRING,
customType: AutomationCustomIOType.CODE,
title: "Code",
},
},
required: ["code"],
},
outputs: {
properties: {
stdout: {
type: AutomationIOType.STRING,
description: "Standard output of your bash command or script",
},
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the command was successful",
},
},
required: ["stdout"],
},
},
}
export async function run({ export async function run({
inputs, inputs,

View File

@ -1,48 +1,4 @@
import { import { CollectStepInputs, CollectStepOutputs } from "@budibase/types"
AutomationActionStepId,
AutomationStepDefinition,
AutomationStepType,
AutomationIOType,
CollectStepInputs,
CollectStepOutputs,
} from "@budibase/types"
export const definition: AutomationStepDefinition = {
name: "Collect Data",
tagline: "Collect data to be sent to design",
icon: "Collection",
description:
"Collects specified data so it can be provided to the design section",
type: AutomationStepType.ACTION,
internal: true,
features: {},
stepId: AutomationActionStepId.COLLECT,
inputs: {},
schema: {
inputs: {
properties: {
collection: {
type: AutomationIOType.STRING,
title: "What to Collect",
},
},
required: ["collection"],
},
outputs: {
properties: {
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
value: {
type: AutomationIOType.STRING,
description: "Collected data",
},
},
required: ["success", "value"],
},
},
}
export async function run({ export async function run({
inputs, inputs,

View File

@ -6,75 +6,10 @@ import {
} from "../automationUtils" } from "../automationUtils"
import { buildCtx } from "./utils" import { buildCtx } from "./utils"
import { import {
AutomationActionStepId, ContextEmitter,
AutomationCustomIOType,
AutomationFeature,
AutomationIOType,
AutomationStepDefinition,
AutomationStepType,
CreateRowStepInputs, CreateRowStepInputs,
CreateRowStepOutputs, CreateRowStepOutputs,
} from "@budibase/types" } from "@budibase/types"
import { EventEmitter } from "events"
export const definition: AutomationStepDefinition = {
name: "Create Row",
tagline: "Create a {{inputs.enriched.table.name}} row",
icon: "TableRowAddBottom",
description: "Add a row to your database",
type: AutomationStepType.ACTION,
internal: true,
features: {
[AutomationFeature.LOOPING]: true,
},
stepId: AutomationActionStepId.CREATE_ROW,
inputs: {},
schema: {
inputs: {
properties: {
row: {
type: AutomationIOType.OBJECT,
properties: {
tableId: {
type: AutomationIOType.STRING,
customType: AutomationCustomIOType.TABLE,
},
},
customType: AutomationCustomIOType.ROW,
title: "Table",
required: ["tableId"],
},
},
required: ["row"],
},
outputs: {
properties: {
row: {
type: AutomationIOType.OBJECT,
customType: AutomationCustomIOType.ROW,
description: "The new row",
},
response: {
type: AutomationIOType.OBJECT,
description: "The response from the table",
},
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the row creation was successful",
},
id: {
type: AutomationIOType.STRING,
description: "The identifier of the new row",
},
revision: {
type: AutomationIOType.STRING,
description: "The revision of the new row",
},
},
required: ["success", "id", "revision"],
},
},
}
export async function run({ export async function run({
inputs, inputs,
@ -83,7 +18,7 @@ export async function run({
}: { }: {
inputs: CreateRowStepInputs inputs: CreateRowStepInputs
appId: string appId: string
emitter: EventEmitter emitter: ContextEmitter
}): Promise<CreateRowStepOutputs> { }): Promise<CreateRowStepOutputs> {
if (inputs.row == null || inputs.row.tableId == null) { if (inputs.row == null || inputs.row.tableId == null) {
return { return {

View File

@ -1,44 +1,5 @@
import { wait } from "../../utilities" import { wait } from "../../utilities"
import { import { DelayStepInputs, DelayStepOutputs } from "@budibase/types"
AutomationActionStepId,
AutomationIOType,
AutomationStepDefinition,
AutomationStepType,
DelayStepInputs,
DelayStepOutputs,
} from "@budibase/types"
export const definition: AutomationStepDefinition = {
name: "Delay",
icon: "Clock",
tagline: "Delay for {{inputs.time}} milliseconds",
description: "Delay the automation until an amount of time has passed",
stepId: AutomationActionStepId.DELAY,
internal: true,
features: {},
inputs: {},
schema: {
inputs: {
properties: {
time: {
type: AutomationIOType.NUMBER,
title: "Delay in milliseconds",
},
},
required: ["time"],
},
outputs: {
properties: {
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the delay was successful",
},
},
required: ["success"],
},
},
type: AutomationStepType.LOGIC,
}
export async function run({ export async function run({
inputs, inputs,

View File

@ -1,66 +1,12 @@
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, ContextEmitter,
AutomationStepType,
AutomationIOType,
AutomationCustomIOType,
AutomationFeature,
DeleteRowStepInputs, DeleteRowStepInputs,
DeleteRowStepOutputs, DeleteRowStepOutputs,
AutomationStepDefinition,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepDefinition = {
description: "Delete a row from your database",
icon: "TableRowRemoveCenter",
name: "Delete Row",
tagline: "Delete a {{inputs.enriched.table.name}} row",
type: AutomationStepType.ACTION,
stepId: AutomationActionStepId.DELETE_ROW,
internal: true,
features: {
[AutomationFeature.LOOPING]: true,
},
inputs: {},
schema: {
inputs: {
properties: {
tableId: {
type: AutomationIOType.STRING,
customType: AutomationCustomIOType.TABLE,
title: "Table",
},
id: {
type: AutomationIOType.STRING,
title: "Row ID",
},
},
required: ["tableId", "id"],
},
outputs: {
properties: {
row: {
type: AutomationIOType.OBJECT,
customType: AutomationCustomIOType.ROW,
description: "The deleted row",
},
response: {
type: AutomationIOType.OBJECT,
description: "The response from the table",
},
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the deletion was successful",
},
},
required: ["row", "success"],
},
},
}
export async function run({ export async function run({
inputs, inputs,
appId, appId,
@ -68,7 +14,7 @@ export async function run({
}: { }: {
inputs: DeleteRowStepInputs inputs: DeleteRowStepInputs
appId: string appId: string
emitter: EventEmitter emitter: ContextEmitter
}): Promise<DeleteRowStepOutputs> { }): Promise<DeleteRowStepOutputs> {
if (inputs.id == null) { if (inputs.id == null) {
return { return {

View File

@ -1,71 +1,10 @@
import fetch from "node-fetch" import fetch from "node-fetch"
import { getFetchResponse } from "./utils" import { getFetchResponse } from "./utils"
import { import { ExternalAppStepOutputs, DiscordStepInputs } from "@budibase/types"
AutomationActionStepId,
AutomationStepType,
AutomationIOType,
AutomationFeature,
ExternalAppStepOutputs,
DiscordStepInputs,
AutomationStepDefinition,
} from "@budibase/types"
const DEFAULT_USERNAME = "Budibase Automate" const DEFAULT_USERNAME = "Budibase Automate"
const DEFAULT_AVATAR_URL = "https://i.imgur.com/a1cmTKM.png" const DEFAULT_AVATAR_URL = "https://i.imgur.com/a1cmTKM.png"
export const definition: AutomationStepDefinition = {
name: "Discord Message",
tagline: "Send a message to a Discord server",
description: "Send a message to a Discord server",
icon: "ri-discord-line",
stepId: AutomationActionStepId.discord,
type: AutomationStepType.ACTION,
internal: false,
features: {
[AutomationFeature.LOOPING]: true,
},
inputs: {},
schema: {
inputs: {
properties: {
url: {
type: AutomationIOType.STRING,
title: "Discord Webhook URL",
},
username: {
type: AutomationIOType.STRING,
title: "Bot Name",
},
avatar_url: {
type: AutomationIOType.STRING,
title: "Bot Avatar URL",
},
content: {
type: AutomationIOType.STRING,
title: "Message",
},
},
required: ["url", "content"],
},
outputs: {
properties: {
httpStatus: {
type: AutomationIOType.NUMBER,
description: "The HTTP status code of the request",
},
response: {
type: AutomationIOType.STRING,
description: "The response from the Discord Webhook",
},
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the message sent successfully",
},
},
},
},
}
export async function run({ export async function run({
inputs, inputs,
}: { }: {

View File

@ -1,69 +1,12 @@
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"
import { import {
AutomationActionStepId, ContextEmitter,
AutomationCustomIOType,
AutomationFeature,
AutomationIOType,
AutomationStepDefinition,
AutomationStepType,
ExecuteQueryStepInputs, ExecuteQueryStepInputs,
ExecuteQueryStepOutputs, ExecuteQueryStepOutputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepDefinition = {
name: "External Data Connector",
tagline: "Execute Data Connector",
icon: "Data",
description: "Execute a query in an external data connector",
type: AutomationStepType.ACTION,
stepId: AutomationActionStepId.EXECUTE_QUERY,
internal: true,
features: {
[AutomationFeature.LOOPING]: true,
},
inputs: {},
schema: {
inputs: {
properties: {
query: {
type: AutomationIOType.OBJECT,
properties: {
queryId: {
type: AutomationIOType.STRING,
customType: AutomationCustomIOType.QUERY,
},
},
customType: AutomationCustomIOType.QUERY_PARAMS,
title: "Parameters",
required: ["queryId"],
},
},
required: ["query"],
},
outputs: {
properties: {
response: {
type: AutomationIOType.OBJECT,
description: "The response from the datasource execution",
},
info: {
type: AutomationIOType.OBJECT,
description:
"Some query types may return extra data, like headers from a REST query",
},
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
},
required: ["response", "success"],
},
},
}
export async function run({ export async function run({
inputs, inputs,
appId, appId,
@ -71,7 +14,7 @@ export async function run({
}: { }: {
inputs: ExecuteQueryStepInputs inputs: ExecuteQueryStepInputs
appId: string appId: string
emitter: EventEmitter emitter: ContextEmitter
}): Promise<ExecuteQueryStepOutputs> { }): Promise<ExecuteQueryStepOutputs> {
if (inputs.query == null) { if (inputs.query == null) {
return { return {

View File

@ -2,55 +2,10 @@ import * as scriptController from "../../api/controllers/script"
import { buildCtx } from "./utils" import { buildCtx } from "./utils"
import * as automationUtils from "../automationUtils" import * as automationUtils from "../automationUtils"
import { import {
AutomationActionStepId, ContextEmitter,
AutomationCustomIOType,
AutomationFeature,
AutomationIOType,
AutomationStepDefinition,
AutomationStepType,
ExecuteScriptStepInputs, ExecuteScriptStepInputs,
ExecuteScriptStepOutputs, ExecuteScriptStepOutputs,
} from "@budibase/types" } from "@budibase/types"
import { EventEmitter } from "events"
export const definition: AutomationStepDefinition = {
name: "JS Scripting",
tagline: "Execute JavaScript Code",
icon: "Code",
description: "Run a piece of JavaScript code in your automation",
type: AutomationStepType.ACTION,
internal: true,
stepId: AutomationActionStepId.EXECUTE_SCRIPT,
inputs: {},
features: {
[AutomationFeature.LOOPING]: true,
},
schema: {
inputs: {
properties: {
code: {
type: AutomationIOType.STRING,
customType: AutomationCustomIOType.CODE,
title: "Code",
},
},
required: ["code"],
},
outputs: {
properties: {
value: {
type: AutomationIOType.STRING,
description: "The result of the return statement",
},
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
},
required: ["success"],
},
},
}
export async function run({ export async function run({
inputs, inputs,
@ -61,7 +16,7 @@ export async function run({
inputs: ExecuteScriptStepInputs inputs: ExecuteScriptStepInputs
appId: string appId: string
context: object context: object
emitter: EventEmitter emitter: ContextEmitter
}): Promise<ExecuteScriptStepOutputs> { }): Promise<ExecuteScriptStepOutputs> {
if (inputs.code == null) { if (inputs.code == null) {
return { return {

View File

@ -1,74 +1,7 @@
import { import { FilterStepInputs, FilterStepOutputs } from "@budibase/types"
AutomationActionStepId, import { automations } from "@budibase/shared-core"
AutomationStepDefinition,
AutomationStepType,
AutomationIOType,
FilterStepInputs,
FilterStepOutputs,
} from "@budibase/types"
export const FilterConditions = { const FilterConditions = automations.steps.filter.FilterConditions
EQUAL: "EQUAL",
NOT_EQUAL: "NOT_EQUAL",
GREATER_THAN: "GREATER_THAN",
LESS_THAN: "LESS_THAN",
}
export const PrettyFilterConditions = {
[FilterConditions.EQUAL]: "Equals",
[FilterConditions.NOT_EQUAL]: "Not equals",
[FilterConditions.GREATER_THAN]: "Greater than",
[FilterConditions.LESS_THAN]: "Less than",
}
export const definition: AutomationStepDefinition = {
name: "Condition",
tagline: "{{inputs.field}} {{inputs.condition}} {{inputs.value}}",
icon: "Branch2",
description:
"Conditionally halt automations which do not meet certain conditions",
type: AutomationStepType.LOGIC,
internal: true,
features: {},
stepId: AutomationActionStepId.FILTER,
inputs: {
condition: FilterConditions.EQUAL,
},
schema: {
inputs: {
properties: {
field: {
type: AutomationIOType.STRING,
title: "Reference Value",
},
condition: {
type: AutomationIOType.STRING,
title: "Condition",
enum: Object.values(FilterConditions),
pretty: Object.values(PrettyFilterConditions),
},
value: {
type: AutomationIOType.STRING,
title: "Comparison Value",
},
},
required: ["field", "condition", "value"],
},
outputs: {
properties: {
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
result: {
type: AutomationIOType.BOOLEAN,
description: "Whether the logic block passed",
},
},
required: ["success", "result"],
},
},
}
export async function run({ export async function run({
inputs, inputs,

View File

@ -1,62 +1,6 @@
import fetch from "node-fetch" import fetch from "node-fetch"
import { getFetchResponse } from "./utils" import { getFetchResponse } from "./utils"
import { import { ExternalAppStepOutputs, MakeIntegrationInputs } from "@budibase/types"
AutomationActionStepId,
AutomationStepDefinition,
AutomationStepType,
AutomationIOType,
AutomationFeature,
ExternalAppStepOutputs,
MakeIntegrationInputs,
} from "@budibase/types"
export const definition: AutomationStepDefinition = {
name: "Make Integration",
stepTitle: "Make",
tagline: "Trigger a Make scenario",
description:
"Performs a webhook call to Make and gets the response (if configured)",
icon: "ri-shut-down-line",
stepId: AutomationActionStepId.integromat,
type: AutomationStepType.ACTION,
internal: false,
features: {
[AutomationFeature.LOOPING]: true,
},
inputs: {},
schema: {
inputs: {
properties: {
url: {
type: AutomationIOType.STRING,
title: "Webhook URL",
},
body: {
type: AutomationIOType.JSON,
title: "Payload",
},
},
required: ["url", "body"],
},
outputs: {
properties: {
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether call was successful",
},
httpStatus: {
type: AutomationIOType.NUMBER,
description: "The HTTP status code returned",
},
response: {
type: AutomationIOType.OBJECT,
description: "The webhook response - this can have properties",
},
},
required: ["success", "response"],
},
},
}
export async function run({ export async function run({
inputs, inputs,

View File

@ -1,73 +1,11 @@
import fetch, { HeadersInit } from "node-fetch" import fetch, { HeadersInit } from "node-fetch"
import { getFetchResponse } from "./utils" import { getFetchResponse } from "./utils"
import { import {
AutomationActionStepId,
AutomationStepDefinition,
AutomationStepType,
AutomationIOType,
AutomationFeature,
HttpMethod, HttpMethod,
ExternalAppStepOutputs, ExternalAppStepOutputs,
n8nStepInputs, n8nStepInputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepDefinition = {
name: "n8n Integration",
stepTitle: "n8n",
tagline: "Trigger an n8n workflow",
description:
"Performs a webhook call to n8n and gets the response (if configured)",
icon: "ri-shut-down-line",
stepId: AutomationActionStepId.n8n,
type: AutomationStepType.ACTION,
internal: false,
features: {
[AutomationFeature.LOOPING]: true,
},
inputs: {},
schema: {
inputs: {
properties: {
url: {
type: AutomationIOType.STRING,
title: "Webhook URL",
},
method: {
type: AutomationIOType.STRING,
title: "Method",
enum: Object.values(HttpMethod),
},
authorization: {
type: AutomationIOType.STRING,
title: "Authorization",
},
body: {
type: AutomationIOType.JSON,
title: "Payload",
},
},
required: ["url", "method"],
},
outputs: {
properties: {
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether call was successful",
},
httpStatus: {
type: AutomationIOType.NUMBER,
description: "The HTTP status code returned",
},
response: {
type: AutomationIOType.OBJECT,
description: "The webhook response - this can have properties",
},
},
required: ["success", "response"],
},
},
}
export async function run({ export async function run({
inputs, inputs,
}: { }: {

View File

@ -1,67 +1,10 @@
import { OpenAI } from "openai" import { OpenAI } from "openai"
import { import { OpenAIStepInputs, OpenAIStepOutputs } from "@budibase/types"
AutomationActionStepId,
AutomationStepDefinition,
AutomationStepType,
AutomationIOType,
OpenAIStepInputs,
OpenAIStepOutputs,
} 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"
import * as pro from "@budibase/pro" import * as pro from "@budibase/pro"
enum Model {
GPT_4O_MINI = "gpt-4o-mini",
GPT_4O = "gpt-4o",
GPT_4 = "gpt-4",
GPT_35_TURBO = "gpt-3.5-turbo",
}
export const definition: AutomationStepDefinition = {
name: "OpenAI",
tagline: "Send prompts to ChatGPT",
icon: "Algorithm",
description: "Interact with the OpenAI ChatGPT API.",
type: AutomationStepType.ACTION,
internal: true,
features: {},
stepId: AutomationActionStepId.OPENAI,
inputs: {
prompt: "",
},
schema: {
inputs: {
properties: {
prompt: {
type: AutomationIOType.STRING,
title: "Prompt",
},
model: {
type: AutomationIOType.STRING,
title: "Model",
enum: Object.values(Model),
},
},
required: ["prompt", "model"],
},
outputs: {
properties: {
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
response: {
type: AutomationIOType.STRING,
description: "What was output",
},
},
required: ["success", "response"],
},
},
}
/** /**
* Maintains backward compatibility with automation steps created before the introduction * Maintains backward compatibility with automation steps created before the introduction
* of custom configurations and Budibase AI * of custom configurations and Budibase AI

View File

@ -2,12 +2,6 @@ import fetch from "node-fetch"
import { getFetchResponse } from "./utils" import { getFetchResponse } from "./utils"
import * as automationUtils from "../automationUtils" import * as automationUtils from "../automationUtils"
import { import {
AutomationActionStepId,
AutomationCustomIOType,
AutomationFeature,
AutomationIOType,
AutomationStepDefinition,
AutomationStepType,
ExternalAppStepOutputs, ExternalAppStepOutputs,
OutgoingWebhookStepInputs, OutgoingWebhookStepInputs,
} from "@budibase/types" } from "@budibase/types"
@ -26,69 +20,6 @@ const BODY_REQUESTS = [RequestType.POST, RequestType.PUT, RequestType.PATCH]
* NOTE: this functionality is deprecated - it no longer should be used. * NOTE: this functionality is deprecated - it no longer should be used.
*/ */
export const definition: AutomationStepDefinition = {
deprecated: true,
name: "Outgoing webhook",
tagline: "Send a {{inputs.requestMethod}} request",
icon: "Send",
description: "Send a request of specified method to a URL",
type: AutomationStepType.ACTION,
internal: true,
features: {
[AutomationFeature.LOOPING]: true,
},
stepId: AutomationActionStepId.OUTGOING_WEBHOOK,
inputs: {
requestMethod: "POST",
url: "http://",
requestBody: "{}",
headers: "{}",
},
schema: {
inputs: {
properties: {
requestMethod: {
type: AutomationIOType.STRING,
enum: Object.values(RequestType),
title: "Request method",
},
url: {
type: AutomationIOType.STRING,
title: "URL",
},
requestBody: {
type: AutomationIOType.STRING,
title: "JSON Body",
customType: AutomationCustomIOType.WIDE,
},
headers: {
type: AutomationIOType.STRING,
title: "Headers",
customType: AutomationCustomIOType.WIDE,
},
},
required: ["requestMethod", "url"],
},
outputs: {
properties: {
response: {
type: AutomationIOType.OBJECT,
description: "The response from the webhook",
},
httpStatus: {
type: AutomationIOType.NUMBER,
description: "The HTTP status code returned",
},
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
},
required: ["response", "success"],
},
},
}
export async function run({ export async function run({
inputs, inputs,
}: { }: {

View File

@ -4,84 +4,12 @@ import { buildCtx } from "./utils"
import * as automationUtils from "../automationUtils" import * as automationUtils from "../automationUtils"
import { import {
FieldType, FieldType,
AutomationActionStepId,
AutomationCustomIOType,
AutomationFeature,
AutomationIOType,
AutomationStepDefinition,
AutomationStepType,
EmptyFilterOption, EmptyFilterOption,
SortOrder, SortOrder,
QueryRowsStepInputs, QueryRowsStepInputs,
QueryRowsStepOutputs, QueryRowsStepOutputs,
} from "@budibase/types" } from "@budibase/types"
const SortOrderPretty = {
[SortOrder.ASCENDING]: "Ascending",
[SortOrder.DESCENDING]: "Descending",
}
export const definition: AutomationStepDefinition = {
description: "Query rows from the database",
icon: "Search",
name: "Query rows",
tagline: "Query rows from {{inputs.enriched.table.name}} table",
type: AutomationStepType.ACTION,
stepId: AutomationActionStepId.QUERY_ROWS,
internal: true,
features: {
[AutomationFeature.LOOPING]: true,
},
inputs: {},
schema: {
inputs: {
properties: {
tableId: {
type: AutomationIOType.STRING,
customType: AutomationCustomIOType.TABLE,
title: "Table",
},
filters: {
type: AutomationIOType.OBJECT,
customType: AutomationCustomIOType.FILTERS,
title: "Filtering",
},
sortColumn: {
type: AutomationIOType.STRING,
title: "Sort Column",
customType: AutomationCustomIOType.COLUMN,
},
sortOrder: {
type: AutomationIOType.STRING,
title: "Sort Order",
enum: Object.values(SortOrder),
pretty: Object.values(SortOrderPretty),
},
limit: {
type: AutomationIOType.NUMBER,
title: "Limit",
customType: AutomationCustomIOType.QUERY_LIMIT,
},
},
required: ["tableId"],
},
outputs: {
properties: {
rows: {
type: AutomationIOType.ARRAY,
customType: AutomationCustomIOType.ROWS,
description: "The rows that were found",
},
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the query was successful",
},
},
required: ["rows", "success"],
},
},
}
async function getTable(appId: string, tableId: string) { async function getTable(appId: string, tableId: string) {
const ctx: any = buildCtx(appId, null, { const ctx: any = buildCtx(appId, null, {
params: { params: {

View File

@ -1,102 +1,6 @@
import { sendSmtpEmail } from "../../utilities/workerRequests" import { sendSmtpEmail } from "../../utilities/workerRequests"
import * as automationUtils from "../automationUtils" import * as automationUtils from "../automationUtils"
import { import { SmtpEmailStepInputs, BaseAutomationOutputs } from "@budibase/types"
AutomationActionStepId,
AutomationStepDefinition,
AutomationStepType,
AutomationIOType,
AutomationFeature,
AutomationCustomIOType,
SmtpEmailStepInputs,
BaseAutomationOutputs,
} from "@budibase/types"
export const definition: AutomationStepDefinition = {
description: "Send an email using SMTP",
tagline: "Send SMTP email to {{inputs.to}}",
icon: "Email",
name: "Send Email (SMTP)",
type: AutomationStepType.ACTION,
internal: true,
features: {
[AutomationFeature.LOOPING]: true,
},
stepId: AutomationActionStepId.SEND_EMAIL_SMTP,
inputs: {},
schema: {
inputs: {
properties: {
to: {
type: AutomationIOType.STRING,
title: "Send To",
},
from: {
type: AutomationIOType.STRING,
title: "Send From",
},
cc: {
type: AutomationIOType.STRING,
title: "CC",
},
bcc: {
type: AutomationIOType.STRING,
title: "BCC",
},
subject: {
type: AutomationIOType.STRING,
title: "Email Subject",
},
contents: {
type: AutomationIOType.STRING,
title: "HTML Contents",
},
addInvite: {
type: AutomationIOType.BOOLEAN,
title: "Add calendar invite",
},
startTime: {
type: AutomationIOType.DATE,
title: "Start Time",
dependsOn: "addInvite",
},
endTime: {
type: AutomationIOType.DATE,
title: "End Time",
dependsOn: "addInvite",
},
summary: {
type: AutomationIOType.STRING,
title: "Meeting Summary",
dependsOn: "addInvite",
},
location: {
type: AutomationIOType.STRING,
title: "Location",
dependsOn: "addInvite",
},
attachments: {
type: AutomationIOType.ATTACHMENT,
customType: AutomationCustomIOType.MULTI_ATTACHMENTS,
title: "Attachments",
},
},
required: ["to", "from", "subject", "contents"],
},
outputs: {
properties: {
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the email was sent",
},
response: {
type: AutomationIOType.OBJECT,
description: "A response from the email client, this may be an error",
},
},
required: ["success"],
},
},
}
export async function run({ export async function run({
inputs, inputs,

View File

@ -1,58 +1,4 @@
import { import { ServerLogStepInputs, ServerLogStepOutputs } from "@budibase/types"
AutomationActionStepId,
AutomationStepDefinition,
AutomationStepType,
AutomationIOType,
AutomationFeature,
ServerLogStepInputs,
ServerLogStepOutputs,
} from "@budibase/types"
/**
* Note, there is some functionality in this that is not currently exposed as it
* is complex and maybe better to be opinionated here.
* GET/DELETE requests cannot handle body elements so they will not be sent if configured.
*/
export const definition: AutomationStepDefinition = {
name: "Backend log",
tagline: "Console log a value in the backend",
icon: "Monitoring",
description: "Logs the given text to the server (using console.log)",
type: AutomationStepType.ACTION,
internal: true,
features: {
[AutomationFeature.LOOPING]: true,
},
stepId: AutomationActionStepId.SERVER_LOG,
inputs: {
text: "",
},
schema: {
inputs: {
properties: {
text: {
type: AutomationIOType.STRING,
title: "Log",
},
},
required: ["text"],
},
outputs: {
properties: {
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
message: {
type: AutomationIOType.STRING,
description: "What was output",
},
},
required: ["success", "message"],
},
},
}
export async function run({ export async function run({
inputs, inputs,

View File

@ -1,59 +1,6 @@
import fetch from "node-fetch" import fetch from "node-fetch"
import { getFetchResponse } from "./utils" import { getFetchResponse } from "./utils"
import { import { ExternalAppStepOutputs, SlackStepInputs } from "@budibase/types"
AutomationActionStepId,
AutomationStepDefinition,
AutomationStepType,
AutomationIOType,
AutomationFeature,
ExternalAppStepOutputs,
SlackStepInputs,
} from "@budibase/types"
export const definition: AutomationStepDefinition = {
name: "Slack Message",
tagline: "Send a message to Slack",
description: "Send a message to Slack",
icon: "ri-slack-line",
stepId: AutomationActionStepId.slack,
type: AutomationStepType.ACTION,
internal: false,
features: {
[AutomationFeature.LOOPING]: true,
},
inputs: {},
schema: {
inputs: {
properties: {
url: {
type: AutomationIOType.STRING,
title: "Incoming Webhook URL",
},
text: {
type: AutomationIOType.STRING,
title: "Message",
},
},
required: ["url", "text"],
},
outputs: {
properties: {
httpStatus: {
type: AutomationIOType.NUMBER,
description: "The HTTP status code of the request",
},
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the message sent successfully",
},
response: {
type: AutomationIOType.STRING,
description: "The response from the Slack Webhook",
},
},
},
},
}
export async function run({ export async function run({
inputs, inputs,

View File

@ -1,10 +1,5 @@
import { import {
AutomationActionStepId,
AutomationStepDefinition,
AutomationStepType,
AutomationIOType,
Automation, Automation,
AutomationCustomIOType,
TriggerAutomationStepInputs, TriggerAutomationStepInputs,
TriggerAutomationStepOutputs, TriggerAutomationStepOutputs,
} from "@budibase/types" } from "@budibase/types"
@ -13,54 +8,6 @@ import { context } from "@budibase/backend-core"
import { features } from "@budibase/pro" import { features } from "@budibase/pro"
import env from "../../environment" import env from "../../environment"
export const definition: AutomationStepDefinition = {
name: "Trigger an automation",
tagline: "Triggers an automation synchronously",
icon: "Sync",
description: "Triggers an automation synchronously",
type: AutomationStepType.ACTION,
internal: true,
features: {},
stepId: AutomationActionStepId.TRIGGER_AUTOMATION_RUN,
inputs: {},
schema: {
inputs: {
properties: {
automation: {
type: AutomationIOType.OBJECT,
properties: {
automationId: {
type: AutomationIOType.STRING,
customType: AutomationCustomIOType.AUTOMATION,
},
},
customType: AutomationCustomIOType.AUTOMATION_FIELDS,
title: "automatioFields",
required: ["automationId"],
},
timeout: {
type: AutomationIOType.NUMBER,
title: "Timeout (ms)",
},
},
required: ["automationId"],
},
outputs: {
properties: {
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the automation was successful",
},
value: {
type: AutomationIOType.OBJECT,
description: "Automation Result",
},
},
required: ["success", "value"],
},
},
}
export async function run({ export async function run({
inputs, inputs,
}: { }: {

View File

@ -1,77 +1,12 @@
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"
import { import {
AutomationActionStepId, ContextEmitter,
AutomationCustomIOType,
AutomationFeature,
AutomationIOType,
AutomationStepDefinition,
AutomationStepType,
UpdateRowStepInputs, UpdateRowStepInputs,
UpdateRowStepOutputs, UpdateRowStepOutputs,
} from "@budibase/types" } from "@budibase/types"
export const definition: AutomationStepDefinition = {
name: "Update Row",
tagline: "Update a {{inputs.enriched.table.name}} row",
icon: "Refresh",
description: "Update a row in your database",
type: AutomationStepType.ACTION,
internal: true,
features: {
[AutomationFeature.LOOPING]: true,
},
stepId: AutomationActionStepId.UPDATE_ROW,
inputs: {},
schema: {
inputs: {
properties: {
meta: {
type: AutomationIOType.OBJECT,
title: "Field settings",
},
row: {
type: AutomationIOType.OBJECT,
customType: AutomationCustomIOType.ROW,
title: "Table",
},
rowId: {
type: AutomationIOType.STRING,
title: "Row ID",
},
},
required: ["row", "rowId"],
},
outputs: {
properties: {
row: {
type: AutomationIOType.OBJECT,
customType: AutomationCustomIOType.ROW,
description: "The updated row",
},
response: {
type: AutomationIOType.OBJECT,
description: "The response from the table",
},
success: {
type: AutomationIOType.BOOLEAN,
description: "Whether the action was successful",
},
id: {
type: AutomationIOType.STRING,
description: "The identifier of the updated row",
},
revision: {
type: AutomationIOType.STRING,
description: "The revision of the updated row",
},
},
required: ["success", "id", "revision"],
},
},
}
export async function run({ export async function run({
inputs, inputs,
appId, appId,
@ -79,7 +14,7 @@ export async function run({
}: { }: {
inputs: UpdateRowStepInputs inputs: UpdateRowStepInputs
appId: string appId: string
emitter: EventEmitter emitter: ContextEmitter
}): Promise<UpdateRowStepOutputs> { }): Promise<UpdateRowStepOutputs> {
if (inputs.rowId == null || inputs.row == null) { if (inputs.rowId == null || inputs.row == null) {
return { return {

View File

@ -1,4 +1,4 @@
import { EventEmitter } from "events" import { ContextEmitter } from "@budibase/types"
export async function getFetchResponse(fetched: any) { export async function getFetchResponse(fetched: any) {
let status = fetched.status, let status = fetched.status,
@ -22,7 +22,7 @@ export async function getFetchResponse(fetched: any) {
// opts can contain, body, params and version // opts can contain, body, params and version
export function buildCtx( export function buildCtx(
appId: string, appId: string,
emitter?: EventEmitter | null, emitter?: ContextEmitter | null,
opts: any = {} opts: any = {}
) { ) {
const ctx: any = { const ctx: any = {

View File

@ -1,55 +1,6 @@
import fetch from "node-fetch" import fetch from "node-fetch"
import { getFetchResponse } from "./utils" import { getFetchResponse } from "./utils"
import { import { ZapierStepInputs, ZapierStepOutputs } from "@budibase/types"
AutomationActionStepId,
AutomationStepDefinition,
AutomationStepType,
AutomationIOType,
AutomationFeature,
ZapierStepInputs,
ZapierStepOutputs,
} from "@budibase/types"
export const definition: AutomationStepDefinition = {
name: "Zapier Webhook",
stepId: AutomationActionStepId.zapier,
type: AutomationStepType.ACTION,
internal: false,
features: {
[AutomationFeature.LOOPING]: true,
},
description: "Trigger a Zapier Zap via webhooks",
tagline: "Trigger a Zapier Zap",
icon: "ri-flashlight-line",
inputs: {},
schema: {
inputs: {
properties: {
url: {
type: AutomationIOType.STRING,
title: "Webhook URL",
},
body: {
type: AutomationIOType.JSON,
title: "Payload",
},
},
required: ["url"],
},
outputs: {
properties: {
httpStatus: {
type: AutomationIOType.NUMBER,
description: "The HTTP status code of the request",
},
response: {
type: AutomationIOType.STRING,
description: "The response from Zapier",
},
},
},
},
}
export async function run({ export async function run({
inputs, inputs,

View File

@ -1,5 +1,7 @@
import * as setup from "./utilities" import * as setup from "./utilities"
import { FilterConditions } from "../steps/filter" import { automations } from "@budibase/shared-core"
const FilterConditions = automations.steps.filter.FilterConditions
describe("test the filter logic", () => { describe("test the filter logic", () => {
const config = setup.getConfig() const config = setup.getConfig()

View File

@ -6,9 +6,11 @@ import {
DatabaseName, DatabaseName,
datasourceDescribe, datasourceDescribe,
} from "../../../integrations/tests/utils" } from "../../../integrations/tests/utils"
import { FilterConditions } from "../../../automations/steps/filter"
import { Knex } from "knex" import { Knex } from "knex"
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"
import { automations } from "@budibase/shared-core"
const FilterConditions = automations.steps.filter.FilterConditions
describe("Automation Scenarios", () => { describe("Automation Scenarios", () => {
let config = setup.getConfig() let config = setup.getConfig()

View File

@ -1,5 +1,4 @@
import { v4 as uuidv4 } from "uuid" import { v4 as uuidv4 } from "uuid"
import { testAutomation } from "../../../api/routes/tests/utilities/TestFunctions"
import { BUILTIN_ACTION_DEFINITIONS } from "../../actions" import { BUILTIN_ACTION_DEFINITIONS } from "../../actions"
import { TRIGGER_DEFINITIONS } from "../../triggers" import { TRIGGER_DEFINITIONS } from "../../triggers"
import { import {
@ -7,7 +6,6 @@ import {
AppActionTriggerOutputs, AppActionTriggerOutputs,
Automation, Automation,
AutomationActionStepId, AutomationActionStepId,
AutomationResults,
AutomationStep, AutomationStep,
AutomationStepInputs, AutomationStepInputs,
AutomationTrigger, AutomationTrigger,
@ -24,6 +22,7 @@ import {
ExecuteQueryStepInputs, ExecuteQueryStepInputs,
ExecuteScriptStepInputs, ExecuteScriptStepInputs,
FilterStepInputs, FilterStepInputs,
isDidNotTriggerResponse,
LoopStepInputs, LoopStepInputs,
OpenAIStepInputs, OpenAIStepInputs,
QueryRowsStepInputs, QueryRowsStepInputs,
@ -36,13 +35,14 @@ import {
SearchFilters, SearchFilters,
ServerLogStepInputs, ServerLogStepInputs,
SmtpEmailStepInputs, SmtpEmailStepInputs,
TestAutomationRequest,
UpdateRowStepInputs, UpdateRowStepInputs,
WebhookTriggerInputs, WebhookTriggerInputs,
WebhookTriggerOutputs, WebhookTriggerOutputs,
} from "@budibase/types" } from "@budibase/types"
import TestConfiguration from "../../../tests/utilities/TestConfiguration" import TestConfiguration from "../../../tests/utilities/TestConfiguration"
import * as setup from "../utilities" import * as setup from "../utilities"
import { definition } from "../../../automations/steps/branch" import { automations } from "@budibase/shared-core"
type TriggerOutputs = type TriggerOutputs =
| RowCreatedTriggerOutputs | RowCreatedTriggerOutputs
@ -103,7 +103,7 @@ class BaseStepBuilder {
branchStepInputs.children![branchId] = stepBuilder.build() branchStepInputs.children![branchId] = stepBuilder.build()
}) })
const branchStep: AutomationStep = { const branchStep: AutomationStep = {
...definition, ...automations.steps.branch.definition,
id: uuidv4(), id: uuidv4(),
stepId: AutomationActionStepId.BRANCH, stepId: AutomationActionStepId.BRANCH,
inputs: branchStepInputs, inputs: branchStepInputs,
@ -279,7 +279,7 @@ class StepBuilder extends BaseStepBuilder {
class AutomationBuilder extends BaseStepBuilder { class AutomationBuilder extends BaseStepBuilder {
private automationConfig: Automation private automationConfig: Automation
private config: TestConfiguration private config: TestConfiguration
private triggerOutputs: any private triggerOutputs: TriggerOutputs
private triggerSet = false private triggerSet = false
constructor( constructor(
@ -398,21 +398,19 @@ class AutomationBuilder extends BaseStepBuilder {
async run() { async run() {
const automation = await this.save() const automation = await this.save()
const results = await testAutomation( const response = await this.config.api.automation.test(
this.config, automation._id!,
automation, this.triggerOutputs as TestAutomationRequest
this.triggerOutputs
) )
return this.processResults(results)
if (isDidNotTriggerResponse(response)) {
throw new Error(response.message)
} }
private processResults(results: { response.steps.shift()
body: AutomationResults
}): AutomationResults {
results.body.steps.shift()
return { return {
trigger: results.body.trigger, trigger: response.trigger,
steps: results.body.steps, steps: response.steps,
} }
} }
} }

View File

@ -21,6 +21,7 @@ import {
AutomationRowEvent, AutomationRowEvent,
UserBindings, UserBindings,
AutomationResults, AutomationResults,
DidNotTriggerResponse,
} from "@budibase/types" } from "@budibase/types"
import { executeInThread } from "../threads/automation" import { executeInThread } from "../threads/automation"
import { dataFilters, sdk } from "@budibase/shared-core" import { dataFilters, sdk } from "@budibase/shared-core"
@ -33,14 +34,6 @@ const JOB_OPTS = {
import * as automationUtils from "../automations/automationUtils" import * as automationUtils from "../automations/automationUtils"
import { doesTableExist } from "../sdk/app/tables/getters" import { doesTableExist } from "../sdk/app/tables/getters"
type DidNotTriggerResponse = {
outputs: {
success: false
status: AutomationStatus.STOPPED
}
message: AutomationStoppedReason.TRIGGER_FILTER_NOT_MET
}
async function getAllAutomations() { async function getAllAutomations() {
const db = context.getAppDB() const db = context.getAppDB()
let automations = await db.allDocs<Automation>( let automations = await db.allDocs<Automation>(
@ -156,14 +149,26 @@ export function isAutomationResults(
) )
} }
export async function externalTrigger( interface AutomationTriggerParams {
automation: Automation,
params: {
fields: Record<string, any> fields: Record<string, any>
timeout?: number timeout?: number
appId?: string appId?: string
user?: UserBindings user?: UserBindings
}, }
export async function externalTrigger(
automation: Automation,
params: AutomationTriggerParams,
options: { getResponses: true }
): Promise<AutomationResults | DidNotTriggerResponse>
export async function externalTrigger(
automation: Automation,
params: AutomationTriggerParams,
options?: { getResponses: false }
): Promise<AutomationJob | DidNotTriggerResponse>
export async function externalTrigger(
automation: Automation,
params: AutomationTriggerParams,
{ getResponses }: { getResponses?: boolean } = {} { getResponses }: { getResponses?: boolean } = {}
): Promise<AutomationResults | DidNotTriggerResponse | AutomationJob> { ): Promise<AutomationResults | DidNotTriggerResponse | AutomationJob> {
if (automation.disabled) { if (automation.disabled) {

View File

@ -54,7 +54,6 @@ const environment = {
REDIS_URL: process.env.REDIS_URL, REDIS_URL: process.env.REDIS_URL,
REDIS_PASSWORD: process.env.REDIS_PASSWORD, REDIS_PASSWORD: process.env.REDIS_PASSWORD,
REDIS_CLUSTERED: process.env.REDIS_CLUSTERED, REDIS_CLUSTERED: process.env.REDIS_CLUSTERED,
HTTP_MIGRATIONS: process.env.HTTP_MIGRATIONS,
CLUSTER_MODE: process.env.CLUSTER_MODE, CLUSTER_MODE: process.env.CLUSTER_MODE,
API_REQ_LIMIT_PER_SEC: process.env.API_REQ_LIMIT_PER_SEC, API_REQ_LIMIT_PER_SEC: process.env.API_REQ_LIMIT_PER_SEC,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,

View File

@ -4,12 +4,17 @@ import {
JsTimeoutError, JsTimeoutError,
setJSRunner, setJSRunner,
setOnErrorLog, setOnErrorLog,
setTestingBackendJS,
} from "@budibase/string-templates" } from "@budibase/string-templates"
import { context, logging } from "@budibase/backend-core" import { context, logging } from "@budibase/backend-core"
import tracer from "dd-trace" import tracer from "dd-trace"
import { IsolatedVM } from "./vm" import { IsolatedVM } from "./vm"
export function init() { export function init() {
// enforce that if we're using isolated-VM runner then we are running backend JS
if (env.isTest()) {
setTestingBackendJS()
}
setJSRunner((js: string, ctx: Record<string, any>) => { setJSRunner((js: string, ctx: Record<string, any>) => {
return tracer.trace("runJS", {}, () => { return tracer.trace("runJS", {}, () => {
try { try {

View File

@ -1,27 +0,0 @@
import { db as dbCore } from "@budibase/backend-core"
import sdk from "../../sdk"
/**
* Date:
* January 2022
*
* Description:
* Add the url to the app metadata if it doesn't exist
*/
export const run = async (appDb: any) => {
let metadata
try {
metadata = await appDb.get(dbCore.DocumentType.APP_METADATA)
} catch (e) {
// sometimes the metadata document doesn't exist
// exit early instead of failing the migration
console.error("Error retrieving app metadata. Skipping", e)
return
}
if (!metadata.url) {
metadata.url = sdk.applications.getAppUrl({ name: metadata.name })
console.log(`Adding url to app: ${metadata.url}`)
await appDb.put(metadata)
}
}

View File

@ -1,149 +0,0 @@
import * as automations from "./app/automations"
import * as datasources from "./app/datasources"
import * as layouts from "./app/layouts"
import * as queries from "./app/queries"
import * as roles from "./app/roles"
import * as tables from "./app/tables"
import * as screens from "./app/screens"
import * as global from "./global"
import { App, AppBackfillSucceededEvent, Event } from "@budibase/types"
import { db as dbUtils, events } from "@budibase/backend-core"
import env from "../../../environment"
import { DEFAULT_TIMESTAMP } from "."
const failGraceful = env.SELF_HOSTED && !env.isDev()
const handleError = (e: any, errors?: any) => {
if (failGraceful) {
if (errors) {
errors.push(e)
}
return
}
console.trace(e)
throw e
}
const EVENTS = [
Event.AUTOMATION_CREATED,
Event.AUTOMATION_STEP_CREATED,
Event.DATASOURCE_CREATED,
Event.LAYOUT_CREATED,
Event.QUERY_CREATED,
Event.ROLE_CREATED,
Event.SCREEN_CREATED,
Event.TABLE_CREATED,
Event.VIEW_CREATED,
Event.VIEW_CALCULATION_CREATED,
Event.VIEW_FILTER_CREATED,
Event.APP_PUBLISHED,
Event.APP_CREATED,
]
/**
* Date:
* May 2022
*
* Description:
* Backfill app events.
*/
export const run = async (appDb: any) => {
try {
if (await global.isComplete()) {
// make sure new apps aren't backfilled
// return if the global migration for this tenant is complete
// which runs after the app migrations
return
}
// tell the event pipeline to start caching
// events for this tenant
await events.backfillCache.start(EVENTS)
let timestamp: string | number = DEFAULT_TIMESTAMP
const app: App = await appDb.get(dbUtils.DocumentType.APP_METADATA)
if (app.createdAt) {
timestamp = app.createdAt as string
}
if (dbUtils.isProdAppID(app.appId)) {
await events.app.published(app, timestamp)
}
const totals: any = {}
const errors: any = []
if (dbUtils.isDevAppID(app.appId)) {
await events.app.created(app, timestamp)
try {
totals.automations = await automations.backfill(appDb, timestamp)
} catch (e) {
handleError(e, errors)
}
try {
totals.datasources = await datasources.backfill(appDb, timestamp)
} catch (e) {
handleError(e, errors)
}
try {
totals.layouts = await layouts.backfill(appDb, timestamp)
} catch (e) {
handleError(e, errors)
}
try {
totals.queries = await queries.backfill(appDb, timestamp)
} catch (e) {
handleError(e, errors)
}
try {
totals.roles = await roles.backfill(appDb, timestamp)
} catch (e) {
handleError(e, errors)
}
try {
totals.screens = await screens.backfill(appDb, timestamp)
} catch (e) {
handleError(e, errors)
}
try {
totals.tables = await tables.backfill(appDb, timestamp)
} catch (e) {
handleError(e, errors)
}
}
const properties: AppBackfillSucceededEvent = {
appId: app.appId,
automations: totals.automations,
datasources: totals.datasources,
layouts: totals.layouts,
queries: totals.queries,
roles: totals.roles,
tables: totals.tables,
screens: totals.screens,
}
if (errors.length) {
properties.errors = errors.map((e: any) =>
JSON.stringify(e, Object.getOwnPropertyNames(e))
)
properties.errorCount = errors.length
} else {
properties.errorCount = 0
}
await events.backfill.appSucceeded(properties)
// tell the event pipeline to stop caching events for this tenant
await events.backfillCache.end()
} catch (e) {
handleError(e)
await events.backfill.appFailed(e)
}
}

View File

@ -1,26 +0,0 @@
import { events } from "@budibase/backend-core"
import { getAutomationParams } from "../../../../db/utils"
import { Automation } from "@budibase/types"
const getAutomations = async (appDb: any): Promise<Automation[]> => {
const response = await appDb.allDocs(
getAutomationParams(null, {
include_docs: true,
})
)
return response.rows.map((row: any) => row.doc)
}
export const backfill = async (appDb: any, timestamp: string | number) => {
const automations = await getAutomations(appDb)
for (const automation of automations) {
await events.automation.created(automation, timestamp)
for (const step of automation.definition.steps) {
await events.automation.stepCreated(automation, step, timestamp)
}
}
return automations.length
}

View File

@ -1,22 +0,0 @@
import { events } from "@budibase/backend-core"
import { getDatasourceParams } from "../../../../db/utils"
import { Datasource } from "@budibase/types"
const getDatasources = async (appDb: any): Promise<Datasource[]> => {
const response = await appDb.allDocs(
getDatasourceParams(null, {
include_docs: true,
})
)
return response.rows.map((row: any) => row.doc)
}
export const backfill = async (appDb: any, timestamp: string | number) => {
const datasources: Datasource[] = await getDatasources(appDb)
for (const datasource of datasources) {
await events.datasource.created(datasource, timestamp)
}
return datasources.length
}

View File

@ -1,29 +0,0 @@
import { events } from "@budibase/backend-core"
import { getLayoutParams } from "../../../../db/utils"
import { Layout } from "@budibase/types"
const getLayouts = async (appDb: any): Promise<Layout[]> => {
const response = await appDb.allDocs(
getLayoutParams(null, {
include_docs: true,
})
)
return response.rows.map((row: any) => row.doc)
}
export const backfill = async (appDb: any, timestamp: string | number) => {
const layouts: Layout[] = await getLayouts(appDb)
for (const layout of layouts) {
// exclude default layouts
if (
layout._id === "layout_private_master" ||
layout._id === "layout_public_master"
) {
continue
}
await events.layout.created(layout, timestamp)
}
return layouts.length
}

View File

@ -1,47 +0,0 @@
import { events } from "@budibase/backend-core"
import { getQueryParams } from "../../../../db/utils"
import { Query, Datasource, SourceName } from "@budibase/types"
const getQueries = async (appDb: any): Promise<Query[]> => {
const response = await appDb.allDocs(
getQueryParams(null, {
include_docs: true,
})
)
return response.rows.map((row: any) => row.doc)
}
const getDatasource = async (
appDb: any,
datasourceId: string
): Promise<Datasource> => {
return appDb.get(datasourceId)
}
export const backfill = async (appDb: any, timestamp: string | number) => {
const queries: Query[] = await getQueries(appDb)
for (const query of queries) {
let datasource: Datasource
try {
datasource = await getDatasource(appDb, query.datasourceId)
} catch (e: any) {
// handle known bug where a datasource has been deleted
// and the query has not
if (e.status === 404) {
datasource = {
type: "unknown",
_id: query.datasourceId,
source: "unknown" as SourceName,
}
} else {
throw e
}
}
await events.query.created(datasource, query, timestamp)
}
return queries.length
}

View File

@ -1,22 +0,0 @@
import { events } from "@budibase/backend-core"
import { getRoleParams } from "../../../../db/utils"
import { Role } from "@budibase/types"
const getRoles = async (appDb: any): Promise<Role[]> => {
const response = await appDb.allDocs(
getRoleParams(null, {
include_docs: true,
})
)
return response.rows.map((row: any) => row.doc)
}
export const backfill = async (appDb: any, timestamp: string | number) => {
const roles = await getRoles(appDb)
for (const role of roles) {
await events.role.created(role, timestamp)
}
return roles.length
}

View File

@ -1,22 +0,0 @@
import { events } from "@budibase/backend-core"
import { getScreenParams } from "../../../../db/utils"
import { Screen } from "@budibase/types"
const getScreens = async (appDb: any): Promise<Screen[]> => {
const response = await appDb.allDocs(
getScreenParams(null, {
include_docs: true,
})
)
return response.rows.map((row: any) => row.doc)
}
export const backfill = async (appDb: any, timestamp: string | number) => {
const screens = await getScreens(appDb)
for (const screen of screens) {
await events.screen.created(screen, timestamp)
}
return screens.length
}

Some files were not shown because too many files have changed in this diff Show More